Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
95c7277
feat: add ViewerElement.SourceId and document metres and Z up
mahach666 Nov 12, 2026
1f8df99
Add IOViewer.IfcIO adapter for IfcIO models
mahach666 Nov 12, 2026
ce3083e
Document the IfcIO adapter
mahach666 Nov 12, 2026
938f802
Skip IFC type objects in the IfcIO adapter and document its limits
mahach666 Nov 12, 2026
edee6b0
Freeze the 0.2.0 JSON snapshots as legacy fixtures
mahach666 Nov 12, 2026
e9c8b52
Remove the IfcIO adapter from IOViewer
mahach666 Nov 12, 2026
2855bdd
Build the viewer on the IOScene scene model
mahach666 Nov 12, 2026
af377d4
Port the fixture generator, SyntheticModel and e2e tests to the scene
mahach666 Nov 12, 2026
77729c2
Document the scene-based API in the README and docs
mahach666 Nov 12, 2026
cfdd7d0
Record the 0.3.0 break in the changelog and set version 0.3.0-preview.1
mahach666 Nov 12, 2026
907031c
Publish NuGet packages before the GitHub release
mahach666 Nov 12, 2026
4f1d3b9
Describe the local feed for IOScene previews in CONTRIBUTING
mahach666 Nov 12, 2026
2199b96
Keep only the legacy task and clash references that 0.2.0 resolved
mahach666 Nov 12, 2026
a7e02c0
Record that a snapshot link name without a linked file is listed on t…
mahach666 Nov 12, 2026
5024cb4
Read viewer JSON from the stream instead of a full copy
mahach666 Nov 12, 2026
037f57b
Build on IOScene 0.1.0-preview.2 and follow its settled contract
mahach666 Nov 12, 2026
e3c032c
Test the GLB colour conversion in its linear segment and when clamped
mahach666 Nov 12, 2026
c38a7d3
Show property sets with the same name as one group without dropping p…
mahach666 Nov 12, 2026
8a686d7
Find the suffix of a repeated property name without searching from (2…
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
40 changes: 25 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ jobs:
if ($env:GITHUB_REF_NAME -cne "v$($version.Trim())") {
throw "The release tag must match the package version v$($version.Trim())."
}
# Before anything is published: a tag whose commit has no changelog section for the version fails here.
- name: Extract release notes
shell: pwsh
run: |
$version = $env:GITHUB_REF_NAME.Substring(1)
$changelog = Get-Content -LiteralPath CHANGELOG.md -Encoding utf8
$start = [array]::FindIndex($changelog, [Predicate[string]] { param($line) $line.StartsWith("## [$version]") })
if ($start -lt 0) { throw "CHANGELOG.md has no entry for $version" }
$notes = @()
for ($i = $start + 1; $i -lt $changelog.Count -and -not $changelog[$i].StartsWith('## ['); $i++) { $notes += $changelog[$i] }
$notes | Set-Content -LiteralPath release-notes.md -Encoding utf8
- run: dotnet restore IOViewer.slnx
- run: dotnet build IOViewer.slnx -c Release --no-restore
- run: dotnet test IOViewer.slnx -c Release --no-build
Expand All @@ -37,21 +48,8 @@ jobs:
with:
name: packages
path: artifacts/*
- name: Create release
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = $env:GITHUB_REF_NAME.Substring(1)
$changelog = Get-Content -LiteralPath CHANGELOG.md -Encoding utf8
$start = [array]::FindIndex($changelog, [Predicate[string]] { param($line) $line.StartsWith("## [$version]") })
if ($start -lt 0) { throw "CHANGELOG.md has no entry for $version" }
$notes = @()
for ($i = $start + 1; $i -lt $changelog.Count -and -not $changelog[$i].StartsWith('## ['); $i++) { $notes += $changelog[$i] }
$notes | Set-Content -LiteralPath release-notes.md -Encoding utf8
$files = @(Get-ChildItem artifacts -File | ForEach-Object { $_.FullName })
gh release create $env:GITHUB_REF_NAME --title $env:GITHUB_REF_NAME --notes-file release-notes.md @files
if ($LASTEXITCODE -ne 0) { throw "gh release create failed with exit code $LASTEXITCODE" }
# The push comes before the GitHub release: a refused first publication of a package id can be fixed and the
# job run again, because --skip-duplicate makes the push repeatable and no release exists yet.
# Trusted publishing: the NuGet policy for this repository and workflow file exchanges the GitHub OIDC token for a short-lived key.
- uses: NuGet/login@v1
id: nuget-login
Expand All @@ -64,3 +62,15 @@ jobs:
dotnet nuget push $package.FullName --api-key '${{ steps.nuget-login.outputs.NUGET_API_KEY }}' --source https://api.nuget.org/v3/index.json --skip-duplicate
if ($LASTEXITCODE -ne 0) { throw "dotnet nuget push failed for $($package.Name) with exit code $LASTEXITCODE" }
}
- name: Create release
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = $env:GITHUB_REF_NAME.Substring(1)
$files = @(Get-ChildItem artifacts -File | ForEach-Object { $_.FullName })
# A version with a hyphen is a prerelease.
$flags = @()
if ($version.Contains('-')) { $flags += '--prerelease' }
gh release create $env:GITHUB_REF_NAME --title $env:GITHUB_REF_NAME --notes-file release-notes.md @flags @files
if ($LASTEXITCODE -ne 0) { throw "gh release create failed with exit code $LASTEXITCODE" }
87 changes: 87 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,93 @@ API between minor releases.

## [Unreleased]

Version 0.3.0 builds the viewer on the scene model of the [IOScene](https://github.com/IOExtend/IOScene) package
(`IOScene` `[0.1.0-preview.2, 0.2.0-0)`, a new dependency of the `IOViewer` package). This is a breaking change of the
library API. What breaks for existing users:

1. The model types move to namespace `IOScene` with `Scene*` names, `ViewerClash*` included; `doc.Elements` becomes
`doc.Scene.Elements`, and the members in the table below change.
2. The documented quick start (a `ViewerMesh` with its own vertices) becomes `SceneDocument.AddMesh`.
3. Written JSON has a new shape. IOViewer 0.2.0 reads a 0.3.0 file as an empty document with exit code 0, because it
ignores unknown members.
4. A page regenerated from an old snapshot shows each mesh without an element as an element (in the element count, the
tree and hide).

Not broken: loading 0.1/0.2 JSON, existing HTML pages, the `ioviewer` arguments, the importer to exporter pipeline
(`GlbViewerImporter` to `HtmlViewerExporter`, `ViewerDocument.Title`) and the one-package install (`IOScene` comes with
the `IOViewer` package). The `IOViewer.Cli` tool only gains inputs.

### Migration from 0.2.0

| 0.2.0 (`IOViewer.Model`) | 0.3.0 |
|---|---|
| `Vector2d`, `Vector3d`, `BoundingBox3d`, `Transform3d`, `RgbaColor` | the same names in namespace `IOScene` |
| `Vector4d`, `Quaterniond` | removed, no replacement |
| `ViewerDocument.Elements`, `Meshes`, `Geometries`, `Materials`, `Textures`, `Viewpoints`, `ClashTests` | `ViewerDocument.Scene.Elements`, `Instances`, `Geometries`, `Materials`, `Textures`, `Viewpoints`, `ClashTests` |
| `ViewerDocument.SourcePath`, `Author`, `CreatedAt`, `Application` | `Scene.Models[0].Path`, `Author`, `CreatedAt`, `Application` |
| `ViewerDocument.Grids` (one grid system) | `Scene.Grids` (a list of `SceneGridSystem`, each with `ModelId`) |
| `ViewerDocument.TimeLiner` | `Scene.Schedules` (a list of `SceneSchedule`) |
| `ViewerDocument.LinkedFiles` (`ViewerLinkedFile` `Name`, `Path`, `Visible`) | `Scene.Models` (`SceneModel` with a unique `Key`, `Name`, `Path`); `Visible = false` is the model's key in `ViewerDocument.HiddenModelKeys` |
| `ViewerDocument.BoundingBox`, `ComputeBoundingBox()` | `Scene.ComputeBounds()` |
| `ViewerDocument.Title` (default `3D BIM Model`) | kept; it reads and writes `Scene.Title` (default empty) |
| `ViewerDocument.SectionPlanes`, `SectionBox`, `ViewerSectionPlane`, `ViewerSectionBox` | kept; their `Vector3d` is `IOScene.Vector3d` |
| - | `ViewerDocument.Scene`, `ViewerDocument.HiddenModelKeys`, `new ViewerDocument(SceneDocument)` |
| `ViewerElement` | `SceneElement` |
| `ViewerElement.LinkName` | `SceneElement.ModelId`, the id of a `SceneModel` |
| `ViewerElement.Level` (a string) | `SceneElement.LevelId`, the id of a `SceneLevel` in `Scene.Levels` |
| `ViewerElement.Properties`, `AddProperty(group, name, value)` | `ScenePropertySet` in `Scene.PropertySets` (`Name` = group), referenced by `SceneElement.PropertySetIds`; each `SceneProperty` has a `SceneValue` (`SceneValue.FromText` for a string) |
| `ViewerElement.MeshIndices`, `ViewerElement.BoundingBox` | removed; `SceneInstance.ElementId` links an element to its geometry |
| `ViewerMesh` | `SceneInstance`, which places a `SceneGeometry` for an element |
| `ViewerMesh.Vertices`, `Indices`, `Normals`, `UVs` (own geometry) | a `SceneGeometry` placed by one instance; `SceneDocument.AddMesh` adds both and the element |
| `ViewerMesh.ElementId` (optional) | `SceneInstance.ElementId` (required: every instance belongs to an element) |
| `ViewerMesh.GeometryId`, `Transform`, `MaterialId`, `Name` | `SceneInstance.GeometryId`, `Transform`, `MaterialId`, `Name` |
| `ViewerMesh.Color`, `ViewerMesh.Opacity`, `ViewerMaterial.Opacity` | the alpha of `SceneInstance.Color` or `SceneMaterial.Color`, the only opacity |
| `ViewerMesh.TextureIndex`, `ViewerMaterial.TextureIndex` | `SceneMaterial.TextureId`, the id of a `SceneTexture` |
| `ViewerMesh.Id`, `BoundingBox`, `ComputeBoundingBox()` | removed |
| `ViewerGeometry` | `SceneGeometry`, with `Topology` (`Triangles`, `Lines`, `Points`); empty `Indices` means non-indexed |
| `ViewerMaterial` | `SceneMaterial` |
| `ViewerTexture` (`Base64Data` string) | `SceneTexture` (`Data` bytes) |
| `ViewerGridSystem`, `ViewerGridLine` | `SceneGridSystem`, `SceneGridLine` |
| `ViewerViewpoint`, `ViewerCamera`, `ViewerProjection` | `SceneViewpoint`, `SceneCamera`, `SceneProjection` |
| `ViewerTimeLinerSchedule` | `SceneSchedule`; `GetOverallStart()` and `GetOverallEnd()` are removed |
| `ViewerTimeLinerTask`, `ViewerTimeLinerTaskType` | `SceneTask`, `SceneTaskType` |
| `ViewerTimeLinerTask.AttachedElementIds` | `SceneTask.ElementIds` |
| `ViewerTimeLinerTask.AttachedSetName` | removed |
| `ViewerClashTest`, `ViewerClashResult`, `ViewerClashStatus` | `SceneClashTest`, `SceneClashResult`, `SceneClashStatus` |
| `ViewerClashResult.Distance` (`double`) | `SceneClashResult.Distance` (`double?`, null when the source does not report it) |
| constructors with parameters (`new ViewerViewpoint(name, camera)` and others) | object initializers |

### Added
- `HtmlViewerExporter.Save`, `Export` and `ExportToString` take a `SceneDocument` too.
- `JsonViewerImporter` and the `ioviewer` tool read three kinds of JSON: viewer JSON (`"Kind": "ioviewer"`), IOScene
scene files (`"Kind": "ioscene"`) and IOViewer 0.1/0.2 snapshots (no `Kind`). In a snapshot, meshes that share an
`ElementId` no element has get one element with that id, and each other mesh without an element gets one of its own.
A newer format, a missing or wrong `Format` and an unknown kind are rejected with `InvalidDataException`.
- The HTML viewer shows `SceneElement.SourceId`, the element's id in its source format (such as a Revit UniqueId or an
IFC GlobalId), as its own row in the properties panel ("Source ID" / "ID источника"), and the model tree search finds
it. It is written to the page's element list only when set.

### Changed
- `JsonViewerExporter` writes viewer JSON format 2: `{ "Kind": "ioviewer", "Format": 2, "Scene", "SectionPlanes",
"SectionBox", "HiddenModelKeys" }`, with the scene in the encoding of IOScene's `SceneJson`.
- `GlbViewerImporter` and `ObjViewerImporter` return documents whose scene has one model and passes
`SceneDocument.Validate()`.
- The viewer draws triangles only: instances of line or point geometry are skipped, and the `ioviewer` tool prints a
warning with their number.
- Opacity is a colour alpha byte, so an opacity that is not a multiple of 1/255 is rounded (0.35 becomes 89/255).
- In an IOViewer 0.1/0.2 snapshot, an element `LinkName` that matches no linked file becomes a model of the scene, so
a page made from the snapshot lists it in the linked files panel, where its elements can be hidden.
- The documentation states the model's units and axes: all coordinates and lengths are in metres, with Z up.
- A clash distance is nullable. The page writes `null` for a distance the source does not report and the clash list
shows no distance for it, where it showed 0. A 0.1/0.2 snapshot keeps the distance it has (0 when it has none).
- `GlbViewerImporter` converts a material's base colour factor from glTF's linear values to sRGB. Imported GLB colours
look lighter than in 0.2.0, which is now correct. The alpha stays linear; every channel is now rounded to the nearest
byte, where it was truncated.
- A 4D task of a type this version does not know is shown as a construct task; it was left out of the schedule.
- The properties panel shows the property sets of an element that have the same name as one group, and a property name
that repeats in a group gets a numbered suffix (`Height (2)`), so no property is lost. Elements that share an id,
which `SceneDocument.Validate()` rejects, show the properties of all of them.

## [0.2.0] - 2026-11-12

### Fixed
Expand Down
30 changes: 28 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,30 @@ 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.

### Local feed for unpublished packages

The model data comes from the `IOScene` package. `Directory.Packages.props`
references it with a range such as `[0.1.0-preview.1, 0.2.0-0)`: a patch of
IOScene is additive and a minor is breaking. NuGet restores the lowest version
in the range that it finds.

To build against an IOScene version that is not on nuget.org yet (an IOScene
preview, or the lower bound itself before it is published), pack IOScene into
a local folder and pass that folder:

```sh
dotnet build IOViewer.slnx -c Release -p:RestoreAdditionalProjectSources=<local-feed>
dotnet test IOViewer.slnx -c Release --no-build
```

For a newer preview, also raise the lower bound of the range to it. Once a
restore has put the package in the NuGet global packages folder, later
restores, `dotnet run` of the tools and `npm run e2e` find it there. Do not
commit a `nuget.config` that points to a local feed, and commit a lower bound
only when that version is on nuget.org: until then, restoring the solution
fails in the CI build job, the Docs workflow and `release.yml`, so do not tag
a release before then.

### Viewer

The HTML viewer is a JavaScript project in `src/IOViewer/Viewer` (three.js,
Expand Down Expand Up @@ -104,8 +128,10 @@ Write commit messages in the imperative mood, for example,
## Releases

Push a tag matching the package version, such as `v0.2.0`, to run `release.yml`.
The workflow builds and tests the solution, attaches packages, symbols, source and checksums to a GitHub release,
and publishes the NuGet packages through Trusted Publishing. The release notes are the section of `CHANGELOG.md` for the version, so add that section before the tag.
The workflow builds and tests the solution, publishes the NuGet packages through Trusted Publishing, and then attaches
packages, symbols, source and checksums to a GitHub release, marked as a prerelease when the version has a hyphen
(`v0.3.0-preview.1`). The release notes are the section of `CHANGELOG.md` for the version, so add that section before
the tag; the workflow stops before publishing when it is missing.
The NuGet policy must name this repository, `release.yml`, the configured NuGet profile and the `IOViewer*` package pattern.

## Licensing contributions
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>0.2.0</Version>
<Version>0.3.0-preview.1</Version>
<Authors>mahach666</Authors>
<Copyright>Copyright (c) 2026 mahach666</Copyright>
<Product>IOViewer</Product>
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Patch = additive, minor = breaking while IOScene is 0.x; the -0 keeps out previews of the next breaking line. -->
<PackageVersion Include="IOScene" Version="[0.1.0-preview.2, 0.2.0-0)" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.11.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="4.0.1" />
Expand Down
Loading
Loading