3D viewer: switch STEP engine to occt-wasm (OCCT V8, module worker); fix mobile rendering (Uint16 chunking, index wrapping, tight near/far); state-coloured box + debug line; app widget parity; licenses/docs
This commit is contained in:
+116
-56
@@ -11,45 +11,23 @@
|
||||
|
||||
var MODEL_RE = /\.(stl|step|stp)$/i;
|
||||
|
||||
var STEP_PARAMS = { linearDeflection: 0.0002, angularDeflection: 0.1 };
|
||||
var STEP_PARAMS = { linearDeflection: 0.4, angularDeflection: 0.2 };
|
||||
|
||||
var WORKER_SRC = [
|
||||
"self.onmessage = function (e) {",
|
||||
'import { OcctKernel } from "' + OCCT_JS_ABS + '";',
|
||||
"let kernel = null; let initP = null;",
|
||||
"self.onmessage = async function (e) {",
|
||||
" var d = e.data;",
|
||||
" if (d.type === 'loadLib') {",
|
||||
" try { importScripts(d.libUrl); } catch (err) {",
|
||||
" self.postMessage({ type: 'error', message: 'importScripts: ' + (err && err.message || err) }); return;",
|
||||
" }",
|
||||
" if (typeof self.occtimportjs !== 'function') {",
|
||||
" self.postMessage({ type: 'error', message: 'STEP parser not found.' }); return;",
|
||||
" }",
|
||||
" self.occtimportjs({ locateFile: function () { return 'occt.wasm'; }, wasmBinary: d.wasm }).then(function (occt) {",
|
||||
" self.occt = occt;",
|
||||
" self.postMessage({ type: 'ready' });",
|
||||
" }).catch(function (err) {",
|
||||
" self.postMessage({ type: 'error', message: 'STEP init: ' + (err && err.message || err) });",
|
||||
" });",
|
||||
" return;",
|
||||
" }",
|
||||
" if (d.type === 'parse' && self.occt) {",
|
||||
" try {",
|
||||
" var result = self.occt.ReadStepFile(d.data, d.params);",
|
||||
" var meshes = [];",
|
||||
" (result.meshes || []).forEach(function (m) {",
|
||||
" if (!m.attributes || !m.attributes.position) return;",
|
||||
" var out = { position: m.attributes.position.array, color: m.color };",
|
||||
" if (m.attributes.index && m.attributes.index.array) out.index = m.attributes.index.array;",
|
||||
" meshes.push(out);",
|
||||
" });",
|
||||
" var transfer = [];",
|
||||
" meshes.forEach(function (m) {",
|
||||
" if (m.position && m.position.buffer) transfer.push(m.position.buffer);",
|
||||
" if (m.index && m.index.buffer) transfer.push(m.index.buffer);",
|
||||
" });",
|
||||
" self.postMessage({ type: 'done', success: !!result.success, meshes: meshes }, transfer);",
|
||||
" } catch (err) {",
|
||||
" self.postMessage({ type: 'error', message: 'parse: ' + (err && err.message || err) });",
|
||||
" }",
|
||||
" if (d.type !== 'parse') return;",
|
||||
" try {",
|
||||
" if (!initP) initP = OcctKernel.init({ wasm: d.wasm });",
|
||||
" kernel = await initP;",
|
||||
" var shape = kernel.importStep(d.data);",
|
||||
" var mesh = kernel.tessellate(shape, d.params);",
|
||||
" kernel.release(shape);",
|
||||
" self.postMessage({ type: 'done', positions: mesh.positions, normals: mesh.normals, indices: mesh.indices }, [mesh.positions.buffer, mesh.normals.buffer, mesh.indices.buffer]);",
|
||||
" } catch (err) {",
|
||||
" self.postMessage({ type: 'error', message: 'parse: ' + (err && err.message || err) });",
|
||||
" }",
|
||||
"};"
|
||||
].join("\n");
|
||||
@@ -67,6 +45,68 @@
|
||||
return String(n);
|
||||
}
|
||||
|
||||
function fixWinding(geo) {
|
||||
if (geo.index) return;
|
||||
var pos = geo.attributes.position;
|
||||
var count = pos.count;
|
||||
geo.computeBoundingBox();
|
||||
var center = geo.boundingBox.getCenter(new THREE.Vector3());
|
||||
var vA = new THREE.Vector3(), vB = new THREE.Vector3(), vC = new THREE.Vector3();
|
||||
var e1 = new THREE.Vector3(), e2 = new THREE.Vector3(), n = new THREE.Vector3(), toC = new THREE.Vector3();
|
||||
var p = pos.array;
|
||||
for (var i = 0; i < count; i += 3) {
|
||||
vA.fromBufferAttribute(pos, i); vB.fromBufferAttribute(pos, i + 1); vC.fromBufferAttribute(pos, i + 2);
|
||||
e1.subVectors(vB, vA); e2.subVectors(vC, vA); n.crossVectors(e1, e2);
|
||||
toC.subVectors(center, vA);
|
||||
if (n.dot(toC) > 0) {
|
||||
var x = (i + 1) * 3, y = (i + 2) * 3;
|
||||
var t;
|
||||
t = p[x]; p[x] = p[y]; p[y] = t;
|
||||
t = p[x + 1]; p[x + 1] = p[y + 1]; p[y + 1] = t;
|
||||
t = p[x + 2]; p[x + 2] = p[y + 2]; p[y + 2] = t;
|
||||
}
|
||||
}
|
||||
pos.needsUpdate = true;
|
||||
}
|
||||
|
||||
function chunkGeometry(geo, group, mat) {
|
||||
var pos = geo.attributes.position;
|
||||
var idx = geo.index;
|
||||
var maxVerts = 65000;
|
||||
if (!idx || pos.count <= maxVerts) {
|
||||
geo.computeVertexNormals();
|
||||
group.add(new THREE.Mesh(geo, mat));
|
||||
return;
|
||||
}
|
||||
var triCount = idx.count / 3;
|
||||
var t = 0;
|
||||
while (t < triCount) {
|
||||
var vmap = new Map();
|
||||
var vpos = [];
|
||||
var iarr = [];
|
||||
while (t < triCount && vmap.size < maxVerts - 3) {
|
||||
var a = idx.getX(t * 3), b = idx.getX(t * 3 + 1), c = idx.getX(t * 3 + 2);
|
||||
var ids = [a, b, c];
|
||||
for (var k = 0; k < 3; k++) {
|
||||
var id = ids[k];
|
||||
var loc = vmap.get(id);
|
||||
if (loc === undefined) {
|
||||
loc = vpos.length / 3;
|
||||
vmap.set(id, loc);
|
||||
vpos.push(pos.getX(id), pos.getY(id), pos.getZ(id));
|
||||
}
|
||||
iarr.push(loc);
|
||||
}
|
||||
t++;
|
||||
}
|
||||
var g = new THREE.BufferGeometry();
|
||||
g.setAttribute("position", new THREE.Float32BufferAttribute(vpos, 3));
|
||||
g.setIndex(new THREE.BufferAttribute(new Uint16Array(iarr), 1));
|
||||
g.computeVertexNormals();
|
||||
group.add(new THREE.Mesh(g, mat));
|
||||
}
|
||||
}
|
||||
|
||||
function makeViewer(name) {
|
||||
var box = document.createElement("div");
|
||||
box.className = "model-viewer";
|
||||
@@ -92,6 +132,9 @@
|
||||
cap.textContent = name;
|
||||
box.appendChild(cap);
|
||||
}
|
||||
var dbg = document.createElement("div");
|
||||
dbg.className = "model-viewer-debug";
|
||||
box.appendChild(dbg);
|
||||
return box;
|
||||
}
|
||||
|
||||
@@ -135,6 +178,11 @@
|
||||
var s = el.querySelector(".model-viewer-status");
|
||||
if (s) s.textContent = text;
|
||||
}
|
||||
function setStep(state, msg) {
|
||||
el.className = "model-viewer model-" + state;
|
||||
var dbg = el.querySelector(".model-viewer-debug");
|
||||
if (dbg) dbg.textContent = new Date().toLocaleTimeString() + " [" + state + "] " + msg;
|
||||
}
|
||||
function setProgress(percent) {
|
||||
var bar = el.querySelector(".model-viewer-progress-fill");
|
||||
if (!bar) return;
|
||||
@@ -150,6 +198,7 @@
|
||||
var s = el.querySelector(".model-viewer-status");
|
||||
if (s) { s.textContent = msg || "Could not load 3D model."; s.classList.add("error"); }
|
||||
setProgress(0);
|
||||
setStep("error", msg || "Could not load 3D model.");
|
||||
}
|
||||
function showData(extra) {
|
||||
var d = el.querySelector(".model-viewer-data");
|
||||
@@ -183,6 +232,7 @@
|
||||
|
||||
setProgress(null);
|
||||
setStatus("Loading 3D libraries\u2026");
|
||||
setStep("loading", "Loading 3D libraries");
|
||||
|
||||
loadThree(function () {
|
||||
var wrap = el.querySelector(".model-viewer-canvas") || el;
|
||||
@@ -226,6 +276,9 @@
|
||||
camera.position.set(center.x + d * 0.9, center.y + d * 0.6, center.z + d);
|
||||
controls.target.copy(center);
|
||||
controls.update();
|
||||
camera.near = d * 0.05;
|
||||
camera.far = d * 10;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
function showCanvas() {
|
||||
wrap.innerHTML = "";
|
||||
@@ -240,6 +293,7 @@
|
||||
fetchProgress(url, function (p) { setProgress(p); }).then(function (buf) {
|
||||
fileSize = buf.byteLength;
|
||||
showData();
|
||||
setStep("loading", "Downloaded model " + formatBytes(fileSize));
|
||||
var data = new Uint8Array(buf);
|
||||
if (isStep) {
|
||||
setStatus("Downloading STEP parser\u2026");
|
||||
@@ -247,32 +301,38 @@
|
||||
fetchProgress(OCCT_WASM_ABS, function () {}).then(function (wasmBuf) {
|
||||
setStatus("Parsing STEP\u2026");
|
||||
setProgress(null);
|
||||
setStep("parsing", "STEP parser ready, parsing");
|
||||
var t0 = Date.now();
|
||||
var parseTimer = setInterval(function () { setStatus("Parsing STEP\u2026 (" + Math.round((Date.now() - t0) / 1000) + "s)"); }, 1000);
|
||||
var parseTimer = setInterval(function () {
|
||||
var el2 = Date.now() - t0;
|
||||
setStatus("Parsing STEP\u2026 (" + Math.round(el2 / 1000) + "s)");
|
||||
setStep("parsing", "Parsing STEP " + Math.round(el2 / 1000) + "s");
|
||||
if (el2 > 240000 && !settled) { worker.terminate(); URL.revokeObjectURL(wurl); clearInterval(parseTimer); onError("STEP parse timed out."); }
|
||||
}, 1000);
|
||||
var blob = new Blob([WORKER_SRC], { type: "application/javascript" });
|
||||
var wurl = URL.createObjectURL(blob);
|
||||
var worker = new Worker(wurl);
|
||||
var worker = new Worker(wurl, { type: "module" });
|
||||
var settled = false;
|
||||
worker.onmessage = function (e) {
|
||||
if (e.data.type === "ready") {
|
||||
worker.postMessage({ type: "parse", data: data, params: STEP_PARAMS }, [data.buffer]);
|
||||
} else if (e.data.type === "done") {
|
||||
if (e.data.type === "done") {
|
||||
settled = true; worker.terminate(); URL.revokeObjectURL(wurl); clearInterval(parseTimer);
|
||||
if (!e.data.success) { onError("Could not parse STEP file."); return; }
|
||||
var tris = 0;
|
||||
(e.data.meshes || []).forEach(function (m) {
|
||||
try {
|
||||
var geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(m.position, 3));
|
||||
if (m.index) geo.setIndex(m.index);
|
||||
geo.computeVertexNormals();
|
||||
var color = m.color && m.color.length === 3 ? new THREE.Color(m.color[0], m.color[1], m.color[2]) : null;
|
||||
var mat = new THREE.MeshStandardMaterial({ color: color || 0xb8bb26, metalness: 0.2, roughness: 0.6, side: THREE.DoubleSide, flatShading: true });
|
||||
group.add(new THREE.Mesh(geo, mat));
|
||||
tris += geo.index ? geo.index.count / 3 : geo.attributes.position.count / 3;
|
||||
});
|
||||
fit();
|
||||
showCanvas();
|
||||
showData(formatCount(tris) + " triangles");
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(e.data.positions, 3));
|
||||
if (e.data.indices) geo.setIndex(new THREE.BufferAttribute(e.data.indices, 1));
|
||||
var geoStats = "posV=" + geo.attributes.position.count + " idx=" + (geo.index ? geo.index.count : 0);
|
||||
fixWinding(geo);
|
||||
var mat = new THREE.MeshStandardMaterial({ color: 0xb8bb26, metalness: 0.2, roughness: 0.6, side: THREE.DoubleSide, flatShading: false });
|
||||
chunkGeometry(geo, group, mat);
|
||||
var tris = (e.data.indices && e.data.indices.length) ? e.data.indices.length / 3 : e.data.positions.length / 9;
|
||||
fit();
|
||||
showCanvas();
|
||||
showData(formatCount(tris) + " triangles");
|
||||
var capGL = renderer.capabilities ? (renderer.capabilities.isWebGL2 ? "WebGL2" : "WebGL1") : "?";
|
||||
setStep("done", "Rendered " + formatCount(tris) + " tris; meshes=" + group.children.length + " gl=" + capGL + " " + geoStats);
|
||||
} catch (err) {
|
||||
onError("build: " + (err && err.message || err));
|
||||
}
|
||||
} else if (e.data.type === "error") {
|
||||
settled = true; worker.terminate(); URL.revokeObjectURL(wurl); clearInterval(parseTimer);
|
||||
onError(e.data.message || "Could not parse STEP file.");
|
||||
@@ -281,7 +341,7 @@
|
||||
worker.onerror = function (e) {
|
||||
if (!settled) { settled = true; worker.terminate(); URL.revokeObjectURL(wurl); clearInterval(parseTimer); onError("STEP worker error: " + (e && e.message || "unknown")); }
|
||||
};
|
||||
worker.postMessage({ type: "loadLib", libUrl: OCCT_JS_ABS, wasm: new Uint8Array(wasmBuf) }, [wasmBuf]);
|
||||
worker.postMessage({ type: "parse", data: data, params: STEP_PARAMS, wasm: new Uint8Array(wasmBuf) }, [data.buffer, wasmBuf]);
|
||||
}).catch(function () { onError("Could not download STEP parser."); });
|
||||
} else {
|
||||
var geo = new THREE.STLLoader().parse(data.buffer);
|
||||
|
||||
Reference in New Issue
Block a user