Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3f2da1f
feat(model): add shared geometry with transforms and clash element ids
mahach666 Nov 11, 2026
4b72b7d
feat(glb): keep node transforms and shared meshes, convert Y-up to Z-up
mahach666 Nov 11, 2026
597399c
fix(glb): reject non-tree node graphs and traverse without recursion
mahach666 Nov 11, 2026
beab46c
feat(export): stream viewer payload format 2 with shared geometry and…
mahach666 Nov 11, 2026
cbb639e
fix(json): keep grids, schedule tasks and element properties on import
mahach666 Nov 11, 2026
2753d18
feat(viewer): three.js viewer scaffold with batched rendering
mahach666 Nov 11, 2026
be7695b
test(viewer): commit fixture generator
mahach666 Nov 11, 2026
c6bd103
fix(viewer): correct shading of mirrored instances
mahach666 Nov 11, 2026
fff050d
feat(viewer): navisworks-style navigation
mahach666 Nov 11, 2026
5b57525
fix(viewer): cache visible bounds, recover stuck drags, safe camera a…
mahach666 Nov 11, 2026
88cca81
feat(viewer): gpu picking, multi and box selection
mahach666 Nov 11, 2026
5dc842a
feat(viewer): floating panels, grouped toolbar, context menu
mahach666 Nov 11, 2026
0cbdbb7
fix(viewer): keep context menu open on panel scroll, tidy menu and to…
mahach666 Nov 11, 2026
bbcb560
feat(viewer): view cube synced to camera
mahach666 Nov 11, 2026
34ad6bc
fix(viewer): deterministic pole views and robust view cube
mahach666 Nov 11, 2026
14b934f
feat(viewer): section planes with gizmo and section box
mahach666 Nov 11, 2026
9047cfa
fix(viewer): reachable box centre handle and well-conditioned section…
mahach666 Nov 11, 2026
6a9e5ac
feat(viewer): distance measure and display modes
mahach666 Nov 11, 2026
02360a1
fix(viewer): keep overlays within camera range and contain popover keys
mahach666 Nov 11, 2026
5c43b52
feat(viewer): views tour, clashes, 4D schedule and links panels
mahach666 Nov 11, 2026
bd80ea6
fix(viewer): layered 4D and link visibility, UTC schedule dates, view…
mahach666 Nov 11, 2026
8e2f267
test(viewer): synthetic large-model generator and performance harness
mahach666 Nov 11, 2026
c224c25
perf(viewer): meet 200k element interaction targets
mahach666 Nov 11, 2026
cdb6734
fix(viewer): robust adaptive detail and honest full-frame timings
mahach666 Nov 11, 2026
6dafb75
test(viewer): timing ladder and full-detail metrics in the perf harness
mahach666 Nov 11, 2026
03bc9c7
feat(export): format 3 with grid-quantized positions and octahedral n…
mahach666 Nov 11, 2026
a781591
fix(export): keep deduplicating off-grid translated copies in format 3
mahach666 Nov 11, 2026
bebcbe2
fix(export): keep scaled shared geometry in f32 and pin f32 placement
mahach666 Nov 11, 2026
bc0f655
perf(viewer): merged spatial chunks with a slot-state texture
mahach666 Nov 11, 2026
9bcf5ce
fix(viewer): u32 index boundary, lazy normal decode, robust load and …
mahach666 Nov 11, 2026
d6b97c8
perf(viewer): instanced remainder above the merged-memory cap
mahach666 Nov 11, 2026
6f941f8
perf(viewer): linear-time cell split, direct instance matrices, no sl…
mahach666 Nov 11, 2026
a62869e
perf(viewer): start the motion-detail budget at most at an integrated…
mahach666 Nov 11, 2026
fa0e733
perf(viewer): spatially bounded instanced cells, safe selection split…
mahach666 Nov 11, 2026
cf63372
docs: describe the new viewer, add third-party notices and viewer CI
mahach666 Nov 11, 2026
ab7bcbb
docs: correct size baseline and CLI title help
mahach666 Nov 11, 2026
aa67a38
fix: robust CLI export, pickable element-less geometry, focus, wheel …
mahach666 Nov 11, 2026
0318267
test(viewer): give the instanced/merged screenshot comparison more ti…
mahach666 Nov 11, 2026
10de037
fix: shared-geometry precision, OBJ/GLB elements, mirrored winding an…
mahach666 Nov 12, 2026
9dc1daf
fix: list documents without elements by mesh ElementId or per mesh
mahach666 Nov 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
- [ ] `dotnet build IOViewer.slnx -c Release` has no warnings
- [ ] `dotnet test IOViewer.slnx -c Release` passes
- [ ] Generated HTML and JSON for the same inputs are unchanged, or the differences are listed and intended
- [ ] No customer models, personal data, vendor code or third-party JavaScript is added
- [ ] No customer models, personal data or vendor code is added; new third-party code is listed in `THIRD-PARTY-NOTICES.md`
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ updates:
directory: /
schedule:
interval: weekly
- package-ecosystem: npm
directory: /src/IOViewer/Viewer
schedule:
interval: weekly
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,29 @@ jobs:
dotnet-version: |
8.0.x
- run: dotnet build src/IOViewer.Cli/IOViewer.Cli.csproj -c Release

