653 lines
22 KiB
JavaScript
653 lines
22 KiB
JavaScript
import * as THREE from 'three';
|
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
|
|
|
|
export const Z_SPACING = 3;
|
|
|
|
export function dbg(...args) {
|
|
try {
|
|
const fn = window?.go?.main?.App?.JSDebugLog;
|
|
if (!fn) return;
|
|
const msg = '[engine] ' + args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
|
|
fn(msg);
|
|
} catch (_) {}
|
|
}
|
|
|
|
export class FormerEngine {
|
|
constructor(container) {
|
|
this.container = container;
|
|
this.layers = [];
|
|
this.layerMeshes = [];
|
|
this.selectedLayerIndex = -1;
|
|
this.enclosureLayerIndex = -1;
|
|
this.trayLayerIndex = -1;
|
|
|
|
this.layerGroup = null;
|
|
this.arrowGroup = null;
|
|
this.elementGroup = null;
|
|
this.selectionOutline = null;
|
|
|
|
this._onLayerSelect = null;
|
|
this._onCutoutSelect = null;
|
|
this._onCutoutHover = null;
|
|
this._onPlacedCutoutSelect = null;
|
|
|
|
this._modes = {};
|
|
|
|
this._clickHandlers = [];
|
|
this._mouseMoveHandlers = [];
|
|
this._mouseDownHandlers = [];
|
|
this._mouseUpHandlers = [];
|
|
|
|
this._initScene();
|
|
this._initControls();
|
|
this._initGrid();
|
|
this._initRaycasting();
|
|
this._animate();
|
|
}
|
|
|
|
// ===== Plugin API =====
|
|
|
|
use(mode) {
|
|
this._modes[mode.name] = mode;
|
|
mode.install(this);
|
|
}
|
|
|
|
getMode(name) {
|
|
return this._modes[name];
|
|
}
|
|
|
|
registerClickHandler(handler) { this._clickHandlers.push(handler); }
|
|
unregisterClickHandler(handler) {
|
|
const i = this._clickHandlers.indexOf(handler);
|
|
if (i >= 0) this._clickHandlers.splice(i, 1);
|
|
}
|
|
|
|
registerMouseMoveHandler(handler) { this._mouseMoveHandlers.push(handler); }
|
|
unregisterMouseMoveHandler(handler) {
|
|
const i = this._mouseMoveHandlers.indexOf(handler);
|
|
if (i >= 0) this._mouseMoveHandlers.splice(i, 1);
|
|
}
|
|
|
|
registerMouseDownHandler(handler) { this._mouseDownHandlers.push(handler); }
|
|
unregisterMouseDownHandler(handler) {
|
|
const i = this._mouseDownHandlers.indexOf(handler);
|
|
if (i >= 0) this._mouseDownHandlers.splice(i, 1);
|
|
}
|
|
|
|
registerMouseUpHandler(handler) { this._mouseUpHandlers.push(handler); }
|
|
unregisterMouseUpHandler(handler) {
|
|
const i = this._mouseUpHandlers.indexOf(handler);
|
|
if (i >= 0) this._mouseUpHandlers.splice(i, 1);
|
|
}
|
|
|
|
// ===== Callbacks =====
|
|
|
|
onLayerSelect(cb) { this._onLayerSelect = cb; }
|
|
onCutoutSelect(cb) { this._onCutoutSelect = cb; }
|
|
onCutoutHover(cb) { this._onCutoutHover = cb; }
|
|
onPlacedCutoutSelect(cb) { this._onPlacedCutoutSelect = cb; }
|
|
|
|
// ===== Scene Setup =====
|
|
|
|
_initScene() {
|
|
this.scene = new THREE.Scene();
|
|
this.scene.background = new THREE.Color(0x000000);
|
|
|
|
const w = this.container.clientWidth;
|
|
const h = this.container.clientHeight;
|
|
this.camera = new THREE.PerspectiveCamera(45, w / h, 0.1, 50000);
|
|
this.camera.position.set(0, -60, 80);
|
|
this.camera.up.set(0, 0, 1);
|
|
this.camera.lookAt(0, 0, 0);
|
|
|
|
this.renderer = new THREE.WebGLRenderer({ antialias: true, logarithmicDepthBuffer: true });
|
|
this.renderer.setSize(w, h);
|
|
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
this.container.appendChild(this.renderer.domElement);
|
|
|
|
this._ambientLight = new THREE.AmbientLight(0xffffff, 0.9);
|
|
this.scene.add(this._ambientLight);
|
|
this._dirLight = new THREE.DirectionalLight(0xffffff, 0.3);
|
|
this._dirLight.position.set(50, -50, 100);
|
|
this.scene.add(this._dirLight);
|
|
|
|
this.layerGroup = new THREE.Group();
|
|
this.scene.add(this.layerGroup);
|
|
|
|
this.arrowGroup = new THREE.Group();
|
|
this.arrowGroup.visible = false;
|
|
this.scene.add(this.arrowGroup);
|
|
|
|
this.elementGroup = new THREE.Group();
|
|
this.elementGroup.visible = false;
|
|
this.scene.add(this.elementGroup);
|
|
|
|
this._resizeObserver = new ResizeObserver(() => this._onResize());
|
|
this._resizeObserver.observe(this.container);
|
|
}
|
|
|
|
_initControls() {
|
|
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
|
this.controls.enableDamping = true;
|
|
this.controls.dampingFactor = 0.1;
|
|
this.controls.enableZoom = true;
|
|
this.controls.mouseButtons = {
|
|
LEFT: THREE.MOUSE.ROTATE,
|
|
MIDDLE: THREE.MOUSE.PAN,
|
|
RIGHT: THREE.MOUSE.DOLLY
|
|
};
|
|
this.controls.target.set(0, 0, 0);
|
|
this.controls.update();
|
|
|
|
this.renderer.domElement.addEventListener('wheel', (e) => {
|
|
e.preventDefault();
|
|
|
|
if (this._traditionalControls) {
|
|
if (e.shiftKey && e.deltaX === 0) {
|
|
e.stopImmediatePropagation();
|
|
const dist = this.camera.position.distanceTo(this.controls.target);
|
|
const panSpeed = dist * 0.001;
|
|
const right = new THREE.Vector3().setFromMatrixColumn(this.camera.matrixWorld, 0);
|
|
const offset = new THREE.Vector3();
|
|
offset.addScaledVector(right, e.deltaY * panSpeed);
|
|
this.camera.position.add(offset);
|
|
this.controls.target.add(offset);
|
|
this.controls.update();
|
|
}
|
|
return;
|
|
}
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
if (e.ctrlKey || e.metaKey) {
|
|
const factor = 1 + e.deltaY * 0.01;
|
|
const dir = new THREE.Vector3().subVectors(this.camera.position, this.controls.target);
|
|
dir.multiplyScalar(factor);
|
|
this.camera.position.copy(this.controls.target).add(dir);
|
|
} else {
|
|
let dx = e.deltaX;
|
|
let dy = e.deltaY;
|
|
if (e.shiftKey && dx === 0) {
|
|
dx = dy;
|
|
dy = 0;
|
|
}
|
|
const dist = this.camera.position.distanceTo(this.controls.target);
|
|
const panSpeed = dist * 0.001;
|
|
const right = new THREE.Vector3().setFromMatrixColumn(this.camera.matrixWorld, 0);
|
|
const up = new THREE.Vector3().setFromMatrixColumn(this.camera.matrixWorld, 1);
|
|
const offset = new THREE.Vector3();
|
|
offset.addScaledVector(right, dx * panSpeed);
|
|
offset.addScaledVector(up, -dy * panSpeed);
|
|
this.camera.position.add(offset);
|
|
this.controls.target.add(offset);
|
|
}
|
|
this.controls.update();
|
|
}, { passive: false, capture: true });
|
|
|
|
this._ctrlDragging = false;
|
|
this._ctrlDragLastY = 0;
|
|
|
|
this.renderer.domElement.addEventListener('mousedown', (e) => {
|
|
if (e.button === 0 && (e.ctrlKey || e.metaKey)) {
|
|
this._ctrlDragging = true;
|
|
this._ctrlDragLastY = e.clientY;
|
|
e.preventDefault();
|
|
e.stopImmediatePropagation();
|
|
}
|
|
}, { capture: true });
|
|
|
|
const onCtrlDragMove = (e) => {
|
|
if (!this._ctrlDragging) return;
|
|
const dy = e.clientY - this._ctrlDragLastY;
|
|
this._ctrlDragLastY = e.clientY;
|
|
const factor = 1 + dy * 0.005;
|
|
const dir = new THREE.Vector3().subVectors(this.camera.position, this.controls.target);
|
|
dir.multiplyScalar(factor);
|
|
this.camera.position.copy(this.controls.target).add(dir);
|
|
this.controls.update();
|
|
};
|
|
|
|
const onCtrlDragUp = () => {
|
|
this._ctrlDragging = false;
|
|
};
|
|
|
|
window.addEventListener('mousemove', onCtrlDragMove);
|
|
window.addEventListener('mouseup', onCtrlDragUp);
|
|
this._ctrlDragCleanup = () => {
|
|
window.removeEventListener('mousemove', onCtrlDragMove);
|
|
window.removeEventListener('mouseup', onCtrlDragUp);
|
|
};
|
|
}
|
|
|
|
_initGrid() {
|
|
this.gridHelper = new THREE.GridHelper(2000, 100, 0x333333, 0x222222);
|
|
this.gridHelper.rotation.x = Math.PI / 2;
|
|
this.gridHelper.position.z = -0.5;
|
|
this.scene.add(this.gridHelper);
|
|
this.gridVisible = true;
|
|
}
|
|
|
|
toggleGrid() {
|
|
this.gridVisible = !this.gridVisible;
|
|
this.gridHelper.visible = this.gridVisible;
|
|
return this.gridVisible;
|
|
}
|
|
|
|
_initRaycasting() {
|
|
this.raycaster = new THREE.Raycaster();
|
|
this.mouse = new THREE.Vector2();
|
|
this._isDragging = false;
|
|
this._mouseDownPos = { x: 0, y: 0 };
|
|
|
|
const canvas = this.renderer.domElement;
|
|
|
|
canvas.addEventListener('mousedown', e => {
|
|
this._isDragging = false;
|
|
this._mouseDownPos = { x: e.clientX, y: e.clientY };
|
|
for (const h of this._mouseDownHandlers) h(e);
|
|
});
|
|
|
|
canvas.addEventListener('mousemove', e => {
|
|
const dx = e.clientX - this._mouseDownPos.x;
|
|
const dy = e.clientY - this._mouseDownPos.y;
|
|
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) this._isDragging = true;
|
|
for (const h of this._mouseMoveHandlers) h(e);
|
|
});
|
|
|
|
canvas.addEventListener('mouseup', e => {
|
|
for (const h of this._mouseUpHandlers) h(e);
|
|
});
|
|
|
|
canvas.addEventListener('click', e => {
|
|
if (this._isDragging) return;
|
|
this._updateMouse(e);
|
|
this.raycaster.setFromCamera(this.mouse, this.camera);
|
|
|
|
for (const h of this._clickHandlers) {
|
|
if (h(e)) return;
|
|
}
|
|
|
|
// Default: layer selection
|
|
const clickables = this.layerMeshes.filter((m, i) => m && this.layers[i]?.visible);
|
|
const hits = this.raycaster.intersectObjects(clickables, true);
|
|
if (hits.length > 0) {
|
|
let hitObj = hits[0].object;
|
|
let idx = this.layerMeshes.indexOf(hitObj);
|
|
if (idx < 0) {
|
|
hitObj.traverseAncestors(p => {
|
|
const ei = this.layerMeshes.indexOf(p);
|
|
if (ei >= 0) idx = ei;
|
|
});
|
|
}
|
|
if (idx >= 0) this.selectLayer(idx);
|
|
} else {
|
|
this.selectLayer(-1);
|
|
}
|
|
});
|
|
}
|
|
|
|
_updateMouse(e) {
|
|
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
}
|
|
|
|
_onResize() {
|
|
const w = this.container.clientWidth;
|
|
const h = this.container.clientHeight;
|
|
if (w === 0 || h === 0) return;
|
|
this.camera.aspect = w / h;
|
|
this.camera.updateProjectionMatrix();
|
|
this.renderer.setSize(w, h);
|
|
}
|
|
|
|
_animate() {
|
|
this._animId = requestAnimationFrame(() => this._animate());
|
|
this.controls.update();
|
|
this.renderer.render(this.scene, this.camera);
|
|
}
|
|
|
|
// ===== Layer Loading =====
|
|
|
|
async loadLayers(layers, imageUrls) {
|
|
this.layers = layers;
|
|
const loader = new THREE.TextureLoader();
|
|
|
|
while (this.layerGroup.children.length > 0) {
|
|
const child = this.layerGroup.children[0];
|
|
if (child.geometry) child.geometry.dispose();
|
|
if (child.material) {
|
|
if (child.material.map) child.material.map.dispose();
|
|
child.material.dispose();
|
|
}
|
|
this.layerGroup.remove(child);
|
|
}
|
|
this.layerMeshes = [];
|
|
this.enclosureLayerIndex = -1;
|
|
this.trayLayerIndex = -1;
|
|
|
|
// Reset enclosure mode state if present
|
|
const enc = this.getMode('enclosure');
|
|
if (enc) {
|
|
enc.enclosureMesh = null;
|
|
enc.trayMesh = null;
|
|
}
|
|
|
|
let maxW = 0, maxH = 0;
|
|
|
|
for (let i = 0; i < layers.length; i++) {
|
|
const layer = layers[i];
|
|
const url = imageUrls[i];
|
|
|
|
if (layer.name === 'Enclosure') {
|
|
this.enclosureLayerIndex = i;
|
|
this.layerMeshes.push(null);
|
|
continue;
|
|
}
|
|
if (layer.name === 'Tray') {
|
|
this.trayLayerIndex = i;
|
|
this.layerMeshes.push(null);
|
|
continue;
|
|
}
|
|
|
|
if (!url) { this.layerMeshes.push(null); continue; }
|
|
|
|
try {
|
|
const tex = await new Promise((resolve, reject) => {
|
|
loader.load(url, resolve, undefined, reject);
|
|
});
|
|
tex.minFilter = THREE.LinearFilter;
|
|
tex.magFilter = THREE.LinearFilter;
|
|
|
|
const imgW = tex.image.width;
|
|
const imgH = tex.image.height;
|
|
if (imgW > maxW) maxW = imgW;
|
|
if (imgH > maxH) maxH = imgH;
|
|
|
|
const geo = new THREE.PlaneGeometry(imgW, imgH);
|
|
const mat = new THREE.MeshBasicMaterial({
|
|
map: tex,
|
|
transparent: true,
|
|
opacity: layer.visible ? layer.baseAlpha : 0,
|
|
side: THREE.DoubleSide,
|
|
depthWrite: false,
|
|
});
|
|
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
mesh.position.set(imgW / 2, -imgH / 2, i * Z_SPACING);
|
|
mesh.visible = layer.visible;
|
|
mesh.userData = { layerIndex: i };
|
|
|
|
this.layerGroup.add(mesh);
|
|
this.layerMeshes.push(mesh);
|
|
} catch (e) {
|
|
console.warn(`Failed to load layer ${i}:`, e);
|
|
this.layerMeshes.push(null);
|
|
}
|
|
}
|
|
|
|
this._maxW = maxW;
|
|
this._maxH = maxH;
|
|
|
|
if (maxW > 0 && maxH > 0) {
|
|
const cx = maxW / 2;
|
|
const cy = -maxH / 2;
|
|
const cz = (layers.length * Z_SPACING) / 2;
|
|
this.controls.target.set(cx, cy, cz);
|
|
const dist = Math.max(maxW, maxH) * 0.7;
|
|
this.camera.position.set(cx, cy - dist * 0.5, cz + dist * 0.6);
|
|
this.camera.lookAt(cx, cy, cz);
|
|
this.controls.update();
|
|
|
|
this.gridHelper.position.set(cx, cy, -0.5);
|
|
}
|
|
}
|
|
|
|
// ===== Layer Visibility =====
|
|
|
|
setLayerVisibility(index, visible) {
|
|
if (index < 0 || index >= this.layerMeshes.length) return;
|
|
const mesh = this.layerMeshes[index];
|
|
if (!mesh) return;
|
|
this.layers[index].visible = visible;
|
|
mesh.visible = visible;
|
|
if (mesh.material && !mesh.userData?.isEnclosure) {
|
|
if (!visible) mesh.material.opacity = 0;
|
|
else mesh.material.opacity = this.layers[index].baseAlpha;
|
|
}
|
|
}
|
|
|
|
setGroupOpacity(group, opacity) {
|
|
group.traverse(c => {
|
|
if (c.material) c.material.opacity = opacity;
|
|
});
|
|
}
|
|
|
|
setLayerHighlight(index, highlight) {
|
|
const hasHL = highlight && index >= 0;
|
|
this.layers.forEach((l, i) => {
|
|
l.highlight = (i === index && highlight);
|
|
const mesh = this.layerMeshes[i];
|
|
if (!mesh || !l.visible) return;
|
|
if (mesh.userData?.isEnclosure) {
|
|
this.setGroupOpacity(mesh, (hasHL && !l.highlight) ? 0.15 : 0.55);
|
|
} else if (mesh.material) {
|
|
mesh.material.opacity = (hasHL && !l.highlight) ? l.baseAlpha * 0.3 : l.baseAlpha;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ===== Selection =====
|
|
|
|
selectLayer(index) {
|
|
this.selectedLayerIndex = index;
|
|
|
|
if (this.selectionOutline) {
|
|
this.scene.remove(this.selectionOutline);
|
|
this.selectionOutline.geometry?.dispose();
|
|
this.selectionOutline.material?.dispose();
|
|
this.selectionOutline = null;
|
|
}
|
|
|
|
this.arrowGroup.visible = false;
|
|
while (this.arrowGroup.children.length) {
|
|
const c = this.arrowGroup.children[0];
|
|
this.arrowGroup.remove(c);
|
|
}
|
|
|
|
if (index < 0 || index >= this.layerMeshes.length) {
|
|
if (this._onLayerSelect) this._onLayerSelect(-1);
|
|
return;
|
|
}
|
|
|
|
const mesh = this.layerMeshes[index];
|
|
if (!mesh) {
|
|
if (this._onLayerSelect) this._onLayerSelect(index);
|
|
return;
|
|
}
|
|
|
|
if (mesh.geometry && !mesh.userData?.isEnclosure) {
|
|
const edges = new THREE.EdgesGeometry(mesh.geometry);
|
|
const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({
|
|
color: 0x89b4fa, linewidth: 2,
|
|
}));
|
|
line.position.copy(mesh.position);
|
|
line.position.z += 0.1;
|
|
this.selectionOutline = line;
|
|
this.scene.add(line);
|
|
}
|
|
|
|
const pos = mesh.position.clone();
|
|
if (mesh.userData?.isEnclosure) {
|
|
const box = new THREE.Box3().setFromObject(mesh);
|
|
box.getCenter(pos);
|
|
}
|
|
const arrowLen = 8;
|
|
const upArrow = new THREE.ArrowHelper(
|
|
new THREE.Vector3(0, 0, 1),
|
|
new THREE.Vector3(pos.x, pos.y, pos.z + 2),
|
|
arrowLen, 0x89b4fa, 3, 2
|
|
);
|
|
const downArrow = new THREE.ArrowHelper(
|
|
new THREE.Vector3(0, 0, -1),
|
|
new THREE.Vector3(pos.x, pos.y, pos.z - 2),
|
|
arrowLen, 0x89b4fa, 3, 2
|
|
);
|
|
this.arrowGroup.add(upArrow);
|
|
this.arrowGroup.add(downArrow);
|
|
this.arrowGroup.visible = true;
|
|
|
|
if (this._onLayerSelect) this._onLayerSelect(index);
|
|
}
|
|
|
|
moveSelectedZ(delta) {
|
|
if (this.selectedLayerIndex < 0) return;
|
|
const mesh = this.layerMeshes[this.selectedLayerIndex];
|
|
if (!mesh) return;
|
|
mesh.position.z += delta;
|
|
if (this.selectionOutline) this.selectionOutline.position.z = mesh.position.z + 0.1;
|
|
if (this.arrowGroup.children.length >= 2) {
|
|
const pos = mesh.position;
|
|
this.arrowGroup.children[0].position.set(pos.x, pos.y, pos.z + 2);
|
|
this.arrowGroup.children[1].position.set(pos.x, pos.y, pos.z - 2);
|
|
}
|
|
}
|
|
|
|
// ===== Camera =====
|
|
|
|
_homeTopDown(layerIndex) {
|
|
const mesh = this.layerMeshes[layerIndex];
|
|
if (!mesh) return;
|
|
const pos = mesh.position.clone();
|
|
if (mesh.userData?.isEnclosure) {
|
|
const box = new THREE.Box3().setFromObject(mesh);
|
|
box.getCenter(pos);
|
|
}
|
|
let imgW, imgH;
|
|
if (mesh.geometry?.parameters) {
|
|
imgW = mesh.geometry.parameters.width;
|
|
imgH = mesh.geometry.parameters.height;
|
|
} else {
|
|
imgW = this._maxW || 500;
|
|
imgH = this._maxH || 500;
|
|
}
|
|
const dist = Math.max(imgW, imgH) * 1.1;
|
|
this.camera.position.set(pos.x, pos.y - dist * 0.01, pos.z + dist);
|
|
this.camera.up.set(0, 0, 1);
|
|
this.controls.target.set(pos.x, pos.y, pos.z);
|
|
this.controls.update();
|
|
}
|
|
|
|
homeTopDown(layerIndex) {
|
|
if (layerIndex !== undefined && layerIndex >= 0) {
|
|
this._homeTopDown(layerIndex);
|
|
} else if (this.selectedLayerIndex >= 0) {
|
|
this._homeTopDown(this.selectedLayerIndex);
|
|
} else {
|
|
const cx = (this._maxW || 500) / 2;
|
|
const cy = -(this._maxH || 500) / 2;
|
|
const cz = (this.layers.length * Z_SPACING) / 2;
|
|
const dist = Math.max(this._maxW || 500, this._maxH || 500) * 1.1;
|
|
this.camera.position.set(cx, cy - dist * 0.01, cz + dist);
|
|
this.camera.up.set(0, 0, 1);
|
|
this.controls.target.set(cx, cy, cz);
|
|
this.controls.update();
|
|
}
|
|
}
|
|
|
|
resetView() {
|
|
if (this.layers.length === 0) return;
|
|
const maxW = this._maxW || 500;
|
|
const maxH = this._maxH || 500;
|
|
const cx = maxW / 2;
|
|
const cy = -maxH / 2;
|
|
const cz = (this.layers.length * Z_SPACING) / 2;
|
|
const dist = Math.max(maxW, maxH) * 0.7;
|
|
this.camera.position.set(cx, cy - dist * 0.5, cz + dist * 0.6);
|
|
this.camera.up.set(0, 0, 1);
|
|
this.controls.target.set(cx, cy, cz);
|
|
this.controls.update();
|
|
}
|
|
|
|
setControlScheme(traditional) {
|
|
this._traditionalControls = traditional;
|
|
if (traditional) {
|
|
this.controls.mouseButtons = {
|
|
LEFT: THREE.MOUSE.ROTATE,
|
|
MIDDLE: THREE.MOUSE.PAN,
|
|
RIGHT: THREE.MOUSE.PAN
|
|
};
|
|
} else {
|
|
this.controls.mouseButtons = {
|
|
LEFT: THREE.MOUSE.ROTATE,
|
|
MIDDLE: THREE.MOUSE.PAN,
|
|
RIGHT: THREE.MOUSE.DOLLY
|
|
};
|
|
}
|
|
}
|
|
|
|
// ===== Shared Utilities =====
|
|
|
|
disposeGroup(group) {
|
|
if (!group) return;
|
|
if (group.parent) group.parent.remove(group);
|
|
group.traverse(c => {
|
|
if (c.geometry) c.geometry.dispose();
|
|
if (c.material) {
|
|
if (c.material.map) c.material.map.dispose();
|
|
c.material.dispose();
|
|
}
|
|
});
|
|
}
|
|
|
|
loadSTLCentered(arrayBuffer, materialOpts) {
|
|
const loader = new STLLoader();
|
|
const geometry = loader.parse(arrayBuffer);
|
|
geometry.computeBoundingBox();
|
|
if (materialOpts.computeNormals) {
|
|
geometry.computeVertexNormals();
|
|
delete materialOpts.computeNormals;
|
|
}
|
|
const mat = new THREE.MeshPhongMaterial(materialOpts);
|
|
const mesh = new THREE.Mesh(geometry, mat);
|
|
const box = geometry.boundingBox;
|
|
const center = new THREE.Vector3(
|
|
(box.min.x + box.max.x) / 2,
|
|
(box.min.y + box.max.y) / 2,
|
|
(box.min.z + box.max.z) / 2
|
|
);
|
|
mesh.position.set(-center.x, -center.y, -center.z);
|
|
const size = box.getSize(new THREE.Vector3());
|
|
return { mesh, center, size, box };
|
|
}
|
|
|
|
fitCamera(size, target) {
|
|
const maxDim = Math.max(size.x, size.y, size.z);
|
|
const dist = maxDim * 1.5;
|
|
const t = target || new THREE.Vector3(0, 0, 0);
|
|
this.controls.target.copy(t);
|
|
this.camera.position.set(t.x, t.y - dist * 0.5, t.z + dist * 0.6);
|
|
this.camera.up.set(0, 0, 1);
|
|
this.controls.update();
|
|
}
|
|
|
|
// ===== Dispose =====
|
|
|
|
dispose() {
|
|
if (this._ctrlDragCleanup) this._ctrlDragCleanup();
|
|
if (this._animId) cancelAnimationFrame(this._animId);
|
|
if (this._resizeObserver) this._resizeObserver.disconnect();
|
|
|
|
for (const mode of Object.values(this._modes)) {
|
|
if (mode.dispose) mode.dispose();
|
|
}
|
|
|
|
this.controls.dispose();
|
|
this.renderer.dispose();
|
|
if (this.renderer.domElement.parentNode) {
|
|
this.renderer.domElement.parentNode.removeChild(this.renderer.domElement);
|
|
}
|
|
}
|
|
}
|