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
16 changes: 16 additions & 0 deletions changelog.d/research/1492-shading-plates-not-saved-meshes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- **Core Interior's 20 shading devices were never saved-mesh bodies on
`main`: RE-78's 158 was the count before RE-95 (#1492).** RE-78 counted 158
saved-mesh bodies on Core Interior (132 doors, 6 windows, 20 shading
devices), and `mesh_bodies_vs_ifc` and `rvt-ifc --saved-meshes` count 138.
Nothing changed on `main`. The 158 was measured on 2026-09-27, when the 20
plates and the 116 rooms were record boxes; RE-95 (#467) closed the plates'
sketch outlines and RE-101 (#473) gave the rooms their stored-solid
outlines on 2026-09-28, both before the saved-mesh option landed on
2026-09-30, and `saved_meshes::attach` replaces only a body whose profile is
not resolved, as it did when it landed (73861770). No body that measured
exact was lost, and RE-78 now says so under its first table. The new
`tools/re/shading_bodies_vs_ifc.py` scores the plates' bodies by shape
against Revit's, which neither `mesh_bodies_vs_ifc` (saved meshes only) nor
`plan_profiles_vs_ifc` (plan outline only) did: on Core Interior all 20
are within 0.01% of Revit's volume and every vertex within 0.001 ft of its
body's surface (the largest distance 1.5e-06 ft), Measure run 42133202790.
10 changes: 7 additions & 3 deletions reports/element-framing/RE-78-saved-graphics-meshes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
**Issues:** #255, #227
**Credit:** rosejn (#255: the native object-graph and saved-graphics reader, `native_saved_scene` and the modules under it, extracted with their authorship).

**Result:** positive for doors, shading devices, columns, members, plates, furniture and most fixtures; negative for windows. Opt-in.
**Result:** positive for doors, columns, members, plates, furniture and most fixtures; negative for windows. Opt-in. Core Interior's shading devices, which this result first named, and its rooms are not saved-mesh bodies on `main` (the note under the first table, 2026-11-05, #1492).

- A Revit file keeps the graphics it last drew for each element. rosejn's reader decodes them per ElementId in document feet, at a chosen detail level (Fine here).
- `saved_meshes::attach` replaces every body that is only its element record's bounding box (`BodySource` `partition_element_record_bbox`, no resolved profile) with the element's saved mesh, in the element's own placement. Elements drawn from decoded data keep their bodies, and so do aggregate wholes (stairs, curtain walls), whose parts are already drawn. A Tag carried by several entities is left alone.
Expand All @@ -17,7 +17,7 @@
| file | category | n | within 0.01 ft, saved mesh | within 0.01 ft, box |
|---|---|---:|---:|---:|
| Core Interior (2024) | IfcDoor | 132 | 132 | 0 |
| | IfcShadingDevice | 20 | 20 | |
| | IfcShadingDevice, before RE-95 (see the note below) | 20 | 20 | |
| | IfcWindow | 6 | 6 | |
| Snowdon Towers (2024) | IfcMember | 1,595 | 1,565 | 641 |
| | IfcPlate | 484 | 461 | 228 |
Expand All @@ -31,7 +31,11 @@
| | IfcCovering | 68 | 68 | 51 |
| | all replaced | 3,736 | 3,332 | 1,138 |

Core Interior's 116 rooms also take their saved mesh: the room volume, with the same extents as the record box but the room's own outline (up to 31 triangles, not 12). Revit's reference export carries no spaces to score them against.
Core Interior's 116 rooms also took their saved mesh when this was measured: the room volume, with the same extents as the record box but the room's own outline (up to 31 triangles, not 12). Revit's reference export carries no spaces to score them against.

**Note, 2026-11-05 (#1492): Core Interior's 20 shading devices and 116 rooms are not saved-mesh bodies on `main`, and never were.** The two Core Interior rows above and the sentence on its rooms were measured on 2026-09-27, when the 20 shading-device plates and the rooms were still record boxes. RE-95 (#467, merged 2026-09-28) closed the plates' sketch outlines, so each plate is drawn as an extrusion of its own 28-vertex outline with one void, and RE-101 (#473, merged 2026-09-28) gave each room the outline of its stored solid and `ProfileResolved` true (RE-313: 116 of 116 rooms on a stored solid). `saved_meshes::attach` replaces a body only where its `BodySource` is the record box and its `ProfileResolved` is false, and that condition is in `src/ifc/saved_meshes.rs` as it stands at 73861770, the first commit on `main` with `--saved-meshes`, whose history holds both #467 and #473. So the option never drew the plates or the rooms on `main`, and no body that measured exact was lost: they stopped being record boxes before they could be replaced by a mesh. Core Interior's candidates are its 132 doors and 6 windows, 138 in all, which is what `rvt-ifc --saved-meshes` prints (`138 of 138 bounding-box bodies drawn from their saved graphics`, Measure run 41850769925 at `e024f03c`); the count of 158 in the rows above is the branch's from before its rebase onto `main` on 2026-09-30, and PR #420's description repeats it. The Snowdon rows are from the same earlier tree, are local only and have not been re-run.

**The 20 plates as they are drawn, against Revit's export.** Measure run [42133202790](https://github.com/DrunkOnJava/rvt-rs/actions/runs/42133202790) (Core Interior only, `--saved-meshes`, code `daf361021a4d2905972c7598858efb67cda28008`, scorers from the branch of this note, which added `tools/re/shading_bodies_vs_ifc.py`): `shading_bodies_vs_ifc` counts the 20 as `BodySource` `partition_element_record_bbox` with `ProfileResolved` true (the record-box source stays on a body whose outline is resolved, so the property to read is `ProfileResolved`), and scores each against Revit's body of the same Tag, both meshed by IfcOpenShell in their own site frames: volume within 0.01% on 20 of 20 (median ratio 1.0000), every vertex of each body within 0.001 ft of the other's surface on 20 of 20, and Revit's vertices on rvt-rs's surface on 20 of 20. The largest distance either way is 1.539e-06 ft (rvt-rs's vertices to Revit's surface; Revit's to rvt-rs's, 5.7e-14 ft at most), on 10 plates of `Floor:Structural Slab` and 10 of `Floor:Arch Topping Slab - 2"`. `plan_profiles_vs_ifc` in the same run scores their plan outline: area within 0.1%, vertices and outline within 0.01 ft, each on 20 of 20 (the 80 `IfcSlab` as well). The same run's `mesh_bodies_vs_ifc` and `saved_meshes_vs_ifc` score 138 bodies, 132 doors and 6 windows, no plate and no room, and `rvt-ifc` prints `138 of 138 bounding-box bodies drawn from their saved graphics`. So the plates' bodies are Revit's to the export's own precision, and replacing them with their saved meshes could not improve on them; nothing was lost. Not measured: the plates' saved meshes against Revit's (the option leaves the plates alone, so no run draws them), and the rooms' shape (Revit's export holds no spaces to score them against). To reproduce once this note has landed: `gh workflow run measure.yml --ref main -f ref=daf361021a4d2905972c7598858efb67cda28008 -f base=none -f models=core-interior -f ifc_flags=--saved-meshes -f scorers="plan_profiles_vs_ifc mesh_bodies_vs_ifc saved_meshes_vs_ifc shading_bodies_vs_ifc"`.

Snowdon Towers: 3,789 of 4,744 bounding-box bodies replaced, 2.55 M triangles, 37 s and 5.6 GB peak memory for the export (`--profile ci`). Without the option the IFC is 12 MB.

Expand Down
75 changes: 75 additions & 0 deletions tools/re/shading_bodies_vs_ifc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Score the shape of rvt-rs's IfcShadingDevice bodies against the body Revit's
own IFC export writes for the same element (#1492, RE-78).

Core Interior's 20 shading devices are floors that Revit's export writes as
`IfcShadingDevice`. They were record boxes until RE-95 closed their sketch
outlines, so they are drawn from those outlines, and `saved_meshes::attach`
leaves a body whose profile is resolved alone: they are never saved-mesh
bodies, and `mesh_bodies_vs_ifc.py`, which scores `BodySource`
`saved_graphics_mesh`, does not meet them. `plan_profiles_vs_ifc.py` scores
their plan outline and not their thickness. This is `mesh_bodies_vs_ifc.py`
with another selection, every `IfcShadingDevice` with a Tag whatever its
`BodySource`: per class and under it per Revit type, how many have Revit's
volume within 1% and 0.01%, every vertex of each body within 0.01 ft and
0.001 ft of the other's surface, and "ref on ours", then the elements
furthest off.

Before that it counts what drew the bodies it scores, by class: their
`BodySource` and `ProfileResolved`, so the table says whether the plates are
a saved mesh, a record box or an outline.

Needs IfcOpenShell and numpy.

Usage:

python3 tools/re/shading_bodies_vs_ifc.py <rvt-rs.ifc> <revit.ifc>
"""

import collections
import os
import sys

import ifcopenshell

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import mesh_bodies_vs_ifc # noqa: E402

CLASS = "IfcShadingDevice"
SOURCE = "shading_device"
MARKERS = ("BodySource", "ProfileResolved")


def markers(element):
found = {}
for rel in element.IsDefinedBy:
if not rel.is_a("IfcRelDefinesByProperties"):
continue
for prop in getattr(rel.RelatingPropertyDefinition, "HasProperties", None) or []:
if prop.Name in MARKERS and prop.NominalValue is not None:
found[prop.Name] = prop.NominalValue.wrappedValue
return found


def main():
if len(sys.argv) != 3:
print(__doc__)
return 2
counts = collections.Counter()
model = ifcopenshell.open(sys.argv[1])
for element in model.by_type(CLASS):
if element.Tag:
found = markers(element)
counts[tuple(found.get(name) for name in MARKERS)] += 1
del model
if not counts:
print(f"{CLASS} with a Tag: none")
for (source, resolved), n in sorted(counts.items(), key=lambda item: (-item[1], str(item[0]))):
print(f"{CLASS} BodySource {source} ProfileResolved {resolved}: {n}")
mesh_bodies_vs_ifc.SOURCE = SOURCE
mesh_bodies_vs_ifc.body_sources = lambda element: SOURCE if element.is_a(CLASS) else None
return mesh_bodies_vs_ifc.main()


if __name__ == "__main__":
sys.exit(main())
Loading