4.3 KiB
Embedding a 3D model (STEP / STL) in a note
The share can render .step / .stp / .stl models interactively (three.js + occt-import-js,
hosted locally). The viewer fills the available screen width (full-bleed, ~100vw) with a
height based on the viewport (min(70vh, 900px)), so models use the maximum available space
instead of the readable content width. The caption below the model always shows
<file name> · <size> · <triangle count> (e.g. Wrench.stl · 711 KB · 14.6k triangles).
There are three ways to embed a model, depending on how you store the file.
Option A – Model as its own note (file note)
- Create (or upload) a file note and make sure its title ends in
.step,.stpor.stl, e.g.bracket.steporrear_housing.stp. - Put it under a category in the blog and set
publish=true. - The note's share page then shows the interactive 3D viewer automatically: drag to rotate, scroll to zoom.
Option B – Model embedded inline in a text note (attachment)
- Upload the STEP/STL file as an attachment of the note: drag & drop it onto the note, or use the attachment dialog (paperclip icon).
- Rename the attachment so its title ends in
.step,.stpor.stl(the viewer detects a model by the file name). Example:flange.step. - Insert the attachment link into the note body: right-click the attachment → "Insert link", or drag the attachment into the text.
- On the share, that link is automatically replaced by the embedded 3D viewer.
Option C – Model embedded inline from a file note (recommended for blog posts)
This lets you write blog posts that contain models inline, referencing a model that also exists as its own (published) file note — the model stays in one place.
- Create the model as a file note (title ends in
.step/.stp/.stl), publish it (see Option A). The viewer needs it inblog_data, i.e. it must be a published article reachable in the blog tree. - In your text note, add a link to that file note, e.g.
[Wrench.stl](#root/<noteId>)(markdown) or drag the note reference into the editor. - On the share, the link to a model file note is replaced by the inline viewer. You can embed several models per post this way.
Tip: STL (plain triangle meshes) is rendered with three.js only; STEP (CAD B-rep) is parsed by occt-wasm (OpenCASCADE V8, WASM, MIT/Apache-2.0) in a Web Worker (module worker, non-blocking). The first STEP view downloads the ~22 MB WASM once and then caches it in the browser. STEP tessellation is set to
linearDeflection: 0.4, angularDeflection: 0.2(≈480k triangles for the ignis battery module; tuned for mobile).
Requirements
- The
3d model viewerscript must be wired:~shareHtml → 3d model viewer(inheritable) on the blog root,shareHtmlLocation=body:end. - The five libraries must exist as notes and be referenced by the script via the
placeholder tokens in
scripts/3d_model_viewer.js:__THREE_ID__,__THREE_ORBIT_ID__,__THREE_STL_ID__,__OCCT_JS_ID__(the occt-wasm bundle),__OCCT_WASM_ID__(occt-wasm.wasm) – see the README installation for the note titles. The STEP worker is a module worker (type: "module"), so the browser must support ES modules in workers (all modern browsers). - Mobile compatibility: the mesh is split into chunks of ≤65000 vertices
(
chunkGeometry,Uint16indices) so it renders on GPUs withoutOES_element_index_uint; the camera near/far is tightened afterfit()for 16-bit depth buffers. The caption always shows a small debug line with the current step (loading/parsing/done/error) – useful while diagnosing. - For inline embedding from a file note (Option C) the
blog_datanote must be present and the target model must be a published article (the viewer resolves./<alias>links againstwindow.blogData).
Trilium app widget (independent of the share)
The same viewer is also available inside Trilium itself (independent of the blog):
a note-detail-pane widget (app/3d-model-viewer-app.jsx + app/3d-worker.js) renders
the viewer whenever you open a file note whose title ends in .step / .stp / .stl.
The worker is loaded from a code note (not a Blob) so it works under the app's CSP.
Wire it by giving the JSX note the label #widget=3dModelViewer and reload the app.