diff --git a/README.md b/README.md index 3aaba2813..8e4676558 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ The full list, with each gap's status and evidence, is in [`docs/evidence.md`](d - Files from before Revit 2008 are not read, and 2008 to 2014 files are experimental, measured on local files only. 1.0 covers Revit 2024 to 2027 (since 2026-11-08), and 2015 to 2023 stay outside it until a 1.x release adds each year with its same-release Revit exports . - Revit 2015 to 2022 and 2027 projects are experimental: measured against the same models saved by a neighbouring release, not against Revit's own export (BIMcollab's 2015 example, RE-198, and Vitoria's 2018 `ALD26 Before`, RE-315, aside, and held-out projects of 2020, 2021, 2022 and 2027, whose counts are a regression check and no part of these results). Revit 2026 is experimental too, measured on one project, flowbim's `AA-SingleDwellingHouse` (local only), and two held-out projects, counts only. -- On Revit 2008 to 2023 projects an element's body is its bounding box, cut back at its joins where they are read, except a wall at an angle on Revit 2021 to 2023, or on 2015 to 2020 where its box closes on its line's rectangle, drawn along its line, and a pipe on 2015 to 2023, drawn as its cylinder (RE-370, held to the same samples' 2024 saves). On 2026 and 2027 it is that too, except for pipes as their cylinders (RE-370), beams along their axes, stair runs and tread landings (a monolithic landing stays its box), and floors, ceilings and slabs drawn from their sketch lines (RE-341; held to the same project's 2024 and 2025 saves, which cannot catch an error those saves share, and not scored against a Revit export except for one stair flight; quantities and property sets of those bodies are not compared). A roof there keeps the record-box solve as on `main`, without its slope: a sloped roof can still be drawn as its outline at the record box's full height, a level block, or as its box, and no 2026 or 2027 scorer checks it. On 2025 beams along their axes, tread landings (a monolithic landing stays its box) and a sketch's arcs are drawn too (RE-345), held to the same project's 2024 save alone, which cannot catch an error that save shares; no Revit export of a 2025 beam, tread landing or arc exists here: the 28 Revit 2025 models read (puzzbobb's 24 and RE1's four) hold no beam or tread landing, and the 25 of them RE-345 counts with Revit's own 2025 exports (puzzbobb's 24 and RE1 Architecture) hold no arc in a floor, ceiling or roof sketch. A shaft opening is cut from floors, ceilings and roofs on 2024 only (RE-345); on 2025 to 2027 they stay whole and the export counts the shafts. On 2024 and 2025 it is the box only where the bytes are not decoded: roofs by extrusion outside Revit 2025, hip roofs over a T or U footprint, monolithic spiral stair flights and straight riserless ones with a nosing, edited wall profiles, ducts and pipe fittings. From 2023, doors and windows are drawn from Revit's saved meshes where those decode whole, and from 2024 so are the walls that embed a curtain wall where Revit raised their base (RE-340); furniture and other loadable families stay boxes unless `--saved-meshes` is given, and `--family-extrusions` (opt-in; implies `--saved-meshes`) draws an instance whose family's extrusions reproduce its mesh from them, as extruded profiles; an arc is written as the arc, whatever its radius. +- On Revit 2008 to 2023 projects an element's body is its bounding box, cut back at its joins where they are read, except a wall at an angle on Revit 2021 to 2023, or on 2015 to 2020 where its box closes on its line's rectangle, drawn along its line, a pipe on 2015 to 2023, drawn as its cylinder (RE-370, held to the same samples' 2024 saves), and a beam on 2015 to 2023 along its location line (RE-362): read on Autodesk's sample projects, held to the same samples' later saves, which cannot catch an error those share, and not checked by any Revit export; none of the 91 beams of BIMcollab's Revit 2015 example holds a line the reader finds (#1514), so its reach beyond those samples is not shown. On 2026 and 2027 it is that too, except for pipes as their cylinders (RE-370), beams along their axes, stair runs and tread landings (a monolithic landing stays its box), and floors, ceilings and slabs drawn from their sketch lines (RE-341; held to the same project's 2024 and 2025 saves, which cannot catch an error those saves share, and not scored against a Revit export except for one stair flight; quantities and property sets of those bodies are not compared). A roof there keeps the record-box solve as on `main`, without its slope: a sloped roof can still be drawn as its outline at the record box's full height, a level block, or as its box, and no 2026 or 2027 scorer checks it. On 2025 beams along their axes, tread landings (a monolithic landing stays its box) and a sketch's arcs are drawn too (RE-345), held to the same project's 2024 save alone, which cannot catch an error that save shares; no Revit export of a 2025 beam, tread landing or arc exists here: the 28 Revit 2025 models read (puzzbobb's 24 and RE1's four) hold no beam or tread landing, and the 25 of them RE-345 counts with Revit's own 2025 exports (puzzbobb's 24 and RE1 Architecture) hold no arc in a floor, ceiling or roof sketch. A shaft opening is cut from floors, ceilings and roofs on 2024 only (RE-345); on 2025 to 2027 they stay whole and the export counts the shafts. On 2024 and 2025 it is the box only where the bytes are not decoded: roofs by extrusion outside Revit 2025, hip roofs over a T or U footprint, monolithic spiral stair flights and straight riserless ones with a nosing, edited wall profiles, ducts and pipe fittings. From 2023, doors and windows are drawn from Revit's saved meshes where those decode whole, and from 2024 so are the walls that embed a curtain wall where Revit raised their base (RE-340); furniture and other loadable families stay boxes unless `--saved-meshes` is given, and `--family-extrusions` (opt-in; implies `--saved-meshes`) draws an instance whose family's extrusions reproduce its mesh from them, as extruded profiles; an arc is written as the arc, whatever its radius. - `rvt-elements` and Python list each element's saved parameters on Revit 2023 to 2027 files, experimentally on 2015 to 2022 ones and not on 2008 to 2014, but built-in parameters have no name, a wall's or floor's type parameters are not joined to it, and only `rvt-ifc --revit-property-sets` writes the rest into IFC. - Family files give metadata, previews and types but no family geometry, and template files (`.rte`, `.rft`) are not measured, because no template is in any corpus. - A room is left out of the export when its own phase is not the export phase, the project's last unless `rvt-ifc --phase` names another, as Revit's exporter does (RE-326); a room whose phase is unset or unread is written, and so is an Area or an MEP space. Where no phase is named and the last phase holds no room, the rooms written are those of the latest phase that holds any, and the diagnostics, `rvt-ifc` and the Python bindings say so (#1359): Autodesk's `rac_basic_sample_project` has 14 rooms in Working Drawings and none in its last phase, and writes them by default; naming the last phase writes none. The phase rule is measured on no Revit export made with the default setup, because none has such a room: the one Revit export of a project with rooms outside its last phase, a Revit 2018 export of `rac_basic_sample_project` that a user posted on the IFC for Revit wiki (local only), holds the 14, and rvt-rs writes them with `--phase "Working Drawings"`. The fallback is a choice, not a measurement of Revit's default. The Python bindings and the viewer export the default phase. diff --git a/changelog.d/added/1190-2015-2023-beam-axes.md b/changelog.d/added/1190-2015-2023-beam-axes.md new file mode 100644 index 000000000..2a18337d8 --- /dev/null +++ b/changelog.d/added/1190-2015-2023-beam-axes.md @@ -0,0 +1,9 @@ +- **Revit 2015 to 2023 projects draw beams along their location lines, with + `Span`, `Slope`, `Roll` and `Length` (#1190, RE-362).** A beam that a named + column trims keeps its cut box (RE-122), and a steel beam's I section is not + read on these releases. **No Revit export scores any beam this rule + draws** among the models Measure fetches without a held-out pair (Measure run + 42134881703): BIMcollab's 91 beams stay record boxes (none holds a line the reader + finds, #1514, so its reach is Autodesk's samples), and the three.js export + of `rac_advanced` holds no `IfcBeam`. Each beam is held to the same Autodesk + project's later save, which cannot catch an error that save shares. diff --git a/docs/evidence.md b/docs/evidence.md index 4d7137d16..577a2dae5 100644 --- a/docs/evidence.md +++ b/docs/evidence.md @@ -15,11 +15,11 @@ The recovery figures in this README compare rvt-rs's export with Revit's own IFC | Revit 2024 and 2025 projects | yes | typed IFC entities from each element's own record; by ElementId the set equals Revit's export on Core Interior, RE1 Architecture, Electrical and Mechanical, the 22 of puzzbobb's Revit 2025 models that have an export, and the BIM4LCA office as built; RE1 Plumbing and the office at building permit each write one element Revit's export leaves out, and 23 of Snowdon Towers' 5,968 are not in it (local only; details below) | measured per category: exact joins, profiles and cuts where decoded, the element's bounding box where not; on 2025, since RE-345, beams, tread landings and a sketch's arcs, circles and shaft voids are drawn and held to the same project's 2024 save alone, with no Revit export of any of them (the 28 Revit 2025 models read, puzzbobb's 24 and RE1's four, hold no beam or tread landing, and the 25 with Revit's own 2025 exports, puzzbobb's 24 and RE1 Architecture, no arc, circle or shaft void), which cannot catch an error that save shares; a pipe RE-131's connector entries do not place is drawn from its own record since RE-370, whose line and diameter give Revit's export on 93 of 93 pipes | names, family and type names, storey, materials and layers, Revit's GlobalId, stair dimensions, IFC export overrides; most other parameters not read | | Revit 2026 projects | yes | **experimental (since 0.5.0):** typed IFC entities from each element's record (RE-124), on flowbim.ee's single-dwelling house ([reference models](reference-models.md)) 113 of the 115 elements of Revit's export, none outside it | the element's bounding box, walls cut back at their joins (on that house wall thickness Revit's on 59 of 59, length on 35 of 59); since RE-341 beams along their axes (no 2026 or 2027 held-out model holds a beam, so only two Autodesk samples saved from earlier releases have been read), stair runs and tread landings (a monolithic landing stays its box), and floors, ceilings and slabs from their sketch lines (shaft openings are not cut there since RE-345: the floors, ceilings and roofs they pass through stay whole, and the export counts the shafts), none scored against a Revit export, but held to the same project's 2024 and 2025 saves (RE-341; that cannot catch an error those saves share, and quantities and property sets are compared only where RE-346 adds them) and, for one stair flight, to Revit's own 2026 export; roofs keep the record-box solve as on `main`, without their slope, so a sloped roof can still be drawn as a level block; since RE-370 pipes are drawn as their cylinders with their ports placed, from each pipe's own record, held to the same Autodesk MEP samples' 2024 saves and not to a Revit export | Revit's GlobalIds (113 of 113), storeys with Revit's names and elevations (4 of 4), rooms with Revit's number and name (13 of 13) and, since RE-314, their stored solid's outline as on 2025, doors and windows in their host walls, family and type names, wall and floor layer sets as thick as Revit's, walls joined by their join lists; since RE-346 pipes placed by RE-131's connector entries first, as on 2025, family instances' ports placed, walls cut to their doors' and windows' outlines, and `IsExternal`, railing heights and lengths, stair names, rooms' height and net volume and pipe and duct quantities as on 2025 (a door's or window's Area not on 2027, the ObjectType of a stair, a flight and a stringer not on 2026 and 2027, where Revit's own exports differ), held to the same project's 2025 save and not to a Revit export, with no held-out pipe, duct or port; roof slopes, spiral stair runs, wall profile openings, `IsExternal` on walls, curtain walls, members and plates, and IFC export overrides not read | | Revit 2027 projects | yes; 325 fields of the 2027 schema carry a flag whose meaning is not measured (RE-264) | **experimental (since 0.6.0):** typed IFC entities from each element's record, with the constants the file's schema gives (RE-179); on Autodesk's five 2027 sample projects the same ElementIds and classes as their 2026 copies (20,818 of 20,818), with **Revit 2027's own export measured only on two held-out projects, counts only** | as on 2026 | as on 2026: storeys, rooms (their outline as on 2025, RE-314), doors and windows in their hosts, family and type names, layer sets, wall joins and mullion materials read the same as the 2026 copies; 79 corner mullions are not put in a curtain wall; no door or window Area (Revit's 2027 export gives one that is not Width times Height, RE-346) | -| Revit 2023 projects | yes | typed IFC entities (since 0.4.0) from each element's record (RE-81): on two 2023 projects every element of Revit's export (modelo_bim, 37 of 37) or all but the 8 window trims nested in windows (Exemplo_data, 37 of 45), none outside it | the element's bounding box, walls at an angle along their lines (on 2015 to 2020 only where the record box closes on the line's rectangle) and joined by their join lists, held to the same Autodesk samples' 2024 saves and not scored against a Revit export (RE-364), and otherwise cut back by the walls they join (Exemplo_data 17 of 17 and modelo_bim 4 of 4 as Revit's, RE-120, measured on the cut-back box, before RE-364, and not measured again), beams stopped at the faces of the columns they frame into (modelo_bim 8 of 8, RE-122); pipes their cylinders, held to the same samples' 2024 saves (RE-370); rooms their outline (RE-102) | ElementId, category, each door's and window's host wall (30 of 30 as Revit's, RE-85), and the storeys of Revit's own Levels with each element on the one its record names, a beam on the one at its top (Exemplo_data 37 of 37 and modelo_bim 37 of 37 in Revit's storey, RE-107, RE-119), each family instance's family and type name (50 of 50 as Revit names them, RE-109), each wall's, floor's and roof's type and system family (24 of 24 with Revit's type, RE-111, RE-112), family instances' materials (44 of 45 as Revit's, RE-113), and layer sets, named by their own or their category's material (RE-114, RE-115): Exemplo_data 36 of 37 and modelo_bim 31 of 37 elements with Revit's materials, and materials with Revit's shading colours (20 of 21, none different, RE-116); no layered or angled joins or IFC export overrides, and the diagnostics say so; on 2016 to 2023, elements in a non-primary design option or outside the export phase are left out since RE-315; on 2015 to 2023 each pipe's type, cylinder, quantities, vertical invert and ports from its own native record, held to the same Autodesk MEP samples' 2024 saves (93,987 values equal on 2015 to 2027, 3 different, of one pipe edited between the saves) and not scored against a Revit export (RE-370); on 2015 to 2023 each element's Phase Created, the phase its own `m_createdPhaseId` names, held to the same Autodesk samples' 2024 saves (84,045 of 84,045 across 30 saves, each sample holding its elements in one phase, so the field is shown to be read but not to tell two phases apart) and not scored against a Revit export (RE-368); on 2015 to 2023 each element's `Constraints` Level parameters, the Levels its own native record names, held to the same Autodesk samples' later saves (85,121 of 85,121) and not scored against a Revit export (RE-369) | +| Revit 2023 projects | yes | typed IFC entities (since 0.4.0) from each element's record (RE-81): on two 2023 projects every element of Revit's export (modelo_bim, 37 of 37) or all but the 8 window trims nested in windows (Exemplo_data, 37 of 45), none outside it | the element's bounding box, walls at an angle along their lines (on 2015 to 2020 only where the record box closes on the line's rectangle) and joined by their join lists, held to the same Autodesk samples' 2024 saves and not scored against a Revit export (RE-364), and otherwise cut back by the walls they join (Exemplo_data 17 of 17 and modelo_bim 4 of 4 as Revit's, RE-120, measured on the cut-back box, before RE-364, and not measured again), beams stopped at the faces of the columns they frame into (modelo_bim 8 of 8, RE-122), and the others along their axes, also on 2015 to 2022, held to the same Autodesk samples' later saves (`rac_advanced` 33 of 33; `rst_basic` 2019 to 2023 312 of 323, the 11 being the 2024 save's I sections; RE-362, on Autodesk's samples only: none of BIMcollab's 91 beams holds a line the reader finds, #1514, and no Revit export scores a beam drawn this way); pipes their cylinders, held to the same samples' 2024 saves (RE-370); rooms their outline (RE-102) | ElementId, category, each door's and window's host wall (30 of 30 as Revit's, RE-85), and the storeys of Revit's own Levels with each element on the one its record names, a beam on the one at its top (Exemplo_data 37 of 37 and modelo_bim 37 of 37 in Revit's storey, RE-107, RE-119), each family instance's family and type name (50 of 50 as Revit names them, RE-109), each wall's, floor's and roof's type and system family (24 of 24 with Revit's type, RE-111, RE-112), family instances' materials (44 of 45 as Revit's, RE-113), and layer sets, named by their own or their category's material (RE-114, RE-115): Exemplo_data 36 of 37 and modelo_bim 31 of 37 elements with Revit's materials, and materials with Revit's shading colours (20 of 21, none different, RE-116); no layered or angled joins or IFC export overrides, and the diagnostics say so; on 2016 to 2023, elements in a non-primary design option or outside the export phase are left out since RE-315; on 2015 to 2023 each pipe's type, cylinder, quantities, vertical invert and ports from its own native record, held to the same Autodesk MEP samples' 2024 saves (93,987 values equal on 2015 to 2027, 3 different, of one pipe edited between the saves) and not scored against a Revit export (RE-370); on 2015 to 2023 each element's Phase Created, the phase its own `m_createdPhaseId` names, held to the same Autodesk samples' 2024 saves (84,045 of 84,045 across 30 saves, each sample holding its elements in one phase, so the field is shown to be read but not to tell two phases apart) and not scored against a Revit export (RE-368); on 2015 to 2023 each element's `Constraints` Level parameters, the Levels its own native record names, held to the same Autodesk samples' later saves (85,121 of 85,121) and not scored against a Revit export (RE-369) | | Revit 2019 to 2022 projects | yes | **experimental (since 0.6.0):** typed IFC entities from each element's record, which 2019 to 2022 frame as 2023 does, read with the tags each release's schema gives (RE-180); on Autodesk's 2019 to 2022 sample projects every ElementId shared with the 2023 copy has the class it has there (`rac_basic`, `rst_basic` and `rac_advanced` of each release), with **Revit's own export of 2020, 2021 and 2022 files measured only on held-out projects, counts only, and none of a 2019 file** | as on 2023 | as on 2023: storeys, doors and windows in their hosts, family and type names, layer sets and materials read as the 2023 copies do, with a few type names and type materials different (RE-180 §6) | | Revit 2015 to 2018 projects | yes on 2016 to 2018; on 2015, partial: they open on Autodesk's sample projects, and previews and PartAtom are not measured | **experimental (since 0.6.0):** typed IFC entities from each element's record, which 2015 to 2018 frame as 2019 does, read with the tags each release's schema gives (RE-181); on Autodesk's 2016, 2017 and 2018 sample projects every ElementId shared with the 2019 copy has the class it has there (`rac_basic`, `rst_basic` and `rac_advanced` of each release), and on five of its six 2015 sample projects (the sixth is another model), with **Revit's own export measured on one 2015 project and one 2018 project**: BIMcollab's structural example (RE-198, a public download, not redistributable) has all 241 of Revit's elements by ElementId, each in Revit's class or IFC4's for its IFC2X3 one, 3 air terminals Revit's export leaves out (#879), 16 footings on no storey and none of its elements' GlobalIds Revit's (#878); BIM-Speed's Vitoria `ALD26 Before` (2018, CC BY 4.0), most of whose frames carry no ElementId of their own and sit in their element's record (RE-315), has 1,314 of Revit's 1,321 elements by ElementId (1,136 before its 102 conduits, 74 conduit fittings and 2 gutters were recovered, RE-319), 1,305 of them in Revit's class or IFC4's for its IFC2X3 one (9 curtain walls are written as walls), none written that Revit's export leaves out, 1,251 on Revit's storey, and 7 storeys, as many as Revit's; the 7 missing are 4 panel walls and 3 assemblies, and its held-out sibling writes 1,514 elements and spaces against Revit's 1,520, counts only, none of its elements one that Revit's export lacks (198 were, until the export phase and design options were applied, which the counts cannot tell apart); 2014's element records are read as 2015's are, measured on one local 2014 file only (RE-259, the row below) | as on 2019 | as on 2019: storeys, doors and windows in their hosts, family and type names, layer sets and materials read as the 2019 copies do, but where the files' own bytes differ (RE-181 §6) | -| Revit 2011 to 2014 projects | yes: 2011 to 2013 open since RE-259, whose schemas have no GUID lists; measured on local closed-source files only, outside the 1.0 range (2015 to 2027) | **experimental:** typed IFC entities from each element's record, which 2011 to 2014 frame as 2015 does, read with the tags each release's schema gives (RE-259); on a local copy of Autodesk's 2013 `rac_advanced` sample every ElementId shared with its 2015 copy has the class it has there and 62,071 of 62,153 fields agree, with **no Revit export of a file before 2015 measured** and no same-model pair for 2011, 2012 or 2014 | as on 2015, without walls along their lines | as on 2015: storeys, doors and windows in their hosts, family and type names, layer sets and materials read as the 2015 copy does, but where the files' own bytes differ (RE-259 §7) | -| Revit 2008 to 2010 projects | yes: they open and report their release, which 2008 and 2009 take from their build date (RE-259); measured on local closed-source files only, outside the 1.0 range (2015 to 2027) | **experimental:** typed IFC entities from each element's record, which 2009 and 2010 frame as 2011 does and 2008 4 bytes shorter (RE-260); on one local 2009 project 8 doors and their wall, on one 2008 project one generic model, with **no Revit export and no same-model copy of any 2008 to 2010 file measured** | as on 2015, without walls along their lines | storeys with their names and elevations (RE-260, also on 2011 and 2012), materials named and coloured; type names, layer sets and hosts as on 2015 but not measured | +| Revit 2011 to 2014 projects | yes: 2011 to 2013 open since RE-259, whose schemas have no GUID lists; measured on local closed-source files only, outside the 1.0 range (2015 to 2027) | **experimental:** typed IFC entities from each element's record, which 2011 to 2014 frame as 2015 does, read with the tags each release's schema gives (RE-259); on a local copy of Autodesk's 2013 `rac_advanced` sample every ElementId shared with its 2015 copy has the class it has there and 62,071 of 62,153 fields agree, with **no Revit export of a file before 2015 measured** and no same-model pair for 2011, 2012 or 2014 | as on 2015, without walls along their lines or beams along their axes | as on 2015: storeys, doors and windows in their hosts, family and type names, layer sets and materials read as the 2015 copy does, but where the files' own bytes differ (RE-259 §7) | +| Revit 2008 to 2010 projects | yes: they open and report their release, which 2008 and 2009 take from their build date (RE-259); measured on local closed-source files only, outside the 1.0 range (2015 to 2027) | **experimental:** typed IFC entities from each element's record, which 2009 and 2010 frame as 2011 does and 2008 4 bytes shorter (RE-260); on one local 2009 project 8 doors and their wall, on one 2008 project one generic model, with **no Revit export and no same-model copy of any 2008 to 2010 file measured** | as on 2015, without walls along their lines or beams along their axes | storeys with their names and elevations (RE-260, also on 2011 and 2012), materials named and coloured; type names, layer sets and hosts as on 2015 but not measured | | Families (`.rfa`) | yes | family metadata, OmniClass, previews; no family geometry | none | a family's types and type parameters as its `PartAtom` stream lists them (`rvt-info`, `PartAtom::family_types`) | | Templates (`.rte`, `.rft`) | the same container code; no template file is in any corpus, so not measured | not measured | none | none | diff --git a/docs/format-reconnaissance.md b/docs/format-reconnaissance.md index 855475b18..84a902b4a 100644 --- a/docs/format-reconnaissance.md +++ b/docs/format-reconnaissance.md @@ -2533,3 +2533,7 @@ On the BIM4LCA office (Revit 2024), a type's text parameters are stored in a sec ## Addendum — RE-355 the direction of a void's first line says which line Revit tags (2026-11-05) Of the lines of a floor's void that meet end to end in one cycle, the one with the lowest ElementId (the first drawn) is stored running along the loop's counter-clockwise walk or against it (its `GLine` origin, direction and end parameters). Against it, Revit tags that line; along it, Revit tags the line after it on the walk. That is Revit's tag on 133 of 133 voids: the BIM4LCA office as built (36) and at building permit (35), and Core Interior (62, from the export Measure's scorers read), 3147725 included, the one void RE-339 left wrong (its rectangle is stored along the walk and its notch lines against it). Reading the direction the first two lines were drawn in instead is right on all 70 office voids it reads and wrong on 20 of Core Interior's 62 (its shading devices, loops of 27 lines). `tag_voids` now reads it (`first_line_tag`), measured on Revit 2024 only, and the rule is also Revit's on 12 of 12 voids of Autodesk's Snowdon Towers architectural sample (11 before), with 3 voids of Snowdon Towers structural moved and no export to check them. [RE-355](../reports/element-framing/RE-355-void-loop-directions.md). + +## Addendum — RE-362 Revit 2015 to 2023 beams along their location lines (2026-11-05) + +A structural-framing element's bounded line (RE-49) and its `FamInstCurveDriver.m_rotation` (RE-276) read on Revit 2015 to 2023 as on 2024: no new layout, the line being the record 2015 to 2023 walls already read (RE-114, RE-180, RE-181). Against the same Autodesk samples saved by a later release, `rac_advanced` 2015 to 2023's 33 beams are all the 2024 save's, `rst_advanced` 2015's 54 are all the 2019 save's, and `rst_basic`'s are the 2024 save's on 312 of 323 (2019 to 2023), 298 (2017, 2018) and 285 (2016). The reach is Autodesk's samples: none of the 91 beams of BIMcollab's Revit 2015 example, which Revit's export holds, has a bounded line record in its element data (RE-362 section 5, #1514). The 11 are the 2024 save's I sections, not tried on these releases; the rest are in the saves' own record boxes, which already differ. No Revit export of these beams is scored (RE-362). diff --git a/docs/status.md b/docs/status.md index d754b0ad8..da0193f39 100644 --- a/docs/status.md +++ b/docs/status.md @@ -65,7 +65,7 @@ glTF, plan SVG and CSV, from the CLIs, Python, the zero-upload browser viewer, and a local, read-only MCP server (`rvt-rs-mcp`, [`docs/mcp.md`](mcp.md)) that answers an agent's questions from the model the IFC export writes. On Revit 2023 projects it reads each element's ElementId, category and bounding box (RE-81), walls at an angle along their lines (on 2015 to 2020 only where the record box closes on the line's rectangle) and joined by their join lists on 2015 to 2023 (RE-364), or cut back at their joins (RE-120), beams at their columns -(RE-122), door and +(RE-122) and elsewhere along their location lines on 2015 to 2023, read on Autodesk's samples only (RE-362: none of BIMcollab's 91 beams holds a line, #1514), door and window hosts (RE-85), pipes' types, cylinders and ports on 2015 to 2027 (RE-370), each element's Phase Created on 2015 to 2023 (RE-368), each element's Level parameters on 2015 to 2023 (RE-369), room outlines (RE-102) with their numbers and names (RE-117, which names every release's rooms), Levels with the storey each element sits on (RE-107; diff --git a/docs/support-matrix.json b/docs/support-matrix.json index 58731e161..03fc8f6c4 100644 --- a/docs/support-matrix.json +++ b/docs/support-matrix.json @@ -398,10 +398,10 @@ { "id": "element-record-beam-axes", "area": "elements", - "name": "Beams along their location lines from Revit 2024 to 2027 structural-framing element data", + "name": "Beams along their location lines from Revit 2024 to 2027 structural-framing element data, and 2015 to 2023 on Autodesk's samples", "status": "partial", "evidence": "partition_beam_axes::bounded_line_at / first_bounded_line / scan_bounded_lines / beam_body; partition_schema_mvp::attach_beam_axes (BEAM_AXIS_* fields); ifc::export_content RecordGeometry and BEAM_AXIS_BODY_SOURCE; probes/examples/probe_re49_beam_axes.rs; reports/element-framing/RE-49-beam-axes.md; RE-328 beams longer in their box than on their line: partition_beam_axes::beam_check / beam_body_in_box, ifc::export_content BEAM_BOX_BODY_SOURCE, probes/examples/probe_re328_beam_rejections.rs, probes/examples/probe_re328_beam_meshes.rs, reports/element-framing/RE-328-beam-rejections.md; RE-103 I sections: partition_type_parameters::type_i_sections, partition_schema_mvp::attach_beam_sections (BEAM_I_SECTION_FIELDS), partition_beam_axes::i_section_body, export_content beam_i_solid (SectionShape I), probes/examples/probe_re103_beam_sections.rs, tools/re/beam_sections_in_ifc.py, reports/element-framing/RE-103-beam-i-sections.md; RE-104 columns: partition_schema_mvp COLUMN_I_AXIS_FIELDS, export_content column_i_section and COLUMN_I_SECTION_BODY_SOURCE, probes/examples/probe_re104_column_sections.rs, reports/element-framing/RE-104-column-i-sections.md; RE-105 material profile sets: ifc::material_profile_sets_from_sections, entities::MaterialProfile.profile, reports/element-framing/RE-105-material-profile-sets.md; RE-221 hollow-section columns: partition_type_parameters::type_hollow_sections (HollowSection, HOLLOW_SECTION_PARAMETER_NAMES), partition_schema_mvp::attach_hollow_sections (HOLLOW_SECTION_FIELDS), export_content hollow_section_profile and COLUMN_HOLLOW_SECTION_BODY_SOURCE, entities::ProfileDef::IndexedPolyCurveWithVoids, probes/examples/probe_hss_sections.rs, tools/re/column_bodies_vs_ifc.py, reports/element-framing/RE-221-hollow-section-columns.md; RE-345 Revit 2025: tools/re/bodies_across_saves.py, reports/element-framing/RE-345-2025-beams-landings-arcs.md", - "notes": "A structural-framing element's data carries its location line as a bounded line record: 04 00 08 01, start and end parameters (f64 ft), an origin (3 f64, model ft) and a unit direction (3 f64), found in the data up to the next element-data header. With the element's record box, beam_body solves a box length x width x depth along the line: the length is the line's, the depth comes from the vertical extent, the width from one plan extent, and the other plan extent must come out to 0.01 ft. A level beam exports as Extrusion::rectangle(length, width, depth) rotated to the line's plan angle; a sloped one keeps its record box as the extrusion and sweeps its section along its centreline (SolidShape::SweptPath, IfcFixedReferenceSweptAreaSolid with +Z as the fixed reference). On Snowdon Towers Structural (local only) all 942 exported framing elements carry a line, 940 inside their box, and 923 solve (802 level on a model axis, 111 level rotated in plan, 10 sloped); IfcOpenShell meshes each exported solid to its record box (938 to 0.001 ft, 4 to 0.0016 ft). Against the meshes of the 2027 edition's VIM export, scored only where that edition kept the beam's Location, type and mesh consistent (839), the section across the line equals Revit's on 613 and Revit's less the top a floor join cuts on 179; the ends equal Revit's on 485 and Revit trims 352 at their supports (median 0.625 ft at the larger end), which is not read. Rotated beams' solids are 1.108 times Revit's own beam envelope at the median, against 8.406 times for the record box they replace. RE-328 (2026-11-04) measured why the other 19 keep their box, against each beam's own saved mesh in the file (Revit's tessellation, not an IFC export): 17 of them are level, unrotated beams whose record box is longer than their line by at most a foot, not beams with oblique end cuts; an unrotated level beam in that class (34 on Autodesk's 2024 samples: 17 Snowdon, 9 rst_basic, 8 rac_advanced) is drawn as the box along the line, BodySource partition_beam_box_along_axis: its direction and section come from the line and the record box (the section across the line is the mesh's on all 25 of them whose mesh decodes whole), its length is the record box's, not the beam's, and Revit's mesh ends inside it at one end or both on 9 of the 25, so it can run past the beam at its ends. It gets no Qto_BeamBaseQuantities.Length, Span, Slope or Roll and no I section; its AxisLength is the line's and its BodyLength the box's. 16 sloped, rolled beams of rst_basic and 2 Snowdon beams whose line leaves the box by up to 100 ft keep their box. Status is partial: ends are not trimmed, floor-join cuts are not applied, the section is a rectangle envelope, 18 beams keep their box, and no licensed CI file has structural framing. Revit 2024, and 2026 and 2027 since RE-341: Autodesk's rst_basic and rac_advanced saved by 2026 and 2027 draw their 2024 saves' beams (345 along their axes and 9 along it on rst_basic, 29 and 8 on rac_advanced, Measure run 41988011289, head 1d5bbf03), none scored against a Revit export of a 2026 or 2027 file, and no 2026 or 2027 held-out model holds an IfcBeam, so the reader has run only on two Autodesk samples saved from earlier releases, never on a natively authored 2026 beam; tools/re/bodies_across_saves.py holds them to the 2024 save. Revit 2025 since RE-345: the same two samples saved by 2025 draw their 2024 saves' beams, 29 along their axes and 8 along it on rac_advanced and 345 and 9 on rst_basic (391 beams, each equal to the 2024 save's in class, GlobalId, storey, placement, volume and vertices; Measure run 42091457764, head 830a4d94), and **no Revit export checks a 2025 beam**: the 28 Revit 2025 models read (puzzbobb's 24 and RE1's four) hold no beam, so the reader has run only on two Autodesk samples saved from 2024, and the same-project check cannot catch an error the 2024 save shares.", + "notes": "A structural-framing element's data carries its location line as a bounded line record: 04 00 08 01, start and end parameters (f64 ft), an origin (3 f64, model ft) and a unit direction (3 f64), found in the data up to the next element-data header. With the element's record box, beam_body solves a box length x width x depth along the line: the length is the line's, the depth comes from the vertical extent, the width from one plan extent, and the other plan extent must come out to 0.01 ft. A level beam exports as Extrusion::rectangle(length, width, depth) rotated to the line's plan angle; a sloped one keeps its record box as the extrusion and sweeps its section along its centreline (SolidShape::SweptPath, IfcFixedReferenceSweptAreaSolid with +Z as the fixed reference). On Snowdon Towers Structural (local only) all 942 exported framing elements carry a line, 940 inside their box, and 923 solve (802 level on a model axis, 111 level rotated in plan, 10 sloped); IfcOpenShell meshes each exported solid to its record box (938 to 0.001 ft, 4 to 0.0016 ft). Against the meshes of the 2027 edition's VIM export, scored only where that edition kept the beam's Location, type and mesh consistent (839), the section across the line equals Revit's on 613 and Revit's less the top a floor join cuts on 179; the ends equal Revit's on 485 and Revit trims 352 at their supports (median 0.625 ft at the larger end), which is not read. Rotated beams' solids are 1.108 times Revit's own beam envelope at the median, against 8.406 times for the record box they replace. RE-328 (2026-11-04) measured why the other 19 keep their box, against each beam's own saved mesh in the file (Revit's tessellation, not an IFC export): 17 of them are level, unrotated beams whose record box is longer than their line by at most a foot, not beams with oblique end cuts; an unrotated level beam in that class (34 on Autodesk's 2024 samples: 17 Snowdon, 9 rst_basic, 8 rac_advanced) is drawn as the box along the line, BodySource partition_beam_box_along_axis: its direction and section come from the line and the record box (the section across the line is the mesh's on all 25 of them whose mesh decodes whole), its length is the record box's, not the beam's, and Revit's mesh ends inside it at one end or both on 9 of the 25, so it can run past the beam at its ends. It gets no Qto_BeamBaseQuantities.Length, Span, Slope or Roll and no I section; its AxisLength is the line's and its BodyLength the box's. 16 sloped, rolled beams of rst_basic and 2 Snowdon beams whose line leaves the box by up to 100 ft keep their box. Status is partial: ends are not trimmed, floor-join cuts are not applied, the section is a rectangle envelope, 18 beams keep their box, and no licensed CI file has structural framing. Revit 2024, and 2026 and 2027 since RE-341: Autodesk's rst_basic and rac_advanced saved by 2026 and 2027 draw their 2024 saves' beams (345 along their axes and 9 along it on rst_basic, 29 and 8 on rac_advanced, Measure run 41988011289, head 1d5bbf03), none scored against a Revit export of a 2026 or 2027 file, and no 2026 or 2027 held-out model holds an IfcBeam, so the reader has run only on two Autodesk samples saved from earlier releases, never on a natively authored 2026 beam; tools/re/bodies_across_saves.py holds them to the 2024 save. Revit 2025 since RE-345: the same two samples saved by 2025 draw their 2024 saves' beams, 29 along their axes and 8 along it on rac_advanced and 345 and 9 on rst_basic (391 beams, each equal to the 2024 save's in class, GlobalId, storey, placement, volume and vertices; Measure run 42091457764, head 830a4d94), and **no Revit export checks a 2025 beam**: the 28 Revit 2025 models read (puzzbobb's 24 and RE1's four) hold no beam, so the reader has run only on two Autodesk samples saved from 2024, and the same-project check cannot catch an error the 2024 save shares. Read on Revit 2015 to 2023 as well (RE-362), through the same record: measured on Autodesk's sample projects only, against the same samples' later saves (rac_advanced 2015 to 2023 33 of 33 against the 2024 saves, rst_basic 2019 to 2023 312 of 323, rst_advanced 2015 54 of 54 against its 2019 save), which cannot catch an error those saves share, and not checked by any Revit export: none of the 91 beams of BIMcollab's Revit 2015 example holds a bounded line record in its element data (#1514), and the three.js export of rac_advanced holds no IfcBeam. A beam a column cuts (RE-122) keeps its cut box.", "user_impact": "Beams, joists and girders export along their own axes instead of as boxes around them, so a diagonal beam is a beam, not a slab of the bay it crosses. Since RE-103 a steel beam whose type stores an I section is that I (IfcIShapeProfileDef), flanges across and web upright: 377 of Snowdon Towers' 942 framing elements, and on all 271 its VIM export can score, Revit's own mesh has the section's corners, flange tips and web faces where rvt-rs puts them. Channels, angles, joists and concrete beams keep their box along the line. Since RE-104 a structural column whose type stores an I section is that I turned to the column's own X axis: all 40 of Snowdon Towers' steel columns, and Revit's own mesh on all 24 its VIM export can score. Since RE-105 each of those 417 members is written with an IfcMaterialProfileSetUsage whose IfcMaterialProfile pairs its type's material with its I. Since RE-221 a column whose type is a square hollow section is that section, rounded corners and all (IfcArbitraryProfileDefWithVoids over IfcIndexedPolyCurves, as Revit writes it), read from its family's parameters b, Ht, t and kr: Revit's body on all 79 such columns of the BIM4LCA office as built and 55 at building permit." }, { diff --git a/docs/supported-profile.md b/docs/supported-profile.md index 3c28bd9aa..e7cce2e9b 100644 --- a/docs/supported-profile.md +++ b/docs/supported-profile.md @@ -48,12 +48,12 @@ apart: |---|---|---|---|---| | Revit 2024 and 2025 project (`.rvt`) | yes | yes, measured on Core Interior, the four RE1 models, Snowdon Towers (local only), Projeto1 and `teste_export_2025` (both local only; RE1's exports are by Revit 26.2; one plumbing fixture on RE1 Plumbing and 23 elements on Snowdon Towers are not in Revit's export) | see below | see below | | Revit 2026 project | yes | experimental (since 0.5.0): measured on flowbim's `AA-SingleDwellingHouse` (local only), a 2026 project, against Revit's own IFC4X3 export, 113 of its 115 elements and none outside it (RE-124) | the element's bounding box, walls cut back at their joins; pipes their cylinders (RE-370); rooms their outline, read as on 2025 (RE-314); beams along their axes, stair runs and landings, and floors, ceilings and slabs from their sketch lines (shaft openings are not cut there since RE-345: the floors, ceilings and roofs they pass through stay whole, and the export counts the shafts), none scored against a Revit export, but held to the same project's 2024 and 2025 saves (which cannot catch an error those saves share) and, for one stair flight, to Revit's own export; tread landings only, a monolithic landing stays its box; a roof keeps the record-box solve as on `main`, without its slope, so a sloped roof can still be a level block (RE-341); ports placed and walls cut to their doors' and windows' outlines, held to the same project's 2025 save and not to a Revit export (RE-346; a door's or window's Area is not written on 2027, where Revit's export differs) | Revit's GlobalIds, storeys, rooms with Revit's numbers and names, doors and windows in their hosts, family and type names, layer sets | -| Revit 2023 project | yes | yes, measured on Exemplo_data and modelo_bim (both local only) against Revit's IFC4 exports (RE-81); components nested in doors and windows are left out | the element's bounding box; walls at an angle along their lines (on 2015 to 2020 only where the record box closes on the line's rectangle) and joined by their join lists, held to the same Autodesk samples' 2024 saves and not scored against a Revit export (RE-364); pipes their cylinders, held to the same samples' 2024 saves (RE-370); rooms their outline (RE-102) | ElementId, category, door and window host walls (RE-85), Levels as storeys with each element on the one its record names (RE-107), family instances' family and type names (RE-109), and each element's Phase Created on 2015 to 2023, held to the samples' later saves (RE-368), and each element's Level parameters on 2015 to 2023, held to the samples' later saves (RE-369), and on 2015 to 2023 pipes' types, cylinders and ports, held to the samples' 2024 saves (RE-370) | +| Revit 2023 project | yes | yes, measured on Exemplo_data and modelo_bim (both local only) against Revit's IFC4 exports (RE-81); components nested in doors and windows are left out | the element's bounding box; beams along their axes on Autodesk's samples (held to the same samples' later saves: the 2024 saves, `rst_advanced` 2015's 2019 save, `rst_basic` 2015 none), none of BIMcollab's 91 beams reading a line (#1514), and no Revit export scoring any (RE-362); walls at an angle along their lines (on 2015 to 2020 only where the record box closes on the line's rectangle) and joined by their join lists, held to the same Autodesk samples' 2024 saves and not scored against a Revit export (RE-364); pipes their cylinders, held to the same samples' 2024 saves (RE-370); rooms their outline (RE-102) | ElementId, category, door and window host walls (RE-85), Levels as storeys with each element on the one its record names (RE-107), family instances' family and type names (RE-109), and each element's Phase Created on 2015 to 2023, held to the samples' later saves (RE-368), and each element's Level parameters on 2015 to 2023, held to the samples' later saves (RE-369), and on 2015 to 2023 pipes' types, cylinders and ports, held to the samples' 2024 saves (RE-370) | | Revit 2027 project | partial: opens on Autodesk's sample projects; previews and PartAtom not measured | experimental (since 0.6.0): the same ElementIds and classes as the 2026 copies of Autodesk's five sample projects (RE-179); Revit 2027's own export is measured only on two held-out projects, counts only | as on 2026 | as on 2026, except that a door's or window's Area is not written (RE-346) | | Revit 2019 to 2022 project | yes | experimental (since 0.6.0): 2023's records and readers with each release's schema tags; every ElementId shared with the 2023 copies of Autodesk's sample projects keeps its class, and the `rac_basic`, `rst_basic` and 2021 and 2022 `rac_advanced` exports have the 2023 copies' building elements, class for class (RE-180); Revit's own export of 2020, 2021 and 2022 files is measured only on held-out projects, counts only, and none of a 2019 file | as on 2023 | as on 2023 | | Revit 2015 to 2018 project | yes on 2016 to 2018; 2015 partial: opens on Autodesk's sample projects; previews and PartAtom not measured | experimental (since 0.6.0): 2019's records and readers with each release's schema tags; every ElementId shared with the 2019 copies of Autodesk's 2016 to 2018 sample projects keeps its class, and their exports have the 2019 copies' building elements, class for class; on five of the six 2015 samples every shared ElementId keeps its class, and the sixth, `rst_basic`, is another model (RE-181); Revit's own export is measured on one 2015 project (RE-198) and one 2018 project, Vitoria's `ALD26 Before`, where 1,314 of its 1,321 elements are written by ElementId (1,136 before conduits, conduit fittings and gutters, RE-319), 1,305 of them in Revit's class or IFC4's (9 curtain walls come out as walls), none that the export leaves out, 1,251 on Revit's storey (RE-315) | as on 2019 | as on 2019 | -| Revit 2011 to 2014 project | yes, on local closed-source files only, outside the 1.0 range (2024 to 2027); 2011 to 2013 open since RE-259 | experimental: 2015's records and readers with each release's schema tags; on a local copy of Autodesk's 2013 `rac_advanced` sample (local only) every ElementId shared with its 2015 copy keeps its class, and its export has the 2015 copy's building elements, class for class, but for a re-divided curtain wall (RE-259); no Revit export of a file before 2015 is measured | as on 2015, without walls along their lines | as on 2015 | -| Revit 2008 to 2010 project | yes, on local closed-source files only, outside the 1.0 range (2024 to 2027) (RE-259) | experimental: 2011's records and readers with each release's schema tags, 2008's envelope 4 bytes shorter in the local closed-source files (local only) (RE-260); checked only for consistency within each file, with no Revit export and no same-model copy | as on 2015, without walls along their lines | storeys and named materials | +| Revit 2011 to 2014 project | yes, on local closed-source files only, outside the 1.0 range (2024 to 2027); 2011 to 2013 open since RE-259 | experimental: 2015's records and readers with each release's schema tags; on a local copy of Autodesk's 2013 `rac_advanced` sample (local only) every ElementId shared with its 2015 copy keeps its class, and its export has the 2015 copy's building elements, class for class, but for a re-divided curtain wall (RE-259); no Revit export of a file before 2015 is measured | as on 2015, without walls along their lines or beams along their axes | as on 2015 | +| Revit 2008 to 2010 project | yes, on local closed-source files only, outside the 1.0 range (2024 to 2027) (RE-259) | experimental: 2011's records and readers with each release's schema tags, 2008's envelope 4 bytes shorter in the local closed-source files (local only) (RE-260); checked only for consistency within each file, with no Revit export and no same-model copy | as on 2015, without walls along their lines or beams along their axes | storeys and named materials | | Family (`.rfa`) | yes | family metadata and OmniClass only | none | a family's types and type parameters as its `PartAtom` stream lists them: Revit's text at save, not read from the family's own records | | Template (`.rte`, `.rft`) | the same container code; no template file is in any corpus, so not measured | not measured | none | none | diff --git a/probes/examples/probe_re362_beam_lines_pre2023.rs b/probes/examples/probe_re362_beam_lines_pre2023.rs new file mode 100644 index 000000000..c5a132f72 --- /dev/null +++ b/probes/examples/probe_re362_beam_lines_pre2023.rs @@ -0,0 +1,304 @@ +//! RE-362: why a Revit 2015 to 2023 beam keeps its record box, for BIMcollab's +//! Revit 2015 example (the one non-held-out model with Revit's own export that +//! holds beams, where Measure run 42134881703 drew 0 of its 91 beams along a line). +//! +//! FACT under test: a structural-framing element is drawn along its location +//! line only where its element data holds one bounded line (copies that agree) +//! and `partition_beam_axes::beam_body` builds a solid from that line and the +//! element's record box (RE-49, RE-362). This is `probe_re328_beam_rejections` +//! on the releases RE-362 reads (`beam_axis_read_on`, 2015 to 2027): it sorts +//! every structural-framing element the partition reader recovers by what its +//! element data holds (`no_bbox`, `no_data`, `no_line`, `disagree`, `one_line` +//! `accepted` or declined by its first failing test, with the same detail), and +//! prints counts only, each line as `native readers: beam lines: ...` in the +//! restricted alphabet `tools/ci/scrub-holdout-results.sh` keeps for a model +//! fetched without a licence. A beam a column cuts (RE-122) keeps its cut box and +//! is counted apart (`column_cut`). For a line the reader accepts it also counts +//! the lines the beam's data holds (`accepted lines_in_data`) and whether another +//! one also makes a beam solid (`accepted other_line_fits`): where another does, +//! the body could be along the wrong line, which no scorer here would see. No +//! element id, name or coordinate is printed. +//! +//! ```text +//! cargo run --profile ci -p rvt-probes --example probe_re362_beam_lines_pre2023 -- model.rvt +//! ``` + +use std::collections::{BTreeMap, BTreeSet}; + +use rvt::RevitFile; +use rvt::research::element_record_beam_cuts::BEAM_BODY_SOURCE_FIELD; +use rvt::research::partition_beam_axes::{ + BeamReject, BoundedLine, END_TOLERANCE_FEET, MIN_PLAN_CONDITION, all_line_copies, + beam_axis_read_on, beam_body_in_box, beam_check, beam_section_fit, box_along, box_overshoot, + box_overshoot_feet, line_copies, +}; +use rvt::research::partition_schema_mvp::{ + BEAM_ROTATION_FIELD, beam_axis_from_fields, beam_i_section_from_fields, element_record_bbox, + recover_partition_schema_mvp, +}; +use rvt::walker::{DecodedElement, InstanceField, WalkerLimits}; + +fn say(text: std::fmt::Arguments) { + println!("native readers: beam lines: {text}"); +} + +fn bump(counts: &mut BTreeMap, key: impl Into) { + *counts.entry(key.into()).or_default() += 1; +} + +fn decade(feet: f64) -> String { + if feet.is_nan() || feet <= 0.0 { + return "0".to_string(); + } + format!("<=1e{}", feet.log10().ceil().clamp(-30.0, 30.0) as i32) +} + +fn dot(a: [f64; 3], b: [f64; 3]) -> f64 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} + +fn float_field(element: &DecodedElement, wanted: &str) -> Option { + element.fields.iter().find_map(|(name, value)| match value { + InstanceField::Float { value, .. } if name == wanted => Some(*value), + _ => None, + }) +} + +fn orientation(direction: [f64; 3]) -> &'static str { + if direction[0].hypot(direction[1]) < 0.1 { + "steep" + } else if direction[2].abs() > 1e-9 { + "sloped" + } else if direction[0].abs() < 1e-9 || direction[1].abs() < 1e-9 { + "level_on_axis" + } else { + "level_rotated" + } +} + +fn reject_name(reject: &BeamReject) -> &'static str { + match reject { + BeamReject::NotFinite => "not_finite", + BeamReject::InvertedBox => "inverted_box", + BeamReject::EndOutside { .. } => "end_outside", + BeamReject::ZeroLength => "zero_length", + BeamReject::TooSteep => "too_steep", + BeamReject::NoSection => "no_section", + BeamReject::OpenClosure { .. } => "open_closure", + } +} + +/// What `beam_body_in_box` and the rotation test do with a line `beam_check` +/// declined for a section that does not close, saying the first condition +/// that stops the beam. +fn rule_outcome(bbox: [f64; 6], start: [f64; 3], end: [f64; 3], rotation: &str) -> &'static str { + let delta = [end[0] - start[0], end[1] - start[1], end[2] - start[2]]; + let length = dot(delta, delta).sqrt(); + let d = [delta[0] / length, delta[1] / length, delta[2] / length]; + if d[2].abs() > 1e-9 { + return "sloped"; + } + if (d[0] * d[0] - d[1] * d[1]).abs() < MIN_PLAN_CONDITION { + return "plan_angle_near_45"; + } + let Some(free) = box_along(bbox, d) else { + return "free_solve_none"; + }; + if free[0] <= length { + return "box_not_longer"; + } + if beam_body_in_box(bbox, start, end).is_none() { + return "line_not_in_body"; + } + match rotation { + "zero" => "drawn", + "nonzero" => "rotation_nonzero", + _ => "rotation_unread", + } +} + +fn main() -> anyhow::Result<()> { + let path = std::env::args().nth(1).expect("usage: MODEL.rvt"); + let mut rf = RevitFile::open_with_limits(&path, rvt::cli::open_limits(None, None, true))?; + let version = rf.basic_file_info()?.version; + let mvp = recover_partition_schema_mvp(&mut rf, version, WalkerLimits::default())?; + let beams: Vec<&DecodedElement> = mvp + .products + .iter() + .filter(|element| element.class == "StructuralFraming" && element.id.is_some()) + .collect(); + say(format_args!("release {version}")); + say(format_args!("layout_read {}", beam_axis_read_on(version))); + say(format_args!( + "streams_not_read {}", + rf.unread_streams().len() + )); + say(format_args!("beams {}", beams.len())); + let along_line = beams + .iter() + .filter(|beam| beam_axis_from_fields(&beam.fields).is_some()) + .count(); + say(format_args!("along_line {along_line}")); + if !beam_axis_read_on(version) { + return Ok(()); + } + let ids: BTreeSet = beams.iter().filter_map(|beam| beam.id).collect(); + let copies = line_copies(&mut rf, version, &ids)?; + let all_lines = all_line_copies(&mut rf, version, &ids)?; + let mut counts: BTreeMap = BTreeMap::new(); + for beam in &beams { + let Some(id) = beam.id else { continue }; + // A beam a column cuts (RE-122) keeps its cut box: its record box is the + // cut box, so no line is read for it. + if beam + .fields + .iter() + .any(|(name, _)| name == BEAM_BODY_SOURCE_FIELD) + { + bump(&mut counts, "column_cut"); + continue; + } + let attached = beam_axis_from_fields(&beam.fields).is_some(); + let Some(bbox) = element_record_bbox(beam) else { + bump(&mut counts, "no_bbox"); + continue; + }; + let line: BoundedLine = match copies.get(&id) { + None => { + bump(&mut counts, "no_data"); + continue; + } + Some(found) if found.lines.is_empty() => { + bump(&mut counts, "no_line"); + continue; + } + Some(found) if found.lines.iter().any(|(_, l)| *l != found.lines[0].1) => { + bump(&mut counts, "disagree"); + continue; + } + Some(found) => found.lines[0].1, + }; + let (start, end) = (line.start(), line.end()); + let verdict = beam_check(bbox, start, end); + let why = match &verdict { + Ok(_) => "accepted", + Err(reject) => reject_name(reject), + }; + let orient = orientation(line.direction); + bump(&mut counts, format!("one_line {why}")); + bump(&mut counts, format!("one_line {why} {orient}")); + let held = all_lines + .get(&id) + .and_then(|found| found.first()) + .map_or(0, |(_, lines)| lines.len()); + let other_fits = all_lines.get(&id).is_some_and(|found| { + found + .iter() + .flat_map(|(_, lines)| lines.iter()) + .filter(|other| **other != line) + .any(|other| beam_check(bbox, other.start(), other.end()).is_ok()) + }); + if verdict.is_ok() { + // Whether the first line, the one the reader takes, is the only line + // in the data that makes a beam solid: where another does too, the + // body could be along the wrong one. + bump( + &mut counts, + format!("one_line accepted lines_in_data {}", held.min(4)), + ); + bump( + &mut counts, + format!("one_line accepted other_line_fits {other_fits}"), + ); + if !attached { + bump(&mut counts, "MISMATCH accepted_but_not_attached"); + } + continue; + } + let rotation = match float_field(beam, BEAM_ROTATION_FIELD) { + Some(0.0) => "zero", + Some(_) => "nonzero", + None => "unread", + }; + bump(&mut counts, format!("one_line {why} rotation {rotation}")); + let section = if beam_i_section_from_fields(&beam.fields).is_some() { + "known" + } else { + "unknown" + }; + bump(&mut counts, format!("one_line {why} i_section {section}")); + bump( + &mut counts, + format!("one_line {why} lines_in_data {}", held.min(4)), + ); + bump( + &mut counts, + format!("one_line {why} other_line_fits {other_fits}"), + ); + if let Some(free) = box_along(bbox, line.direction) { + let line_length = line.end_parameter - line.start_parameter; + let gap = free[0] - line_length; + let sign = if gap < 0.0 { + "box_shorter" + } else { + "box_longer" + }; + bump( + &mut counts, + format!("one_line {why} free_length {sign} {}", decade(gap.abs())), + ); + } else { + bump(&mut counts, format!("one_line {why} free_length none")); + } + let mut drawn_by_rule = false; + match verdict { + Err(BeamReject::EndOutside { feet }) => { + let outside = [start, end] + .iter() + .filter(|p| box_overshoot_feet(bbox, **p) > END_TOLERANCE_FEET) + .count(); + bump(&mut counts, format!("end_outside ends {outside}")); + bump(&mut counts, format!("end_outside size {}", decade(feet))); + let worst = [start, end] + .map(|p| box_overshoot(bbox, p)) + .into_iter() + .max_by(|a, b| dot(*a, *a).total_cmp(&dot(*b, *b))) + .unwrap_or([0.0; 3]); + let along = dot(worst, line.direction); + let across = (dot(worst, worst) - along * along).max(0.0).sqrt(); + let kind = if across <= 1e-6 { + "along_line" + } else { + "across_line" + }; + bump( + &mut counts, + format!("end_outside overshoot {orient} {kind}"), + ); + let then = match beam_section_fit(bbox, start, end) { + Ok(_) => "fits", + Err(other) => reject_name(&other), + }; + bump( + &mut counts, + format!("end_outside without_ends_test {orient} {then}"), + ); + } + Err(BeamReject::OpenClosure { feet }) => { + bump(&mut counts, format!("open_closure miss {}", decade(feet))); + let outcome = rule_outcome(bbox, start, end, rotation); + bump(&mut counts, format!("open_closure rule {outcome}")); + bump(&mut counts, format!("open_closure rule {orient} {outcome}")); + drawn_by_rule = outcome == "drawn"; + } + _ => {} + } + if attached != drawn_by_rule { + bump(&mut counts, "MISMATCH attached_differs_from_rule"); + } + } + for (key, n) in &counts { + say(format_args!("{key} {n}")); + } + Ok(()) +} diff --git a/reports/element-framing/RE-362-2015-2023-beam-axes.md b/reports/element-framing/RE-362-2015-2023-beam-axes.md new file mode 100644 index 000000000..a9473ac32 --- /dev/null +++ b/reports/element-framing/RE-362-2015-2023-beam-axes.md @@ -0,0 +1,82 @@ +# RE-362: Revit 2015 to 2023 beams along their location lines + +**Date:** 2026-11-05 +**Issues:** #1190 (geometry beyond the box on 2015 to 2023, 2026 and 2027); follows RE-49 (the beam's bounded line), RE-114, RE-180 and RE-181 (the same line record on a wall, 2015 to 2023), RE-276 (a beam's rotation), RE-280 (native records on 2015 to 2022), RE-341 (the same reader on 2026 and 2027). +**Artefacts:** Autodesk's own sample projects, pinned in `research/autodesk-sample-projects.tsv` and fetched by `tools/fetch-autodesk-samples.sh`, never stored: `rac_advanced` 2015 to 2024, `rst_basic` 2015 to 2024, `rst_advanced` 2015 and 2019. +**Scorer:** `tools/re/save_pair_bodies.py` (box and volume, not shape equality), on `main` since RE-341. +**Status:** positive against the same samples saved by a later release. **No Revit export scores any beam this rule draws on 2015 to 2023, among the models Measure fetches without a held-out pair**, and a later save cannot catch an error it shares. Measure run 42134881703 (head 0c39def2, `base=main`, `bimcollab=true`, `racadv=true`, `samples=true` for 2015 to 2024, counts only) scored the two sources of a Revit export that hold such beams: BIMcollab's Revit 2015 example holds 91 beams in Revit's export, and rvt-rs draws all 91 as record boxes, on `main` and on this head (0 drawn by line; `Span` and `Slope` missing on all 91, as on `main`, where Revit writes both); the three.js IFC2X3 export of `rac_advanced`, which the `racadv-2015` to `racadv-2020` models are scored against, holds no `IfcBeam` (it writes 3,308 `IfcMember`, 1,349 `IfcPlate`, 176 `IfcColumn` and 75 `IfcBuildingElementProxy`), and none of the 38 beams has an element with its Tag in it under any class. The held-out 2018 to 2023 models are not in that run. + +## 1. Before + +`recover_partition_schema_mvp` takes 2015 to 2023 down a branch of its own that ends before the beam readers, so a beam there was its record box, with no `Span`, `Slope`, `Roll` or `Length`, even though 2015 to 2023 walls read the same line record. On `main`, of `exported.body_sources`: + +| | `rac_advanced` 2023 / 2024 | `rst_basic` 2023 / 2024 | +|---|---|---| +| `partition_beam_axis` | 0 / 29 | 0 / 345 | +| `partition_beam_box_along_axis` | 0 / 8 | 0 / 9 | + +## 2. The change + +The 2015 to 2023 branch calls `attach_beam_rotations` and `attach_beam_axes`, nothing else. The line is read as a wall's is. The rotation's native read names `NATIVE_RELEASES`, since its default stops at 2023 (RE-280 measured the framing on 2015 to 2022). `BEAM_AXIS_SUPPORTED_REVIT_VERSIONS` (2024 to 2027) is left alone: it also gates `scan_bounded_lines`, which is not measured on these releases (since #1452 the sketch curves and shafts have lists of their own, and the probes that list a line's copies gate on `beam_axis_read_on`), so 2015 to 2023 get a list of their own for beams only, `BEAM_AXIS_2015_TO_2023_REVIT_VERSIONS`: not the 32-bit set, which is 2008 to 2023. On 2024 to 2027 `attach_beam_axes` reads what it read: `scan_bounded_lines` is that gate and then `scan_first_bounded_lines`, which it now calls after its own gate, and the native read of the rotation names the releases 2015 to 2027 where it named 2023 to 2027. 2008 to 2014 stay as they were. + +## 3. Bodies against a later save + +Each beam the change draws, paired by `Tag` with the later save's, to 3 mm on the world box and 0.1% on the volume: + +| save | reference | same | differ | +|---|---|---:|---:| +| `rac_advanced` 2015 to 2023 (each) | 2024 | 33 | 0 | +| `rst_basic` 2019 to 2023 (each) | 2024 | 312 | 11 | +| `rst_basic` 2017, 2018 (each) | 2024 | 298 | 25 | +| `rst_basic` 2016 | 2024 | 285 | 38 | +| `rst_advanced` 2015 | 2019 | 54 | 0 | + +`rst_basic` 2015 is another model (RE-181), so it has no later save: its 29 beams are only checked within the file (section 4). + +**The 11** have the same box and another volume: the 2024 save draws those steel beams as their type's I section (RE-103), which is not read here (section 6). + +**The other 14 on 2017 and 2018, and 27 on 2016, are in the saves' own data.** With no line read at all (`main`'s exports, each beam its raw record box), all 14 boxes on 2017 and on 2018 differ from the 2023 save's, by 3.9 mm, and all 27 on 2016, by 4.1 to 24.4 mm. Why Revit's upgrade moved them is not decoded. + +`rst_advanced` 2015 and 2019 against a 2027 copy, which is not pinned, give 48 same and 6 differ, the same on both, 0.1 m and 0.2 m; those six are also at their own save's record box. + +## 4. Within each file + +Every beam's new body is within 3 mm of its own save's record box, the worst 0.001 m, on all 20 `rac_advanced`, `rst_basic` and `rst_advanced` files of 2015 to 2023. That covers each difference in section 3. The reader requires it (a line's ends must lie in the box), so it shows only that the differences belong to the saves, not that the box is right. + +## 5. What else changes + +- **Column cuts stay.** RE-122's trim, measured on one 2023 project against Revit's export, keeps precedence, and the code says so: a beam it cuts carries `m_beam_body_source`, and `attach_beam_axes` and `attach_beam_rotations` skip it, so its bytes are `main`'s. (An earlier head tested the line against the record box, which the trim rewrites to the cut box, and attached it where they coincide: 5 of `rst_basic`'s 31 cut beams on each of 2016 to 2023, 40 in all, with the same world box and volume, a turned description and no `Span`, since the cut source wins. The skip also stops the rotation decode on cut beams, which on `rst_advanced` cuts the decoded set from 397 beams to 64.) That leaves 31 beams on `rst_basic` and 4 on `rac_advanced` as they were on `main`, without `Span`, `Roll` or `Length`, where 2024 draws them along their lines. +- **Properties.** `Pset_BeamCommon.Span`, `Slope`, `Roll` and `Qto_BeamBaseQuantities.Length` are written where the line is, at 1e-3 of the later save's on every value except `rst_basic` 2016's `Span` and `Length` on the 13 beams above. Per file, `rac_advanced` 25, 25, 25 and 25; `rst_basic` 320, 306, 224 and 320 (307, 306, 224 and 307 on 2016 equal); `rst_advanced` 54, 42, 42 and 54. +- **`Roll`** is written only for a rotation of exactly 0, as elsewhere; RE-276 measured the value at 0 only. +- **Storeys and GlobalIds** of every beam are as on `main`. +- **Byte identity.** Measure run 42134881703 compared the head's exports with `main`'s by hash, for Autodesk's samples of 2015 to 2024 and the `racadv` and BIMcollab models: 26 move, and nothing else. They are 20 sample saves (`rac_advanced` 2015 to 2023, `rst_basic` 2015 to 2023, `rst_advanced` 2015 and 2019) and the 6 `racadv` models (`racadv-2015` to `racadv-2020`, the same files as the 2015 to 2020 `rac_advanced` saves). No 2024 export moves (the five Autodesk projects and Snowdon's five), nor `rac_basic`, `rme_advanced`, `rme_basic` or BIMcollab. 2025 to 2027 were not in that run. The contributor's local count was 11 of 206 files, "2017 to 2023", which section 3's own beams on 2015 and 2016 contradict. +- **Against Revit's exports** (Measure run 42134881703): none of the beams this rule draws is in a Revit export. On the `racadv` models, `Span` and `Slope` are written on 25 beams that the three.js export does not carry (0 on beams it does carry), and `Roll` on 25, an IFC4-only property that an IFC2X3 export cannot hold: that is what the rule writes there, with nothing to score it against. On BIMcollab it writes nothing, since it draws none of its 91 beams. `box_bodies_vs_ifc` counts the `racadv` beams that stay record boxes at 34 on `main` and 1 on this head, each with no element of that Tag in Revit's export. +- **Cost.** The contributor measured under 0.15 s on a 1.2 s export (`rst_basic` 2023, 2019, single runs). Not measured by us: with #1335 open it is to be measured on a same-CPU pair in the unfiltered run, after the skip above. Read from the code before the skip, the beams' native read adds 4% to 11% on `rst_basic` and 20% to 23% on `rst_advanced`; 0.11.0 read neither, so it is pure addition. +- **Storey binding, `record_base_elevation_feet` (`ifc/mod.rs`).** `partition_beam_axis` is not in its list of record-backed sources, and is not added: a beam that body draws and whose record names no Level would bind by its base elevation on none of 2024 to 2027 either, so 2015 to 2023 do what `main` does for the same body. Adding it changes 2024 to 2027 exports, outside this change, for a case with 0 observed: every beam this change draws binds as on `main` (storeys equal). +- **`--saved-meshes`.** The default scope (doors and windows) is untouched. With the flag, a beam drawn along its line is not a saved-mesh candidate (`saved_meshes.rs` takes only `partition_element_record_bbox` and the box along a line), as on 2024 to 2027. Saved graphics are read on 2023 only of 2015 to 2023, so there 323 of `rst_basic` 2023's 370 beams, and 33 of `rac_advanced` 2023's 38, stop being candidates; whether a saved mesh is a better body than the line's is not measured. + +- **Why BIMcollab's 91 beams read no line** (Measure run 42139236978, head 207e4130, `-f bimcollab=true -f models="bimcollab-*" -f probe=probe_re362_beam_lines_pre2023`, counts only). The probe is `probe_re328_beam_rejections` on the releases this change reads. On the Revit 2015 file: the release gate passes (`beam_axis_read_on`), every stream is read, and there are 91 structural-framing elements, each with a record box (`no_bbox` 0) and element data found in a partition (`no_data` 0). **All 91 are `no_line`: no copy of any beam's element data holds a bounded line record** in the window the reader takes (from the element-data header to the next one, at most 0x10000 bytes). So the first failing test is the absence of the line, 91 of 91; no beam has copies that disagree (`disagree` 0) or one line (`one_line` 0), so the box tests (`beam_check`, `beam_body`) never run and refuse nothing. +- **What that says about the reach.** The line record the reader takes (RE-49, RE-114: the bytes `04 00 08 01` and a bounded line) is what Autodesk's 2015 samples hold for most of their beams that RE-122 does not cut: 33 of 34 on `rac_advanced`, 29 of 39 on `rst_basic`, 54 of 64 on `rst_advanced` (the rest are record boxes with no line read, as are 333 of `rst_advanced`'s 397 and 120 of `rst_basic`'s 159, which RE-122 cuts). BIMcollab's structural model, the one 2015 structural file from outside Autodesk's samples that Measure fetches, holds none in 91. So the reader's reach on 2015 is Autodesk's samples, and it is not shown to read a 2015 structural model from anywhere else: either that file stores a beam's location line in another record or form that is not decoded, or the reader's window or tag does not reach it. The probe says the line is absent from the window, not where it is. The same holds for 2016 to 2023: the reach is measured on the same samples only, and no model from outside them has been read (the held-out 2018 to 2023 models are in the unfiltered run). No fix is made: there is no one-line change to measure, since nothing shows where BIMcollab's line is. A candidate not tried is the native `FamInstCurveDriver` record, which the rotation already reads on 2015 (RE-280), and the curve a wall takes from its native record (#1475), for the beam's location curve. + +- **The final run** (Measure run 42518199499, head e05db36a against `main` at ab5f8e1c, unfiltered, with the samples, held-out models, BIMcollab and `racadv`). Gdynia (Revit 2021, held out, counts only) is the first Revit export to score a beam this rule draws: `Pset_BeamCommon.Span` and `Slope` 3 equal, none different and 7 missing of 10 each; all values equal 689 to 695, different 18 and values Revit's export does not write 8, both unchanged; 9 fewer record boxes. On `racadv` 2015 to 2020 values Revit's export does not write go from 342 to 392, all 25 `Span` and 25 `Slope`: that IFC2X3 export holds no `IfcBeam`, so it is counted apart and removes no value. +- **BIMcollab's 91 beams, a measured negative** (puzzbobb on #1514, local counts, not re-run here). None of the 91 structural-framing records holds a `FamInstCurveDriver` or a `GLine`, so there is no location line to find: each is a family instance placed by its transform. Against Revit's export the world box is within 3 mm on all 91 and the volume within 0.1% on 74; the 17 others are steel sections. `Span` equals one extent of the symbol's outline on all 91, its Y on 43 and its X on 48, and nothing read says which, so none is written, and these beams keep their boxes. + +## 6. Not measured, not read + +- Against a Revit export: see the status above. Where BIMcollab's line is: not known (section 5). +- A beam's I section (RE-103) was not tried. `type_i_sections_at` is limited to 2024 and later, and it looks for entries of `f64`, eight `ff` and an `i64` parameter; whether a 32-bit file stores them that way was not looked at. +- `tools/re/bodies_across_saves.py` and Measure's gate do not hold these saves; they cover 2026 and 2027. The saves are held to later saves of the same samples, which cannot catch an error those share, and `rst_basic` 2015 and `rst_advanced` 2019 have none (`rst_advanced` 2015 is held to its 2019 save). +- Whether the first line in a beam's data is its location line: `probe_re362_beam_lines_pre2023` counts, for the lines the reader accepts, the lines the data holds and whether another also makes a beam solid. A perpendicular line spanning the box's other extent would be a body no scorer here sees. Not yet run on the 2015 and 2023 structural samples. +- Sketch lines, arcs, circles and shaft lines on 2015 to 2023: another slice. + +## 7. Reproduce + +```bash +tools/fetch-autodesk-samples.sh samples 2015 2024 +for f in samples/*.rvt; do rvt-ifc "$f" -o "${f%.rvt}.ifc"; done +S=partition_beam_axis,partition_beam_box_along_axis +python3 tools/re/save_pair_bodies.py samples/2023-rstbasicsampleproject.ifc samples/2024-rstbasicsampleproject.ifc --sources $S +python3 tools/re/save_pair_bodies.py samples/2016-rst_basic_sample_project.ifc samples/2024-rstbasicsampleproject.ifc --sources $S +# Section 4: the same save exported by this head against main's ("box off" is the offset). +python3 tools/re/save_pair_bodies.py head/2016-rst_basic_sample_project.ifc main/2016-rst_basic_sample_project.ifc --sources $S +``` diff --git a/src/research/partition_beam_axes.rs b/src/research/partition_beam_axes.rs index 2875da6dc..1d0257455 100644 --- a/src/research/partition_beam_axes.rs +++ b/src/research/partition_beam_axes.rs @@ -115,6 +115,24 @@ pub fn supports_shaft_outlines(revit_version: u32) -> bool { SHAFT_OUTLINE_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) } +/// Releases 2015 to 2023, where a beam's location line and rotation are read +/// from the 32-bit records, apart from [`BEAM_AXIS_SUPPORTED_REVIT_VERSIONS`] +/// (2024 to 2027), which also gates [`scan_bounded_lines`], not measured on these +/// releases. The line is the record a wall's is on them (RE-114, RE-180, +/// RE-181). It is measured on Autodesk's sample projects only, against the same +/// samples' later saves (the 2024 saves, `rst_advanced` 2015's 2019 save, +/// `rst_basic` 2015 none), and on no model from elsewhere: none of BIMcollab's +/// 91 Revit 2015 beams holds a line in the window (RE-362, #1514). A +/// range of its own, not the whole 32-bit set (`REVIT_32BIT_RELEASES`, 2008 to +/// 2023): 2008 to 2014 are not read. +pub const BEAM_AXIS_2015_TO_2023_REVIT_VERSIONS: std::ops::RangeInclusive = 2015..=2023; + +/// Whether a beam's location line is read on `revit_version`. +pub fn beam_axis_read_on(revit_version: u32) -> bool { + supports_revit_version(revit_version) + || BEAM_AXIS_2015_TO_2023_REVIT_VERSIONS.contains(&revit_version) +} + /// A bounded line record. #[derive(Debug, Clone, Copy, PartialEq)] pub struct BoundedLine { @@ -747,13 +765,13 @@ fn collect_copies( /// What [`scan_bounded_lines`] finds for each id in `ids`, without choosing /// among copies: the partitions whose element data names the id, and the /// line each copy holds, for a probe of why a beam keeps its box (#1247). -/// Empty for a release this layout is not measured on. +/// Empty for a release a beam's line is not read on ([`beam_axis_read_on`]). pub fn line_copies( rf: &mut RevitFile, revit_version: u32, ids: &BTreeSet, ) -> Result> { - if !supports_revit_version(revit_version) { + if !beam_axis_read_on(revit_version) { return Ok(BTreeMap::new()); } let (mut found, holders, _) = @@ -771,14 +789,14 @@ pub fn line_copies( pub type AllLineCopies = BTreeMap)>>; /// What [`all_bounded_lines`] finds in each copy of each id's element data in -/// `ids`, without choosing among copies (RE-328). Empty for a release this -/// layout is not measured on. +/// `ids`, without choosing among copies (RE-328). Empty for a release a +/// beam's line is not read on ([`beam_axis_read_on`]). pub fn all_line_copies( rf: &mut RevitFile, revit_version: u32, ids: &BTreeSet, ) -> Result { - if !supports_revit_version(revit_version) { + if !beam_axis_read_on(revit_version) { return Ok(BTreeMap::new()); } let (found, _, _) = collect_copies(rf, revit_version, ids, all_bounded_lines, false)?; diff --git a/src/research/partition_schema_mvp.rs b/src/research/partition_schema_mvp.rs index 382ee48ec..9e51241fa 100644 --- a/src/research/partition_schema_mvp.rs +++ b/src/research/partition_schema_mvp.rs @@ -186,8 +186,8 @@ pub fn recover_partition_schema_mvp( } // --- Revit 2015 to 2023 element records (RE-81, RE-180, RE-181, #421): - // identity, category and box only; nothing else 2024 decodes on top of - // records is read. --- + // identity, category, box and the readers called here; nothing else 2024 + // decodes on top of records is read. --- if crate::partition_element_records_2023::is_32bit_release(revit_version) { recover_2023_records(rf, revit_version, &mut out); attach_room_outlines(rf, revit_version, &mut out.rooms); @@ -239,6 +239,14 @@ pub fn recover_partition_schema_mvp( &mut out.products, ], ); + // --- Beams along their location lines, the rotation first (RE-276, RE-49, + // RE-362). A beam a named column trims keeps its cut box (RE-122): both + // readers skip it, since the trim rewrote the record box they would test + // a line against. --- + if crate::partition_beam_axes::beam_axis_read_on(revit_version) { + attach_beam_rotations(rf, &mut out.products); + attach_beam_axes(rf, revit_version, &mut out.products); + } // --- System families' names (RE-63); a wall's, roof's or // ceiling's needs its type's layers, not read on 2023 --- attach_system_family_names( diff --git a/src/research/partition_schema_mvp/framing.rs b/src/research/partition_schema_mvp/framing.rs index 772192ab4..1bb34656b 100644 --- a/src/research/partition_schema_mvp/framing.rs +++ b/src/research/partition_schema_mvp/framing.rs @@ -268,6 +268,16 @@ pub fn beam_unrotated(fields: &[(String, InstanceField)]) -> bool { }) } +/// Whether a column cut a beam's body (RE-122): its record box fields are then +/// the cut box's, which `apply_beam_column_trim` wrote over the record's, so the +/// beam keeps that body and neither a location line nor a rotation is read for it. +fn beam_column_cut(element: &DecodedElement) -> bool { + element + .fields + .iter() + .any(|(name, _)| name == crate::element_record_beam_cuts::BEAM_BODY_SOURCE_FIELD) +} + /// Give each structural-framing element the location line its data carries, /// when the line and the element's record box make a beam solid /// ([`crate::partition_beam_axes::beam_body`], RE-49), or, for an unrotated @@ -275,30 +285,31 @@ pub fn beam_unrotated(fields: &[(String, InstanceField)]) -> bool { /// beam whose box is longer than its line /// ([`crate::partition_beam_axes::beam_body_in_box`], RE-328). A line that /// leaves the box, or a box no solid along the line reproduces, is not -/// attached. +/// attached. A beam a column cuts (RE-122) is skipped: its record box fields are +/// the cut box's, a line is tested against them, and its body is the cut box's. pub(super) fn attach_beam_axes( rf: &mut RevitFile, revit_version: u32, products: &mut [DecodedElement], ) { use crate::partition_beam_axes as pba; - if !pba::supports_revit_version(revit_version) { + if !pba::beam_axis_read_on(revit_version) { return; } let beams: BTreeSet = products .iter() - .filter(|element| element.class == "StructuralFraming") + .filter(|element| element.class == "StructuralFraming" && !beam_column_cut(element)) .filter_map(|element| element.id) .collect(); if beams.is_empty() { return; } - let Ok(lines) = pba::scan_bounded_lines(rf, revit_version, &beams) else { + let Ok(lines) = pba::scan_first_bounded_lines(rf, revit_version, &beams) else { return; }; for element in products .iter_mut() - .filter(|element| element.class == "StructuralFraming") + .filter(|element| element.class == "StructuralFraming" && !beam_column_cut(element)) { let Some(line) = element.id.and_then(|id| lines.get(&id)) else { continue; @@ -327,11 +338,12 @@ pub(super) fn attach_beam_axes( /// `m_rotation`, radians, of its `FamInstCurveDriver`, which revit-ifc's /// `RollCalculator` reads from the element's `STRUCTURAL_BEND_DIR_ANGLE` /// parameter (CLEANROOM.md source 5). A beam whose copies disagree, or whose -/// record is not read, gets none. +/// record is not read, gets none, nor does one a column cuts (RE-122), which +/// is not drawn along a line. pub(super) fn attach_beam_rotations(rf: &mut RevitFile, products: &mut [DecodedElement]) { let beams: BTreeSet = products .iter() - .filter(|element| element.class == "StructuralFraming") + .filter(|element| element.class == "StructuralFraming" && !beam_column_cut(element)) .filter_map(|element| element.id) .map(u64::from) .collect(); @@ -340,6 +352,7 @@ pub(super) fn attach_beam_rotations(rf: &mut RevitFile, products: &mut [DecodedE } let options = crate::native_document::Options { selected_ids: beams, + releases: crate::native_document::NATIVE_RELEASES, ..Default::default() }; let mut rotations: BTreeMap> = BTreeMap::new();