Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ on:
- 'docfx.json'
- '.config/dotnet-tools.json'
- 'src/IfcIO/**'
- 'src/IfcIO.Scene/**'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'docs/**'
- 'docfx.json'
- '.config/dotnet-tools.json'
- 'src/IfcIO/**'
- 'src/IfcIO.Scene/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

Expand Down
50 changes: 30 additions & 20 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 -and $env:GITHUB_REF_NAME.StartsWith("v") -and $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 commit without a changelog section for the version fails here.
- name: Extract release notes
shell: pwsh
run: |
$version = if ($env:GITHUB_REF_NAME -and $env:GITHUB_REF_NAME.StartsWith("v")) { $env:GITHUB_REF_NAME.Substring(1) } else { (dotnet msbuild src/IfcIO/IfcIO.csproj -getProperty:Version -nologo).Trim() }
$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 IfcIO.slnx
- run: dotnet build IfcIO.slnx -c Release --no-restore
- run: dotnet test IfcIO.slnx -c Release --no-build
Expand All @@ -37,39 +48,38 @@ jobs:
with:
name: packages
path: artifacts/*
# 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.
# 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
with:
user: mahach
- name: Publish NuGet packages
shell: pwsh
run: |
foreach ($package in Get-ChildItem artifacts -Filter *.nupkg) {
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 or update release
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$version = if ($env:GITHUB_REF_NAME -and $env:GITHUB_REF_NAME.StartsWith("v")) { $env:GITHUB_REF_NAME.Substring(1) } else { (dotnet msbuild src/IfcIO/IfcIO.csproj -getProperty:Version -nologo).Trim() }
$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 })
$tagName = if ($env:GITHUB_REF_NAME -and $env:GITHUB_REF_NAME.StartsWith("v")) { $env:GITHUB_REF_NAME } else { "v$version" }
# A tag with a hyphen is a prerelease.
$flags = @()
if ($tagName.Contains('-')) { $flags += '--prerelease' }

$exists = gh release view $tagName 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "Updating existing release $tagName..."
gh release edit $tagName --notes-file release-notes.md
gh release edit $tagName --notes-file release-notes.md @flags
gh release upload $tagName @files --clobber
} else {
Write-Host "Creating release $tagName..."
gh release create $tagName --title "IfcIO $tagName" --notes-file release-notes.md @files
}
# 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
with:
user: mahach
- name: Publish NuGet packages
shell: pwsh
run: |
foreach ($package in Get-ChildItem artifacts -Filter *.nupkg) {
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" }
gh release create $tagName --title "IfcIO $tagName" --notes-file release-notes.md @flags @files
}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to the `IfcIO` library are documented in this file.

## [Unreleased]

### Added
- **`IfcIO.Scene`**: `IfcSceneReader` converts an IFC file or `IfcModel` into an `IOScene` `SceneDocument` in metres with Z up (GlobalIds as `SourceId` and `IfcGuid`, storeys as levels and tree nodes, property and quantity sets keyed `ifc:`, materials, 4D schedules), with diagnostics for elements without geometry. See `docs/scene-adapter.md`.
- **`IfcIO.Scene` NuGet package**: published on the `IfcIO` release tag and depends on exactly the same `IfcIO` version (`[X.Y.Z]`). It needs the released `IOScene` 0.1.0; the release fails while it would depend on an `IOScene` preview.

### Fixed
- **World placement of geometry read from IFC (`IfcModel.Open`)**: element meshes are now in world coordinates. Previously object placements were ignored and every element sat at its local origin.
- `IfcLocalPlacement` chains (`PlacementRelTo`) are resolved up to the root, relative to the `WorldCoordinateSystem` of the 3D `Model` representation context.
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Report security vulnerabilities privately through the **Security** tab.
- Keep nullable reference types enabled.
- Warnings are treated as errors.

## Scene adapter

`src/IfcIO.Scene` converts IfcIO models into `IOScene` documents and uses only the public API of `IfcIO`; the `IfcIO`
package itself keeps no dependencies. `IOScene` is referenced with the range `[0.1.0-preview.2, 0.2.0-0)`. To build
against an `IOScene` preview that is not on NuGet.org yet, restore from a local folder of packages:
`dotnet build IfcIO.slnx -p:RestoreAdditionalProjectSources=<folder>`. Changing the `SceneModel.Key`, `SourceId` or
`IfcGuid` the adapter writes, its `ifc:` key format, or the value form (`Text`, `Kind`, `Unit`) it writes under a key
fails the golden identity test and is a breaking release.

## Releases & NuGet Publishing

Push a tag matching the package version, such as `v0.1.0`, to trigger the `release.yml` workflow.
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageVersion Include="IOScene" Version="[0.1.0-preview.2, 0.2.0-0)" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.4" />
Expand Down
2 changes: 2 additions & 0 deletions IfcIO.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<Folder Name="/src/">
<Project Path="src/IfcIO/IfcIO.csproj" />
<Project Path="src/IfcIO.Cli/IfcIO.Cli.csproj" />
<Project Path="src/IfcIO.Scene/IfcIO.Scene.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/IfcIO.Scene.Tests/IfcIO.Scene.Tests.csproj" />
<Project Path="tests/IfcIO.Tests/IfcIO.Tests.csproj" />
</Folder>
</Solution>
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ model.Save("Building_4D_5D.ifc");

See the [public API](docs/public-api.md) for the supported surface and [getting started guide](docs/getting-started.md) for detailed workflows.

`IfcIO.Scene` (a separate package released with `IfcIO`, from `IOScene` 0.1.0 on) converts IFC models into `IOScene` scene documents for viewers and coordination tools; see the [scene adapter](docs/scene-adapter.md) page for usage and limits.

## Documentation

The documentation site with API references and guides is at <https://ioextend.github.io/IfcIO/>. [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/IOExtend/IfcIO) offers an automatically generated overview of the repository.
Expand Down
3 changes: 2 additions & 1 deletion docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"metadata": [
{
"src": [
{ "src": "src/IfcIO", "files": ["IfcIO.csproj"] }
{ "src": "src/IfcIO", "files": ["IfcIO.csproj"] },
{ "src": "src/IfcIO.Scene", "files": ["IfcIO.Scene.csproj"] }
],
"dest": "docs/api",
"properties": { "TargetFramework": "net10.0" },
Expand Down
104 changes: 104 additions & 0 deletions docs/scene-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: Scene adapter
---

# IfcIO.Scene: IFC to IOScene

`IfcIO.Scene` converts an IFC file read by IfcIO into an `IOScene` `SceneDocument`: one scene model in metres with
Z up, with elements, storeys, property sets, quantities, materials and 4D schedules. Viewers and coordination tools
that read scenes work from it. The scene model and its rules are specified in the
[IOScene contract](https://github.com/IOExtend/IOScene/blob/main/docs/contract.md); property keys are registered in
its [key registry](https://github.com/IOExtend/IOScene/blob/main/docs/keys.md).

Status: preview. The `IfcIO.Scene` package is released with IfcIO, on the same tag, and depends on exactly the same
`IfcIO` version. Its first release needs `IOScene` 0.1.0: until then it references `IOScene` with the range
`[0.1.0-preview.2, 0.2.0-0)` and a release fails on purpose.

## Usage

```csharp
using IfcIO.Scene;
using IOScene;

var result = IfcSceneReader.Read("house.ifc");
foreach (var diagnostic in result.Diagnostics)
{
Console.WriteLine($"{diagnostic.Severity} {diagnostic.Code} {diagnostic.SourceId}: {diagnostic.Message}");
}
SceneJson.Save(result.Document, "house.ioscene.json");
```

`IfcSceneReader.Read(path, options)` opens `.ifc` and `.ifczip` files with `IfcModel.Open`. A model already in memory
is converted with `IfcSceneReader.Read(model, options)`; it has no file name, so `IfcSceneOptions.ModelKey` is
required. An empty `ModelKey` throws `ArgumentException` in both methods. `IfcSceneOptions.IncludeSpaces` also converts spaces (off by default, because closed room volumes hide the
elements inside them).

`IfcSceneResult` holds the `Document`, the `Diagnostics` of the conversion, the model's `LengthUnit` and the exact
`MetresPerUnit` factor, and the counts `ElementCount`, `ElementsWithGeometry`, `GeometryCount` and `SkippedTasks`.

## Mapping

| IFC, as IfcIO reads it | Scene |
|---|---|
| the file | one `SceneModel`: `Key` = `ModelKey`, else the file name without directory (`house.ifc`); `SourceFormat` `ifc`; `SourceLengthUnit` `mm`, `m`, `ft` or `in`; `Producer` `IfcIO.Scene <version>` |
| storey | a `SceneLevel` (elevation in metres) and a tree node of kind `Level` with the storey's GlobalId |
| element (`IfcModel.AllElements`) | a `SceneElement` of kind `Element`, child of its storey node: `Id` = STEP id, `SourceId` = `IfcGuid` = GlobalId, `Category` = IFC class in upper case, `TypeName` = type product name |
| property set, quantity set | a `ScenePropertySet` with key `ifc:<SetName>`, properties `ifc:<SetName>.<Name>`; a set shared by elements in the file is one scene set. `PropertySetIds` lists the element's property sets, then its quantity sets |
| property value | `Text`, as IfcIO reads it |
| count quantity | `Number` with `Unit` null (dimensionless) |
| length, area, volume, weight quantity | `Text` only: the number as IfcIO reads it, in the file's units, not converted |
| element mesh | one `SceneGeometry` (triangles, metres, world coordinates) placed once by a `SceneInstance` without transform |
| surface style colour | a `SceneMaterial` (alpha is the opacity); elements without one get a colour for their IFC class on the instance |
| work schedule | a `SceneSchedule` per work schedule with tasks; task `Id` = task GlobalId, elements by scene id |

`(SceneModel.Key, SourceId)` is the identity that may be stored outside the scene. How this adapter derives both, and
`IfcGuid`, the `ifc:` key format and the value form (`Text`, `Kind`, `Unit`) it writes under each key, are frozen and
covered by a golden identity test (`tests/IfcIO.Scene.Tests/Fixtures/placement.identity.txt`); changing any of them
is a breaking release.

### Values

When a `SceneValue` has a `Number`, its `Unit` is authoritative and a null `Unit` means dimensionless. The adapter
converts no property or quantity value to SI, so it writes a number only when the value is dimensionless:

- A count quantity (`IfcQuantityCount`) is `Number` with `Unit` null; `Text` is the same number.
- Length, area, volume and weight quantities are in the file's units, which IfcIO does not report for quantities, so
they are `Kind = Text` with the number only in `Text` (invariant culture, as IfcIO reads it). IfcIO does not read
time quantities.
- Property values are `Text` as IfcIO reads them. IfcIO keeps a value read from a file as text without its measure
type (`IfcLengthMeasure`, `IfcInteger`, ...), so the adapter cannot tell a dimensionless value from a dimensioned
one and does not type it.

The value form written under each key (`Text`, `Kind`, `Unit`) is frozen with the key format.

### Coordinates

Vertices are in world coordinates, in metres, and every instance has no transform, which the contract allows. A file
placed far from the origin (for example at map coordinates) therefore gives large vertex coordinates; a consumer
rebases them, for example on `ComputeBounds().Center`, before narrowing them to float32.

## Limits

- **IFC4 tessellated geometry is not read.** IfcIO's reader returns no mesh for `IfcTriangulatedFaceSet`, including
the one IfcIO itself writes for IFC4 models: the point and index lists lose their inner brackets while being parsed,
so no triangle is found. Such elements are in the scene without geometry and reported as `no-geometry`. Verified
with an IFC4 model saved by IfcIO and opened again; a test pins this behaviour.
- Mapped items (`IfcMappedItem`) are baked into each element's world mesh, so shared geometry is not shared in the
scene.
- IfcIO places an element in a storey only through `IfcRelContainedInSpatialStructure`. Elements contained in the site
or building, and spaces aggregated to a storey (`IfcRelAggregates`), are in the first storey. A file without storeys
gets IfcIO's default storey `Level 1`, which has no GlobalId. Site, building and project are not tree nodes.
- IfcIO lists type objects (`IfcWallType`, `IfcDoorStyle`) and door and window lining and panel properties among the
elements; the adapter skips them. Type property sets are not read yet; when they are, they will use the same
`ifc:` keys and come after the occurrence sets in `PropertySetIds`, so a lookup by key finds the occurrence value
first.
- Property values are text even when the IFC value is a number or a boolean: IfcIO keeps only the text.
- Length, area, volume and weight quantities are text in file units. For a file whose length unit IfcIO converts to
metres while reading (for example centimetres), geometry is in metres but quantities stay in file units.
- An element without a GlobalId in the file gets a new random GlobalId from IfcIO each time it is read, so its
`SourceId` is not stable.
- Transparency is not read from files by IfcIO; materials built in memory keep their alpha.
- Tasks without planned start and finish are left out (IfcIO reads task times from IFC4 files only); their subtasks
are kept. Task costs and progress are not converted. Openings, ports, grids, zones, systems and cost schedules are
not converted.
- The application, author and creation date of the file are not read by IfcIO, so the scene model leaves them empty.
2 changes: 2 additions & 0 deletions docs/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
items:
- name: Public API
href: public-api.md
- name: Scene adapter
href: scene-adapter.md
- name: API reference
href: api/
- name: GitHub
Expand Down
30 changes: 30 additions & 0 deletions src/IfcIO.Scene/IfcIO.Scene.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Converts IFC models read by IfcIO into IOScene documents in metres with Z up, keeping GlobalIds, storeys, property sets, quantities and 4D schedules.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- IOScene is referenced by a range whose upper bound is a prerelease ([0.N.0, 0.N+1.0-0)); NU5104 is expected. -->
<NoWarn>$(NoWarn);NU5104</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\IfcIO\IfcIO.csproj" />
<PackageReference Include="IOScene" />
</ItemGroup>

<!-- The shared pack metadata expects icon.png beside the project; the adapter ships the engine's icon. -->
<ItemGroup>
<None Remove="$(MSBuildProjectDirectory)/icon.png" />
<None Include="..\IfcIO\icon.png" Pack="true" PackagePath="/" Visible="false" />
</ItemGroup>

<!-- Adapter and engine ship on one tag, so the packed engine dependency is exact [X.Y.Z], not >=; a mix gives NU1608. -->
<Target Name="PinProjectReferencesExactly" AfterTargets="_GetProjectReferenceVersions">
<ItemGroup>
<_Pinned Include="@(_ProjectReferencesWithVersions)"><ProjectVersion>[%(_ProjectReferencesWithVersions.ProjectVersion)]</ProjectVersion></_Pinned>
<_ProjectReferencesWithVersions Remove="@(_ProjectReferencesWithVersions)" />
<_ProjectReferencesWithVersions Include="@(_Pinned)" />
</ItemGroup>
</Target>

</Project>
21 changes: 21 additions & 0 deletions src/IfcIO.Scene/IfcSceneOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace IfcIO.Scene;

/// <summary>
/// Options for <see cref="IfcSceneReader"/>.
/// </summary>
public sealed class IfcSceneOptions
{
/// <summary>
/// The key of the scene model (<c>SceneModel.Key</c>), which with each element's <c>SourceId</c> forms the identity
/// that may be stored outside the scene. Null uses the file name without directory, for example <c>house.ifc</c>.
/// Required when the model is converted from memory, where there is no file name. An empty key is invalid, and
/// <see cref="IfcSceneReader"/> throws <see cref="ArgumentException"/> for it.
/// </summary>
public string? ModelKey { get; set; }

/// <summary>
/// Also converts spaces (<c>IfcSpace</c>, <see cref="IfcModel.AllSpaces"/>), drawn translucent unless their
/// material says otherwise. False by default, because closed room volumes hide the elements inside them.
/// </summary>
public bool IncludeSpaces { get; set; }
}
Loading
Loading