799 lines
30 KiB
JavaScript
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; }
|