Clinics upload intraoral scans as raw STL/PLY/OBJ and labs need to see
what was captured before accepting the case. Built a tooth-level 3D
preview that runs entirely in the browser — no server-side rendering,
no extra service, just three.js + react-three-fiber driven off the
existing download proxy.
Component (src/components/stl-viewer.tsx)
- Detects format from the filename (.stl / .ply / .obj).
- fetch() → ArrayBuffer → STLLoader / PLYLoader / OBJLoader.parse.
- OBJ comes back as a Group; merge the child meshes into a single
BufferGeometry (positions concat) so all three formats render
through the same mesh path.
- Scene: dark background, ambient + two directional lights, a
light-grey standard material, drei <Bounds> for auto-fit framing,
OrbitControls with damping. A 'Sığdır' button in the corner re-fits
the camera by remounting Bounds.
- Cleanup: geometry.dispose() on unmount, fetch cancellation guard,
inline error/loading states.
Wiring (job-files-panel.tsx)
- VIEWABLE_RE = /\.(stl|ply|obj)$/i. Only those rows get the new
eye-icon button.
- STLViewer is loaded via next/dynamic with ssr:false so three.js
(~500KB minified) only enters the bundle when the dialog actually
opens. Mounting is also gated on viewerOpen, so closing the dialog
frees the WebGL context.
- Dialog is a tall (85vh) wide (max-w-5xl) shell with the filename
+ size in the header and the canvas filling the rest.
Deps added: three, @react-three/fiber, @react-three/drei (+ types).
Last build hit ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING — the Nixpacks
image bundles corepack 0.24.1 which can't hydrate pnpm 11's binary on
Node 22 (known corepack/pnpm/Node interop bug). Rather than fight the
toolchain, downgrade to the version Coolify's Nixpacks already ships
natively: pnpm 9.15.9.
packageManager: pnpm@9.15.9
pnpm-lock.yaml regenerated under 9.15.9 (no patchedDependencies hash
drift, no settings/* block, lockfile version 9.0)
Verified locally: 'pnpm build' produces the same Next 16 output as
before, and the node-fetch-native-with-agent patch is still applied in
node_modules (globalThis.fetch present in the patched agent.cjs). Coolify
should now do 'pnpm i --frozen-lockfile' without any version mismatch.
Last build crashed with ERR_PNPM_LOCKFILE_CONFIG_MISMATCH on
patchedDependencies — Nixpacks bundles pnpm 9.15.9 by default, but our
lockfile is in pnpm 11's settings/patchedDependencies format. Adding the
'packageManager' field tells corepack-aware tooling (Nixpacks included)
to install pnpm 11.1.2 in the build container, matching what we use
locally. After this, 'pnpm i --frozen-lockfile' reads the same lockfile
format the project was developed with and the patched dependency
declaration lines up.
Coolify's Nixpacks pulls pnpm 9.15.9, which sees pnpm-workspace.yaml as a
monorepo descriptor and dies with 'packages field missing or empty' the
moment 'pnpm install' starts. The whole reason we kept that file around
was the 'allowBuilds' / 'patchedDependencies' blocks needed by pnpm 11
locally — both have a longstanding equivalent on the 'pnpm' key inside
package.json that pnpm 9 also understands. Consolidated them there and
removed the workspace file entirely.
package.json
"pnpm": {
"onlyBuiltDependencies": ["sharp", "unrs-resolver"],
"patchedDependencies": {
"node-fetch-native-with-agent@1.7.2":
"patches/node-fetch-native-with-agent@1.7.2.patch"
}
}
Lockfile regenerated under pnpm 11 locally; install succeeds. The
node-fetch-native-with-agent patch (the Node 26 / undici workaround) is
still applied so Coolify's pnpm install will replicate the fix in the
build container.