Add local 3D model viewer (three.js + occt-import-js) for STEP/STL, 8px box + caption; embed guide; licenses
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<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 MODEL_RE = /\.(stl|step|stp)$/i;
|
||||
|
||||
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";
|
||||
wrap.appendChild(status);
|
||||
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 initViewer(el, url, ext) {
|
||||
var isStep = /\.(step|stp)$/i.test(ext);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function status(text) {
|
||||
var s = el.querySelector(".model-viewer-status");
|
||||
if (s) s.textContent = text;
|
||||
}
|
||||
|
||||
function fit(group) {
|
||||
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();
|
||||
}
|
||||
|
||||
function buildScene(cb) {
|
||||
var statusEl = el.querySelector(".model-viewer-status");
|
||||
var wrap = el.querySelector(".model-viewer-canvas") || el;
|
||||
var w = wrap.clientWidth || el.clientWidth || 400;
|
||||
var h = Math.min(520, Math.max(320, Math.round(w * 0.62)));
|
||||
var renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(w, h);
|
||||
renderer.setClearColor(0x1d2021, 1);
|
||||
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);
|
||||
|
||||
var scene = new THREE.Scene();
|
||||
var camera = new THREE.PerspectiveCamera(45, w / 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);
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
animate();
|
||||
|
||||
cb({ group: group, scene: scene, camera: camera, controls: controls, statusEl: statusEl });
|
||||
}
|
||||
|
||||
function onError(msg) {
|
||||
var s = el.querySelector(".model-viewer-status");
|
||||
if (s) { s.textContent = msg || "Could not load 3D model."; s.classList.add("error"); }
|
||||
}
|
||||
|
||||
loadThree(function () {
|
||||
buildScene(function (ctx) {
|
||||
fetch(url).then(function (r) {
|
||||
if (!r.ok) throw new Error("fetch " + r.status);
|
||||
return r.arrayBuffer();
|
||||
}).then(function (buf) {
|
||||
var data = new Uint8Array(buf);
|
||||
if (isStep) {
|
||||
ctx.statusEl.textContent = "Loading STEP parser\u2026";
|
||||
function runOcct() {
|
||||
ctx.statusEl.textContent = "Parsing STEP\u2026";
|
||||
window.occtimportjs({ locateFile: function () { return OCCT_WASM; } }).then(function (occt) {
|
||||
var result = occt.ReadStepFile(data, null);
|
||||
if (!result || !result.success) { onError("Could not parse STEP file."); return; }
|
||||
(result.meshes || []).forEach(function (mesh) {
|
||||
if (!mesh.attributes || !mesh.attributes.position) return;
|
||||
var geo = new THREE.BufferGeometry();
|
||||
geo.setAttribute("position", new THREE.Float32BufferAttribute(mesh.attributes.position.array, 3));
|
||||
if (mesh.attributes.normal && mesh.attributes.normal.array) {
|
||||
geo.setAttribute("normal", new THREE.Float32BufferAttribute(mesh.attributes.normal.array, 3));
|
||||
}
|
||||
if (mesh.attributes.index && mesh.attributes.index.array) {
|
||||
geo.setIndex(mesh.attributes.index.array);
|
||||
}
|
||||
var color = mesh.color && mesh.color.length === 3
|
||||
? new THREE.Color(mesh.color[0], mesh.color[1], mesh.color[2]) : null;
|
||||
var mat = new THREE.MeshStandardMaterial({
|
||||
color: color || 0xb8bb26,
|
||||
metalness: 0.25,
|
||||
roughness: 0.55,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
ctx.group.add(new THREE.Mesh(geo, mat));
|
||||
});
|
||||
fit(ctx.group);
|
||||
}).catch(function (e) { onError("Could not load STEP parser."); });
|
||||
}
|
||||
if (window.occtimportjs) { runOcct(); } else { loadScript(OCCT_JS, runOcct); }
|
||||
} else {
|
||||
var geo = new THREE.STLLoader().parse(data);
|
||||
geo.computeVertexNormals();
|
||||
var mat = new THREE.MeshStandardMaterial({ color: 0xb8bb26, metalness: 0.25, roughness: 0.55 });
|
||||
ctx.group.add(new THREE.Mesh(geo, mat));
|
||||
fit(ctx.group);
|
||||
}
|
||||
}).catch(function (e) { 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);
|
||||
a.replaceWith(el);
|
||||
initViewer(el, href, m[0]);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user