use crate::partition_arc_walls::{self, PartitionArcWall};
use crate::partition_name_candidates::{
NameBucket, building_storey_name_candidates, classify_name, collect_name_candidates,
};
use crate::rect_opening_index::ArcWallRectOpeningIndex;
use crate::walker::{DecodedElement, ElementProvenance, InstanceField, WalkerLimits};
use crate::{Result, RevitFile};
use std::collections::{BTreeMap, BTreeSet};
use std::f64::consts::FRAC_PI_4;
#[derive(Debug, Clone, Default)]
pub struct PartitionSchemaMvp {
pub levels: Vec<DecodedElement>,
pub materials: Vec<DecodedElement>,
pub rooms: Vec<DecodedElement>,
pub rect_openings: Vec<DecodedElement>,
pub columns: Vec<DecodedElement>,
pub walls: Vec<DecodedElement>,
pub doors: Vec<DecodedElement>,
pub windows: Vec<DecodedElement>,
pub slabs: Vec<DecodedElement>,
pub products: Vec<DecodedElement>,
}
impl PartitionSchemaMvp {
pub fn into_elements(self) -> Vec<DecodedElement> {
let mut out = Vec::with_capacity(
self.levels.len()
+ self.materials.len()
+ self.rooms.len()
+ self.rect_openings.len()
+ self.columns.len()
+ self.walls.len()
+ self.doors.len()
+ self.windows.len()
+ self.slabs.len()
+ self.products.len(),
);
out.extend(self.levels);
out.extend(self.materials);
out.extend(self.rooms);
out.extend(self.rect_openings);
out.extend(self.columns);
out.extend(self.walls);
out.extend(self.doors);
out.extend(self.windows);
out.extend(self.slabs);
out.extend(self.products);
out
}
}
pub fn recover_partition_schema_mvp(
rf: &mut RevitFile,
revit_version: u32,
limits: WalkerLimits,
) -> Result<PartitionSchemaMvp> {
let mut out = PartitionSchemaMvp::default();
let strings = rf.partition_string_records().unwrap_or_default();
let string_values: Vec<&str> = strings.iter().map(|r| r.value.as_str()).collect();
let level_names = building_storey_name_candidates(string_values.iter().copied());
let name_set = collect_name_candidates(string_values.iter().copied());
let walls = match partition_arc_walls::scan_partition_arc_walls_with_limits(
rf,
revit_version,
limits,
) {
Ok(scan) => scan.walls,
Err(_) => Vec::new(),
};
out.levels = levels_from_storeys_and_names(&walls, &level_names);
out.materials = match materials_from_records(rf, revit_version) {
Some(materials) => materials,
None => materials_from_names(&name_set),
};
if ArcWallRectOpeningIndex::supports_revit_version(revit_version) {
out.rect_openings = rect_openings_from_partitions(rf, revit_version, limits)?;
}
if revit_version == crate::partition_element_records_2023::REVIT_2023 {
recover_2023_records(rf, &mut out);
attach_room_outlines(rf, revit_version, &mut out.rooms);
let level_elevations: std::collections::BTreeMap<u32, f64> =
crate::partition_level_records::recover_partition_levels(rf, revit_version)
.unwrap_or_default()
.iter()
.map(|level| (level.element_id, level.elevation_feet))
.collect();
let mut record_backed = [
&mut out.walls,
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.slabs,
&mut out.rooms,
&mut out.products,
];
resolve_framing_top_levels(&level_elevations, &mut record_backed);
resolve_base_constraint_levels(&level_elevations, &mut record_backed);
resolve_base_at_level(&level_elevations, &mut record_backed);
resolve_remaining_levels(&level_elevations, &mut record_backed);
attach_system_family_names(
rf,
revit_version,
&mut [&mut out.walls, &mut out.slabs, &mut out.products],
);
attach_wall_layers(rf, revit_version, &mut out.walls);
attach_slab_layers(
rf,
revit_version,
out.slabs
.iter_mut()
.chain(out.products.iter_mut())
.collect(),
);
let mut type_materials = crate::partition_type_materials::type_material_names_2023(rf);
for element in out.walls.iter().chain(&out.slabs).chain(&out.products) {
let system = element.fields.iter().any(|(name, value)| {
name == FAMILY_NAME_SOURCE_FIELD
&& matches!(value, InstanceField::String(source) if source == SYSTEM_FAMILY_SOURCE)
});
let type_id = element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
});
if let (true, Some(type_id)) = (system, type_id) {
type_materials.remove(&type_id);
}
}
for elements in [
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.products,
] {
attach_type_materials(&type_materials, elements);
}
return Ok(out);
}
let level_ids = level_element_ids(rf, revit_version)?;
let (column_records, wall_records) = column_and_wall_records(rf, revit_version)?;
let wall_instances: Vec<crate::partition_element_records::PartitionElementRecord> =
select_instance_records(wall_records.clone())
.into_values()
.collect();
out.walls = wall_instances_from_records(wall_records, &level_ids);
out.columns = column_instances_from_records(column_records, &level_ids, &wall_instances);
let wall_ids: BTreeSet<u32> = out.walls.iter().filter_map(|wall| wall.id).collect();
out.doors = openings_from_partition_category_records(
rf,
revit_version,
crate::partition_element_records::OST_DOORS,
"Door",
&wall_ids,
&level_ids,
)?;
out.windows = openings_from_partition_category_records(
rf,
revit_version,
crate::partition_element_records::OST_WINDOWS,
"Window",
&wall_ids,
&level_ids,
)?;
out.slabs = slabs_from_partition_category_records(rf, revit_version, &level_ids)?;
out.rooms = rooms_from_partition_category_records(rf, revit_version, &level_ids)?;
attach_room_outlines(rf, revit_version, &mut out.rooms);
out.products = product_instances_from_partition_records(rf, revit_version, &level_ids)?;
let level_elevations: std::collections::BTreeMap<u32, f64> =
crate::partition_level_records::recover_partition_levels(rf, revit_version)
.unwrap_or_default()
.iter()
.map(|level| (level.element_id, level.elevation_feet))
.collect();
let mut record_backed = [
&mut out.walls,
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.slabs,
&mut out.rooms,
&mut out.products,
];
resolve_base_constraint_levels(&level_elevations, &mut record_backed);
resolve_base_at_level(&level_elevations, &mut record_backed);
resolve_hosted_levels(rf, &level_elevations, &mut record_backed);
resolve_remaining_levels(&level_elevations, &mut record_backed);
attach_aggregate_wholes(rf, &mut out.products);
attach_curtain_walls(rf, revit_version, &mut out.walls, &mut out.products);
bind_opening_hosts(&out.walls, &mut out.doors);
bind_opening_hosts(&out.walls, &mut out.windows);
attach_stair_dimensions(rf, revit_version, &mut out.products);
attach_stair_run_bodies(rf, revit_version, &mut out.products);
attach_beam_axes(rf, revit_version, &mut out.products);
attach_roof_profiles(rf, revit_version, &mut out.products);
attach_shaft_voids(rf, revit_version, [&mut out.slabs, &mut out.products]);
if let Ok(classes) = rf.schema_classes() {
for elements in [
&mut out.walls,
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.slabs,
&mut out.rooms,
&mut out.products,
] {
attach_revit_classes(&classes, elements);
}
}
attach_instance_axes(
rf,
revit_version,
[&mut out.doors, &mut out.windows, &mut out.products],
);
attach_curtain_axes(rf, revit_version, &mut out.products);
for elements in [
&mut out.walls,
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.slabs,
&mut out.products,
] {
attach_family_and_type_names(rf, elements);
}
let mut unnamed: Vec<&mut DecodedElement> = [&mut out.walls, &mut out.slabs, &mut out.products]
.into_iter()
.flat_map(|elements| elements.iter_mut())
.filter(|element| {
!element
.fields
.iter()
.any(|(name, _)| name == TYPE_NAME_FIELD)
})
.collect();
attach_system_type_names(rf, revit_version, &mut unnamed);
let mut unnamed_panels: Vec<&mut DecodedElement> = out
.products
.iter_mut()
.filter(|element| {
element.class == "CurtainWallPanel"
&& !element
.fields
.iter()
.any(|(name, _)| name == TYPE_NAME_FIELD)
})
.collect();
attach_panel_wall_types(rf, revit_version, &mut unnamed_panels);
attach_model_text_types(rf, revit_version, &mut out.products);
attach_wall_sweep_types(rf, revit_version, &mut out.products);
attach_wall_layers(rf, revit_version, &mut out.walls);
attach_roof_slopes(rf, revit_version, &mut out.products);
attach_slab_layers(
rf,
revit_version,
out.slabs
.iter_mut()
.chain(out.products.iter_mut())
.collect(),
);
attach_system_family_names(
rf,
revit_version,
&mut [&mut out.walls, &mut out.slabs, &mut out.products],
);
attach_stair_names(rf, revit_version, &mut out.products);
attach_ifc_export_overrides(
rf,
revit_version,
[
&mut out.walls,
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.slabs,
&mut out.products,
],
);
out.products
.retain(|product| !is_empty_curtain_panel(product));
for product in &mut out.products {
if product.class == "CurtainWallPanel"
&& product.fields.iter().any(|(name, value)| {
name == CURTAIN_PANEL_WALL_FIELD && matches!(value, InstanceField::Bool(true))
})
{
product.class = CURTAIN_PANEL_WALL_CLASS.into();
}
}
let type_parameters = crate::partition_type_parameters::type_text_parameters(rf, revit_version);
if !type_parameters.is_empty() {
for elements in [
&mut out.walls,
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.slabs,
&mut out.products,
] {
attach_type_text_parameters(&type_parameters, elements);
}
}
let type_materials = crate::partition_type_materials::type_material_names(rf, revit_version);
if !type_materials.is_empty() {
for elements in [
&mut out.walls,
&mut out.columns,
&mut out.doors,
&mut out.windows,
&mut out.slabs,
&mut out.products,
] {
attach_type_materials(&type_materials, elements);
}
}
attach_window_openings(rf, revit_version, &mut out.windows);
attach_door_openings(rf, revit_version, &mut out.doors);
attach_beam_sections(rf, revit_version, &mut out.products);
Ok(out)
}
pub const IFC_EXPORT_AS_FIELD: &str = "m_ifc_export_as";
pub const IFC_PREDEFINED_TYPE_FIELD: &str = "m_ifc_predefined_type";
fn attach_ifc_export_overrides(
rf: &mut RevitFile,
revit_version: u32,
lists: [&mut Vec<DecodedElement>; 6],
) {
use crate::partition_ifc_export_overrides as ieo;
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let parameters = ieo::scan_export_parameters(rf, revit_version, &declared).unwrap_or_default();
if parameters.is_empty() {
return;
}
let none = ieo::ExportParameters::default();
for elements in lists {
for element in elements.iter_mut() {
let own = element
.id
.and_then(|id| parameters.get(&id))
.unwrap_or(&none);
let of_type = element
.fields
.iter()
.find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
.and_then(|id| parameters.get(&id));
if let Some(entity) = own.effective_export_as(of_type) {
element
.fields
.push((IFC_EXPORT_AS_FIELD.into(), InstanceField::String(entity)));
}
if let Some(predefined) = own.effective_predefined_type(of_type) {
element.fields.push((
IFC_PREDEFINED_TYPE_FIELD.into(),
InstanceField::String(predefined),
));
}
}
}
}
fn attach_system_type_names(
rf: &mut RevitFile,
revit_version: u32,
elements: &mut [&mut DecodedElement],
) {
use crate::partition_type_records as ptr;
if elements.is_empty() || !ptr::supports_revit_version(revit_version) {
return;
}
let Some(header) = crate::partition_names::element_data_header(revit_version) else {
return;
};
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let mut type_ids_by_category: std::collections::BTreeMap<i64, BTreeSet<u32>> =
std::collections::BTreeMap::new();
let mut picks: Vec<(usize, u32)> = Vec::new();
for (index, element) in elements.iter().enumerate() {
let Some((references, category)) = record_references(rf, element) else {
continue;
};
let type_ids = type_ids_by_category.entry(category).or_insert_with(|| {
let records =
ptr::scan_type_records(rf, revit_version, category, &declared).unwrap_or_default();
ptr::type_definition_ids(&records)
});
if let Some(type_id) = ptr::unique_type_reference(&references, type_ids) {
picks.push((index, type_id));
}
}
if picks.is_empty() {
return;
}
let wanted: BTreeSet<u32> = picks.iter().map(|(_, id)| *id).collect();
let mut names = type_data_names(rf, &header, &wanted);
let railing_types: BTreeSet<u32> = picks
.iter()
.filter(|(index, id)| {
elements[*index].class == "Railing" && !matches!(names.get(id), Some(Some(_)))
})
.map(|(_, id)| *id)
.collect();
if revit_version == 2024 && !railing_types.is_empty() {
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
for (id, name) in
crate::partition_names::find_railing_type_names(inflated.bytes(), &railing_types)
{
match names.get(&id) {
Some(Some(held)) if *held != name => {
names.insert(id, None);
}
Some(None) => {}
_ => {
names.insert(id, Some(name));
}
}
}
}
}
attach_type_picks(elements, picks, &names);
}
fn type_data_names(
rf: &mut RevitFile,
header: &[u8; 10],
wanted: &BTreeSet<u32>,
) -> std::collections::BTreeMap<u32, Option<String>> {
let mut names: std::collections::BTreeMap<u32, Option<String>> =
std::collections::BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
for (id, name) in
crate::partition_names::find_element_data_names(inflated.bytes(), header, wanted)
{
match names.get(&id) {
None => {
names.insert(id, Some(name));
}
Some(held) if held.as_deref() != Some(name.as_str()) => {
names.insert(id, None);
}
_ => {}
}
}
}
names
}
fn attach_type_picks(
elements: &mut [&mut DecodedElement],
picks: Vec<(usize, u32)>,
names: &std::collections::BTreeMap<u32, Option<String>>,
) {
for (index, type_id) in picks {
let Some(Some(name)) = names.get(&type_id) else {
continue;
};
let element = &mut elements[index];
element.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: type_id,
},
));
element
.fields
.push((TYPE_NAME_FIELD.into(), InstanceField::String(name.clone())));
}
}
fn attach_wall_sweep_types(
rf: &mut RevitFile,
revit_version: u32,
products: &mut [DecodedElement],
) {
if revit_version != 2024 {
return;
}
let Some(header) = crate::partition_names::element_data_header(revit_version) else {
return;
};
let has = |element: &DecodedElement, field: &str| {
element.fields.iter().any(|(name, _)| name == field)
};
let mut lists: Vec<(usize, BTreeSet<u32>)> = Vec::new();
for (index, element) in products.iter().enumerate() {
if element.class != "WallSweep"
|| has(element, TYPE_NAME_FIELD)
|| has(element, FAMILY_NAME_FIELD)
|| has(element, ELEMENT_NAME_FIELD)
{
continue;
}
let Some((references, _)) = record_references(rf, element) else {
continue;
};
let named: BTreeSet<u32> = references
.iter()
.filter_map(|&slot| u32::try_from(slot).ok())
.filter(|id| Some(*id) != element.id)
.collect();
lists.push((index, named));
}
if lists.is_empty() {
return;
}
let entries = rf.element_names();
let wanted: BTreeSet<u32> = lists
.iter()
.flat_map(|(_, ids)| ids.iter().copied())
.filter(|id| !entries.entries.contains_key(id))
.collect();
let mut objects: BTreeSet<u32> = BTreeSet::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
objects.extend(crate::partition_names::find_type_object_ids(
inflated.bytes(),
&wanted,
));
}
let names = type_data_names(rf, &header, &objects);
for (index, ids) in lists {
let types: Vec<u32> = ids
.iter()
.copied()
.filter(|id| objects.contains(id))
.collect();
let element = &mut products[index];
match types.as_slice() {
[] => {
if let Some(id) = element.id {
element.fields.push((
ELEMENT_NAME_FIELD.into(),
InstanceField::String(id.to_string()),
));
}
}
[type_id] => {
let Some(Some(name)) = names.get(type_id) else {
continue;
};
element.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: *type_id,
},
));
element
.fields
.push((TYPE_NAME_FIELD.into(), InstanceField::String(name.clone())));
element.fields.push((
FAMILY_NAME_FIELD.into(),
InstanceField::String("Wall Sweep".into()),
));
element.fields.push((
FAMILY_NAME_SOURCE_FIELD.into(),
InstanceField::String(SYSTEM_FAMILY_SOURCE.into()),
));
}
_ => {}
}
}
}
fn attach_model_text_types(
rf: &mut RevitFile,
revit_version: u32,
products: &mut [DecodedElement],
) {
if revit_version != 2024 {
return;
}
let has = |element: &DecodedElement, field: &str| {
element.fields.iter().any(|(name, _)| name == field)
};
let mut lists: Vec<(usize, BTreeSet<u32>)> = Vec::new();
for (index, element) in products.iter().enumerate() {
if element.class != "GenericModel"
|| has(element, TYPE_NAME_FIELD)
|| has(element, FAMILY_NAME_FIELD)
{
continue;
}
let Some((references, _)) = record_references(rf, element) else {
continue;
};
let named: BTreeSet<u32> = references
.iter()
.filter_map(|&slot| u32::try_from(slot).ok())
.filter(|id| Some(*id) != element.id)
.collect();
lists.push((index, named));
}
if lists.is_empty() {
return;
}
let wanted: BTreeSet<u32> = lists
.iter()
.flat_map(|(_, ids)| ids.iter().copied())
.collect();
let mut names: std::collections::BTreeMap<u32, Option<String>> =
std::collections::BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
for (id, name) in crate::partition_names::find_text_type_names(inflated.bytes(), &wanted) {
match names.get(&id) {
Some(Some(held)) if *held != name => {
names.insert(id, None);
}
Some(None) => {}
_ => {
names.insert(id, Some(name));
}
}
}
}
for (index, ids) in lists {
let text_types: Vec<(u32, &String)> = ids
.iter()
.filter_map(|id| match names.get(id) {
Some(Some(name)) => Some((*id, name)),
_ => None,
})
.collect();
let [(type_id, name)] = text_types.as_slice() else {
continue;
};
let element = &mut products[index];
element.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: *type_id,
},
));
element.fields.push((
TYPE_NAME_FIELD.into(),
InstanceField::String((*name).clone()),
));
element.fields.push((
FAMILY_NAME_FIELD.into(),
InstanceField::String("Model Text".into()),
));
element.fields.push((
FAMILY_NAME_SOURCE_FIELD.into(),
InstanceField::String(TYPE_KIND_FAMILY_SOURCE.into()),
));
}
}
fn attach_panel_wall_types(
rf: &mut RevitFile,
revit_version: u32,
panels: &mut [&mut DecodedElement],
) {
use crate::partition_type_records as ptr;
if panels.is_empty()
|| !ptr::supports_revit_version(revit_version)
|| !crate::partition_compound_structure::COMPOUND_STRUCTURE_SUPPORTED_REVIT_VERSIONS
.contains(&revit_version)
{
return;
}
let Some(header) = crate::partition_names::element_data_header(revit_version) else {
return;
};
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let wall_types = ptr::type_definition_ids(
&ptr::scan_type_records(
rf,
revit_version,
crate::partition_element_records::OST_WALLS,
&declared,
)
.unwrap_or_default(),
);
let mut picks: Vec<(usize, u32)> = Vec::new();
for (index, panel) in panels.iter().enumerate() {
let Some((references, _)) = record_references(rf, panel) else {
continue;
};
if let Some(type_id) = ptr::unique_type_reference(&references, &wall_types) {
picks.push((index, type_id));
}
}
if picks.is_empty() {
return;
}
let candidates: BTreeSet<u32> = picks.iter().map(|(_, id)| *id).collect();
let materials: BTreeSet<u32> =
ptr::scan_type_records(rf, revit_version, ptr::OST_MATERIALS, &declared)
.unwrap_or_default()
.iter()
.map(|record| record.element_id)
.collect();
let layered: BTreeSet<u32> = crate::partition_compound_structure::scan_type_layers(
rf,
revit_version,
&candidates,
&materials,
&declared,
)
.unwrap_or_default()
.into_iter()
.filter(|(_, layers)| !layers.is_empty())
.map(|(id, _)| id)
.collect();
picks.retain(|(_, id)| layered.contains(id));
let names = type_data_names(rf, &header, &layered);
attach_type_picks(panels, picks, &names);
for panel in panels.iter_mut() {
let wall_type = panel.fields.iter().any(|(name, value)| {
matches!(value, InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD && layered.contains(id))
});
if wall_type {
panel
.fields
.push((CURTAIN_PANEL_WALL_FIELD.into(), InstanceField::Bool(true)));
}
}
}
pub const EMPTY_PANEL_FAMILY_NAME: &str = "Empty System Panel";
fn is_empty_curtain_panel(element: &DecodedElement) -> bool {
element.class == "CurtainWallPanel"
&& element.fields.iter().any(|(name, value)| {
matches!(value, InstanceField::String(family) if name == FAMILY_NAME_FIELD && family == EMPTY_PANEL_FAMILY_NAME)
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EmptyCurtainPanels {
pub count: usize,
pub types_with_material: BTreeSet<u32>,
}
pub fn scan_empty_curtain_panels(
rf: &mut RevitFile,
revit_version: u32,
) -> Result<EmptyCurtainPanels> {
use crate::partition_element_records as per;
let mut out = EmptyCurtainPanels::default();
if !per::supports_revit_version(revit_version) {
return Ok(out);
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(out),
};
let names = rf.element_names();
let empty_families: BTreeSet<u32> = names
.entries
.iter()
.filter(|(_, entry)| entry.name == EMPTY_PANEL_FAMILY_NAME)
.map(|(id, _)| *id)
.collect();
if empty_families.is_empty() {
return Ok(out);
}
let records =
per::scan_category_records(rf, revit_version, per::OST_CURTAIN_WALL_PANELS, &declared)?;
let options = rf.design_options();
let mut panels: BTreeSet<u32> = BTreeSet::new();
let mut types: BTreeSet<u32> = BTreeSet::new();
for record in records
.iter()
.filter(|r| r.is_exported_instance() && !options.excludes(r) && r.has_volume())
{
let Some(type_id) = crate::partition_names::resolve_type(
&names,
&record.references,
record.builtin_category,
record.element_id,
) else {
continue;
};
if crate::partition_names::resolve_family(&names, type_id)
.is_some_and(|family| empty_families.contains(&family))
{
panels.insert(record.element_id);
types.insert(type_id);
}
}
out.count = panels.len();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
for (id, material) in
crate::partition_names::find_panel_type_materials(inflated.bytes(), &types)
{
if material.is_some() {
out.types_with_material.insert(id);
}
}
}
Ok(out)
}
pub const CURTAIN_PANEL_WALL_FIELD: &str = "m_curtain_panel_is_wall";
pub const CURTAIN_PANEL_WALL_CLASS: &str = "CurtainPanelWall";
pub const TYPE_NAME_FIELD: &str = "m_type_name";
pub const FAMILY_NAME_FIELD: &str = "m_family_name";
pub const TYPE_ID_FIELD: &str = "m_type_id";
pub const CLASS_TAG_FIELD: &str = "m_class_tag";
pub const REVIT_CLASS_FIELD: &str = "m_revit_class";
fn attach_revit_classes(classes: &crate::formats::SchemaClasses, elements: &mut [DecodedElement]) {
for element in elements.iter_mut() {
let tag = element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Integer { value, .. } if name == CLASS_TAG_FIELD => {
u16::try_from(*value).ok()
}
_ => None,
});
if let Some(class) = tag.and_then(|tag| classes.by_tag(tag)) {
element.fields.push((
REVIT_CLASS_FIELD.into(),
InstanceField::String(class.name.clone()),
));
}
}
}
pub const CURTAIN_AXES_FIELDS: [&str; 9] = [
"m_curtain_axis_xx",
"m_curtain_axis_xy",
"m_curtain_axis_xz",
"m_curtain_axis_yx",
"m_curtain_axis_yy",
"m_curtain_axis_yz",
"m_curtain_axis_zx",
"m_curtain_axis_zy",
"m_curtain_axis_zz",
];
pub const CURTAIN_AXES_CLASSES: [&str; 2] = ["CurtainWallMullion", "CurtainWallPanel"];
pub fn curtain_axes_from_fields(fields: &[(String, InstanceField)]) -> Option<[[f64; 3]; 3]> {
let values = CURTAIN_AXES_FIELDS.map(|wanted| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
});
let mut axes = [[0.0; 3]; 3];
for (index, value) in values.into_iter().enumerate() {
axes[index / 3][index % 3] = value?;
}
Some(axes)
}
fn attach_curtain_axes(rf: &mut RevitFile, revit_version: u32, products: &mut [DecodedElement]) {
let ids: BTreeSet<u32> = products
.iter()
.filter(|element| CURTAIN_AXES_CLASSES.contains(&element.class.as_str()))
.filter_map(|element| element.id)
.collect();
if ids.is_empty() {
return;
}
let Ok(transforms) =
crate::partition_instance_transforms::scan_instance_transforms(rf, revit_version, &ids)
else {
return;
};
for element in products.iter_mut() {
let Some(transform) = element.id.and_then(|id| transforms.get(&id)) else {
continue;
};
let on_axes = transform.axes.iter().all(|axis| {
axis.iter()
.filter(|v| v.abs() > INSTANCE_TURN_MIN_RADIANS)
.count()
== 1
});
if on_axes {
continue;
}
let values = transform.axes.iter().flatten().copied();
for (name, value) in CURTAIN_AXES_FIELDS.iter().zip(values) {
element
.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
}
}
pub const INSTANCE_X_AXIS_FIELDS: [&str; 2] = ["m_instance_x_axis_x", "m_instance_x_axis_y"];
pub const INSTANCE_ORIGIN_FIELDS: [&str; 2] = ["m_instance_origin_x", "m_instance_origin_y"];
pub const INSTANCE_TURN_MIN_RADIANS: f64 = 1e-4;
fn attach_instance_axes(
rf: &mut RevitFile,
revit_version: u32,
groups: [&mut Vec<DecodedElement>; 3],
) {
let is_instance = |element: &DecodedElement| {
element.fields.iter().any(|(name, value)| {
name == REVIT_CLASS_FIELD
&& matches!(value, InstanceField::String(class) if class == "FamilyInstance")
})
};
let ids: BTreeSet<u32> = groups
.iter()
.flat_map(|elements| elements.iter())
.filter(|element| is_instance(element))
.filter_map(|element| element.id)
.collect();
if ids.is_empty() {
return;
}
let Ok(transforms) =
crate::partition_instance_transforms::scan_instance_transforms(rf, revit_version, &ids)
else {
return;
};
for element in groups.into_iter().flat_map(|elements| elements.iter_mut()) {
let Some((transform, [x, y])) = element
.id
.and_then(|id| transforms.get(&id))
.and_then(|transform| Some((transform, transform.plan_axis()?)))
else {
continue;
};
let turn = y.abs().atan2(x.abs());
if turn.min(std::f64::consts::FRAC_PI_2 - turn) < INSTANCE_TURN_MIN_RADIANS {
continue;
}
let [ox, oy, _] = transform.origin;
for (name, value) in INSTANCE_X_AXIS_FIELDS
.iter()
.zip([x, y])
.chain(INSTANCE_ORIGIN_FIELDS.iter().zip([ox, oy]))
{
element
.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
}
}
pub const FILLER_OPENING_FIELDS: [&str; 7] = [
"m_filler_opening_x",
"m_filler_opening_y",
"m_filler_opening_axis_x",
"m_filler_opening_axis_y",
"m_filler_opening_base",
"m_filler_opening_width",
"m_filler_opening_height",
];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FillerOpening {
pub centre: [f64; 2],
pub axis: [f64; 2],
pub base_feet: f64,
pub width_feet: f64,
pub height_feet: f64,
}
fn attach_window_openings(rf: &mut RevitFile, revit_version: u32, windows: &mut [DecodedElement]) {
let type_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
};
let types: BTreeSet<u32> = windows.iter().filter_map(type_of).collect();
let openings =
crate::partition_type_parameters::type_window_openings(rf, revit_version, &types);
if openings.is_empty() {
return;
}
let ids: BTreeSet<u32> = windows
.iter()
.filter(|element| type_of(element).is_some_and(|id| openings.contains_key(&id)))
.filter_map(|element| element.id)
.collect();
let Ok(transforms) =
crate::partition_instance_transforms::scan_instance_transforms(rf, revit_version, &ids)
else {
return;
};
for element in windows.iter_mut() {
let Some((opening, transform)) = type_of(element)
.and_then(|id| openings.get(&id))
.zip(element.id.and_then(|id| transforms.get(&id)))
else {
continue;
};
push_filler_opening(
element,
transform,
opening.default_sill_feet,
opening.width_feet,
opening.height_feet,
);
}
}
pub const DOOR_ROUGH_HEIGHT_TOLERANCE_FEET: f64 = 1e-3;
pub const BEAM_I_SECTION_FIELDS: [&str; 4] = [
"m_i_section_width",
"m_i_section_depth",
"m_i_section_web",
"m_i_section_flange",
];
pub fn beam_i_section_from_fields(
fields: &[(String, InstanceField)],
) -> Option<crate::partition_type_parameters::ISection> {
let [width, depth, web, flange] = BEAM_I_SECTION_FIELDS.map(|wanted| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
});
Some(crate::partition_type_parameters::ISection {
width_feet: width?,
depth_feet: depth?,
web_feet: web?,
flange_feet: flange?,
})
}
pub const COLUMN_I_AXIS_FIELDS: [&str; 2] = ["m_i_section_axis_x", "m_i_section_axis_y"];
const I_SECTION_CLASSES: [&str; 2] = ["StructuralFraming", "StructuralColumn"];
fn attach_beam_sections(rf: &mut RevitFile, revit_version: u32, products: &mut [DecodedElement]) {
let type_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
};
let types: BTreeSet<u32> = products
.iter()
.filter(|element| I_SECTION_CLASSES.contains(&element.class.as_str()))
.filter_map(type_of)
.collect();
let sections = crate::partition_type_parameters::type_i_sections(rf, revit_version, &types);
let columns: BTreeSet<u32> = products
.iter()
.filter(|element| element.class == "StructuralColumn")
.filter(|element| type_of(element).is_some_and(|id| sections.contains_key(&id)))
.filter_map(|element| element.id)
.collect();
let transforms = if columns.is_empty() {
BTreeMap::new()
} else {
crate::partition_instance_transforms::scan_instance_transforms(rf, revit_version, &columns)
.unwrap_or_default()
};
for element in products
.iter_mut()
.filter(|element| I_SECTION_CLASSES.contains(&element.class.as_str()))
{
let Some(section) = type_of(element).and_then(|id| sections.get(&id)).copied() else {
continue;
};
let values = [
section.width_feet,
section.depth_feet,
section.web_feet,
section.flange_feet,
];
for (name, value) in BEAM_I_SECTION_FIELDS.iter().zip(values) {
element
.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
let axis = element
.id
.and_then(|id| transforms.get(&id))
.filter(|transform| transform.is_upright())
.and_then(|transform| transform.plan_axis());
if let Some(axis) = axis {
for (name, value) in COLUMN_I_AXIS_FIELDS.iter().zip(axis) {
element
.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
}
}
}
fn attach_door_openings(rf: &mut RevitFile, revit_version: u32, doors: &mut [DecodedElement]) {
let type_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
};
let box_height = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == "m_bboxHeight" => Some(*value),
_ => None,
})
};
let types: BTreeSet<u32> = doors.iter().filter_map(type_of).collect();
let openings = crate::partition_type_parameters::type_door_openings(rf, revit_version, &types);
let rough = |element: &DecodedElement| {
if element
.fields
.iter()
.any(|(name, _)| name == TYPE_WITHOUT_GEOMETRY_FIELD)
{
return None;
}
let opening = type_of(element).and_then(|id| openings.get(&id))?;
let height = box_height(element)?;
((height - opening.rough_height_feet).abs() <= DOOR_ROUGH_HEIGHT_TOLERANCE_FEET)
.then_some(*opening)
};
let ids: BTreeSet<u32> = doors
.iter()
.filter(|element| rough(element).is_some())
.filter_map(|element| element.id)
.collect();
if ids.is_empty() {
return;
}
let Ok(transforms) =
crate::partition_instance_transforms::scan_instance_transforms(rf, revit_version, &ids)
else {
return;
};
for element in doors.iter_mut() {
let Some((opening, transform)) =
rough(element).zip(element.id.and_then(|id| transforms.get(&id)))
else {
continue;
};
push_filler_opening(
element,
transform,
0.0,
opening.rough_width_feet,
opening.rough_height_feet,
);
}
}
fn push_filler_opening(
element: &mut DecodedElement,
transform: &crate::partition_instance_transforms::InstanceTransform,
base: f64,
width: f64,
height: f64,
) {
if !transform.is_upright() {
return;
}
let Some([ax, ay]) = transform.plan_axis() else {
return;
};
let [ox, oy, oz] = transform.origin;
let values = [ox, oy, ax, ay, oz + base, width, height];
for (name, value) in FILLER_OPENING_FIELDS.iter().zip(values) {
element
.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
}
pub fn filler_opening_from_fields(fields: &[(String, InstanceField)]) -> Option<FillerOpening> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [x, y, ax, ay, base, width, height] = FILLER_OPENING_FIELDS.map(float);
Some(FillerOpening {
centre: [x?, y?],
axis: [ax?, ay?],
base_feet: base?,
width_feet: width?,
height_feet: height?,
})
}
pub fn instance_origin_from_fields(fields: &[(String, InstanceField)]) -> Option<[f64; 2]> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [x, y] = INSTANCE_ORIGIN_FIELDS.map(float);
Some([x?, y?])
}
pub fn instance_x_axis_from_fields(fields: &[(String, InstanceField)]) -> Option<[f64; 2]> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [x, y] = INSTANCE_X_AXIS_FIELDS.map(float);
Some([x?, y?])
}
pub const TYPE_PARAMETER_FIELD_PREFIX: &str = "m_type_parameter:";
pub const TYPE_MATERIAL_FIELD: &str = "m_type_material";
pub const TYPE_WITHOUT_GEOMETRY_FIELD: &str = "m_type_without_geometry";
fn attach_type_materials(materials: &BTreeMap<u32, Vec<String>>, elements: &mut [DecodedElement]) {
for element in elements.iter_mut() {
let type_id = element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
});
let Some(names) = type_id.and_then(|id| materials.get(&id)) else {
continue;
};
if names.is_empty() {
element.fields.push((
TYPE_WITHOUT_GEOMETRY_FIELD.into(),
InstanceField::Bool(true),
));
}
for name in names {
element.fields.push((
TYPE_MATERIAL_FIELD.into(),
InstanceField::String(name.clone()),
));
}
}
}
fn attach_type_text_parameters(
parameters: &BTreeMap<u32, BTreeMap<&'static str, String>>,
elements: &mut [DecodedElement],
) {
for element in elements.iter_mut() {
let type_id = element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
});
let Some(values) = type_id.and_then(|id| parameters.get(&id)) else {
continue;
};
for (name, value) in values {
element.fields.push((
format!("{TYPE_PARAMETER_FIELD_PREFIX}{name}"),
InstanceField::String(value.clone()),
));
}
}
}
fn attach_family_and_type_names(rf: &mut RevitFile, elements: &mut [DecodedElement]) {
let names = rf.element_names();
if names.entries.is_empty() {
return;
}
for element in elements.iter_mut() {
let Some(own) = element.id else {
continue;
};
let Some((references, category)) = record_references(rf, element) else {
continue;
};
let Some(type_id) =
crate::partition_names::resolve_type(&names, &references, category, own)
else {
continue;
};
let Some(family_id) = crate::partition_names::resolve_family(&names, type_id) else {
continue;
};
let (Some(type_entry), Some(family_entry)) =
(names.entries.get(&type_id), names.entries.get(&family_id))
else {
continue;
};
element.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: type_id,
},
));
element.fields.push((
TYPE_NAME_FIELD.into(),
InstanceField::String(type_entry.name.clone()),
));
element.fields.push((
FAMILY_NAME_FIELD.into(),
InstanceField::String(family_entry.name.clone()),
));
}
}
pub const AGGREGATE_WHOLE_FIELD: &str = "m_aggregate_whole";
pub const AGGREGATE_WHOLE_CLASSES: &[&str] = &["Stair", CURTAIN_WALL_CLASS];
pub const CURTAIN_WALL_CLASS: &str = "CurtainWall";
pub const CURTAIN_WALL_PART_CLASSES: &[&str] = &["CurtainWallPanel", "CurtainWallMullion"];
pub const STAIR_PART_CLASSES: &[&str] = &["StairsRun", "StairsLanding", "StairsStringer"];
fn record_references(rf: &mut RevitFile, element: &DecodedElement) -> Option<(Vec<u64>, i64)> {
let mut stream = None;
let mut offset = None;
let mut category = None;
for (name, value) in &element.fields {
match (name.as_str(), value) {
("m_source_stream", InstanceField::String(v)) => stream = Some(v.clone()),
("m_source_offset", InstanceField::Integer { value, .. }) => {
offset = usize::try_from(*value).ok();
}
("m_builtinCategory", InstanceField::Integer { value, .. }) => {
category = Some(*value);
}
_ => {}
}
}
let inflated = rf.inflated_partition(&stream?).ok()?;
let references = offset?
.checked_add(crate::partition_element_records::REFERENCE_LIST_OFFSET)
.and_then(|at| {
crate::partition_element_records::decode_reference_list(inflated.bytes(), at)
})
.unwrap_or_default();
Some((references, category?))
}
fn attach_aggregate_wholes(rf: &mut RevitFile, products: &mut [DecodedElement]) {
let stairs: BTreeSet<u32> = products
.iter()
.filter(|element| AGGREGATE_WHOLE_CLASSES.contains(&element.class.as_str()))
.filter_map(|element| element.id)
.collect();
if stairs.is_empty() {
return;
}
for element in products.iter_mut() {
if !STAIR_PART_CLASSES.contains(&element.class.as_str()) {
continue;
}
let Some((references, _)) = record_references(rf, element) else {
continue;
};
let named: BTreeSet<u32> = references
.iter()
.filter_map(|&id| u32::try_from(id).ok())
.filter(|id| stairs.contains(id) && Some(*id) != element.id)
.collect();
if named.len() == 1 {
let whole = *named.iter().next().expect("one stair");
element.fields.push((
AGGREGATE_WHOLE_FIELD.into(),
InstanceField::ElementId { tag: 0, id: whole },
));
}
}
}
pub const ELEMENT_NAME_FIELD: &str = "m_element_name";
pub const TYPE_KIND_FAMILY_SOURCE: &str = "system_family_by_type_kind";
fn stair_part_word(class: &str) -> Option<&'static str> {
match class {
"StairsRun" => Some("Run"),
"StairsLanding" => Some("Landing"),
"StairsStringer" => Some("Stringer"),
_ => None,
}
}
fn attach_stair_names(rf: &mut RevitFile, revit_version: u32, products: &mut [DecodedElement]) {
use crate::partition_element_records as per;
use crate::partition_stairs as ps;
use crate::partition_type_records as ptr;
use std::collections::BTreeMap;
if !ps::supports_revit_version(revit_version) || !ptr::supports_revit_version(revit_version) {
return;
}
let has = |element: &DecodedElement, field: &str| {
element.fields.iter().any(|(name, _)| name == field)
};
let id_field = |element: &DecodedElement, field: &str| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == field => Some(*id),
_ => None,
})
};
if !products.iter().any(|element| element.class == "Stair") {
return;
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let type_ids = |rf: &mut RevitFile, category: i64| {
ptr::type_definition_ids(
&ptr::scan_type_records(rf, revit_version, category, &declared).unwrap_or_default(),
)
};
let named = |references: &[u64], set: &BTreeSet<u32>| -> BTreeSet<u32> {
references
.iter()
.filter_map(|&slot| u32::try_from(slot).ok())
.filter(|id| set.contains(id))
.collect()
};
let stair_type_ids = type_ids(rf, per::OST_STAIRS);
let mut stair_type_of: BTreeMap<u32, u32> = BTreeMap::new();
for element in products.iter() {
let (Some(id), "Stair") = (element.id, element.class.as_str()) else {
continue;
};
if let Some((references, _)) = record_references(rf, element) {
if let Some(type_id) = ptr::unique_type_reference(&references, &stair_type_ids) {
stair_type_of.insert(id, type_id);
}
}
}
let stair_types = ps::scan_component_types(
rf,
revit_version,
ps::ComponentKind::Stair,
&stair_type_of.values().copied().collect(),
)
.unwrap_or_default();
let stair_ids: BTreeSet<u32> = products
.iter()
.filter(|element| element.class == "Stair")
.filter_map(|element| element.id)
.collect();
let components_of = |stair: u32| {
stair_type_of
.get(&stair)
.and_then(|type_id| stair_types.get(type_id))
.map(|found| &found.components)
};
let part_kinds = [
("StairsRun", per::OST_STAIRS_RUNS, None),
(
"StairsLanding",
per::OST_STAIRS_LANDINGS,
Some(ps::ComponentKind::Landing),
),
(
"StairsStringer",
per::OST_STAIRS_STRINGER_CARRIAGE,
Some(ps::ComponentKind::Support),
),
];
let mut part_type: BTreeMap<usize, u32> = BTreeMap::new();
let mut joins: Vec<(usize, u32)> = Vec::new();
for (class, category, _) in part_kinds {
let own_types = type_ids(rf, category);
for (index, element) in products.iter().enumerate() {
if element.class != class {
continue;
}
let Some((references, _)) = record_references(rf, element) else {
continue;
};
let candidates = match id_field(element, TYPE_ID_FIELD) {
Some(id) => BTreeSet::from([id]),
None => named(&references, &own_types),
};
let stairs: Vec<u32> = match id_field(element, AGGREGATE_WHOLE_FIELD) {
Some(stair) => vec![stair],
None => named(&references, &stair_ids)
.into_iter()
.filter(|id| Some(*id) != element.id)
.collect(),
};
let pairs: Vec<(u32, u32)> = stairs
.iter()
.flat_map(|&stair| candidates.iter().map(move |&type_id| (stair, type_id)))
.filter(|(stair, type_id)| {
components_of(*stair).is_some_and(|listed| listed.contains(type_id))
})
.collect();
let chosen_type = match (candidates.len(), pairs.as_slice()) {
(1, _) => candidates.iter().next().copied(),
(_, [(_, type_id)]) => Some(*type_id),
_ => None,
};
if let Some(type_id) = chosen_type {
part_type.insert(index, type_id);
}
if let ([_, _, ..], [(stair, _)]) = (stairs.as_slice(), pairs.as_slice()) {
joins.push((index, *stair));
}
}
}
for (index, stair) in joins {
products[index].fields.push((
AGGREGATE_WHOLE_FIELD.into(),
InstanceField::ElementId { tag: 0, id: stair },
));
}
let mut found: BTreeMap<usize, (Option<&'static str>, Option<String>)> = BTreeMap::new();
for (index, element) in products.iter().enumerate() {
if let (Some(id), "Stair") = (element.id, element.class.as_str()) {
if let Some(stair_type) = stair_type_of.get(&id).and_then(|t| stair_types.get(t)) {
found.insert(index, (stair_type.family, Some(stair_type.name.clone())));
}
}
}
for (class, _, kind) in part_kinds {
let ids: BTreeSet<u32> = part_type
.iter()
.filter(|(index, _)| products[**index].class == class)
.map(|(_, id)| *id)
.collect();
let of_class = part_type
.iter()
.filter(|(index, _)| products[**index].class == class);
match kind {
Some(kind) => {
let types =
ps::scan_component_types(rf, revit_version, kind, &ids).unwrap_or_default();
for (&index, type_id) in of_class {
if let Some(read) = types.get(type_id) {
found.insert(index, (read.family, Some(read.name.clone())));
}
}
}
None => {
let types = ps::scan_run_types(rf, revit_version, &ids).unwrap_or_default();
for (&index, type_id) in of_class {
if let Some(read) = types.get(type_id) {
let family = if read.monolithic {
"Monolithic Run"
} else {
"Non-Monolithic Run"
};
found.insert(index, (Some(family), read.name.clone()));
}
}
}
}
}
for (index, (family, name)) in found {
let element = &mut products[index];
if has(element, FAMILY_NAME_FIELD) {
continue;
}
if let (Some(name), false) = (&name, has(element, TYPE_NAME_FIELD)) {
let type_id = part_type
.get(&index)
.copied()
.or_else(|| element.id.and_then(|id| stair_type_of.get(&id).copied()));
if let Some(type_id) = type_id {
element.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: type_id,
},
));
}
element
.fields
.push((TYPE_NAME_FIELD.into(), InstanceField::String(name.clone())));
}
if let (Some(family), true) = (family, has(element, TYPE_NAME_FIELD)) {
element.fields.push((
FAMILY_NAME_FIELD.into(),
InstanceField::String(family.into()),
));
element.fields.push((
FAMILY_NAME_SOURCE_FIELD.into(),
InstanceField::String(TYPE_KIND_FAMILY_SOURCE.into()),
));
}
}
let mut stair_names: BTreeMap<u32, String> = BTreeMap::new();
for element in products.iter_mut() {
let (Some(id), "Stair") = (element.id, element.class.as_str()) else {
continue;
};
let Some(family) = stair_type_of
.get(&id)
.and_then(|type_id| stair_types.get(type_id))
.and_then(|stair_type| stair_type.family)
else {
continue;
};
let name = format!("{family}:Stair:{id}");
element.fields.push((
ELEMENT_NAME_FIELD.into(),
InstanceField::String(name.clone()),
));
stair_names.insert(id, name);
}
if stair_names.is_empty() {
return;
}
let Some(marker) = per::bbox_marker(revit_version) else {
return;
};
let assigned = rf.second_prologue_ids();
let none = BTreeMap::new();
let mut members: BTreeMap<(u32, &'static str), BTreeSet<u32>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let ids = assigned.get(&stream).unwrap_or(&none);
let categories: Vec<i64> = part_kinds
.iter()
.map(|(_, category, _)| *category)
.collect();
let found = per::find_categories_records_assigned(
&stream,
inflated.bytes(),
&categories,
&declared,
&marker,
ids,
);
for ((class, _, _), records) in part_kinds.iter().zip(found) {
let Some(word) = stair_part_word(class) else {
continue;
};
for record in records {
for stair in named(&record.references, &stair_ids) {
if stair_names.contains_key(&stair) {
members
.entry((stair, word))
.or_default()
.insert(record.element_id);
}
}
}
}
}
for element in products.iter_mut() {
let (Some(id), Some(word)) = (element.id, stair_part_word(&element.class)) else {
continue;
};
let Some(stair) = id_field(element, AGGREGATE_WHOLE_FIELD) else {
continue;
};
let (Some(stair_name), Some(ids)) = (stair_names.get(&stair), members.get(&(stair, word)))
else {
continue;
};
let Some(rank) = ids.iter().position(|member| *member == id) else {
continue;
};
element.fields.push((
ELEMENT_NAME_FIELD.into(),
InstanceField::String(format!("{stair_name} {word} {}", rank + 1)),
));
}
}
pub const STAIR_RISER_COUNT_FIELD: &str = "m_stair_riser_count";
pub const STAIR_RISER_HEIGHT_FIELD: &str = "m_stair_riser_height";
pub const STAIR_TREAD_DEPTH_FIELD: &str = "m_stair_tread_depth";
fn attach_stair_dimensions(
rf: &mut RevitFile,
revit_version: u32,
products: &mut [DecodedElement],
) {
use crate::partition_stairs as ps;
if !ps::supports_revit_version(revit_version) {
return;
}
let stairs: Vec<u32> = products
.iter()
.filter(|element| element.class == "Stair")
.filter_map(|element| element.id)
.collect();
let whole_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == AGGREGATE_WHOLE_FIELD => Some(*id),
_ => None,
})
};
let runs: Vec<(u32, u32)> = products
.iter()
.filter(|element| element.class == "StairsRun")
.filter_map(|element| Some((element.id?, whole_of(element)?)))
.filter(|(_, stair)| stairs.contains(stair))
.collect();
if stairs.is_empty() {
return;
}
let run_ids: Vec<u32> = runs.iter().map(|(run, _)| *run).collect();
let Ok((dimensions, run_counts)) =
ps::scan_stair_dimensions(rf, revit_version, &stairs, &run_ids)
else {
return;
};
let mut flight_counts: std::collections::BTreeMap<u32, u32> = std::collections::BTreeMap::new();
for (stair, found) in &dimensions {
let of_stair: Vec<(u32, Option<u32>)> = runs
.iter()
.filter(|(_, whole)| whole == stair)
.map(|(run, _)| (*run, run_counts.get(run).copied()))
.collect();
flight_counts.extend(ps::flight_riser_counts(found.riser_count, &of_stair));
}
let push = |element: &mut DecodedElement, found: &ps::StairDimensions, risers: Option<u32>| {
if let Some(risers) = risers {
element.fields.push((
STAIR_RISER_COUNT_FIELD.into(),
InstanceField::Integer {
value: i64::from(risers),
signed: false,
size: 4,
},
));
}
element.fields.push((
STAIR_RISER_HEIGHT_FIELD.into(),
InstanceField::Float {
value: found.riser_height_feet,
size: 8,
},
));
element.fields.push((
STAIR_TREAD_DEPTH_FIELD.into(),
InstanceField::Float {
value: found.tread_depth_feet,
size: 8,
},
));
};
for element in products.iter_mut() {
let Some(id) = element.id else {
continue;
};
if element.class == "Stair" {
if let Some(found) = dimensions.get(&id) {
push(element, found, Some(found.riser_count));
}
} else if element.class == "StairsRun" {
let Some(found) = whole_of(element).and_then(|stair| dimensions.get(&stair)) else {
continue;
};
let found = *found;
push(element, &found, flight_counts.get(&id).copied());
}
}
}
pub const WALL_EXTERIOR_FIELDS: [&str; 2] = ["m_wall_exterior_x", "m_wall_exterior_y"];
pub const WALL_LAYERS_FIELD: &str = "m_wall_layers";
pub const WALL_AXIS_START_FIELDS: [&str; 2] = ["m_wall_axis_start_x", "m_wall_axis_start_y"];
pub const WALL_AXIS_END_FIELDS: [&str; 2] = ["m_wall_axis_end_x", "m_wall_axis_end_y"];
pub const WALL_ARC_FIELDS: [&str; 9] = [
"m_wall_arc_centre_x",
"m_wall_arc_centre_y",
"m_wall_arc_radius",
"m_wall_arc_start_angle",
"m_wall_arc_end_angle",
"m_wall_arc_x_axis_x",
"m_wall_arc_x_axis_y",
"m_wall_arc_y_axis_x",
"m_wall_arc_y_axis_y",
];
pub const WALL_TYPE_THICKNESS_FIELD: &str = "m_wall_type_thickness";
pub const WALL_EXTERIOR_FACE_ANGLE_FIELD: &str = "m_wall_exterior_face_angle";
pub const WALL_JOIN_REACH_FIELDS: [&str; 2] = ["m_wall_join_start_reach", "m_wall_join_end_reach"];
pub const WALL_JOIN_PARTNER_FIELDS: [&str; 2] = ["m_wall_join_start_wall", "m_wall_join_end_wall"];
pub const WALL_JOIN_LAYER_REACH_FIELDS: [&str; 2] = [
"m_wall_join_start_layer_reaches",
"m_wall_join_end_layer_reaches",
];
pub const WALL_JOIN_LAYER_EDGE_REACH_FIELDS: [&str; 2] = [
"m_wall_join_start_layer_edge_reaches",
"m_wall_join_end_layer_edge_reaches",
];
pub const WALL_JOIN_THROUGH_FIELDS: [&str; 2] =
["m_wall_join_start_through", "m_wall_join_end_through"];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WallAxis {
pub start: [f64; 2],
pub end: [f64; 2],
pub thickness_feet: f64,
pub join_reach_feet: [Option<f64>; 2],
}
pub fn wall_axis_from_fields(fields: &[(String, InstanceField)]) -> Option<WallAxis> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [sx, sy] = WALL_AXIS_START_FIELDS.map(float);
let [ex, ey] = WALL_AXIS_END_FIELDS.map(float);
Some(WallAxis {
start: [sx?, sy?],
end: [ex?, ey?],
thickness_feet: float(WALL_TYPE_THICKNESS_FIELD)?,
join_reach_feet: WALL_JOIN_REACH_FIELDS.map(float),
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WallArc {
pub centre: [f64; 2],
pub radius: f64,
pub angles: [f64; 2],
pub x_axis: [f64; 2],
pub y_axis: [f64; 2],
pub thickness_feet: f64,
}
impl WallArc {
pub fn point(&self, angle: f64, offset: f64) -> [f64; 2] {
let (sin, cos) = angle.sin_cos();
let radius = self.radius + offset;
[0, 1].map(|axis| {
self.centre[axis] + radius * (cos * self.x_axis[axis] + sin * self.y_axis[axis])
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WallTaper {
pub angle_radians: f64,
pub exterior: [f64; 2],
}
pub fn wall_is_tapered(fields: &[(String, InstanceField)]) -> bool {
fields
.iter()
.any(|(name, _)| name == WALL_EXTERIOR_FACE_ANGLE_FIELD)
}
pub fn wall_taper_from_fields(fields: &[(String, InstanceField)]) -> Option<WallTaper> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [x, y] = WALL_EXTERIOR_FIELDS.map(float);
Some(WallTaper {
angle_radians: float(WALL_EXTERIOR_FACE_ANGLE_FIELD)?,
exterior: [x?, y?],
})
}
pub fn wall_arc_from_fields(fields: &[(String, InstanceField)]) -> Option<WallArc> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [cx, cy, radius, start, end, xx, xy, yx, yy] = WALL_ARC_FIELDS.map(float);
Some(WallArc {
centre: [cx?, cy?],
radius: radius?,
angles: [start?, end?],
x_axis: [xx?, xy?],
y_axis: [yx?, yy?],
thickness_feet: float(WALL_TYPE_THICKNESS_FIELD)?,
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct WallLayerEnds {
pub exterior: [f64; 2],
pub widths: Vec<f64>,
pub reach_feet: [Option<Vec<[f64; 2]>>; 2],
}
pub fn wall_layer_ends_from_fields(fields: &[(String, InstanceField)]) -> Option<WallLayerEnds> {
let floats = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Vector(items) if name == wanted => items
.iter()
.map(|item| match item {
InstanceField::Float { value, .. } => Some(*value),
_ => None,
})
.collect::<Option<Vec<f64>>>(),
_ => None,
})
};
let square = WALL_JOIN_LAYER_REACH_FIELDS.map(floats);
let slanted = WALL_JOIN_LAYER_EDGE_REACH_FIELDS.map(floats);
if square.iter().chain(&slanted).all(Option::is_none) {
return None;
}
let widths: Vec<f64> = fields
.iter()
.find_map(|(name, value)| {
(name == WALL_LAYERS_FIELD).then(|| layer_bands_from_field(value))
})
.flatten()?
.iter()
.map(|band| band.width_feet)
.collect();
if square
.iter()
.flatten()
.any(|reach| reach.len() != widths.len())
|| slanted
.iter()
.flatten()
.any(|reach| reach.len() != 2 * widths.len())
{
return None;
}
let [start, end] = [0, 1].map(|slot| match (&slanted[slot], &square[slot]) {
(Some(edges), _) => Some(edges.chunks(2).map(|pair| [pair[0], pair[1]]).collect()),
(None, Some(reaches)) => Some(reaches.iter().map(|reach| [*reach, *reach]).collect()),
(None, None) => None,
});
let reach_feet = [start, end];
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [Some(ex), Some(ey)] = WALL_EXTERIOR_FIELDS.map(float) else {
return None;
};
Some(WallLayerEnds {
exterior: [ex, ey],
widths,
reach_feet,
})
}
pub const SLAB_LAYERS_FIELD: &str = "m_slab_layers";
pub fn unplaced_wall_layers_from_fields(
fields: &[(String, InstanceField)],
) -> Option<Vec<crate::ifc::LayerBand>> {
if fields
.iter()
.any(|(name, _)| WALL_EXTERIOR_FIELDS.contains(&name.as_str()))
{
return None;
}
fields
.iter()
.find_map(|(name, value)| (name == WALL_LAYERS_FIELD).then_some(value))
.and_then(layer_bands_from_field)
}
fn layer_bands_from_field(value: &InstanceField) -> Option<Vec<crate::ifc::LayerBand>> {
let InstanceField::Vector(items) = value else {
return None;
};
let bands = items
.iter()
.map(|item| match item {
InstanceField::Vector(parts) => match parts.as_slice() {
[
InstanceField::Float { value: width, .. },
InstanceField::Integer { value: colour, .. },
InstanceField::Float {
value: transparency,
..
},
rest @ ..,
] => Some(crate::ifc::LayerBand {
width_feet: *width,
color_packed: u32::try_from(*colour).ok(),
transparency: *transparency,
name: match rest {
[InstanceField::String(name)] => Some(name.clone()),
_ => None,
},
}),
_ => None,
},
_ => None,
})
.collect::<Option<Vec<_>>>()?;
(!bands.is_empty()).then_some(bands)
}
fn layer_bands_field(
layers: &[crate::partition_compound_structure::CompoundLayer],
appearances: &std::collections::BTreeMap<u32, crate::partition_materials::MaterialAppearance>,
names: &std::collections::BTreeMap<u32, String>,
) -> InstanceField {
InstanceField::Vector(
layers
.iter()
.filter(|layer| layer.width_feet > 0.0)
.map(|layer| {
let appearance = layer.material.and_then(|m| appearances.get(&m));
let mut band = vec![
InstanceField::Float {
value: layer.width_feet,
size: 8,
},
InstanceField::Integer {
value: appearance.map_or(-1, |a| i64::from(a.color_packed())),
signed: true,
size: 8,
},
InstanceField::Float {
value: appearance.map_or(0.0, |a| f64::from(a.transparency)),
size: 8,
},
];
if let Some(name) = layer.material.and_then(|m| names.get(&m)) {
band.push(InstanceField::String(name.clone()));
}
InstanceField::Vector(band)
})
.collect(),
)
}
pub fn element_layers_from_fields(
fields: &[(String, InstanceField)],
) -> Option<crate::ifc::ElementLayers> {
let field = |wanted: &str| {
fields
.iter()
.find_map(|(name, value)| (name == wanted).then_some(value))
};
if let Some(layers) = field(SLAB_LAYERS_FIELD).and_then(layer_bands_from_field) {
return Some(crate::ifc::ElementLayers {
exterior_normal: [0.0, 0.0],
layers,
stacked: true,
system_family: None,
});
}
let float = |wanted: &str| match field(wanted) {
Some(InstanceField::Float { value, .. }) => Some(*value),
_ => None,
};
let [x, y] = WALL_EXTERIOR_FIELDS.map(float);
let layers = field(WALL_LAYERS_FIELD).and_then(layer_bands_from_field)?;
Some(crate::ifc::ElementLayers {
exterior_normal: [x?, y?],
layers,
stacked: false,
system_family: None,
})
}
pub fn system_family(class: &str, has_layers: bool) -> Option<&'static str> {
match (class, has_layers) {
(CURTAIN_WALL_CLASS, _) => Some("Curtain Wall"),
("Railing", _) => Some("Railing"),
("Floor", _) => Some("Floor"),
("BuildingPad", _) => Some("Pad"),
("SlabEdge", _) => Some("Slab Edge"),
("Ramp", _) => Some("Ramp"),
("Wall" | "CurtainWallPanel", true) => Some("Basic Wall"),
("Ceiling", true) => Some("Compound Ceiling"),
("Roof", true) => Some("Basic Roof"),
_ => None,
}
}
pub fn localized_system_family(family: &'static str, locale: Option<&str>) -> &'static str {
match (locale, family) {
(Some("PTB"), "Basic Wall") => "Parede básica",
(Some("PTB"), "Floor") => "Piso",
(Some("PTB"), "Basic Roof") => "Telhado básico",
_ => family,
}
}
pub const FAMILY_NAME_SOURCE_FIELD: &str = "m_family_name_source";
pub const SYSTEM_FAMILY_SOURCE: &str = "system_family_by_category";
fn attach_system_family_names(
rf: &mut RevitFile,
revit_version: u32,
elements: &mut [&mut Vec<DecodedElement>],
) {
use crate::partition_compound_structure as pcs;
let type_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
};
let shows_layers = |element: &DecodedElement| {
element.fields.iter().any(|(name, _)| {
name == WALL_LAYERS_FIELD
|| name == SLAB_LAYERS_FIELD
|| name == ROOF_TYPE_THICKNESS_FIELD
})
};
let mut layered: BTreeSet<u32> = elements
.iter()
.flat_map(|list| list.iter())
.filter(|element| shows_layers(element))
.filter_map(type_of)
.collect();
let pending: BTreeSet<u32> = elements
.iter()
.flat_map(|list| list.iter())
.filter(|element| {
matches!(
element.class.as_str(),
"Wall" | "Ceiling" | "Roof" | "CurtainWallPanel"
)
})
.filter_map(type_of)
.filter(|id| !layered.contains(id))
.collect();
if !pending.is_empty() && pcs::layer_layout(revit_version).is_some() {
if let Ok(records) = crate::elem_table::parse_records(rf) {
let declared = crate::elem_table::declared_ids(&records);
let materials: BTreeSet<u32> =
if revit_version == crate::partition_element_records_2023::REVIT_2023 {
declared.clone()
} else {
crate::partition_type_records::scan_type_records(
rf,
revit_version,
crate::partition_type_records::OST_MATERIALS,
&declared,
)
.unwrap_or_default()
.iter()
.map(|record| record.element_id)
.collect()
};
if let Ok(types) =
pcs::scan_type_layers(rf, revit_version, &pending, &materials, &declared)
{
layered.extend(
types
.into_iter()
.filter(|(_, layers)| !layers.is_empty())
.map(|(id, _)| id),
);
}
}
}
let locale = rf.basic_file_info().ok().and_then(|info| info.locale);
for element in elements.iter_mut().flat_map(|list| list.iter_mut()) {
let has = |field: &str| element.fields.iter().any(|(name, _)| name == field);
if !has(TYPE_NAME_FIELD) || has(FAMILY_NAME_FIELD) {
continue;
}
let has_layers = type_of(element).is_some_and(|id| layered.contains(&id));
let Some(family) = system_family(&element.class, has_layers) else {
continue;
};
element.fields.push((
FAMILY_NAME_FIELD.into(),
InstanceField::String(localized_system_family(family, locale.as_deref()).into()),
));
element.fields.push((
FAMILY_NAME_SOURCE_FIELD.into(),
InstanceField::String(SYSTEM_FAMILY_SOURCE.into()),
));
}
}
fn attach_wall_layers(rf: &mut RevitFile, revit_version: u32, walls: &mut [DecodedElement]) {
use crate::partition_compound_structure as pcs;
if !pcs::WALL_LINE_SUPPORTED_REVIT_VERSIONS.contains(&revit_version)
|| pcs::layer_layout(revit_version).is_none()
{
return;
}
let centreline_bodies = revit_version != crate::partition_element_records_2023::REVIT_2023;
let type_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
};
let wall_ids: BTreeSet<u32> = walls
.iter()
.filter(|wall| type_of(wall).is_some())
.filter_map(|wall| wall.id)
.collect();
if wall_ids.is_empty() {
return;
}
let types: BTreeSet<u32> = walls.iter().filter_map(type_of).collect();
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let Some((materials, names)) = materials_and_names(rf, revit_version, &declared) else {
return;
};
let (Ok(layers), Ok(orientations), Ok(lines), Ok(arcs), Ok(appearances), Ok(face_angles)) = (
pcs::scan_type_layers(rf, revit_version, &types, &materials, &declared),
pcs::scan_wall_orientations(rf, revit_version, &wall_ids),
pcs::scan_wall_lines(rf, revit_version, &wall_ids),
pcs::scan_wall_arcs(rf, revit_version, &wall_ids),
crate::partition_materials::scan_material_appearances(rf, revit_version, &declared),
pcs::scan_wall_type_face_angles(rf, revit_version, &types),
) else {
return;
};
let category_material = crate::partition_materials::scan_category_material(
rf,
revit_version,
crate::partition_element_records::OST_WALLS,
)
.ok()
.flatten()
.filter(|id| names.contains_key(id));
let layers: std::collections::BTreeMap<u32, Vec<pcs::CompoundLayer>> = layers
.into_iter()
.map(|(type_id, type_layers)| {
let type_layers = type_layers
.into_iter()
.map(|layer| pcs::CompoundLayer {
material: layer.material.or(category_material),
..layer
})
.collect();
(type_id, type_layers)
})
.collect();
for wall in walls.iter_mut() {
let (Some(id), Some(type_id)) = (wall.id, type_of(wall)) else {
continue;
};
let (Some(type_layers), Some(orientation)) = (layers.get(&type_id), orientations.get(&id))
else {
continue;
};
let thickness: f64 = type_layers.iter().map(|layer| layer.width_feet).sum();
let measured = thickness.is_finite() && thickness > 0.0;
let taper = face_angles
.get(&type_id)
.filter(|_| orientation.word == 2 && lines.contains_key(&id))
.and_then(|&[first, exterior, third]| {
(first == 0.0 && third == 0.0 && exterior > 0.0 && exterior < FRAC_PI_4)
.then_some(exterior)
});
let centred = centreline_bodies && measured && (orientation.word == 1 || taper.is_some());
let float = |value: f64| InstanceField::Float { value, size: 8 };
let (dx, dy) = if let Some(line) = lines.get(&id) {
let (start, end) = (line.start(), line.end());
let (dx, dy) = (end[0] - start[0], end[1] - start[1]);
let length = dx.hypot(dy);
if !length.is_finite() || length <= 1e-9 {
continue;
}
if centred {
for (names, point) in [(WALL_AXIS_START_FIELDS, start), (WALL_AXIS_END_FIELDS, end)]
{
for (name, value) in names.iter().zip(point) {
wall.fields.push(((*name).into(), float(value)));
}
}
}
(dx / length, dy / length)
} else if let Some(arc) = arcs.get(&id) {
if arc.x_axis[2].abs() > 1e-9 || arc.y_axis[2].abs() > 1e-9 {
continue;
}
if centred {
let values = [
arc.centre[0],
arc.centre[1],
arc.radius,
arc.start_angle,
arc.end_angle,
arc.x_axis[0],
arc.x_axis[1],
arc.y_axis[0],
arc.y_axis[1],
];
for (name, value) in WALL_ARC_FIELDS.iter().zip(values) {
wall.fields.push(((*name).into(), float(value)));
}
}
let (sin, cos) = ((arc.start_angle + arc.end_angle) * 0.5).sin_cos();
let tangent = [
-sin * arc.x_axis[0] + cos * arc.y_axis[0],
-sin * arc.x_axis[1] + cos * arc.y_axis[1],
];
let length = tangent[0].hypot(tangent[1]);
if !length.is_finite() || length <= 1e-9 {
continue;
}
(tangent[0] / length, tangent[1] / length)
} else {
continue;
};
if centred {
wall.fields
.push((WALL_TYPE_THICKNESS_FIELD.into(), float(thickness)));
if let Some(angle) = taper {
wall.fields
.push((WALL_EXTERIOR_FACE_ANGLE_FIELD.into(), float(angle)));
}
}
let exterior = if orientation.flip {
[dy, -dx]
} else {
[-dy, dx]
};
let bands = layer_bands_field(type_layers, &appearances, &names);
if matches!(&bands, InstanceField::Vector(items) if items.is_empty()) {
continue;
}
for (name, value) in WALL_EXTERIOR_FIELDS.iter().zip(exterior) {
wall.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
wall.fields.push((WALL_LAYERS_FIELD.into(), bands));
}
for wall in walls.iter_mut() {
if wall
.fields
.iter()
.any(|(name, _)| name == WALL_LAYERS_FIELD)
{
continue;
}
let Some(type_layers) = type_of(wall).and_then(|type_id| layers.get(&type_id)) else {
continue;
};
let bands = layer_bands_field(type_layers, &appearances, &names);
if matches!(&bands, InstanceField::Vector(items) if items.is_empty()) {
continue;
}
wall.fields.push((WALL_LAYERS_FIELD.into(), bands));
}
if centreline_bodies {
attach_wall_butt_joins(rf, revit_version, walls, &layers);
}
}
fn attach_wall_butt_joins(
rf: &mut RevitFile,
revit_version: u32,
walls: &mut [DecodedElement],
type_layers: &std::collections::BTreeMap<
u32,
Vec<crate::partition_compound_structure::CompoundLayer>,
>,
) {
use crate::element_record_wall_joins as joins;
let float = |wall: &DecodedElement, wanted: &str| {
wall.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let lines: Vec<joins::WallLine> = walls
.iter()
.filter_map(|wall| {
let axis = wall_axis_from_fields(&wall.fields)?;
let base = float(wall, "m_locationZ")?;
let type_id = wall.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})?;
let layers: Vec<(f64, u32)> = type_layers
.get(&type_id)?
.iter()
.filter(|layer| layer.width_feet > 0.0)
.map(|layer| (layer.width_feet, layer.function))
.collect();
let [Some(ex), Some(ey)] = WALL_EXTERIOR_FIELDS.map(|name| float(wall, name)) else {
return None;
};
Some(joins::WallLine {
element_id: wall.id?,
start: axis.start,
end: axis.end,
thickness_feet: axis.thickness_feet,
layers,
exterior: [ex, ey],
base_feet: base,
top_feet: base + float(wall, "m_bboxHeight")?,
})
})
.collect();
if lines.len() < 2 {
return;
}
let every: BTreeSet<u32> = walls.iter().filter_map(|wall| wall.id).collect();
let read: BTreeSet<u32> = lines.iter().map(|line| line.element_id).collect();
let Ok(partners) = crate::partition_compound_structure::scan_wall_join_partners(
rf,
revit_version,
&every,
&read,
) else {
return;
};
let found = joins::butt_joins(&lines, &partners);
for wall in walls.iter_mut() {
let Some(ends) = wall.id.and_then(|id| found.get(&id)) else {
continue;
};
for (slot, join) in ends.iter().enumerate() {
let Some(join) = join else {
continue;
};
wall.fields.push((
WALL_JOIN_PARTNER_FIELDS[slot].into(),
InstanceField::ElementId {
tag: 0,
id: join.partner,
},
));
wall.fields.push((
WALL_JOIN_THROUGH_FIELDS[slot].into(),
InstanceField::Bool(join.runs_through),
));
if let Some(reach) = join.reach_feet {
wall.fields.push((
WALL_JOIN_REACH_FIELDS[slot].into(),
InstanceField::Float {
value: reach,
size: 8,
},
));
}
if let Some(edges) = &join.layer_edge_reach_feet {
wall.fields.push((
WALL_JOIN_LAYER_EDGE_REACH_FIELDS[slot].into(),
InstanceField::Vector(
edges
.iter()
.flatten()
.map(|value| InstanceField::Float {
value: *value,
size: 8,
})
.collect(),
),
));
}
if let Some(reaches) = &join.layer_reach_feet {
wall.fields.push((
WALL_JOIN_LAYER_REACH_FIELDS[slot].into(),
InstanceField::Vector(
reaches
.iter()
.map(|value| InstanceField::Float {
value: *value,
size: 8,
})
.collect(),
),
));
}
}
}
}
pub const STAIR_RUN_ORIGIN_FIELDS: [&str; 3] = [
"m_stair_run_origin_x",
"m_stair_run_origin_y",
"m_stair_run_origin_z",
];
pub const STAIR_RUN_CLIMB_FIELDS: [&str; 2] = ["m_stair_run_climb_x", "m_stair_run_climb_y"];
pub const STAIR_RUN_ACROSS_FIELDS: [&str; 2] = ["m_stair_run_across_x", "m_stair_run_across_y"];
pub const STAIR_RUN_WIDTH_FIELD: &str = "m_stair_run_width";
pub const STAIR_RUN_PROFILE_FIELD: &str = "m_stair_run_profile";
#[derive(Debug, Clone, PartialEq)]
pub struct StairRunBody {
pub origin: [f64; 3],
pub climb: [f64; 2],
pub across: [f64; 2],
pub width_feet: f64,
pub profile: Vec<[f64; 2]>,
}
pub fn stair_run_body_from_fields(fields: &[(String, InstanceField)]) -> Option<StairRunBody> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let profile = fields.iter().find_map(|(name, value)| match value {
InstanceField::Vector(points) if name == STAIR_RUN_PROFILE_FIELD => points
.iter()
.map(|point| match point {
InstanceField::Vector(pair) => match pair.as_slice() {
[
InstanceField::Float { value: u, .. },
InstanceField::Float { value: z, .. },
] => Some([*u, *z]),
_ => None,
},
_ => None,
})
.collect::<Option<Vec<[f64; 2]>>>(),
_ => None,
})?;
let [ox, oy, oz] = STAIR_RUN_ORIGIN_FIELDS.map(float);
let [cx, cy] = STAIR_RUN_CLIMB_FIELDS.map(float);
let [ax, ay] = STAIR_RUN_ACROSS_FIELDS.map(float);
Some(StairRunBody {
origin: [ox?, oy?, oz?],
climb: [cx?, cy?],
across: [ax?, ay?],
width_feet: float(STAIR_RUN_WIDTH_FIELD)?,
profile: (profile.len() >= 3).then_some(profile)?,
})
}
fn attach_stair_run_bodies(
rf: &mut RevitFile,
revit_version: u32,
products: &mut [DecodedElement],
) {
use crate::partition_stairs as ps;
use crate::partition_type_records as ptr;
if !ps::supports_revit_version(revit_version) {
return;
}
let float = |element: &DecodedElement, wanted: &str| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let integer = |element: &DecodedElement, wanted: &str| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Integer { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let run_indices: Vec<usize> = products
.iter()
.enumerate()
.filter(|(_, element)| element.class == "StairsRun" && element.id.is_some())
.map(|(index, _)| index)
.collect();
if run_indices.is_empty() {
return;
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let type_records = ptr::scan_type_records(
rf,
revit_version,
crate::partition_element_records::OST_STAIRS_RUNS,
&declared,
)
.unwrap_or_default();
let run_types = ptr::type_definition_ids(&type_records);
let mut type_of: std::collections::BTreeMap<usize, u32> = std::collections::BTreeMap::new();
for &index in &run_indices {
if let Some((references, _)) = record_references(rf, &products[index]) {
if let Some(type_id) = ptr::unique_type_reference(&references, &run_types) {
type_of.insert(index, type_id);
}
}
}
let run_ids: BTreeSet<u32> = run_indices
.iter()
.filter_map(|&index| products[index].id)
.collect();
let type_ids: BTreeSet<u32> = type_of.values().copied().collect();
let (Ok(lines), Ok(types)) = (
ps::scan_run_lines(rf, revit_version, &run_ids),
ps::scan_run_types(rf, revit_version, &type_ids),
) else {
return;
};
let sketches: std::collections::BTreeMap<u32, ps::RunSketch> = lines
.iter()
.filter_map(|(&id, lines)| Some((id, ps::run_sketch(lines)?)))
.collect();
let ends = ps::scan_run_ends(rf, revit_version, &sketches).unwrap_or_default();
for index in run_indices {
let element = &mut products[index];
let Some(run_type) = type_of.get(&index).and_then(|id| types.get(id)) else {
continue;
};
let has_type_name = element
.fields
.iter()
.any(|(name, _)| name == TYPE_NAME_FIELD);
if let (Some(name), false) = (&run_type.name, has_type_name) {
element.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: type_of[&index],
},
));
element
.fields
.push((TYPE_NAME_FIELD.into(), InstanceField::String(name.clone())));
}
let (Some(riser_height), Some(risers)) = (
float(element, STAIR_RISER_HEIGHT_FIELD),
integer(element, STAIR_RISER_COUNT_FIELD),
) else {
continue;
};
let end_with_riser = element
.id
.and_then(|id| ends.get(&id))
.map(|ends| ends.end_with_riser);
let riser_lines = risers + i64::from(end_with_riser == Some(false));
let Some(sketch) = element
.id
.and_then(|id| sketches.get(&id))
.filter(|sketch| i64::try_from(sketch.risers.len()) == Ok(riser_lines))
else {
continue;
};
let Some(profile) = ps::run_side_profile(sketch, run_type, riser_height, end_with_riser)
else {
continue;
};
let scalar = |value: f64| InstanceField::Float { value, size: 8 };
for (name, value) in STAIR_RUN_ORIGIN_FIELDS.iter().zip(sketch.origin) {
element.fields.push(((*name).into(), scalar(value)));
}
for (name, value) in STAIR_RUN_CLIMB_FIELDS.iter().zip(sketch.climb) {
element.fields.push(((*name).into(), scalar(value)));
}
for (name, value) in STAIR_RUN_ACROSS_FIELDS.iter().zip(sketch.across) {
element.fields.push(((*name).into(), scalar(value)));
}
element
.fields
.push((STAIR_RUN_WIDTH_FIELD.into(), scalar(sketch.width_feet)));
element.fields.push((
STAIR_RUN_PROFILE_FIELD.into(),
InstanceField::Vector(
profile
.iter()
.map(|&[u, z]| InstanceField::Vector(vec![scalar(u), scalar(z)]))
.collect(),
),
));
}
}
pub const BEAM_AXIS_START_FIELDS: [&str; 3] = [
"m_beam_axis_start_x",
"m_beam_axis_start_y",
"m_beam_axis_start_z",
];
pub const BEAM_AXIS_END_FIELDS: [&str; 3] = [
"m_beam_axis_end_x",
"m_beam_axis_end_y",
"m_beam_axis_end_z",
];
pub fn element_record_bbox(element: &DecodedElement) -> Option<[f64; 6]> {
let field = |wanted: &str| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let (x, y, z) = (
field("m_locationX")?,
field("m_locationY")?,
field("m_locationZ")?,
);
let (width, depth, height) = (
field("m_bboxWidth")?,
field("m_bboxDepth")?,
field("m_bboxHeight")?,
);
Some([
x - width / 2.0,
y - depth / 2.0,
z,
x + width / 2.0,
y + depth / 2.0,
z + height,
])
}
pub fn beam_axis_from_fields(fields: &[(String, InstanceField)]) -> Option<([f64; 3], [f64; 3])> {
let field = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let point = |names: [&str; 3]| Some([field(names[0])?, field(names[1])?, field(names[2])?]);
Some((point(BEAM_AXIS_START_FIELDS)?, point(BEAM_AXIS_END_FIELDS)?))
}
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) {
return;
}
let beams: BTreeSet<u32> = products
.iter()
.filter(|element| element.class == "StructuralFraming")
.filter_map(|element| element.id)
.collect();
if beams.is_empty() {
return;
}
let Ok(lines) = pba::scan_bounded_lines(rf, revit_version, &beams) else {
return;
};
for element in products
.iter_mut()
.filter(|element| element.class == "StructuralFraming")
{
let Some(line) = element.id.and_then(|id| lines.get(&id)) else {
continue;
};
let (start, end) = (line.start(), line.end());
let resolves = element_record_bbox(element)
.and_then(|bbox| pba::beam_body(bbox, start, end))
.is_some();
if !resolves {
continue;
}
for (names, point) in [(BEAM_AXIS_START_FIELDS, start), (BEAM_AXIS_END_FIELDS, end)] {
for (name, value) in names.iter().zip(point) {
element
.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
}
}
}
fn attach_curtain_walls(
rf: &mut RevitFile,
revit_version: u32,
walls: &mut [DecodedElement],
products: &mut [DecodedElement],
) {
let wall_ids: BTreeSet<u32> = walls.iter().filter_map(|wall| wall.id).collect();
if wall_ids.is_empty() {
return;
}
let mut references: Vec<Option<Vec<u64>>> = Vec::with_capacity(products.len());
let mut curtain_walls: BTreeSet<u32> = BTreeSet::new();
for product in products.iter() {
if !CURTAIN_WALL_PART_CLASSES.contains(&product.class.as_str()) {
references.push(None);
continue;
}
let list = record_references(rf, product).map(|(list, _)| list);
if product.class == "CurtainWallMullion" {
for id in list.iter().flatten() {
if let Ok(id) = u32::try_from(*id) {
if wall_ids.contains(&id) {
curtain_walls.insert(id);
}
}
}
}
references.push(list);
}
let unrecorded: BTreeSet<u32> = references
.iter()
.flatten()
.flatten()
.filter_map(|&id| u32::try_from(id).ok())
.filter(|id| !wall_ids.contains(id))
.collect();
let grids = scan_curtain_grid_walls(rf, revit_version, &unrecorded, &wall_ids);
curtain_walls.extend(grids.values().copied());
if curtain_walls.is_empty() {
return;
}
for wall in walls.iter_mut() {
if wall.id.is_some_and(|id| curtain_walls.contains(&id)) {
wall.class = CURTAIN_WALL_CLASS.into();
}
}
let wall_boxes: Vec<(u32, [f64; 6])> = walls
.iter()
.filter_map(|wall| Some((wall.id?, element_record_bbox(wall)?)))
.filter(|(id, _)| curtain_walls.contains(id))
.collect();
for (product, list) in products.iter_mut().zip(references) {
let Some(list) = list else {
continue;
};
let named: BTreeSet<u32> = list
.iter()
.filter_map(|&id| u32::try_from(id).ok())
.filter(|id| curtain_walls.contains(id) && Some(*id) != product.id)
.collect();
let on_grid: BTreeSet<u32> = list
.iter()
.filter_map(|&id| u32::try_from(id).ok())
.filter_map(|id| grids.get(&id).copied())
.collect();
let whole = match (on_grid.len(), named.len()) {
(1, _) => on_grid.iter().next().copied(),
(0, 1) => named.iter().next().copied(),
(0, 0) => element_record_bbox(product).and_then(|part| {
let mut inside = wall_boxes
.iter()
.filter(|(_, wall)| box_contains(wall, &part, CURTAIN_PART_BOX_TOLERANCE_FEET))
.map(|(id, _)| *id);
let first = inside.next()?;
inside.next().is_none().then_some(first)
}),
_ => None,
};
if let Some(whole) = whole {
product.fields.push((
AGGREGATE_WHOLE_FIELD.into(),
InstanceField::ElementId { tag: 0, id: whole },
));
}
}
}
pub const CURTAIN_GRID_WALL_OFFSETS: [usize; 2] = [85, 478];
fn scan_curtain_grid_walls(
rf: &mut RevitFile,
revit_version: u32,
candidates: &BTreeSet<u32>,
walls: &BTreeSet<u32>,
) -> BTreeMap<u32, u32> {
let Some(header) = crate::partition_names::element_data_header(revit_version) else {
return BTreeMap::new();
};
let u64_at = |buf: &[u8], at: usize| {
buf.get(at..at.checked_add(8)?)
.map(|b| u64::from_le_bytes(b.try_into().expect("8 bytes")))
};
let mut found: BTreeMap<u32, Option<u32>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for hit in memchr::memmem::find_iter(buf, &header) {
let id_at = hit + header.len();
let Some(id) = u64_at(buf, id_at)
.and_then(|id| u32::try_from(id).ok())
.filter(|id| candidates.contains(id))
else {
continue;
};
let [first, second] =
CURTAIN_GRID_WALL_OFFSETS.map(|offset| u64_at(buf, id_at + 8 + offset));
let wall = match (first, second) {
(Some(first), Some(second)) if first == second => u32::try_from(first)
.ok()
.filter(|wall| walls.contains(wall)),
_ => None,
};
let Some(wall) = wall else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(wall));
}
Some(held) => {
if *held != Some(wall) {
*held = None;
}
}
}
}
}
found
.into_iter()
.filter_map(|(id, wall)| wall.map(|wall| (id, wall)))
.collect()
}
const CURTAIN_PART_BOX_TOLERANCE_FEET: f64 = 0.05;
fn box_contains(outer: &[f64; 6], inner: &[f64; 6], tolerance: f64) -> bool {
(0..3).all(|axis| {
inner[axis] >= outer[axis] - tolerance && inner[axis + 3] <= outer[axis + 3] + tolerance
})
}
pub fn product_instances_from_partition_records(
rf: &mut RevitFile,
revit_version: u32,
level_ids: &BTreeSet<u32>,
) -> Result<Vec<DecodedElement>> {
use crate::partition_element_records::PRODUCT_RECORD_CATEGORIES;
if !crate::partition_element_records::supports_revit_version(revit_version) {
return Ok(Vec::new());
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(Vec::new()),
};
if declared.is_empty() {
return Ok(Vec::new());
}
let categories: Vec<i64> = PRODUCT_RECORD_CATEGORIES.iter().map(|(c, _)| *c).collect();
let records = crate::partition_element_records::scan_category_records_multi(
rf,
revit_version,
&categories,
&declared,
)?;
let records = without_non_primary_options(rf, records);
let mut by_category: std::collections::BTreeMap<i64, Vec<_>> =
std::collections::BTreeMap::new();
for record in records {
by_category
.entry(record.builtin_category)
.or_default()
.push(record);
}
let mut out = Vec::new();
for (category, class) in PRODUCT_RECORD_CATEGORIES {
if let Some(records) = by_category.remove(&category) {
out.extend(instances_from_records(records, class, level_ids));
}
}
Ok(out)
}
pub fn rooms_from_partition_category_records(
rf: &mut RevitFile,
revit_version: u32,
level_ids: &BTreeSet<u32>,
) -> Result<Vec<DecodedElement>> {
let Some(records) = category_records(
rf,
revit_version,
crate::partition_element_records::OST_ROOMS,
)?
else {
return Ok(Vec::new());
};
let room_ids: BTreeSet<u32> = records
.iter()
.filter(|record| record.is_exported_instance())
.map(|record| record.element_id)
.collect();
let parameters =
crate::partition_room_parameters::scan_room_parameters(rf, revit_version, &room_ids)
.unwrap_or_default();
let mut rooms = room_instances_from_records(records, ¶meters, level_ids);
attach_room_parameter_entries(rf, revit_version, &room_ids, &mut rooms);
Ok(rooms)
}
fn attach_room_parameter_entries(
rf: &mut RevitFile,
revit_version: u32,
room_ids: &BTreeSet<u32>,
rooms: &mut [DecodedElement],
) {
let entries =
crate::partition_room_parameters::scan_room_parameter_entries(rf, revit_version, room_ids);
for room in rooms.iter_mut() {
let Some((number, name)) = room.id.and_then(|id| entries.get(&id)) else {
continue;
};
let text = |wanted: &str| {
room.fields.iter().find_map(|(field, value)| match value {
InstanceField::String(text) if field == wanted => Some(text.clone()),
_ => None,
})
};
if text("m_name").as_ref() == Some(name) && text(ROOM_NUMBER_FIELD).as_ref() == Some(number)
{
continue;
}
room.fields.retain(|(field, _)| {
field != "m_name" && field != ROOM_NUMBER_FIELD && field != ROOM_PARAMETER_SOURCE_FIELD
});
room.fields
.push(("m_name".into(), InstanceField::String(name.clone())));
room.fields.push((
ROOM_NUMBER_FIELD.into(),
InstanceField::String(number.clone()),
));
room.fields.push((
ROOM_PARAMETER_SOURCE_FIELD.into(),
InstanceField::String(
crate::partition_room_parameters::ROOM_PARAMETER_ENTRY_SOURCE.into(),
),
));
}
}
fn attach_room_outlines(rf: &mut RevitFile, revit_version: u32, rooms: &mut [DecodedElement]) {
use crate::partition_room_boundaries as prb;
if !prb::ROOM_SOLID_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) {
return;
}
for room in rooms.iter_mut() {
let (Some(id), Some(stream)) = (room.id, room_source_stream(room)) else {
continue;
};
let float = |wanted: &str| {
room.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let (Some(cx), Some(cy), Some(z), Some(dx), Some(dy), Some(dz)) = (
float("m_locationX"),
float("m_locationY"),
float("m_locationZ"),
float("m_bboxWidth"),
float("m_bboxDepth"),
float("m_bboxHeight"),
) else {
continue;
};
let bbox = [
cx - dx / 2.0,
cy - dy / 2.0,
z,
cx + dx / 2.0,
cy + dy / 2.0,
z + dz,
];
if let Ok(Some(profile)) = prb::room_outline(rf, &stream, id, bbox) {
let mut fields = profile.fields();
for (name, value) in fields.iter_mut() {
if name == crate::element_record_plan_profiles::PLAN_PROFILE_SOURCE_FIELD {
*value = InstanceField::String(prb::ROOM_OUTLINE_SOURCE.into());
}
}
room.fields.extend(fields);
}
}
}
fn room_source_stream(room: &DecodedElement) -> Option<String> {
room.fields.iter().find_map(|(name, value)| match value {
InstanceField::String(stream) if name == "m_source_stream" => Some(stream.clone()),
_ => None,
})
}
pub const ROOM_NUMBER_FIELD: &str = "m_number";
pub const ROOM_PARAMETER_SOURCE_FIELD: &str = "m_room_parameter_source";
pub fn room_instances_from_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
parameters: &std::collections::BTreeMap<u32, crate::partition_room_parameters::RoomParameters>,
level_ids: &BTreeSet<u32>,
) -> Vec<DecodedElement> {
select_instance_records(records)
.values()
.map(|record| {
let mut decoded = element_record_decoded(record, "Room", level_ids);
if let Some(block) = parameters.get(&record.element_id) {
decoded
.fields
.push(("m_name".into(), InstanceField::String(block.name.clone())));
decoded.fields.push((
ROOM_NUMBER_FIELD.into(),
InstanceField::String(block.number.clone()),
));
decoded.fields.push((
ROOM_PARAMETER_SOURCE_FIELD.into(),
InstanceField::String(
crate::partition_room_parameters::ROOM_PARAMETER_SOURCE.into(),
),
));
let already_bound = decoded.fields.iter().any(|(name, _)| {
name == crate::element_record_level_refs::LEVEL_REFERENCE_FIELD
});
if !already_bound {
if let Some(id) = block.level_element_id(level_ids) {
decoded.fields.push((
crate::element_record_level_refs::LEVEL_REFERENCE_FIELD.into(),
InstanceField::ElementId { tag: 0, id },
));
if let Some(slot) = decoded
.fields
.iter_mut()
.find(|(name, _)| name == "m_level_bound")
{
slot.1 = InstanceField::Bool(true);
}
}
}
}
decoded
})
.collect()
}
pub fn columns_from_partition_category_records(
rf: &mut RevitFile,
revit_version: u32,
level_ids: &BTreeSet<u32>,
) -> Result<Vec<DecodedElement>> {
let (column_records, wall_records) = column_and_wall_records(rf, revit_version)?;
let wall_instances: Vec<crate::partition_element_records::PartitionElementRecord> =
select_instance_records(wall_records)
.into_values()
.collect();
Ok(column_instances_from_records(
column_records,
level_ids,
&wall_instances,
))
}
fn column_and_wall_records(
rf: &mut RevitFile,
revit_version: u32,
) -> Result<(
Vec<crate::partition_element_records::PartitionElementRecord>,
Vec<crate::partition_element_records::PartitionElementRecord>,
)> {
use crate::partition_element_records as per;
if !per::supports_revit_version(revit_version) {
return Ok((Vec::new(), Vec::new()));
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok((Vec::new(), Vec::new())),
};
if declared.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let records = per::scan_category_records_multi(
rf,
revit_version,
&[per::OST_COLUMNS, per::OST_WALLS],
&declared,
)?;
let records = without_non_primary_options(rf, records);
Ok(records
.into_iter()
.partition(|record| record.builtin_category == per::OST_COLUMNS))
}
pub fn walls_from_partition_category_records(
rf: &mut RevitFile,
revit_version: u32,
level_ids: &BTreeSet<u32>,
) -> Result<Vec<DecodedElement>> {
let Some(records) = category_records(
rf,
revit_version,
crate::partition_element_records::OST_WALLS,
)?
else {
return Ok(Vec::new());
};
Ok(wall_instances_from_records(records, level_ids))
}
pub fn level_element_ids(rf: &mut RevitFile, revit_version: u32) -> Result<BTreeSet<u32>> {
if !crate::partition_level_records::supports_revit_version(revit_version) {
return Ok(BTreeSet::new());
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(BTreeSet::new()),
};
crate::partition_level_records::scan_partition_level_ids(rf, revit_version, &declared)
}
fn category_records(
rf: &mut RevitFile,
revit_version: u32,
builtin_category: i64,
) -> Result<Option<Vec<crate::partition_element_records::PartitionElementRecord>>> {
if !crate::partition_element_records::supports_revit_version(revit_version) {
return Ok(None);
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(None),
};
if declared.is_empty() {
return Ok(None);
}
let records = crate::partition_element_records::scan_category_records(
rf,
revit_version,
builtin_category,
&declared,
)?;
Ok(Some(without_non_primary_options(rf, records)))
}
pub fn instances_from_partition_category_records(
rf: &mut RevitFile,
revit_version: u32,
builtin_category: i64,
class: &str,
level_ids: &BTreeSet<u32>,
) -> Result<Vec<DecodedElement>> {
if !crate::partition_element_records::supports_revit_version(revit_version) {
return Ok(Vec::new());
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(Vec::new()),
};
if declared.is_empty() {
return Ok(Vec::new());
}
let records = crate::partition_element_records::scan_category_records(
rf,
revit_version,
builtin_category,
&declared,
)?;
let records = without_non_primary_options(rf, records);
Ok(instances_from_records(records, class, level_ids))
}
pub fn instances_from_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
class: &str,
level_ids: &BTreeSet<u32>,
) -> Vec<DecodedElement> {
select_instance_records(records)
.values()
.map(|record| element_record_decoded(record, class, level_ids))
.collect()
}
pub const OPENING_HOST_FIELD: &str = "m_hostId";
pub const OPENING_HOST_PROVENANCE_FIELD: &str = "m_host_provenance";
pub const OPENING_HOST_PROVENANCE: &str = "partition_element_record_reference_list";
const OPENING_HOST_CANDIDATES_FIELD: &str = "m_hostCandidateIds";
pub fn opening_instances_from_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
class: &str,
host_candidates: &BTreeSet<u32>,
level_ids: &BTreeSet<u32>,
) -> Vec<DecodedElement> {
select_instance_records(records)
.values()
.map(|record| {
let mut decoded = element_record_decoded(record, class, level_ids);
attach_host_candidates(record, host_candidates, &mut decoded);
decoded
})
.collect()
}
fn attach_host_candidates(
record: &crate::partition_element_records::PartitionElementRecord,
host_candidates: &BTreeSet<u32>,
decoded: &mut DecodedElement,
) {
let walls: Vec<u32> = record
.references
.iter()
.filter_map(|id| u32::try_from(*id).ok())
.filter(|id| *id != record.element_id && host_candidates.contains(id))
.collect();
let (below, above): (Vec<u32>, Vec<u32>) =
walls.iter().partition(|id| **id < record.element_id);
let candidates: Vec<u32> = below.into_iter().rev().chain(above).collect();
let Some(&host) = candidates.first() else {
return;
};
decoded.fields.push((
OPENING_HOST_CANDIDATES_FIELD.into(),
InstanceField::Vector(
candidates
.iter()
.map(|&id| InstanceField::ElementId { tag: 0, id })
.collect(),
),
));
decoded.fields.push((
OPENING_HOST_FIELD.into(),
InstanceField::ElementId { tag: 0, id: host },
));
decoded.fields.push((
OPENING_HOST_PROVENANCE_FIELD.into(),
InstanceField::String(OPENING_HOST_PROVENANCE.into()),
));
}
fn bind_opening_hosts(walls: &[DecodedElement], openings: &mut [DecodedElement]) {
let curtain_walls: BTreeSet<u32> = walls
.iter()
.filter(|wall| wall.class == CURTAIN_WALL_CLASS)
.filter_map(|wall| wall.id)
.collect();
for element in openings.iter_mut() {
let candidates = element_ids_field(element, OPENING_HOST_CANDIDATES_FIELD);
if candidates.is_empty() {
continue;
}
let host = candidates
.into_iter()
.find(|id| !curtain_walls.contains(id));
element.fields.retain(|(name, _)| {
name != OPENING_HOST_CANDIDATES_FIELD
&& name != OPENING_HOST_FIELD
&& name != OPENING_HOST_PROVENANCE_FIELD
});
if let Some(host) = host {
element.fields.push((
OPENING_HOST_FIELD.into(),
InstanceField::ElementId { tag: 0, id: host },
));
element.fields.push((
OPENING_HOST_PROVENANCE_FIELD.into(),
InstanceField::String(OPENING_HOST_PROVENANCE.into()),
));
}
}
}
pub fn openings_from_partition_category_records(
rf: &mut RevitFile,
revit_version: u32,
builtin_category: i64,
class: &str,
host_candidates: &BTreeSet<u32>,
level_ids: &BTreeSet<u32>,
) -> Result<Vec<DecodedElement>> {
if !crate::partition_element_records::supports_revit_version(revit_version) {
return Ok(Vec::new());
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(Vec::new()),
};
if declared.is_empty() {
return Ok(Vec::new());
}
let records = crate::partition_element_records::scan_category_records(
rf,
revit_version,
builtin_category,
&declared,
)?;
let records = without_non_primary_options(rf, records);
Ok(opening_instances_from_records(
records,
class,
host_candidates,
level_ids,
))
}
fn without_non_primary_options(
rf: &mut RevitFile,
mut records: Vec<crate::partition_element_records::PartitionElementRecord>,
) -> Vec<crate::partition_element_records::PartitionElementRecord> {
let options = rf.design_options();
records.retain(|record| !options.excludes(record));
records
}
fn recover_2023_records(rf: &mut RevitFile, out: &mut PartitionSchemaMvp) {
use crate::partition_element_records as per;
let records = crate::partition_element_records_2023::scan_records(
rf,
crate::partition_element_records_2023::REVIT_2023,
);
let names = type_and_family_names_2023(rf, &records);
let system_types = system_type_names_2023(rf, &records);
let selected = select_newest_instance_records(records);
let family_instance = rf.schema_classes().ok().and_then(|classes| {
classes
.classes
.iter()
.find(|class| class.name == "FamilyInstance")
.map(|class| class.tag)
});
let nested = match family_instance {
Some(tag) => nested_in_openings(&selected, tag),
None => BTreeSet::new(),
};
let level_ids = level_element_ids(rf, crate::partition_element_records_2023::REVIT_2023)
.unwrap_or_default();
for record in selected.values() {
if nested.contains(&record.element_id) {
continue;
}
let Some((_, class)) = per::RECOVERED_CATEGORIES
.iter()
.find(|(category, _)| *category == record.builtin_category)
else {
continue;
};
let mut decoded = element_record_decoded(record, class, &level_ids);
if let Some((type_id, type_name, family_name)) = names.get(&record.element_id) {
decoded.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: *type_id,
},
));
decoded.fields.push((
TYPE_NAME_FIELD.into(),
InstanceField::String(type_name.clone()),
));
decoded.fields.push((
FAMILY_NAME_FIELD.into(),
InstanceField::String(family_name.clone()),
));
} else if let Some((type_id, type_name)) = system_types.get(&record.element_id) {
decoded.fields.push((
TYPE_ID_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: *type_id,
},
));
decoded.fields.push((
TYPE_NAME_FIELD.into(),
InstanceField::String(type_name.clone()),
));
}
match record.builtin_category {
per::OST_WALLS => out.walls.push(decoded),
per::OST_DOORS => out.doors.push(decoded),
per::OST_WINDOWS => out.windows.push(decoded),
per::OST_FLOORS | per::OST_BUILDING_PAD => out.slabs.push(decoded),
per::OST_ROOMS => out.rooms.push(decoded),
per::OST_COLUMNS => out.columns.push(decoded),
_ => out.products.push(decoded),
}
}
let wall_records: Vec<per::PartitionElementRecord> = out
.walls
.iter()
.filter_map(|wall| wall.id.and_then(|id| selected.get(&id)).cloned())
.collect();
let trims = crate::element_record_wall_joins::join_trims(&wall_records);
for wall in out.walls.iter_mut() {
if let Some((record, trim)) = wall
.id
.and_then(|id| Some((selected.get(&id)?, trims.get(&id)?)))
{
apply_wall_join_trim(wall, record, trim);
}
}
let framing: Vec<per::PartitionElementRecord> = out
.products
.iter()
.filter(|element| element.class == "StructuralFraming")
.filter_map(|element| element.id.and_then(|id| selected.get(&id)).cloned())
.collect();
let columns: Vec<per::PartitionElementRecord> = selected
.values()
.filter(|record| {
matches!(
record.builtin_category,
per::OST_COLUMNS | per::OST_STRUCTURAL_COLUMNS
) && !nested.contains(&record.element_id)
})
.cloned()
.collect();
let beam_trims = crate::element_record_beam_cuts::beam_column_trims(&framing, &columns);
for beam in out.products.iter_mut() {
if let Some(trim) = beam.id.and_then(|id| beam_trims.get(&id)) {
apply_beam_column_trim(beam, trim);
}
}
let room_ids: BTreeSet<u32> = out.rooms.iter().filter_map(|room| room.id).collect();
attach_room_parameter_entries(
rf,
crate::partition_element_records_2023::REVIT_2023,
&room_ids,
&mut out.rooms,
);
let wall_ids: BTreeSet<u32> = out.walls.iter().filter_map(|wall| wall.id).collect();
for element in out.doors.iter_mut().chain(out.windows.iter_mut()) {
if let Some(record) = element.id.and_then(|id| selected.get(&id)) {
attach_host_candidates(record, &wall_ids, element);
}
}
bind_opening_hosts(&out.walls, &mut out.doors);
bind_opening_hosts(&out.walls, &mut out.windows);
}
fn nested_in_openings(
selected: &BTreeMap<u32, crate::partition_element_records::PartitionElementRecord>,
family_instance: u16,
) -> BTreeSet<u32> {
use crate::partition_element_records::{OST_DOORS, OST_WINDOWS};
let is_opening = |category: i64| category == OST_DOORS || category == OST_WINDOWS;
selected
.values()
.filter(|child| child.class_tag == family_instance && !is_opening(child.builtin_category))
.filter(|child| {
child.references.iter().any(|&reference| {
u32::try_from(reference)
.ok()
.and_then(|parent| selected.get(&parent))
.is_some_and(|parent| {
is_opening(parent.builtin_category)
&& parent.references.contains(&u64::from(child.element_id))
})
})
})
.map(|child| child.element_id)
.collect()
}
fn select_instance_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
) -> std::collections::BTreeMap<u32, crate::partition_element_records::PartitionElementRecord> {
use crate::partition_element_records::PartitionElementRecord;
use std::collections::BTreeMap;
let mut by_id: BTreeMap<u32, PartitionElementRecord> = BTreeMap::new();
for record in records {
if !record.is_exported_instance() || !record.has_volume() {
continue;
}
let better = match by_id.get(&record.element_id) {
None => true,
Some(existing) => {
let candidate = z_extent_key(&record);
let held = z_extent_key(existing);
candidate > held
|| (candidate == held
&& (record.stream.as_str(), record.offset)
> (existing.stream.as_str(), existing.offset))
}
};
if better {
by_id.insert(record.element_id, record);
}
}
without_nested_components(by_id)
}
const SYSTEM_TYPE_CATEGORIES_2023: [i64; 4] = [
crate::partition_element_records::OST_WALLS,
crate::partition_element_records::OST_FLOORS,
-2_000_035,
-2_000_038,
];
fn system_type_names_2023(
rf: &mut RevitFile,
records: &[crate::partition_element_records::PartitionElementRecord],
) -> BTreeMap<u32, (u32, String)> {
let Ok(table) = crate::elem_table::parse_records(rf) else {
return BTreeMap::new();
};
let declared = crate::elem_table::declared_ids(&table);
let entries = crate::partition_names::name_entries_2023(rf, &declared);
let recorded: BTreeSet<u32> = records.iter().map(|record| record.element_id).collect();
let mut named_by: BTreeMap<u32, BTreeSet<i64>> = BTreeMap::new();
for record in records {
for &reference in &record.references {
if let Ok(id) = u32::try_from(reference) {
if !recorded.contains(&id) && !entries.contains_key(&id) {
named_by
.entry(id)
.or_default()
.insert(record.builtin_category);
}
}
}
}
let wanted: BTreeSet<u32> = named_by.keys().copied().collect();
let mut names: BTreeMap<u32, Option<String>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
for (id, name) in
crate::partition_names::find_element_data_names_2023(inflated.bytes(), &wanted)
{
match names.get(&id) {
None => {
names.insert(id, Some(name));
}
Some(held) if held.as_deref() != Some(name.as_str()) => {
names.insert(id, None);
}
_ => {}
}
}
}
let mut out = BTreeMap::new();
for record in records.iter().filter(|record| {
record.is_exported_instance()
&& SYSTEM_TYPE_CATEGORIES_2023.contains(&record.builtin_category)
}) {
let candidates: BTreeSet<u32> = record
.references
.iter()
.filter_map(|&reference| u32::try_from(reference).ok())
.filter(|id| matches!(names.get(id), Some(Some(_))))
.collect();
let most_specific = candidates.iter().copied().filter(|id| {
candidates.iter().all(|other| {
other == id
|| (named_by[id].is_subset(&named_by[other]) && named_by[id] != named_by[other])
})
});
let mut picked = most_specific;
let (Some(type_id), None) = (picked.next(), picked.next()) else {
continue;
};
if let Some(Some(name)) = names.get(&type_id) {
out.insert(record.element_id, (type_id, name.clone()));
}
}
out
}
fn type_and_family_names_2023(
rf: &mut RevitFile,
records: &[crate::partition_element_records::PartitionElementRecord],
) -> BTreeMap<u32, (u32, String, String)> {
use crate::partition_element_records::PLACEMENT_KIND_SYMBOL;
let Ok(table) = crate::elem_table::parse_records(rf) else {
return BTreeMap::new();
};
let names =
crate::partition_names::name_entries_2023(rf, &crate::elem_table::declared_ids(&table));
let recorded: BTreeSet<u32> = records.iter().map(|record| record.element_id).collect();
let mut symbols: BTreeMap<i64, BTreeSet<u32>> = BTreeMap::new();
let mut symbol_records: BTreeMap<
u32,
&crate::partition_element_records::PartitionElementRecord,
> = BTreeMap::new();
for record in records
.iter()
.filter(|record| record.placement_kind == PLACEMENT_KIND_SYMBOL)
{
symbols
.entry(record.builtin_category)
.or_default()
.insert(record.element_id);
symbol_records.insert(record.element_id, record);
}
let family_of = |type_id: u32| {
let record = symbol_records.get(&type_id)?;
let mut families = record.references.iter().filter_map(|&reference| {
let id = u32::try_from(reference).ok()?;
(id != type_id && !recorded.contains(&id))
.then(|| names.get(&id))
.flatten()
});
let family = families.next()?;
families.next().is_none().then(|| family.clone())
};
let mut out = BTreeMap::new();
for record in records
.iter()
.filter(|record| record.is_exported_instance())
{
let Some(set) = symbols.get(&record.builtin_category) else {
continue;
};
let mut types = record.references.iter().filter_map(|&reference| {
let id = u32::try_from(reference).ok()?;
(set.contains(&id) && names.contains_key(&id)).then_some(id)
});
let (Some(type_id), None) = (types.next(), types.next()) else {
continue;
};
let (Some(type_name), Some(family_name)) = (names.get(&type_id), family_of(type_id)) else {
continue;
};
out.insert(record.element_id, (type_id, type_name.clone(), family_name));
}
out
}
fn select_newest_instance_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
) -> std::collections::BTreeMap<u32, crate::partition_element_records::PartitionElementRecord> {
use crate::partition_element_records::PartitionElementRecord;
let partition = |record: &PartitionElementRecord| {
record
.stream
.rsplit('/')
.next()
.and_then(|n| n.parse::<u32>().ok())
.unwrap_or(0)
};
let mut by_id: BTreeMap<u32, PartitionElementRecord> = BTreeMap::new();
for record in records {
if !record.is_exported_instance() || !record.has_volume() {
continue;
}
let newer = by_id.get(&record.element_id).is_none_or(|held| {
(partition(&record), record.offset) > (partition(held), held.offset)
});
if newer {
by_id.insert(record.element_id, record);
}
}
without_nested_components(by_id)
}
fn without_nested_components(
mut by_id: BTreeMap<u32, crate::partition_element_records::PartitionElementRecord>,
) -> BTreeMap<u32, crate::partition_element_records::PartitionElementRecord> {
use crate::partition_element_records::NESTED_COMPONENT_CATEGORIES;
let nested: Vec<u32> = by_id
.values()
.filter(|child| NESTED_COMPONENT_CATEGORIES.contains(&child.builtin_category))
.filter(|child| {
child.references.iter().any(|&reference| {
u32::try_from(reference)
.ok()
.filter(|&parent| parent < child.element_id)
.and_then(|parent| by_id.get(&parent))
.is_some_and(|parent| {
parent.builtin_category == child.builtin_category
&& parent.references.contains(&u64::from(child.element_id))
})
})
})
.map(|child| child.element_id)
.collect();
for id in nested {
by_id.remove(&id);
}
by_id
}
fn z_extent_key(record: &crate::partition_element_records::PartitionElementRecord) -> i64 {
let (_, _, dz) = record.extents_feet();
if !dz.is_finite() {
return i64::MIN;
}
(dz * 10_000.0).round() as i64
}
pub fn slabs_from_partition_category_records(
rf: &mut RevitFile,
revit_version: u32,
level_ids: &BTreeSet<u32>,
) -> Result<Vec<DecodedElement>> {
use crate::partition_element_records as per;
if !per::supports_revit_version(revit_version) {
return Ok(Vec::new());
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(Vec::new()),
};
if declared.is_empty() {
return Ok(Vec::new());
}
let categories = [
per::OST_FLOORS,
per::OST_BUILDING_PAD,
per::OST_SKETCH_LINES,
];
let scanned = per::scan_category_records_multi(rf, revit_version, &categories, &declared)?;
let scanned = without_non_primary_options(rf, scanned);
let sketch_lines: Vec<per::PartitionElementRecord> = scanned
.iter()
.filter(|record| record.builtin_category == per::OST_SKETCH_LINES)
.cloned()
.collect();
let mut plates: std::collections::BTreeMap<u32, [f64; 4]> = Default::default();
for record in scanned.iter().filter(|record| {
record.builtin_category == per::OST_FLOORS
|| record.builtin_category == per::OST_BUILDING_PAD
}) {
let b = record.bbox_feet;
plates
.entry(record.element_id)
.or_insert([b[0], b[1], b[3], b[4]]);
}
let profiles = sketch_plan_profiles(rf, revit_version, &sketch_lines, &plates);
let mut out = Vec::new();
for (category, class) in [
(per::OST_FLOORS, "Floor"),
(per::OST_BUILDING_PAD, "BuildingPad"),
] {
let records: Vec<per::PartitionElementRecord> = scanned
.iter()
.filter(|record| record.builtin_category == category)
.cloned()
.collect();
for mut decoded in instances_from_records(records, class, level_ids) {
if let Some(id) = decoded.id {
if let Some(profile) = profiles.get(&id) {
decoded.fields.extend(profile.fields());
}
}
out.push(decoded);
}
}
Ok(out)
}
pub const SKETCH_BOX_TOLERANCE_FEET: f64 = 1e-3;
fn sketch_plan_profiles(
rf: &mut RevitFile,
revit_version: u32,
sketch_lines: &[crate::partition_element_records::PartitionElementRecord],
owners: &std::collections::BTreeMap<u32, [f64; 4]>,
) -> std::collections::BTreeMap<u32, crate::element_record_plan_profiles::PlanProfile> {
use crate::element_record_plan_profiles as erpp;
use std::collections::BTreeMap;
let line_ids: BTreeSet<u32> = sketch_lines.iter().map(|r| r.element_id).collect();
let lists: BTreeMap<u32, Vec<u32>> =
erpp::scan_sketch_curve_lists(rf, revit_version, &line_ids)
.into_iter()
.filter(|(owner, _)| owners.contains_key(owner))
.collect();
let listed: BTreeMap<u32, u32> = lists
.iter()
.flat_map(|(owner, curves)| curves.iter().map(move |id| (*id, *owner)))
.collect();
let remapped: Vec<crate::partition_element_records::PartitionElementRecord> = sketch_lines
.iter()
.filter_map(|record| {
let owner = match listed.get(&record.element_id) {
Some(owner) => Some(*owner),
None => record
.owner_reference
.filter(|owner| !lists.contains_key(owner)),
}?;
let mut record = record.clone();
record.owner_reference = Some(owner);
Some(record)
})
.collect();
let mut named: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
for record in sketch_lines {
if let Some(owner) = record.owner_reference {
named.entry(owner).or_default().insert(record.element_id);
}
}
let relisted: BTreeSet<u32> = lists
.iter()
.filter(|(owner, curves)| {
named.get(owner) != Some(&curves.iter().copied().collect::<BTreeSet<u32>>())
})
.map(|(owner, _)| *owner)
.collect();
let spans_box = |owner: u32, profile: &erpp::PlanProfile| {
owners.get(&owner).is_some_and(|plan_box| {
profile.plan_extent_feet().is_some_and(|extent| {
extent
.iter()
.zip(plan_box)
.all(|(a, b)| (a - b).abs() <= SKETCH_BOX_TOLERANCE_FEET)
})
})
};
let sketch_lines = remapped.as_slice();
let mut profiles = erpp::plan_profiles_from_sketch_line_records(sketch_lines);
profiles.retain(|owner, profile| !relisted.contains(owner) || spans_box(*owner, profile));
profiles.retain(|owner, _| owners.contains_key(owner));
let mut unsolved: BTreeMap<u32, BTreeMap<u32, [f64; 6]>> = BTreeMap::new();
for record in sketch_lines {
let Some(owner) = record.owner_reference else {
continue;
};
if owners.contains_key(&owner) && !profiles.contains_key(&owner) {
unsolved
.entry(owner)
.or_default()
.entry(record.element_id)
.or_insert(record.bbox_feet);
}
}
if unsolved.is_empty() {
return profiles;
}
let ids: BTreeSet<u32> = unsolved
.values()
.flat_map(|lines| lines.keys().copied())
.collect();
let (Ok(lines), Ok(curves)) = (
crate::partition_beam_axes::scan_bounded_lines(rf, revit_version, &ids),
crate::partition_beam_axes::scan_sketch_curves(rf, revit_version, &ids),
) else {
return profiles;
};
let arcs: BTreeMap<u32, crate::partition_beam_axes::BoundedArc> = curves
.iter()
.filter_map(|(&id, curve)| Some((id, curve.arc?)))
.collect();
let eps = erpp::VERTEX_EPS_FEET;
let is_point = |bbox: &[f64; 6]| (0..3).all(|axis| (bbox[axis + 3] - bbox[axis]).abs() <= eps);
for (owner, segments) in unsolved {
let ends: Vec<[f64; 3]> = segments
.keys()
.filter_map(|id| lines.get(id))
.flat_map(|line| [line.start(), line.end()])
.chain(
segments
.keys()
.filter_map(|id| arcs.get(id))
.flat_map(|arc| [arc.point(arc.start_angle), arc.point(arc.end_angle)]),
)
.collect();
let exact: Option<(Vec<[f64; 4]>, bool)> = segments
.iter()
.filter(|(id, bbox)| {
let point = [bbox[0], bbox[1], bbox[2]];
!(is_point(bbox)
&& !lines.contains_key(id)
&& !arcs.contains_key(id)
&& ends
.iter()
.any(|end| (0..3).all(|axis| (end[axis] - point[axis]).abs() <= eps)))
})
.map(|(id, bbox)| {
let inside = |p: [f64; 3]| {
(0..3)
.all(|axis| p[axis] >= bbox[axis] - eps && p[axis] <= bbox[axis + 3] + eps)
};
let line = lines.get(id).and_then(|line| {
let (a, b) = (line.start(), line.end());
(inside(a) && inside(b) && (a[2] - b[2]).abs() <= eps)
.then(|| vec![[a[0], a[1], b[0], b[1]]])
});
let arc = arcs.get(id).and_then(|arc| {
let points = erpp::arc_points(arc)?;
let flat = arc.x_axis[2].abs() <= eps && arc.y_axis[2].abs() <= eps;
(flat && points.iter().all(|&p| inside(p))).then(|| {
points
.windows(2)
.map(|pair| [pair[0][0], pair[0][1], pair[1][0], pair[1][1]])
.collect::<Vec<_>>()
})
});
match (line, arc) {
(Some(chords), None) => Some((chords, false)),
(None, Some(chords)) => Some((chords, true)),
_ => None,
}
})
.collect::<Option<Vec<(Vec<[f64; 4]>, bool)>>>()
.map(|curves| {
let has_arc = curves.iter().any(|(_, arc)| *arc);
let chords: Vec<[f64; 4]> =
curves.into_iter().flat_map(|(chords, _)| chords).collect();
(chords, has_arc)
});
let Some((mut profile, has_arc)) = exact
.and_then(|(chords, has_arc)| Some((erpp::plan_profile_from_lines(&chords)?, has_arc)))
else {
continue;
};
if !(has_arc || relisted.contains(&owner)) || spans_box(owner, &profile) {
profile.segment_ids = segments.keys().copied().collect();
profiles.insert(owner, profile);
}
}
profiles
}
pub const SKETCHED_PRODUCT_CLASSES: &[&str] = &["Roof", "Ceiling"];
fn attach_roof_profiles(rf: &mut RevitFile, revit_version: u32, products: &mut [DecodedElement]) {
use crate::partition_element_records as per;
let float = |element: &DecodedElement, wanted: &str| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let roofs: std::collections::BTreeMap<u32, [f64; 4]> = products
.iter()
.filter(|element| SKETCHED_PRODUCT_CLASSES.contains(&element.class.as_str()))
.filter_map(|element| {
let (x, y) = (
float(element, "m_locationX")?,
float(element, "m_locationY")?,
);
let (w, d) = (
float(element, "m_bboxWidth")?,
float(element, "m_bboxDepth")?,
);
Some((
element.id?,
[x - w / 2.0, y - d / 2.0, x + w / 2.0, y + d / 2.0],
))
})
.collect();
if roofs.is_empty() || !per::supports_revit_version(revit_version) {
return;
}
let declared = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let Ok(sketch_lines) =
per::scan_category_records_multi(rf, revit_version, &[per::OST_SKETCH_LINES], &declared)
else {
return;
};
let profiles = sketch_plan_profiles(rf, revit_version, &sketch_lines, &roofs);
for roof in products
.iter_mut()
.filter(|element| SKETCHED_PRODUCT_CLASSES.contains(&element.class.as_str()))
{
if let Some(profile) = roof.id.and_then(|id| profiles.get(&id)) {
roof.fields.extend(profile.fields());
}
}
}
type ShaftCurve = (Vec<[(f64, f64); 2]>, bool);
type ShaftOutline = (Vec<(f64, f64)>, (f64, f64));
pub const SHAFT_CROSSING_EPS: f64 = 1e-6;
pub const SHAFT_HEIGHT_TOLERANCE_FEET: f64 = 1e-3;
fn attach_shaft_voids(
rf: &mut RevitFile,
revit_version: u32,
groups: [&mut Vec<DecodedElement>; 2],
) {
use crate::element_record_plan_profiles as erpp;
use crate::partition_element_records as per;
if !per::supports_revit_version(revit_version) {
return;
}
let declared = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let Ok(scanned) = per::scan_category_records_multi(
rf,
revit_version,
&[per::OST_SHAFT_OPENING, per::OST_SKETCH_LINES],
&declared,
) else {
return;
};
let mut boxes: std::collections::BTreeMap<u32, [f64; 4]> = Default::default();
let mut heights: std::collections::BTreeMap<u32, (f64, f64)> = Default::default();
for record in scanned
.iter()
.filter(|record| record.builtin_category == per::OST_SHAFT_OPENING)
{
let b = record.bbox_feet;
boxes
.entry(record.element_id)
.or_insert([b[0], b[1], b[3], b[4]]);
heights.entry(record.element_id).or_insert((b[2], b[5]));
}
if boxes.is_empty() {
return;
}
let sketch_lines: Vec<per::PartitionElementRecord> = scanned
.into_iter()
.filter(|record| record.builtin_category == per::OST_SKETCH_LINES)
.collect();
let shafts: Vec<ShaftOutline> = shaft_outlines(rf, revit_version, &sketch_lines, &boxes)
.into_iter()
.filter_map(|(id, outline)| Some((outline, *heights.get(&id)?)))
.collect();
let float = |element: &DecodedElement, wanted: &str| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
for element in groups.into_iter().flat_map(|elements| elements.iter_mut()) {
let Some(profile) = erpp::plan_profile_from_fields(&element.fields) else {
continue;
};
let (Some(base), Some(height)) = (
float(element, "m_locationZ"),
float(element, "m_bboxHeight"),
) else {
continue;
};
if !profile.pieces.is_empty() {
continue;
}
let eps = SHAFT_HEIGHT_TOLERANCE_FEET;
let voids: Vec<Vec<(f64, f64)>> = shafts
.iter()
.filter(|(outline, (bottom, top))| {
base >= bottom - eps
&& base + height <= top + eps
&& erpp::loop_within(&profile.outer_xy, outline)
&& profile
.inner_xy
.iter()
.all(|void| erpp::loops_disjoint(void, outline))
})
.map(|(outline, _)| outline.clone())
.collect();
if !voids.is_empty() {
erpp::add_voids_to_fields(&mut element.fields, &voids);
}
}
}
fn shaft_outlines(
rf: &mut RevitFile,
revit_version: u32,
sketch_lines: &[crate::partition_element_records::PartitionElementRecord],
shafts: &std::collections::BTreeMap<u32, [f64; 4]>,
) -> std::collections::BTreeMap<u32, Vec<(f64, f64)>> {
use crate::element_record_plan_profiles as erpp;
use std::collections::BTreeMap;
let line_ids: BTreeSet<u32> = sketch_lines.iter().map(|r| r.element_id).collect();
let lists = erpp::scan_sketch_curve_lists(rf, revit_version, &line_ids);
let mut members: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
for &shaft in shafts.keys() {
let listed = lists
.get(&shaft)
.map(|curves| curves.iter().copied().collect());
let named = || {
sketch_lines
.iter()
.filter(|r| r.owner_reference == Some(shaft))
.map(|r| r.element_id)
.collect()
};
members.insert(shaft, listed.unwrap_or_else(named));
}
let ids: BTreeSet<u32> = members.values().flatten().copied().collect();
let (Ok(lines), Ok(circles)) = (
crate::partition_beam_axes::scan_bounded_lines(rf, revit_version, &ids),
crate::partition_beam_axes::scan_sketch_circles(rf, revit_version, &ids),
) else {
return BTreeMap::new();
};
let cross = |o: (f64, f64), a: (f64, f64), b: (f64, f64)| {
(a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
};
let apart = |o: (f64, f64), p: (f64, f64), q: (f64, f64), r: (f64, f64)| {
let (c1, c2) = (cross(o, p, q), cross(o, p, r));
c1 * c2 < 0.0 && c1.abs() > SHAFT_CROSSING_EPS && c2.abs() > SHAFT_CROSSING_EPS
};
let crossing =
|[a, b]: [(f64, f64); 2], [c, d]: [(f64, f64); 2]| apart(a, b, c, d) && apart(c, d, a, b);
let mut out = BTreeMap::new();
for (shaft, ids) in members {
let curves: Option<Vec<ShaftCurve>> = ids
.iter()
.map(|id| {
if let Some(line) = lines.get(id) {
let (a, b) = (line.start(), line.end());
return ((a[2] - b[2]).abs() <= erpp::VERTEX_EPS_FEET)
.then(|| (vec![[(a[0], a[1]), (b[0], b[1])]], true));
}
let circle = circles.get(id)?;
let flat = circle.x_axis[2].abs() <= erpp::VERTEX_EPS_FEET
&& circle.y_axis[2].abs() <= erpp::VERTEX_EPS_FEET;
let points = erpp::arc_points(circle).filter(|_| flat)?;
let chords = points
.windows(2)
.map(|pair| [(pair[0][0], pair[0][1]), (pair[1][0], pair[1][1])])
.collect();
Some((chords, false))
})
.collect();
let Some(curves) = curves else {
continue;
};
let straight: Vec<[(f64, f64); 2]> = curves
.iter()
.filter(|(_, line)| *line)
.flat_map(|(chords, _)| chords.iter().copied())
.collect();
let boundary: Vec<[f64; 4]> = curves
.iter()
.filter(|(chords, line)| !*line || !straight.iter().any(|t| crossing(chords[0], *t)))
.flat_map(|(chords, _)| chords.iter().map(|[a, b]| [a.0, a.1, b.0, b.1]))
.collect();
let Some(profile) = erpp::plan_profile_from_lines(&boundary) else {
continue;
};
let spans = profile.plan_extent_feet().is_some_and(|extent| {
extent
.iter()
.zip(&shafts[&shaft])
.all(|(a, b)| (a - b).abs() <= SKETCH_BOX_TOLERANCE_FEET)
});
if spans && profile.inner_xy.is_empty() && profile.pieces.is_empty() {
out.insert(shaft, profile.outer_xy);
}
}
out
}
pub const ROOF_SLOPE_EDGE_FIELDS: [&str; 4] = [
"m_roof_slope_edge_start_x",
"m_roof_slope_edge_start_y",
"m_roof_slope_edge_end_x",
"m_roof_slope_edge_end_y",
];
pub const ROOF_SLOPE_ANGLE_FIELD: &str = "m_roof_slope_angle";
pub const ROOF_TYPE_THICKNESS_FIELD: &str = "m_roof_type_thickness";
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RoofSlope {
pub edge_start: [f64; 2],
pub edge_end: [f64; 2],
pub angle_radians: f64,
pub thickness_feet: f64,
}
pub fn roof_slope_from_fields(fields: &[(String, InstanceField)]) -> Option<RoofSlope> {
let float = |wanted: &str| {
fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let [sx, sy, ex, ey] = ROOF_SLOPE_EDGE_FIELDS.map(float);
Some(RoofSlope {
edge_start: [sx?, sy?],
edge_end: [ex?, ey?],
angle_radians: float(ROOF_SLOPE_ANGLE_FIELD)?,
thickness_feet: float(ROOF_TYPE_THICKNESS_FIELD)?,
})
}
fn attach_roof_slopes(rf: &mut RevitFile, revit_version: u32, products: &mut [DecodedElement]) {
use crate::partition_element_records as per;
use crate::partition_roof_slopes as prs;
if !prs::ROOF_SLOPE_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) {
return;
}
let type_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
};
let roofs: BTreeSet<u32> = products
.iter()
.filter(|element| element.class == "Roof" && type_of(element).is_some())
.filter(|element| {
crate::element_record_plan_profiles::plan_profile_from_fields(&element.fields).is_some()
})
.filter_map(|element| element.id)
.collect();
if roofs.is_empty() {
return;
}
let declared = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let Ok(sketch_lines) =
per::scan_category_records_multi(rf, revit_version, &[per::OST_SKETCH_LINES], &declared)
else {
return;
};
let mut edges: std::collections::BTreeMap<u32, BTreeSet<u32>> = Default::default();
for line in &sketch_lines {
if let Some(owner) = line.owner_reference.filter(|owner| roofs.contains(owner)) {
edges.entry(owner).or_default().insert(line.element_id);
}
}
let line_ids: BTreeSet<u32> = edges.values().flatten().copied().collect();
let Ok(slopes) = prs::scan_edge_slopes(rf, revit_version, &line_ids) else {
return;
};
let mut defining: std::collections::BTreeMap<u32, (u32, f64)> = Default::default();
for (&roof, lines) in &edges {
if !lines.iter().all(|line| slopes.contains_key(line)) {
continue;
}
let mut sloped = lines
.iter()
.filter_map(|line| slopes.get(line).map(|slope| (*line, slope)))
.filter(|(_, slope)| slope.defines_slope);
if let (Some((line, slope)), None) = (sloped.next(), sloped.next()) {
defining.insert(roof, (line, slope.angle_radians));
}
}
if defining.is_empty() {
return;
}
let defining_lines: BTreeSet<u32> = defining.values().map(|(line, _)| *line).collect();
let types: BTreeSet<u32> = products
.iter()
.filter(|element| element.id.is_some_and(|id| defining.contains_key(&id)))
.filter_map(type_of)
.collect();
let materials: BTreeSet<u32> = crate::partition_type_records::scan_type_records(
rf,
revit_version,
crate::partition_type_records::OST_MATERIALS,
&declared,
)
.unwrap_or_default()
.iter()
.map(|record| record.element_id)
.collect();
let (Ok(lines), Ok(layers)) = (
crate::partition_beam_axes::scan_bounded_lines(rf, revit_version, &defining_lines),
crate::partition_compound_structure::scan_type_layers(
rf,
revit_version,
&types,
&materials,
&declared,
),
) else {
return;
};
for roof in products
.iter_mut()
.filter(|element| element.class == "Roof")
{
let (Some(id), Some(type_id)) = (roof.id, type_of(roof)) else {
continue;
};
let Some(&(line_id, angle)) = defining.get(&id) else {
continue;
};
let (Some(line), Some(type_layers)) = (lines.get(&line_id), layers.get(&type_id)) else {
continue;
};
let thickness: f64 = type_layers.iter().map(|layer| layer.width_feet).sum();
if !(thickness.is_finite() && thickness > 0.0) {
continue;
}
let (start, end) = (line.start(), line.end());
for (name, value) in ROOF_SLOPE_EDGE_FIELDS
.iter()
.zip([start[0], start[1], end[0], end[1]])
{
roof.fields
.push(((*name).into(), InstanceField::Float { value, size: 8 }));
}
roof.fields.push((
ROOF_SLOPE_ANGLE_FIELD.into(),
InstanceField::Float {
value: angle,
size: 8,
},
));
roof.fields.push((
ROOF_TYPE_THICKNESS_FIELD.into(),
InstanceField::Float {
value: thickness,
size: 8,
},
));
}
}
pub const STACKED_LAYER_CLASSES: &[&str] = &["Floor", "BuildingPad", "Roof", "Ceiling"];
pub const SLAB_LAYER_HEIGHT_TOLERANCE_FEET: f64 = 1e-4;
fn materials_and_names(
rf: &mut RevitFile,
revit_version: u32,
declared: &BTreeSet<u32>,
) -> Option<(BTreeSet<u32>, BTreeMap<u32, String>)> {
if revit_version == crate::partition_element_records_2023::REVIT_2023 {
return Some(crate::partition_materials::scan_materials_2023(
rf, declared,
));
}
let materials = crate::partition_type_records::scan_type_records(
rf,
revit_version,
crate::partition_type_records::OST_MATERIALS,
declared,
)
.unwrap_or_default()
.iter()
.map(|record| record.element_id)
.collect();
let names =
crate::partition_materials::scan_material_names(rf, revit_version, declared).ok()?;
Some((materials, names))
}
fn attach_slab_layers(
rf: &mut RevitFile,
revit_version: u32,
mut elements: Vec<&mut DecodedElement>,
) {
use crate::partition_compound_structure as pcs;
if pcs::layer_layout(revit_version).is_none() {
return;
}
let type_of = |element: &DecodedElement| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. } if name == TYPE_ID_FIELD => Some(*id),
_ => None,
})
};
elements.retain(|element| {
STACKED_LAYER_CLASSES.contains(&element.class.as_str())
&& type_of(element).is_some()
&& roof_slope_from_fields(&element.fields).is_none()
});
if elements.is_empty() {
return;
}
let types: BTreeSet<u32> = elements
.iter()
.filter_map(|element| type_of(element))
.collect();
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let Some((materials, names)) = materials_and_names(rf, revit_version, &declared) else {
return;
};
let (Ok(layers), Ok(appearances)) = (
pcs::scan_type_layers(rf, revit_version, &types, &materials, &declared),
crate::partition_materials::scan_material_appearances(rf, revit_version, &declared),
) else {
return;
};
let mut category_materials: BTreeMap<&str, Option<u32>> = BTreeMap::new();
for (class, category) in [
("Floor", crate::partition_element_records::OST_FLOORS),
("Roof", crate::partition_element_records::OST_ROOFS),
("Ceiling", crate::partition_element_records::OST_CEILINGS),
] {
let material =
crate::partition_materials::scan_category_material(rf, revit_version, category)
.ok()
.flatten()
.filter(|id| names.contains_key(id));
category_materials.insert(class, material);
}
for element in elements {
let Some(type_layers) = type_of(element).and_then(|id| layers.get(&id)) else {
continue;
};
let category_material = category_materials
.get(element.class.as_str())
.copied()
.flatten();
let type_layers: Vec<pcs::CompoundLayer> = type_layers
.iter()
.map(|layer| pcs::CompoundLayer {
material: layer.material.or(category_material),
..*layer
})
.collect();
let type_layers = &type_layers;
let height = element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == "m_bboxHeight" => Some(*value),
_ => None,
});
let total: f64 = type_layers.iter().map(|layer| layer.width_feet).sum();
if !height.is_some_and(|h| (h - total).abs() <= SLAB_LAYER_HEIGHT_TOLERANCE_FEET) {
continue;
}
let bands = layer_bands_field(type_layers, &appearances, &names);
if matches!(&bands, InstanceField::Vector(items) if !items.is_empty()) {
element.fields.push((SLAB_LAYERS_FIELD.into(), bands));
}
}
}
pub fn columns_from_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
level_ids: &BTreeSet<u32>,
) -> Vec<DecodedElement> {
column_instances_from_records(records, level_ids, &[])
}
pub const TYPE_SYMBOL_FIELD: &str = "m_type_symbol_id";
pub const TYPE_PROFILE_WIDTH_FIELD: &str = "m_type_profile_width";
pub const TYPE_PROFILE_DEPTH_FIELD: &str = "m_type_profile_depth";
pub const TYPE_PROFILE_SOURCE_FIELD: &str = "m_type_profile_source";
pub const TYPE_PROFILE_SOURCE: &str = "family_type_symbol_envelope";
pub const TYPE_PROFILE_EPS_FEET: f64 = 1e-6;
pub fn column_instances_from_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
level_ids: &BTreeSet<u32>,
wall_records: &[crate::partition_element_records::PartitionElementRecord],
) -> Vec<DecodedElement> {
use crate::partition_element_records::PartitionElementRecord;
use std::collections::BTreeMap;
let symbols: BTreeMap<u32, [f64; 6]> = records
.iter()
.filter(|record| record.is_type_symbol())
.map(|record| (record.element_id, record.bbox_feet))
.collect();
let symbol_ids: BTreeSet<u32> = symbols.keys().copied().collect();
let selected = select_instance_records(records);
let instances: Vec<PartitionElementRecord> = selected.values().cloned().collect();
let cuts = crate::element_record_column_cuts::column_cut_boxes(&instances, wall_records);
selected
.values()
.map(|record: &PartitionElementRecord| {
let mut decoded = element_record_decoded(record, "Column", level_ids);
if let Some(symbol) = record
.type_symbol_reference(&symbol_ids)
.and_then(|id| symbols.get(&id).map(|bbox| (id, *bbox)))
{
attach_type_symbol_profile(&mut decoded, record, symbol.0, &symbol.1);
}
if let Some(cut) = cuts.get(&record.element_id) {
apply_column_join_cut(&mut decoded, cut);
}
decoded
})
.collect()
}
fn apply_column_join_cut(
decoded: &mut DecodedElement,
cut: &crate::element_record_column_cuts::ColumnCut,
) {
use crate::element_record_column_cuts as cuts;
let bbox = cut.bbox_feet;
let replacements: [(&str, f64); 6] = [
("m_locationX", (bbox[0] + bbox[3]) * 0.5),
("m_locationY", (bbox[1] + bbox[4]) * 0.5),
("m_locationZ", bbox[2]),
("m_bboxWidth", bbox[3] - bbox[0]),
("m_bboxDepth", bbox[4] - bbox[1]),
("m_bboxHeight", bbox[5] - bbox[2]),
];
for (name, value) in decoded.fields.iter_mut() {
if let Some((_, replacement)) = replacements
.iter()
.find(|(field, _)| *field == name.as_str())
{
*value = InstanceField::Float {
value: *replacement,
size: 8,
};
}
}
decoded.fields.push((
cuts::COLUMN_BODY_SOURCE_FIELD.into(),
InstanceField::String(cuts::COLUMN_BODY_JOIN_CUT.into()),
));
decoded.fields.push((
cuts::COLUMN_CUT_WALL_COUNT_FIELD.into(),
InstanceField::Integer {
value: cut.wall_count as i64,
signed: false,
size: 8,
},
));
}
fn attach_type_symbol_profile(
decoded: &mut DecodedElement,
record: &crate::partition_element_records::PartitionElementRecord,
symbol_id: u32,
symbol_bbox: &[f64; 6],
) {
let width = symbol_bbox[3] - symbol_bbox[0];
let depth = symbol_bbox[4] - symbol_bbox[1];
if !(width.is_finite() && depth.is_finite()) || width <= 0.0 || depth <= 0.0 {
return;
}
let (dx, dy, _) = record.extents_feet();
if (width - dx).abs() > TYPE_PROFILE_EPS_FEET || (depth - dy).abs() > TYPE_PROFILE_EPS_FEET {
return;
}
decoded.fields.push((
TYPE_SYMBOL_FIELD.into(),
InstanceField::ElementId {
tag: 0,
id: symbol_id,
},
));
decoded.fields.push((
TYPE_PROFILE_WIDTH_FIELD.into(),
InstanceField::Float {
value: width,
size: 8,
},
));
decoded.fields.push((
TYPE_PROFILE_DEPTH_FIELD.into(),
InstanceField::Float {
value: depth,
size: 8,
},
));
decoded.fields.push((
TYPE_PROFILE_SOURCE_FIELD.into(),
InstanceField::String(TYPE_PROFILE_SOURCE.into()),
));
}
pub fn wall_instances_from_records(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
level_ids: &BTreeSet<u32>,
) -> Vec<DecodedElement> {
use crate::element_record_wall_joins as joins;
let selected = select_instance_records(records);
let boxes: Vec<crate::partition_element_records::PartitionElementRecord> =
selected.values().cloned().collect();
let trims = joins::join_trims(&boxes);
selected
.values()
.map(|record| {
let mut decoded = element_record_decoded(record, "Wall", level_ids);
if let Some(trim) = trims.get(&record.element_id) {
apply_wall_join_trim(&mut decoded, record, trim);
}
decoded
})
.collect()
}
fn apply_wall_join_trim(
decoded: &mut DecodedElement,
record: &crate::partition_element_records::PartitionElementRecord,
trim: &crate::element_record_wall_joins::WallJoinTrim,
) {
use crate::element_record_wall_joins as joins;
let low = record.bbox_feet[trim.axis] + trim.start_feet;
let high = record.bbox_feet[trim.axis + 3] - trim.end_feet;
let (centre_field, extent_field) = if trim.axis == 0 {
("m_locationX", "m_bboxWidth")
} else {
("m_locationY", "m_bboxDepth")
};
for (name, value) in decoded.fields.iter_mut() {
if name == centre_field {
*value = InstanceField::Float {
value: (low + high) * 0.5,
size: 8,
};
} else if name == extent_field {
*value = InstanceField::Float {
value: high - low,
size: 8,
};
}
}
decoded.fields.push((
joins::WALL_BODY_SOURCE_FIELD.into(),
InstanceField::String(joins::WALL_BODY_JOIN_TRIMMED.into()),
));
decoded.fields.push((
joins::WALL_THICKNESS_FIELD.into(),
InstanceField::Float {
value: trim.thickness_feet,
size: 8,
},
));
decoded.fields.push((
joins::WALL_TRIM_START_FIELD.into(),
InstanceField::Float {
value: trim.start_feet,
size: 8,
},
));
decoded.fields.push((
joins::WALL_TRIM_END_FIELD.into(),
InstanceField::Float {
value: trim.end_feet,
size: 8,
},
));
}
fn apply_beam_column_trim(
decoded: &mut DecodedElement,
trim: &crate::element_record_beam_cuts::BeamColumnTrim,
) {
use crate::element_record_beam_cuts as cuts;
let (centre_field, extent_field) = if trim.axis == 0 {
("m_locationX", "m_bboxWidth")
} else {
("m_locationY", "m_bboxDepth")
};
for (name, value) in decoded.fields.iter_mut() {
if name == centre_field {
*value = InstanceField::Float {
value: (trim.low_feet + trim.high_feet) * 0.5,
size: 8,
};
} else if name == extent_field {
*value = InstanceField::Float {
value: trim.high_feet - trim.low_feet,
size: 8,
};
}
}
decoded.fields.push((
cuts::BEAM_BODY_SOURCE_FIELD.into(),
InstanceField::String(cuts::BEAM_BODY_COLUMN_CUT.into()),
));
}
fn resolve_base_constraint_levels(
elevations: &std::collections::BTreeMap<u32, f64>,
elements: &mut [&mut Vec<DecodedElement>],
) {
use crate::element_record_level_refs as refs;
if elevations.is_empty() {
return;
}
for element in elements.iter_mut().flat_map(|list| list.iter_mut()) {
let mut named = Vec::new();
let mut base = None;
for (name, value) in &element.fields {
match (name.as_str(), value) {
(refs::CONSTRAINT_LEVELS_FIELD, InstanceField::Vector(ids)) => {
named = ids
.iter()
.filter_map(|id| match id {
InstanceField::ElementId { id, .. } => Some(*id),
_ => None,
})
.collect();
}
("m_locationZ", InstanceField::Float { value, .. }) => base = Some(*value),
_ => {}
}
}
let Some(level) = base.and_then(|z| refs::base_constraint_level(&named, elevations, z))
else {
continue;
};
bind_record_level(element, level, refs::BASE_CONSTRAINT_SOURCE);
}
}
pub const FRAMING_TOP_LEVEL_SOURCE: &str = "structural_framing_top_level";
fn resolve_framing_top_levels(
elevations: &std::collections::BTreeMap<u32, f64>,
elements: &mut [&mut Vec<DecodedElement>],
) {
use crate::element_record_level_refs as refs;
const TOLERANCE_FEET: f64 = 1e-3;
for element in elements.iter_mut().flat_map(|list| list.iter_mut()) {
if element.class != "StructuralFraming" {
continue;
}
let float = |wanted: &str| {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == wanted => Some(*value),
_ => None,
})
};
let (Some(base), Some(height)) = (float("m_locationZ"), float("m_bboxHeight")) else {
continue;
};
let top = base + height;
let mut at_top = elevations
.iter()
.filter(|(_, elevation)| (**elevation - top).abs() <= TOLERANCE_FEET)
.map(|(id, _)| *id);
let (Some(level), None) = (at_top.next(), at_top.next()) else {
continue;
};
element.fields.retain(|(name, _)| {
name != refs::LEVEL_REFERENCE_FIELD && name != refs::LEVEL_BIND_SOURCE_FIELD
});
bind_record_level(element, level, FRAMING_TOP_LEVEL_SOURCE);
}
}
fn bind_record_level(element: &mut DecodedElement, level: u32, source: &str) {
use crate::element_record_level_refs as refs;
for (name, value) in element.fields.iter_mut() {
if name == "m_level_bound" {
*value = InstanceField::Bool(true);
}
}
element.fields.push((
refs::LEVEL_REFERENCE_FIELD.into(),
InstanceField::ElementId { tag: 0, id: level },
));
element.fields.push((
refs::LEVEL_BIND_SOURCE_FIELD.into(),
InstanceField::String(source.into()),
));
}
fn element_ids_field(element: &DecodedElement, field: &str) -> Vec<u32> {
element
.fields
.iter()
.find_map(|(name, value)| match value {
InstanceField::Vector(ids) if name == field => Some(
ids.iter()
.filter_map(|id| match id {
InstanceField::ElementId { id, .. } => Some(*id),
_ => None,
})
.collect(),
),
_ => None,
})
.unwrap_or_default()
}
fn record_level(element: &DecodedElement) -> Option<u32> {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::ElementId { id, .. }
if name == crate::element_record_level_refs::LEVEL_REFERENCE_FIELD =>
{
Some(*id)
}
_ => None,
})
}
fn resolve_hosted_levels(
rf: &mut RevitFile,
elevations: &std::collections::BTreeMap<u32, f64>,
elements: &mut [&mut Vec<DecodedElement>],
) {
use crate::element_record_level_refs as refs;
if elevations.is_empty() {
return;
}
let pending = elements.iter().flat_map(|list| list.iter()).any(|element| {
record_level(element).is_none()
&& !element_ids_field(element, refs::REFERENCES_FIELD).is_empty()
});
if !pending {
return;
}
let hosts: std::collections::BTreeMap<u32, Option<u32>> = elements
.iter()
.flat_map(|list| list.iter())
.filter(|element| matches!(element.class.as_str(), "Stair" | "Ramp"))
.filter_map(|element| Some((element.id?, record_level(element))))
.collect();
let declared = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return,
};
let level_ids: BTreeSet<u32> = elevations.keys().copied().collect();
let level_objects =
crate::partition_level_records::scan_level_objects(rf, &declared, &level_ids);
for element in elements.iter_mut().flat_map(|list| list.iter_mut()) {
if record_level(element).is_some() {
continue;
}
let references = element_ids_field(element, refs::REFERENCES_FIELD);
if references.is_empty() {
continue;
}
if element.class == "Railing" {
let named: Vec<&Option<u32>> =
references.iter().filter_map(|id| hosts.get(id)).collect();
if let [Some(level)] = named.as_slice() {
bind_record_level(element, *level, refs::HOST_SOURCE);
}
continue;
}
let carried: BTreeSet<u32> = references
.iter()
.filter_map(|id| level_objects.get(id).copied())
.collect();
let base = element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == "m_locationZ" => Some(*value),
_ => None,
});
let (Some(&level), 1) = (carried.first(), carried.len()) else {
continue;
};
if element.class == "PlumbingFixture" {
bind_record_level(element, level, refs::PLUMBING_LEVEL_OBJECT_SOURCE);
continue;
}
let Some(base) = base else {
continue;
};
if refs::level_at_or_below(elevations, base) == Some(level) {
bind_record_level(element, level, refs::LEVEL_OBJECT_SOURCE);
}
}
}
fn record_base_feet(element: &DecodedElement) -> Option<f64> {
element.fields.iter().find_map(|(name, value)| match value {
InstanceField::Float { value, .. } if name == "m_locationZ" => Some(*value),
_ => None,
})
}
fn resolve_base_at_level(
elevations: &std::collections::BTreeMap<u32, f64>,
elements: &mut [&mut Vec<DecodedElement>],
) {
use crate::element_record_level_refs as refs;
for element in elements.iter_mut().flat_map(|list| list.iter_mut()) {
if record_level(element).is_some() || !matches!(element.class.as_str(), "Stair" | "Ramp") {
continue;
}
let Some(base) = record_base_feet(element) else {
continue;
};
let mut at = elevations
.iter()
.filter(|(_, at)| (**at - base).abs() <= 1e-3);
if let (Some((&level, _)), None) = (at.next(), at.next()) {
bind_record_level(element, level, refs::BASE_AT_LEVEL_SOURCE);
}
}
}
pub const ELEVATION_PLACED_CLASSES: &[&str] = &[
"SlabEdge",
"LightingFixture",
"WallSweep",
"GenericModel",
"Hardscape",
"Stair",
];
fn resolve_remaining_levels(
elevations: &std::collections::BTreeMap<u32, f64>,
elements: &mut [&mut Vec<DecodedElement>],
) {
use crate::element_record_level_refs as refs;
if elevations.is_empty() {
return;
}
let leveled: std::collections::BTreeMap<u32, (String, u32)> = elements
.iter()
.flat_map(|list| list.iter())
.filter_map(|element| Some((element.id?, (element.class.clone(), record_level(element)?))))
.collect();
for element in elements.iter_mut().flat_map(|list| list.iter_mut()) {
if record_level(element).is_some() {
continue;
}
let references = element_ids_field(element, refs::REFERENCES_FIELD);
let same_class: BTreeSet<u32> = references
.iter()
.filter_map(|id| leveled.get(id))
.filter(|(class, _)| *class == element.class)
.map(|(_, level)| *level)
.collect();
let hosts = references
.iter()
.filter(|id| {
leveled
.get(id)
.is_some_and(|(class, _)| *class == element.class)
})
.count();
if let (1, Some(&level)) = (hosts, same_class.first()) {
bind_record_level(element, level, refs::SAME_CLASS_HOST_SOURCE);
continue;
}
if !ELEVATION_PLACED_CLASSES.contains(&element.class.as_str()) {
continue;
}
if let Some(level) =
record_base_feet(element).and_then(|base| refs::elevation_band_level(elevations, base))
{
bind_record_level(element, level, refs::BASE_ELEVATION_SOURCE);
}
}
}
fn element_record_decoded(
record: &crate::partition_element_records::PartitionElementRecord,
class: &str,
level_ids: &BTreeSet<u32>,
) -> DecodedElement {
let (cx, cy) = record.plan_centre_feet();
let (dx, dy, dz) = record.extents_feet();
let level_id =
crate::element_record_level_refs::unique_level_reference(&record.references, level_ids);
let mut fields = vec![
(
"m_locationX".into(),
InstanceField::Float { value: cx, size: 8 },
),
(
"m_locationY".into(),
InstanceField::Float { value: cy, size: 8 },
),
(
"m_locationZ".into(),
InstanceField::Float {
value: record.bbox_feet[2],
size: 8,
},
),
(
"m_bboxWidth".into(),
InstanceField::Float { value: dx, size: 8 },
),
(
"m_bboxDepth".into(),
InstanceField::Float { value: dy, size: 8 },
),
(
"m_bboxHeight".into(),
InstanceField::Float { value: dz, size: 8 },
),
(
"m_builtinCategory".into(),
InstanceField::Integer {
value: record.builtin_category,
signed: true,
size: 8,
},
),
(
"m_source_stream".into(),
InstanceField::String(record.stream.clone()),
),
(
"m_source_offset".into(),
InstanceField::Integer {
value: record.offset as i64,
signed: false,
size: 8,
},
),
(
CLASS_TAG_FIELD.into(),
InstanceField::Integer {
value: i64::from(record.class_tag),
signed: false,
size: 2,
},
),
(
"m_source".into(),
InstanceField::String("partition_element_record".into()),
),
(
"m_level_bound".into(),
InstanceField::Bool(level_id.is_some()),
),
];
if level_id.is_none() {
let named = crate::element_record_level_refs::named_levels(&record.references, level_ids);
if named.is_empty() {
let mut references: Vec<u32> = Vec::new();
for slot in &record.references {
if let Ok(id) = u32::try_from(*slot) {
if id != record.element_id && !references.contains(&id) {
references.push(id);
}
}
}
if !references.is_empty() {
fields.push((
crate::element_record_level_refs::REFERENCES_FIELD.into(),
InstanceField::Vector(
references
.iter()
.map(|&id| InstanceField::ElementId { tag: 0, id })
.collect(),
),
));
}
}
if named.len() == 2 {
fields.push((
crate::element_record_level_refs::CONSTRAINT_LEVELS_FIELD.into(),
InstanceField::Vector(
named
.iter()
.map(|&id| InstanceField::ElementId { tag: 0, id })
.collect(),
),
));
}
}
if let Some(id) = level_id {
fields.push((
crate::element_record_level_refs::LEVEL_REFERENCE_FIELD.into(),
InstanceField::ElementId { tag: 0, id },
));
}
let provenance = ElementProvenance::partition(
&record.stream,
record.offset,
"partition_element_record",
"partition_schema_mvp::element_category_record",
0.8,
Some("level_binding_unresolved"),
);
DecodedElement {
id: Some(record.element_id),
class: class.into(),
fields,
byte_range: record.offset
..record
.offset
.saturating_add(crate::partition_element_records::RECORD_MIN_LEN),
provenance,
}
}
fn levels_from_storeys_and_names(
walls: &[PartitionArcWall],
level_names: &[String],
) -> Vec<DecodedElement> {
let recovery = partition_arc_walls::recover_storeys_from_arc_walls(walls, level_names);
if recovery.storeys.is_empty() {
return level_names
.iter()
.enumerate()
.map(|(i, name)| level_decoded(name, None, i))
.collect();
}
recovery
.storeys
.into_iter()
.enumerate()
.map(|(i, s)| level_decoded(&s.name, Some(s.elevation_feet), i))
.collect()
}
fn level_decoded(name: &str, elevation: Option<f64>, index: usize) -> DecodedElement {
let mut fields = vec![
("m_name".into(), InstanceField::String(name.to_string())),
("m_isBuildingStory".into(), InstanceField::Bool(true)),
(
"m_source".into(),
InstanceField::String("partition_schema_mvp".into()),
),
];
if let Some(elev) = elevation {
fields.push((
"m_elevation".into(),
InstanceField::Float {
value: elev,
size: 8,
},
));
}
let confidence = if elevation.is_some() { 0.75 } else { 0.55 };
DecodedElement {
id: None,
class: "Level".into(),
fields,
byte_range: index..index,
provenance: ElementProvenance::partition(
"partition",
index,
"partition_schema_mvp",
"partition_schema_mvp::level",
confidence,
if elevation.is_none() {
Some("elevation_unknown")
} else {
None
},
),
}
}
fn materials_from_records(rf: &mut RevitFile, revit_version: u32) -> Option<Vec<DecodedElement>> {
use crate::partition_materials as pm;
let revit_2023 = revit_version == crate::partition_element_records_2023::REVIT_2023;
if !pm::MATERIALS_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) && !revit_2023 {
return None;
}
let declared = crate::elem_table::declared_ids(&crate::elem_table::parse_records(rf).ok()?);
let (ids, names) = if revit_2023 {
pm::scan_materials_2023(rf, &declared)
} else {
let ids: BTreeSet<u32> = crate::partition_type_records::scan_type_records(
rf,
revit_version,
crate::partition_type_records::OST_MATERIALS,
&declared,
)
.ok()?
.iter()
.map(|record| record.element_id)
.collect();
(
ids,
pm::scan_material_names(rf, revit_version, &declared).ok()?,
)
};
let appearances = pm::scan_material_appearances(rf, revit_version, &declared).ok()?;
Some(
ids.into_iter()
.filter_map(|id| {
let name = names.get(&id)?;
let mut fields = vec![("m_name".into(), InstanceField::String(name.clone()))];
if let Some(appearance) = appearances.get(&id) {
fields.push((
"m_color".into(),
InstanceField::Integer {
value: i64::from(appearance.color_packed()),
signed: false,
size: 4,
},
));
fields.push((
"m_transparency".into(),
InstanceField::Float {
value: f64::from(appearance.transparency),
size: 4,
},
));
}
fields.push((
"m_source".into(),
InstanceField::String("partition_materials".into()),
));
Some(DecodedElement {
id: Some(id),
class: "Material".into(),
fields,
byte_range: 0..0,
provenance: ElementProvenance::partition(
"partition",
0,
"partition_materials",
"partition_materials::material_record",
0.9,
None::<String>,
),
})
})
.collect(),
)
}
fn materials_from_names(name_set: &BTreeSet<(NameBucket, String)>) -> Vec<DecodedElement> {
name_set
.iter()
.filter(|(b, n)| *b == NameBucket::MaterialLike && is_strict_material_name(n))
.enumerate()
.map(|(i, (_, name))| {
DecodedElement {
id: None,
class: "Material".into(),
fields: vec![
("m_name".into(), InstanceField::String(name.clone())),
(
"m_source".into(),
InstanceField::String("partition_schema_mvp".into()),
),
],
byte_range: i..i,
provenance: Default::default(),
}
.with_provenance(ElementProvenance::partition(
"partition",
i,
"partition_schema_mvp",
"partition_schema_mvp::material_name",
0.6,
None::<String>,
))
})
.collect()
}
pub fn is_strict_material_name(s: &str) -> bool {
let t = s.trim();
if t.len() < 3 || t.len() > 48 {
return false;
}
if t.contains(':') || t.contains('/') || t.contains('\\') || t.contains('.') {
return false;
}
let lower = t.to_ascii_lowercase();
if lower.contains("schema") || lower.ends_with(" material") || lower.contains("default") {
return false;
}
classify_name(t) == Some(NameBucket::MaterialLike)
}
fn partition_streams_largest_first(rf: &mut RevitFile) -> Vec<String> {
let mut streams: Vec<(usize, String)> = rf
.stream_names()
.into_iter()
.filter(|s| s.starts_with("Partitions/"))
.filter_map(|s| {
let raw = rf.read_stream(&s).ok()?;
Some((raw.len(), s))
})
.collect();
streams.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
streams.into_iter().map(|(_, s)| s).collect()
}
fn rect_openings_from_partitions(
rf: &mut RevitFile,
revit_version: u32,
limits: WalkerLimits,
) -> Result<Vec<DecodedElement>> {
let mut out = Vec::new();
let opening_cap = limits.max_candidates.min(5_000);
let elem_ids: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => BTreeSet::new(),
};
for stream in partition_streams_largest_first(rf) {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let concat = inflated.bytes();
let offsets = ArcWallRectOpeningIndex::find_all_for_revit_version(revit_version, concat);
for off in offsets {
if out.len() >= opening_cap {
break;
}
let Ok(rec) = ArcWallRectOpeningIndex::decode(concat, off) else {
continue;
};
let a_in = elem_ids.contains(&rec.related_id_a);
let b_in = elem_ids.contains(&rec.related_id_b);
let host_confirmed = a_in;
out.push(DecodedElement {
id: None,
class: "ArcWallRectOpening".into(),
fields: vec![
(
"m_index".into(),
InstanceField::Integer {
value: i64::from(rec.index),
signed: false,
size: 4,
},
),
(
"m_related_id_a".into(),
InstanceField::ElementId {
tag: 0,
id: rec.related_id_a,
},
),
(
"m_related_id_b".into(),
InstanceField::ElementId {
tag: 0,
id: rec.related_id_b,
},
),
(
"m_related_id_a_in_elem_table".into(),
InstanceField::Bool(a_in),
),
(
"m_related_id_b_in_elem_table".into(),
InstanceField::Bool(b_in),
),
(
"m_host_id".into(),
InstanceField::ElementId {
tag: 0,
id: rec.related_id_a,
},
),
(
"m_host_elem_table_confirmed".into(),
InstanceField::Bool(host_confirmed),
),
(
"m_host_provenance".into(),
InstanceField::String(if host_confirmed {
"related_id_a_in_elem_table".into()
} else {
"related_id_a_unvalidated".into()
}),
),
(
"m_source_stream".into(),
InstanceField::String(stream.clone()),
),
(
"m_source_offset".into(),
InstanceField::Integer {
value: off as i64,
signed: false,
size: 8,
},
),
(
"m_source".into(),
InstanceField::String("partition_rect_opening_index".into()),
),
],
byte_range: off..off
.saturating_add(crate::rect_opening_index::OPENING_INDEX_STRIDE),
provenance: ElementProvenance::partition(
&stream,
off,
"partition_rect_opening_index",
"partition_schema_mvp::arcwall_rect_opening",
if host_confirmed { 0.7 } else { 0.55 },
if host_confirmed {
None
} else {
Some("related_id_a_unvalidated")
},
),
});
}
if !out.is_empty() {
break;
}
}
Ok(out)
}
#[cfg(test)]
mod tests {
fn columns_from_records_t(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
) -> Vec<DecodedElement> {
columns_from_records(records, &BTreeSet::new())
}
fn instances_from_records_t(
records: Vec<crate::partition_element_records::PartitionElementRecord>,
class: &str,
) -> Vec<DecodedElement> {
instances_from_records(records, class, &BTreeSet::new())
}
use super::*;
#[test]
fn strict_material_keeps_concrete_rejects_schema() {
assert!(is_strict_material_name("Concrete"));
assert!(is_strict_material_name("Masonry - Brick"));
assert!(!is_strict_material_name("HardwoodSchema"));
assert!(!is_strict_material_name("Glass/Glazing:Default:Glass"));
}
#[test]
fn level_decoded_projects_elevation() {
let el = level_decoded("Level 1", Some(10.0), 0);
assert_eq!(el.class, "Level");
let level = crate::elements::level::Level::from_decoded(&el);
assert_eq!(level.name.as_deref(), Some("Level 1"));
assert_eq!(level.elevation_feet, Some(10.0));
assert_eq!(level.is_building_story, Some(true));
}
fn column_record(
element_id: u32,
bbox_feet: [f64; 6],
) -> crate::partition_element_records::PartitionElementRecord {
element_record(
element_id,
crate::partition_element_records::OST_COLUMNS,
bbox_feet,
)
}
fn element_record(
element_id: u32,
builtin_category: i64,
bbox_feet: [f64; 6],
) -> crate::partition_element_records::PartitionElementRecord {
crate::partition_element_records::PartitionElementRecord {
stream: "Partitions/46".into(),
offset: element_id as usize,
element_id,
flags: 0x0141,
builtin_category,
container: crate::partition_element_records::CONTAINER_NONE,
placement_kind: crate::partition_element_records::PLACEMENT_KIND_INSTANCE,
class_tag: 0,
bbox_feet,
preceding_reference: None,
owner_reference: None,
references: Vec::new(),
id_from_enclosing_record: false,
design_option: None,
}
}
#[test]
fn selection_keeps_the_greatest_vertical_extent_for_one_id() {
let mut thin = element_record(
70433,
crate::partition_element_records::OST_FLOORS,
[20.0, 25.0, 30.667, 167.0, 114.0, 30.833],
);
thin.stream = "Partitions/46".into();
let mut full = element_record(
70433,
crate::partition_element_records::OST_FLOORS,
[20.0, 25.0, 29.833, 167.0, 114.0, 30.833],
);
full.stream = "Partitions/55".into();
for records in [
vec![thin.clone(), full.clone()],
vec![full.clone(), thin.clone()],
] {
let out = instances_from_records_t(records, "Floor");
assert_eq!(out.len(), 1);
let height =
out[0]
.fields
.iter()
.find_map(|(name, value)| match (name.as_str(), value) {
("m_bboxHeight", InstanceField::Float { value, .. }) => Some(*value),
_ => None,
});
assert!(
height.is_some_and(|h| (h - 1.0).abs() < 1e-3),
"expected the 1 ft frame, got {height:?}"
);
}
}
#[test]
fn selection_tie_breaks_on_the_first_stream_and_offset() {
let mut a = element_record(
20311,
crate::partition_element_records::OST_FLOORS,
[9.0, 16.0, 75.833, 177.0, 123.0, 76.0],
);
a.stream = "Partitions/51".into();
let mut b = a.clone();
b.stream = "Partitions/46".into();
let out = instances_from_records_t(vec![a, b], "Floor");
assert_eq!(out.len(), 1);
assert_eq!(out[0].id, Some(20311));
}
#[test]
fn selection_drops_type_symbol_envelopes() {
let mut symbol = column_record(5755, [-1.0, -1.0, 0.0, 1.0, 1.0, 9.0]);
symbol.placement_kind = crate::partition_element_records::PLACEMENT_KIND_SYMBOL;
let records = vec![
symbol,
column_record(20375, [23.0, 109.0, 76.0, 25.0, 111.0, 90.33]),
];
let out = columns_from_records_t(records);
assert_eq!(out.len(), 1);
assert_eq!(out[0].id, Some(20375));
assert_eq!(out[0].class, "Column");
}
#[test]
fn selection_drops_container_members_not_the_higher_id() {
let mut member = column_record(20375, [23.0, 109.0, 76.0, 25.0, 111.0, 90.33]);
member.container = 16_229;
let records = vec![
column_record(16347, [23.0, 109.0, 76.0, 25.0, 111.0, 91.0]),
member,
column_record(20376, [48.0, 109.0, 76.0, 50.0, 111.0, 90.33]),
];
let out = columns_from_records_t(records);
let ids: Vec<Option<u32>> = out.iter().map(|e| e.id).collect();
assert_eq!(ids, vec![Some(16347), Some(20376)]);
}
#[test]
fn selection_labels_each_category_with_its_own_class() {
for (category, class) in [
(crate::partition_element_records::OST_WALLS, "Wall"),
(crate::partition_element_records::OST_DOORS, "Door"),
(crate::partition_element_records::OST_WINDOWS, "Window"),
] {
let out = instances_from_records_t(
vec![element_record(
4242,
category,
[0.0, 0.0, 0.0, 4.0, 1.0, 8.0],
)],
class,
);
assert_eq!(out.len(), 1);
assert_eq!(out[0].class, class);
assert_eq!(out[0].id, Some(4242));
assert_eq!(
out[0].provenance.decoder.as_deref(),
Some("partition_schema_mvp::element_category_record")
);
}
}
#[test]
fn selection_keeps_one_record_per_element_id() {
let mut second = column_record(20375, [23.0, 109.0, 76.0, 25.0, 111.0, 90.33]);
second.offset = 999_999;
let out = columns_from_records_t(vec![
column_record(20375, [23.0, 109.0, 76.0, 25.0, 111.0, 90.33]),
second,
]);
assert_eq!(out.len(), 1);
}
#[test]
fn column_decoded_carries_plan_centre_and_extents() {
let out = columns_from_records_t(vec![column_record(
20375,
[23.0, 109.0, 76.0, 25.0, 111.0, 90.33],
)]);
let column = &out[0];
let field = |name: &str| {
column
.fields
.iter()
.find(|(n, _)| n == name)
.and_then(|(_, v)| match v {
InstanceField::Float { value, .. } => Some(*value),
_ => None,
})
};
assert_eq!(field("m_locationX"), Some(24.0));
assert_eq!(field("m_locationY"), Some(110.0));
assert_eq!(field("m_locationZ"), Some(76.0));
assert_eq!(field("m_bboxWidth"), Some(2.0));
assert_eq!(field("m_bboxDepth"), Some(2.0));
assert!((field("m_bboxHeight").unwrap() - 14.33).abs() < 1e-9);
assert!(column.provenance.confidence >= 0.55);
}
}