restructure code, internalize devices

This commit is contained in:
Stewart Allen 2020-06-03 22:39:30 -04:00
commit 02feb524e6
110 changed files with 592 additions and 681 deletions

84
src/moto/ajax.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}
})();