Former/frontend/src/engine/modes/vectorwrap-mode.js

799 lines
30 KiB
JavaScript

import * as THREE from 'three';
import { dbg } from '../former-engine.js';
export function createVectorWrapMode() {
const mode = {
name: 'vectorwrap',
engine: null,
_externalModelMesh: null,
_externalModelCenter: null,
_externalModelSize: null,
_projEncGroup: null,
_projEncLidMesh: null,
_projEncTrayMesh: null,
_projEncAssembled: true,
_vwGroup: null,
_vectorOverlayMesh: null,
_vwTexture: null,
_vwWidth: 0,
_vwHeight: 0,
_vwRestPositions: null,
_ffdCols: 0,
_ffdRows: 0,
_ffdGroup: null,
_ffdControlPoints: null,
_ffdCurrentPoints: null,
_ffdSpheres: null,
_ffdLines: null,
_ffdGridVisible: true,
_ffdDragging: null,
_ffdDragCleanup: null,
_cameraLocked: false,
_sleeveMesh: null,
_surfaceDragCleanup: null,
_dragStartUV: null,
install(engine) {
this.engine = engine;
},
loadExternalModel(stlArrayBuffer) {
this._disposeExternalModel();
const eng = this.engine;
const result = eng.loadSTLCentered(stlArrayBuffer, {
color: 0x888888,
side: THREE.DoubleSide,
flatShading: true,
});
this._externalModelMesh = result.mesh;
this._externalModelCenter = result.center;
this._externalModelSize = result.size;
eng.scene.add(result.mesh);
eng.fitCamera(result.size);
eng.gridHelper.position.set(0, 0, result.box.min.z - result.center.z - 0.5);
},
_disposeExternalModel() {
if (this._externalModelMesh) {
this.engine.scene.remove(this._externalModelMesh);
this._externalModelMesh.geometry.dispose();
this._externalModelMesh.material.dispose();
this._externalModelMesh = null;
}
},
loadProjectEnclosure(enclosureSTL, traySTL) {
dbg('vw-mode: loadProjectEnclosure called, encSTL=', enclosureSTL?.byteLength || 0, 'traySTL=', traySTL?.byteLength || 0);
this._disposeExternalModel();
this._disposeProjectEnclosure();
const eng = this.engine;
this._projEncGroup = new THREE.Group();
this._projEncAssembled = true;
if (enclosureSTL && enclosureSTL.byteLength > 84) {
dbg('vw-mode: parsing enclosure STL...');
try {
const loader = new (await_stl_loader())();
const geometry = loader.parse(enclosureSTL);
geometry.computeVertexNormals();
geometry.computeBoundingBox();
dbg('vw-mode: enclosure geometry verts=', geometry.attributes.position.count, 'bbox=', JSON.stringify(geometry.boundingBox));
const mat = new THREE.MeshPhongMaterial({
color: 0xffe090, side: THREE.DoubleSide, flatShading: true,
});
this._projEncLidMesh = new THREE.Mesh(geometry, mat);
this._projEncGroup.add(this._projEncLidMesh);
dbg('vw-mode: enclosure lid mesh added');
} catch (e) {
dbg('vw-mode: enclosure STL parse FAILED:', e?.message || e);
}
} else {
dbg('vw-mode: skipping enclosure STL (empty or too small)');
}
if (traySTL && traySTL.byteLength > 84) {
dbg('vw-mode: parsing tray STL...');
try {
const loader = new (await_stl_loader())();
const geometry = loader.parse(traySTL);
geometry.computeVertexNormals();
geometry.computeBoundingBox();
dbg('vw-mode: tray geometry verts=', geometry.attributes.position.count, 'bbox=', JSON.stringify(geometry.boundingBox));
const mat = new THREE.MeshPhongMaterial({
color: 0xa0d880, side: THREE.DoubleSide, flatShading: true,
});
this._projEncTrayMesh = new THREE.Mesh(geometry, mat);
this._projEncGroup.add(this._projEncTrayMesh);
dbg('vw-mode: tray mesh added');
} catch (e) {
dbg('vw-mode: tray STL parse FAILED:', e?.message || e);
}
} else {
dbg('vw-mode: skipping tray STL (empty or too small)');
}
dbg('vw-mode: projEncGroup children=', this._projEncGroup.children.length);
if (this._projEncGroup.children.length === 0) {
dbg('vw-mode: NO meshes loaded, returning early');
return;
}
const box = new THREE.Box3().setFromObject(this._projEncGroup);
const cx = (box.min.x + box.max.x) / 2;
const cy = (box.min.y + box.max.y) / 2;
const cz = (box.min.z + box.max.z) / 2;
this._projEncGroup.position.set(-cx, -cy, -cz);
this._externalModelCenter = new THREE.Vector3(cx, cy, cz);
this._externalModelSize = box.getSize(new THREE.Vector3());
dbg('vw-mode: group centered, size=', this._externalModelSize.x.toFixed(1), 'x', this._externalModelSize.y.toFixed(1), 'x', this._externalModelSize.z.toFixed(1));
eng.scene.add(this._projEncGroup);
eng.fitCamera(this._externalModelSize);
eng.gridHelper.position.set(0, 0, box.min.z - cz - 0.5);
dbg('vw-mode: loadProjectEnclosure complete');
},
toggleProjectEnclosurePart(part) {
const mesh = part === 'lid' ? this._projEncLidMesh : this._projEncTrayMesh;
if (!mesh) return true;
mesh.visible = !mesh.visible;
return mesh.visible;
},
toggleProjectEnclosureAssembly() {
if (!this._projEncLidMesh || !this._projEncGroup) return true;
this._projEncAssembled = !this._projEncAssembled;
if (this._projEncAssembled) {
this._projEncLidMesh.position.set(0, 0, 0);
} else {
const box = new THREE.Box3().setFromObject(this._projEncGroup);
const width = box.max.x - box.min.x;
this._projEncLidMesh.position.x = width * 1.2;
}
return this._projEncAssembled;
},
_disposeProjectEnclosure() {
if (this._projEncGroup) {
this.engine.disposeGroup(this._projEncGroup);
this._projEncGroup = null;
}
this._projEncLidMesh = null;
this._projEncTrayMesh = null;
this._projEncAssembled = true;
},
loadVectorOverlay(imageUrl, svgWidthMM, svgHeightMM) {
this._disposeVectorOverlay();
const eng = this.engine;
const loader = new THREE.TextureLoader();
loader.load(imageUrl, (tex) => {
tex.minFilter = THREE.LinearFilter;
tex.magFilter = THREE.LinearFilter;
const w = svgWidthMM || tex.image.width * (25.4 / 96);
const h = svgHeightMM || tex.image.height * (25.4 / 96);
this._vwWidth = w;
this._vwHeight = h;
this._vwTexture = tex;
const segsX = 32, segsY = 32;
const geo = new THREE.PlaneGeometry(w, h, segsX, segsY);
const mat = new THREE.MeshBasicMaterial({
map: tex,
transparent: true,
opacity: 0.7,
side: THREE.DoubleSide,
depthWrite: false,
});
const mesh = new THREE.Mesh(geo, mat);
let zPos = 0;
const targetObj = this._externalModelMesh || this._projEncGroup;
if (targetObj) {
const box = new THREE.Box3().setFromObject(targetObj);
zPos = box.max.z + 2;
}
this._vwGroup = new THREE.Group();
this._vwGroup.position.set(0, 0, zPos);
this._vwGroup.add(mesh);
this._vectorOverlayMesh = mesh;
const pos = geo.attributes.position;
this._vwRestPositions = new Float32Array(pos.count * 3);
for (let i = 0; i < pos.count; i++) {
this._vwRestPositions[i * 3] = pos.getX(i);
this._vwRestPositions[i * 3 + 1] = pos.getY(i);
this._vwRestPositions[i * 3 + 2] = pos.getZ(i);
}
eng.scene.add(this._vwGroup);
this.initFFDGrid(4, 4);
});
},
initFFDGrid(cols, rows) {
this._disposeFFDGrid();
const eng = this.engine;
const w = this._vwWidth;
const h = this._vwHeight;
if (!w || !h) return;
this._ffdCols = cols;
this._ffdRows = rows;
this._ffdGroup = new THREE.Group();
this._ffdControlPoints = [];
this._ffdCurrentPoints = [];
this._ffdSpheres = [];
const halfW = w / 2, halfH = h / 2;
for (let iy = 0; iy <= rows; iy++) {
for (let ix = 0; ix <= cols; ix++) {
const px = -halfW + (ix / cols) * w;
const py = -halfH + (iy / rows) * h;
this._ffdControlPoints.push(new THREE.Vector2(px, py));
this._ffdCurrentPoints.push(new THREE.Vector2(px, py));
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(Math.max(w, h) * 0.012, 8, 8),
new THREE.MeshBasicMaterial({ color: 0x89b4fa, transparent: true, opacity: 0.8 })
);
sphere.position.set(px, py, 0.5);
sphere.userData.ffdIndex = iy * (cols + 1) + ix;
this._ffdSpheres.push(sphere);
this._ffdGroup.add(sphere);
}
}
this._ffdLines = [];
this._rebuildFFDLines();
this._vwGroup.add(this._ffdGroup);
this._ffdGridVisible = true;
this._ffdDragging = null;
this._setupFFDDrag();
},
_rebuildFFDLines() {
if (this._ffdLines) {
for (const line of this._ffdLines) {
this._ffdGroup.remove(line);
line.geometry.dispose();
line.material.dispose();
}
}
this._ffdLines = [];
const cols = this._ffdCols, rows = this._ffdRows;
const cp = this._ffdCurrentPoints;
const lineMat = new THREE.LineBasicMaterial({ color: 0x585b70, opacity: 0.5, transparent: true });
for (let iy = 0; iy <= rows; iy++) {
const pts = [];
for (let ix = 0; ix <= cols; ix++) {
const p = cp[iy * (cols + 1) + ix];
pts.push(new THREE.Vector3(p.x, p.y, 0.3));
}
const geo = new THREE.BufferGeometry().setFromPoints(pts);
const line = new THREE.Line(geo, lineMat.clone());
this._ffdLines.push(line);
this._ffdGroup.add(line);
}
for (let ix = 0; ix <= cols; ix++) {
const pts = [];
for (let iy = 0; iy <= rows; iy++) {
const p = cp[iy * (cols + 1) + ix];
pts.push(new THREE.Vector3(p.x, p.y, 0.3));
}
const geo = new THREE.BufferGeometry().setFromPoints(pts);
const line = new THREE.Line(geo, lineMat.clone());
this._ffdLines.push(line);
this._ffdGroup.add(line);
}
},
_applyFFD() {
if (!this._vectorOverlayMesh || !this._ffdCurrentPoints) return;
const pos = this._vectorOverlayMesh.geometry.attributes.position;
const rest = this._vwRestPositions;
const cols = this._ffdCols, rows = this._ffdRows;
const cp = this._ffdCurrentPoints;
const restCp = this._ffdControlPoints;
const w = this._vwWidth, h = this._vwHeight;
const halfW = w / 2, halfH = h / 2;
for (let i = 0; i < pos.count; i++) {
const rx = rest[i * 3];
const ry = rest[i * 3 + 1];
let u = (rx + halfW) / w;
let v = (ry + halfH) / h;
u = Math.max(0, Math.min(1, u));
v = Math.max(0, Math.min(1, v));
const cellX = Math.min(Math.floor(u * cols), cols - 1);
const cellY = Math.min(Math.floor(v * rows), rows - 1);
const lu = (u * cols) - cellX;
const lv = (v * rows) - cellY;
const i00 = cellY * (cols + 1) + cellX;
const i10 = i00 + 1;
const i01 = i00 + (cols + 1);
const i11 = i01 + 1;
const r00 = restCp[i00], r10 = restCp[i10];
const r01 = restCp[i01], r11 = restCp[i11];
const c00 = cp[i00], c10 = cp[i10];
const c01 = cp[i01], c11 = cp[i11];
const d00x = c00.x - r00.x, d00y = c00.y - r00.y;
const d10x = c10.x - r10.x, d10y = c10.y - r10.y;
const d01x = c01.x - r01.x, d01y = c01.y - r01.y;
const d11x = c11.x - r11.x, d11y = c11.y - r11.y;
const dx = (1 - lu) * (1 - lv) * d00x + lu * (1 - lv) * d10x
+ (1 - lu) * lv * d01x + lu * lv * d11x;
const dy = (1 - lu) * (1 - lv) * d00y + lu * (1 - lv) * d10y
+ (1 - lu) * lv * d01y + lu * lv * d11y;
pos.setXY(i, rx + dx, ry + dy);
}
pos.needsUpdate = true;
this._vectorOverlayMesh.geometry.computeBoundingBox();
this._vectorOverlayMesh.geometry.computeBoundingSphere();
},
_setupFFDDrag() {
const eng = this.engine;
const canvas = eng.renderer.domElement;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let dragPlane = null;
let dragOffset = new THREE.Vector3();
const getWorldPos = (e) => {
const rect = canvas.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
return mouse;
};
const onDown = (e) => {
if (e.button !== 0 || !this._ffdGridVisible) return;
if (e.ctrlKey || e.metaKey || e.shiftKey) return;
getWorldPos(e);
raycaster.setFromCamera(mouse, eng.camera);
const hits = raycaster.intersectObjects(this._ffdSpheres);
if (hits.length === 0) return;
e.stopImmediatePropagation();
eng.controls.enabled = false;
const sphere = hits[0].object;
this._ffdDragging = sphere.userData.ffdIndex;
const normal = eng.camera.getWorldDirection(new THREE.Vector3());
const worldPos = new THREE.Vector3();
sphere.getWorldPosition(worldPos);
dragPlane = new THREE.Plane().setFromNormalAndCoplanarPoint(normal, worldPos);
const intersection = new THREE.Vector3();
raycaster.ray.intersectPlane(dragPlane, intersection);
dragOffset.copy(worldPos).sub(intersection);
};
const onMove = (e) => {
if (this._ffdDragging === null) return;
getWorldPos(e);
raycaster.setFromCamera(mouse, eng.camera);
const intersection = new THREE.Vector3();
if (!raycaster.ray.intersectPlane(dragPlane, intersection)) return;
intersection.add(dragOffset);
const local = this._vwGroup.worldToLocal(intersection.clone());
const idx = this._ffdDragging;
this._ffdCurrentPoints[idx].set(local.x, local.y);
this._ffdSpheres[idx].position.set(local.x, local.y, 0.5);
this._applyFFD();
this._rebuildFFDLines();
};
const onUp = () => {
if (this._ffdDragging !== null) {
this._ffdDragging = null;
eng.controls.enabled = true;
}
};
canvas.addEventListener('pointerdown', onDown, { capture: true });
canvas.addEventListener('pointermove', onMove);
canvas.addEventListener('pointerup', onUp);
this._ffdDragCleanup = () => {
canvas.removeEventListener('pointerdown', onDown, { capture: true });
canvas.removeEventListener('pointermove', onMove);
canvas.removeEventListener('pointerup', onUp);
};
},
setFFDGridVisible(visible) {
this._ffdGridVisible = visible;
if (this._ffdGroup) this._ffdGroup.visible = visible;
},
resetFFDGrid() {
if (!this._ffdControlPoints || !this._ffdCurrentPoints) return;
for (let i = 0; i < this._ffdControlPoints.length; i++) {
this._ffdCurrentPoints[i].copy(this._ffdControlPoints[i]);
if (this._ffdSpheres[i]) {
this._ffdSpheres[i].position.set(
this._ffdControlPoints[i].x,
this._ffdControlPoints[i].y,
0.5
);
}
}
this._applyFFD();
this._rebuildFFDLines();
},
setFFDResolution(cols, rows) {
this.initFFDGrid(cols, rows);
},
getFFDState() {
if (!this._ffdCurrentPoints) return null;
return {
cols: this._ffdCols,
rows: this._ffdRows,
points: this._ffdCurrentPoints.map(p => [p.x, p.y]),
};
},
setFFDState(state) {
if (!state || !state.points) return;
this.initFFDGrid(state.cols, state.rows);
for (let i = 0; i < state.points.length && i < this._ffdCurrentPoints.length; i++) {
this._ffdCurrentPoints[i].set(state.points[i][0], state.points[i][1]);
if (this._ffdSpheres[i]) {
this._ffdSpheres[i].position.set(state.points[i][0], state.points[i][1], 0.5);
}
}
this._applyFFD();
this._rebuildFFDLines();
},
setVWTranslation(x, y) {
if (this._vwGroup) {
this._vwGroup.position.x = x;
this._vwGroup.position.y = y;
}
},
setVWRotation(degrees) {
if (this._vwGroup) {
this._vwGroup.rotation.z = degrees * Math.PI / 180;
}
},
setVWScale(sx, sy) {
if (this._vwGroup) {
this._vwGroup.scale.set(sx, sy, 1);
}
},
_disposeFFDGrid() {
if (this._ffdDragCleanup) {
this._ffdDragCleanup();
this._ffdDragCleanup = null;
}
if (this._ffdLines) {
for (const line of this._ffdLines) {
if (line.parent) line.parent.remove(line);
line.geometry.dispose();
line.material.dispose();
}
this._ffdLines = null;
}
if (this._ffdSpheres) {
for (const s of this._ffdSpheres) {
if (s.parent) s.parent.remove(s);
s.geometry.dispose();
s.material.dispose();
}
this._ffdSpheres = null;
}
if (this._ffdGroup && this._vwGroup) {
this._vwGroup.remove(this._ffdGroup);
}
this._ffdGroup = null;
this._ffdControlPoints = null;
this._ffdCurrentPoints = null;
this._ffdDragging = null;
},
_disposeVectorOverlay() {
this._disposeFFDGrid();
if (this._vwGroup) {
this.engine.scene.remove(this._vwGroup);
}
if (this._vectorOverlayMesh) {
this._vectorOverlayMesh.geometry.dispose();
if (this._vectorOverlayMesh.material.map) {
this._vectorOverlayMesh.material.map.dispose();
}
this._vectorOverlayMesh.material.dispose();
this._vectorOverlayMesh = null;
}
this._vwGroup = null;
this._vwTexture = null;
this._vwRestPositions = null;
},
setVWCameraLock(locked) {
this._cameraLocked = locked;
const eng = this.engine;
if (!eng) return;
if (locked) {
eng.controls.enabled = false;
this._buildSleeveMesh();
this._setupSurfaceDrag();
} else {
eng.controls.enabled = true;
this._disposeSleeveMesh();
if (this._surfaceDragCleanup) {
this._surfaceDragCleanup();
this._surfaceDragCleanup = null;
}
}
},
_buildSleeveMesh() {
this._disposeSleeveMesh();
if (!this._vwTexture || !this._vwGroup) return;
const targetObj = this._externalModelMesh || this._projEncGroup;
if (!targetObj) return;
const box = new THREE.Box3().setFromObject(targetObj);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
// Build a box-shaped sleeve around the enclosure
const hw = size.x / 2, hh = size.y / 2, hz = size.z / 2;
const perimeter = 2 * (size.x + size.y);
// Create a merged geometry wrapping around the box perimeter
// 4 side planes + top + bottom
const segments = [];
// Front face (Y = -hh)
segments.push({ pos: [0, -hh, 0], rot: [Math.PI/2, 0, 0], w: size.x, h: size.z, uStart: 0, uEnd: size.x / perimeter });
// Right face (X = hw)
segments.push({ pos: [hw, 0, 0], rot: [Math.PI/2, 0, -Math.PI/2], w: size.y, h: size.z, uStart: size.x / perimeter, uEnd: (size.x + size.y) / perimeter });
// Back face (Y = hh)
segments.push({ pos: [0, hh, 0], rot: [Math.PI/2, 0, Math.PI], w: size.x, h: size.z, uStart: (size.x + size.y) / perimeter, uEnd: (2 * size.x + size.y) / perimeter });
// Left face (X = -hw)
segments.push({ pos: [-hw, 0, 0], rot: [Math.PI/2, 0, Math.PI/2], w: size.y, h: size.z, uStart: (2 * size.x + size.y) / perimeter, uEnd: 1.0 });
const geometries = [];
for (const seg of segments) {
const geo = new THREE.PlaneGeometry(seg.w, seg.h, 8, 8);
const pos = geo.attributes.position;
const uv = geo.attributes.uv;
// Remap UVs for continuous wrapping
for (let i = 0; i < uv.count; i++) {
const u = uv.getX(i);
const v = uv.getY(i);
uv.setXY(i, seg.uStart + u * (seg.uEnd - seg.uStart), v);
}
uv.needsUpdate = true;
// Apply rotation and position
const euler = new THREE.Euler(seg.rot[0], seg.rot[1], seg.rot[2]);
const quat = new THREE.Quaternion().setFromEuler(euler);
const offset = new THREE.Vector3(seg.pos[0], seg.pos[1], seg.pos[2]);
for (let i = 0; i < pos.count; i++) {
const v3 = new THREE.Vector3(pos.getX(i), pos.getY(i), pos.getZ(i));
v3.applyQuaternion(quat);
v3.add(offset);
pos.setXYZ(i, v3.x, v3.y, v3.z);
}
pos.needsUpdate = true;
geometries.push(geo);
}
// Merge geometries
const merged = this._mergeGeometries(geometries);
if (!merged) return;
const tex = this._vwTexture.clone();
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
tex.needsUpdate = true;
const mat = new THREE.MeshBasicMaterial({
map: tex,
transparent: true,
opacity: 0.7,
side: THREE.DoubleSide,
depthWrite: false,
});
this._sleeveMesh = new THREE.Mesh(merged, mat);
// Position relative to the target object's local space
const eng = this.engine;
eng.scene.add(this._sleeveMesh);
// Hide the flat overlay while sleeve is active
if (this._vectorOverlayMesh) {
this._vectorOverlayMesh.visible = false;
}
if (this._ffdGroup) {
this._ffdGroup.visible = false;
}
},
_mergeGeometries(geometries) {
if (geometries.length === 0) return null;
let totalVerts = 0;
let totalIdx = 0;
for (const g of geometries) {
totalVerts += g.attributes.position.count;
totalIdx += g.index ? g.index.count : 0;
}
const pos = new Float32Array(totalVerts * 3);
const uv = new Float32Array(totalVerts * 2);
const idx = new Uint32Array(totalIdx);
let vOff = 0, iOff = 0, vBase = 0;
for (const g of geometries) {
const gPos = g.attributes.position;
const gUV = g.attributes.uv;
for (let i = 0; i < gPos.count; i++) {
pos[(vOff + i) * 3] = gPos.getX(i);
pos[(vOff + i) * 3 + 1] = gPos.getY(i);
pos[(vOff + i) * 3 + 2] = gPos.getZ(i);
uv[(vOff + i) * 2] = gUV.getX(i);
uv[(vOff + i) * 2 + 1] = gUV.getY(i);
}
if (g.index) {
for (let i = 0; i < g.index.count; i++) {
idx[iOff + i] = g.index.getX(i) + vBase;
}
iOff += g.index.count;
}
vBase += gPos.count;
vOff += gPos.count;
g.dispose();
}
const merged = new THREE.BufferGeometry();
merged.setAttribute('position', new THREE.BufferAttribute(pos, 3));
merged.setAttribute('uv', new THREE.BufferAttribute(uv, 2));
merged.setIndex(new THREE.BufferAttribute(idx, 1));
merged.computeVertexNormals();
return merged;
},
_setupSurfaceDrag() {
if (this._surfaceDragCleanup) {
this._surfaceDragCleanup();
}
if (!this._sleeveMesh) return;
const canvas = this.engine.renderer.domElement;
const camera = this.engine.camera;
let dragging = false;
let lastX = 0, lastY = 0;
const onDown = (e) => {
if (e.button !== 0) return;
dragging = true;
lastX = e.clientX;
lastY = e.clientY;
e.preventDefault();
};
const onMove = (e) => {
if (!dragging || !this._sleeveMesh) return;
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
lastX = e.clientX;
lastY = e.clientY;
// Convert pixel delta to UV offset based on camera FOV and distance
const rect = canvas.getBoundingClientRect();
const fovRad = camera.fov * Math.PI / 180;
const dist = camera.position.length();
const viewH = 2 * dist * Math.tan(fovRad / 2);
const pxToMM = viewH / rect.height;
const tex = this._sleeveMesh.material.map;
if (tex) {
tex.offset.x -= (dx * pxToMM) / (tex.image?.width || 100);
tex.offset.y += (dy * pxToMM) / (tex.image?.height || 100);
}
};
const onUp = () => {
dragging = false;
};
canvas.addEventListener('pointerdown', onDown);
canvas.addEventListener('pointermove', onMove);
canvas.addEventListener('pointerup', onUp);
this._surfaceDragCleanup = () => {
canvas.removeEventListener('pointerdown', onDown);
canvas.removeEventListener('pointermove', onMove);
canvas.removeEventListener('pointerup', onUp);
};
},
_disposeSleeveMesh() {
if (this._sleeveMesh) {
this.engine.scene.remove(this._sleeveMesh);
this._sleeveMesh.geometry.dispose();
if (this._sleeveMesh.material.map) {
this._sleeveMesh.material.map.dispose();
}
this._sleeveMesh.material.dispose();
this._sleeveMesh = null;
}
// Restore flat overlay visibility
if (this._vectorOverlayMesh) {
this._vectorOverlayMesh.visible = true;
}
if (this._ffdGroup && this._ffdGridVisible) {
this._ffdGroup.visible = true;
}
},
dispose() {
this._disposeExternalModel();
this._disposeProjectEnclosure();
this._disposeVectorOverlay();
this._disposeSleeveMesh();
if (this._surfaceDragCleanup) {
this._surfaceDragCleanup();
this._surfaceDragCleanup = null;
}
},
};
return mode;
}
import { STLLoader } from 'three/addons/loaders/STLLoader.js';
function await_stl_loader() { return STLLoader; }