395 lines
18 KiB
JavaScript
395 lines
18 KiB
JavaScript
<script>
|
|
(function () {
|
|
var THREE_JS = "api/notes/__THREE_ID__/download";
|
|
var ORBIT_JS = "api/notes/__THREE_ORBIT_ID__/download";
|
|
var STL_JS = "api/notes/__THREE_STL_ID__/download";
|
|
var OCCT_JS = "api/notes/__OCCT_JS_ID__/download";
|
|
var OCCT_WASM = "api/notes/__OCCT_WASM_ID__/download";
|
|
|
|
var OCCT_JS_ABS = new URL(OCCT_JS, location.href).href;
|
|
var OCCT_WASM_ABS = new URL(OCCT_WASM, location.href).href;
|
|
|
|
var MODEL_RE = /\.(stl|step|stp)$/i;
|
|
|
|
var STEP_PARAMS = { linearDeflection: 0.4, angularDeflection: 0.2 };
|
|
|
|
var WORKER_SRC = [
|
|
'import { OcctKernel } from "' + OCCT_JS_ABS + '";',
|
|
"let kernel = null; let initP = null;",
|
|
"self.onmessage = async function (e) {",
|
|
" var d = e.data;",
|
|
" 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");
|
|
|
|
function formatBytes(n) {
|
|
if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
|
|
if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
|
|
return n + " B";
|
|
}
|
|
|
|
function formatCount(n) {
|
|
n = Math.round(n);
|
|
if (n >= 1000000) return (n / 1000000).toFixed(2) + "M";
|
|
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 makeViewer(name) {
|
|
var box = document.createElement("div");
|
|
box.className = "model-viewer";
|
|
var wrap = document.createElement("div");
|
|
wrap.className = "model-viewer-canvas";
|
|
var status = document.createElement("div");
|
|
status.className = "model-viewer-status";
|
|
status.textContent = "Loading 3D model\u2026";
|
|
var progress = document.createElement("div");
|
|
progress.className = "model-viewer-progress";
|
|
var fill = document.createElement("div");
|
|
fill.className = "model-viewer-progress-fill";
|
|
progress.appendChild(fill);
|
|
var data = document.createElement("div");
|
|
data.className = "model-viewer-data";
|
|
wrap.appendChild(status);
|
|
wrap.appendChild(progress);
|
|
box.appendChild(data);
|
|
box.appendChild(wrap);
|
|
if (name) {
|
|
var cap = document.createElement("div");
|
|
cap.className = "model-viewer-caption";
|
|
cap.textContent = name;
|
|
box.appendChild(cap);
|
|
}
|
|
return box;
|
|
}
|
|
|
|
function loadScript(src, cb) {
|
|
var s = document.createElement("script");
|
|
s.src = src;
|
|
s.onload = cb;
|
|
document.head.appendChild(s);
|
|
}
|
|
|
|
function fetchProgress(url, onProgress) {
|
|
return fetch(url).then(function (r) {
|
|
if (!r.ok) throw new Error("fetch " + r.status);
|
|
var total = parseInt(r.headers.get("Content-Length") || "0", 10);
|
|
var reader = r.body.getReader();
|
|
var chunks = [];
|
|
var received = 0;
|
|
function pump() {
|
|
return reader.read().then(function (res) {
|
|
if (res.done) {
|
|
var buf = new Uint8Array(received);
|
|
var off = 0;
|
|
chunks.forEach(function (c) { buf.set(c, off); off += c.length; });
|
|
return buf.buffer;
|
|
}
|
|
chunks.push(res.value);
|
|
received += res.value.length;
|
|
if (total) onProgress(received / total);
|
|
return pump();
|
|
});
|
|
}
|
|
return pump();
|
|
});
|
|
}
|
|
|
|
function initViewer(el, url, ext) {
|
|
var isStep = /\.(step|stp)$/i.test(ext);
|
|
var fileSize = null;
|
|
|
|
function setStatus(text) {
|
|
var s = el.querySelector(".model-viewer-status");
|
|
if (s) s.textContent = text;
|
|
}
|
|
function setProgress(percent) {
|
|
var bar = el.querySelector(".model-viewer-progress-fill");
|
|
if (!bar) return;
|
|
if (typeof percent === "number") {
|
|
bar.classList.remove("indeterminate");
|
|
bar.style.width = Math.round(percent * 100) + "%";
|
|
} else {
|
|
bar.classList.add("indeterminate");
|
|
bar.style.width = "";
|
|
}
|
|
}
|
|
function onError(msg) {
|
|
var s = el.querySelector(".model-viewer-status");
|
|
if (s) { s.textContent = msg || "Could not load 3D model."; s.classList.add("error"); }
|
|
setProgress(0);
|
|
}
|
|
function showData(extra) {
|
|
var d = el.querySelector(".model-viewer-data");
|
|
if (d) {
|
|
var parts = [];
|
|
if (fileSize) parts.push(formatBytes(fileSize));
|
|
if (extra) parts.push(extra);
|
|
if (parts.length) { d.textContent = parts.join(" \u00b7 "); d.style.display = "block"; }
|
|
}
|
|
var cap = el.querySelector(".model-viewer-caption");
|
|
if (cap && extra) {
|
|
var stats = [];
|
|
if (fileSize) stats.push(formatBytes(fileSize));
|
|
stats.push(extra);
|
|
var name = cap.getAttribute("data-model-name");
|
|
if (!name) { name = (cap.textContent || "").trim(); cap.setAttribute("data-model-name", name); }
|
|
cap.textContent = (name ? name + " \u00b7 " : "") + stats.join(" \u00b7 ");
|
|
}
|
|
}
|
|
|
|
function loadThree(cb) {
|
|
if (window.THREE && window.THREE.OrbitControls) { cb(); return; }
|
|
loadScript(THREE_JS, function () {
|
|
loadScript(ORBIT_JS, function () {
|
|
if (isStep) { cb(); return; }
|
|
if (window.THREE.STLLoader) { cb(); return; }
|
|
loadScript(STL_JS, cb);
|
|
});
|
|
});
|
|
}
|
|
|
|
setProgress(null);
|
|
setStatus("Loading 3D libraries\u2026");
|
|
|
|
loadThree(function () {
|
|
var wrap = el.querySelector(".model-viewer-canvas") || el;
|
|
function canvasSize() {
|
|
return { w: wrap.clientWidth || el.clientWidth || 400, h: wrap.clientHeight || 420 };
|
|
}
|
|
var initSize = canvasSize();
|
|
var renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
renderer.setSize(initSize.w, initSize.h);
|
|
var cs = getComputedStyle(document.documentElement);
|
|
function cssVar(n, fb) { var v = (cs.getPropertyValue(n) || "").trim(); return v || fb; }
|
|
renderer.setClearColor(cssVar("--background-primary", "#1d150f"), 1);
|
|
|
|
var scene = new THREE.Scene();
|
|
var camera = new THREE.PerspectiveCamera(45, initSize.w / initSize.h, 0.1, 1000000);
|
|
camera.position.set(300, 220, 320);
|
|
scene.add(new THREE.AmbientLight(0xffffff, 0.55));
|
|
var dir = new THREE.DirectionalLight(0xffffff, 1.0); dir.position.set(1, 1.5, 1); scene.add(dir);
|
|
var back = new THREE.DirectionalLight(0xffffff, 0.35); back.position.set(-1, -0.5, -1); scene.add(back);
|
|
var controls = new THREE.OrbitControls(camera, renderer.domElement);
|
|
controls.enableDamping = true;
|
|
var group = new THREE.Group();
|
|
scene.add(group);
|
|
|
|
var resizeObs = new ResizeObserver(function () {
|
|
var s = canvasSize();
|
|
if (!s.w || !s.h) return;
|
|
renderer.setSize(s.w, s.h);
|
|
camera.aspect = s.w / s.h;
|
|
camera.updateProjectionMatrix();
|
|
});
|
|
resizeObs.observe(wrap);
|
|
|
|
function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); }
|
|
animate();
|
|
|
|
function fit() {
|
|
var box = new THREE.Box3().setFromObject(group);
|
|
if (box.isEmpty()) return;
|
|
var size = box.getSize(new THREE.Vector3()).length() || 1;
|
|
var center = box.getCenter(new THREE.Vector3());
|
|
var d = size * 2.4;
|
|
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 = "";
|
|
wrap.appendChild(renderer.domElement);
|
|
var hint = document.createElement("div");
|
|
hint.className = "model-viewer-hint";
|
|
hint.textContent = "Drag to rotate \u00b7 scroll to zoom";
|
|
wrap.appendChild(hint);
|
|
}
|
|
|
|
setStatus("Downloading model\u2026");
|
|
fetchProgress(url, function (p) { setProgress(p); }).then(function (buf) {
|
|
fileSize = buf.byteLength;
|
|
showData();
|
|
var data = new Uint8Array(buf);
|
|
if (isStep) {
|
|
setStatus("Downloading STEP parser\u2026");
|
|
setProgress(null);
|
|
fetchProgress(OCCT_WASM_ABS, function () {}).then(function (wasmBuf) {
|
|
setStatus("Parsing STEP\u2026");
|
|
setProgress(null);
|
|
var t0 = Date.now();
|
|
var parseTimer = setInterval(function () {
|
|
var el2 = Date.now() - t0;
|
|
setStatus("Parsing STEP\u2026 (" + 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, { type: "module" });
|
|
var settled = false;
|
|
worker.onmessage = function (e) {
|
|
if (e.data.type === "done") {
|
|
settled = true; worker.terminate(); URL.revokeObjectURL(wurl); clearInterval(parseTimer);
|
|
try {
|
|
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: cssVar("--model-viewer-color", cssVar("--background-active", "#e0a15c")), 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");
|
|
} 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.");
|
|
}
|
|
};
|
|
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: "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);
|
|
geo.computeVertexNormals();
|
|
var mat = new THREE.MeshStandardMaterial({ color: cssVar("--model-viewer-color", cssVar("--background-active", "#e0a15c")), metalness: 0.25, roughness: 0.55, flatShading: true });
|
|
group.add(new THREE.Mesh(geo, mat));
|
|
fit();
|
|
showCanvas();
|
|
showData(formatCount(geo.index ? geo.index.count / 3 : geo.attributes.position.count / 3) + " triangles");
|
|
}
|
|
}).catch(function () { onError("Could not load 3D model."); });
|
|
});
|
|
}
|
|
|
|
var type = (document.body.className.match(/type-([\w-]+)/) || [])[1];
|
|
|
|
if (type === "file") {
|
|
var title = ((document.getElementById("title") || {}).textContent || document.title || "").trim();
|
|
var m = title.match(MODEL_RE);
|
|
if (m) {
|
|
var content = document.getElementById("content");
|
|
if (content) {
|
|
var noteId = document.body.getAttribute("data-note-id");
|
|
var el = makeViewer(title.trim());
|
|
content.innerHTML = "";
|
|
content.appendChild(el);
|
|
initViewer(el, "api/notes/" + noteId + "/download", m[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll("#content a[href*='api/attachments/']").forEach(function (a) {
|
|
var name = (a.textContent || "").trim();
|
|
var m = name.match(MODEL_RE);
|
|
if (!m) return;
|
|
var href = a.getAttribute("href");
|
|
var el = makeViewer(name);
|
|
el.classList.add("model-viewer-inline");
|
|
a.replaceWith(el);
|
|
initViewer(el, href, m[0]);
|
|
});
|
|
|
|
var blog = window.blogData;
|
|
var byAlias = {};
|
|
if (blog && blog.articles) {
|
|
blog.articles.forEach(function (a) {
|
|
var al = String(a.url || "").split("/").pop();
|
|
if (al) byAlias[al] = a;
|
|
});
|
|
}
|
|
document.querySelectorAll("#content a[href]").forEach(function (a) {
|
|
var m = String(a.getAttribute("href") || "").match(/\.\/([^/?#]+)$/);
|
|
if (!m) return;
|
|
var art = byAlias[m[1]];
|
|
if (!art) return;
|
|
var t = String(art.title || "").trim();
|
|
var mm = t.match(MODEL_RE);
|
|
if (!mm) return;
|
|
var el = makeViewer(t);
|
|
el.classList.add("model-viewer-inline");
|
|
a.replaceWith(el);
|
|
initViewer(el, "api/notes/" + art.id + "/download", mm[0]);
|
|
});
|
|
})();
|
|
</script> |