viewer:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/IOViewer/Viewer
steps:
- uses: actions/checkout@v7
# The e2e global setup exports the fixtures with the CLI (`dotnet run`).
- uses: actions/setup-dotnet@v6
with:
global-json-file: global.json
dotnet-version: |
8.0.x
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
cache-dependency-path: src/IOViewer/Viewer/package-lock.json
- run: npm ci
- run: npm run build
# dist/viewer.js is committed and embedded in the library: it must match the sources.
- run: git diff --exit-code dist/
- run: npm test
- run: npx playwright install --with-deps chromium
- run: npm run e2e
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ __pycache__/
# Documentation site (DocFX output and generated API pages)
_site/
docs/api/

# Viewer toolchain (npm, Playwright)
node_modules/
test-results/
playwright-report/
65 changes: 65 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,86 @@ API between minor releases.
## [Unreleased]

### Fixed
- `JsonViewerImporter` keeps grid lines, 4D tasks with their attached element ids and child tasks, clash results,
element properties and element mesh indices; they were silently dropped on load.
- The `IOViewer` package now includes the XML documentation file, so existing API comments show in IntelliSense.
- The release workflow runs only for version tags again, fails on any `gh release` error and no longer overwrites assets of an existing release.
- `tools/pack-release.ps1` fails when creating the source archive fails.
- Documentation no longer claims `.mtl` material support or mesh simplification, neither of which exists.
- The logo on the documentation site no longer covers half of the page; it is sized to the navigation bar.
- `GlbViewerImporter` and `ObjViewerImporter` create elements: one `ViewerElement` per GLB node that draws a mesh
(category `GLB`) and per OBJ object or group (category `OBJ`), so converted models have a model tree and can be
selected, hidden and isolated. Each OBJ mesh keeps only the vertices its faces use instead of every vertex read so
far, and a face that references a vertex not yet read throws `InvalidDataException` on load.
- A document without elements (built through the API or loaded from older JSON) can still be listed, selected, hidden
and isolated in the HTML viewer: meshes that share an `ElementId` appear as one element with that id, which 4D tasks
and clashes match, and each other mesh appears on its own with its mesh id and name, as in 0.1.0. The document itself
is not changed.

