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:
trilium-share-gruvbox
2026-08-28 20:02:11 +02:00
parent 41ec295954
commit 18f8afaf69
10 changed files with 7157 additions and 129 deletions
+79 -19
View File
@@ -7,7 +7,7 @@ const OCCT_JS = "api/notes/__OCCT_JS_ID__/download";
const OCCT_WASM = "api/notes/__OCCT_WASM_ID__/download";
const WORKER_URL = "api/notes/__3D_WORKER_ID__/download";
const STEP_PARAMS = { linearDeflection: 0.0002, angularDeflection: 0.1 };
const STEP_PARAMS = { linearDeflection: 0.4, angularDeflection: 0.2 };
const WORKER_ABS = new URL(WORKER_URL, location.href).href;
const OCCT_JS_ABS = new URL(OCCT_JS, location.href).href;
@@ -24,6 +24,66 @@ function formatCount(n) {
if (n >= 1000) return (n / 1000).toFixed(1) + "k";
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 loadScript(src, cb) {
var s = document.createElement("script");
s.src = src;
@@ -203,6 +263,9 @@ function initViewer(el, url, ext) {
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 = "";
@@ -225,26 +288,23 @@ function initViewer(el, url, ext) {
setStatus("Parsing STEP\u2026");
setProgress(null);
var t0 = Date.now();
var parseTimer = setInterval(function () { setStatus("Parsing STEP\u2026 (" + Math.round((Date.now() - t0) / 1000) + "s)"); }, 1000);
var worker = new Worker(WORKER_ABS);
var parseTimer = setInterval(function () {
var el = Date.now() - t0;
setStatus("Parsing STEP\u2026 (" + Math.round(el / 1000) + "s)");
if (el > 240000 && !settled) { worker.terminate(); clearInterval(parseTimer); onError("STEP parse timed out."); }
}, 1000);
var worker = new Worker(WORKER_ABS, { 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(); clearInterval(parseTimer);
if (!e.data.success) { onError("Could not parse STEP file."); return; }
var tris = 0;
(e.data.meshes || []).forEach(function (m) {
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;
});
var geo = new THREE.BufferGeometry();
geo.setAttribute("position", new THREE.Float32BufferAttribute(e.data.positions, 3));
if (e.data.indices) geo.setIndex(new THREE.BufferAttribute(e.data.indices, 1));
fixWinding(geo);
var mat = new THREE.MeshStandardMaterial({ color: 0xb8bb26, metalness: 0.2, roughness: 0.6, side: THREE.DoubleSide, flatShading: true });
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");
@@ -256,7 +316,7 @@ function initViewer(el, url, ext) {
worker.onerror = function (e) {
if (!settled) { settled = true; worker.terminate(); 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);
+14 -35
View File
@@ -1,38 +1,17 @@
self.onmessage = function (e) {
import { OcctKernel } from "https://YOUR-INSTANCE.example.com/api/notes/__OCCT_JS_ID__/download";
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) });
}
};