restructure code, internalize devices
This commit is contained in:
parent
5b2271f005
commit
02feb524e6
110 changed files with 592 additions and 681 deletions
84
src/moto/ajax.js
Normal file
84
src/moto/ajax.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (self.moto.Ajax) return;
|
||||
|
||||
const MOTO = self.moto;
|
||||
|
||||
MOTO.Ajax = Ajax;
|
||||
|
||||
MOTO.callAjax = function(url, handler) {
|
||||
new Ajax(handler).request(url);
|
||||
};
|
||||
|
||||
const AP = Ajax.prototype,
|
||||
KV = moto.KV,
|
||||
KEY = "moto-ajax",
|
||||
TIME = function() { return new Date().getTime() },
|
||||
MOKEY = moto.id = KV.getItem(KEY) || (TIME().toString(36)+rnd()+rnd());
|
||||
|
||||
MOTO.restore = function(id) {
|
||||
MOKEY = moto.id = id;
|
||||
KV.setItem(KEY, MOKEY);
|
||||
}
|
||||
|
||||
KV.setItem(KEY, MOKEY);
|
||||
|
||||
const STATES = [
|
||||
"request not initialized", // 0
|
||||
"server connection established", // 1
|
||||
"request recieved", // 2
|
||||
"processing request", // 3
|
||||
"request complete" // 4
|
||||
];
|
||||
|
||||
function Ajax(callback, responseType) {
|
||||
this.ajax = new XMLHttpRequest();
|
||||
this.ajax.onreadystatechange = this.onStateChange.bind(this);
|
||||
this.ajax.withCredentials = true;
|
||||
this.state = STATES[0];
|
||||
this.callback = callback;
|
||||
this.responseType = responseType;
|
||||
}
|
||||
|
||||
function rnd() {
|
||||
return Math.round(Math.random()*0xffffffff).toString(36);
|
||||
}
|
||||
|
||||
AP.onStateChange = function() {
|
||||
this.state = STATES[this.ajax.readyState];
|
||||
if (this.ajax.readyState === 4 && this.callback) {
|
||||
let status = this.ajax.status;
|
||||
if (status >= 200 && status < 300) {
|
||||
this.callback(this.ajax.responseType ? this.ajax.response : this.ajax.responseText, this.ajax);
|
||||
} else {
|
||||
this.callback(null, this.ajax);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} url
|
||||
* @param {Object} [send]
|
||||
* @param {Object} [headers]
|
||||
*/
|
||||
AP.request = function(url, send, headers) {
|
||||
this.ajax.open(send ? "POST" : "GET", url, true);
|
||||
if (this.responseType) this.ajax.responseType = this.responseType;
|
||||
headers = headers || {};
|
||||
headers["X-Moto-Ajax"] = MOKEY;
|
||||
for (let k in headers) {
|
||||
this.ajax.setRequestHeader(k, headers[k]);
|
||||
}
|
||||
if (send) {
|
||||
this.ajax.send(send);
|
||||
} else {
|
||||
this.ajax.send();
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
599
src/moto/ctrl.js
Normal file
599
src/moto/ctrl.js
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
let MOTO = self.moto = self.moto || {};
|
||||
|
||||
/**
|
||||
* Adapted from THREE.OrbitControls
|
||||
*/
|
||||
|
||||
MOTO.CTRL = function (object, domElement, notify, slider) {
|
||||
|
||||
this.object = object;
|
||||
this.domElement = ( domElement !== undefined ) ? domElement : document;
|
||||
|
||||
// Set to false to disable this control
|
||||
this.enabled = true;
|
||||
|
||||
// "target" sets the location of focus, where the control orbits around
|
||||
// and where it pans with respect to.
|
||||
this.target = new THREE.Vector3();
|
||||
|
||||
// center is old, deprecated; use "target" instead
|
||||
this.center = this.target;
|
||||
|
||||
// This option actually enables dollying in and out; left as "zoom" for
|
||||
// backwards compatibility
|
||||
this.noZoom = false;
|
||||
this.zoomSpeed = 1.0;
|
||||
this.reverseZoom = false;
|
||||
|
||||
// Limits to how far you can dolly in and out
|
||||
this.minDistance = 0;
|
||||
this.maxDistance = Infinity;
|
||||
|
||||
// Set to true to disable this control
|
||||
this.noRotate = false;
|
||||
this.rotateSpeed = 1.0;
|
||||
|
||||
// Set to true to disable this control
|
||||
this.noPan = false;
|
||||
this.keyPanSpeed = 7.0; // pixels moved per arrow key push
|
||||
|
||||
// How far you can orbit vertically, upper and lower limits.
|
||||
// Range is 0 to Math.PI radians.
|
||||
this.minPolarAngle = 0; // radians
|
||||
this.maxPolarAngle = Math.PI; // radians
|
||||
|
||||
// How far you can orbit horizontally, upper and lower limits.
|
||||
// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
|
||||
this.minAzimuthAngle = -Infinity; // radians
|
||||
this.maxAzimuthAngle = Infinity; // radians
|
||||
|
||||
// Set to true to disable use of the keys
|
||||
this.noKeys = false;
|
||||
|
||||
// The four arrow keys
|
||||
this.keys = {
|
||||
UP: 38,
|
||||
LEFT: 37,
|
||||
RIGHT: 39,
|
||||
BOTTOM: 40
|
||||
};
|
||||
|
||||
// Mouse buttons
|
||||
this.mouseButtons = {
|
||||
ORBIT: THREE.MOUSE.LEFT,
|
||||
ZOOM: THREE.MOUSE.MIDDLE,
|
||||
PAN: THREE.MOUSE.RIGHT
|
||||
};
|
||||
|
||||
let scope = this,
|
||||
domEl = scope.domElement,
|
||||
EPS = 0.000001,
|
||||
rotateStart = new THREE.Vector2(),
|
||||
rotateEnd = new THREE.Vector2(),
|
||||
rotateDelta = new THREE.Vector2(),
|
||||
panStart = new THREE.Vector2(),
|
||||
panEnd = new THREE.Vector2(),
|
||||
panDelta = new THREE.Vector2(),
|
||||
panOffset = new THREE.Vector3(),
|
||||
offset = new THREE.Vector3(),
|
||||
dollyStart = new THREE.Vector2(),
|
||||
dollyEnd = new THREE.Vector2(),
|
||||
dollyDelta = new THREE.Vector2(),
|
||||
theta,
|
||||
thetaDelta = 0,
|
||||
thetaSet = null,
|
||||
phi,
|
||||
phiDelta = 0,
|
||||
phiSet = null,
|
||||
scale = 1,
|
||||
scaleSave = 1,
|
||||
pan = new THREE.Vector3(),
|
||||
lastPosition = new THREE.Vector3(),
|
||||
lastQuaternion = new THREE.Quaternion(),
|
||||
// so camera.up is the orbit axis
|
||||
quat = new THREE.Quaternion().setFromUnitVectors(object.up, new THREE.Vector3(0, 1, 0)),
|
||||
quatInverse = quat.clone().inverse(),
|
||||
// touch tracking
|
||||
firstTouch = [],
|
||||
touchSlide = false,
|
||||
lastDY = 0,
|
||||
// events
|
||||
changeEvent = { type: 'change'},
|
||||
startEvent = { type: 'start'},
|
||||
endEvent = { type: 'end'};
|
||||
|
||||
let STATE = { NONE: -1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5},
|
||||
state = STATE.NONE,
|
||||
MODE = { PERSPECTIVE: 1, ORTHOGRAPHIC: 2, UNKNOWN: 3},
|
||||
mode = isValue(object.fov) ? MODE.PERSPECTIVE : isValue(object.top) ? MODE.ORTHOGRAPHIC : MODE.UNKNOWN;
|
||||
|
||||
// for reset
|
||||
this.target0 = this.target.clone();
|
||||
this.position0 = this.object.position.clone();
|
||||
|
||||
this.rotateLeft = function (angle) {
|
||||
thetaDelta -= angle;
|
||||
};
|
||||
|
||||
this.rotateUp = function (angle) {
|
||||
phiDelta -= angle;
|
||||
};
|
||||
|
||||
// pass in distance in world space to move left
|
||||
this.panLeft = function (distance) {
|
||||
let te = this.object.matrix.elements;
|
||||
|
||||
// get X column of matrix
|
||||
panOffset.set(te[ 0 ], te[ 1 ], te[ 2 ]);
|
||||
panOffset.multiplyScalar(-distance);
|
||||
pan.add(panOffset);
|
||||
};
|
||||
|
||||
// pass in distance in world space to move up
|
||||
this.panUp = function (distance) {
|
||||
let te = this.object.matrix.elements;
|
||||
|
||||
// get Y column of matrix
|
||||
panOffset.set(te[ 4 ], te[ 5 ], te[ 6 ]);
|
||||
panOffset.multiplyScalar(distance);
|
||||
pan.add(panOffset);
|
||||
};
|
||||
|
||||
// pass in x,y of change desired in pixel space,
|
||||
// right and down are positive
|
||||
this.pan = function (deltaX, deltaY) {
|
||||
let element = scope.domElement === document ? scope.domElement.body : scope.domElement;
|
||||
|
||||
switch (mode) {
|
||||
case MODE.PERSPECTIVE:
|
||||
let position = scope.object.position;
|
||||
let offset = position.clone().sub(scope.target);
|
||||
let targetDistance = offset.length();
|
||||
|
||||
// half of the fov is center to top of screen
|
||||
targetDistance *= Math.tan(( scope.object.fov / 2 ) * Math.PI / 180.0);
|
||||
|
||||
// we actually don't use screenWidth, since perspective camera is fixed to screen height
|
||||
scope.panLeft(2 * deltaX * targetDistance / element.clientHeight);
|
||||
scope.panUp(2 * deltaY * targetDistance / element.clientHeight);
|
||||
break;
|
||||
case MODE.ORTHOGRAPHIC:
|
||||
scope.panLeft(deltaX * (scope.object.right - scope.object.left) / element.clientWidth);
|
||||
scope.panUp(deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function isValue(v) {
|
||||
return v !== null && v !== undefined;
|
||||
}
|
||||
|
||||
function firstValue(choices) {
|
||||
for (let i=0; i<choices.length; i++) {
|
||||
let v = choices[i];
|
||||
if (isValue(v)) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
this.setZoom = function(reverse, speed) {
|
||||
scope.reverseZoom = reverse;
|
||||
scope.zoomSpeed = speed || 1.0;
|
||||
};
|
||||
|
||||
this.setPosition = function(set) {
|
||||
thetaSet = firstValue([set.left, set.theta, thetaSet]);
|
||||
phiSet = firstValue([set.up, set.phi, phiSet]);
|
||||
if (set.panX) this.target.x = set.panX;
|
||||
if (set.panY) this.target.y = set.panY;
|
||||
if (set.panZ) this.target.z = set.panZ;
|
||||
if (set.scale) scale = set.scale;
|
||||
};
|
||||
|
||||
this.getPosition = function(scaled) {
|
||||
let t = this.target,
|
||||
pos = { left:theta, up:phi, panX:t.x, panY:t.y, panZ:t.z, scale:scaled ? scaleSave : 1 };
|
||||
return pos;
|
||||
};
|
||||
|
||||
this.dollyIn = function (dollyScale) {
|
||||
if (dollyScale === undefined) {
|
||||
dollyScale = getZoomScale();
|
||||
}
|
||||
scale *= dollyScale;
|
||||
};
|
||||
|
||||
this.dollyOut = function (dollyScale) {
|
||||
if (dollyScale === undefined) {
|
||||
dollyScale = getZoomScale();
|
||||
}
|
||||
scale /= dollyScale;
|
||||
};
|
||||
|
||||
this.update = function () {
|
||||
let position = this.object.position;
|
||||
|
||||
offset.copy(position).sub(this.target);
|
||||
|
||||
// rotate offset to "y-axis-is-up" space
|
||||
offset.applyQuaternion(quat);
|
||||
|
||||
// angle from z-axis around y-axis
|
||||
theta = isValue(thetaSet) ? thetaSet : Math.atan2(offset.x, offset.z);
|
||||
|
||||
// angle from y-axis
|
||||
phi = isValue(phiSet) ? phiSet : Math.atan2(Math.sqrt(offset.x * offset.x + offset.z * offset.z), offset.y);
|
||||
|
||||
theta += thetaDelta;
|
||||
phi += phiDelta;
|
||||
|
||||
// restrict theta to be between desired limits
|
||||
theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, theta));
|
||||
|
||||
// restrict phi to be between desired limits
|
||||
phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, phi));
|
||||
|
||||
// restrict phi to be betwee EPS and PI-EPS
|
||||
phi = Math.max(EPS, Math.min(Math.PI - EPS, phi));
|
||||
|
||||
let radius = offset.length() * (mode === MODE.PERSPECTIVE ? scale : 1);
|
||||
|
||||
// restrict radius to be between desired limits
|
||||
radius = Math.max(this.minDistance, Math.min(this.maxDistance, radius));
|
||||
|
||||
// move target to panned location
|
||||
this.target.add(pan);
|
||||
|
||||
offset.x = radius * Math.sin(phi) * Math.sin(theta);
|
||||
offset.y = radius * Math.cos(phi);
|
||||
offset.z = radius * Math.sin(phi) * Math.cos(theta);
|
||||
|
||||
// rotate offset back to "camera-up-vector-is-up" space
|
||||
offset.applyQuaternion(quatInverse);
|
||||
|
||||
position.copy(this.target).add(offset);
|
||||
this.object.lookAt(this.target);
|
||||
|
||||
thetaDelta = 0;
|
||||
thetaSet = null;
|
||||
phiDelta = 0;
|
||||
phiSet = null;
|
||||
pan.set(0, 0, 0);
|
||||
|
||||
scaleSave *= scale;
|
||||
|
||||
if (mode === MODE.ORTHOGRAPHIC) {
|
||||
scope.object.zoom = 1/scale;
|
||||
scope.object.updateProjectionMatrix();
|
||||
} else {
|
||||
scale = 1;
|
||||
}
|
||||
|
||||
// update condition is:
|
||||
// min(camera displacement, camera rotation in radians)^2 > EPS
|
||||
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
|
||||
if (lastPosition.distanceToSquared(this.object.position) > EPS
|
||||
|| 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS) {
|
||||
|
||||
this.dispatchEvent(changeEvent);
|
||||
lastPosition.copy(this.object.position);
|
||||
lastQuaternion.copy(this.object.quaternion);
|
||||
if (notify) notify(position, true);
|
||||
} else {
|
||||
if (notify) notify(position, false);
|
||||
}
|
||||
};
|
||||
|
||||
this.reset = function () {
|
||||
state = STATE.NONE;
|
||||
scale = 1;
|
||||
scaleSave = 1;
|
||||
this.target.copy(this.target0);
|
||||
this.object.position.copy(this.position0);
|
||||
};
|
||||
|
||||
this.getPolarAngle = function () {
|
||||
return phi;
|
||||
};
|
||||
|
||||
this.getAzimuthalAngle = function () {
|
||||
return theta
|
||||
};
|
||||
|
||||
function getZoomScale() {
|
||||
return Math.pow(0.95, scope.zoomSpeed);
|
||||
}
|
||||
|
||||
function onMouseDown(event) {
|
||||
if (scope.enabled === false) return;
|
||||
event.preventDefault();
|
||||
|
||||
switch (event.button) {
|
||||
case scope.mouseButtons.ORBIT:
|
||||
state = event.metaKey ? STATE.PAN : STATE.ROTATE;
|
||||
break;
|
||||
case scope.mouseButtons.ZOOM:
|
||||
state = STATE.DOLLY;
|
||||
break;
|
||||
case scope.mouseButtons.PAN:
|
||||
state = STATE.PAN;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case STATE.ROTATE:
|
||||
if (scope.noRotate === true) return state = STATE.NONE;
|
||||
rotateStart.set(event.clientX, event.clientY);
|
||||
break;
|
||||
case STATE.DOLLY:
|
||||
if (scope.noZoom === true) return state = STATE.NONE;
|
||||
dollyStart.set(event.clientX, event.clientY);
|
||||
break;
|
||||
case STATE.PAN:
|
||||
if (scope.noPan === true) return state = STATE.NONE;
|
||||
panStart.set(event.clientX, event.clientY);
|
||||
break;
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove, false);
|
||||
document.addEventListener('mouseup', onMouseUp, false);
|
||||
scope.dispatchEvent(startEvent);
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
if (scope.enabled === false) return;
|
||||
event.preventDefault();
|
||||
|
||||
let element = scope.domElement === document ? scope.domElement.body : scope.domElement;
|
||||
|
||||
if (state === STATE.ROTATE) {
|
||||
if (scope.noRotate === true) return;
|
||||
|
||||
rotateEnd.set(event.clientX, event.clientY);
|
||||
rotateDelta.subVectors(rotateEnd, rotateStart);
|
||||
|
||||
// rotating across whole screen goes 360 degrees around
|
||||
scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed);
|
||||
// rotating up and down along whole screen attempts to go 360, but limited to 180
|
||||
scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed);
|
||||
|
||||
rotateStart.copy(rotateEnd);
|
||||
|
||||
} else if (state === STATE.DOLLY) {
|
||||
if (scope.noZoom === true) return;
|
||||
|
||||
dollyEnd.set(event.clientX, event.clientY);
|
||||
dollyDelta.subVectors(dollyEnd, dollyStart);
|
||||
|
||||
if (dollyDelta.y > 0) {
|
||||
scope.dollyIn();
|
||||
} else {
|
||||
scope.dollyOut();
|
||||
}
|
||||
|
||||
dollyStart.copy(dollyEnd);
|
||||
|
||||
} else if (state === STATE.PAN) {
|
||||
if (scope.noPan === true) return;
|
||||
|
||||
panEnd.set(event.clientX, event.clientY);
|
||||
panDelta.subVectors(panEnd, panStart);
|
||||
scope.pan(panDelta.x, panDelta.y);
|
||||
panStart.copy(panEnd);
|
||||
}
|
||||
|
||||
scope.update();
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
if (scope.enabled === false) return;
|
||||
|
||||
document.removeEventListener('mousemove', onMouseMove, false);
|
||||
document.removeEventListener('mouseup', onMouseUp, false);
|
||||
scope.dispatchEvent(endEvent);
|
||||
state = STATE.NONE;
|
||||
}
|
||||
|
||||
function onMouseWheel(event) {
|
||||
if (scope.enabled === false || scope.noZoom === true) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
if (event.shiftKey && slider) {
|
||||
slider(event.deltaX || event.wheelDelta || event.detail);
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = -(event.deltaY || event.wheelDelta || event.detail || 0);
|
||||
|
||||
if (delta === 0) return;
|
||||
|
||||
if (scope.reverseZoom) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
let saveZoomSpeed = scope.zoomSpeed;
|
||||
if (event.ctrlKey) scope.zoomSpeed *= 2;
|
||||
if (event.metaKey) scope.zoomSpeed *= 2;
|
||||
|
||||
if (delta > 0) {
|
||||
scope.dollyOut();
|
||||
} else if (delta < 0) {
|
||||
scope.dollyIn();
|
||||
}
|
||||
|
||||
scope.zoomSpeed = saveZoomSpeed;
|
||||
|
||||
scope.update();
|
||||
scope.dispatchEvent(startEvent);
|
||||
scope.dispatchEvent(endEvent);
|
||||
}
|
||||
|
||||
function onKeyDown(event) {
|
||||
if (scope.enabled === false || scope.noKeys === true || scope.noPan === true) return;
|
||||
|
||||
switch (event.keyCode) {
|
||||
|
||||
case scope.keys.UP:
|
||||
scope.pan(0, scope.keyPanSpeed);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case scope.keys.BOTTOM:
|
||||
scope.pan(0, -scope.keyPanSpeed);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case scope.keys.LEFT:
|
||||
scope.pan(scope.keyPanSpeed, 0);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case scope.keys.RIGHT:
|
||||
scope.pan(-scope.keyPanSpeed, 0);
|
||||
scope.update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function touchstart(event) {
|
||||
if (scope.enabled === false) return;
|
||||
|
||||
switch (event.touches.length) {
|
||||
case 1: // one-fingered touch: rotate
|
||||
if (scope.noRotate === true) return;
|
||||
|
||||
state = STATE.TOUCH_ROTATE;
|
||||
|
||||
rotateStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY);
|
||||
break;
|
||||
|
||||
case 2: // two-fingered touch: dolly
|
||||
if (scope.noZoom === true) return;
|
||||
|
||||
state = STATE.TOUCH_DOLLY;
|
||||
|
||||
let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
firstTouch = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} });
|
||||
touchSlide = false;
|
||||
lastDY = 0;
|
||||
|
||||
dollyStart.set(0, distance);
|
||||
break;
|
||||
|
||||
case 3: // three-fingered touch: pan
|
||||
if (scope.noPan === true) return;
|
||||
|
||||
state = STATE.TOUCH_PAN;
|
||||
|
||||
panStart.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY);
|
||||
break;
|
||||
|
||||
default:
|
||||
state = STATE.NONE;
|
||||
}
|
||||
scope.dispatchEvent(startEvent);
|
||||
}
|
||||
|
||||
function touchmove(event) {
|
||||
if (scope.enabled === false) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
let element = scope.domElement === document ? scope.domElement.body : scope.domElement;
|
||||
|
||||
switch (event.touches.length) {
|
||||
case 1: // one-fingered touch: rotate
|
||||
if (scope.noRotate === true) return;
|
||||
if (state !== STATE.TOUCH_ROTATE) return;
|
||||
|
||||
rotateEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY);
|
||||
rotateDelta.subVectors(rotateEnd, rotateStart);
|
||||
|
||||
// rotating across whole screen goes 360 degrees around
|
||||
scope.rotateLeft(2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed);
|
||||
// rotating up and down along whole screen attempts to go 360, but limited to 180
|
||||
scope.rotateUp(2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed);
|
||||
|
||||
rotateStart.copy(rotateEnd);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case 2: // two-fingered touch: dolly
|
||||
if (scope.noZoom === true) return;
|
||||
if (state !== STATE.TOUCH_DOLLY) return;
|
||||
let dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
|
||||
let dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
|
||||
let distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
let touches = [...event.touches].map(v => { return {x:v.pageX, y:v.pageY} });
|
||||
let ddx = ( (touches[0].x - firstTouch[0].x) + (touches[1].x - firstTouch[1].x) ) / 2;
|
||||
let ddy = ( (touches[0].y - firstTouch[0].y) + (touches[1].y - firstTouch[1].y) ) / 2;
|
||||
|
||||
if (scope.reverseZoom) distance = -distance;
|
||||
|
||||
dollyEnd.set(0, distance);
|
||||
dollyDelta.subVectors(dollyEnd, dollyStart);
|
||||
|
||||
if (touchSlide || (Math.abs(ddy/ddx) > 10 && slider)) {
|
||||
touchSlide = true;
|
||||
slider(lastDY ? lastDY - ddy : ddy);
|
||||
lastDY = ddy;
|
||||
} else {
|
||||
if (dollyDelta.y > 0) {
|
||||
scope.dollyOut();
|
||||
} else {
|
||||
scope.dollyIn();
|
||||
}
|
||||
}
|
||||
|
||||
dollyStart.copy(dollyEnd);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case 3: // three-fingered touch: pan
|
||||
if (scope.noPan === true) return;
|
||||
if (state !== STATE.TOUCH_PAN) return;
|
||||
|
||||
panEnd.set(event.touches[ 0 ].pageX, event.touches[ 0 ].pageY);
|
||||
panDelta.subVectors(panEnd, panStart);
|
||||
scope.pan(panDelta.x, panDelta.y);
|
||||
panStart.copy(panEnd);
|
||||
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
default:
|
||||
state = STATE.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
function touchend(/* event */) {
|
||||
if (scope.enabled === false) return;
|
||||
scope.dispatchEvent(endEvent);
|
||||
state = STATE.NONE;
|
||||
}
|
||||
|
||||
this.onMouseUp = onMouseUp;
|
||||
|
||||
domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false);
|
||||
domEl.addEventListener('mousedown', onMouseDown, false);
|
||||
domEl.addEventListener('mousewheel', onMouseWheel, false);
|
||||
domEl.addEventListener('DOMMouseScroll', onMouseWheel, false); // firefox
|
||||
domEl.addEventListener('touchstart', touchstart, false);
|
||||
domEl.addEventListener('touchend', touchend, false);
|
||||
domEl.addEventListener('touchmove', touchmove, false);
|
||||
|
||||
window.addEventListener('keydown', onKeyDown, false);
|
||||
};
|
||||
|
||||
MOTO.CTRL.prototype = Object.create(THREE.EventDispatcher.prototype);
|
||||
214
src/moto/db.js
Normal file
214
src/moto/db.js
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (self.moto.Storage) return;
|
||||
|
||||
self.moto.Storage = Storage;
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
|
||||
|
||||
let IDB = self.indexedDB || self.mozIndexedDB || self.webkitIndexedDB || self.msIndexedDB,
|
||||
IRR = self.IDBKeyRange,
|
||||
local = null;
|
||||
|
||||
/**
|
||||
* @param {String} dbname
|
||||
* @param {number} [version]
|
||||
* @constructor
|
||||
*/
|
||||
function Storage(dbname, version) {
|
||||
this.db = null;
|
||||
this.name = dbname;
|
||||
this.version = version || 1;
|
||||
this.queue = [];
|
||||
this.initCalled = false;
|
||||
}
|
||||
|
||||
Storage.delete = function(dbname) {
|
||||
IDB.deleteDatabase(dbname);
|
||||
};
|
||||
|
||||
let SP = Storage.prototype;
|
||||
|
||||
/** ******************************************************************
|
||||
* indexedDB implementation
|
||||
******************************************************************* */
|
||||
|
||||
SP.keys = function(callback, lower, upper) {
|
||||
let out = [];
|
||||
this.iterate(function(k,v) {
|
||||
if (k === null) {
|
||||
callback(out);
|
||||
} else {
|
||||
out.push(k);
|
||||
}
|
||||
}, lower, upper, true);
|
||||
};
|
||||
|
||||
SP.iterate = function(callback, lower, upper, nullterm) {
|
||||
if (!this.db) {
|
||||
this.init();
|
||||
return this.queue.push(["iterate", callback, lower, upper]);
|
||||
}
|
||||
let range = lower && upper ? IRR.bound(lower,upper) :
|
||||
lower ? IRR.lowerBound(lower) :
|
||||
upper ? IRR.upperBound(upper) : undefined;
|
||||
// iterate over all db values for debugging
|
||||
this.db.transaction(this.name).objectStore(this.name).openCursor(range).onsuccess = function(event) {
|
||||
let cursor = event.target.result;
|
||||
if (cursor) {
|
||||
callback(cursor.key,cursor.value);
|
||||
cursor.continue();
|
||||
} else if (nullterm) {
|
||||
callback(null);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
SP.deleteStore = function() {
|
||||
if (this.initCalled) throw "cannot delete ObjectStore after init() called";
|
||||
this.version = Number.MAX_SAFE_INTEGER || Number.MAX_VALUE;
|
||||
return this.init(true);
|
||||
};
|
||||
|
||||
SP.init = function(deleteOS) {
|
||||
if (this.initCalled) return;
|
||||
|
||||
let storage = this,
|
||||
name = this.name,
|
||||
request = null;
|
||||
|
||||
function fallback() {
|
||||
console.log("in private browsing mode or browser lacks support for IndexedDB. unable to setup storage for '" + name + "'.");
|
||||
local = {};
|
||||
storage.runQueue();
|
||||
}
|
||||
|
||||
try {
|
||||
request = IDB.open(name, this.version);
|
||||
|
||||
request.onupgradeneeded = function(event) {
|
||||
if (deleteOS) {
|
||||
storage.db.deleteObjectStore(name);
|
||||
return;
|
||||
}
|
||||
storage.db = request.result;
|
||||
storage.store = storage.db.createObjectStore(name);
|
||||
event.target.transaction.oncomplete = function(event) {
|
||||
storage.runQueue();
|
||||
};
|
||||
};
|
||||
|
||||
request.onsuccess = function(event) {
|
||||
storage.db = request.result;
|
||||
storage.runQueue();
|
||||
};
|
||||
|
||||
request.onerror = function(event) {
|
||||
console.log({error:event});
|
||||
fallback();
|
||||
};
|
||||
} catch (e) {
|
||||
fallback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.initCalled = true;
|
||||
return this;
|
||||
};
|
||||
|
||||
SP.runQueue = function() {
|
||||
if (this.queue.length > 0) {
|
||||
let i = 0, q = this.queue, e;
|
||||
while (i < q.length) {
|
||||
e = q[i++];
|
||||
switch (e[0]) {
|
||||
case 'iterate': this.iterate(e[1], e[2], e[3]); break;
|
||||
case 'put': this.put(e[1], e[2], e[3]); break;
|
||||
case 'get': this.get(e[1], e[2]); break;
|
||||
case 'remove': this.remove(e[1], e[2]); break;
|
||||
case 'clear': this.clear();
|
||||
}
|
||||
}
|
||||
this.queue = [];
|
||||
}
|
||||
};
|
||||
|
||||
SP.put = function(key, value, callback) {
|
||||
if (local) {
|
||||
local[key] = value;
|
||||
if (callback) callback(true);
|
||||
return;
|
||||
}
|
||||
if (!this.db) {
|
||||
this.init();
|
||||
return this.queue.push(['put', key, value, callback]);
|
||||
}
|
||||
try {
|
||||
let req = this.db.transaction(this.name, "readwrite").objectStore(this.name).put(value, key);
|
||||
if (callback) {
|
||||
req.onsuccess = function(event) { callback(true) };
|
||||
req.onerror = function(event) { callback(false) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (callback) callback(false);
|
||||
}
|
||||
};
|
||||
|
||||
SP.get = function(key, callback) {
|
||||
if (local) {
|
||||
if (callback) callback(local[key]);
|
||||
return;
|
||||
}
|
||||
if (!this.db) {
|
||||
this.init();
|
||||
return this.queue.push(['get', key, callback]);
|
||||
}
|
||||
try {
|
||||
let req = this.db.transaction(this.name).objectStore(this.name).get(key);
|
||||
if (callback) {
|
||||
req.onsuccess = function(event) { callback(req.result) };
|
||||
req.onerror = function(event) { callback(null) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (callback) callback(null);
|
||||
}
|
||||
};
|
||||
|
||||
SP.remove = function(key, callback) {
|
||||
if (local) {
|
||||
delete local[key];
|
||||
if (callback) callback(true);
|
||||
return;
|
||||
}
|
||||
if (!this.db) {
|
||||
this.init();
|
||||
return this.queue.push(['remove', key, callback]);
|
||||
}
|
||||
try {
|
||||
let req = this.db.transaction(this.name, "readwrite").objectStore(this.name).delete(key);
|
||||
if (callback) {
|
||||
req.onsuccess = function(event) { callback(true) };
|
||||
req.onerror = function(event) { callback(false) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
if (callback) callback(false);
|
||||
}
|
||||
};
|
||||
|
||||
SP.clear = function(key) {
|
||||
if (!this.db) {
|
||||
this.init();
|
||||
return this.queue.push(['clear']);
|
||||
}
|
||||
this.db.transaction(this.name, "readwrite").objectStore(this.name).clear();
|
||||
};
|
||||
|
||||
})();
|
||||
54
src/moto/kv.js
Normal file
54
src/moto/kv.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (self.moto.KV) return;
|
||||
|
||||
try {
|
||||
self.moto.KV = self.localStorage;
|
||||
self.moto.KV.setItem('__test',1);
|
||||
self.moto.KV.getItem('__test');
|
||||
} catch (e) {
|
||||
console.log("in private browsing mode or 3rd party storage blocked. some settings will be lost.");
|
||||
self.moto.KV = new KV();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} dbname
|
||||
* @param {number} [version]
|
||||
* @constructor
|
||||
*/
|
||||
function KV() {
|
||||
this.__data__ = {};
|
||||
this.__mem__ = true;
|
||||
}
|
||||
|
||||
var KP = KV.prototype;
|
||||
|
||||
KP.getItem = function(key) {
|
||||
return this[key];
|
||||
};
|
||||
|
||||
KP.setItem = function(key, val) {
|
||||
this.__data__[key] = val;
|
||||
this[key] = val;
|
||||
};
|
||||
|
||||
KP.removeItem = function(key) {
|
||||
delete this.__data__[key];
|
||||
};
|
||||
|
||||
KP.clear = function() {
|
||||
var d = this.__data__, key;
|
||||
for (key in d) {
|
||||
if (d.hasOwnProperty(key)) {
|
||||
delete d[key];
|
||||
delete this[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
241
src/moto/load-obj.js
Normal file
241
src/moto/load-obj.js
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/**
|
||||
* adapted from THREE.OBJLoader example
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/Wavefront_.obj_file
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (self.moto.OBJ) return;
|
||||
|
||||
self.moto.OBJ = {
|
||||
parse : parse
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String} text
|
||||
* @returns {Array}
|
||||
*/
|
||||
function parse(text) {
|
||||
|
||||
let object,
|
||||
objects = [],
|
||||
geometry,
|
||||
material;
|
||||
|
||||
function parseVertexIndex(value) {
|
||||
let index = parseInt(value);
|
||||
return (index >= 0 ? index - 1 : index + vertices.length / 3) * 3;
|
||||
}
|
||||
|
||||
function parseNormalIndex(value) {
|
||||
let index = parseInt(value);
|
||||
return (index >= 0 ? index - 1 : index + normals.length / 3) * 3;
|
||||
}
|
||||
|
||||
function parseUVIndex(value) {
|
||||
let index = parseInt(value);
|
||||
return (index >= 0 ? index - 1 : index + uvs.length / 2) * 2;
|
||||
}
|
||||
|
||||
function addVertex(a, b, c) {
|
||||
geometry.vertices.push(
|
||||
vertices[a], vertices[a + 1], vertices[a + 2],
|
||||
vertices[b], vertices[b + 1], vertices[b + 2],
|
||||
vertices[c], vertices[c + 1], vertices[c + 2]
|
||||
);
|
||||
}
|
||||
|
||||
function addNormal(a, b, c) {
|
||||
geometry.normals.push(
|
||||
normals[a], normals[a + 1], normals[a + 2],
|
||||
normals[b], normals[b + 1], normals[b + 2],
|
||||
normals[c], normals[c + 1], normals[c + 2]
|
||||
);
|
||||
}
|
||||
|
||||
function addUV(a, b, c) {
|
||||
geometry.uvs.push(
|
||||
uvs[a], uvs[a + 1],
|
||||
uvs[b], uvs[b + 1],
|
||||
uvs[c], uvs[c + 1]
|
||||
);
|
||||
}
|
||||
|
||||
function addFace(a, b, c, d, ua, ub, uc, ud, na, nb, nc, nd) {
|
||||
|
||||
let ia = parseVertexIndex(a);
|
||||
let ib = parseVertexIndex(b);
|
||||
let ic = parseVertexIndex(c);
|
||||
let id;
|
||||
|
||||
if (d === undefined) {
|
||||
addVertex(ia, ib, ic);
|
||||
} else {
|
||||
id = parseVertexIndex(d);
|
||||
addVertex(ia, ib, id);
|
||||
addVertex(ib, ic, id);
|
||||
}
|
||||
|
||||
if (ua !== undefined) {
|
||||
ia = parseUVIndex(ua);
|
||||
ib = parseUVIndex(ub);
|
||||
ic = parseUVIndex(uc);
|
||||
|
||||
if (d === undefined) {
|
||||
addUV(ia, ib, ic);
|
||||
} else {
|
||||
id = parseUVIndex(ud);
|
||||
addUV(ia, ib, id);
|
||||
addUV(ib, ic, id);
|
||||
}
|
||||
}
|
||||
|
||||
if (na !== undefined) {
|
||||
ia = parseNormalIndex(na);
|
||||
ib = parseNormalIndex(nb);
|
||||
ic = parseNormalIndex(nc);
|
||||
|
||||
if (d === undefined) {
|
||||
addNormal(ia, ib, ic);
|
||||
} else {
|
||||
id = parseNormalIndex(nd);
|
||||
addNormal(ia, ib, id);
|
||||
addNormal(ib, ic, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create mesh if no objects in text
|
||||
|
||||
if (/^o /gm.test(text) === false) {
|
||||
geometry = {
|
||||
vertices: [],
|
||||
normals: [],
|
||||
uvs: []
|
||||
};
|
||||
|
||||
object = {
|
||||
name: '',
|
||||
geometry: geometry
|
||||
};
|
||||
|
||||
objects.push(object);
|
||||
}
|
||||
|
||||
let vertices = [],
|
||||
normals = [],
|
||||
uvs = [],
|
||||
// v float float float
|
||||
vertex_pattern = /v(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)/,
|
||||
// vn float float float
|
||||
normal_pattern = /vn(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)/,
|
||||
// vt float float
|
||||
uv_pattern = /vt(+[\d|\.|\+|\-|e|E]+)(+[\d|\.|\+|\-|e|E]+)/,
|
||||
// f vertex vertex vertex ...
|
||||
face_pattern1 = /f(+-?\d+)(+-?\d+)(+-?\d+)(+-?\d+)?/,
|
||||
// f vertex/uv vertex/uv vertex/uv ...
|
||||
face_pattern2 = /f(+(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+))?/,
|
||||
// f vertex/uv/normal vertex/uv/normal vertex/uv/normal ...
|
||||
face_pattern3 = /f(+(-?\d+)\/(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+)\/(-?\d+))(+(-?\d+)\/(-?\d+)\/(-?\d+))?/,
|
||||
// f vertex//normal vertex//normal vertex//normal ...
|
||||
face_pattern4 = /f(+(-?\d+)\/\/(-?\d+))(+(-?\d+)\/\/(-?\d+))(+(-?\d+)\/\/(-?\d+))(+(-?\d+)\/\/(-?\d+))?/,
|
||||
// split text into lines
|
||||
lines = text.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
|
||||
let line = lines[i];
|
||||
line = line.trim();
|
||||
|
||||
let result;
|
||||
|
||||
if (line.length === 0 || line.charAt(0) === '#') {
|
||||
continue;
|
||||
} else if ((result = vertex_pattern.exec(line)) !== null) {
|
||||
// ["v 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
|
||||
vertices.push(
|
||||
parseFloat(result[1]),
|
||||
parseFloat(result[2]),
|
||||
parseFloat(result[3])
|
||||
);
|
||||
} else if ((result = normal_pattern.exec(line)) !== null) {
|
||||
// ["vn 1.0 2.0 3.0", "1.0", "2.0", "3.0"]
|
||||
normals.push(
|
||||
parseFloat(result[1]),
|
||||
parseFloat(result[2]),
|
||||
parseFloat(result[3])
|
||||
);
|
||||
} else if ((result = uv_pattern.exec(line)) !== null) {
|
||||
// ["vt 0.1 0.2", "0.1", "0.2"]
|
||||
uvs.push(
|
||||
parseFloat(result[1]),
|
||||
parseFloat(result[2])
|
||||
);
|
||||
} else if ((result = face_pattern1.exec(line)) !== null) {
|
||||
// ["f 1 2 3", "1", "2", "3", undefined]
|
||||
addFace(
|
||||
result[1], result[2], result[3], result[4]
|
||||
);
|
||||
} else if ((result = face_pattern2.exec(line)) !== null) {
|
||||
// ["f 1/1 2/2 3/3", " 1/1", "1", "1", " 2/2", "2", "2", " 3/3", "3", "3", undefined, undefined, undefined]
|
||||
addFace(
|
||||
result[2], result[5], result[8], result[11],
|
||||
result[3], result[6], result[9], result[12]
|
||||
);
|
||||
} else if ((result = face_pattern3.exec(line)) !== null) {
|
||||
// ["f 1/1/1 2/2/2 3/3/3", " 1/1/1", "1", "1", "1", " 2/2/2", "2", "2", "2", " 3/3/3", "3", "3", "3", undefined, undefined, undefined, undefined]
|
||||
addFace(
|
||||
result[2], result[6], result[10], result[14],
|
||||
result[3], result[7], result[11], result[15],
|
||||
result[4], result[8], result[12], result[16]
|
||||
);
|
||||
} else if ((result = face_pattern4.exec(line)) !== null) {
|
||||
// ["f 1//1 2//2 3//3", " 1//1", "1", "1", " 2//2", "2", "2", " 3//3", "3", "3", undefined, undefined, undefined]
|
||||
addFace(
|
||||
result[2], result[5], result[8], result[11],
|
||||
undefined, undefined, undefined, undefined,
|
||||
result[3], result[6], result[9], result[12]
|
||||
);
|
||||
} else if (/^o /.test(line)) {
|
||||
|
||||
geometry = {
|
||||
vertices: [],
|
||||
normals: [],
|
||||
uvs: []
|
||||
};
|
||||
|
||||
object = {
|
||||
name: line.substring(2).trim(),
|
||||
geometry: geometry
|
||||
};
|
||||
|
||||
objects.push(object)
|
||||
|
||||
} else if (/^g /.test(line)) {
|
||||
// group
|
||||
} else if (/^usemtl /.test(line)) {
|
||||
// material
|
||||
material.name = line.substring(7).trim();
|
||||
} else if (/^mtllib /.test(line)) {
|
||||
// mtl file
|
||||
} else if (/^s /.test(line)) {
|
||||
// smooth shading
|
||||
} else {
|
||||
// unhandled
|
||||
}
|
||||
}
|
||||
|
||||
objects.forEach(function(object) {
|
||||
object.geometry.vertices = new Float32Array(object.geometry.vertices);
|
||||
object.geometry.normals = new Float32Array(object.geometry.normals);
|
||||
object.geometry.uvs = new Float32Array(object.geometry.uvs);
|
||||
});
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
})();
|
||||
254
src/moto/load-stl.js
Normal file
254
src/moto/load-stl.js
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
/**
|
||||
* adapted from THREE.STL-LOADER example
|
||||
*
|
||||
* https://en.wikipedia.org/wiki/STL_(file_format)
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (self.moto.STL) return;
|
||||
|
||||
const SP = STL.prototype;
|
||||
|
||||
self.moto.STL = STL;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function STL() {
|
||||
this.vertices = null;
|
||||
this.normals = null;
|
||||
this.colors = null;
|
||||
}
|
||||
|
||||
|
||||
SP.load = function(url, callback) {
|
||||
let stl = this,
|
||||
xhr = new XMLHttpRequest();
|
||||
|
||||
function onloaded (event) {
|
||||
if (event.target.status === 200 || event.target.status === 0) {
|
||||
stl.parse(event.target.response || event.target.responseText);
|
||||
if (callback) callback(stl.vertices);
|
||||
} else {
|
||||
if (callback) callback(null, event.target.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
xhr.addEventListener('load', onloaded, false);
|
||||
xhr.addEventListener('progress', function (event) { }, false);
|
||||
xhr.addEventListener('error', function () { }, false);
|
||||
|
||||
if (xhr.overrideMimeType) {
|
||||
xhr.overrideMimeType('text/plain; charset=x-user-defined');
|
||||
}
|
||||
|
||||
xhr.open('GET', url, true);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.send(null);
|
||||
};
|
||||
|
||||
SP.encode = function(vertices, normals) {
|
||||
if (!(vertices && vertices.length % 3 === 0)) throw "invalid vertices";
|
||||
|
||||
let vc = vertices.length / 3,
|
||||
bs = (vc * 16) + (vc * (2/3)) + 84,
|
||||
bin = new ArrayBuffer(bs),
|
||||
writer = new DataView(bin),
|
||||
i = 0,
|
||||
j = 0,
|
||||
pos = 80;
|
||||
|
||||
function writeInt16(val) {
|
||||
writer.setUint16(pos, val, true);
|
||||
pos += 2;
|
||||
}
|
||||
|
||||
function writeInt32(val) {
|
||||
writer.setUint32(pos, val, true);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
function writeFloat(val) {
|
||||
writer.setFloat32(pos, val, true);
|
||||
pos += 4;
|
||||
}
|
||||
|
||||
function writeVertex() {
|
||||
writeFloat(vertices[i++]); // x
|
||||
writeFloat(vertices[i++]); // y
|
||||
writeFloat(vertices[i++]); // z
|
||||
}
|
||||
|
||||
writeInt32(vc / 3);
|
||||
while (i < vertices.length) {
|
||||
writeFloat(normals ? normals[j++] : 0); // norm x
|
||||
writeFloat(normals ? normals[j++] : 0); // norm y
|
||||
writeFloat(normals ? normals[j++] : 0); // norm z
|
||||
writeVertex(); // p1
|
||||
writeVertex(); // p2
|
||||
writeVertex(); // p3
|
||||
writeInt16(0); // attributes
|
||||
}
|
||||
|
||||
return bin;
|
||||
};
|
||||
|
||||
SP.parse = function(data) {
|
||||
let binData = this.convertToBinary(data);
|
||||
|
||||
let isBinary = function () {
|
||||
let expect, face_size, n_faces, reader;
|
||||
reader = new DataView(binData);
|
||||
face_size = (32 / 8 * 3) + ((32 / 8 * 3) * 3) + (16 / 8);
|
||||
n_faces = reader.getUint32(80,true);
|
||||
expect = 80 + (32 / 8) + (n_faces * face_size);
|
||||
return expect === reader.byteLength;
|
||||
};
|
||||
|
||||
return isBinary()
|
||||
? this.parseBinary(binData)
|
||||
: this.parseASCII(this.convertToString(data));
|
||||
};
|
||||
|
||||
SP.parseBinary = function(data) {
|
||||
let reader = new DataView(data),
|
||||
faces = reader.getUint32 (80, true),
|
||||
r, g, b, hasColors = false, colors,
|
||||
defaultR, defaultG, defaultB, alpha;
|
||||
|
||||
// check for default color in STL header ("COLOR=rgba" sequence).
|
||||
for (let index = 0; index < 80 - 10; index++) {
|
||||
if ((reader.getUint32(index, false) == 0x434F4C4F /*COLO*/) &&
|
||||
(reader.getUint8(index + 4) == 0x52 /*'R'*/) &&
|
||||
(reader.getUint8(index + 5) == 0x3D /*'='*/)) {
|
||||
hasColors = true;
|
||||
colors = new Float32Array(faces * 3 * 3);
|
||||
defaultR = reader.getUint8(index + 6) / 255;
|
||||
defaultG = reader.getUint8(index + 7) / 255;
|
||||
defaultB = reader.getUint8(index + 8) / 255;
|
||||
alpha = reader.getUint8(index + 9) / 255;
|
||||
}
|
||||
}
|
||||
|
||||
let offset = 0,
|
||||
dataOffset = 84,
|
||||
faceLength = 12 * 4 + 2,
|
||||
vertices = new Float32Array(faces * 3 * 3),
|
||||
normals = new Float32Array(faces * 3 * 3);
|
||||
|
||||
colors = hasColors ? new Uint16Array(faces * 3 * 3) : null;
|
||||
|
||||
for (let face = 0; face < faces; face ++) {
|
||||
|
||||
let start = dataOffset + face * faceLength,
|
||||
normalX = reader.getFloat32(start, true),
|
||||
normalY = reader.getFloat32(start + 4, true),
|
||||
normalZ = reader.getFloat32(start + 8, true);
|
||||
|
||||
if (hasColors) {
|
||||
let packedColor = reader.getUint16(start + 48, true);
|
||||
if ((packedColor & 0x8000) === 0) { // facet has its own unique color
|
||||
r = (packedColor & 0x1F) / 31;
|
||||
g = ((packedColor >> 5) & 0x1F) / 31;
|
||||
b = ((packedColor >> 10) & 0x1F) / 31;
|
||||
} else {
|
||||
r = defaultR;
|
||||
g = defaultG;
|
||||
b = defaultB;
|
||||
}
|
||||
}
|
||||
|
||||
let i = 1, vertexstart;
|
||||
|
||||
while (i <= 3) {
|
||||
vertexstart = start + (i++) * 12;
|
||||
vertices[offset ] = reader.getFloat32 (vertexstart, true);
|
||||
vertices[offset + 1] = reader.getFloat32 (vertexstart + 4, true);
|
||||
vertices[offset + 2] = reader.getFloat32 (vertexstart + 8, true);
|
||||
normals[offset ] = normalX;
|
||||
normals[offset + 1] = normalY;
|
||||
normals[offset + 2] = normalZ;
|
||||
if (hasColors) {
|
||||
colors[offset ] = r;
|
||||
colors[offset + 1] = g;
|
||||
colors[offset + 2] = b;
|
||||
}
|
||||
offset += 3;
|
||||
}
|
||||
}
|
||||
|
||||
this.vertices = vertices;
|
||||
this.normals = normals;
|
||||
this.colors = colors;
|
||||
|
||||
return vertices;
|
||||
};
|
||||
|
||||
SP.parseASCII = function(data) {
|
||||
let result,
|
||||
resultText,
|
||||
patternNormal,
|
||||
patternVertex,
|
||||
vertices = [],
|
||||
normals = [],
|
||||
patternFace = /facet([\s\S]*?)endfacet/g;
|
||||
|
||||
while ((result = patternFace.exec(data)) !== null) {
|
||||
resultText = result[0];
|
||||
patternNormal = /normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;
|
||||
patternVertex = /vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;
|
||||
while ((result = patternNormal.exec(resultText)) !== null) {
|
||||
normals.push(parseFloat(result[1]));
|
||||
normals.push(parseFloat(result[3]));
|
||||
normals.push(parseFloat(result[5]));
|
||||
}
|
||||
while ((result = patternVertex.exec(resultText)) !== null) {
|
||||
vertices.push(parseFloat(result[1]));
|
||||
vertices.push(parseFloat(result[3]));
|
||||
vertices.push(parseFloat(result[5]));
|
||||
}
|
||||
}
|
||||
|
||||
let vToFloat32 = new Float32Array(vertices.length),
|
||||
nToFloat32 = new Float32Array(normals.length),
|
||||
i;
|
||||
|
||||
for (i=0; i<vertices.length; i++) vToFloat32[i] = vertices[i];
|
||||
for (i=0; i<normals.length; i++) nToFloat32[i] = normals[i];
|
||||
|
||||
this.vertices = vToFloat32;
|
||||
this.normals = nToFloat32;
|
||||
|
||||
return vToFloat32;
|
||||
};
|
||||
|
||||
SP.convertToString = function (buf) {
|
||||
if (typeof buf !== "string") {
|
||||
let array_buffer = new Uint8Array(buf);
|
||||
let str = '';
|
||||
for (let i = 0; i < buf.byteLength; i++) {
|
||||
str += String.fromCharCode(array_buffer[i]);
|
||||
}
|
||||
return str;
|
||||
} else {
|
||||
return buf;
|
||||
}
|
||||
};
|
||||
|
||||
SP.convertToBinary = function (buf) {
|
||||
if (typeof buf === "string") {
|
||||
let array_buffer = new Uint8Array(buf.length);
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
array_buffer[i] = buf.charCodeAt(i) & 0xff;
|
||||
}
|
||||
return array_buffer.buffer || array_buffer;
|
||||
} else {
|
||||
return buf;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
64
src/moto/pack.js
Normal file
64
src/moto/pack.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* adapted from
|
||||
* http://codeincomplete.com/posts/2011/5/7/bin_packing/
|
||||
*/
|
||||
|
||||
(function (){
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (self.moto.Pack) return;
|
||||
|
||||
self.moto.Pack = Packer;
|
||||
|
||||
function Packer (w, h, spacing) {
|
||||
this.root = { x: 0, y: 0, w: w, h: h };
|
||||
this.max = { w: 0, h: 0 };
|
||||
this.packed = false;
|
||||
this.spacing = typeof(spacing) === 'number' ? spacing : 1;
|
||||
this.pad = this.spacing / 2;
|
||||
}
|
||||
|
||||
Packer.prototype = {
|
||||
|
||||
fit: function (blocks) {
|
||||
let n = 0, node, block, w, h;
|
||||
while (n < blocks.length) {
|
||||
block = blocks[n++];
|
||||
w = block.w + this.spacing;
|
||||
h = block.h + this.spacing;
|
||||
if (node = this.findNode(this.root, w, h)) {
|
||||
block.fit = this.splitNode(node, w, h);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
this.packed = true;
|
||||
return this;
|
||||
},
|
||||
|
||||
findNode: function (root, w, h) {
|
||||
if (root.used) {
|
||||
return this.findNode(root.right, w, h) || this.findNode(root.down, w, h);
|
||||
} else if (w <= root.w && h <= root.h) {
|
||||
return root;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
splitNode: function (node, w, h) {
|
||||
node.used = true;
|
||||
node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h };
|
||||
node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h };
|
||||
this.max.w = Math.max(this.max.w, node.x + w);
|
||||
this.max.h = Math.max(this.max.h, node.y + h);
|
||||
return node;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
})();
|
||||
778
src/moto/space.js
Normal file
778
src/moto/space.js
Normal file
|
|
@ -0,0 +1,778 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
|
||||
let MOTO = self.moto,
|
||||
WIN = window,
|
||||
DOC = document,
|
||||
SCENE = new THREE.Scene(),
|
||||
WORLD = new THREE.Group(),
|
||||
WC = WORLD.children,
|
||||
PI = Math.PI,
|
||||
PI2 = PI / 2,
|
||||
PI4 = PI / 4,
|
||||
ROUND = Math.round,
|
||||
panY = 0,
|
||||
gridZOff = 0,
|
||||
platformZOff = 0,
|
||||
perspective = 35,
|
||||
refreshTimeout = null,
|
||||
refreshRequested = false,
|
||||
selectRecurse = false,
|
||||
defaultKeys = true,
|
||||
lightIntensity = 0.3,
|
||||
initialized = false,
|
||||
alignedTracking = false,
|
||||
skyColor = 0xbbbbbb,
|
||||
skyGridColor = 0xcccccc,
|
||||
showSkyGrid = true,
|
||||
showPlatform = true,
|
||||
hidePlatformBelow = true,
|
||||
trackcam = addLight(0, 0, 0, lightIntensity/3),
|
||||
trackDelta = {x:0, y:0, z:0},
|
||||
mouse = {x: 0, y: 0},
|
||||
mouseStart = null,
|
||||
mouseDragPoint = null,
|
||||
mouseDragStart = null,
|
||||
mouseDownSelect,
|
||||
mouseUpSelect,
|
||||
mouseHover,
|
||||
mouseDrag,
|
||||
gridOrigin,
|
||||
gridUnitMinor,
|
||||
gridUnitMajor,
|
||||
gridColorMajor,
|
||||
gridColorMinor,
|
||||
gridView,
|
||||
viewControl,
|
||||
trackPlane,
|
||||
platform,
|
||||
platformHover,
|
||||
platformClick,
|
||||
platformClickAt,
|
||||
platformOnMove,
|
||||
platformMoveTimer,
|
||||
light1,
|
||||
light2,
|
||||
light3,
|
||||
light4,
|
||||
light5,
|
||||
camera,
|
||||
renderer,
|
||||
container,
|
||||
freeze = false,
|
||||
isRound = false,
|
||||
platformMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0xcccccc,
|
||||
specular: 0xcccccc,
|
||||
shininess: 5,
|
||||
transparent: true,
|
||||
opacity: 0.6,
|
||||
side: THREE.DoubleSide
|
||||
}),
|
||||
hiddenKey,
|
||||
vizChange,
|
||||
docVisible = true;
|
||||
|
||||
if (typeof DOC.hidden !== "undefined") {
|
||||
hiddenKey = "hidden";
|
||||
vizChange = "visibilitychange";
|
||||
} else if (typeof DOC.msHidden !== "undefined") {
|
||||
hiddenKey = "msHidden";
|
||||
vizChange = "msvisibilitychange";
|
||||
} else if (typeof DOC.webkitHidden !== "undefined") {
|
||||
hiddenKey = "webkitHidden";
|
||||
vizChange = "webkitvisibilitychange";
|
||||
}
|
||||
|
||||
DOC.addEventListener(vizChange, () => {
|
||||
docVisible = DOC[hiddenKey] ? false : true;
|
||||
}, false);
|
||||
|
||||
/** ******************************************************************
|
||||
* TWEENing Functions
|
||||
******************************************************************* */
|
||||
|
||||
function tweenit() {
|
||||
TWEEN.update();
|
||||
setTimeout(tweenit, 20);
|
||||
}
|
||||
|
||||
tweenit();
|
||||
|
||||
function tweenCamPan(x,y,z) {
|
||||
let pos = viewControl.getPosition();
|
||||
pos.panX = x;
|
||||
pos.panY = y;
|
||||
pos.panZ = z;
|
||||
tweenCam(pos);
|
||||
}
|
||||
|
||||
function tweenCam(pos) {
|
||||
let tf = function () {
|
||||
viewControl.setPosition(this);
|
||||
refresh();
|
||||
};
|
||||
new TWEEN.Tween(viewControl.getPosition()).
|
||||
to(pos, 500).
|
||||
onUpdate(tf).
|
||||
start();
|
||||
}
|
||||
|
||||
function tweenPlatform(w,h,d) {
|
||||
let from = {x: platform.scale.x, y: platform.scale.y, z: platform.scale.z},
|
||||
to = {x:w, y:h, z:d},
|
||||
gridMajor = gridUnitMajor,
|
||||
gridMinor = gridUnitMinor,
|
||||
start = function() {
|
||||
setGrid(0);
|
||||
},
|
||||
update = function() {
|
||||
setPlatformSize(this.x, this.y, this.z);
|
||||
refresh();
|
||||
},
|
||||
complete = function() {
|
||||
setGrid(gridMajor, gridMinor, gridColorMajor, gridColorMinor);
|
||||
};
|
||||
new TWEEN.Tween(from).
|
||||
to(to, 500).
|
||||
onStart(start).
|
||||
onUpdate(update).
|
||||
onComplete(complete).
|
||||
start();
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Utility Functions
|
||||
******************************************************************* */
|
||||
|
||||
function width() { return WIN.innerWidth }
|
||||
|
||||
function height() { return WIN.innerHeight }
|
||||
|
||||
function aspect() { return width() / height() }
|
||||
|
||||
function addEventListener(el, key, fn) {
|
||||
el.addEventListener(key, fn);
|
||||
}
|
||||
|
||||
function addEventHandlers(el, pairs) {
|
||||
for (let i=0; i<pairs.length; i += 2) {
|
||||
addEventListener(el, pairs[i], pairs[i+1]);
|
||||
}
|
||||
}
|
||||
|
||||
function onEnterKey(el, fn) {
|
||||
if (Array.isArray(el)) {
|
||||
for (let i=0; i<el.length; i += 2) onEnterKey(el[i], el[i+1]);
|
||||
return;
|
||||
}
|
||||
addEventListener(el, 'keyup', function(event) {
|
||||
if (event.keyCode === 13) fn(event);
|
||||
});
|
||||
}
|
||||
|
||||
function addLight(x,y,z,i) {
|
||||
let l = new THREE.PointLight(0xffffff, i, 0);
|
||||
l.position.set(x,y,z);
|
||||
SCENE.add(l);
|
||||
return l;
|
||||
}
|
||||
|
||||
function updatePlatformPosition() {
|
||||
if (isRound) {
|
||||
platform.position.y = -platform.scale.y/2 - platformZOff;
|
||||
} else {
|
||||
platform.position.y = -platform.scale.z/2 - platformZOff;
|
||||
}
|
||||
requestRefresh();
|
||||
}
|
||||
|
||||
function setPlatformSize(width, depth, height) {
|
||||
if (isRound) {
|
||||
platform.scale.set(width || 300, height || 5, depth || 175);
|
||||
} else {
|
||||
platform.scale.set(width || 300, depth || 175, height || 5);
|
||||
}
|
||||
viewControl.maxDistance = Math.max(width,depth) * 4;
|
||||
updatePlatformPosition();
|
||||
let y = Math.max(width, height) * 1;
|
||||
light1.position.set( width, y, depth);
|
||||
light2.position.set(-width, y, -depth);
|
||||
light4.position.set( width, light4.position.y, -depth);
|
||||
light5.position.set(-width, light5.position.y, depth);
|
||||
}
|
||||
|
||||
function setPlatformSizeUpdateGrid(width, depth, height) {
|
||||
setPlatformSize(width, depth, height);
|
||||
setGrid(gridUnitMajor, gridUnitMinor);
|
||||
}
|
||||
|
||||
function setPlatformColor(color) {
|
||||
let was = platform.material.color.getHex();
|
||||
platform.material.color.set(color);
|
||||
requestRefresh();
|
||||
return was;
|
||||
}
|
||||
|
||||
function setGrid(unitMajor, unitMinor, colorMajor, colorMinor) {
|
||||
if (!unitMajor) return;
|
||||
let oldGridView = gridView;
|
||||
gridView = new THREE.Group();
|
||||
gridUnitMajor = unitMajor;
|
||||
gridUnitMinor = unitMinor;
|
||||
gridColorMajor = colorMajor || gridColorMajor;
|
||||
gridColorMinor = colorMinor || gridColorMinor;
|
||||
let x = platform.scale.x,
|
||||
y = isRound ? platform.scale.z : platform.scale.y,
|
||||
z = isRound ? platform.scale.y : platform.scale.z,
|
||||
xr = ROUND(x / unitMajor) * unitMajor,
|
||||
yr = ROUND(y / unitMajor) * unitMajor,
|
||||
xo = isRound ? Math.floor(x/2) : Math.ceil(xr / 2),
|
||||
yo = isRound ? Math.floor(y/2) : Math.ceil(yr / 2),
|
||||
w = x / 2,
|
||||
h = y / 2,
|
||||
d = z / 2,
|
||||
zp = -d - platformZOff + gridZOff,
|
||||
majors = [], minors = unitMinor ? [] : null, i;
|
||||
for (i = -xo; i <= xo; i++) {
|
||||
if (i >= -w && i <= w) {
|
||||
let oh = isRound ? Math.sqrt(1-(i/xo)*(i/xo)) * h : h;
|
||||
if (i % unitMajor === 0) majors.append({x:i, y:-oh, z:zp}).append({x:i, y:oh, z:zp});
|
||||
else if (minors && i % unitMinor === 0) minors.append({x:i, y:-oh, z:zp}).append({x:i, y:oh, z:zp});
|
||||
}
|
||||
}
|
||||
for (i = -yo; i <= yo; i++) {
|
||||
if (i >= -h && i <= h) {
|
||||
let ow = isRound ? Math.sqrt(1-(i/yo)*(i/yo)) * w : w;
|
||||
if (i % unitMajor === 0) majors.append({x:-ow, y:i, z:zp}).append({x:ow, y:i, z:zp});
|
||||
else if (minors && i % unitMinor === 0) minors.append({x:-ow, y:i, z:zp}).append({x:ow, y:i, z:zp});
|
||||
}
|
||||
}
|
||||
gridView.add(makeLinesFromPoints(majors, gridColorMajor || 0x999999, 1));
|
||||
if (minors) gridView.add(makeLinesFromPoints(minors, gridColorMinor || 0xcccccc, 1));
|
||||
if (oldGridView) Space.scene.remove(oldGridView);
|
||||
Space.scene.add(gridView);
|
||||
}
|
||||
|
||||
function setOrigin(x, y, z) {
|
||||
if (gridOrigin) {
|
||||
if (x === gridOrigin.x && y === gridOrigin.y && z === gridOrigin.z) {
|
||||
return;
|
||||
}
|
||||
Space.scene.remove(gridOrigin.group);
|
||||
}
|
||||
if (x === undefined) {
|
||||
gridOrigin = null;
|
||||
Space.update();
|
||||
return;
|
||||
}
|
||||
let cmat = new THREE.MeshPhongMaterial({
|
||||
color: 0xcceeff,
|
||||
specular: 0xcceeff,
|
||||
shininess: 5,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
let rmat = new THREE.MeshPhongMaterial({
|
||||
color: 0x88aadd,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
let PIP = Math.PI/2;
|
||||
let pi1, pi2, pi3, pi4;
|
||||
let group = new THREE.Group();
|
||||
gridOrigin = {x, y, z, group};
|
||||
group.add(pi1 = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(4.6, 50, PIP*0, PIP*1),
|
||||
cmat
|
||||
));
|
||||
pi1.position.x = 0.25;
|
||||
pi1.position.y = 0.25;
|
||||
group.add(pi2 = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(4.6, 50, PIP*1, PIP*1),
|
||||
cmat
|
||||
));
|
||||
pi2.position.x = -0.25;
|
||||
pi2.position.y = 0.25;
|
||||
group.add(pi3 = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(4.6, 50, PIP*2, PIP*1),
|
||||
cmat
|
||||
));
|
||||
pi3.position.x = -0.25;
|
||||
pi3.position.y = -0.25;
|
||||
group.add(pi4 = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(4.6, 50, PIP*3, PIP*1),
|
||||
cmat
|
||||
));
|
||||
pi4.position.x = 0.25;
|
||||
pi4.position.y = -0.25;
|
||||
group.add(new THREE.Mesh(
|
||||
new THREE.RingGeometry(5, 5.5, 50),
|
||||
rmat
|
||||
));
|
||||
group.add(new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(0.5, 10),
|
||||
rmat
|
||||
));
|
||||
group.add(new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(10, 0.5),
|
||||
rmat
|
||||
));
|
||||
group.rotation.x = -PI2;
|
||||
group.position.x = x;
|
||||
group.position.y = z;
|
||||
group.position.z = y;
|
||||
Space.scene.add(group);
|
||||
Space.update();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
refreshRequested = false;
|
||||
viewControl.update();
|
||||
}
|
||||
|
||||
/** deferred refresh that collapses multiple requests */
|
||||
function requestRefresh(timeout) {
|
||||
if (refreshRequested === false) {
|
||||
refreshRequested = true;
|
||||
clearTimeout(refreshTimeout);
|
||||
refreshTimeout = setTimeout(refresh, timeout || 10);
|
||||
}
|
||||
}
|
||||
|
||||
function onResize() {
|
||||
camera.aspect = aspect();
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(width(), height());
|
||||
container.style.width = width();
|
||||
container.style.height = height();
|
||||
requestRefresh();
|
||||
}
|
||||
|
||||
function alignTracking(point, rot, out) {
|
||||
if (point && rot) {
|
||||
alignedTracking = true;
|
||||
trackPlane.position.set(point.x, point.y, point.z);
|
||||
trackPlane.rotation.set(rot.x, rot.y, rot.z);
|
||||
trackDelta = out;
|
||||
} else {
|
||||
alignedTracking = false;
|
||||
trackPlane.position.set(0, 0, 0);
|
||||
trackPlane.rotation.set(PI2, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function cca(c) {
|
||||
return c.charCodeAt(0);
|
||||
}
|
||||
|
||||
function inputHasFocus() {
|
||||
return DOC.activeElement && (DOC.activeElement != DOC.body);
|
||||
}
|
||||
|
||||
function keyHandler(evt) {
|
||||
if (!defaultKeys || inputHasFocus()) return false;
|
||||
if (evt.metaKey) return false;
|
||||
let handled = true;
|
||||
switch (evt.charCode) {
|
||||
case cca('z'):
|
||||
Space.view.reset();
|
||||
break;
|
||||
case cca('h'):
|
||||
Space.view.home();
|
||||
break;
|
||||
case cca('t'):
|
||||
Space.view.top();
|
||||
break;
|
||||
// case cca('f'):
|
||||
// Space.view.front();
|
||||
// break;
|
||||
// case cca('l'):
|
||||
// Space.view.left();
|
||||
// break;
|
||||
// case cca('r'):
|
||||
// Space.view.right();
|
||||
// break;
|
||||
// case cca('b'):
|
||||
// Space.view.back();
|
||||
// break;
|
||||
default:
|
||||
handled = false;
|
||||
break;
|
||||
}
|
||||
if (handled) evt.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* ThreeJS Helper Functions
|
||||
******************************************************************* */
|
||||
|
||||
function makeLinesFromPoints(points, color, width) {
|
||||
if (points.length % 2 != 0) throw "invalid line : "+points.length;
|
||||
let geo = new THREE.Geometry(),
|
||||
i = 0, p1, p2, mesh;
|
||||
while (i < points.length) {
|
||||
p1 = points[i++];
|
||||
p2 = points[i++];
|
||||
geo.vertices.push(new THREE.Vector3(p1.x, p1.y, p1.z));
|
||||
geo.vertices.push(new THREE.Vector3(p2.x, p2.y, p2.z));
|
||||
}
|
||||
geo.verticesNeedUpdate = true;
|
||||
mesh = new THREE.LineSegments(geo, new THREE.LineBasicMaterial({
|
||||
color: color,
|
||||
linewidth: width || 1
|
||||
}));
|
||||
return mesh;
|
||||
}
|
||||
|
||||
function intersect(objects, recurse) {
|
||||
let lookAt = new THREE.Vector3(mouse.x, mouse.y, 0.0).unproject(camera);
|
||||
let ray = new THREE.Raycaster(camera.position, lookAt.sub(camera.position).normalize());
|
||||
return ray.intersectObjects(objects, recurse);
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Mouse Functions
|
||||
******************************************************************* */
|
||||
|
||||
function onMouseDown(event) {
|
||||
if (event.target === renderer.domElement) {
|
||||
DOC.activeElement.blur();
|
||||
event.preventDefault();
|
||||
let selection = null,
|
||||
trackTo = alignedTracking ? trackPlane : platform,
|
||||
isVis = trackTo.visible;
|
||||
if (mouseDownSelect) selection = mouseDownSelect();
|
||||
if (selection && selection.length > 0) {
|
||||
trackTo.visible = true;
|
||||
let int = intersect(selection.slice().append(trackTo), false);
|
||||
trackTo.visible = isVis;
|
||||
if (int.length > 0) {
|
||||
let trackInt, selectInt;
|
||||
for (let i=0; i<int.length; i++) {
|
||||
if (!trackInt && int[i].object === trackTo) {
|
||||
trackInt = int[i];
|
||||
} else if (!selectInt && selection.contains(int[i].object)) {
|
||||
selectInt = int[i];
|
||||
}
|
||||
}
|
||||
if (trackInt && selectInt) {
|
||||
mouseDragPoint = trackInt.point.clone();
|
||||
mouseDragStart = mouseDragPoint;
|
||||
viewControl.enabled = false;
|
||||
}
|
||||
if (selectInt) {
|
||||
mouseDownSelect(selectInt, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (platformClick) {
|
||||
let vis = platform.visible;
|
||||
platform.visible = true;
|
||||
let int = intersect([platform], false);
|
||||
platform.visible = vis;
|
||||
platformClickAt = int && int.length > 0 ? int[0].point : null;
|
||||
}
|
||||
} else {
|
||||
viewControl.enabled = false;
|
||||
}
|
||||
mouseStart = {
|
||||
x: (event.clientX / width()) * 2 - 1,
|
||||
y: -(event.clientY / height()) * 2 + 1};
|
||||
}
|
||||
|
||||
function onMouseUp(event) {
|
||||
if (!viewControl.enabled) {
|
||||
viewControl.enabled = true;
|
||||
viewControl.onMouseUp(event);
|
||||
}
|
||||
let mouseEnd = {
|
||||
x: (event.clientX / width()) * 2 - 1,
|
||||
y: -(event.clientY / height()) * 2 + 1};
|
||||
// only fire on mouse move between mouseStart (down) and up
|
||||
if (mouseEnd.x - mouseStart.x + mouseEnd.y - mouseStart.y === 0) {
|
||||
event.preventDefault();
|
||||
let refresh = false,
|
||||
selection = null;
|
||||
if (mouseUpSelect) selection = mouseUpSelect();
|
||||
if (selection && selection.length > 0) {
|
||||
let int = intersect(selection, selectRecurse);
|
||||
if (int.length > 0) {
|
||||
mouseUpSelect(int[0], event);
|
||||
refresh = true;
|
||||
} else {
|
||||
mouseUpSelect(null, event);
|
||||
}
|
||||
}
|
||||
if (!refresh && platformClickAt) {
|
||||
platformClick(platformClickAt);
|
||||
}
|
||||
if (refresh) requestRefresh();
|
||||
} else if (mouseDrag && mouseDragStart) {
|
||||
mouseDrag(null,null,true);
|
||||
}
|
||||
mouseDragPoint = null;
|
||||
mouseDragStart = null;
|
||||
}
|
||||
|
||||
function onMouseMove(event) {
|
||||
let int, vis;
|
||||
if (viewControl.enabled) {
|
||||
event.preventDefault();
|
||||
let selection = mouseHover ? mouseHover() : null;
|
||||
if (selection && selection.length > 0) {
|
||||
int = intersect(selection, selectRecurse);
|
||||
if (int.length > 0) mouseHover(int[0], event);
|
||||
}
|
||||
if ((!int || int.length == 0) && platformHover) {
|
||||
vis = platform.visible;
|
||||
platform.visible = true;
|
||||
int = intersect([platform], false);
|
||||
platform.visible = vis;
|
||||
if (int && int.length > 0) platformHover(int[0].point);
|
||||
}
|
||||
} else if (mouseDragPoint && mouseDrag && mouseDrag()) {
|
||||
event.preventDefault();
|
||||
let trackTo = alignedTracking ? trackPlane : platform;
|
||||
let vis = trackTo.visible;
|
||||
trackTo.visible = true;
|
||||
int = intersect([trackTo], false);
|
||||
trackTo.visible = vis;
|
||||
if (int.length > 0 && int[0].object === trackTo) {
|
||||
let delta = mouseDragPoint.clone().sub(int[0].point);
|
||||
let offset = mouseDragStart.clone().sub(int[0].point);
|
||||
mouseDragPoint = int[0].point;
|
||||
mouseDrag({x: -delta.x, y: delta.z}, offset.multiplyVectors(offset, trackDelta));
|
||||
requestRefresh();
|
||||
}
|
||||
}
|
||||
mouse = {
|
||||
x: (event.clientX / width()) * 2 - 1,
|
||||
y: -(event.clientY / height()) * 2 + 1};
|
||||
}
|
||||
|
||||
/** ******************************************************************
|
||||
* Space Object
|
||||
******************************************************************* */
|
||||
|
||||
let Space = MOTO.Space = {
|
||||
alignTracking: alignTracking,
|
||||
addEventListener: addEventListener,
|
||||
addEventHandlers: addEventHandlers,
|
||||
onEnterKey: onEnterKey,
|
||||
onResize: onResize,
|
||||
update: requestRefresh,
|
||||
refresh: refresh,
|
||||
|
||||
showSkyGrid: function(b) { showSkyGrid = b },
|
||||
setSkyColor: function(c) { skyColor = c },
|
||||
setSkyGridColor: function(c) { skyGridColor = c },
|
||||
|
||||
scene: {
|
||||
add: function (o) {
|
||||
o.rotation.x = WORLD.rotation.x;
|
||||
return SCENE.add(o);
|
||||
},
|
||||
remove: function (o) {
|
||||
return SCENE.remove(o);
|
||||
},
|
||||
freeze: function (b) {
|
||||
let fz = freeze;
|
||||
freeze = b;
|
||||
return fz;
|
||||
}
|
||||
},
|
||||
|
||||
platform: {
|
||||
tweenTo: tweenPlatform,
|
||||
setSize: setPlatformSizeUpdateGrid,
|
||||
setColor: setPlatformColor,
|
||||
setOrigin: setOrigin,
|
||||
setGrid: setGrid,
|
||||
add: function(o) { WORLD.add(o) },
|
||||
remove: function(o) { WORLD.remove(o) },
|
||||
setMaxZ: function(z) { panY = z / 2 },
|
||||
isHidden: function() { return !showPlatform },
|
||||
setHidden: function(b) { showPlatform = !b; platform.visible = !b },
|
||||
setHiding: function(b) { hidePlatformBelow = b },
|
||||
setZOff: function(z) { platformZOff = z; updatePlatformPosition() },
|
||||
setGZOff: function(z) { gridZOff = z; updatePlatformPosition() },
|
||||
opacity: function(o) { platform.material.opacity = o },
|
||||
onMove: function(f) { platformOnMove = f },
|
||||
onHover: function(f) { platformHover = f },
|
||||
onClick: function(f) { platformClick = f},
|
||||
size: function() { return platform.scale },
|
||||
isVisible: function() { return platform.visible },
|
||||
showGrid: function(b) { gridView.visible = b },
|
||||
setRound: function(bool) {
|
||||
let current = platform;
|
||||
isRound = bool;
|
||||
if (bool) {
|
||||
platform = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(.5, .5, 1, 60),
|
||||
platformMaterial
|
||||
);
|
||||
platform.rotation.x = 0;
|
||||
} else {
|
||||
platform = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
platformMaterial
|
||||
);
|
||||
platform.rotation.x = -PI2;
|
||||
}
|
||||
|
||||
platform.position.y = current.position.y;
|
||||
platform.visible = current.visible;
|
||||
|
||||
SCENE.remove(current);
|
||||
SCENE.add(platform);
|
||||
},
|
||||
world: WORLD
|
||||
},
|
||||
|
||||
view: {
|
||||
top: function() { tweenCam({left: 0, up: 0, panX: 0, panY: panY, panZ: 0}) },
|
||||
back: function() { tweenCam({left: PI, up: PI2, panX: 0, panY: panY, panZ: 0}) },
|
||||
home: function() { tweenCam({left: 0, up: PI4, panX: 0, panY: panY, panZ: 0}) },
|
||||
front: function() { tweenCam({left: 0, up: PI2, panX: 0, panY: panY, panZ: 0}) },
|
||||
right: function() { tweenCam({left: PI2, up: PI2, panX: 0, panY: panY, panZ: 0}) },
|
||||
left: function() { tweenCam({left: -PI2, up: PI2, panX: 0, panY: panY, panZ: 0}) },
|
||||
reset: function() { viewControl.reset(); requestRefresh() },
|
||||
load: function(cam) { viewControl.setPosition(cam) },
|
||||
save: function() { return viewControl.getPosition(true) },
|
||||
panTo: function(x,y,z) { tweenCamPan(x,y,z) },
|
||||
setZoom: function(r,v) { viewControl.setZoom(r,v) }
|
||||
},
|
||||
|
||||
mouse: {
|
||||
downSelect: function(f) { mouseDownSelect = f },
|
||||
upSelect: function(f) { mouseUpSelect = f },
|
||||
onDrag: function(f) { mouseDrag = f },
|
||||
onHover: function(f) { mouseHover = f }
|
||||
},
|
||||
|
||||
useDefaultKeys: function(b) {
|
||||
defaultKeys = b;
|
||||
},
|
||||
|
||||
selectRecurse: function(b) {
|
||||
selectRecurse = b;
|
||||
},
|
||||
|
||||
objects: function() {
|
||||
return WC;
|
||||
},
|
||||
|
||||
screenshot: function(format) {
|
||||
return renderer.domElement.toDataURL(format || "image/png");
|
||||
},
|
||||
|
||||
init: function(domelement, slider) {
|
||||
container = domelement;
|
||||
|
||||
WORLD.rotation.x = -PI2;
|
||||
SCENE.add(WORLD);
|
||||
|
||||
domelement.style.width = width();
|
||||
domelement.style.height = height();
|
||||
|
||||
renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: true
|
||||
});
|
||||
camera = perspective ?
|
||||
new THREE.PerspectiveCamera(perspective, aspect(), 5, 100000) :
|
||||
new THREE.OrthographicCamera(-100 * aspect(), 100 * aspect(), 100, -100, 0.1, 100000);
|
||||
|
||||
camera.position.set(0, 200, 340);
|
||||
renderer.setSize(width(), height());
|
||||
domelement.appendChild(renderer.domElement);
|
||||
|
||||
viewControl = new MOTO.CTRL(camera, domelement, function (position, moved) {
|
||||
if (platform) {
|
||||
platform.visible = hidePlatformBelow ?
|
||||
initialized && position.y >= 0 && showPlatform : showPlatform;
|
||||
}
|
||||
if (trackcam) {
|
||||
trackcam.position.copy(camera.position);
|
||||
}
|
||||
if (moved && platformOnMove) {
|
||||
clearTimeout(platformMoveTimer);
|
||||
platformMoveTimer = setTimeout(platformOnMove, 500);
|
||||
}
|
||||
}, slider);
|
||||
|
||||
viewControl.noKeys = true;
|
||||
viewControl.maxDistance = 1000;
|
||||
|
||||
SCENE.add(new THREE.AmbientLight(0x707070));
|
||||
|
||||
light1 = addLight( 200, 250, 200, lightIntensity * 1.15);
|
||||
light2 = addLight(-200, 250, -200, lightIntensity * 0.95);
|
||||
light3 = addLight( 0, -200, 0, lightIntensity * 0.5);
|
||||
light4 = addLight( 200, 5, -200, lightIntensity * 0.35);
|
||||
light5 = addLight(-200, 5, 200, lightIntensity * 0.4);
|
||||
|
||||
platform = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(1, 1, 1),
|
||||
platformMaterial
|
||||
);
|
||||
|
||||
platform.position.y = platformZOff;
|
||||
platform.rotation.x = -PI2;
|
||||
platform.visible = showPlatform;
|
||||
|
||||
trackPlane = new THREE.Mesh(
|
||||
new THREE.PlaneBufferGeometry(100000, 100000, 1, 1),
|
||||
new THREE.MeshBasicMaterial( { color: 0x777777, opacity: 0, transparent: true, side:THREE.DoubleSide } )
|
||||
);
|
||||
trackPlane.visible = false;
|
||||
trackPlane.rotation.x = PI2;
|
||||
|
||||
let sky = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(50000, 50000, 50000, 1, 1, 1),
|
||||
new THREE.MeshBasicMaterial({ color: skyColor, side: THREE.DoubleSide })
|
||||
),
|
||||
skygrid = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(5000, 5000, 5000, 10, 10, 10),
|
||||
new THREE.MeshBasicMaterial({ color: skyGridColor, side: THREE.DoubleSide })
|
||||
);
|
||||
|
||||
|
||||
SCENE.add(platform);
|
||||
SCENE.add(trackPlane);
|
||||
SCENE.add(sky);
|
||||
|
||||
if (showSkyGrid) {
|
||||
skygrid.material.wireframe = true;
|
||||
SCENE.add(skygrid);
|
||||
}
|
||||
|
||||
addEventHandlers(WIN, [
|
||||
'resize', onResize,
|
||||
'mousemove', onMouseMove,
|
||||
'mousedown', onMouseDown,
|
||||
'mouseup', onMouseUp,
|
||||
'keypress', keyHandler
|
||||
]);
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
if (docVisible && !freeze) renderer.render(SCENE, camera);
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
};
|
||||
|
||||
})();
|
||||
579
src/moto/ui.js
Normal file
579
src/moto/ui.js
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
(function() {
|
||||
|
||||
if (!self.moto) self.moto = {};
|
||||
if (moto.ui) return;
|
||||
|
||||
let SELF = self,
|
||||
lastRegion = null,
|
||||
lastGroup = null,
|
||||
lastDiv = null,
|
||||
addTo = null,
|
||||
hideAction = null,
|
||||
inputAction = null,
|
||||
groups = {},
|
||||
groupShow = {},
|
||||
groupName = undefined,
|
||||
hideable = {},
|
||||
hasModes = [],
|
||||
isExpert = [],
|
||||
letMode = null,
|
||||
letExpert = false,
|
||||
SDB = moto.KV,
|
||||
DOC = SELF.document,
|
||||
prefix = "tab",
|
||||
NOMODE = "nomode",
|
||||
exitimer = undefined;
|
||||
|
||||
SELF.$ = (SELF.$ || function (id) { return DOC.getElementById(id) } );
|
||||
|
||||
moto.ui = {
|
||||
prefix: function(pre) { prefix = pre; return moto.ui },
|
||||
hideAction: function(fn) { hideAction = fn; return moto.ui },
|
||||
inputAction: function(fn) { inputAction = fn; return moto.ui },
|
||||
setMode: setMode,
|
||||
setExpert: setExpert,
|
||||
bound: bound,
|
||||
toInt: toInt,
|
||||
toFloat: toFloat,
|
||||
hidePop: hidePop,
|
||||
isPopped: isPopped,
|
||||
newText: newTextArea,
|
||||
newLabel: newLabel,
|
||||
newRange: newRange,
|
||||
newInput: newInputField,
|
||||
newButton: newButton,
|
||||
newBoolean: newBooleanField,
|
||||
newSelect: newSelect,
|
||||
newTable: newTables,
|
||||
newTableRow: newTableRow,
|
||||
newRow: newRow,
|
||||
newBlank: newBlank,
|
||||
newGroup: newGroup,
|
||||
setGroup: setGroup,
|
||||
hidePoppers: hidePoppers,
|
||||
checkpoint,
|
||||
restore
|
||||
};
|
||||
|
||||
function setMode(mode) {
|
||||
$('control-left').classList.add('compact');
|
||||
$('control-right').classList.add('compact');
|
||||
Object.keys(groupShow).forEach(group => {
|
||||
updateGroupShow(group);
|
||||
});
|
||||
letMode = mode;
|
||||
hasModes.forEach(function(div) {
|
||||
div.setMode(div._group && !groupShow[div._group] ? NOMODE : mode);
|
||||
});
|
||||
}
|
||||
|
||||
function setExpert(bool) {
|
||||
letExpert = bool;
|
||||
setMode(letMode);
|
||||
}
|
||||
|
||||
function checkpoint() {
|
||||
return { addTo, lastDiv, lastGroup, lastRegion, groupName };
|
||||
}
|
||||
|
||||
function restore(opt) {
|
||||
if (opt) {
|
||||
addTo = opt.addTo;
|
||||
lastDiv = opt.lastDiv;
|
||||
lastGroup = opt.lastGroup;
|
||||
lastRegion = opt.lastRegion;
|
||||
groupName = opt.groupName;
|
||||
}
|
||||
}
|
||||
|
||||
// at present only used by the layers popup menu
|
||||
function setGroup(div) {
|
||||
addTo = lastDiv = div;
|
||||
groupName = undefined;
|
||||
lastGroup = [];
|
||||
return div;
|
||||
}
|
||||
|
||||
function newGroup(label, div, options) {
|
||||
lastDiv = div || lastDiv;
|
||||
return addCollapsableGroup(label, lastDiv, options);
|
||||
}
|
||||
|
||||
function addCollapsableGroup(label, div, options) {
|
||||
let opt = options || {};
|
||||
let group = opt.group || label;
|
||||
lastRegion = opt.region || lastRegion;
|
||||
hideable[group] = opt.nocompact === true ? false : true;
|
||||
|
||||
let row = DOC.createElement('div'),
|
||||
a = DOC.createElement('a'),
|
||||
dbkey = `beta-${prefix}-show-${group}`,
|
||||
popper = !opt.nocompact;
|
||||
|
||||
if (popper) {
|
||||
let pop = DOC.createElement('div');
|
||||
pop.classList.add('popper');
|
||||
if (lastRegion) pop.classList.add(lastRegion);
|
||||
if (opt.poprow) pop.classList.add('poprow')
|
||||
row.appendChild(pop);
|
||||
addTo = pop;
|
||||
} else {
|
||||
addTo = lastDiv;
|
||||
}
|
||||
|
||||
div.appendChild(row);
|
||||
row.setAttribute("class", "grouphead noselect");
|
||||
row.appendChild(a);
|
||||
a.appendChild(DOC.createTextNode(label));
|
||||
addModeControls(row, opt);
|
||||
lastGroup = groups[group] = [];
|
||||
lastGroup.key = dbkey;
|
||||
groupName = group;
|
||||
groupShow[group] = popper ? SDB[dbkey] === 'true' : SDB[dbkey] !== 'false';
|
||||
row.onclick = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
toggleGroup(group, dbkey);
|
||||
};
|
||||
if (popper) {
|
||||
row.onmouseenter = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(exitimer);
|
||||
showGroup(group);
|
||||
};
|
||||
row.onmouseleave = function(ev) {
|
||||
if (ev.target !== a && ev.target !== row) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(exitimer);
|
||||
exitimer = setTimeout(showGroup, 1000);
|
||||
};
|
||||
addTo._group = group;
|
||||
addModeControls(addTo, opt);
|
||||
lastGroup.push(addTo);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function hidePoppers() {
|
||||
showGroup(undefined);
|
||||
}
|
||||
|
||||
function showGroup(groupname) {
|
||||
Object.keys(groups).forEach(name => {
|
||||
let group = groups[name];
|
||||
groupShow[name] = SDB[group.key] = (groupname === name);
|
||||
updateGroupShow(name);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGroup(groupname, dbkey) {
|
||||
let show = SDB[dbkey] === 'false';
|
||||
groupShow[groupname] = hideable[groupname] ? SDB[dbkey] = show : true;
|
||||
updateGroupShow(groupname);
|
||||
}
|
||||
|
||||
function updateGroupShow(groupname) {
|
||||
let show = groupShow[groupname] || !hideable[groupname];
|
||||
let group = groups[groupname];
|
||||
group.forEach(div => {
|
||||
if (show) div.setMode(letMode);
|
||||
else div.setMode(NOMODE);
|
||||
});
|
||||
}
|
||||
|
||||
function toInt() {
|
||||
let nv = this.value !== '' ? parseInt(this.value) : null;
|
||||
if (isNaN(nv)) nv = 0;
|
||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||
this.value = nv;
|
||||
return nv;
|
||||
}
|
||||
|
||||
function toFloat() {
|
||||
let nv = this.value !== '' ? parseFloat(this.value) : null;
|
||||
if (nv !== null && this.bound) nv = this.bound(nv);
|
||||
this.value = nv;
|
||||
return nv;
|
||||
}
|
||||
|
||||
function bound(low,high) {
|
||||
return function(v) {
|
||||
if (isNaN(v)) return low;
|
||||
return v < low ? low : v > high ? high : v;
|
||||
};
|
||||
}
|
||||
|
||||
function raw() {
|
||||
return this.value !== '' ? this.value : null;
|
||||
}
|
||||
|
||||
function newLabel(text) {
|
||||
let label = DOC.createElement('label');
|
||||
label.appendChild(DOC.createTextNode(text));
|
||||
label.setAttribute("class", "noselect");
|
||||
return label;
|
||||
}
|
||||
|
||||
function addId(el, options) {
|
||||
if (options && options.id) {
|
||||
el.setAttribute("id", options.id);
|
||||
}
|
||||
}
|
||||
|
||||
function addModeControls(el, options) {
|
||||
options = options || {};
|
||||
el.__show = true;
|
||||
el.__modeSave = null;
|
||||
el.showMe = function() {
|
||||
if (el.__show) return;
|
||||
el.style.display = el.__modeSave;
|
||||
el.__show = true;
|
||||
el.__modeSave = null;
|
||||
};
|
||||
el.hideMe = function() {
|
||||
if (!el.__show) return;
|
||||
el.__show = false;
|
||||
el.__modeSave = el.style.display;
|
||||
el.style.display = 'none';
|
||||
};
|
||||
el.setVisible = function(show) {
|
||||
if (show) el.showMe();
|
||||
else el.hideMe();
|
||||
};
|
||||
el.setMode = function(mode) {
|
||||
let show = options.expert === undefined || (options.expert === letExpert);
|
||||
el.setVisible(el.hasMode(mode) && show);
|
||||
}
|
||||
el.hasMode = function(mode) {
|
||||
if (mode === NOMODE) return false;
|
||||
if (!el.modes) return true;
|
||||
return el.modes.contains(mode);
|
||||
}
|
||||
if (options.modes) {
|
||||
el.modes = options.modes;
|
||||
hasModes.push(el);
|
||||
}
|
||||
}
|
||||
|
||||
function newDiv(options) {
|
||||
let div = DOC.createElement('div');
|
||||
addModeControls(div, options);
|
||||
addTo.appendChild(div);
|
||||
lastGroup.push(div);
|
||||
div._group = groupName;
|
||||
return div;
|
||||
}
|
||||
|
||||
let lastPop = null;
|
||||
let savePop = null;
|
||||
|
||||
function hidePop() {
|
||||
if (lastPop) {
|
||||
lastPop.style.display = "none";
|
||||
}
|
||||
if (savePop) {
|
||||
savePop();
|
||||
savePop = null;
|
||||
}
|
||||
lastPop = null;
|
||||
}
|
||||
|
||||
function isPopped() {
|
||||
return lastPop !== null;
|
||||
}
|
||||
|
||||
function newTextArea(label, options) {
|
||||
let opt = options || {},
|
||||
row = newDiv(options),
|
||||
btn = DOC.createElement("button"),
|
||||
box = DOC.createElement("div"),
|
||||
pop = DOC.createElement("div"),
|
||||
lbl = DOC.createElement("label"),
|
||||
txt = DOC.createElement("textarea");
|
||||
|
||||
pop.setAttribute("class", "poptext flow-col");
|
||||
box.setAttribute("style", "position: relative");
|
||||
box.appendChild(pop);
|
||||
pop.appendChild(lbl);
|
||||
pop.appendChild(txt);
|
||||
lbl.appendChild(DOC.createTextNode(label));
|
||||
|
||||
txt.setAttribute("cols", 40);
|
||||
txt.setAttribute("rows", 20);
|
||||
txt.setAttribute("wrap", "off");
|
||||
|
||||
btn.appendChild(DOC.createTextNode("edit"));
|
||||
btn.onclick = function(ev) {
|
||||
ev.stopPropagation();
|
||||
if (ev.target === txt) {
|
||||
// drop clicks on TextArea
|
||||
ev.target.focus();
|
||||
} else {
|
||||
// first time, button click / show
|
||||
btn.parentNode.appendChild(box);
|
||||
btn.parentNode.onclick = btn.onclick;
|
||||
txt.scrollTop = 0;
|
||||
txt.scrollLeft = 0;
|
||||
txt.selectionEnd = 0;
|
||||
let rows = txt.value.split('\n');
|
||||
let cols = 0;
|
||||
rows.forEach(row => {
|
||||
cols = Math.max(cols, row.length);
|
||||
});
|
||||
txt.setAttribute("cols", Math.max(30, cols + 1));
|
||||
txt.setAttribute("rows", Math.max(10, rows.length + 1));
|
||||
|
||||
let showing = pop === lastPop;
|
||||
hidePop();
|
||||
if (!showing) {
|
||||
savePop = inputAction;
|
||||
pop.style.display = "flex";
|
||||
lastPop = pop;
|
||||
txt.focus();
|
||||
} else {
|
||||
inputAction();
|
||||
}
|
||||
}
|
||||
};
|
||||
addModeControls(btn, opt);
|
||||
addId(btn, opt);
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(btn);
|
||||
row.setAttribute("class", "flow-row");
|
||||
if (opt.title) row.setAttribute("title", options.title);
|
||||
btn.setVisible = row.setVisible;
|
||||
|
||||
return txt;
|
||||
}
|
||||
|
||||
function newInputField(label, options) {
|
||||
let opt = options || {},
|
||||
row = newDiv(opt),
|
||||
hide = opt && opt.hide,
|
||||
size = opt ? opt.size || 5 : 5,
|
||||
height = opt ? opt.height : 0,
|
||||
ip = height > 1 ? DOC.createElement('textarea') : DOC.createElement('input'),
|
||||
action = opt.action || inputAction;
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "flow-row");
|
||||
if (height > 1) {
|
||||
ip.setAttribute("cols", size);
|
||||
ip.setAttribute("rows", height);
|
||||
ip.setAttribute("wrap", "off");
|
||||
} else {
|
||||
if (Number.isInteger(size)) {
|
||||
ip.setAttribute("size", size);
|
||||
} else {
|
||||
ip.setAttribute("style", `width:${size}`);
|
||||
}
|
||||
}
|
||||
ip.setAttribute("type", "text");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (opt) {
|
||||
if (opt.disabled) ip.setAttribute("disabled", "true");
|
||||
if (opt.title) row.setAttribute("title", opt.title);
|
||||
if (opt.convert) ip.convert = opt.convert.bind(ip);
|
||||
if (opt.bound) ip.bound = opt.bound;
|
||||
if (opt.action) action = opt.action;
|
||||
}
|
||||
ip.addEventListener('focus', function(event) {
|
||||
hidePop();
|
||||
});
|
||||
if (action) {
|
||||
ip.addEventListener('keydown', function(event) {
|
||||
let key = event.key;
|
||||
if (
|
||||
opt.text ||
|
||||
(key >= '0' && key <= '9') ||
|
||||
key === '.' ||
|
||||
key === '-' ||
|
||||
key === 'Backspace' ||
|
||||
key === 'Delete' ||
|
||||
key === 'ArrowLeft' ||
|
||||
key === 'ArrowRight' ||
|
||||
key === 'Tab' ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
(key === ',' && options.comma)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
ip.addEventListener('keyup', function(event) {
|
||||
if (event.keyCode === 13) {
|
||||
// action(event);
|
||||
ip.blur();
|
||||
}
|
||||
});
|
||||
ip.addEventListener('blur', function(event) {
|
||||
action(event);
|
||||
});
|
||||
}
|
||||
if (!ip.convert) ip.convert = raw.bind(ip);
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newRange(label, options) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('input'),
|
||||
hide = options && options.hide,
|
||||
action = inputAction;
|
||||
|
||||
if (label) row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "flow-row");
|
||||
ip.setAttribute("type", "range");
|
||||
ip.setAttribute("min", (options && options.min ? options.min : 0));
|
||||
ip.setAttribute("max", (options && options.max ? options.max : 100));
|
||||
ip.setAttribute("value", 0);
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (options) {
|
||||
if (options.title) {
|
||||
ip.setAttribute("title", options.title);
|
||||
row.setAttribute("title", options.title);
|
||||
}
|
||||
if (options.action) action = options.action;
|
||||
}
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newSelect(label, options, source) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('select'),
|
||||
hide = options && options.hide,
|
||||
action = inputAction;
|
||||
|
||||
row.appendChild(newLabel(label));
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("source", source || "tools");
|
||||
row.setAttribute("class", "flow-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
if (options) {
|
||||
if (options.convert) ip.convert = options.convert.bind(ip);
|
||||
if (options.disabled) ip.setAttribute("disabled", "true");
|
||||
if (options.title) row.setAttribute("title", options.title);
|
||||
if (options.action) action = options.action;
|
||||
}
|
||||
ip.onchange = function() { action() };
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newBooleanField(label, action, options) {
|
||||
let row = newDiv(options),
|
||||
ip = DOC.createElement('input'),
|
||||
hide = options && options.hide;
|
||||
|
||||
if (label) {
|
||||
row.appendChild(newLabel(label));
|
||||
}
|
||||
row.appendChild(ip);
|
||||
row.setAttribute("class", "flow-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
ip.setAttribute("type", "checkbox");
|
||||
ip.checked = false;
|
||||
if (options) {
|
||||
if (options.disabled) ip.setAttribute("disabled", "true");
|
||||
if (options.title) {
|
||||
ip.setAttribute("title", options.title);
|
||||
row.setAttribute("title", options.title);
|
||||
}
|
||||
}
|
||||
if (action) ip.onclick = function() { action() };
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newBlank(options) {
|
||||
let row = newDiv(options),
|
||||
hide = options && options.hide;
|
||||
|
||||
row.setAttribute("class", "flow-row");
|
||||
row.style.display = hide ? 'none' : '';
|
||||
ip.setVisible = row.setVisible;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
function newButton(label, action, options) {
|
||||
let b = DOC.createElement('button'),
|
||||
t = DOC.createTextNode(label);
|
||||
|
||||
b.onclick = function() {
|
||||
hidePoppers();
|
||||
if (action) action();
|
||||
};
|
||||
|
||||
if (options) {
|
||||
if (options.class) b.classList.add(options.class);
|
||||
if (options.icon) {
|
||||
let d = DOC.createElement('div');
|
||||
d.innerHTML = options.icon;
|
||||
b.appendChild(d);
|
||||
}
|
||||
}
|
||||
|
||||
b.appendChild(t);
|
||||
|
||||
addModeControls(b, options);
|
||||
addId(b, options);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
function newTableRow(arrayOfArrays, options) {
|
||||
return newRow(newTables(arrayOfArrays), options);
|
||||
}
|
||||
|
||||
function newTables(arrayOfArrays) {
|
||||
let array = [];
|
||||
for (let i=0; i<arrayOfArrays.length; i++) {
|
||||
array.push(newRowTable(arrayOfArrays[i]));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function newRowTable(array) {
|
||||
let div = newDiv();
|
||||
div.setAttribute("class", "tablerow");
|
||||
array.forEach(function(c) {
|
||||
div.appendChild(c);
|
||||
});
|
||||
return div;
|
||||
}
|
||||
|
||||
function newRow(children, options) {
|
||||
let row = addCollapsableElement((options && options.noadd) ? null : addTo);
|
||||
if (children) children.forEach(function (c) { row.appendChild(c) });
|
||||
addModeControls(row, options);
|
||||
return row;
|
||||
}
|
||||
|
||||
function addCollapsableElement(parent) {
|
||||
let row = newDiv();
|
||||
if (parent) parent.appendChild(row);
|
||||
if (lastGroup) lastGroup.push(row);
|
||||
return row;
|
||||
}
|
||||
|
||||
})();
|
||||
Loading…
Reference in a new issue