### Added
- A new HTML viewer built on three.js 0.186.1, embedded in the page, which still works offline without network requests:
- floating panels (model tree with search, properties, views, sections, clashes, 4D, linked files), a toolbar with
labelled groups, a status bar with selected and total counts, and a context menu;
- Navisworks-style mouse: orbit around the point under the cursor, pan with the middle or right button, zoom to the
cursor, Ctrl+click, window (left to right) and crossing (right to left) box selection; hotkeys H, I, Shift+H, F, Esc;
- a view cube drawn from the camera orientation, with face clicks, 90° arrows and a home button;
- up to six section planes moved and rotated with an in-scene gizmo, or a section box that keeps or cuts its inside,
with face handles, a centre handle and a box around the selection;
- distance measurement, display modes (materials, textures, white, wireframe), grid lines, global opacity,
perspective or orthographic camera and fullscreen;
- saved viewpoints with a tour, a clash list that flies to the clash and selects both elements, a 4D schedule player
and linked-file visibility;
- Russian or English interface, chosen from the browser language;
- a message instead of a blank page when the browser has no WebGL 2, the model data cannot be read, the graphics
context is lost or the model has more objects than the GPU can address.
- Shared geometry: `ViewerDocument.Geometries`, `ViewerGeometry`, `ViewerMesh.GeometryId`, `ViewerMesh.Transform` and
the `Transform3d` affine transform. The HTML exporter writes each shared geometry once and also writes meshes that
are exact translated copies of each other once.
- `ViewerClashResult.Element1Id` and `Element2Id`. The viewer selects clashing elements by Id, and by name only when the
name is unique.
- The HTML exporter writes the projection of saved viewpoints, so orthographic viewpoints open orthographic.
- `THIRD-PARTY-NOTICES.md` with the three.js license, included in the NuGet packages and the release archives.
- A CI job for the viewer: it rebuilds the bundle, checks that the committed `dist/viewer.js` matches the sources and
runs the unit and Playwright end-to-end tests. Dependabot also updates the viewer's npm packages.
- Links to the DeepWiki overview of the repository in the README and on the documentation site.

### Changed
- `GlbViewerImporter` keeps node transforms and shared meshes (one `ViewerGeometry` per mesh primitive, one
`ViewerMesh` per node that uses it) and converts glTF's Y-up to the viewer's Z-up, so GLB models no longer lie on
their side. A node hierarchy that is not a tree (a node referenced twice, or a cycle) now throws
`InvalidDataException`.
- `HtmlViewerExporter` throws `InvalidDataException` for indices outside a mesh's or geometry's vertices, a
`GeometryId` that is not in the document, a duplicate geometry id or a mesh with both a `GeometryId` and its own
vertices, instead of writing broken geometry. `Save` checks the data before it opens the file, so a failed export
leaves an existing file as it was, and the CLI reports such input with exit code 3.
- `HtmlViewerOptions.MaxMeshVertices` defaults to no limit (was 5,000,000) and no longer cuts the model silently: when a
lower limit leaves meshes out, the viewer shows a banner with the number of meshes written.
- The HTML payload uses format 3: one base64 binary block, positions quantized to 16-bit steps of 2⁻¹⁰ m (error at most
0.49 mm, 0.85 mm for rotated shared geometry) and octahedral 16-bit normals. Geometry larger than about 64 m, and
shared geometry that any instance scales up, stays 32-bit float. For a real 746 MB IFC water-supply model (via GLB)
the page is 44.4 MB, about the size 0.1.0 wrote (44.1 MB) although it now also carries normals; with
32-bit float positions and normals it would be 57.5 MB. The HTML is written to the output stream as it is produced.
- The viewer merges geometry into spatial chunks per material and keeps per-object state (hidden, selected, tint) in a
texture, so hide, isolate, selection and 4D colouring never rebuild geometry. When merged data would exceed
384 MiB, repeated shapes are drawn as instanced copies in spatially bounded cells instead.
- Measured on an integrated 2-CU AMD GPU: a model of 200,000 elements and 19.2 million triangles opens in about 1.3 s,
orbits at 30 fps at full detail (60 fps on an RTX 4080), draws a full frame in 30.3 ms and shows a click, hide or
show all in about 52 ms, using about 327 MB of GPU memory. A model of 1,000,000 elements opens in about 2.2 s, draws
a full frame in 0.21 s and uses about 88 MB of GPU memory; it orbits at 20 fps (95th percentile), picking an object
takes about 22 ms and a still frame costs about 23 ms of CPU time.
- Transparent objects are sorted back to front per chunk or cell, not per object.
- Package descriptions and documentation say "no external dependencies, works offline" instead of "zero-dependency".
- Tests run on both .NET 8 and .NET 10.
- `global.json`, CI workflows and repository settings follow the shared IOExtend conventions: SDK 10.0.100 without prerelease, NuGet vulnerability audit, Dependabot, LF line endings, issue and pull request templates.
- Copyright holder is `mahach666` in the license, package metadata and README.
- README and documentation state that IOViewer is not affiliated with Autodesk.
- `SECURITY.md` and `CONTRIBUTING.md` describe supported versions, security scope, development setup and the clean-room policy.
- `CONTRIBUTING.md` describes the viewer toolchain (Node.js 24, `npm ci`, `npm run build`, `npm test`, `npm run e2e`,
manual `npm run perf`).

