From c35a53827ca9850751494b3e6a24bb7db879a2b3 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:30:56 -0500 Subject: [PATCH 1/6] research(elements): probe doors and windows whose type maps all its geometry to no material (#1247) (RE-298) --- .../examples/probe_re298_unset_map_doors.rs | 161 ++++++++++++++++++ tools/re/void_only_vs_ifc.py | 16 +- 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 probes/examples/probe_re298_unset_map_doors.rs diff --git a/probes/examples/probe_re298_unset_map_doors.rs b/probes/examples/probe_re298_unset_map_doors.rs new file mode 100644 index 000000000..7eeffccdf --- /dev/null +++ b/probes/examples/probe_re298_unset_map_doors.rs @@ -0,0 +1,161 @@ +//! RE-298: doors and windows whose type draws all its geometry with no +//! material (#1247, RE-84, RE-149). +//! +//! FACT under test: RE-84 writes a door or window whose type has a value +//! block with no geometry-material map as its opening alone, as Revit's +//! export does for a family that only cuts its host. RE-82's map reader +//! takes a map only where some value is a material, so a type whose every +//! map value is unset (`0xff` x 8: geometry drawn with no material of its +//! own, RE-149) reads as having no map too. RE-149 gives such a column or +//! product `` and clears RE-84's flag; a door or window keeps the +//! flag and is written as an opening. If the all-unset map means the type +//! draws geometry, Revit exports these doors and windows as the elements. +//! +//! Prints each door and window record, with the five columns of +//! `probe_re84_void_only_families` (ElementId, class, type ElementId, the +//! type's map material count or `-`/`?`, family), then: +//! - `u` where the type's value block opens with an all-unset map +//! (`partition_type_materials::unset_material_types`), else `-`; +//! - the triangles of the element's saved graphics at fine detail, `-` where +//! it has none; +//! - its family's `shows_unset` (`native_family_forms`, RE-204): `t`, `f`, +//! `?` where undecided, `-` where the family is not read. +//! +//! `tools/re/void_only_vs_ifc.py` reads the first five columns, and counts +//! the sixth where present. +//! +//! ```text +//! cargo run --profile ci -p rvt-probes --example probe_re298_unset_map_doors -- model.rvt +//! ``` + +use std::collections::{BTreeMap, BTreeSet}; + +use rvt::research::native_document::{Options, extract_graphs}; +use rvt::research::native_family_forms::family_forms; +use rvt::research::native_saved_scene::extract_at_detail; +use rvt::research::partition_schema_mvp::{FAMILY_NAME_FIELD, TYPE_ID_FIELD}; +use rvt::research::partition_type_materials::{type_material_names, unset_material_types}; +use rvt::walker::InstanceField; + +fn main() -> anyhow::Result<()> { + let Some(path) = std::env::args().nth(1) else { + anyhow::bail!("usage: probe_re298_unset_map_doors FILE.rvt"); + }; + let mut rf = rvt::RevitFile::open(&path)?; + let version = rf.basic_file_info()?.version; + let maps = type_material_names(&mut rf, version); + let unset = unset_material_types(&mut rf, version); + let mvp = rvt::research::partition_schema_mvp::recover_partition_schema_mvp( + &mut rf, + version, + rvt::walker::WalkerLimits::default(), + )?; + let elements: Vec<_> = mvp.doors.iter().chain(&mvp.windows).collect(); + let type_of = |element: &rvt::walker::DecodedElement| { + element.fields.iter().find_map(|(name, value)| match value { + InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id), + _ => None, + }) + }; + + let types: BTreeSet = elements + .iter() + .filter_map(|e| type_of(e)) + .map(u64::from) + .collect(); + let mut family_of: BTreeMap = BTreeMap::new(); + let options = Options { + selected_ids: types, + ..Options::default() + }; + if let Err(e) = extract_graphs(&mut rf, &options, |record| { + let family = record + .graph + .as_ref() + .and_then(|graph| graph.objects.first()) + .and_then(|root| root.fields.get("m_familyId")) + .and_then(|id| id.pointer("/m_id/m_id64")) + .and_then(serde_json::Value::as_i64) + .and_then(|id| u64::try_from(id).ok()); + if let Some(family) = family { + family_of.insert(record.identity.element_id, family); + } + Ok(()) + }) { + eprintln!("type records not read: {e}"); + } + let families: BTreeSet = family_of.values().copied().collect(); + let forms = family_forms(&mut rf, &families).unwrap_or_else(|e| { + eprintln!("family forms not read: {e}"); + BTreeMap::new() + }); + + let ids: BTreeSet = elements + .iter() + .filter_map(|e| e.id) + .map(u64::from) + .collect(); + let options = Options { + selected_ids: ids, + max_graph_values: 1_000_000, + max_graph_objects: 1_000_000, + ..Default::default() + }; + let mut triangles: BTreeMap = BTreeMap::new(); + match extract_at_detail(&mut rf, &options, 3) { + Ok(scene) => { + for element in &scene.elements { + let n: usize = element + .meshes + .primitives + .iter() + .map(|p| p.triangles.len()) + .sum(); + *triangles.entry(element.identity.element_id).or_default() += n; + } + } + Err(e) => eprintln!("saved graphics not read: {e}"), + } + + for element in elements { + let type_id = type_of(element); + let family = element.fields.iter().find_map(|(name, value)| match value { + InstanceField::String(text) if name == FAMILY_NAME_FIELD => Some(text.as_str()), + _ => None, + }); + let materials = match type_id { + None => "?".to_string(), + Some(id) => maps.get(&id).map_or("-".into(), |m| m.len().to_string()), + }; + let unset_map = if type_id.is_some_and(|id| unset.contains(&id)) { + "u" + } else { + "-" + }; + let saved = element + .id + .and_then(|id| triangles.get(&u64::from(id))) + .filter(|n| **n > 0) + .map_or("-".to_string(), |n| n.to_string()); + let shows = type_id + .and_then(|id| family_of.get(&u64::from(id))) + .and_then(|family| forms.get(family)) + .map_or("-", |forms| match forms.shows_unset { + Some(true) => "t", + Some(false) => "f", + None => "?", + }); + println!( + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + element.id.map_or(String::new(), |id| id.to_string()), + element.class, + type_id.map_or(String::new(), |id| id.to_string()), + materials, + family.unwrap_or(""), + unset_map, + saved, + shows, + ); + } + Ok(()) +} diff --git a/tools/re/void_only_vs_ifc.py b/tools/re/void_only_vs_ifc.py index 4c5538d42..0152bc8fd 100644 --- a/tools/re/void_only_vs_ifc.py +++ b/tools/re/void_only_vs_ifc.py @@ -14,7 +14,10 @@ It prints the count of each outcome for types with a map (`n`), types with a value block and no map (`0`), types with no value block (`-`) and elements -with no type (`?`). +with no type (`?`). Where the probe prints more columns +(`probe_re298_unset_map_doors`: all-unset map, saved triangles, family +forms), it also prints the counts of each map count by those columns, the +saved triangles as `mesh` or `-`. Usage: @@ -48,6 +51,7 @@ def main(argv): o.Tag for o in f.by_type("IfcOpeningElement") if o.Tag and o.id() not in filled } table = collections.Counter() + extra = collections.Counter() rows = [] for line in open(args[0], encoding="utf-8"): parts = line.rstrip("\n").split("\t") @@ -63,11 +67,19 @@ def main(argv): else: outcome = "absent" table[(materials, outcome)] += 1 - rows.append((materials, outcome, tag, cls, family)) + more = parts[5:8] + if more: + if len(more) > 1: + more[1] = "-" if more[1] == "-" else "mesh" + extra[(materials, " ".join(more), outcome)] += 1 + rows.append((materials, outcome, tag, cls, family, *parts[5:8])) for materials in ("n", "0", "-", "?"): counts = {o: table[(materials, o)] for o in ("element", "opening only", "absent")} if any(counts.values()): print(f"type map materials {materials}: {counts}") + for key in sorted({(m, k) for m, k, _ in extra}): + counts = {o: extra[(*key, o)] for o in ("element", "opening only", "absent")} + print(f"type map materials {key[0]}, unset map / saved / forms {key[1]}: {counts}") if listing: for row in sorted(rows): if row[0] != "n" or row[1] != "element": From 1fcad647294b7166f1075b0144f5bdec1105bce7 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:35:22 -0500 Subject: [PATCH 2/6] research(elements): the RE-298 probe prints each type's native geometry-material map (#1247) --- .../examples/probe_re298_unset_map_doors.rs | 32 ++++++++++++++++--- tools/re/void_only_vs_ifc.py | 14 +++++--- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/probes/examples/probe_re298_unset_map_doors.rs b/probes/examples/probe_re298_unset_map_doors.rs index 7eeffccdf..005775830 100644 --- a/probes/examples/probe_re298_unset_map_doors.rs +++ b/probes/examples/probe_re298_unset_map_doors.rs @@ -19,7 +19,10 @@ //! - the triangles of the element's saved graphics at fine detail, `-` where //! it has none; //! - its family's `shows_unset` (`native_family_forms`, RE-204): `t`, `f`, -//! `?` where undecided, `-` where the family is not read. +//! `?` where undecided, `-` where the family is not read; +//! - the type's native `m_geomTag2MaterialId` (RE-203) as +//! `/`, `-` where the type record is not read: the +//! map itself, whatever either byte scan makes of it. //! //! `tools/re/void_only_vs_ifc.py` reads the first five columns, and counts //! the sixth where present. @@ -64,15 +67,32 @@ fn main() -> anyhow::Result<()> { .map(u64::from) .collect(); let mut family_of: BTreeMap = BTreeMap::new(); + let mut native_map: BTreeMap = BTreeMap::new(); let options = Options { selected_ids: types, ..Options::default() }; if let Err(e) = extract_graphs(&mut rf, &options, |record| { - let family = record + let root = record .graph .as_ref() - .and_then(|graph| graph.objects.first()) + .and_then(|graph| graph.objects.first()); + if let Some(map) = root + .and_then(|root| root.fields.get("m_geomTag2MaterialId")) + .and_then(serde_json::Value::as_array) + { + let unset = map + .iter() + .filter(|entry| { + entry + .pointer("/second/m_id/m_id64") + .and_then(serde_json::Value::as_i64) + == Some(-1) + }) + .count(); + native_map.insert(record.identity.element_id, (map.len(), unset)); + } + let family = root .and_then(|root| root.fields.get("m_familyId")) .and_then(|id| id.pointer("/m_id/m_id64")) .and_then(serde_json::Value::as_i64) @@ -145,8 +165,11 @@ fn main() -> anyhow::Result<()> { Some(false) => "f", None => "?", }); + let native = type_id + .and_then(|id| native_map.get(&u64::from(id))) + .map_or("-".to_string(), |(n, unset)| format!("{n}/{unset}")); println!( - "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", + "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", element.id.map_or(String::new(), |id| id.to_string()), element.class, type_id.map_or(String::new(), |id| id.to_string()), @@ -155,6 +178,7 @@ fn main() -> anyhow::Result<()> { unset_map, saved, shows, + native, ); } Ok(()) diff --git a/tools/re/void_only_vs_ifc.py b/tools/re/void_only_vs_ifc.py index 0152bc8fd..10ca6fc8d 100644 --- a/tools/re/void_only_vs_ifc.py +++ b/tools/re/void_only_vs_ifc.py @@ -16,8 +16,9 @@ value block and no map (`0`), types with no value block (`-`) and elements with no type (`?`). Where the probe prints more columns (`probe_re298_unset_map_doors`: all-unset map, saved triangles, family -forms), it also prints the counts of each map count by those columns, the -saved triangles as `mesh` or `-`. +forms, native map), it also prints the counts of each map count by those +columns, the saved triangles as `mesh` or `-` and the native map as +`none`, `unset` (every entry unset), `mixed` or `named` (no entry unset). Usage: @@ -67,19 +68,22 @@ def main(argv): else: outcome = "absent" table[(materials, outcome)] += 1 - more = parts[5:8] + more = parts[5:9] if more: if len(more) > 1: more[1] = "-" if more[1] == "-" else "mesh" + if len(more) > 3 and more[3] != "-": + n, unset = (int(x) for x in more[3].split("/")) + more[3] = "none" if n == 0 else "unset" if unset == n else "mixed" if unset else "named" extra[(materials, " ".join(more), outcome)] += 1 - rows.append((materials, outcome, tag, cls, family, *parts[5:8])) + rows.append((materials, outcome, tag, cls, family, *parts[5:9])) for materials in ("n", "0", "-", "?"): counts = {o: table[(materials, o)] for o in ("element", "opening only", "absent")} if any(counts.values()): print(f"type map materials {materials}: {counts}") for key in sorted({(m, k) for m, k, _ in extra}): counts = {o: extra[(*key, o)] for o in ("element", "opening only", "absent")} - print(f"type map materials {key[0]}, unset map / saved / forms {key[1]}: {counts}") + print(f"type map materials {key[0]}, unset map / saved / forms / native map {key[1]}: {counts}") if listing: for row in sorted(rows): if row[0] != "n" or row[1] != "element": From 43bcb6b5539429dac2b2b13231ad9f5bcef3f6e3 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:37:08 -0500 Subject: [PATCH 3/6] research(elements): the unset-map door probe is RE-299, RE-298 being taken (#1247) --- ...98_unset_map_doors.rs => probe_re299_unset_map_doors.rs} | 6 +++--- tools/re/void_only_vs_ifc.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename probes/examples/{probe_re298_unset_map_doors.rs => probe_re299_unset_map_doors.rs} (97%) diff --git a/probes/examples/probe_re298_unset_map_doors.rs b/probes/examples/probe_re299_unset_map_doors.rs similarity index 97% rename from probes/examples/probe_re298_unset_map_doors.rs rename to probes/examples/probe_re299_unset_map_doors.rs index 005775830..ee32fc63c 100644 --- a/probes/examples/probe_re298_unset_map_doors.rs +++ b/probes/examples/probe_re299_unset_map_doors.rs @@ -1,4 +1,4 @@ -//! RE-298: doors and windows whose type draws all its geometry with no +//! RE-299: doors and windows whose type draws all its geometry with no //! material (#1247, RE-84, RE-149). //! //! FACT under test: RE-84 writes a door or window whose type has a value @@ -28,7 +28,7 @@ //! the sixth where present. //! //! ```text -//! cargo run --profile ci -p rvt-probes --example probe_re298_unset_map_doors -- model.rvt +//! cargo run --profile ci -p rvt-probes --example probe_re299_unset_map_doors -- model.rvt //! ``` use std::collections::{BTreeMap, BTreeSet}; @@ -42,7 +42,7 @@ use rvt::walker::InstanceField; fn main() -> anyhow::Result<()> { let Some(path) = std::env::args().nth(1) else { - anyhow::bail!("usage: probe_re298_unset_map_doors FILE.rvt"); + anyhow::bail!("usage: probe_re299_unset_map_doors FILE.rvt"); }; let mut rf = rvt::RevitFile::open(&path)?; let version = rf.basic_file_info()?.version; diff --git a/tools/re/void_only_vs_ifc.py b/tools/re/void_only_vs_ifc.py index 10ca6fc8d..7bcf77222 100644 --- a/tools/re/void_only_vs_ifc.py +++ b/tools/re/void_only_vs_ifc.py @@ -15,7 +15,7 @@ It prints the count of each outcome for types with a map (`n`), types with a value block and no map (`0`), types with no value block (`-`) and elements with no type (`?`). Where the probe prints more columns -(`probe_re298_unset_map_doors`: all-unset map, saved triangles, family +(`probe_re299_unset_map_doors`: all-unset map, saved triangles, family forms, native map), it also prints the counts of each map count by those columns, the saved triangles as `mesh` or `-` and the native map as `none`, `unset` (every entry unset), `mixed` or `named` (no entry unset). From f1279a5a0c3b0e15e21e3a856b332e3d040868d8 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:54:35 -0500 Subject: [PATCH 4/6] fix(elements): a door or window whose type's native map holds entries is not written as its opening alone (#1247) (RE-299) --- .../fixed/1247-native-map-void-only-doors.md | 9 ++++ .../examples/probe_re299_unset_map_doors.rs | 25 ++++----- .../RE-299-native-map-void-only-doors.md | 51 +++++++++++++++++++ .../RE-84-void-only-doors-windows.md | 2 + src/research/native_type_materials.rs | 20 +++++++- .../partition_schema_mvp/materials.rs | 10 +++- 6 files changed, 102 insertions(+), 15 deletions(-) create mode 100644 changelog.d/fixed/1247-native-map-void-only-doors.md create mode 100644 reports/element-framing/RE-299-native-map-void-only-doors.md diff --git a/changelog.d/fixed/1247-native-map-void-only-doors.md b/changelog.d/fixed/1247-native-map-void-only-doors.md new file mode 100644 index 000000000..9fcf8db23 --- /dev/null +++ b/changelog.d/fixed/1247-native-map-void-only-doors.md @@ -0,0 +1,9 @@ +- **A door or window is no longer written as its opening alone when its + type draws geometry in no material of its own.** RE-84 takes a type + whose value block holds no geometry-material map as one that only cuts + its host; the scan of that block missed a map whose values are all unset + or name a material whose name it did not read. The type's native map + now decides: it is empty on every door and window Revit exports as its + opening alone (RE1's CasedOpening, Snowdon Towers' 47), and those keep + their openings; no export of the reference models, the BIM4LCA office, + the R2025 models or Autodesk's samples changes (#1247, RE-299). diff --git a/probes/examples/probe_re299_unset_map_doors.rs b/probes/examples/probe_re299_unset_map_doors.rs index ee32fc63c..0946bf008 100644 --- a/probes/examples/probe_re299_unset_map_doors.rs +++ b/probes/examples/probe_re299_unset_map_doors.rs @@ -1,15 +1,16 @@ -//! RE-299: doors and windows whose type draws all its geometry with no -//! material (#1247, RE-84, RE-149). +//! RE-299: which doors and windows only cut their host (#1247, RE-84). //! -//! FACT under test: RE-84 writes a door or window whose type has a value -//! block with no geometry-material map as its opening alone, as Revit's -//! export does for a family that only cuts its host. RE-82's map reader -//! takes a map only where some value is a material, so a type whose every -//! map value is unset (`0xff` x 8: geometry drawn with no material of its -//! own, RE-149) reads as having no map too. RE-149 gives such a column or -//! product `` and clears RE-84's flag; a door or window keeps the -//! flag and is written as an opening. If the all-unset map means the type -//! draws geometry, Revit exports these doors and windows as the elements. +//! FACT: RE-84 writes a door or window as its opening alone where RE-82's +//! scan of its type's value block finds no geometry-material map. That scan +//! takes a map only where every value is a material whose name it read, so +//! a type whose map values are all unset (−1, RE-149), or name a material +//! the name scan dropped, reads as having none. The type's native +//! `m_geomTag2MaterialId` (RE-203) tells them apart: every door and window +//! Revit exports as its opening alone (RE1 Architecture's CasedOpening, +//! Snowdon Towers' 9 Door-Opening and 38 Schematic Opening Cut) has a type +//! whose native map is empty, and no saved graphics, while the byte scan's +//! all-unset map (`unset_material_types`) also fires on CasedOpening and +//! Door-Opening, so it cannot. //! //! Prints each door and window record, with the five columns of //! `probe_re84_void_only_families` (ElementId, class, type ElementId, the @@ -25,7 +26,7 @@ //! map itself, whatever either byte scan makes of it. //! //! `tools/re/void_only_vs_ifc.py` reads the first five columns, and counts -//! the sixth where present. +//! the rest where present. //! //! ```text //! cargo run --profile ci -p rvt-probes --example probe_re299_unset_map_doors -- model.rvt diff --git a/reports/element-framing/RE-299-native-map-void-only-doors.md b/reports/element-framing/RE-299-native-map-void-only-doors.md new file mode 100644 index 000000000..f4efcca27 --- /dev/null +++ b/reports/element-framing/RE-299-native-map-void-only-doors.md @@ -0,0 +1,51 @@ +# RE-299 — A door or window only cuts its host where its type's native map is empty + +**Date:** 2026-11-02 +**Issues:** #1247 (73 doors of a Revit 2024 project written as `IfcOpeningElement`), #309, #227; refines RE-84 +**Probe:** `probes/examples/probe_re299_unset_map_doors.rs`; scoring `tools/re/void_only_vs_ifc.py` (now counts the probe's further columns) +**Run:** Measure [41538550670](https://github.com/DrunkOnJava/rvt-rs/actions/runs/41538550670), the probe at 1fcad647 (then named `probe_re298_unset_map_doors`, the same code), every reference model, the BIM4LCA office, puzzbobb's R2025 models, Autodesk's 36 sample projects and the held-out models +**Result:** positive. RE-84 decides that a door's or window's type draws no geometry from a byte scan of the type's value block, which misses a map whose values it cannot name. The type's native `m_geomTag2MaterialId` is the map itself: it is empty on every door and window Revit exports as its opening alone, so a type whose native map holds entries is no longer taken as drawing nothing. + +## 1. The miss + +RE-84 writes a hosted door or window as its opening alone where its type has a parameter value block and RE-82's scan of it finds no geometry-material map. The scan (`partition_type_materials::map_at`) takes a map only where every value is either unset or a material whose name `scan_material_names` read, and at least one is a material. Two kinds of map fail that and leave the type with an empty list, which `attach_type_materials` reads as "draws no geometry": + +- a map whose every value is unset (−1): a family whose geometry takes no material of its own, as one drawn entirely in its category's or subcategories' materials does (RE-149); +- a map naming a material the name scan dropped, as it drops one whose copies in different partitions hold different names. + +RE-149 already lifts the flag from columns and products whose block opens with an all-unset map (`unset_material_types`), and gives them ``. Doors and windows were left out of that on purpose, and the probe shows why that byte signal cannot decide them either: it fires on RE1's CasedOpening and on Snowdon Towers' Door-Opening, which Revit exports as openings. + +## 2. Measured + +`probe_re299_unset_map_doors` prints every door and window record with its type's scanned map count (RE-84's column), whether the type's block opens with an all-unset map, its saved-graphics triangles, its family's `shows_unset` (RE-204) and its native map as entries and unset entries. Every door and window whose type the scan reads as having no map: + +| file | elements | Revit's export | all-unset block (RE-149) | saved graphics | native map | +|---|---:|---|---|---|---| +| RE1 Architecture (2025): CasedOpening | 1 | opening only | yes | none | 0 entries | +| Snowdon Towers architectural (2024): Door-Opening | 9 | opening only (RE-84, local export) | yes | none | 0 entries | +| Snowdon Towers architectural (2024): Schematic Opening Cut | 38 | opening only (RE-84, local export) | no | none | 0 entries | + +Snowdon Towers is read here as one of Autodesk's sample projects (`research/autodesk-sample-projects.tsv`); Revit's export of it is local only, and RE-84 measured these 47 against it. No other door or window on Core Interior (138), the office as built (537) and at permit (533), RE1's four models, the R2025 models or the other 35 sample projects has a type the scan reads as having no map, so none is moved: every one of them is exported as the element, as in Revit's exports where there is one (Core Interior 138 of 138, office 537 of 537 and 533 of 533, R2025 24 of 24, RE1 Architecture 5 of 5). + +The native map holds entries on every door and window type Revit exports as the element (Core Interior's 138, all naming materials only; the office's 431 and 425 that the scan leaves out, whose maps mix materials and unset values), and on none whose instances it exports as openings. The saved graphics agree: none of the 48 openings-only has any. + +## 3. What rvt-rs does + +`refine_type_materials` already reads each column, door, window and product type's native record (RE-203); `native_type_materials::geometry_maps` now also returns the types whose `m_geomTag2MaterialId` holds any entry. A type among them that the scan left with an empty list is dropped from the list instead, so it is a type whose materials are not read rather than one that draws nothing, and its doors and windows are written as the elements, with no type material: what Revit's export gives such a door or window is not measured, so nothing is written for it. Where the native record is not read the scan decides, as before. + +Every export of the corpora above is unchanged: the 48 openings-only keep an empty native map, and no other door or window had the flag. The positive case is #1247's project, which is not public. + +## 4. Not claimed + +- What material Revit's export gives a door or window whose map is all unset (RE-149 measured `` on products only). +- Whether #1247's 73 doors have entries in their native map: the reporter's run of the probe decides it. + +## 5. Reproduce + +```bash +gh workflow run measure.yml --ref research/1247-unset-map-doors -f ref=1fcad647 -f base=none \ + -f probe=probe_re298_unset_map_doors -f probe_scorer=void_only_vs_ifc \ + -f nordic=true -f r2025=true -f samples=true -f holdout=true -f scorers=none +``` + +Where the branch is gone, push 1fcad647 to a branch first. The probe has the name `probe_re299_unset_map_doors` from 43bcb6b5 on. diff --git a/reports/element-framing/RE-84-void-only-doors-windows.md b/reports/element-framing/RE-84-void-only-doors-windows.md index 1702cc4f5..9a7d4c860 100644 --- a/reports/element-framing/RE-84-void-only-doors-windows.md +++ b/reports/element-framing/RE-84-void-only-doors-windows.md @@ -44,6 +44,8 @@ Shape: each such opening takes #227's cut (RE-83), the element's box across and The 11 Snowdon elements left are void-only doors and windows RE-23 does not bind to a host: their host wall is in their reference list but not in the slot before their own ElementId (#439). Without a host there is no wall to cut, so they stay the element for now. +Update 2026-11-02 (RE-299): the scan misses a map whose values are all unset or name a material whose name it did not read, and so read such a type as drawing nothing; the type's native `m_geomTag2MaterialId` now overrides it where it holds entries. It is empty on all 48 void-only doors and windows above. + Update 2026-09-27 (RE-85): with hosts read from anywhere in the reference list, all 47 Snowdon ones are openings voiding the wall Revit's opening voids, and no door or window is left that Revit does not export. ## Not claimed diff --git a/src/research/native_type_materials.rs b/src/research/native_type_materials.rs index 2580d51eb..6216ae372 100644 --- a/src/research/native_type_materials.rs +++ b/src/research/native_type_materials.rs @@ -36,9 +36,22 @@ pub fn geometry_materials( types: &BTreeSet, materials: &BTreeSet, ) -> anyhow::Result> { + Ok(geometry_maps(rf, types, materials)?.0) +} + +/// [`geometry_materials`], and the types of `types` whose +/// `m_geomTag2MaterialId` holds any entry, whatever its values: types that +/// draw geometry, though their map may name no material (every value −1, +/// RE-149) or only materials not among `materials` (RE-299). +pub fn geometry_maps( + rf: &mut RevitFile, + types: &BTreeSet, + materials: &BTreeSet, +) -> anyhow::Result<(BTreeMap, BTreeSet)> { let mut out = BTreeMap::new(); + let mut mapped = BTreeSet::new(); if types.is_empty() { - return Ok(out); + return Ok((out, mapped)); } let options = native_document::Options { selected_ids: types.clone(), @@ -59,6 +72,9 @@ pub fn geometry_materials( else { return Ok(()); }; + if !map.is_empty() { + mapped.insert(record.identity.element_id); + } let mut found = GeometryMaterials { family: root.fields.get("m_familyId").and_then(element_id), ..GeometryMaterials::default() @@ -84,5 +100,5 @@ pub fn geometry_materials( } Ok(()) })?; - Ok(out) + Ok((out, mapped)) } diff --git a/src/research/partition_schema_mvp/materials.rs b/src/research/partition_schema_mvp/materials.rs index 480709688..b7a493b22 100644 --- a/src/research/partition_schema_mvp/materials.rs +++ b/src/research/partition_schema_mvp/materials.rs @@ -74,9 +74,17 @@ pub(super) fn refine_type_materials( return partly_unset; }; let materials: BTreeSet = names.keys().copied().collect(); - let Ok(found) = crate::native_type_materials::geometry_materials(rf, &types, &materials) else { + let Ok((found, mapped)) = crate::native_type_materials::geometry_maps(rf, &types, &materials) + else { return partly_unset; }; + // RE-299: a type whose native map holds entries draws geometry, though + // the scan read no map in its block, every value being −1 or a material + // whose name it did not read. Left with no list it would read as a type + // that draws nothing, whose hosted doors and windows are written as + // their openings alone (RE-84). + type_materials + .retain(|type_id, list| !list.is_empty() || !mapped.contains(&u64::from(*type_id))); // A family whose geometry with no material is all hidden or void has // no `` in Revit's export (RE-204), and a material only its // hidden forms have is not exported (RE-205). From 3641b00fee71c5a5bdbc68330d3821af48f6db81 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:54:58 -0500 Subject: [PATCH 5/6] docs: RE-299's addendum to the reconnaissance (#1247) --- docs/rvt-moat-break-reconnaissance.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/rvt-moat-break-reconnaissance.md b/docs/rvt-moat-break-reconnaissance.md index c9480b8d7..0dd14e435 100644 --- a/docs/rvt-moat-break-reconnaissance.md +++ b/docs/rvt-moat-break-reconnaissance.md @@ -2244,6 +2244,10 @@ Revit's export writes a floor's `Qto_SlabBaseQuantities.NetVolume` as the volume Revit's export writes a floor's `Qto_SlabBaseQuantities.GrossArea` as the area of the profile its slab extrudes, its sketch outline less its holes: equal to rvt-rs's profile on all 40 of the BIM4LCA office's slabs drawn from their record box, where the slab's other quantities follow its body less often (a sloped slab's record box is deeper than its layers). Stair landings' slabs get none. rvt-rs writes it on each floor drawn as one extrusion: 31 equal on the office, none different. Report: `reports/element-framing/RE-249-slab-gross-area.md`. +## Addendum — RE-299 a door or window that only cuts its host (2026-11-02) + +RE-84 writes a door or window as its opening alone where the scan of its type's value block finds no geometry-material map, and the scan rejects a map whose values are all unset or name a material whose name it did not read. The type's native `m_geomTag2MaterialId` is empty on every door and window Revit exports as its opening alone (RE1's CasedOpening, Snowdon Towers' 9 Door-Opening and 38 Schematic Opening Cut, none with saved graphics), while the byte scan's all-unset map also fires on CasedOpening and Door-Opening. A type whose native map holds entries is no longer read as drawing nothing; no corpus export changes, and #1247's 73 doors are the case it is for. Report: `reports/element-framing/RE-299-native-map-void-only-doors.md`. + ## Addendum — RE-298 a flat roof's slab quantities, and a level floor's Depth (2026-10-30) Revit's export writes a slab's `GrossVolume` as its `GrossArea` times its `Depth` on all 42 of the BIM4LCA office's slabs with a volume. A flat roof written as a slab (all 10, `Basic Roof`) has the extrusion's depth, its layers' total, however many there are (3 for `YP-3`, 5 for `YP-2`, 10 for `YP-1`), its `GrossArea` is its sketch's profile's area, and its `NetVolume` is the area times the depth except on the one a hatch cuts, which holds four `WallCutoutGStep`s; rvt-rs writes these. A level floor of several layers (the 10 of types `VP-1`, `VP-2` and `AP-1`) has its last layer's thickness as `Depth` in the office's export (24.0.5), but Revit 26.4's export of the held-out Revit 2026 house writes another value on 2 such slabs, so rvt-rs writes none (the maintainer's standing rule, 2026-10-31). Report: `reports/element-framing/RE-298-slab-depth-roofs.md`. From f832c256c953824f2a49109fa868199fbfdc258a Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 2 Nov 2026 18:28:32 -0500 Subject: [PATCH 6/6] fix(elements): a type whose map is all unset and whose family shows none of that geometry keeps drawing nothing (#1247) (RE-299) --- .../RE-299-native-map-void-only-doors.md | 7 +++- src/research/native_type_materials.rs | 39 ++++++++++++++----- .../partition_schema_mvp/materials.rs | 37 +++++++++++++----- 3 files changed, 62 insertions(+), 21 deletions(-) diff --git a/reports/element-framing/RE-299-native-map-void-only-doors.md b/reports/element-framing/RE-299-native-map-void-only-doors.md index f4efcca27..8c3fd0898 100644 --- a/reports/element-framing/RE-299-native-map-void-only-doors.md +++ b/reports/element-framing/RE-299-native-map-void-only-doors.md @@ -31,7 +31,9 @@ The native map holds entries on every door and window type Revit exports as the ## 3. What rvt-rs does -`refine_type_materials` already reads each column, door, window and product type's native record (RE-203); `native_type_materials::geometry_maps` now also returns the types whose `m_geomTag2MaterialId` holds any entry. A type among them that the scan left with an empty list is dropped from the list instead, so it is a type whose materials are not read rather than one that draws nothing, and its doors and windows are written as the elements, with no type material: what Revit's export gives such a door or window is not measured, so nothing is written for it. Where the native record is not read the scan decides, as before. +`refine_type_materials` already reads each column, door, window and product type's native record (RE-203); `native_type_materials::geometry_maps` now also returns the types whose `m_geomTag2MaterialId` holds any entry, with whether every value is −1 and the type's family. A type among them that the scan left with an empty list is dropped from the list instead, so it is a type whose materials are not read rather than one that draws nothing. Its doors and windows are written as the elements, sized and named as other doors and windows are (the flag also gated RE-215's cut-loop sizes, the opening-index sizes and RE-154's type name), with no type material: what Revit's export gives such a door or window is not measured, so nothing is written for it. + +One case keeps the flag: a map whose every value is −1, of a family whose own document shows none of that geometry (`native_family_forms`, RE-204: every such form hidden or void, or no form and no nested instance). Such a type draws nothing visible, as the scan read it. No measured file holds one. Where the native record or the family is not read, the scan decides, as before. Every export of the corpora above is unchanged: the 48 openings-only keep an empty native map, and no other door or window had the flag. The positive case is #1247's project, which is not public. @@ -39,6 +41,7 @@ Every export of the corpora above is unchanged: the 48 openings-only keep an emp - What material Revit's export gives a door or window whose map is all unset (RE-149 measured `` on products only). - Whether #1247's 73 doors have entries in their native map: the reporter's run of the probe decides it. +- The all −1, hidden-or-void case of §3 is reasoned from RE-204, not measured: no file in the corpora has such a door or window type. ## 5. Reproduce @@ -48,4 +51,4 @@ gh workflow run measure.yml --ref research/1247-unset-map-doors -f ref=1fcad647 -f nordic=true -f r2025=true -f samples=true -f holdout=true -f scorers=none ``` -Where the branch is gone, push 1fcad647 to a branch first. The probe has the name `probe_re299_unset_map_doors` from 43bcb6b5 on. +Where the branch is gone, push 1fcad647 to a branch first. The probe has the name `probe_re299_unset_map_doors` from 43bcb6b5 on; once this lands, `--ref main` carries the scorer and `-f ref=` the probe. diff --git a/src/research/native_type_materials.rs b/src/research/native_type_materials.rs index 6216ae372..3dee0d5fb 100644 --- a/src/research/native_type_materials.rs +++ b/src/research/native_type_materials.rs @@ -39,17 +39,26 @@ pub fn geometry_materials( Ok(geometry_maps(rf, types, materials)?.0) } -/// [`geometry_materials`], and the types of `types` whose -/// `m_geomTag2MaterialId` holds any entry, whatever its values: types that -/// draw geometry, though their map may name no material (every value −1, -/// RE-149) or only materials not among `materials` (RE-299). +/// A type's `m_geomTag2MaterialId` that holds entries, whatever its values. +#[derive(Clone, Debug, Default)] +pub struct GeometryMap { + /// Every value is −1: no geometry has a material of its own. + pub all_unset: bool, + /// The type's family (`m_familyId`). + pub family: Option, +} + +/// [`geometry_materials`], and each type of `types` whose +/// `m_geomTag2MaterialId` holds any entry, whatever its values: a type with +/// geometry, though its map may name no material (every value −1, RE-149) +/// or only materials not among `materials` (RE-299). pub fn geometry_maps( rf: &mut RevitFile, types: &BTreeSet, materials: &BTreeSet, -) -> anyhow::Result<(BTreeMap, BTreeSet)> { +) -> anyhow::Result<(BTreeMap, BTreeMap)> { let mut out = BTreeMap::new(); - let mut mapped = BTreeSet::new(); + let mut mapped = BTreeMap::new(); if types.is_empty() { return Ok((out, mapped)); } @@ -72,13 +81,25 @@ pub fn geometry_maps( else { return Ok(()); }; - if !map.is_empty() { - mapped.insert(record.identity.element_id); - } let mut found = GeometryMaterials { family: root.fields.get("m_familyId").and_then(element_id), ..GeometryMaterials::default() }; + if !map.is_empty() { + let all_unset = map.iter().all(|entry| { + entry + .pointer("/second/m_id/m_id64") + .and_then(serde_json::Value::as_i64) + == Some(-1) + }); + mapped.insert( + record.identity.element_id, + GeometryMap { + all_unset, + family: found.family, + }, + ); + } for entry in map { let Some(id) = entry .pointer("/second/m_id/m_id64") diff --git a/src/research/partition_schema_mvp/materials.rs b/src/research/partition_schema_mvp/materials.rs index b7a493b22..8e914df0d 100644 --- a/src/research/partition_schema_mvp/materials.rs +++ b/src/research/partition_schema_mvp/materials.rs @@ -38,8 +38,10 @@ pub(super) fn attach_type_materials( /// name the same set. Returns the replaced types that also map geometry to /// no material that their family shows ([`crate::native_family_forms`]), /// whose instances take their own material parameters' or -/// `` for it ([`attach_partly_unset_materials`]). Left as scanned -/// where the native record path or the material names are not read. +/// `` for it ([`attach_partly_unset_materials`]). A type the scan +/// found no map for whose native map holds geometry loses its empty list +/// (RE-299). Left as scanned where the native record path or the material +/// names are not read. pub(super) fn refine_type_materials( rf: &mut RevitFile, revit_version: u32, @@ -78,18 +80,33 @@ pub(super) fn refine_type_materials( else { return partly_unset; }; - // RE-299: a type whose native map holds entries draws geometry, though - // the scan read no map in its block, every value being −1 or a material - // whose name it did not read. Left with no list it would read as a type - // that draws nothing, whose hosted doors and windows are written as - // their openings alone (RE-84). - type_materials - .retain(|type_id, list| !list.is_empty() || !mapped.contains(&u64::from(*type_id))); // A family whose geometry with no material is all hidden or void has // no `` in Revit's export (RE-204), and a material only its // hidden forms have is not exported (RE-205). - let families: BTreeSet = found.values().filter_map(|found| found.family).collect(); + let families: BTreeSet = found + .values() + .filter_map(|found| found.family) + .chain(mapped.values().filter_map(|map| map.family)) + .collect(); let forms = crate::native_family_forms::family_forms(rf, &families).unwrap_or_default(); + // RE-299: a type whose native map holds entries has geometry, though the + // scan read no map in its block, every value being −1 or a material whose + // name it did not read. Left with no list it would read as a type that + // draws nothing, whose hosted doors and windows are written as their + // openings alone (RE-84). One whose map is all −1 and whose family shows + // none of that geometry (RE-204: every such form hidden or void, or none) + // still draws nothing, and keeps the empty list. + let draws = |type_id: u32| { + mapped.get(&u64::from(type_id)).is_some_and(|map| { + !map.all_unset + || map + .family + .and_then(|family| forms.get(&family)) + .and_then(|forms| forms.shows_unset) + != Some(false) + }) + }; + type_materials.retain(|type_id, list| !list.is_empty() || !draws(*type_id)); for (type_id, found) in found { let Ok(type_id) = u32::try_from(type_id) else { continue;