Skip to content

Add a public API for the triangle meshes of the model elements - #197

Merged
mahach666 merged 12 commits into
mainfrom
feat/public-mesh-api
Nov 12, 2026
Merged

mahach666 merged 12 commits into
mainfrom
feat/public-mesh-api

Conversation

@mahach666

@mahach666 mahach666 commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

What changes and why

Adds a public mesh API in RvtIO.Meshes, so that adapters can read the triangle meshes of a document without InternalsVisibleTo.

  • ElementMeshes.Extract(document, options) returns a MeshSet for an opened RvtDocument, made from the faces the document already holds (the file is not read again), in feet, Revit internal coordinates, Z up.
  • Per element (ElementMesh): its own triangles, one TriangleMesh per material (flat normals, counter-clockwise around them), or placements (SymbolPlacement: symbol id and MeshTransform) of shared symbol meshes in MeshSet.Symbols; plus a MeshCoverage and the number of faces that did not triangulate.
  • A placement is used only when the element is exactly its symbols placed rigidly: no faces of its own, nothing cut by the tight box, every symbol resolved, no box in place of a rescaled component, orthonormal transforms. A cut, clipped or scaled instance keeps its own triangles. A mirrored placement keeps the winding of its symbol and carries the mirror in its transform (negative Determinant). ElementMeshOptions.UseSymbolPlacements = false gives every element its own triangles.
  • Coverage is Partial when a placed symbol, or a symbol nested in one at any depth, has no geometry, in both modes. The instance expansion passes a nested symbol that placed nothing up to the element, the same way it passes a box substitute.
  • The drawable elements follow a subset of the product rules of the IFC export of the current profile; face materials and hidden Geometry nodes follow its rules. The rules not applied (the "export to IFC" flag, topography and others) are listed in the ElementMeshes remarks, so the meshes can include elements the IFC leaves out.
  • A file of a release other than Revit 2022 has its geometry decoded only with RvtReaderOptions.R22Layout; without it MeshSet.GeometryUndecoded is true and its elements are Undecoded. The R22Layout doc comment and the docs now say so (they named only Revit 2023 and later).
  • Internal groundwork, no output change: the instance expansion records on an element's geometry whether a placed symbol was clipped, replaced by its box or unresolved; the product rules shared with the IFC export move from IfcExporter into the internal ProductRules (and ExportPhaseFilter into RvtIO.Model), so ElementMeshes does not build an IfcExporter.
  • Docs: README, docs index, getting started (example), public API (the new types), CHANGELOG [Unreleased] / Added. Existing public types are unchanged; IFC and JSON output is unchanged.

Also in this branch, not part of the mesh API:

  • release.yml: the release notes are extracted before anything is published, the NuGet push runs before gh release create (a refused first publication can be fixed and the job run again), and a tag with a hyphen creates a pre-release.
  • docs/test-fixtures.md: the counts of the tests that run without the private fixtures are updated, including the sample test classes added to main since the last count.

Tests

