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; }