merge experimental void app
This commit is contained in:
parent
9e142fbe1e
commit
02fb3cd30d
80 changed files with 30437 additions and 813 deletions
|
|
@ -1,4 +1,4 @@
|
|||
const root = await navigator.storage.getDirectory();
|
||||
const root = await navigator.storage?.getDirectory();
|
||||
|
||||
function resolvePath(path) {
|
||||
if (!path) {
|
||||
|
|
|
|||
|
|
@ -85,6 +85,13 @@ class Orbit extends EventDispatcher {
|
|||
PAN: MOUSE.MIDDLE
|
||||
};
|
||||
|
||||
// Onshape
|
||||
this.mouseVoid = {
|
||||
ORBIT: MOUSE.RIGHT,
|
||||
// ZOOM: MOUSE.LEFT,
|
||||
PAN: MOUSE.MIDDLE
|
||||
};
|
||||
|
||||
this.mouseButtons = this.mouseDefault;
|
||||
|
||||
this.setMouse = function(bindings) {
|
||||
|
|
@ -116,6 +123,7 @@ class Orbit extends EventDispatcher {
|
|||
pan = new Vector3(),
|
||||
lastPosition = new Vector3(),
|
||||
lastQuaternion = new Quaternion(),
|
||||
lastZoom = object.zoom !== undefined ? object.zoom : 1,
|
||||
// so camera.up is the orbit axis
|
||||
quat = new Quaternion().setFromUnitVectors(object.up, new Vector3(0, 1, 0)),
|
||||
quatInverse = quat.clone().invert(),
|
||||
|
|
@ -325,11 +333,13 @@ class Orbit extends EventDispatcher {
|
|||
// 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) {
|
||||
|| 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS
|
||||
|| Math.abs(lastZoom - this.object.zoom) > EPS) {
|
||||
|
||||
this.dispatchEvent(changeEvent);
|
||||
lastPosition.copy(this.object.position);
|
||||
lastQuaternion.copy(this.object.quaternion);
|
||||
lastZoom = this.object.zoom;
|
||||
if (notify) notify(position, true);
|
||||
} else {
|
||||
if (notify) notify(position, false);
|
||||
|
|
@ -681,7 +691,23 @@ class Orbit extends EventDispatcher {
|
|||
|
||||
this.onMouseUp = onMouseUp;
|
||||
|
||||
domEl.addEventListener('contextmenu', function (event) { event.preventDefault() }, false);
|
||||
this.dispose = function() {
|
||||
domEl.removeEventListener('contextmenu', onContextMenu, false);
|
||||
domEl.removeEventListener('mousedown', onMouseDown, false);
|
||||
domEl.removeEventListener('wheel', onMouseWheel, false);
|
||||
domEl.removeEventListener('mousewheel', onMouseWheel, false);
|
||||
domEl.removeEventListener('DOMMouseScroll', onMouseWheel, false);
|
||||
domEl.removeEventListener('touchstart', touchstart, false);
|
||||
domEl.removeEventListener('touchend', touchend, false);
|
||||
domEl.removeEventListener('touchmove', touchmove, false);
|
||||
|
||||
document.removeEventListener('mousemove', onMouseMove, false);
|
||||
document.removeEventListener('mouseup', onMouseUp, false);
|
||||
window.removeEventListener('keydown', onKeyDown, false);
|
||||
};
|
||||
|
||||
function onContextMenu(event) { event.preventDefault() }
|
||||
domEl.addEventListener('contextmenu', onContextMenu, false);
|
||||
domEl.addEventListener('mousedown', onMouseDown, false);
|
||||
domEl.addEventListener('wheel', onMouseWheel, false); // Modern standard (Chrome, Safari, Firefox)
|
||||
domEl.addEventListener('mousewheel', onMouseWheel, false); // Legacy Chrome/Safari
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
377
src/moto/trackball.js
Normal file
377
src/moto/trackball.js
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
/** Copyright Stewart Allen <sa@grid.space> -- All Rights Reserved */
|
||||
|
||||
"use strict";
|
||||
|
||||
import { THREE } from '../ext/three.js';
|
||||
import { TrackballControls } from '../ext/three.js';
|
||||
|
||||
const { MOUSE, Vector3 } = THREE;
|
||||
const BUTTON = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
|
||||
const ACTION = { ROTATE: 0, DOLLY: 1, PAN: 2 };
|
||||
const EPS = 1e-6;
|
||||
const VOID_ROTATE_SPEED = 36.0;
|
||||
const VOID_PAN_SPEED_PERSPECTIVE = 1.0;
|
||||
const VOID_PAN_SPEED_ORTHO = 2.4;
|
||||
const VOID_ZOOM_SPEED_PERSPECTIVE_MULT = 1.35;
|
||||
const VOID_ZOOM_SPEED_ORTHO_MULT = 2.2;
|
||||
|
||||
class Trackball {
|
||||
constructor(object, domElement, notify, slider) {
|
||||
this.object = object;
|
||||
this.domElement = domElement !== undefined ? domElement : document;
|
||||
|
||||
this.control = new TrackballControls(object, this.domElement);
|
||||
this.control.staticMoving = true;
|
||||
this.control.dynamicDampingFactor = 0;
|
||||
this.control.rotateSpeed = VOID_ROTATE_SPEED;
|
||||
this.control.zoomSpeed = 2.0;
|
||||
this.control.panSpeed = VOID_PAN_SPEED_PERSPECTIVE;
|
||||
|
||||
this.target = this.control.target;
|
||||
this.center = this.target;
|
||||
|
||||
this.mouseDefault = {
|
||||
ORBIT: MOUSE.LEFT,
|
||||
ZOOM: MOUSE.MIDDLE,
|
||||
PAN: MOUSE.RIGHT
|
||||
};
|
||||
this.mouseOnshape = {
|
||||
ORBIT: MOUSE.RIGHT,
|
||||
ZOOM: MOUSE.LEFT,
|
||||
PAN: MOUSE.MIDDLE
|
||||
};
|
||||
this.mouseVoid = {
|
||||
ORBIT: MOUSE.RIGHT,
|
||||
PAN: MOUSE.MIDDLE
|
||||
};
|
||||
this.mouseButtons = this.mouseDefault;
|
||||
|
||||
this.orbitPivotOnRight = false;
|
||||
this.continuousRotate = true;
|
||||
this.reverseZoom = false;
|
||||
this.zoomSpeed = 1.0;
|
||||
this._keysDisabled = false;
|
||||
|
||||
this.isTrackballAdapter = true;
|
||||
|
||||
const mapButtons = () => {
|
||||
const actions = {
|
||||
LEFT: -1,
|
||||
MIDDLE: -1,
|
||||
RIGHT: -1
|
||||
};
|
||||
const bind = this.mouseButtons || this.mouseDefault;
|
||||
const buttonToAction = Object.create(null);
|
||||
if (bind.ORBIT !== undefined) buttonToAction[bind.ORBIT] = ACTION.ROTATE;
|
||||
if (bind.ZOOM !== undefined) buttonToAction[bind.ZOOM] = ACTION.DOLLY;
|
||||
if (bind.PAN !== undefined) buttonToAction[bind.PAN] = ACTION.PAN;
|
||||
actions.LEFT = buttonToAction[BUTTON.LEFT] ?? actions.LEFT;
|
||||
actions.MIDDLE = buttonToAction[BUTTON.MIDDLE] ?? actions.MIDDLE;
|
||||
actions.RIGHT = buttonToAction[BUTTON.RIGHT] ?? actions.RIGHT;
|
||||
this.control.mouseButtons = actions;
|
||||
};
|
||||
|
||||
mapButtons();
|
||||
|
||||
const emitNotify = (moved) => {
|
||||
if (notify) notify(this.object.position, moved);
|
||||
};
|
||||
this._emitNotify = emitNotify;
|
||||
this._lastPosition = this.object.position.clone();
|
||||
this._lastQuaternion = this.object.quaternion.clone();
|
||||
this._lastZoom = this.object.zoom !== undefined ? this.object.zoom : 1;
|
||||
|
||||
// Keep Space idle-halo semantics identical to Orbit: any control change
|
||||
// is treated as active camera motion.
|
||||
this.control.addEventListener('change', () => {
|
||||
this._emitNotify?.(true);
|
||||
});
|
||||
|
||||
// Mirror Orbit semantics: always signal notify from update(), with moved=true/false.
|
||||
// Patch underlying control.update() so internal handlers also flow through this path.
|
||||
const rawUpdate = this.control.update.bind(this.control);
|
||||
this.control.update = (...args) => {
|
||||
rawUpdate(...args);
|
||||
const moved =
|
||||
this._lastPosition.distanceToSquared(this.object.position) > EPS
|
||||
|| 8 * (1 - this._lastQuaternion.dot(this.object.quaternion)) > EPS
|
||||
|| Math.abs((this.object.zoom || 1) - this._lastZoom) > EPS;
|
||||
this._emitNotify?.(moved);
|
||||
if (moved) {
|
||||
this._lastPosition.copy(this.object.position);
|
||||
this._lastQuaternion.copy(this.object.quaternion);
|
||||
this._lastZoom = this.object.zoom !== undefined ? this.object.zoom : 1;
|
||||
}
|
||||
};
|
||||
|
||||
this._animating = false;
|
||||
this._raf = null;
|
||||
this._tick = () => {
|
||||
if (!this._animating) return;
|
||||
if (this.control.enabled) {
|
||||
this.control.update();
|
||||
}
|
||||
this._raf = self.requestAnimationFrame(this._tick);
|
||||
};
|
||||
this._startTick = () => {
|
||||
if (this._animating) return;
|
||||
this._animating = true;
|
||||
this._tick();
|
||||
};
|
||||
this._stopTick = () => {
|
||||
this._animating = false;
|
||||
if (this._raf) {
|
||||
self.cancelAnimationFrame(this._raf);
|
||||
this._raf = null;
|
||||
}
|
||||
};
|
||||
|
||||
this._onPointerDown = (event) => {
|
||||
const b = event?.button;
|
||||
if (b === BUTTON.LEFT || b === BUTTON.MIDDLE || b === BUTTON.RIGHT) {
|
||||
this._startTick();
|
||||
}
|
||||
};
|
||||
this._onPointerUp = () => this._stopTick();
|
||||
this._onPointerCancel = () => this._stopTick();
|
||||
this._onWheel = () => {
|
||||
if (!this.control.enabled) return;
|
||||
// Run after Trackball's wheel handler mutates zoom deltas.
|
||||
self.requestAnimationFrame(() => {
|
||||
if (this.control.enabled) {
|
||||
this.control.update();
|
||||
}
|
||||
});
|
||||
};
|
||||
if (this.domElement?.addEventListener) {
|
||||
this.domElement.addEventListener('pointerdown', this._onPointerDown, true);
|
||||
this.domElement.addEventListener('wheel', this._onWheel, false);
|
||||
}
|
||||
if (this.domElement?.ownerDocument?.addEventListener) {
|
||||
this.domElement.ownerDocument.addEventListener('pointerup', this._onPointerUp, true);
|
||||
this.domElement.ownerDocument.addEventListener('pointercancel', this._onPointerCancel, true);
|
||||
}
|
||||
|
||||
Object.defineProperty(this, 'enabled', {
|
||||
get: () => this.control.enabled,
|
||||
set: (v) => { this.control.enabled = !!v; }
|
||||
});
|
||||
Object.defineProperty(this, 'noKeys', {
|
||||
get: () => !!this._keysDisabled,
|
||||
set: (v) => {
|
||||
const next = !!v;
|
||||
if (next === this._keysDisabled) return;
|
||||
this._keysDisabled = next;
|
||||
if (typeof window !== 'undefined') {
|
||||
if (next) {
|
||||
window.removeEventListener('keydown', this.control._onKeyDown);
|
||||
window.removeEventListener('keyup', this.control._onKeyUp);
|
||||
} else {
|
||||
window.addEventListener('keydown', this.control._onKeyDown);
|
||||
window.addEventListener('keyup', this.control._onKeyUp);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Object.defineProperty(this, 'minDistance', {
|
||||
get: () => this.control.minDistance,
|
||||
set: (v) => { this.control.minDistance = v; }
|
||||
});
|
||||
Object.defineProperty(this, 'maxDistance', {
|
||||
get: () => this.control.maxDistance,
|
||||
set: (v) => { this.control.maxDistance = v; }
|
||||
});
|
||||
}
|
||||
|
||||
setMouse(bindings) {
|
||||
this.mouseButtons = bindings || this.mouseDefault;
|
||||
const bind = this.mouseButtons || this.mouseDefault;
|
||||
const actions = {
|
||||
LEFT: -1,
|
||||
MIDDLE: -1,
|
||||
RIGHT: -1
|
||||
};
|
||||
const buttonToAction = Object.create(null);
|
||||
if (bind.ORBIT !== undefined) buttonToAction[bind.ORBIT] = ACTION.ROTATE;
|
||||
if (bind.ZOOM !== undefined) buttonToAction[bind.ZOOM] = ACTION.DOLLY;
|
||||
if (bind.PAN !== undefined) buttonToAction[bind.PAN] = ACTION.PAN;
|
||||
actions.LEFT = buttonToAction[BUTTON.LEFT] ?? actions.LEFT;
|
||||
actions.MIDDLE = buttonToAction[BUTTON.MIDDLE] ?? actions.MIDDLE;
|
||||
actions.RIGHT = buttonToAction[BUTTON.RIGHT] ?? actions.RIGHT;
|
||||
this.control.mouseButtons = actions;
|
||||
}
|
||||
|
||||
setOrbitPivotOnRight(enabled) {
|
||||
this.orbitPivotOnRight = !!enabled;
|
||||
}
|
||||
|
||||
setContinuousRotate(enabled) {
|
||||
this.continuousRotate = !!enabled;
|
||||
}
|
||||
|
||||
setZoom(reverse, speed) {
|
||||
this.reverseZoom = !!reverse;
|
||||
this.zoomSpeed = speed || 1.0;
|
||||
const mult = this.object.isOrthographicCamera
|
||||
? VOID_ZOOM_SPEED_ORTHO_MULT
|
||||
: VOID_ZOOM_SPEED_PERSPECTIVE_MULT;
|
||||
this.control.zoomSpeed = this.zoomSpeed * mult;
|
||||
}
|
||||
|
||||
getTarget() {
|
||||
return this.control.target;
|
||||
}
|
||||
|
||||
setTarget(t) {
|
||||
this.control.target.copy(t);
|
||||
}
|
||||
|
||||
setPosition(set) {
|
||||
const t = this.control.target;
|
||||
if (set.panX !== undefined) t.x = set.panX;
|
||||
if (set.panY !== undefined) t.y = set.panY;
|
||||
if (set.panZ !== undefined) t.z = set.panZ;
|
||||
|
||||
const hasCamPos = Number.isFinite(set?.camX) && Number.isFinite(set?.camY) && Number.isFinite(set?.camZ);
|
||||
if (hasCamPos) {
|
||||
this.object.position.set(set.camX, set.camY, set.camZ);
|
||||
if (Number.isFinite(set?.upX) && Number.isFinite(set?.upY) && Number.isFinite(set?.upZ)) {
|
||||
const up = new Vector3(set.upX, set.upY, set.upZ);
|
||||
if (up.lengthSq() > 1e-12) this.object.up.copy(up.normalize());
|
||||
}
|
||||
this.object.lookAt(t);
|
||||
if (set.scale !== undefined && isFinite(set.scale) && set.scale > 0) {
|
||||
if (this.object.isPerspectiveCamera) {
|
||||
const eye = this.object.position.clone().sub(t).multiplyScalar(set.scale);
|
||||
this.object.position.copy(t).add(eye);
|
||||
} else if (this.object.isOrthographicCamera) {
|
||||
this.object.zoom = this.object.zoom / set.scale;
|
||||
this.object.updateProjectionMatrix();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let off = this.object.position.clone().sub(t);
|
||||
let radius = off.length();
|
||||
if (!isFinite(radius) || radius <= 0) radius = 1;
|
||||
|
||||
const left = set.left !== undefined ? set.left : Math.atan2(off.x, off.z);
|
||||
const up = set.up !== undefined ? set.up : Math.atan2(Math.sqrt(off.x * off.x + off.z * off.z), off.y);
|
||||
|
||||
off = new Vector3(
|
||||
radius * Math.sin(up) * Math.sin(left),
|
||||
radius * Math.cos(up),
|
||||
radius * Math.sin(up) * Math.cos(left)
|
||||
);
|
||||
|
||||
// Avoid singular lookAt matrices at poles by ensuring camera.up is not parallel to view direction.
|
||||
const viewDir = off.clone().normalize();
|
||||
let upVec = null;
|
||||
if (Number.isFinite(set?.upX) && Number.isFinite(set?.upY) && Number.isFinite(set?.upZ)) {
|
||||
upVec = new Vector3(set.upX, set.upY, set.upZ);
|
||||
} else {
|
||||
upVec = this.object.up.clone();
|
||||
}
|
||||
if (upVec.lengthSq() < 1e-12) upVec.set(0, 1, 0);
|
||||
upVec.projectOnPlane(viewDir);
|
||||
if (upVec.lengthSq() < 1e-8) upVec = new Vector3(0, 0, 1).projectOnPlane(viewDir);
|
||||
if (upVec.lengthSq() < 1e-8) upVec = new Vector3(1, 0, 0).projectOnPlane(viewDir);
|
||||
if (upVec.lengthSq() > 1e-12) this.object.up.copy(upVec.normalize());
|
||||
|
||||
this.object.position.copy(t).add(off);
|
||||
this.object.lookAt(t);
|
||||
|
||||
if (set.scale !== undefined && isFinite(set.scale) && set.scale > 0) {
|
||||
if (this.object.isPerspectiveCamera) {
|
||||
const eye = this.object.position.clone().sub(t).multiplyScalar(set.scale);
|
||||
this.object.position.copy(t).add(eye);
|
||||
} else if (this.object.isOrthographicCamera) {
|
||||
this.object.zoom = this.object.zoom / set.scale;
|
||||
this.object.updateProjectionMatrix();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPosition(scaled) {
|
||||
const t = this.control.target;
|
||||
const off = this.object.position.clone().sub(t);
|
||||
const left = Math.atan2(off.x, off.z);
|
||||
const up = Math.atan2(Math.sqrt(off.x * off.x + off.z * off.z), off.y);
|
||||
return {
|
||||
left,
|
||||
up,
|
||||
panX: t.x,
|
||||
panY: t.y,
|
||||
panZ: t.z,
|
||||
camX: this.object.position.x,
|
||||
camY: this.object.position.y,
|
||||
camZ: this.object.position.z,
|
||||
upX: this.object.up.x,
|
||||
upY: this.object.up.y,
|
||||
upZ: this.object.up.z,
|
||||
scale: scaled ? (this.object.isOrthographicCamera ? 1 / this.object.zoom : 1) : 1
|
||||
};
|
||||
}
|
||||
|
||||
update() {
|
||||
this.control.panSpeed = this.object.isOrthographicCamera
|
||||
? VOID_PAN_SPEED_ORTHO
|
||||
: VOID_PAN_SPEED_PERSPECTIVE;
|
||||
const zoomMult = this.object.isOrthographicCamera
|
||||
? VOID_ZOOM_SPEED_ORTHO_MULT
|
||||
: VOID_ZOOM_SPEED_PERSPECTIVE_MULT;
|
||||
this.control.zoomSpeed = this.zoomSpeed * zoomMult;
|
||||
this.control.update();
|
||||
}
|
||||
|
||||
addEventListener(type, listener) {
|
||||
this.control.addEventListener(type, listener);
|
||||
}
|
||||
|
||||
removeEventListener(type, listener) {
|
||||
this.control.removeEventListener(type, listener);
|
||||
}
|
||||
|
||||
dispatchEvent(event) {
|
||||
this.control.dispatchEvent(event);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.control.reset();
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
// TrackballControls manages pointer lifecycle internally.
|
||||
}
|
||||
|
||||
resetInputState() {
|
||||
// Clear any latched key/mouse state (e.g. if native prompt swallowed keyup/mouseup)
|
||||
// and ensure key listeners are restored according to noKeys policy.
|
||||
this.control.state = -1;
|
||||
this.control.keyState = -1;
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('keydown', this.control._onKeyDown);
|
||||
window.removeEventListener('keyup', this.control._onKeyUp);
|
||||
if (!this._keysDisabled) {
|
||||
window.addEventListener('keydown', this.control._onKeyDown);
|
||||
window.addEventListener('keyup', this.control._onKeyUp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._stopTick();
|
||||
if (this.domElement?.removeEventListener) {
|
||||
this.domElement.removeEventListener('pointerdown', this._onPointerDown, true);
|
||||
this.domElement.removeEventListener('wheel', this._onWheel, false);
|
||||
}
|
||||
if (this.domElement?.ownerDocument?.removeEventListener) {
|
||||
this.domElement.ownerDocument.removeEventListener('pointerup', this._onPointerUp, true);
|
||||
this.domElement.ownerDocument.removeEventListener('pointercancel', this._onPointerCancel, true);
|
||||
}
|
||||
this.control.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { Trackball };
|
||||
Loading…
Reference in a new issue