Skip to content

feat(projects): add multi-model IFC viewer with shared-coordinate alignment - #811

Open
dronov-dmitry wants to merge 6 commits into
ThatOpen:mainfrom
dronov-dmitry:feat/multi-model-ifc-viewer
Open

dronov-dmitry wants to merge 6 commits into
ThatOpen:mainfrom
dronov-dmitry:feat/multi-model-ifc-viewer

Conversation

@dronov-dmitry

@dronov-dmitry dronov-dmitry commented Oct 3, 2026 •

Copy link
Copy Markdown

Description

Adds a multi-model IFC viewer with an add-ons lab (ТЗ-002), served by the existing dev server: projects/viewer.html, projects/viewer.ts, projects/addons.ts. Several IFC files can be loaded into one window and placed in a common coordinate system; 12 add-ons can be toggled from the sidebar.

What it solves

  • Several models in one scene — the header has a single load button, Add IFC…, which appends one or more files to the current session; drag & drop appends as well, and ?model= can be repeated in the URL. Opening viewer.html with no query params starts an empty session (the status reads "No models loaded — click "Add IFC…" or drop files on this page"), and unloading every model returns to it. There is no separate "replace" mode, so an existing scene cannot be dropped by accident.
  • Models panel — model count, per-model Hide/Show and unload (disposeModel), camera fit over all models that are currently visible (hidden ones are not framed).
  • Shared-coordinate alignment (the interesting part):
    • web-ifc loads with COORDINATE_TO_ORIGIN: true, so each file is normalized by its own coordination matrix (GetCoordinationMatrix → meshes.coordinates, readable via model.getCoordinates());
    • IfcLoader.load(data, coordinate) only toggles fragments.core.settings.autoCoordinate, which re-expresses every later model in the first model's frame (coords_first − coords_i);
    • neither behavior fits every pair of files — measured both cases on copies of the same IFC: site-chained geometry lines up with the per-file normalization, while a file without coordinate metadata in the same geometry frame only lines up in the shared mode. The page therefore exposes a selector — shared (first model) (default) / from each file — switchable without reloading. The page calls load() with coordinate = false and owns the positioning via its own applyAlignment().
  • Add-ons lab — a sidebar section lists 12 mutually-exclusive add-ons, each with its own panel and a Show all / Hide all control:
    • Highlighter, Hoverer, Hider — element select/hover/hide;
    • Clipper — section planes;
    • Measurements — length / area / angle / volume;
    • ItemsFinder, Classifier, BoundingBoxer, Mesher — search/classify/bound/build meshes;
    • Views, Viewpoints, Marker — saved cameras and CSS2D labels.
    • The Highlighter starts on by default when a model is loaded.
  • Clipper fix — the cutting plane now reliably splits the model:
    • planes go to the renderer's global three.clippingPlanes list (localClippingPlanes = false); local per-material planes never clip fragment meshes whose shaders were compiled before the plane existed;
    • the plane stays parallel to the double-clicked face and is slid through the model's bounding-box center so the two halves are visible from the current view;
    • keepCameraSide keeps the near half visible; Delete, Delete all and dragging the plane re-sync the cut.
  • Measurements fixes — the Measurements add-on got a round of usability fixes (commits 825c497, a4bb3b0):
    • Area and Volume actually commit now — the add-on's dblclick handler only ever called measurer.create(), but both tools finalize exclusively through endCreation() (free-mode Area accumulates polygon points, Volume accumulates picked items), so measurements never reached the list — no value, no fill, nothing to delete. Enter now finishes the in-progress area/volume measurement; Length and Angle keep finalizing through their own dblclick sequences.
    • Picks are anchored at the click — LengthMeasurement reused the pointermove-driven lastPick, which goes stale whenever the camera moves without the cursor moving (wheel zoom suspends picks for cameraStopDelay ms), and AreaMeasurement consumed the same stale preview point for every vertex after the first. Both tools now take a fresh pick at click time; Area also projects the click onto the in-progress polygon plane within pickTolerance so the shape stays planar.
    • Angle commit guard — AngleMeasurement.endCreation() now requires the third point (clickCount === 3); previously an external Enter after only two points would commit an angle whose end was still the origin.
    • Clipper-style measurements list — the panel's read-only values list becomes a row list like the Clipper panel: each row shows the measurement value and a × button that deletes just that item (list.delete + onItemDeleted re-render); Clear all is kept for bulk removal. The dead .addon-values CSS rule is dropped from viewer.html.
  • Robustness — the IFC signature (ISO-10303-21) is validated before conversion: the dev server answers 200 text/html for missing files, which previously crashed the tab, and a failing file no longer stops the rest of the batch.
  • Dev server home page — vite.config.ts adds a Projects section with a New empty session link (always present, including a fresh clone with no local projects) followed by one link per projects/<name>/*.ifc. The section is rebuilt whenever projects/ changes.
  • Repo hygiene — .gitignore keeps local models and the generated gallery out of the repo; projects/README.md documents the layout; pnpm artifacts (pnpm-lock.yaml, pnpm-workspace.yaml) are ignored (the repo installs with yarn).

Verification

  • Headless Chromium, two runs, all green with no console errors: 15/15 on the single-button UI, the empty session and its entry points, ?model=/?title= preloading and hide/unload; 21/21 on two files picked at once, second-click lockout while a batch loads, Hide/Show/×, drag & drop of two files, the coords_first − coords_i formula in both coordinate modes (positions reset in from each file, restored in shared), several ?model= params and a failing ?model= URL. Fit-to-view was checked to frame only the visible models.
  • Add-ons suite (headless CDP): all 12 add-ons enable/disable exclusively without touching the loaded models; Clipper cut/restore measured at the pixel level (model pixels drop when a plane is created and return after Delete all).
  • prettier --check and the repo ESLint config pass for the new files; the measurement fixes also pass tsc --noEmit for the touched packages/front files and prettier --check on all changed files.
  • The diff touches packages/front/src/core/Marker/src/renderer-with-2d.ts (positioning-context guard so an absolutely positioned container keeps its layout), packages/front/src/measurement/{Length,Area,Angle}Measurement (click-time picks and the angle commit guard) and vite.config.ts (alias @thatopen/components to the local core sources so the front package shares the same component registry), but not package.json or yarn.lock.
  • The browser checks live as local Playwright scripts; shipping them would mean adding a browser-test dependency, so they are not part of this PR (left unchecked above).

Additional context

  • Issue opened first per the ask-first policy: feat: multi-model IFC viewer with shared-coordinate alignment for projects/ #810 (Closes #810 in the commit message).
  • No new runtime dependencies: only three, @thatopen/components sources and the already-pinned web-ifc@0.0.77 from unpkg (same as the other examples).
  • window.__bim ({ components, world, fragments, ifcLoader, addons, highlighter }) is a tiny debug hook used by the checks above — happy to drop it if unwanted.
  • Open to reshaping the page location/naming (e.g. moving it under examples/) if that fits the repo structure better.

What is the purpose of this pull request?

  • Bug fix
  • New Feature
  • Documentation update
  • Other

Before submitting the PR, please make sure you do the following:

  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Follow the Conventional Commits v1.0.0 standard for PR naming (e.g. feat(examples): add hello-world example).
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.

Video:
https://youtu.be/a8Wp80GinH8


Closes #810

…gnment

- projects/viewer.{html,ts}: load several IFC files into one scene with the
  single "Add IFC…" button (it appends to the current session, so an empty
  scene is a valid starting point: open viewer.html with no query params,
  then add files or drop them on the page), models panel with per-model
  hide/unload, coordinate mode selector (shared first-model frame vs
  per-file normalization), drag & drop, repeated ?model= URL params and IFC
  signature validation before conversion
- vite.config.ts: always add a "New empty session" link to the dev server
  home page and list projects/<name>/*.ifc below it (the section rebuilds
  on projects/ changes)
- .gitignore, projects/README.md: keep local models and the generated gallery
  out of the repo, document the layout

Closes ThatOpen#810
@dronov-dmitry
dronov-dmitry force-pushed the feat/multi-model-ifc-viewer branch from 0352b87 to 803ed2b Compare October 10, 2026 19:14
…page

- rename the home page link to "New empty session (multimodel)" so it
  matches what the session actually is (any number of IFC files in one scene)
- remove the optional "All projects" gallery link: the gallery is a local
  generated artifact and is not part of the repo; the home page already
  lists every projects/<name>/*.ifc on its own
- regenerate index.html with forward-slash example paths (the committed
  copy was generated on Windows and used backslashes, which break on
  the web)
projects/viewer.ts imports three directly; without a root dependency the
dev server resolves it only through workspace hoisting and the vite
dedupe option has nothing to dedupe against.
Adds the ТЗ-002 add-ons lab (12 add-ons) on top of the multi-model viewer:

- addons.ts: mutually-exclusive radio list + per-addon panels for
  Highlighter, Hoverer, Hider, Clipper, Measurements (length/area/angle/
  volume), ItemsFinder, Classifier, BoundingBoxer, Mesher, Views,
  Viewpoints and Marker.
- Clipper now cuts the model reliably: planes go to the renderer's GLOBAL
  clipping list (local per-material planes never clip fragment shaders
  compiled before the plane existed), the plane is kept parallel to the
  double-clicked face and slid through the model center so the two halves
  are visible, and Delete/'Delete all'/drag re-sync the cut.
- viewer.ts: RendererWith2D (CSS2D overlay for the Marker add-on),
  fire-and-forget guard for fragments.core.update (worker FINISH fence
  can hang on an empty scene), window.__bim addons/highlighter hooks.
- vite.config.ts: alias @thatopen/components to the local core sources so
  the front package uses the same component registry; .gitignore the
  pnpm artifacts (the repo installs with yarn).
Length and Area reused the last pointermove-driven pick (lastPick /
_temp.point), which goes stale whenever the camera moves without the
cursor moving (wheel zoom suspends picks for cameraStopDelay ms), so
measurements landed where the cursor was, not on the double-clicked
surface. Both tools now take a fresh pick at click time; Area also
projects the click onto the in-progress polygon plane within
pickTolerance so the shape stays planar.

AngleMeasurement.endCreation now requires the third point
(clickCount === 3): an external Enter after only two points would
previously commit an angle whose end was still the origin.
…pper

The Measurements add-on called only measurer.create() on dblclick and
never endCreation(), so Area (free mode) and Volume — which commit
their result exclusively through endCreation() — never reached the
list: no value, no fill, nothing to delete. Enter now finishes the
in-progress area/volume measurement.

The panel's read-only values list becomes a Clipper-style row list:
each row shows the measurement value and a × button that deletes
just that item (list.delete + onItemDeleted re-render), with
Clear all kept for bulk removal. The dead .addon-values CSS rule is
dropped.

This branch has not been deployed

No deployments
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.

feat: multi-model IFC viewer with shared-coordinate alignment for projects/

1 participant