mesh v2 at /v2/mesh
This commit is contained in:
parent
aee9fd6ed7
commit
6566c45f9a
85 changed files with 138198 additions and 1 deletions
75
src2/moto/ajax.js
Normal file
75
src2/moto/ajax.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
import { local } from '../data/local.js';
|
||||
|
||||
function rnd() {
|
||||
return Math.round(Math.random()*0xffffffff).toString(36);
|
||||
}
|
||||
|
||||
const KEY = "moto-ajax";
|
||||
const MOID = local.getItem(KEY) || (Date.now().toString(36)+rnd()+rnd());
|
||||
|
||||
local.setItem(KEY, MOID);
|
||||
|
||||
const STATES = [
|
||||
"request not initialized", // 0
|
||||
"server connection established", // 1
|
||||
"request recieved", // 2
|
||||
"processing request", // 3
|
||||
"request complete" // 4
|
||||
];
|
||||
|
||||
class Ajax {
|
||||
constructor(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;
|
||||
}
|
||||
|
||||
onStateChange() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request(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"] = MOID;
|
||||
for (let k in headers) {
|
||||
this.ajax.setRequestHeader(k, headers[k]);
|
||||
}
|
||||
if (send) {
|
||||
this.ajax.send(send);
|
||||
} else {
|
||||
this.ajax.send();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function call(url, handler) {
|
||||
return new Ajax(handler).request(url);
|
||||
}
|
||||
|
||||
function restore(id) {
|
||||
MOID = id;
|
||||
local.setItem(KEY, MOID);
|
||||
}
|
||||
|
||||
export const ajax = {
|
||||
new: (cb, rt) => new Ajax(cb, rt),
|
||||
call,
|
||||
restore,
|
||||
id: MOID
|
||||
};
|
||||
136
src2/moto/broker.js
Normal file
136
src2/moto/broker.js
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
class Broker {
|
||||
constructor() {
|
||||
this.topics = {};
|
||||
this.used = {};
|
||||
this.send = {};
|
||||
// using the pattern "broker.send(msg)" induces runtime errors
|
||||
// when there is not at least one registered listener for a topic
|
||||
// while also allowing for a more natural function call interface
|
||||
// and guarding against typos in topic names
|
||||
}
|
||||
|
||||
topics() {
|
||||
return Object.keys(this.topics);
|
||||
}
|
||||
|
||||
// create subscriptions for all functions in an object
|
||||
// a complement wrapObject on the subscription side
|
||||
listeners(object, root) {
|
||||
for (let [key, fn] of Object.entries(object)) {
|
||||
if (typeof fn === 'function') {
|
||||
key = root ? `${root}_${key}` : key;
|
||||
this.subscribe(key, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attach function to a topic
|
||||
// creates topic if new topic
|
||||
// creates a send function if new topic
|
||||
subscribe(topic, listener) {
|
||||
if (Array.isArray(topic)) {
|
||||
for (let t of topic) {
|
||||
this.subscribe(t, listener);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
if (typeof topic !== 'string') {
|
||||
console.trace({invalid_topic: topic});
|
||||
return;
|
||||
}
|
||||
let topics = this.topics;
|
||||
let channel = topics[topic];
|
||||
if (!channel) {
|
||||
channel = topics[topic] = [];
|
||||
}
|
||||
if (channel.indexOf(listener) < 0) {
|
||||
let send = this.send;
|
||||
let name = topic.replace(/[\\ \.-]/g, '_');
|
||||
send[name] = send[name] = this.bind(topic);
|
||||
channel.push(listener);
|
||||
this.publish(".topic.add", topic);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
unsubscribe(topic, listener) {
|
||||
let channel = this.topics[topic];
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
let index = channel.indexOf(listener);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
channel.splice(index,1);
|
||||
if (channel.length === 0) {
|
||||
delete this.topics[topic];
|
||||
this.publish(".topic.remove", topic);
|
||||
}
|
||||
}
|
||||
|
||||
publish(topic, message, options = {}) {
|
||||
if (Array.isArray(topic)) {
|
||||
for (let t of topic) {
|
||||
this.publish(t, message, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (topic !== ".topic.publish") {
|
||||
this.publish(".topic.publish", {topic, message, options});
|
||||
}
|
||||
// store last seen message on a topic
|
||||
// acts as a tracker for all used topics
|
||||
this.used[topic] = message;
|
||||
let channel = this.topics[topic];
|
||||
if (channel && channel.length) {
|
||||
for (let listener of channel) {
|
||||
try {
|
||||
listener(message, topic, options);
|
||||
} catch (e) {
|
||||
// console.log({listener_error: e, topic, options, listener});
|
||||
setTimeout(() => { throw e }, 1);
|
||||
}
|
||||
}
|
||||
} else if (topic.indexOf(".topic.") < 0) {
|
||||
if (self.dbug) {
|
||||
self.dbug.debug(`undelivered '${topic}'`, message);
|
||||
} else {
|
||||
console.log(`undelivered '${topic}'`, message);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
// return function bound to a topic for publishing
|
||||
bind(topic, message, options) {
|
||||
let broker = this;
|
||||
return function(msg, opt) {
|
||||
broker.publish(topic, message || msg, options || opt);
|
||||
}
|
||||
}
|
||||
|
||||
// return function wrapper that publishes a message to a topic
|
||||
// the published message is the return data from the function call
|
||||
wrap(topic, fn) {
|
||||
let broker = this;
|
||||
return function() {
|
||||
return broker.publish(topic, fn(...arguments));
|
||||
}
|
||||
}
|
||||
|
||||
// bind all functions in an object to a topic root
|
||||
// functions are replaced with wrappers that publish call results
|
||||
// a natural use case is wrapping api objects
|
||||
wrapObject(object, root) {
|
||||
for (let [key, fn] of Object.entries(object)) {
|
||||
if (typeof fn === 'function') {
|
||||
object[key] = this.wrap(`${root}_${key}`, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const broker = new Broker();
|
||||
253
src2/moto/client.js
Normal file
253
src2/moto/client.js
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { broker } from './broker.js';
|
||||
|
||||
let ccvalue = self.navigator ? navigator.hardwareConcurrency || 1 : 1,
|
||||
ccmax = ccvalue > 3 ? ccvalue - 1 : 1,
|
||||
workcc = false, // concurrent or not
|
||||
workurl = null, // url to load worker
|
||||
worknum = 1, // number of workers to start
|
||||
workers = [], // array of all workers
|
||||
queue = [], // array of work requests
|
||||
topics = {}; // topic handler (broker lite for worker > client msgs)
|
||||
|
||||
function dispatch(topic, message) {
|
||||
for (let fn of topics[topic] || []) {
|
||||
fn(message);
|
||||
}
|
||||
}
|
||||
|
||||
// code is running in the browser / client context
|
||||
const client = {
|
||||
/** @returns [integer] total number of workers **/
|
||||
live: () => {
|
||||
return workers.length;
|
||||
},
|
||||
|
||||
/** @returns [integer] number of idle workers **/
|
||||
free: () => {
|
||||
return workers.filter(w => w.run === null).length;
|
||||
},
|
||||
|
||||
/** @returns [integer] max cpu cores available **/
|
||||
max: () => {
|
||||
return ccmax;
|
||||
},
|
||||
|
||||
/** process worker topic messages like "ready" **/
|
||||
on: (topic, fn) => {
|
||||
(topics[topic] = topics[topic] || []).addOnce(fn);
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object} registered worker function map
|
||||
* auto-filled when worker process uses bind() or bindObject()
|
||||
**/
|
||||
fn: {},
|
||||
|
||||
/** binds a function endpoint to a task name **/
|
||||
bind: (task) => {
|
||||
if (!client.fn[task]) {
|
||||
client.fn[task] = (data, options) => {
|
||||
return client.call(task, data, options);
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/** add a promised task call to the work queue **/
|
||||
call: (task, data, options = {}) => {
|
||||
let { direct, stream } = options;
|
||||
return new Promise((resolve, reject) => {
|
||||
let ondata, ondone, onerror = reject;
|
||||
if (!stream) {
|
||||
let accum = [];
|
||||
let simple = options.simple !== false;
|
||||
ondata = (data) => {
|
||||
accum.push(data);
|
||||
};
|
||||
ondone = () => {
|
||||
resolve(simple && accum.length === 1 ? accum[0] : accum);
|
||||
};
|
||||
} else {
|
||||
ondata = stream;
|
||||
ondone = resolve;
|
||||
}
|
||||
client.queue(task, data, { direct, ondata, ondone, onerror });
|
||||
});
|
||||
},
|
||||
|
||||
/** danger: directly add an entry to the work queue **/
|
||||
queue: (task, data, options = {}) => {
|
||||
queue.push({task, data, options});
|
||||
client.kick();
|
||||
},
|
||||
|
||||
/** check work queue and distribute tasks to free workers **/
|
||||
kick: () => {
|
||||
if (queue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let rec, worker;
|
||||
outer: for (let qrec of queue) {
|
||||
let { options } = qrec;
|
||||
let { index } = options;
|
||||
if (index >= 0) {
|
||||
// worker designated by index
|
||||
let mod = index % workers.length;
|
||||
if (workers[mod].run === null) {
|
||||
worker = workers[mod];
|
||||
rec = qrec;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// first available worker
|
||||
for (let i=0; i<workers.length; i++) {
|
||||
if (workers[i].run === null) {
|
||||
worker = workers[i];
|
||||
rec = qrec;
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!rec) {
|
||||
// nothing runnable
|
||||
return;
|
||||
}
|
||||
|
||||
// remove queue item
|
||||
let qindex = queue.indexOf(rec);
|
||||
if (qindex < 0) {
|
||||
throw "invalid queue record";
|
||||
}
|
||||
queue.splice(qindex,1);
|
||||
|
||||
let { task, data, options } = rec;
|
||||
let { direct, ondone, ondata, onerror } = options;
|
||||
|
||||
if (ondone || ondata) {
|
||||
worker.run = { ondone, ondata, onerror };
|
||||
}
|
||||
|
||||
let msg = {
|
||||
task,
|
||||
data,
|
||||
stream: ondata ? true : false
|
||||
};
|
||||
try {
|
||||
worker.postMessage(msg, direct);
|
||||
} catch (error) {
|
||||
console.trace('work send error', {data, error});
|
||||
}
|
||||
},
|
||||
|
||||
// factory can be overridden
|
||||
spawn: function(url = workurl) {
|
||||
if (self.createWorker) {
|
||||
return self.createWorker();
|
||||
} else {
|
||||
return new Worker(workurl = url);
|
||||
}
|
||||
},
|
||||
|
||||
// return number of running requests
|
||||
running: function() {
|
||||
return workers.filter(w => w.run).length;
|
||||
},
|
||||
|
||||
stop: function() {
|
||||
for (let w of workers) {
|
||||
w.terminate();
|
||||
}
|
||||
workers = [];
|
||||
},
|
||||
|
||||
// same as restart
|
||||
start: function(url = workurl, cc = workcc || 1) {
|
||||
workcc = cc;
|
||||
worknum = Math.min(ccmax, workcc);
|
||||
workurl = url;
|
||||
client.restart(url, cc);
|
||||
},
|
||||
|
||||
restart: function() {
|
||||
client.stop();
|
||||
|
||||
// send cancel/error notification to requestors
|
||||
for (let w of workers.filter(w => w.run)) {
|
||||
if (w.run.ondone) {
|
||||
w.run.ondone({error: "cancelled operation"});
|
||||
}
|
||||
}
|
||||
|
||||
for (let i=0; i<worknum; i++) {
|
||||
let worker = client.spawn(workurl);
|
||||
worker.run = null;
|
||||
worker.index = i;
|
||||
worker.onmessage = function(e) {
|
||||
let reply = e.data;
|
||||
|
||||
// handle special case for task binding
|
||||
if (reply.bind) {
|
||||
client.bind(reply.bind);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle special case when worker ready
|
||||
if (reply.ready) {
|
||||
dispatch('ready', reply.ready);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle special case for broker publish
|
||||
if (reply.publish) {
|
||||
broker.publish(reply.publish, reply.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (worker.run === null) {
|
||||
console.log({client_unhandled: e});
|
||||
throw "message on idle worker";
|
||||
}
|
||||
|
||||
let { done, data, error } = reply;
|
||||
let { ondone, ondata, onerror } = worker.run;
|
||||
let handled = false;
|
||||
|
||||
if (error) {
|
||||
onerror(error);
|
||||
worker.run = null;
|
||||
handled = true;
|
||||
client.kick();
|
||||
}
|
||||
|
||||
if (ondata && data) {
|
||||
ondata(data);
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (done) {
|
||||
ondone();
|
||||
worker.run = null;
|
||||
handled = true;
|
||||
client.kick();
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
console.log({client_unhandled: e});
|
||||
throw "client unhandled reply";
|
||||
}
|
||||
};
|
||||
|
||||
workers.push(worker);
|
||||
}
|
||||
|
||||
client.kick();
|
||||
}
|
||||
};
|
||||
|
||||
export { client };
|
||||
9
src2/moto/license.js
Normal file
9
src2/moto/license.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const is_self = typeof(self) !== 'undefined';
|
||||
|
||||
const terms = {
|
||||
COPYRIGHT: "Copyright (C) Stewart Allen <sa@grid.space> - All Rights Reserved",
|
||||
LICENSE: "See the license.md file included with the source distribution",
|
||||
VERSION: (is_self ? self : this).debug_version || "4.1.9"
|
||||
};
|
||||
|
||||
export const license = terms;
|
||||
642
src2/moto/orbit.js
Normal file
642
src2/moto/orbit.js
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
|
||||
const {
|
||||
Vector3,
|
||||
Vector2,
|
||||
Quaternion,
|
||||
EventDispatcher,
|
||||
MOUSE
|
||||
} = THREE;
|
||||
|
||||
/**
|
||||
* Adapted from THREE.OrbitControls
|
||||
*/
|
||||
class Orbit extends EventDispatcher {
|
||||
constructor(object, domElement, notify, slider) {
|
||||
super();
|
||||
|
||||
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 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
|
||||
};
|
||||
|
||||
// default mouse buttons
|
||||
this.mouseDefault = {
|
||||
ORBIT: MOUSE.LEFT,
|
||||
ZOOM: MOUSE.MIDDLE,
|
||||
PAN: MOUSE.RIGHT
|
||||
};
|
||||
|
||||
// Onshape
|
||||
this.mouseOnshape = {
|
||||
ORBIT: MOUSE.RIGHT,
|
||||
ZOOM: MOUSE.LEFT,
|
||||
PAN: MOUSE.MIDDLE
|
||||
};
|
||||
|
||||
this.mouseButtons = this.mouseDefault;
|
||||
|
||||
this.setMouse = function(bindings) {
|
||||
this.mouseButtons = bindings;
|
||||
};
|
||||
|
||||
let scope = this,
|
||||
domEl = scope.domElement,
|
||||
EPS = 0.000001,
|
||||
rotateStart = new Vector2(),
|
||||
rotateEnd = new Vector2(),
|
||||
rotateDelta = new Vector2(),
|
||||
panStart = new Vector2(),
|
||||
panEnd = new Vector2(),
|
||||
panDelta = new Vector2(),
|
||||
panOffset = new Vector3(),
|
||||
offset = new Vector3(),
|
||||
dollyStart = new Vector2(),
|
||||
dollyEnd = new Vector2(),
|
||||
dollyDelta = new Vector2(),
|
||||
theta,
|
||||
thetaDelta = 0,
|
||||
thetaSet = null,
|
||||
phi,
|
||||
phiDelta = 0,
|
||||
phiSet = null,
|
||||
scale = 1,
|
||||
scaleSave = 1,
|
||||
pan = new Vector3(),
|
||||
lastPosition = new Vector3(),
|
||||
lastQuaternion = new Quaternion(),
|
||||
// so camera.up is the orbit axis
|
||||
quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)),
|
||||
quatInverse = quat.clone().invert(),
|
||||
// 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 distance in world space to move backward
|
||||
this.panBack = function (distance) {
|
||||
let te = this.object.matrix.elements;
|
||||
|
||||
// get Y column of matrix
|
||||
panOffset.set(te[ 8 ], te[ 9 ], te[ 10 ]);
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
this.getTarget = function() {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
this.setTarget = function(t) {
|
||||
this.target.copy(t);
|
||||
};
|
||||
|
||||
function isValue(v) {
|
||||
return v !== undefined && v !== null;
|
||||
}
|
||||
|
||||
function firstValue(choices) {
|
||||
for (let v of choices) {
|
||||
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 (factor = 1) {
|
||||
let dollyScale = getZoomScale(factor);
|
||||
scale = scale * dollyScale;
|
||||
};
|
||||
|
||||
this.dollyOut = function (factor = 1) {
|
||||
let dollyScale = getZoomScale(factor);
|
||||
scale = 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(factor = 1) {
|
||||
return Math.pow(0.95, factor);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
slider(null);
|
||||
|
||||
let delta = -(event.deltaY || event.wheelDelta || event.detail || 0);
|
||||
|
||||
if (delta === 0) return;
|
||||
|
||||
if (scope.reverseZoom) {
|
||||
delta = -delta;
|
||||
}
|
||||
|
||||
if (delta > 0) {
|
||||
scope.dollyOut();
|
||||
} else if (delta < 0) {
|
||||
scope.dollyIn();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const touches = [...event.touches];
|
||||
|
||||
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
|
||||
case 4: // four-fingered touch: pan
|
||||
if (scope.noPan === true) return;
|
||||
|
||||
state = STATE.TOUCH_PAN;
|
||||
const px = touches.map(t => t.pageX).reduce((a,b) => a+b) / touches.length;
|
||||
const py = touches.map(t => t.pageY).reduce((a,b) => a+b) / touches.length;
|
||||
|
||||
panStart.set(px, py);
|
||||
break;
|
||||
|
||||
default:
|
||||
state = STATE.NONE;
|
||||
}
|
||||
scope.dispatchEvent(startEvent);
|
||||
}
|
||||
|
||||
function touchmove(event) {
|
||||
if (scope.enabled === false) return;
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const 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(dy/dx) < 1 && slider && dx < 200)) {
|
||||
touchSlide = true;
|
||||
slider(lastDY ? (lastDY - ddy) : ddy);
|
||||
lastDY = ddy;
|
||||
} else {
|
||||
if (dollyDelta.y > 0) {
|
||||
scope.dollyOut(0.5);
|
||||
} else if (dollyDelta.y < 0) {
|
||||
scope.dollyIn(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
dollyStart.copy(dollyEnd);
|
||||
scope.update();
|
||||
break;
|
||||
|
||||
case 3: // three-fingered touch: pan
|
||||
case 4: // four-fingered touch: pan
|
||||
if (scope.noPan === true) return;
|
||||
if (state !== STATE.TOUCH_PAN) return;
|
||||
|
||||
const tlist = [...event.touches];
|
||||
const px = tlist.map(t => t.pageX).reduce((a,b) => a+b) / tlist.length;
|
||||
const py = tlist.map(t => t.pageY).reduce((a,b) => a+b) / tlist.length;
|
||||
|
||||
panEnd.set(px, py);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
export { Orbit };
|
||||
159
src2/moto/service.js
Normal file
159
src2/moto/service.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
// code for a ServiceWorker that provides support for running cached/offline
|
||||
|
||||
import { license } from './license.js';
|
||||
|
||||
const version = license.version;
|
||||
const origin = self.location.origin;
|
||||
const stats = {
|
||||
hit: 0,
|
||||
miss: 0,
|
||||
fetch: 0,
|
||||
cache: 0,
|
||||
bypass: 0,
|
||||
preload: 0,
|
||||
timer: undefined
|
||||
};
|
||||
|
||||
function debug() {
|
||||
console.log(`[${version}]`, ...arguments);
|
||||
}
|
||||
|
||||
function update() {
|
||||
clearTimeout(stats.timer);
|
||||
stats.timer = setTimeout(report, 1000);
|
||||
}
|
||||
|
||||
function report() {
|
||||
console.log('sw cache', Object.assign({}, stats));
|
||||
stats.hit = stats.miss = stats.fetch = stats.cache = stats.bypass = stats.preload = 0;
|
||||
}
|
||||
|
||||
debug('sw entry point');
|
||||
|
||||
const addResourcesToCache = async (resources) => {
|
||||
// debug('cache resources', resources);
|
||||
const cache = await caches.open('v'+version);
|
||||
await cache.addAll(resources);
|
||||
};
|
||||
|
||||
const putInCache = async (request, response) => {
|
||||
const cache = await caches.open('v'+version);
|
||||
// debug('cache', request.url);
|
||||
update(stats.cache++);
|
||||
await cache.put(request, response);
|
||||
};
|
||||
|
||||
const cacheFirst = async ({
|
||||
request,
|
||||
preloadResponsePromise,
|
||||
fallbackUrl
|
||||
}) => {
|
||||
// must await this promise when present to avoid errors
|
||||
const preloadResponse = preloadResponsePromise ? await preloadResponsePromise : undefined;
|
||||
|
||||
// try to get the resource from the cache
|
||||
const cache = await caches.open('v'+version);
|
||||
const responseFromCache = await cache.match(request);
|
||||
|
||||
if (responseFromCache) {
|
||||
// debug('cached', request.url);
|
||||
update(stats.hit++);
|
||||
return responseFromCache;
|
||||
} else {
|
||||
// debug('miss', request.url);
|
||||
update(stats.miss++);
|
||||
}
|
||||
|
||||
// try to use the preloaded response, if it's there
|
||||
if (preloadResponse) {
|
||||
update(stats.preload++);
|
||||
debug('using preload response', preloadResponse);
|
||||
putInCache(request, preloadResponse.clone());
|
||||
return preloadResponse;
|
||||
}
|
||||
|
||||
// try to get the resource from the network
|
||||
try {
|
||||
// debug('fetch', request.url);
|
||||
update(fetch.fetch++);
|
||||
const responseFromNetwork = await fetch(request);
|
||||
// response may be used only once
|
||||
// we need to save clone to put one copy in cache and serve second one
|
||||
putInCache(request, responseFromNetwork.clone());
|
||||
return responseFromNetwork;
|
||||
} catch (error) {
|
||||
debug({fallback_error: error});
|
||||
const cache = await caches.open('v'+version);
|
||||
const fallbackResponse = await cache.match(fallbackUrl);
|
||||
if (fallbackResponse) {
|
||||
return fallbackResponse;
|
||||
}
|
||||
// when even the fallback response is not available,
|
||||
// there is nothing we can do, but we must always
|
||||
// return a Response object
|
||||
return new Response('Network error happened', {
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
status: 408
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const enableNavigationPreload = async () => {
|
||||
if (self.registration.navigationPreload) {
|
||||
// Enable navigation preloads!
|
||||
// debug('sw preload');
|
||||
try {
|
||||
await self.registration.navigationPreload.enable();
|
||||
} catch (e) {
|
||||
debug('sw preload fail', e);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
const vkey = 'v'+version;
|
||||
// debug('sw activate');
|
||||
// event.waitUntil(enableNavigationPreload());
|
||||
// cleanup old caches
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then( keylist => keylist
|
||||
.filter(key => key != vkey)
|
||||
.map(key => { debug('sw delete cache', key); return caches.delete(key); }) )
|
||||
.then( deletes => Promise.all(deletes) )
|
||||
);
|
||||
event.waitUntil(new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, 5000);
|
||||
}));
|
||||
debug('sw activated');
|
||||
});
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
// debug('sw install');
|
||||
// event.waitUntil( addResourcesToCache([ ]) );
|
||||
self.skipWaiting();
|
||||
debug('sw installed');
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const { request,preloadResponse } = event;
|
||||
if (request.url.indexOf(origin) !== 0 || request.method !== 'GET') {
|
||||
debug('no-cache', request.url);
|
||||
stats.bypass++;
|
||||
event.respondWith(fetch(request));
|
||||
return;
|
||||
}
|
||||
|
||||
event.respondWith(
|
||||
cacheFirst({
|
||||
request: request,
|
||||
preloadResponsePromise: preloadResponse,
|
||||
fallbackUrl: '//static.grid.space/img/logo_gs.png',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
// debug({ onmessage: event, clients });
|
||||
});
|
||||
1513
src2/moto/space.js
Normal file
1513
src2/moto/space.js
Normal file
File diff suppressed because it is too large
Load diff
168
src2/moto/webui.js
Normal file
168
src2/moto/webui.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
|
||||
let nextid = 1;
|
||||
|
||||
function build(data, context) {
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
let html = [];
|
||||
for (let d of data) {
|
||||
html.appendAll(build(d, context));
|
||||
}
|
||||
return html;
|
||||
}
|
||||
let { type, attr, innr, raw } = data;
|
||||
if (raw) {
|
||||
return [ raw ];
|
||||
}
|
||||
// auto text content
|
||||
if (typeof attr === 'string') {
|
||||
attr = { _: attr };
|
||||
}
|
||||
let elid;
|
||||
let html = [];
|
||||
let text
|
||||
html.push(`<${type}`);
|
||||
let func = {};
|
||||
for (let [key, val] of Object.entries(attr || {})) {
|
||||
if (val === undefined) {
|
||||
continue;
|
||||
}
|
||||
let tov = typeof val;
|
||||
if (key === '_') {
|
||||
text = val;
|
||||
} else if (key.startsWith('_')) {
|
||||
if (val) html.push(` ${key.substring(1)}`);
|
||||
} else if (key === 'id') {
|
||||
elid = val ? (tov === 'object' ? val.join('_') : val) : undefined;
|
||||
} else if (tov === 'function') {
|
||||
func[key] = val;
|
||||
elid = elid || `_${nextid++}`;
|
||||
} else if (tov === 'object') {
|
||||
if (val.length)
|
||||
html.push(` ${key}="${val.filter(v => v !== undefined).join(' ')}"`);
|
||||
} else if (tov !== 'undefined') {
|
||||
html.push(` ${key}="${val}"`);
|
||||
}
|
||||
}
|
||||
if (elid) {
|
||||
html.push(` id="${elid}"`);
|
||||
context.push({elid, func});
|
||||
}
|
||||
html.push('>');
|
||||
if (innr) {
|
||||
let snips = [];
|
||||
if (innr.length) {
|
||||
for (let i of innr) {
|
||||
snips.push(build(i, context));
|
||||
}
|
||||
} else {
|
||||
snips.push(build(innr, context));
|
||||
}
|
||||
html.appendAll(snips.flat());
|
||||
}
|
||||
if (text !== undefined) {
|
||||
html.push(text);
|
||||
}
|
||||
html.push(`</${type}>`);
|
||||
return html;
|
||||
}
|
||||
|
||||
// core html builder funtions
|
||||
const h = {
|
||||
bind: (el, data, opt = {}) => {
|
||||
let ctx = [];
|
||||
let html = build(data, ctx).join('');
|
||||
if (opt.after || opt.before) {
|
||||
let tmpl = document.createElement('template');
|
||||
tmpl.innerHTML = html;
|
||||
opt.before && el.before(tmpl.content);
|
||||
opt.after && el.after(tmpl.content);
|
||||
} else if (opt.append) {
|
||||
el.innerHTML += html;
|
||||
} else {
|
||||
el.innerHTML = html;
|
||||
}
|
||||
let map = {};
|
||||
for (let bind of ctx) {
|
||||
let { elid, func } = bind;
|
||||
let et = $(elid);
|
||||
map[elid] = et;
|
||||
for (let [name, fn] of Object.entries(func)) {
|
||||
et[name] = fn;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
},
|
||||
|
||||
el: (type, attr, innr) => {
|
||||
if (Array.isArray(attr)) {
|
||||
innr = attr;
|
||||
attr = {};
|
||||
}
|
||||
return { type, attr, innr };
|
||||
},
|
||||
|
||||
raw: ( text ) => {
|
||||
return { raw: text }
|
||||
},
|
||||
|
||||
build: (data) => {
|
||||
return build(data, []).join('');
|
||||
}
|
||||
};
|
||||
|
||||
// window ui helpers
|
||||
const $ = (id) => {
|
||||
return document.getElementById(id);
|
||||
};
|
||||
|
||||
const $d = (id, v) => {
|
||||
$(id).style.display = v;
|
||||
};
|
||||
|
||||
const $h = (id, h) => {
|
||||
$(id).innerHTML = h;
|
||||
};
|
||||
|
||||
const $c = (id, add, del) => {
|
||||
if (add) $(id).classList.add(add);
|
||||
if (del) $(id).classList.remove(del);
|
||||
};
|
||||
|
||||
const $C = (clazz, add, del) => {
|
||||
let el = [...document.getElementsByClassName(clazz)];
|
||||
for (let e of el) {
|
||||
if (add) e.classList.add(add);
|
||||
if (del) e.classList.remove(del);
|
||||
}
|
||||
return el;
|
||||
};
|
||||
|
||||
const estop = (evt) => {
|
||||
evt.stopPropagation();
|
||||
evt.preventDefault();
|
||||
};
|
||||
|
||||
// add common element types
|
||||
[
|
||||
"a", "i", "hr", "div", "pre", "code", "span", "label", "input",
|
||||
"button", "svg", "textarea", "select", "option", "img", "canvas"
|
||||
].forEach(type => {
|
||||
h[type] = (attr, innr) => {
|
||||
return h.el(type, attr, innr);
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
$,
|
||||
$d,
|
||||
$h,
|
||||
$c,
|
||||
$C,
|
||||
h,
|
||||
estop
|
||||
};
|
||||
98
src2/moto/worker.js
Normal file
98
src2/moto/worker.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
let endpoints = {};
|
||||
|
||||
// code is running in the worker / server context
|
||||
const dispatch = {
|
||||
// bind an endpoint name to a function
|
||||
bind(name, fn) {
|
||||
endpoints[name] = fn;
|
||||
dispatch.send({bind: name});
|
||||
},
|
||||
|
||||
// bind all functions in an object
|
||||
bindObject(object, root, recurse) {
|
||||
recurse = recurse || root === undefined;
|
||||
for (let [key, fn] of Object.entries(object)) {
|
||||
key = root ? `${root}_${key}` : key;
|
||||
if (typeof fn === 'function') {
|
||||
dispatch.bind(key, fn);
|
||||
} else if (recurse) {
|
||||
dispatch.bindObject(fn, key, recurse);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// allow workers to publish messages on the client side
|
||||
publish(topic, message) {
|
||||
dispatch.send({publish: topic, message});
|
||||
},
|
||||
|
||||
ready() {
|
||||
dispatch.send({ready: true});
|
||||
},
|
||||
|
||||
send(msg) {
|
||||
self.postMessage(msg);
|
||||
},
|
||||
|
||||
onmessage(e) {
|
||||
let msg = e.data,
|
||||
run = endpoints[msg.task],
|
||||
async = false,
|
||||
done = false,
|
||||
send = {
|
||||
// puts function in async mode
|
||||
// prevents auto send.done() on function return
|
||||
async() {
|
||||
async = true;
|
||||
},
|
||||
data(data, direct) {
|
||||
if (canSend(data)) {
|
||||
dispatch.send({ done: false, data: data }, direct);
|
||||
}
|
||||
},
|
||||
done(data, direct) {
|
||||
if (canSend(data)) {
|
||||
dispatch.send({ done: true, data: data }, direct);
|
||||
done = true;
|
||||
}
|
||||
},
|
||||
error(data) {
|
||||
if (canSend(data)) {
|
||||
done = true;
|
||||
dispatch.send({ error: data });
|
||||
}
|
||||
}
|
||||
},
|
||||
canSend = (data) => {
|
||||
if (done) {
|
||||
console.log("unexpected reply dropped", {data, msg});
|
||||
}
|
||||
return !done;
|
||||
};
|
||||
|
||||
if (run) {
|
||||
try {
|
||||
let data = run(msg.data, send);
|
||||
if (!done && (data || async === false)) {
|
||||
send.done(data);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.stack) {
|
||||
console.trace(error.stack);
|
||||
}
|
||||
send.error(error.toString());
|
||||
// dispatch.send({ data: {error: error.toString()}, done: true });
|
||||
}
|
||||
} else {
|
||||
console.log({worker_unhandled: e});
|
||||
send.error(`no registered endpoint: ${msg.task}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// attach onmessage hanler
|
||||
self.onmessage = dispatch.onmessage;
|
||||
|
||||
export const worker = dispatch;
|
||||
Loading…
Reference in a new issue