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/RvtIO/**'
- 'src/RvtIO.Scene/**'
- '.github/workflows/docs.yml'
pull_request:
paths:
- 'docs/**'
- 'docfx.json'
- '.config/dotnet-tools.json'
- 'src/RvtIO/**'
- 'src/RvtIO.Scene/**'
- '.github/workflows/docs.yml'
workflow_dispatch:

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ API between minor releases.

### Added

- Preview project `src/RvtIO.Scene`: `RvtSceneReader` turns a document into a scene of the `IOScene` preview contract, with the element meshes in metres (shared symbols as instanced geometry), the Revit UniqueId as the source id, the IFC GlobalId of Revit's IFC export as the IfcGuid, names, categories, materials and the instance and type parameters as property sets of their display text. See [the scene adapter](docs/scene.md).
- Package `RvtIO.Scene`: the scene adapter is now published on the RvtIO release tag, depends on exactly the same RvtIO version and needs IOScene 0.1.0 (a release that would depend on an IOScene preview fails).
- API: `ElementMeshes.Extract` (namespace `RvtIO.Meshes`) returns the triangle meshes of the drawable model elements of an opened `RvtDocument`, in feet, without reading the file again: per element its own triangles (one mesh per material, flat normals, counter-clockwise around them) and, for an element that is exactly its family symbols placed rigidly, the placements of shared symbol meshes in the frame of the family, with a coverage per element (complete, partial, box in place of a rescaled symbol, no geometry, undecoded; partial also when a placed symbol or a symbol nested in one has no geometry, or a Geometry node that Revit exports has no decoded polygon). A cut or clipped instance, an instance with faces of its own and a scaled one keep their own triangles; a mirrored placement keeps the winding of its symbol and carries the mirror in its transform. The drawable elements follow a subset of the product rules of the IFC export of the current profile (listed in the `ElementMeshes` remarks, with those not applied, such as the "export to IFC" flag and topography, so the meshes can include elements the IFC leaves out); the face materials and the hidden Geometry nodes follow its rules. The types are listed in [the public API](docs/public-api.md). IFC and JSON output is unchanged.
- CLI and diagnostics: `--strict` exits with code 6 (`RvtExitCodes.PartialRead`) instead of 0 when a read finishes partial without exceeding a reader limit (a stream member failed to decode, was rebuilt by structural recovery, or decoded without a matching checksum, or `Global/ElemTable` decoded no row). Nothing is exported when exit code 6 is returned; `limits.strict` records the option and `issues[]` lists the affected streams and reasons (`failed-members`, `recovered-members`, `unverified-members`, `no-rows`) alongside `streams[]`. Default behavior without `--strict` is unchanged. (Closes #54)
- Default output, current IFC profile: every beam with a straight drive curve gets `Pset_BeamCommon.Slope`, the angle in degrees of its drive curve to the horizontal (0 for a horizontal beam, never negative), whether or not BaseQuantities are exported and whatever its body; its quantities are unchanged. A beam without a straight drive curve gets no Slope.
Expand Down
5 changes: 5 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>

<ItemGroup>
<!-- The scene contract of RvtIO.Scene: any patch of the 0.1 line, never a preview of the next (breaking) minor. -->
<PackageVersion Include="IOScene" Version="[0.1.0-preview.2, 0.2.0-0)" />
</ItemGroup>

<ItemGroup>
<!-- Build-time only. -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="11.0.100" />
Expand Down
1 change: 1 addition & 0 deletions RvtIO.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<Folder Name="/src/">
<Project Path="src/RvtIO/RvtIO.csproj" />
<Project Path="src/RvtIO.Cli/RvtIO.Cli.csproj" />
<Project Path="src/RvtIO.Scene/RvtIO.Scene.csproj" />
</Folder>
<Project Path="tests/RvtIO.Tests/RvtIO.Tests.csproj" />
</Solution>
5 changes: 3 additions & 2 deletions docfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
"metadata": [
{
"src": [
{ "src": "src/RvtIO", "files": ["RvtIO.csproj"] }
{ "src": "src/RvtIO", "files": ["RvtIO.csproj"] },
{ "src": "src/RvtIO.Scene", "files": ["RvtIO.Scene.csproj"] }
],
"dest": "docs/api",
"properties": { "TargetFramework": "net10.0" },
"properties": { "TargetFramework": "net10.0", "TargetFrameworks": "net10.0" },
"outputFormat": "apiPage"
}
],
Expand Down
89 changes: 89 additions & 0 deletions docs/scene.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Scene adapter

`RvtIO.Scene` turns a Revit project read by RvtIO into a scene of the `IOScene` package: a `SceneDocument` with one
model, the drawable elements, their triangle meshes in metres, their names and their parameters. A viewer or another
consumer of the scene reads it without knowing Revit. The scene itself is specified in the contract of the `IOScene`
repository (`docs/contract.md`), and the property keys it writes are registered there in `docs/keys.md`.

The adapter is a preview. It is published as the package `RvtIO.Scene` on each RvtIO release tag, depends on exactly
the same RvtIO version and needs IOScene 0.1.0 or a later 0.1 release.

## Usage

```csharp
using IOScene;
using RvtIO.Scene;

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

`Read(path)` opens the file as the command line tool opens it for an IFC export (`R22Layout`, English system family
names, faces with distinct openings kept apart). `Read(document, options)` takes a document you opened yourself; it
needs `RvtSceneOptions.ModelKey`, because a document does not know its file name. `Read(document, meshes, options)` also
takes the meshes you extracted from that document yourself, for example with `ElementMeshes.Extract` and options of your
own. Open a file of a release other than
Revit 2022 with `R22Layout = true`, or its elements have no geometry and the result has the error `geometry-undecoded`.
Only `Read(path)` reports the diagnostics of the read (`read-cut`, `failed-members`, `recovered-members`,
`unverified-members`), as only it opens the file; if you open the document yourself, read them from the
`RvtReaderOptions.Diagnostics` you opened it with.

`RvtSceneResult` holds the `Document`, the `Diagnostics` and counts: elements, elements with geometry, elements by
coverage, geometries, instances and symbol placements.

## What the scene holds

| Scene | From |
|---|---|
| `SceneModel.Key` | `RvtSceneOptions.ModelKey`, else the file name without its directory (`project.rvt`). This derivation does not change. |
| `SceneModel` | `SourceFormat` `rvt`, `SourceLengthUnit` `ft`, `Producer` `RvtIO.Scene <version>`. |
| `SceneElement` | One per drawable element of `ElementMeshes.Extract` (the elements the IFC export would consider products, see the remarks of `ElementMeshes`). `Id` is the element id. |
| `SourceId` | The Revit `UniqueId`. This derivation does not change; a golden test pins it on a fixture file. |
| `IfcGuid` | The GlobalId Revit's IFC export gives the element: the value of its built-in IfcGUID parameter (`IFC_GUID`, matched by id as RvtIO's IFC export matches it) when it is a valid 22-character GlobalId; else the export id of the `UniqueId` (the episode GUID with its last 32 bits XORed with the element id the `UniqueId` ends with) in the IFC base-64 form. It depends on the element only, never on export order or options. This derivation does not change; the golden test pins it. |
| `Name`, `Category`, `TypeName` | `Element.Name`; the English name of the built-in category (else `Element.Category`); the name of the type element. |
| `SceneGeometry`, `SceneInstance` | A symbol placed whole is one geometry per mesh in the frame of its family, and each placement is one instance per mesh with the placement's transform. A mirrored placement keeps its transform as it is: the winding of the triangles is not changed. A mesh of an element's own is a geometry used once, with vertices relative to the lowest corner of the element's meshes, which is the translation of its instances. All lengths are feet x 0.3048, in the internal coordinates of the document, Z up. |
| `SceneMaterial` | One per material element that a mesh uses: its name and its colour (`MATERIAL_PARAM_COLOR`, alpha from `MATERIAL_PARAM_TRANSPARENCY`), else a neutral grey. |
| `ScenePropertySet` | `revit:instance`: the parameters of the element. `revit:type`: the parameters of its type, one set shared by every element of the type. A parameter without a value is left out, and so is one whose value the reader only estimates (`Parameter.IsApproximate`), such as the fixed thermal values of a wall. |
| `SceneProperty` | Name: `Definition.Name` (for a built-in parameter its enum name, such as `WALL_USER_HEIGHT_PARAM`). Key: `revit:bip:<id>` for a built-in parameter, none for the others. Value: text, the display string of the public parameter API (see below). |

### Property text

A value is written as the text `Parameter.AsValueString` shows, with the invariant culture, and only when that text is
the one Revit shows; any other value is left out. By storage type:

| Storage | Text |
|---|---|
| String | The stored text. |
| Double | The value converted to its display unit and the unit symbol, as `AsValueString` writes it from `UnitSymbol` and `ConversionFactor` (`3200.4 mm`), when the unit was resolved from the parameter's measurement spec (`SpecType`) and is mm, m², m³, ° or A: the units whose factor converts Revit's internal unit (foot, square foot, cubic foot, radian, ampere). Left out: a value without a spec, which is every built-in double (the reader then guesses `UnitSymbol` from the parameter name, which gives the factor `TEXT_WIDTH_SCALE` the unit mm), a unit the reader converts with a factor of 1 that is not Revit's conversion (W, V, °C, Pa, ...), and an area per unit length, which the reader shows in m². |
| Integer | A shared, project or family parameter: the number in decimal; such an integer is a plain integer or a Yes/No, which `AsValueString` shows as `1` or `0`. A built-in integer is left out: it may be an enumeration or a Yes/No, and the public API cannot tell it from a number. |
| ElementId | Left out: the public API does not give the name Revit shows for the referenced element. |

The text a value is written with under a key is frozen with the key: changing it is a breaking release.

## Diagnostics

| Code | Severity | Meaning |
|---|---|---|
| `geometry-undecoded` | error | The geometry of the file was not decoded; open it with `R22Layout`. |
| `read-cut` | error | A reader limit cut a stream while the file was read (`Read(path)` only); elements may be missing. |
| `failed-members` | error | Gzip members of a stream failed to decode (`Read(path)` only); elements may be missing. One per stream. |
| `recovered-members` | warning | Gzip members of a stream were rebuilt by structural recovery (`Read(path)` only); their elements may be incomplete or wrong. One per stream. |
| `unverified-members` | warning | Gzip members of a stream decoded without a matching checksum, not counting the rebuilt ones (`Read(path)` only); their elements may be incomplete or wrong. One per stream. |
| `undecoded` | warning | An element of such a file; it is in the scene without geometry. |
| `no-geometry` | warning | An element whose geometry record has nothing that could be drawn; it is in the scene without geometry. |
| `partial-geometry` | warning | Faces that could not be triangulated, or a placed symbol or Geometry node without a decoded polygon. |
| `box-substitute` | warning | A rescaled symbol drawn as its bounding box. |
| `no-source-id` | warning | Elements without a UniqueId; they cannot be identified outside the scene. |

## Limits

- Values are text only, and only those whose display string the public API gives (see Property text). Typed values
(numbers in SI units, booleans, element references) wait for public RvtIO API.
- Shared, project and family parameters have no key yet; the format of their keys is registered with the first release.
- No levels (`LevelId` and the `Levels` table stay empty): RvtIO has no public level of an element.
- No tree: every element is a top-level element of the model. No links, grids or views.
- Materials have a name and a colour; no roughness, metalness or textures.
- Element names are those of `RvtDocument`; the validated names the IFC export applies to files read by their own class
schema are not public API.
7 changes: 4 additions & 3 deletions docs/test-fixtures.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ folder (`TestFiles.Scratch`); no test uses a fixed absolute path.

## What runs without the private files

With no private file at all, 10424 of the 10867 test cases run and pass on net8.0 and 443 are skipped (net10.0: 10428 of
10871, 443 skipped): 437 need private files (listed below; 17 of them only `bim_dump.txt` or `formats_latest_decompressed.bin`)
With no private file at all, 10450 of the 10894 test cases run and pass on net8.0 and 444 are skipped (net10.0: 10454 of
10898, 444 skipped): 438 need private files (listed below; 17 of them only `bim_dump.txt` or `formats_latest_decompressed.bin`)
and 6 need other inputs (two need symbolic links, three
need `RVTIO_FREEFORM_FULL`, `RVTIO_CYL_FULL` or `RVTIO_MESH_FULL`, one is a known gap). The tests that run use synthetic data built
in the tests and the small fixtures committed under `tests/RvtIO.Tests/Fixtures`. With the full private fixture folder the
Expand All @@ -56,7 +56,7 @@ Run with the private files: `RVTIO_TEST_FILES=<folder> dotnet test` (PowerShell:

## Tests that need the private files

294 test methods (437 cases, theory rows counted) are skipped without the files:
295 test methods (438 cases, theory rows counted) are skipped without the files:

| Class | Methods | Cases | Files |
|---|---|---|---|
Expand Down Expand Up @@ -108,6 +108,7 @@ Run with the private files: `RVTIO_TEST_FILES=<folder> dotnet test` (PowerShell:
| `RailingQuantitySourceTests` | 4 | 18 | TestData blocks |
| `RecordRoutingTests` | 1 | 1 | TESTFILE.rvt |
| `ReferenceApiTests` | 18 | 18 | TESTFILE.rvt, bim_dump.txt |
| `RvtSceneIdentitySampleTests` | 1 | 1 | TESTFILE.rvt |
| `SchemaModelTests` | 1 | 1 | Test_2022.rvt |
| `SchemaReleaseTests` | 2 | 7 | Test_2020.rvt, Test_2022.rvt, Test_2025.rvt, TESTFILE.rvt |
| `SchemaStreamEditorSampleTests` | 2 | 2 | any `.rvt` project files |
Expand Down
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.md
- name: Export report and exit codes
href: export-report.md
- name: Coverage report
Expand Down
37 changes: 37 additions & 0 deletions src/RvtIO.Scene/RvtIO.Scene.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Turns a Revit project read by RvtIO into an IOScene scene document.</Description>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<RootNamespace>RvtIO.Scene</RootNamespace>
<AssemblyName>RvtIO.Scene</AssemblyName>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- The IOScene range ends at 0.2.0-0 so that previews of the next breaking line stay out; NU5104 flags that bound. -->
<NoWarn>$(NoWarn);NU5104</NoWarn>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="RvtIO.Tests" />
</ItemGroup>

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

<ItemGroup>
<!-- The package icon is the engine's, which the shared pack metadata otherwise expects beside this project. -->
<None Remove="$(MSBuildProjectDirectory)/icon.png" />
<None Include="..\RvtIO\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>
10 changes: 10 additions & 0 deletions src/RvtIO.Scene/RvtSceneOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace RvtIO.Scene;

/// <summary>Options of <see cref="RvtSceneReader"/>.</summary>
public sealed class RvtSceneOptions
{
/// <summary>The key of the model in the scene (<c>SceneModel.Key</c>). With the <c>SourceId</c> of an element it identifies the
/// element outside the scene, so it must stay the same for every conversion of one file. Null takes the file name without its
/// directory (with the extension) when a path is read; a document passed as it is has no file name and needs this key.</summary>
public string? ModelKey { get; set; }
}
Loading
Loading