chore: add webapp node_modules dependencies.

This commit is contained in:
2026-02-14 17:28:26 -05:00
parent 7960320004
commit 91e7af0c04
15 changed files with 5217 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
import { useEffect, useState } from 'react';
import Module from 'manifold-3d';
import { parseSVG } from 'svg-path-parser';
import * as THREE from 'three';
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter';
// Initialize Manifold WASM once
let wasmModule;
export const useManifold = () => {
const [manifold, setManifold] = useState(null);
useEffect(() => {
const init = async () => {
if (!wasmModule) {
// Fix: Explicitly tell Manifold where to find the WASM file
// distinct from the bundle path
wasmModule = await Module({
locateFile: (path) => {
if (path.endsWith('.wasm')) {
return '/manifold.wasm';
}
return path;
}
});
wasmModule.setup();
}
setManifold(wasmModule);
};
init();
}, []);
return manifold;
};
// --- Geometry Helpers ---
const getPathPoints = (d, scale = 1.0) => {
// 1. Parse SVG path data using browser native API
const pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathEl.setAttribute("d", d);
const len = pathEl.getTotalLength();
if (len < 0.1) return [];
const resolution = 0.5; // mm approx
const numPoints = Math.max(10, Math.ceil(len / resolution));
const loops = [];
let currentLoop = [];
let lastP = null;
for (let i = 0; i <= numPoints; i++) {
const p = pathEl.getPointAtLength((i / numPoints) * len);
if (lastP) {
const dist = Math.hypot(p.x - lastP.x, p.y - lastP.y);
// Detect "Move" command jumps which signify new subpaths
if (dist > 5.0) {
if (currentLoop.length > 2) loops.push(currentLoop);
currentLoop = [];
}
}
currentLoop.push([p.x * scale, -p.y * scale]);
lastP = p;
}
if (currentLoop.length > 2) loops.push(currentLoop);
return loops;
};
export const convertFile = async (file, manifold, addLog) => {
if (!manifold) return null;
// CrossSection is a top-level export, NOT a static property of Manifold class
const { Manifold, CrossSection } = manifold;
const text = await file.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, "image/svg+xml");
const paths = {
black: [],
white: [],
cyan: []
};
// 1. Scale Calculation
let minX = Infinity, maxX = -Infinity;
const backgroundNodes = [];
const allPathNodes = Array.from(doc.querySelectorAll('path'));
allPathNodes.forEach(p => {
const cls = p.getAttribute('class');
if (!cls) backgroundNodes.push(p);
});
backgroundNodes.forEach(p => {
const d = p.getAttribute('d');
if (!d) return;
const pathEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
pathEl.setAttribute("d", d);
const len = pathEl.getTotalLength();
for (let i = 0; i <= 10; i++) {
const pt = pathEl.getPointAtLength(i / 10 * len);
minX = Math.min(minX, pt.x);
maxX = Math.max(maxX, pt.x);
}
});
let scale = 1.0;
const TARGET_WIDTH = 87.80;
if (minX !== Infinity && maxX !== -Infinity) {
const currentWidth = maxX - minX;
if (currentWidth > 0) {
scale = TARGET_WIDTH / currentWidth;
addLog(` Scale factor: ${scale.toFixed(4)}`);
}
}
// 2. Process Paths with Classification
allPathNodes.forEach(p => {
const cls = p.getAttribute('class');
const d = p.getAttribute('d');
if (!d) return;
const loops = getPathPoints(d, scale);
if (cls === 'st2') paths.white.push(loops);
else if (cls === 'st1') paths.cyan.push(loops);
else paths.black.push(loops);
});
// 3. Extrusion Helper using Manifold
const createMeshBlob = (loopsList, height, z) => {
const allContours = loopsList.flat();
if (allContours.length === 0) return null;
// Create CrossSection with Even-Odd rule
// Correct usage: new CrossSection(contours, fillRule)
const cs = new CrossSection(allContours, 'EvenOdd');
// Extrude
const mesh = Manifold.extrude(cs, height, 0, 0, [1, 1]);
// Translate Z
const meshZ = mesh.translate(0, 0, z);
// Convert to Mesh for Three.js export
const meshGL = new THREE.Mesh(getBufferGeometryFromManifold(meshZ, THREE));
const exporter = new STLExporter();
const stlString = exporter.parse(meshGL, { binary: true });
return new Blob([stlString], { type: 'application/octet-stream' });
};
// Constants
const BG_THICK = 3.0;
const TXT_THICK = 2.0;
const TXT_Z = 4.0; // Raised by 4mm
return {
black: createMeshBlob(paths.black, BG_THICK, 0),
white: createMeshBlob(paths.white, TXT_THICK, TXT_Z),
cyan: createMeshBlob(paths.cyan, TXT_THICK, TXT_Z)
};
};
function getBufferGeometryFromManifold(manifoldMesh, THREE) {
const mesh = manifoldMesh.getMesh();
const geometry = new THREE.BufferGeometry();
const numVerts = mesh.vertProperties.length / mesh.numProp;
const vertices = new Float32Array(numVerts * 3);
for (let i = 0; i < numVerts; i++) {
vertices[i * 3] = mesh.vertProperties[i * mesh.numProp];
vertices[i * 3 + 1] = mesh.vertProperties[i * mesh.numProp + 1];
vertices[i * 3 + 2] = mesh.vertProperties[i * mesh.numProp + 2];
}
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const indices = new Uint32Array(mesh.triVerts);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
geometry.computeVertexNormals();
return geometry;
}