### Removed
- The turntable (automatic 360° rotation) of the old viewer.
- The previous hand-written WebGL viewer template, replaced by the three.js viewer.

## [0.1.0] - 2026-11-11

Expand Down
34 changes: 32 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,42 @@ The library is in `src/IOViewer`, the CLI in `src/IOViewer.Cli`, and tests in
`tests/IOViewer.Tests`. Tests use small synthetic models built in code.
The repository ships no customer models.

### Viewer

The HTML viewer is a JavaScript project in `src/IOViewer/Viewer` (three.js,
bundled with esbuild). Its built script `dist/viewer.js` is committed and
embedded in the library, so .NET work does not need Node.js. To change the
viewer, install Node.js 24 and run from `src/IOViewer/Viewer`:

```sh
npm ci # exact versions from package-lock.json
npm run build # rebuilds dist/viewer.js
npm test # unit tests (node --test)
npm run e2e # builds, exports the fixtures with the CLI and runs Playwright (Chromium)
```

Commit `dist/viewer.js` together with the source change: CI rebuilds it and
fails when it differs from the committed file. The end-to-end tests need the
.NET SDK and a Chromium for Playwright (`npx playwright install chromium`).
Fixtures are in `tests/fixtures/viewer`.

`npm run perf` runs the manual performance harness in a visible Chromium
window (for example `PERF_LADDER=25000,200000` measures synthetic models made
by `tools/SyntheticModel`); its settings are described at the top of
`test/perf/orbit.perf.js`. It is not part of CI; include its numbers in the
pull request when a change can affect rendering speed. npm dependencies are
pinned to exact versions. When updating three.js (for example a Dependabot pull
request), also rebuild and commit `dist/viewer.js` and update the version in
`THIRD-PARTY-NOTICES.md`.

## Clean-room policy

- Write all code independently. Do not use decompiled or leaked source code,
proprietary headers, vendor SDKs or code copied from other viewers.
- Do not add third-party JavaScript to the generated HTML. The viewer runs on
plain WebGL and must stay self-contained and offline.
- The only third-party JavaScript in the generated HTML is three.js (MIT),
listed in `THIRD-PARTY-NOTICES.md`. Discuss any other runtime dependency
first and add its license there. The page must stay self-contained and
work offline: no CDN, web fonts or network requests.
- Do not add customer models, confidential data, vendor files or binaries to
the repository. Keep private test files outside version control.
- Do not put personal data in code, documentation, test data or commit
Expand Down
3 changes: 2 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<Authors>mahach666</Authors>
<Copyright>Copyright (c) 2026 mahach666</Copyright>
<Product>IOViewer</Product>
<Description>Fast, pure managed .NET library and CLI tool for generating standalone, zero-dependency interactive 3D WebGL/HTML BIM &amp; CAD model viewers.</Description>
<Description>Fast, pure managed .NET library and CLI tool for generating standalone interactive 3D WebGL/HTML BIM &amp; CAD model viewers with no external dependencies that work offline.</Description>
<NeutralLanguage>en</NeutralLanguage>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/IOExtend/IOViewer</PackageProjectUrl>
Expand All @@ -32,6 +32,7 @@
<ItemGroup Condition="'$(IsPackable)' != 'false' And '$(MSBuildProjectName)' != 'IOViewer.Tests'">
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="/" Visible="false" />
<None Include="$(MSBuildProjectDirectory)/icon.png" Pack="true" PackagePath="/" Visible="false" />
<None Include="$(MSBuildThisFileDirectory)THIRD-PARTY-NOTICES.md" Pack="true" PackagePath="/" Visible="false" />
</ItemGroup>

<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
Expand Down
Loading
Loading