Skip to content

Feat/model viewer - #70

Merged
olavgg merged 5 commits into
mainfrom
feat/model-viewer
Sep 16, 2026
Merged

olavgg merged 5 commits into
mainfrom
feat/model-viewer

Conversation

@olavgg

@olavgg olavgg commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What this changes

How it was verified

Checklist

  • Pull requests this one depends on are named above and merged first, or there are none
  • Commits are signed off (git commit -s), per CONTRIBUTING.md
  • ./gradlew build passes
  • Behaviour visible outside this repo is reflected in the documentation, or does not need to be
    (see AGENTS.md for which of the two documentation sites it belongs in)

@olavgg
olavgg marked this pull request as draft September 10, 2026 11:47
@olavgg
olavgg added this pull request to stack #102 September 15, 2026 10:29
@olavgg
olavgg marked this pull request as ready for review September 15, 2026 10:34
olavgg and others added 5 commits September 15, 2026 20:56
An uploaded STEP or IFC file was an opaque blob you could only download.
The file-information dialog now offers "View in 3D" for a file whose
extension we recognise as a model, opening a modal you can orbit, pan and
zoom in.

Formats: 3dm, 3ds, 3mf, amf, bim, brep, dae, fbx, fcstd, gltf, glb, ifc,
iges, igs, step, stp, stl, obj, off, ply, wrl. Classification is by
extension rather than mime type, because Tika calls most CAD
application/octet-stream and STEP is plain text, so it can arrive as
text/plain.

The renderer is Online3DViewer 0.18.0, MIT, vendored unmodified with its
provenance in static/js/model-viewer/README.md. It stays out of
assets.gradle for the same reason graph-network does: running Closure over
an already-minified megabyte buys nothing. That megabyte is injected on
first open rather than loaded with the files page, so browsing files costs
nothing until someone opens a model. This is the first lazily loaded
library in the console; the graph loads its eleven scripts on every page
that hosts it.

The bytes come from datahub-api directly, no console proxy (CONSTRAINTS
F2), as a blob because the download route is authenticated and the library
cannot attach a bearer token. Two details that are easy to get wrong and
were both confirmed against the running api: the request needs
Accept: */*, since the endpoint declares produces=application/octet-stream
and answers Accept: application/json with a 406; and it needs the Api
timeout raised from its 10s default, which would otherwise abort a
multi-MB model mid-download. The drawing viewer learned the same two.

Closing destroys the viewer. Browsers cap how many live WebGL contexts a
page may hold, so leaking one per open would eventually stop rendering.

Known limitation, written up in the README: six formats (STEP, IGES, BREP,
FCStd, IFC, 3DM, and Draco-compressed glTF) need a decoder that this
version fetches from a CDN at runtime, with the URLs baked into the
minified file and no setting to redirect them. Those formats therefore
need outbound internet from the browser and pull third-party code into the
console's origin. Everything else parses offline. Vendoring the decoders
would cost 17 MB and make this a patched copy rather than an upstream
artefact, so it is left as a decision rather than taken quietly.

Verified in headless Chromium against a real STL: wheel moves the camera
eye, left-drag orbits it, right-drag moves the centre, and the modal path
loads the library only on open, sizes the canvas, clears its status on
load and removes the overlay on close.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Olav Gjerde <olav@intellistream.ai>
Online3DViewer 0.18.0 hardcodes jsdelivr URLs for the decoders that STEP,
IGES, BREP, FCStd, IFC, 3DM and Draco-compressed glTF need, with no setting
to redirect them. That made those formats need outbound internet from the
browser, and pulled third-party code into the console's origin, neither of
which suits a platform deployed on closed networks.

The four decoders are now vendored under libs/ and the URLs rewritten by
patch-cdn-urls.py, which is kept next to them so the patch is reproducible
against a future release rather than being folklore. They live in one
directory because each locates its own .wasm relative to its script URL.

This adds 17 MB to the repository, most of it occt-import-js.wasm at 7.7 MB
and web-ifc at 6.7 MB. Nothing is fetched until a file of that type is
opened, so it costs page weight only for the formats that need it.

Verified in headless Chromium: opening a .step now fetches
occt-import-js-worker.js from our own path and makes no external request at
all, where before it went to jsdelivr.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Olav Gjerde <olav@intellistream.ai>
…lative

The previous commit rewrote the viewer's CDN urls to "/static/js/model-
viewer/libs/...". That works for the three decoders loaded by a script tag,
but not for occt, which O3DV runs in a worker created from a blob: URL. A
blob URL has an opaque base, so a root-relative path is not a resolvable
URL there and importScripts rejects it outright:

  Failed to execute 'importScripts' on 'WorkerGlobalScope':
  The URL '/static/js/model-viewer/libs/occt-import-js.js' is invalid.

The urls are now built from location.origin at runtime, which is what the
jsdelivr urls they replaced were: absolute, with a scheme and host.

My earlier check missed this because it asserted only that no request left
the origin, which was true while the worker was failing. The server log now
shows occt-import-js-worker.js, occt-import-js.js and the 7.7 MB wasm all
fetched and the worker running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Olav Gjerde <olav@intellistream.ai>
A converted plant model rendered as barely visible dark shapes. The cause
is not the model: the viewer shades anything carrying PBR materials, which
every CAD import does, with Physical shading, which sets ambient light to
zero and makes scene.environment the only source. With no environment map
there is nothing left to light with.

Vendors the cube map from the same release zip, six JPEGs at ~460 KB, and
passes it with backgroundIsEnvMap false so it lights the model without
replacing the dialog background. It loads only when a model is opened.

The difference on the real Huldra model, 2185 meshes converted from an
AVEVA E3D export, is the whole feature: before, dark outlines barely
distinguishable from the background; after, legible structural steel, pipe
supports and brackets. The gear-pump example goes from flat grey shapes to
readable bronze gears and steel pipework.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Olav Gjerde <olav@intellistream.ai>
The viewer sent the library the one file you clicked, so an obj opened with
its geometry and lost its materials, and a gltf with an external buffer did
not open at all. Uploading the companions alongside did not help, since
they were never sent.

The model is now read for its own references, rather than the folder being
swept: an obj for its mtllib and map_* lines, an mtl for the textures it
names in turn, a gltf for the buffer and image uris that are not inline
data. Only those names are then resolved against the model's own folder,
through the listing the browser can already fetch, and downloaded.

That keeps the cost proportionate. A folder holding a hundred unrelated
files costs one listing call, and a model naming nothing costs none at all.
Anything referenced but missing is skipped, so a model whose material
library was never uploaded still opens exactly as it did before, untextured.

Verified against the console's own example: opening cut-cube.obj now lists
its folder and fetches cut-cube.mtl, leaving the unrelated file in that
folder alone. Loading the pair resolves the material 'steel' where the obj
by itself resolves one unnamed fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Olav Gjerde <olav@intellistream.ai>
@olavgg
olavgg merged commit bed778e into main Sep 16, 2026
8 of 9 checks passed
@olavgg
olavgg deleted the feat/model-viewer branch September 16, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants