Skip to content

feat!: build the viewer on the IOScene scene model - #27

Merged
mahach666 merged 19 commits into
mainfrom
feat/ioscene
Nov 12, 2026
Merged

mahach666 merged 19 commits into
mainfrom
feat/ioscene

Conversation

@mahach666

@mahach666 mahach666 commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

What changes and why

Builds IOViewer on the scene model of the IOScene package (IOScene [0.1.0-preview.2, 0.2.0-0), a new dependency of the IOViewer package) instead of its own IOViewer.Model types. Version 0.3.0-preview.1.

Breaking for library users (the CHANGELOG [Unreleased] section has a migration table from every 0.2.0 type and member to its replacement)

  • The model types move to namespace IOScene with Scene* names, ViewerClash* included; the Viewer* model classes and the math types leave IOViewer.Model.
  • ViewerDocument wraps a SceneDocument (Scene) and keeps only viewer state: SectionPlanes, SectionBox and HiddenModelKeys. Title reads and writes Scene.Title.
  • JsonViewerExporter writes viewer JSON format 2 ("Kind": "ioviewer"). IOViewer 0.2.0 reads such a file as an empty document with exit code 0.
  • A page regenerated from an old snapshot shows each mesh without an element as an element.
  • Not broken: loading 0.1/0.2 JSON, existing HTML pages, the ioviewer arguments, GLB/OBJ to HTML, the one-package install.

Library

  • HtmlViewerExporter.Save, Export and ExportToString also take a SceneDocument. The payload is the same for the same content: a geometry placed once is baked like an own mesh, a geometry placed several times is written once and placed by matrices.
  • JsonViewerImporter reads viewer JSON, IOScene scene files and 0.1/0.2 snapshots, and reads from the stream instead of copying the whole input. The 0.2.0 classes stay internal in IOViewer.Legacy; one mapper turns a snapshot into a scene, with stand-in elements for meshes without one, and keeps only the task and clash references that 0.2.0 resolved.
  • Clash distance is double?: the page writes null and the clash list shows no distance (it showed 0). A 0.1/0.2 snapshot keeps its value.
  • GlbViewerImporter converts the base colour factor from linear to sRGB, so GLB colours look lighter than in 0.2.0. Alpha stays linear; channels are rounded instead of truncated.
  • A 4D task of an unknown type is shown as a construct task (it was left out); an unknown projection stays perspective.
  • The viewer draws triangles only: line and point instances are skipped, counted in the payload, and the CLI warns with their number.
  • Opacity is the colour alpha byte, so 0.35 becomes 89/255.
  • The properties panel shows SceneElement.SourceId and the model tree search finds it.
  • Property sets of an element that have the same name are one group in the properties panel. They were written as JSON members with the same name, and the page kept only the last. A property name that repeats in a group gets the first free numbered suffix (Height (2)), so no property is lost. Elements that share an id, which Validate() rejects, are one entry with the sets of all of them. No other member name in the page payload comes from model data.

Repository

  • The 0.2.0 JSON snapshots are frozen as legacy fixtures in tests/fixtures/legacy-0.2. The viewer fixtures are regenerated as format 2; the building's survey marker is now an element, so its element count is one higher.
  • release.yml checks the changelog section before publishing anything, pushes the NuGet packages before creating the GitHub release, and marks a version with a hyphen as a prerelease.
  • CONTRIBUTING describes building against an unpublished IOScene preview from a local feed. README, getting started and the public API page describe the scene-based API.

Tests

Added or ported on this branch: LegacyFixtureTests (each frozen 0.2.0 snapshot maps to a valid scene; stand-ins, kept references, link names), the GLB colour conversion in its linear segment and when clamped, and the C# tests, fixture generator, SyntheticModel, viewer unit tests and Playwright e2e tests ported to the scene.

Run against IOScene 0.1.0-preview.2 from a local package folder (the version published on nuget.org has the same public API and method bodies):

  • dotnet test IOViewer.slnx -c Release: 115/115 on net8.0 and net10.0. The 6 new HtmlViewerExporterTests.Props_* cases read the properties block with Utf8JsonReader and fail if an object repeats a member name; the first four fail before the fix. One of them writes 20,000 equal names (85 ms; 9 s with the first version of the suffix search).
  • npm test: 144/144. npx playwright test: 109/109 (run on c38a7d3; the commit after it changes only the C# suffix search and its tests, and gives the same output).
  • The properties block of the committed viewer and legacy fixtures is the same before and after the property-group fix.
  • The tracked dist/viewer.js is identical to a fresh build.
  • Real IFC and RVT models (not committed) converted to pages and opened in a browser: no console errors and no network requests beyond the page.
  • dotnet docfx docfx.json --warningsAsErrors: not run.
  • CI: passes with IOScene 0.1.0-preview.2 from nuget.org (re-run after its publication).

Notes

Merge order:

  1. IOScene 0.1.0-preview.2 is published to nuget.org: done, and CI and the Docs workflow pass on this PR.
  2. This PR.
  3. The CLI's IFC and RVT input (through IfcIO.Scene and RvtIO.Scene) is built on this branch and follows in a separate PR.
  • No release here. Before tagging v0.3.0-preview.1, add a ## [0.3.0-preview.1] changelog section; release.yml now stops before publishing without it.
  • The history adds an IOViewer.IfcIO adapter and later removes it: it moved to the IfcIO repository as IfcIO.Scene, so the net diff contains none of it. ViewerElement.SourceId from an early commit is likewise superseded by SceneElement.SourceId.

ViewerElement.SourceId holds the element's id in its source format (a Revit
UniqueId or an IFC GlobalId); the numeric Id stays the viewer key. The HTML
page writes it to the META elements as "sourceId" only when set, the
properties panel shows it as its own row (Source ID / ID источника) and the
model tree search finds it. JSON round-trips it and leaves it out when null,
so snapshots of documents without source ids are unchanged.

