diff --git a/changelog.d/research/1492-shading-plates-not-saved-meshes.md b/changelog.d/research/1492-shading-plates-not-saved-meshes.md new file mode 100644 index 00000000..78ce2f6e --- /dev/null +++ b/changelog.d/research/1492-shading-plates-not-saved-meshes.md @@ -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. diff --git a/reports/element-framing/RE-78-saved-graphics-meshes.md b/reports/element-framing/RE-78-saved-graphics-meshes.md index 92349c52..4ab395c5 100644 --- a/reports/element-framing/RE-78-saved-graphics-meshes.md +++ b/reports/element-framing/RE-78-saved-graphics-meshes.md @@ -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. @@ -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 | @@ -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. diff --git a/tools/re/shading_bodies_vs_ifc.py b/tools/re/shading_bodies_vs_ifc.py new file mode 100644 index 00000000..0aebe0d0 --- /dev/null +++ b/tools/re/shading_bodies_vs_ifc.py @@ -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 +""" + +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())