Restored with IOScene from a local package folder; RvtIO itself needs nothing unpublished.

  • Build (dotnet build RvtIO.slnx -c Release): 0 errors, 191 warnings, the same warning set as on main and before the merge of main.
  • Without the private fixtures: net8.0 10867 cases, 10424 passed, 443 skipped; net10.0 10871 cases, 10428 passed, 443 skipped. These are the counts in docs/test-fixtures.md, and the skipped classes match its table row for row.
  • With the private fixture folder: net8.0 6 failed, 10740 passed, 121 skipped; net10.0 6 failed, 10744 passed, 121 skipped. The 6 failures (ApiTests.Proxy289128_* x3, ValidatedTests.FamilyInstance289128_BBox_MatchesReference, ValidatedTests.Wall263597_BBox_MatchesReference, ElementHeaderBoundingBoxTests.StoredOutline_IsTheElementBoxOfEveryRelease(release: 2022)) fail with the same messages on main (fdb75ca); they are not caused by this branch.
  • New ElementMeshesTests (synthetic, part of the counts above): 37/37 on both TFMs. Triangulation with holes and winding, untaken and ghost Geometry nodes, materials, placement versus own triangles for clipped, scaled, mirrored and box-substitute instances, coverage (including a symbol without geometry nested one and two levels down in a placed symbol, for a family instance and a transform-only instance, in both modes, and nested symbols that all have geometry staying complete), definitions and containers left out, MeshTransform from the row-major instance transform, null document. ComputedSymbolGeometryTests gains a case for a computed symbol that places a symbol without geometry. Of the 9 nested-symbol cases, 7 fail before that fix and 2 guard against a false partial.
  • Nested-symbol fix: the RvtIO CLI output of two private Revit 2022 samples is the same before and after (compact and full JSON byte-identical; IFC differs only in the FILE_NAME timestamp), and the coverage of every element of five private samples is unchanged in both modes, so none of them has such a nested symbol.
  • New ElementMeshesSampleTests (a private Revit 2022 sample, a private Revit 2025 sample and a private door-in-wall sample): 5/5 on both TFMs with the private files, skipped without them.
  • IFC and compact JSON of the samples after moving the product rules into ProductRules: identical to the base (recorded in that commit).
  • R22Layout: measured on private Revit 2020 and 2025 samples (no element geometry decoded without it); the private Revit 2022 samples give the same meshes either way.
  • Release workflow and docfx build: not run.

Notes

  • Merge order: independent of the IOScene work; no other PR has to merge first, and this one can merge on its own. The branch is on top of current main (fdb75ca, Add --strict option and exit code 6 for partial reads #194).
  • Purely additive public API (ElementMeshes, ElementMeshOptions, MeshSet, ElementMesh, TriangleMesh, SymbolPlacement, MeshTransform, MeshVector, MeshCoverage).
  • Follow-up, not in this PR: adapters that read the decoded faces through InternalsVisibleTo can move to ElementMeshes once a release contains it.

The instance expansion now notes on the element's geometry whether the
tight box of the GRep cut a placed symbol or left it out
(ClipGeometryToBounds reports whether it changed the faces), whether a
rescaled component was replaced by its box, and whether a GInstance
resolved to nothing. The reader also notes whether the geometry of a
64-bit-id file was left undecoded because it was read without the R22
rewrite. The flags are internal and change no output.
The phase reference, type element, transform-only family, non-physical
category, wall opening and export phase checks of the product loop
become internal members so that another export can apply the same
rules. The IFC output is unchanged.
ElementMeshes.Extract returns, for an opened document, the triangles of
every drawable element in feet: its own triangles per material, or the
placements of shared symbol meshes when the element is shown to be
exactly its symbols placed (no own faces, nothing clipped, every symbol
resolved, no box substitute, rigid transforms). A mirrored placement
keeps the winding of its symbol and carries the mirror in its
transform; own triangles are wound around the normals of their faces.
Hidden Geometry nodes, face materials and the element selection follow
the IFC export of the current profile. Each element reports its
coverage, and the set reports a document whose geometry was not
decoded.
…into ProductRules

ElementMeshes no longer builds an IfcExporter for the export phase filter. The
rules are moved without change; IFC and compact JSON output of the samples is
identical to the base.
The IFC export drops the faces of untaken nodes only when the geometry has
no ghost node; the meshes now apply the same condition, a symbol is shared
only when it drops the same nodes as the element, and an element or symbol
with ghost nodes is reported as Partial.

Document the positional properties of MeshVector and MeshTransform, give
ElementMeshOptions a settable property like the other options types, and
name the IFC product rules that ElementMeshes does not apply.
The reader keeps its own Revit 2022 layout only for Revit 2022 files; every other file it reads by the file's own class schema and decodes no element geometry without the option. Measured on the Revit 2020 and 2025 samples, which the docs had left out by naming Revit 2023 and later.
The release notes are extracted before anything is published, the push runs before gh release create so that a refused first publication can be fixed and the job run again, and a tag with a hyphen creates a pre-release.
@mahach666
mahach666 merged commit 3e7d8ce into main Nov 12, 2026
4 checks passed
@mahach666
mahach666 deleted the feat/public-mesh-api 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