ViewerDocument, ViewerMesh.Vertices, ViewerGeometry.Vertices and Transform3d
now state that the model is in metres with Z up.
IfcViewerAdapter converts an IfcModel into a ViewerDocument in metres with
Z up: one element per IFC product with the STEP id as Id, the GlobalId as
SourceId, the storey as Level, and property and quantity sets as property
groups. Colours come from IfcIO materials, with a fallback per IFC class,
and work schedules become the TimeLiner. IfcIO bakes mapped items into
world meshes, so every mesh is its own; the conversion report says so and
lists elements without geometry by reason.

The IfcIO version is set in EngineVersions.props (IfcIOVersion, 0.11.0,
the first release with world placements); until it is published, restore
needs a local feed.
README lists the engine adapters with an IfcIO example, getting started
gets a From an IFC file section with the mapping and its limits, and the
public API page, docfx metadata, docs workflow paths and changelog cover
IOViewer.IfcIO. CONTRIBUTING states that engine adapters live here and
how to build against an engine release from a local feed.
IfcIO matches products by class-name prefix, so type objects such as
IfcWallType or IfcDoorStyle and door and window lining properties reached
the viewer as elements without geometry. The adapter now skips them.

Document that Level comes from IfcIO's storey assignment, which puts
elements outside a storey and spaces in the first storey. Pack the whole
solution with a prerelease of the next version for local engine builds,
and note that CI, docs and release restore fail until the IfcIO version
is published.
The adapter moves to the IfcIO repository as IfcIO.Scene. Remove its
project, its tests, EngineVersions.props, the IfcIO package version, the
two projects in the solution, the docs workflow path filters, the docfx
entry and the documentation that describes it. The solution restores
from nuget.org alone again.
Reference IOScene [0.1.0-preview.1, 0.2.0-0) and drop the Viewer* model
classes and math types. ViewerDocument now holds a SceneDocument, the
section planes and box, and the model keys hidden at start; Title reads
and writes the scene title. HtmlViewerExporter gains Save, Export and
ExportToString overloads that take a SceneDocument.

The payload writer reads the scene and keeps the page payload of the
same content: a geometry placed once is baked on the global grid as own
meshes were, a geometry placed several times is written once and placed
by matrices. The instance colour and the material colour carry the only
opacity. Instances of lines, points or an unknown topology are skipped
and counted in META "skipped", and the CLI warns about them.

GLB and OBJ imports produce one model with elements, geometries,
instances and materials that pass Validate(). JsonViewerExporter writes
viewer JSON format 2 with the scene encoded by SceneJson.CreateOptions().
JsonViewerImporter reads viewer JSON, scene files and IOViewer 0.1/0.2
snapshots; the 0.2.0 classes stay internal in IOViewer.Legacy and one
mapper turns a snapshot into a scene, with stand-in elements for meshes
without one.
The viewer fixtures are regenerated as viewer JSON format 2. The survey
marker of the building is now an element, as every instance of a scene
belongs to one, so the element counts and the tree count one more. The
test for a mesh without an element runs on a copy of the building with
the marker's element removed.
The Unreleased section lists what breaks for existing users, what does
not, and a table from each 0.2.0 model type and member to its scene
replacement.
The changelog is checked before anything is published, the push runs
before "Create release" so a refused first publication can be fixed and
the job run again, and a version with a hyphen is released as a
prerelease.
The 0.2.0 viewer matched a 4D task or clash id to an element, or to an
ElementId group when the snapshot had no elements, and never to a mesh's
own id. The mapper kept any id that named a scene element, so a
reference that named nothing in 0.2.0 could now drive a mesh stand-in.
…he page

The legacy mapper turns such a link name into a scene model, and the page
lists every model after the first as a linked file. 0.2.0 listed only the
snapshot's LinkedFiles. Pin the behaviour with a test and note it in the
changelog.
JsonViewerImporter copied the whole input into memory two or three times
before reading it, and so also capped it at the array size limit. It now
finds the root Kind and Format with a reader that works in blocks,
rewinds and deserialises from the stream. A stream that cannot seek is
copied into memory once.
A clash distance is nullable: the page writes null and the clash list
shows no distance for it; a 0.1/0.2 snapshot keeps the value it has.
GLB base colour factors are converted from linear to sRGB. A task type
the viewer does not know is a construct task in the payload and in the
schedule, and an unknown projection stays perspective. The viewer
fixture's third clash now reports no distance.
…roperties

The page parses the properties block with JSON.parse, which keeps only
the last of equal member names, so of two sets with the same name on one
element only the last one was shown. Sets with the same name are now one
group, in the order the element first references them, and a property
name that repeats in a group gets the first free suffix " (2)", " (3)".
Names are compared as written, where a lone surrogate becomes U+FFFD.
Elements that share an id, which Validate rejects, are one entry, as the
page finds properties by element id.
…) each time

A name that repeats k times in one group searched " (2)", " (3)", ... from the
start for each repeat, so it took time quadratic in k: a set with 20,000 equal
names took 9 s to write. The search now goes on from the last suffix given for
that name; every suffix below it is taken, so the names written are the same.
@mahach666
mahach666 merged commit fbfd586 into main Nov 12, 2026
6 of 10 checks passed
@mahach666
mahach666 deleted the feat/ioscene branch November 12, 2026 21:48
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.

1 participant