use crate::{Result, RevitFile};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
pub const PARTITION_ELEMENT_RECORD_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2024, 2025];
pub const OST_COLUMNS: i64 = -2_000_100;
pub const OST_WALLS: i64 = -2_000_011;
pub const OST_DOORS: i64 = -2_000_023;
pub const OST_WINDOWS: i64 = -2_000_014;
pub const OST_FLOORS: i64 = -2_000_032;
pub const OST_BUILDING_PAD: i64 = -2_001_263;
pub const OST_ROOMS: i64 = -2_000_160;
pub const OST_SKETCH_LINES: i64 = -2_000_045;
pub const OST_SHAFT_OPENING: i64 = -2_000_996;
pub const OST_LIGHTING_DEVICES: i64 = -2_008_087;
pub const OST_FIRE_ALARM_DEVICES: i64 = -2_008_085;
pub const OST_DATA_DEVICES: i64 = -2_008_083;
pub const OST_ELECTRICAL_FIXTURES: i64 = -2_001_060;
pub const OST_ELECTRICAL_EQUIPMENT: i64 = -2_001_040;
pub const OST_MECHANICAL_EQUIPMENT: i64 = -2_001_140;
pub const NESTED_COMPONENT_CATEGORIES: [i64; 3] = [
OST_LIGHTING_DEVICES,
OST_ELECTRICAL_FIXTURES,
OST_ELECTRICAL_EQUIPMENT,
];
pub const OST_FURNITURE: i64 = -2_000_080;
pub const OST_CASEWORK: i64 = -2_001_000;
pub const OST_PLUMBING_FIXTURES: i64 = -2_001_160;
pub const OST_SPECIALITY_EQUIPMENT: i64 = -2_001_350;
pub const OST_CEILINGS: i64 = -2_000_038;
pub const OST_CURTAIN_WALL_MULLIONS: i64 = -2_000_171;
pub const OST_CURTAIN_WALL_PANELS: i64 = -2_000_170;
pub const OST_STAIRS_RAILING: i64 = -2_000_126;
pub const OST_CORNICES: i64 = -2_000_181;
pub const OST_DUCT_CURVES: i64 = -2_008_000;
pub const OST_DUCT_FITTING: i64 = -2_008_010;
pub const OST_PIPE_CURVES: i64 = -2_008_044;
pub const OST_PIPE_FITTING: i64 = -2_008_049;
pub const OST_STRUCTURAL_FRAMING: i64 = -2_001_320;
pub const OST_STRUCTURAL_COLUMNS: i64 = -2_001_330;
pub const OST_STRUCTURAL_FOUNDATION: i64 = -2_001_300;
pub const OST_GENERIC_MODEL: i64 = -2_000_151;
pub const OST_LIGHTING_FIXTURES: i64 = -2_001_120;
pub const OST_DUCT_TERMINAL: i64 = -2_008_013;
pub const OST_FOOD_SERVICE_EQUIPMENT: i64 = -2_001_043;
pub const OST_PLANTING: i64 = -2_001_360;
pub const OST_PARKING: i64 = -2_001_180;
pub const OST_ENTOURAGE: i64 = -2_001_370;
pub const OST_HARDSCAPE: i64 = -2_001_036;
pub const OST_VERTICAL_CIRCULATION: i64 = -2_001_052;
pub const OST_RAMPS: i64 = -2_000_180;
pub const OST_STAIRS: i64 = -2_000_120;
pub const OST_STAIRS_RUNS: i64 = -2_000_919;
pub const OST_STAIRS_LANDINGS: i64 = -2_000_920;
pub const OST_STAIRS_STRINGER_CARRIAGE: i64 = -2_000_123;
pub const OST_ROOFS: i64 = -2_000_035;
pub const OST_EDGE_SLAB: i64 = -2_001_392;
pub const PRODUCT_RECORD_CATEGORIES: [(i64, &str); 38] = [
(OST_FURNITURE, "Furniture"),
(OST_CASEWORK, "Casework"),
(OST_PLUMBING_FIXTURES, "PlumbingFixture"),
(OST_SPECIALITY_EQUIPMENT, "SpecialtyEquipment"),
(OST_CEILINGS, "Ceiling"),
(OST_CURTAIN_WALL_MULLIONS, "CurtainWallMullion"),
(OST_CURTAIN_WALL_PANELS, "CurtainWallPanel"),
(OST_STAIRS_RAILING, "Railing"),
(OST_CORNICES, "WallSweep"),
(OST_DUCT_CURVES, "Duct"),
(OST_DUCT_FITTING, "DuctFitting"),
(OST_PIPE_CURVES, "Pipe"),
(OST_PIPE_FITTING, "PipeFitting"),
(OST_STRUCTURAL_FRAMING, "StructuralFraming"),
(OST_STRUCTURAL_COLUMNS, "StructuralColumn"),
(OST_STRUCTURAL_FOUNDATION, "StructuralFoundation"),
(OST_GENERIC_MODEL, "GenericModel"),
(OST_LIGHTING_FIXTURES, "LightingFixture"),
(OST_DUCT_TERMINAL, "DuctTerminal"),
(OST_FOOD_SERVICE_EQUIPMENT, "FoodServiceEquipment"),
(OST_PLANTING, "Planting"),
(OST_PARKING, "Parking"),
(OST_ENTOURAGE, "Entourage"),
(OST_HARDSCAPE, "Hardscape"),
(OST_VERTICAL_CIRCULATION, "VerticalCirculation"),
(OST_RAMPS, "Ramp"),
(OST_STAIRS, "Stair"),
(OST_STAIRS_RUNS, "StairsRun"),
(OST_STAIRS_LANDINGS, "StairsLanding"),
(OST_STAIRS_STRINGER_CARRIAGE, "StairsStringer"),
(OST_ROOFS, "Roof"),
(OST_EDGE_SLAB, "SlabEdge"),
(OST_LIGHTING_DEVICES, "LightingDevice"),
(OST_FIRE_ALARM_DEVICES, "FireAlarmDevice"),
(OST_DATA_DEVICES, "DataDevice"),
(OST_ELECTRICAL_FIXTURES, "ElectricalFixture"),
(OST_ELECTRICAL_EQUIPMENT, "ElectricalEquipment"),
(OST_MECHANICAL_EQUIPMENT, "MechanicalEquipment"),
];
pub const VOLUME_EPSILON_FEET: f64 = 1e-6;
pub const BUILTIN_CATEGORY_MIN: i64 = -2_100_000;
pub const BUILTIN_CATEGORY_MAX: i64 = -1_990_000;
pub const CATEGORY_OFFSET: usize = 0x12;
pub const CONTAINER_OFFSET: usize = 0x32;
pub const CONTAINER_NONE: u64 = u64::MAX;
pub const DESIGN_OPTION_OFFSET: usize = 0x2a;
pub const PLACEMENT_KIND_OFFSET: usize = 0x42;
pub const PLACEMENT_KIND_INSTANCE: u32 = 0xffff_ef7f;
pub const PLACEMENT_KIND_SYMBOL: u32 = 0xffff_8000;
pub const CLASS_TAG_OFFSET: usize = 0x4a;
pub const BBOX_MARKER_OFFSET: usize = 0x50;
pub const BBOX_OFFSET: usize = 0x58;
pub const RECORD_MIN_LEN: usize = BBOX_OFFSET + 48;
pub const REFERENCE_LIST_OFFSET: usize = RECORD_MIN_LEN;
pub const REFERENCE_LIST_MAX_ENTRIES: usize = 1024;
pub const BBOX_MARKER: [u8; 8] = [0x46, 0x01, 0xff, 0xff, 0xff, 0xff, 0xab, 0x05];
pub const BBOX_MARKER_2025: [u8; 8] = [0x59, 0x01, 0xff, 0xff, 0xff, 0xff, 0xd3, 0x05];
pub fn bbox_marker(revit_version: u32) -> Option<[u8; 8]> {
match revit_version {
2024 => Some(BBOX_MARKER),
2025 => Some(BBOX_MARKER_2025),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PartitionElementRecord {
pub stream: String,
pub offset: usize,
pub element_id: u32,
pub flags: u32,
pub builtin_category: i64,
pub container: u64,
pub placement_kind: u32,
#[serde(default)]
pub class_tag: u16,
pub bbox_feet: [f64; 6],
pub preceding_reference: Option<u32>,
pub owner_reference: Option<u32>,
#[serde(default)]
pub references: Vec<u64>,
#[serde(default)]
pub id_from_enclosing_record: bool,
#[serde(default)]
pub design_option: Option<u32>,
}
impl PartitionElementRecord {
pub fn footprint_key(&self) -> (i64, i64, i64) {
(
quantise(self.bbox_feet[0]),
quantise(self.bbox_feet[1]),
quantise(self.bbox_feet[2]),
)
}
pub fn plan_centre_feet(&self) -> (f64, f64) {
(
(self.bbox_feet[0] + self.bbox_feet[3]) * 0.5,
(self.bbox_feet[1] + self.bbox_feet[4]) * 0.5,
)
}
pub fn extents_feet(&self) -> (f64, f64, f64) {
(
self.bbox_feet[3] - self.bbox_feet[0],
self.bbox_feet[4] - self.bbox_feet[1],
self.bbox_feet[5] - self.bbox_feet[2],
)
}
pub fn has_volume(&self) -> bool {
let (dx, dy, dz) = self.extents_feet();
[dx, dy, dz]
.iter()
.all(|extent| extent.is_finite() && *extent > VOLUME_EPSILON_FEET)
}
pub fn is_family_local(&self) -> bool {
is_family_local_bbox(&self.bbox_feet)
}
pub fn container_element_id(&self) -> Option<u32> {
if self.container == CONTAINER_NONE || self.container > u64::from(u32::MAX) {
return None;
}
Some(self.container as u32)
}
pub fn is_container_member(&self) -> bool {
self.container != CONTAINER_NONE
}
pub fn is_placed_instance(&self) -> bool {
self.placement_kind == PLACEMENT_KIND_INSTANCE
}
pub fn is_type_symbol(&self) -> bool {
self.placement_kind == PLACEMENT_KIND_SYMBOL
}
pub fn is_exported_instance(&self) -> bool {
!self.is_container_member() && self.is_placed_instance()
}
pub fn type_symbol_reference(&self, symbols: &BTreeSet<u32>) -> Option<u32> {
let own = u64::from(self.element_id);
let index = self.references.iter().position(|slot| *slot == own)?;
self.references[..index]
.iter()
.rev()
.filter(|slot| **slot <= u64::from(u32::MAX))
.map(|slot| *slot as u32)
.find(|id| symbols.contains(id))
}
}
fn quantise(v: f64) -> i64 {
(v * 10_000.0).round() as i64
}
pub fn is_family_local_bbox(bbox: &[f64; 6]) -> bool {
(bbox[0] + bbox[3]).abs() < 1e-6 && (bbox[1] + bbox[4]).abs() < 1e-6
}
pub fn supports_revit_version(revit_version: u32) -> bool {
PARTITION_ELEMENT_RECORD_SUPPORTED_REVIT_VERSIONS.contains(&revit_version)
&& bbox_marker(revit_version).is_some()
}
fn read_u64(buf: &[u8], off: usize) -> Option<u64> {
buf.get(off..off + 8)
.map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
}
fn read_f64(buf: &[u8], off: usize) -> Option<f64> {
read_u64(buf, off).map(f64::from_bits)
}
fn read_u32(buf: &[u8], off: usize) -> Option<u32> {
buf.get(off..off.checked_add(4)?)
.map(|s| u32::from_le_bytes(s.try_into().expect("4 bytes")))
}
fn read_u16(buf: &[u8], off: usize) -> Option<u16> {
buf.get(off..off.checked_add(2)?)
.map(|s| u16::from_le_bytes(s.try_into().expect("2 bytes")))
}
pub fn decode_reference_list(buf: &[u8], at: usize) -> Option<Vec<u64>> {
let count = buf
.get(at..at + 4)
.map(|s| u32::from_le_bytes(s.try_into().expect("4 bytes")))? as usize;
if count == 0 || count > REFERENCE_LIST_MAX_ENTRIES {
return None;
}
let start = at.checked_add(4)?;
let end = start.checked_add(count.checked_mul(8)?)?;
if end > buf.len() {
return None;
}
let mut out = Vec::with_capacity(count);
for index in 0..count {
out.push(read_u64(buf, start + index * 8)?);
}
Some(out)
}
pub fn decode_owner_reference(buf: &[u8], at: usize) -> Option<u32> {
let first = decode_reference_list(buf, at)?;
let second_at = at
.checked_add(4)?
.checked_add(first.len().checked_mul(8)?)?;
let second = decode_reference_list(buf, second_at)?;
let last = *second.last()?;
if last == 0 || last > u64::from(u32::MAX) {
return None;
}
Some(last as u32)
}
fn slot_before(entries: &[u64], element_id: u32) -> Option<u32> {
let target = u64::from(element_id);
let index = entries.iter().rposition(|value| *value == target)?;
let previous = *entries.get(index.checked_sub(1)?)?;
if previous == 0 || previous > u64::from(u32::MAX) {
return None;
}
Some(previous as u32)
}
pub fn decode_at(
stream: &str,
buf: &[u8],
offset: usize,
declared_ids: &BTreeSet<u32>,
) -> Option<PartitionElementRecord> {
decode_at_with_marker(stream, buf, offset, declared_ids, &BBOX_MARKER)
}
pub fn decode_at_with_marker(
stream: &str,
buf: &[u8],
offset: usize,
declared_ids: &BTreeSet<u32>,
marker: &[u8; 8],
) -> Option<PartitionElementRecord> {
if offset.checked_add(RECORD_MIN_LEN)? > buf.len() {
return None;
}
let raw_id = read_u64(buf, offset)?;
if carries_no_element_id(raw_id) {
return None;
}
let element_id = raw_id as u32;
if !declared_ids.contains(&element_id) {
return None;
}
decode_frame_as(stream, buf, offset, element_id, marker)
}
pub fn decode_frame_as(
stream: &str,
buf: &[u8],
offset: usize,
element_id: u32,
marker: &[u8; 8],
) -> Option<PartitionElementRecord> {
if offset.checked_add(RECORD_MIN_LEN)? > buf.len() {
return None;
}
if u16::from_le_bytes([buf[offset + 0x10], buf[offset + 0x11]]) != 0 {
return None;
}
let builtin_category = read_u64(buf, offset + CATEGORY_OFFSET)? as i64;
if !(BUILTIN_CATEGORY_MIN..=BUILTIN_CATEGORY_MAX).contains(&builtin_category) {
return None;
}
if buf[offset + BBOX_MARKER_OFFSET..offset + BBOX_MARKER_OFFSET + 8] != marker[..] {
return None;
}
let mut bbox_feet = [0.0f64; 6];
for (index, slot) in bbox_feet.iter_mut().enumerate() {
let value = read_f64(buf, offset + BBOX_OFFSET + index * 8)?;
if !value.is_finite() {
return None;
}
*slot = value;
}
for axis in 0..3 {
if bbox_feet[axis + 3] < bbox_feet[axis] {
return None;
}
}
let flags = read_u64(buf, offset + 0x08).map(|v| (v & 0xffff_ffff) as u32)?;
let container = read_u64(buf, offset + CONTAINER_OFFSET)?;
let placement_kind =
read_u64(buf, offset + PLACEMENT_KIND_OFFSET).map(|v| (v & 0xffff_ffff) as u32)?;
let references = offset
.checked_add(REFERENCE_LIST_OFFSET)
.and_then(|at| decode_reference_list(buf, at))
.unwrap_or_default();
let preceding_reference = slot_before(&references, element_id);
let owner_reference = offset
.checked_add(REFERENCE_LIST_OFFSET)
.and_then(|at| decode_owner_reference(buf, at));
let design_option = frame_design_option(buf, offset);
let class_tag = u16::from_le_bytes([
buf[offset + CLASS_TAG_OFFSET],
buf[offset + CLASS_TAG_OFFSET + 1],
]);
Some(PartitionElementRecord {
stream: stream.to_string(),
offset,
element_id,
flags,
builtin_category,
container,
placement_kind,
class_tag,
bbox_feet,
preceding_reference,
owner_reference,
references,
id_from_enclosing_record: false,
design_option,
})
}
pub fn frame_design_option(buf: &[u8], offset: usize) -> Option<u32> {
read_u64(buf, offset.checked_add(DESIGN_OPTION_OFFSET)?)
.filter(|value| *value != u64::MAX)
.and_then(|value| u32::try_from(value).ok())
}
fn is_instance_frame(buf: &[u8], offset: usize) -> bool {
read_u64(buf, offset + CONTAINER_OFFSET) == Some(CONTAINER_NONE)
&& read_u64(buf, offset + PLACEMENT_KIND_OFFSET)
.is_some_and(|v| (v & 0xffff_ffff) as u32 == PLACEMENT_KIND_INSTANCE)
}
pub const PARTITION_RECORD_HEADER_LEN: usize = 16;
pub const PARTITION_RECORD_TRAILER_LEN: usize = 16;
pub fn record_prologue_constant(marker: &[u8; 8]) -> u16 {
u16::from_le_bytes([marker[6], marker[7]]).wrapping_sub(12)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PartitionRecordSpan {
pub start: usize,
pub end: usize,
pub element_id: u64,
}
pub fn partition_record_chain(buf: &[u8], marker: &[u8; 8]) -> Vec<PartitionRecordSpan> {
let constant = record_prologue_constant(marker);
let mut chain = Vec::new();
let mut at = 0usize;
while let Some(element_id) = read_u64(buf, at) {
let Some(size) = read_u32(buf, at + 8).map(|v| v as usize) else {
break;
};
if size < PARTITION_RECORD_HEADER_LEN || read_u16(buf, at + 0x0c) != Some(constant) {
break;
}
let Some(end) = at.checked_add(size) else {
break;
};
let flags = end.checked_add(8).and_then(|t| read_u32(buf, t));
let echo = end.checked_add(12).and_then(|t| read_u32(buf, t));
if !matches!(flags, Some(0 | 0x0100_0000)) || echo.map(|v| v as usize) != Some(size) {
break;
}
chain.push(PartitionRecordSpan {
start: at,
end,
element_id,
});
at = end + PARTITION_RECORD_TRAILER_LEN;
}
chain
}
pub fn enclosing_record(
chain: &[PartitionRecordSpan],
offset: usize,
) -> Option<&PartitionRecordSpan> {
let index = chain.partition_point(|span| span.start <= offset);
let span = chain.get(index.checked_sub(1)?)?;
(offset < span.end).then_some(span)
}
fn frame_has_volume(buf: &[u8], offset: usize) -> bool {
let mut bbox = [0.0f64; 6];
for (index, slot) in bbox.iter_mut().enumerate() {
match read_f64(buf, offset + BBOX_OFFSET + index * 8) {
Some(value) if value.is_finite() => *slot = value,
_ => return false,
}
}
(0..3).all(|axis| bbox[axis + 3] - bbox[axis] > VOLUME_EPSILON_FEET)
}
pub fn assign_second_prologue_ids(
buf: &[u8],
marker: &[u8; 8],
declared_ids: &BTreeSet<u32>,
) -> BTreeMap<usize, u32> {
let chain = partition_record_chain(buf, marker);
let mut out = BTreeMap::new();
for hit in memchr::memmem::find_iter(buf, marker) {
let Some(offset) = hit.checked_sub(BBOX_MARKER_OFFSET) else {
continue;
};
if !is_second_prologue_instance(buf, offset) {
continue;
}
let Some(span) = enclosing_record(&chain, offset) else {
continue;
};
if let Ok(id) = u32::try_from(span.element_id) {
if declared_ids.contains(&id) {
out.insert(offset, id);
}
}
}
out
}
pub fn carries_no_element_id(raw: u64) -> bool {
raw == 0 || raw >= u64::from(u32::MAX)
}
fn is_second_prologue_instance(buf: &[u8], offset: usize) -> bool {
read_u64(buf, offset + CATEGORY_OFFSET)
.is_some_and(|v| (BUILTIN_CATEGORY_MIN..=BUILTIN_CATEGORY_MAX).contains(&(v as i64)))
&& read_u64(buf, offset).is_some_and(carries_no_element_id)
&& is_instance_frame(buf, offset)
}
pub fn compute_second_prologue_ids(rf: &mut RevitFile) -> SecondPrologueIds {
let mut out = SecondPrologueIds::new();
let Ok(version) = rf.basic_file_info().map(|b| b.version) else {
return out;
};
let Some(marker) = bbox_marker(version).filter(|_| supports_revit_version(version)) else {
return out;
};
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return out,
};
if declared.is_empty() {
return out;
}
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let assigned = assign_second_prologue_ids(inflated.bytes(), &marker, &declared);
if !assigned.is_empty() {
out.insert(stream, assigned);
}
}
out
}
pub type SecondPrologueIds = BTreeMap<String, BTreeMap<usize, u32>>;
pub fn find_category_records_assigned(
stream: &str,
buf: &[u8],
builtin_category: i64,
declared_ids: &BTreeSet<u32>,
marker: &[u8; 8],
assigned: &BTreeMap<usize, u32>,
) -> Vec<PartitionElementRecord> {
let mut out =
find_category_records_with_marker(stream, buf, builtin_category, declared_ids, marker);
for (&offset, &element_id) in assigned {
if read_u64(buf, offset + CATEGORY_OFFSET).map(|v| v as i64) != Some(builtin_category) {
continue;
}
if let Some(mut record) = decode_frame_as(stream, buf, offset, element_id, marker) {
record.id_from_enclosing_record = true;
out.push(record);
}
}
out.sort_by_key(|record| record.offset);
out
}
pub fn find_categories_records_assigned(
stream: &str,
buf: &[u8],
builtin_categories: &[i64],
declared_ids: &BTreeSet<u32>,
marker: &[u8; 8],
assigned: &BTreeMap<usize, u32>,
) -> Vec<Vec<PartitionElementRecord>> {
let mut out: Vec<Vec<PartitionElementRecord>> = vec![Vec::new(); builtin_categories.len()];
if buf.len() >= RECORD_MIN_LEN {
let mut groups: BTreeMap<[u8; 6], Vec<(usize, [u8; 2])>> = BTreeMap::new();
for (index, category) in builtin_categories.iter().enumerate() {
let bytes = (*category as u64).to_le_bytes();
let suffix: [u8; 6] = bytes[2..].try_into().expect("6 bytes");
groups
.entry(suffix)
.or_default()
.push((index, [bytes[0], bytes[1]]));
}
for (suffix, members) in &groups {
let finder = memchr::memmem::Finder::new(suffix);
let mut cursor = 2usize;
while cursor + suffix.len() <= buf.len() {
let Some(found) = finder.find(&buf[cursor..]) else {
break;
};
let at = cursor + found - 2;
cursor += found + 1;
let low = [buf[at], buf[at + 1]];
for (index, _) in members.iter().filter(|(_, bytes)| *bytes == low) {
if at >= CATEGORY_OFFSET {
if let Some(record) = decode_at_with_marker(
stream,
buf,
at - CATEGORY_OFFSET,
declared_ids,
marker,
) {
out[*index].push(record);
}
}
}
}
}
}
for (&offset, &element_id) in assigned {
let Some(category) = read_u64(buf, offset + CATEGORY_OFFSET).map(|v| v as i64) else {
continue;
};
for (index, wanted) in builtin_categories.iter().enumerate() {
if *wanted != category {
continue;
}
if let Some(mut record) = decode_frame_as(stream, buf, offset, element_id, marker) {
record.id_from_enclosing_record = true;
out[index].push(record);
}
}
}
for records in &mut out {
records.sort_by_key(|record| record.offset);
}
out
}
pub fn find_category_records(
stream: &str,
buf: &[u8],
builtin_category: i64,
declared_ids: &BTreeSet<u32>,
) -> Vec<PartitionElementRecord> {
find_category_records_with_marker(stream, buf, builtin_category, declared_ids, &BBOX_MARKER)
}
pub fn find_category_records_with_marker(
stream: &str,
buf: &[u8],
builtin_category: i64,
declared_ids: &BTreeSet<u32>,
marker: &[u8; 8],
) -> Vec<PartitionElementRecord> {
let needle = (builtin_category as u64).to_le_bytes();
let mut out = Vec::new();
if buf.len() < RECORD_MIN_LEN {
return out;
}
let mut cursor = 0usize;
while cursor + 8 <= buf.len() {
let Some(found) = find_subslice(&buf[cursor..], &needle) else {
break;
};
let hit = cursor + found;
if hit >= CATEGORY_OFFSET {
if let Some(record) =
decode_at_with_marker(stream, buf, hit - CATEGORY_OFFSET, declared_ids, marker)
{
out.push(record);
}
}
cursor = hit + 1;
}
out
}
fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
memchr::memmem::find(haystack, needle)
}
pub const RECOVERED_CATEGORIES: [(i64, &str); 45] = [
(OST_WALLS, "Wall"),
(OST_DOORS, "Door"),
(OST_WINDOWS, "Window"),
(OST_COLUMNS, "Column"),
(OST_FLOORS, "Floor"),
(OST_BUILDING_PAD, "BuildingPad"),
(OST_ROOMS, "Room"),
(OST_FURNITURE, "Furniture"),
(OST_CASEWORK, "Casework"),
(OST_PLUMBING_FIXTURES, "PlumbingFixture"),
(OST_SPECIALITY_EQUIPMENT, "SpecialtyEquipment"),
(OST_CEILINGS, "Ceiling"),
(OST_CURTAIN_WALL_MULLIONS, "CurtainWallMullion"),
(OST_CURTAIN_WALL_PANELS, "CurtainWallPanel"),
(OST_STAIRS_RAILING, "Railing"),
(OST_CORNICES, "WallSweep"),
(OST_DUCT_CURVES, "Duct"),
(OST_DUCT_FITTING, "DuctFitting"),
(OST_PIPE_CURVES, "Pipe"),
(OST_PIPE_FITTING, "PipeFitting"),
(OST_STRUCTURAL_FRAMING, "StructuralFraming"),
(OST_STRUCTURAL_COLUMNS, "StructuralColumn"),
(OST_STRUCTURAL_FOUNDATION, "StructuralFoundation"),
(OST_GENERIC_MODEL, "GenericModel"),
(OST_LIGHTING_FIXTURES, "LightingFixture"),
(OST_DUCT_TERMINAL, "DuctTerminal"),
(OST_FOOD_SERVICE_EQUIPMENT, "FoodServiceEquipment"),
(OST_PLANTING, "Planting"),
(OST_PARKING, "Parking"),
(OST_ENTOURAGE, "Entourage"),
(OST_HARDSCAPE, "Hardscape"),
(OST_VERTICAL_CIRCULATION, "VerticalCirculation"),
(OST_RAMPS, "Ramp"),
(OST_STAIRS, "Stair"),
(OST_STAIRS_RUNS, "StairsRun"),
(OST_STAIRS_LANDINGS, "StairsLanding"),
(OST_STAIRS_STRINGER_CARRIAGE, "StairsStringer"),
(OST_ROOFS, "Roof"),
(OST_EDGE_SLAB, "SlabEdge"),
(OST_LIGHTING_DEVICES, "LightingDevice"),
(OST_FIRE_ALARM_DEVICES, "FireAlarmDevice"),
(OST_DATA_DEVICES, "DataDevice"),
(OST_ELECTRICAL_FIXTURES, "ElectricalFixture"),
(OST_ELECTRICAL_EQUIPMENT, "ElectricalEquipment"),
(OST_MECHANICAL_EQUIPMENT, "MechanicalEquipment"),
];
pub fn count_unattributed_frames(buf: &[u8], categories: &[i64]) -> BTreeMap<i64, usize> {
count_unattributed_frames_with_marker(buf, categories, &BBOX_MARKER)
}
pub fn count_unattributed_frames_with_marker(
buf: &[u8],
categories: &[i64],
marker: &[u8; 8],
) -> BTreeMap<i64, usize> {
count_unattributed_frames_excluding(buf, categories, marker, &BTreeMap::new())
}
pub fn count_unattributed_frames_excluding(
buf: &[u8],
categories: &[i64],
marker: &[u8; 8],
assigned: &BTreeMap<usize, u32>,
) -> BTreeMap<i64, usize> {
let mut counts = BTreeMap::new();
let chain = partition_record_chain(buf, marker);
for hit in memchr::memmem::find_iter(buf, marker) {
let Some(offset) = hit.checked_sub(BBOX_MARKER_OFFSET) else {
continue;
};
if assigned.contains_key(&offset) || enclosing_record(&chain, offset).is_none() {
continue;
}
let Some(category) = read_u64(buf, offset + CATEGORY_OFFSET).map(|v| v as i64) else {
continue;
};
if !categories.contains(&category) {
continue;
}
if !read_u64(buf, offset).is_some_and(carries_no_element_id) {
continue;
}
if is_instance_frame(buf, offset) && frame_has_volume(buf, offset) {
*counts.entry(category).or_insert(0) += 1;
}
}
counts
}
pub fn scan_unattributed_frames(
rf: &mut RevitFile,
revit_version: u32,
) -> Result<BTreeMap<String, usize>> {
let mut by_class = BTreeMap::new();
let Some(marker) = bbox_marker(revit_version).filter(|_| supports_revit_version(revit_version))
else {
return Ok(by_class);
};
let categories: Vec<i64> = RECOVERED_CATEGORIES.iter().map(|(c, _)| *c).collect();
let assigned_all = rf.second_prologue_ids();
let none = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let assigned = assigned_all.get(&stream).unwrap_or(&none);
for (category, count) in
count_unattributed_frames_excluding(inflated.bytes(), &categories, &marker, assigned)
{
if let Some((_, class)) = RECOVERED_CATEGORIES.iter().find(|(c, _)| *c == category) {
*by_class.entry((*class).to_string()).or_insert(0) += count;
}
}
}
Ok(by_class)
}
pub fn scan_volumeless_instances(
rf: &mut RevitFile,
revit_version: u32,
) -> Result<BTreeMap<String, usize>> {
let mut by_class = BTreeMap::new();
if !supports_revit_version(revit_version) {
return Ok(by_class);
}
let declared: BTreeSet<u32> = match crate::elem_table::parse_records(rf) {
Ok(records) => crate::elem_table::declared_ids(&records),
Err(_) => return Ok(by_class),
};
let categories: Vec<i64> = RECOVERED_CATEGORIES.iter().map(|(c, _)| *c).collect();
let records = scan_category_records_multi(rf, revit_version, &categories, &declared)?;
let options = rf.design_options();
let mut with_volume: BTreeSet<u32> = BTreeSet::new();
let mut without: BTreeMap<u32, i64> = BTreeMap::new();
for record in records
.iter()
.filter(|r| r.is_exported_instance() && !options.excludes(r))
{
if record.has_volume() {
with_volume.insert(record.element_id);
} else {
without.insert(record.element_id, record.builtin_category);
}
}
for (id, category) in without {
if with_volume.contains(&id) {
continue;
}
if let Some((_, class)) = RECOVERED_CATEGORIES.iter().find(|(c, _)| *c == category) {
*by_class.entry((*class).to_string()).or_insert(0) += 1;
}
}
Ok(by_class)
}
pub fn scan_category_records(
rf: &mut RevitFile,
revit_version: u32,
builtin_category: i64,
declared_ids: &BTreeSet<u32>,
) -> Result<Vec<PartitionElementRecord>> {
scan_category_records_multi(rf, revit_version, &[builtin_category], declared_ids)
}
pub fn scan_category_records_multi(
rf: &mut RevitFile,
revit_version: u32,
builtin_categories: &[i64],
declared_ids: &BTreeSet<u32>,
) -> Result<Vec<PartitionElementRecord>> {
let Some(marker) = bbox_marker(revit_version).filter(|_| supports_revit_version(revit_version))
else {
return Ok(Vec::new());
};
if declared_ids.is_empty() {
return Ok(Vec::new());
}
let streams = rf.partition_stream_names();
let assigned_all = rf.second_prologue_ids();
let none = BTreeMap::new();
let mut per_category: Vec<Vec<PartitionElementRecord>> =
vec![Vec::new(); builtin_categories.len()];
for stream in streams {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let assigned = assigned_all.get(&stream).unwrap_or(&none);
for (index, records) in find_categories_records_assigned(
&stream,
inflated.bytes(),
builtin_categories,
declared_ids,
&marker,
assigned,
)
.into_iter()
.enumerate()
{
per_category[index].extend(records);
}
}
Ok(per_category.into_iter().flatten().collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_record(element_id: u32, category: i64, bbox: [f64; 6]) -> Vec<u8> {
let mut buf = vec![0xffu8; RECORD_MIN_LEN];
buf[0..8].copy_from_slice(&u64::from(element_id).to_le_bytes());
buf[8..12].copy_from_slice(&0x0141u32.to_le_bytes());
buf[12..16].copy_from_slice(&0x059fu32.to_le_bytes());
buf[16..18].copy_from_slice(&0u16.to_le_bytes());
buf[CATEGORY_OFFSET..CATEGORY_OFFSET + 8].copy_from_slice(&(category as u64).to_le_bytes());
buf[PLACEMENT_KIND_OFFSET..PLACEMENT_KIND_OFFSET + 4]
.copy_from_slice(&PLACEMENT_KIND_INSTANCE.to_le_bytes());
buf[BBOX_MARKER_OFFSET..BBOX_MARKER_OFFSET + 8].copy_from_slice(&BBOX_MARKER);
for (index, value) in bbox.iter().enumerate() {
let at = BBOX_OFFSET + index * 8;
buf[at..at + 8].copy_from_slice(&value.to_le_bytes());
}
buf
}
fn declared(ids: &[u32]) -> BTreeSet<u32> {
ids.iter().copied().collect()
}
#[test]
fn bbox_marker_is_chosen_per_release() {
assert_eq!(bbox_marker(2024), Some(BBOX_MARKER));
assert_eq!(bbox_marker(2025), Some(BBOX_MARKER_2025));
for unproven in [2023, 2026, 2027] {
assert_eq!(bbox_marker(unproven), None);
assert!(!supports_revit_version(unproven));
}
assert_eq!(
u16::from_le_bytes([BBOX_MARKER[6], BBOX_MARKER[7]]),
1411 + 40
);
assert_eq!(
u16::from_le_bytes([BBOX_MARKER_2025[6], BBOX_MARKER_2025[7]]),
1451 + 40
);
let mut buf = synth_record(415_431, OST_WALLS, [0.0, 0.0, 0.0, 0.4, 26.2, 11.5]);
buf[BBOX_MARKER_OFFSET..BBOX_MARKER_OFFSET + 8].copy_from_slice(&BBOX_MARKER_2025);
let ids = declared(&[415_431]);
let record = decode_at_with_marker("Partitions/68", &buf, 0, &ids, &BBOX_MARKER_2025)
.expect("2025 record decodes with the 2025 marker");
assert_eq!(record.element_id, 415_431);
assert!(decode_at("Partitions/68", &buf, 0, &ids).is_none());
assert_eq!(
find_category_records_with_marker(
"Partitions/68",
&buf,
OST_WALLS,
&ids,
&BBOX_MARKER_2025
)
.len(),
1
);
}
#[test]
fn unattributed_frames_are_counted_per_category() {
let bbox = [0.0, 0.0, 0.0, 1.0, 18.9, 24.7];
let attributed = synth_record(22805, OST_WALLS, bbox);
let mut all_ff = synth_record(1, OST_WALLS, bbox);
all_ff[0..8].fill(0xff);
let mut high_word = synth_record(1, OST_DOORS, bbox);
high_word[0..8].copy_from_slice(&0x0000_0006_ffff_ffff_u64.to_le_bytes());
let mut outside = synth_record(1, OST_SKETCH_LINES, bbox);
outside[0..8].fill(0xff);
let mut contained = synth_record(1, OST_WALLS, bbox);
contained[0..8].fill(0xff);
contained[CONTAINER_OFFSET..CONTAINER_OFFSET + 8].copy_from_slice(&20274u64.to_le_bytes());
let mut symbol = synth_record(1, OST_DOORS, bbox);
symbol[0..8].fill(0xff);
symbol[PLACEMENT_KIND_OFFSET..PLACEMENT_KIND_OFFSET + 4]
.copy_from_slice(&PLACEMENT_KIND_SYMBOL.to_le_bytes());
let first = as_record(attributed);
let buf = [
first.clone(),
nested(9001, &all_ff),
nested(9002, &high_word),
nested(9003, &outside),
nested(9004, &contained),
nested(9005, &symbol),
]
.concat();
let counts = count_unattributed_frames(&buf, &[OST_WALLS, OST_DOORS]);
assert_eq!(counts, BTreeMap::from([(OST_WALLS, 1), (OST_DOORS, 1)]));
assert!(count_unattributed_frames(&first, &[OST_WALLS]).is_empty());
}
fn with_reference_list(mut buf: Vec<u8>, entries: &[u64]) -> Vec<u8> {
buf.truncate(REFERENCE_LIST_OFFSET);
buf.extend_from_slice(&(entries.len() as u32).to_le_bytes());
for entry in entries {
buf.extend_from_slice(&entry.to_le_bytes());
}
buf
}
#[test]
fn decodes_a_well_formed_column_record() {
let bbox = [135.5, 79.0, 0.0, 137.5, 81.0, 30.333];
let buf = synth_record(22805, OST_COLUMNS, bbox);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[22805])).expect("decodes");
assert_eq!(record.element_id, 22805);
assert_eq!(record.builtin_category, OST_COLUMNS);
assert_eq!(record.flags, 0x0141);
let (dx, dy, _) = record.extents_feet();
assert!((dx - 2.0).abs() < 1e-9);
assert!((dy - 2.0).abs() < 1e-9);
assert!(!record.is_family_local());
}
#[test]
fn rejects_ids_absent_from_elem_table() {
let buf = synth_record(22805, OST_COLUMNS, [0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
assert!(decode_at("Partitions/46", &buf, 0, &declared(&[9999])).is_none());
}
#[test]
fn rejects_missing_bbox_marker() {
let mut buf = synth_record(22805, OST_COLUMNS, [0.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
buf[BBOX_MARKER_OFFSET] = 0x00;
assert!(decode_at("Partitions/46", &buf, 0, &declared(&[22805])).is_none());
}
#[test]
fn rejects_inverted_bounding_box() {
let buf = synth_record(22805, OST_COLUMNS, [10.0, 0.0, 0.0, 1.0, 1.0, 1.0]);
assert!(decode_at("Partitions/46", &buf, 0, &declared(&[22805])).is_none());
}
#[test]
fn family_local_bbox_is_recognised() {
let buf = synth_record(5755, OST_COLUMNS, [-1.0, -1.0, 0.0, 1.0, 1.0, 9.0]);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[5755])).expect("decodes");
assert!(record.is_family_local());
}
#[test]
fn placed_standalone_record_is_an_exported_instance() {
let buf = synth_record(22805, OST_COLUMNS, [135.5, 79.0, 0.0, 137.5, 81.0, 30.3]);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[22805])).expect("decodes");
assert_eq!(record.container, CONTAINER_NONE);
assert_eq!(record.placement_kind, PLACEMENT_KIND_INSTANCE);
assert!(record.container_element_id().is_none());
assert!(!record.is_container_member());
assert!(record.is_placed_instance());
assert!(!record.is_type_symbol());
assert!(record.is_exported_instance());
}
#[test]
fn container_member_is_not_an_exported_instance() {
let mut buf = synth_record(16347, OST_COLUMNS, [23.0, 109.0, 76.0, 25.0, 111.0, 91.0]);
buf[CONTAINER_OFFSET..CONTAINER_OFFSET + 8].copy_from_slice(&16_229u64.to_le_bytes());
let record = decode_at("Partitions/46", &buf, 0, &declared(&[16347])).expect("decodes");
assert_eq!(record.container_element_id(), Some(16_229));
assert!(record.is_container_member());
assert!(record.is_placed_instance());
assert!(!record.is_exported_instance());
}
#[test]
fn type_symbol_is_not_an_exported_instance() {
let mut buf = synth_record(17331, OST_DOORS, [-1.749, -0.332, 0.0, 1.749, 3.251, 8.249]);
buf[PLACEMENT_KIND_OFFSET..PLACEMENT_KIND_OFFSET + 4]
.copy_from_slice(&PLACEMENT_KIND_SYMBOL.to_le_bytes());
let record = decode_at("Partitions/46", &buf, 0, &declared(&[17331])).expect("decodes");
assert!(!record.is_family_local(), "proxy misses this symbol");
assert!(record.is_type_symbol());
assert!(!record.is_placed_instance());
assert!(!record.is_exported_instance());
}
#[test]
fn out_of_range_container_reference_is_not_an_element_id() {
let mut buf = synth_record(22805, OST_COLUMNS, [1.0, 2.0, 0.0, 3.0, 4.0, 5.0]);
buf[CONTAINER_OFFSET..CONTAINER_OFFSET + 8]
.copy_from_slice(&0x0001_0000_0000u64.to_le_bytes());
let record = decode_at("Partitions/46", &buf, 0, &declared(&[22805])).expect("decodes");
assert!(record.is_container_member());
assert_eq!(record.container_element_id(), None);
assert!(!record.is_exported_instance());
}
#[test]
fn scan_finds_records_at_any_offset() {
let bbox = [1.0, 2.0, 3.0, 3.0, 4.0, 13.0];
let mut buf = vec![0u8; 37];
buf.extend(synth_record(4242, OST_COLUMNS, bbox));
buf.extend(vec![0u8; 11]);
let found = find_category_records("Partitions/46", &buf, OST_COLUMNS, &declared(&[4242]));
assert_eq!(found.len(), 1);
assert_eq!(found[0].element_id, 4242);
assert_eq!(found[0].offset, 37);
}
#[test]
fn unsupported_release_yields_nothing() {
assert!(!supports_revit_version(2023));
assert!(supports_revit_version(2024));
}
#[test]
fn reference_list_decodes_its_counted_slots() {
let buf = with_reference_list(
synth_record(25947, OST_DOORS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, 20274, 23756, 24301, 25488, 25947],
);
let entries = decode_reference_list(&buf, REFERENCE_LIST_OFFSET).expect("decodes");
assert_eq!(entries, vec![3, 20274, 23756, 24301, 25488, 25947]);
}
#[test]
fn reference_list_rejects_a_length_that_runs_past_the_buffer() {
let mut buf = with_reference_list(
synth_record(25947, OST_DOORS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, 25488, 25947],
);
buf[REFERENCE_LIST_OFFSET..REFERENCE_LIST_OFFSET + 4].copy_from_slice(&64u32.to_le_bytes());
assert!(decode_reference_list(&buf, REFERENCE_LIST_OFFSET).is_none());
}
#[test]
fn reference_list_rejects_an_unbounded_length() {
let mut buf = with_reference_list(
synth_record(25947, OST_DOORS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, 25488, 25947],
);
buf[REFERENCE_LIST_OFFSET..REFERENCE_LIST_OFFSET + 4]
.copy_from_slice(&(REFERENCE_LIST_MAX_ENTRIES as u32 + 1).to_le_bytes());
assert!(decode_reference_list(&buf, REFERENCE_LIST_OFFSET).is_none());
}
#[test]
fn preceding_reference_is_the_slot_before_the_records_own_id() {
let buf = with_reference_list(
synth_record(25947, OST_DOORS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, 20274, 23756, 24301, 25488, 25947],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[25947])).expect("decodes");
assert_eq!(record.preceding_reference, Some(25_488));
}
#[test]
fn preceding_reference_ignores_slots_after_the_records_own_id() {
let buf = with_reference_list(
synth_record(20810, OST_DOORS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, 17333, 20308, 20798, 20810, 132_750],
);
let record = decode_at("Partitions/59", &buf, 0, &declared(&[20810])).expect("decodes");
assert_eq!(record.preceding_reference, Some(20_798));
}
#[test]
fn preceding_reference_is_absent_without_a_self_slot() {
let buf = with_reference_list(
synth_record(22805, OST_COLUMNS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, 1435, 17337],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[22805])).expect("decodes");
assert_eq!(record.preceding_reference, None);
}
#[test]
fn preceding_reference_is_absent_when_the_record_leads_its_list() {
let buf = with_reference_list(
synth_record(22805, OST_COLUMNS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[22805, 1435],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[22805])).expect("decodes");
assert_eq!(record.preceding_reference, None);
}
#[test]
fn preceding_reference_rejects_a_slot_outside_the_element_id_range() {
let buf = with_reference_list(
synth_record(22805, OST_COLUMNS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, u64::from(u32::MAX) + 1, 22805],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[22805])).expect("decodes");
assert_eq!(record.preceding_reference, None);
}
#[test]
fn owner_reference_is_the_last_slot_of_the_second_list() {
let mut buf = with_reference_list(
synth_record(
20310,
OST_SKETCH_LINES,
[20.0, 25.0, 46.0, 20.0, 114.0, 46.0],
),
&[20309, 20310, 20312, 20313, 20315],
);
buf.extend_from_slice(&2u32.to_le_bytes());
buf.extend_from_slice(&20_308u64.to_le_bytes());
buf.extend_from_slice(&20_311u64.to_le_bytes());
let record = decode_at("Partitions/46", &buf, 0, &declared(&[20310])).expect("decodes");
assert_eq!(record.builtin_category, OST_SKETCH_LINES);
assert_eq!(record.owner_reference, Some(20_311));
}
#[test]
fn owner_reference_is_absent_without_a_second_list() {
let buf = with_reference_list(
synth_record(
20310,
OST_SKETCH_LINES,
[20.0, 25.0, 46.0, 20.0, 114.0, 46.0],
),
&[20309, 20310],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[20310])).expect("decodes");
assert_eq!(record.owner_reference, None);
}
#[test]
fn owner_reference_rejects_a_slot_outside_the_element_id_range() {
let mut buf = with_reference_list(
synth_record(
20310,
OST_SKETCH_LINES,
[20.0, 25.0, 46.0, 20.0, 114.0, 46.0],
),
&[20309, 20310],
);
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&(u64::from(u32::MAX) + 1).to_le_bytes());
let record = decode_at("Partitions/46", &buf, 0, &declared(&[20310])).expect("decodes");
assert_eq!(record.owner_reference, None);
}
#[test]
fn type_symbol_reference_is_the_symbol_slot_before_the_records_own_id() {
let buf = with_reference_list(
synth_record(20375, OST_COLUMNS, [23.0, 109.0, 76.0, 25.0, 111.0, 90.333]),
&[3, 4258, 4546, 5755, 20307, 20308, 20375, 20843, 75407],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[20375])).expect("decodes");
let symbols: BTreeSet<u32> = [5755u32, 75407].into_iter().collect();
assert_eq!(record.type_symbol_reference(&symbols), Some(5755));
}
#[test]
fn type_symbol_reference_is_absent_without_a_symbol_slot() {
let buf = with_reference_list(
synth_record(20375, OST_COLUMNS, [23.0, 109.0, 76.0, 25.0, 111.0, 90.333]),
&[3, 4258, 20375, 75407],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[20375])).expect("decodes");
let symbols: BTreeSet<u32> = [5755u32, 75407].into_iter().collect();
assert_eq!(record.type_symbol_reference(&symbols), None);
}
#[test]
fn type_symbol_reference_is_absent_without_a_self_slot() {
let buf = with_reference_list(
synth_record(20375, OST_COLUMNS, [23.0, 109.0, 76.0, 25.0, 111.0, 90.333]),
&[3, 5755, 20307],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[20375])).expect("decodes");
let symbols: BTreeSet<u32> = [5755u32].into_iter().collect();
assert_eq!(record.type_symbol_reference(&symbols), None);
}
#[test]
fn reference_list_is_kept_verbatim_on_the_record() {
let buf = with_reference_list(
synth_record(25947, OST_DOORS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]),
&[3, 20274, 23756, 24301, 25488, 25947],
);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[25947])).expect("decodes");
assert_eq!(
record.references,
vec![3, 20274, 23756, 24301, 25488, 25947]
);
}
#[test]
fn a_record_without_a_reference_list_still_decodes() {
let buf = synth_record(22805, OST_COLUMNS, [1.0, 2.0, 0.0, 3.0, 4.0, 8.0]);
let record = decode_at("Partitions/46", &buf, 0, &declared(&[22805])).expect("decodes");
assert_eq!(record.preceding_reference, None);
assert_eq!(record.owner_reference, None);
assert!(record.references.is_empty());
}
#[test]
fn product_categories_are_distinct_mapped_and_counted() {
let mut seen = BTreeSet::new();
for (category, class) in PRODUCT_RECORD_CATEGORIES {
assert!(seen.insert(category), "{category} listed twice");
assert!(
(BUILTIN_CATEGORY_MIN..=BUILTIN_CATEGORY_MAX).contains(&category),
"{category} outside the BuiltInCategory band"
);
assert!(
crate::ifc::category_map::lookup(class).is_some(),
"{class} has no IFC mapping"
);
assert!(
RECOVERED_CATEGORIES.contains(&(category, class)),
"{class} frames without an ElementId would go uncounted"
);
}
}
#[test]
fn a_product_category_record_decodes_like_any_other() {
let buf = synth_record(738550, OST_FURNITURE, [1.0, 2.0, 0.0, 3.0, 4.0, 2.5]);
let record = decode_at("Partitions/12", &buf, 0, &declared(&[738550])).expect("decodes");
assert_eq!(record.builtin_category, OST_FURNITURE);
assert_eq!(record.element_id, 738550);
}
fn second_prologue(category: i64, references: &[u64]) -> Vec<u8> {
let bbox = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let mut buf = with_reference_list(synth_record(1, category, bbox), references);
buf[0..8].copy_from_slice(&0x0000_0001_ffff_ffff_u64.to_le_bytes());
buf
}
fn first_prologue(element_id: u32, category: i64) -> Vec<u8> {
let bbox = [0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
with_reference_list(
synth_record(element_id, category, bbox),
&[3, u64::from(element_id)],
)
}
fn as_record(mut frame: Vec<u8>) -> Vec<u8> {
let size = frame.len() as u32;
frame[8..12].copy_from_slice(&size.to_le_bytes());
trailer(&mut frame, size, 0);
frame
}
fn nested(element_id: u64, frame: &[u8]) -> Vec<u8> {
let mut record = element_id.to_le_bytes().to_vec();
let size = (PARTITION_RECORD_HEADER_LEN + 6 + frame.len()) as u32;
record.extend_from_slice(&size.to_le_bytes());
record.extend_from_slice(&0x059fu16.to_le_bytes());
record.extend_from_slice(&1u16.to_le_bytes());
record.extend_from_slice(&[0u8; 6]);
record.extend_from_slice(frame);
trailer(&mut record, size, 0x0100_0000);
record
}
fn trailer(record: &mut Vec<u8>, size: u32, flags: u32) {
record.extend_from_slice(&0x0bcd_u64.to_le_bytes());
record.extend_from_slice(&flags.to_le_bytes());
record.extend_from_slice(&size.to_le_bytes());
}
fn nested_frame_at(start: usize) -> usize {
start + PARTITION_RECORD_HEADER_LEN + 6
}
#[test]
fn the_prologue_constant_sits_twelve_below_the_marker_constant() {
assert_eq!(record_prologue_constant(&BBOX_MARKER), 0x059f);
assert_eq!(record_prologue_constant(&BBOX_MARKER_2025), 0x05c7);
}
#[test]
fn the_leading_record_chain_stops_at_the_first_non_record() {
let first = as_record(first_prologue(100, OST_WALLS));
let second = nested(150, &second_prologue(OST_WALLS, &[3, 90]));
let chained = [first.clone(), second.clone()].concat();
let family = nested(160, &second_prologue(OST_WALLS, &[3, 90]));
let buf = [chained.clone(), vec![0x42; 40], family].concat();
let chain = partition_record_chain(&buf, &BBOX_MARKER);
let ids: Vec<(usize, u64)> = chain.iter().map(|r| (r.start, r.element_id)).collect();
assert_eq!(ids, vec![(0, 100), (first.len(), 150)]);
assert_eq!(chain[1].end, chained.len() - 16);
let mut broken = first.clone();
let echo = broken.len() - 4;
broken[echo..].copy_from_slice(&7u32.to_le_bytes());
assert!(partition_record_chain(&broken, &BBOX_MARKER).is_empty());
let mut flagged = first;
let flags = flagged.len() - 8;
flagged[flags..flags + 4].copy_from_slice(&2u32.to_le_bytes());
assert!(partition_record_chain(&flagged, &BBOX_MARKER).is_empty());
assert!(partition_record_chain(&chained, &BBOX_MARKER_2025).is_empty());
}
#[test]
fn a_second_prologue_frame_takes_its_enclosing_record_id() {
let declared = declared(&[100, 150, 160, 49480]);
let first = as_record(first_prologue(100, OST_WALLS));
let door = nested(150, &second_prologue(OST_DOORS, &[3, 49480, 150, 160]));
let undeclared = nested(170, &second_prologue(OST_DOORS, &[3, 150]));
let chained = [first.clone(), door.clone(), undeclared].concat();
let family = nested(160, &second_prologue(OST_DOORS, &[3, 160]));
let buf = [chained, vec![0x42; 40], family].concat();
let assigned = assign_second_prologue_ids(&buf, &BBOX_MARKER, &declared);
assert_eq!(
assigned.into_iter().collect::<Vec<_>>(),
vec![(nested_frame_at(first.len()), 150)]
);
}
#[test]
fn a_frame_with_the_invalid_id_takes_its_enclosing_record_id() {
assert!(carries_no_element_id(0));
assert!(carries_no_element_id(u64::from(u32::MAX)));
assert!(carries_no_element_id(0x0000_0001_ffff_ffff));
assert!(!carries_no_element_id(1_644_693));
let declared = declared(&[100, 1_644_693]);
let mut run = second_prologue(OST_STAIRS_RUNS, &[3, 1_644_692, 1_644_693]);
run[0..8].copy_from_slice(&u64::from(u32::MAX).to_le_bytes());
let first = as_record(first_prologue(100, OST_STAIRS_RUNS));
let buf = [first.clone(), nested(1_644_693, &run)].concat();
let assigned = assign_second_prologue_ids(&buf, &BBOX_MARKER, &declared);
assert_eq!(
assigned.into_iter().collect::<Vec<_>>(),
vec![(nested_frame_at(first.len()), 1_644_693)]
);
}
#[test]
fn a_second_prologue_frame_that_is_not_a_placed_instance_gets_no_id() {
let declared = declared(&[100, 150]);
let mut symbol = second_prologue(OST_DOORS, &[3, 150]);
symbol[PLACEMENT_KIND_OFFSET..PLACEMENT_KIND_OFFSET + 4]
.copy_from_slice(&PLACEMENT_KIND_SYMBOL.to_le_bytes());
let buf = [
as_record(first_prologue(100, OST_DOORS)),
nested(150, &symbol),
]
.concat();
assert!(assign_second_prologue_ids(&buf, &BBOX_MARKER, &declared).is_empty());
}
#[test]
fn an_assigned_frame_decodes_under_its_category_and_is_not_counted_missing() {
let declared = declared(&[100, 150, 200]);
let buf = [
as_record(first_prologue(100, OST_WALLS)),
nested(150, &second_prologue(OST_WALLS, &[3, 90])),
as_record(first_prologue(200, OST_WALLS)),
]
.concat();
let assigned = assign_second_prologue_ids(&buf, &BBOX_MARKER, &declared);
let records = find_category_records_assigned(
"Partitions/68",
&buf,
OST_WALLS,
&declared,
&BBOX_MARKER,
&assigned,
);
let ids: Vec<(u32, bool)> = records
.iter()
.map(|r| (r.element_id, r.id_from_enclosing_record))
.collect();
assert_eq!(ids, vec![(100, false), (150, true), (200, false)]);
assert!(
count_unattributed_frames_excluding(&buf, &[OST_WALLS], &BBOX_MARKER, &assigned)
.is_empty()
);
assert_eq!(
count_unattributed_frames(&buf, &[OST_WALLS]),
BTreeMap::from([(OST_WALLS, 1)])
);
}
#[test]
fn frames_after_the_record_chain_are_not_counted_missing() {
let chain = as_record(first_prologue(100, OST_WALLS));
let family = nested(7, &second_prologue(OST_WALLS, &[3, 7]));
let buf = [chain, vec![0x42; 40], family].concat();
assert!(count_unattributed_frames(&buf, &[OST_WALLS]).is_empty());
}
#[test]
fn a_placed_instance_without_volume_is_left_out_and_not_counted_missing() {
let flat = [0.0, 0.0, 0.0, 5.0, 5.0, 0.0];
let solid = [0.0, 0.0, 0.0, 5.0, 5.0, 3.0];
let declared = declared(&[100, 200]);
let buf = [
synth_record(100, OST_SPECIALITY_EQUIPMENT, flat),
synth_record(200, OST_SPECIALITY_EQUIPMENT, solid),
]
.concat();
let records =
find_category_records("Partitions/68", &buf, OST_SPECIALITY_EQUIPMENT, &declared);
let volumes: Vec<(u32, bool)> = records
.iter()
.map(|r| (r.element_id, r.has_volume()))
.collect();
assert_eq!(volumes, vec![(100, false), (200, true)]);
let mut hidden_flat = synth_record(1, OST_SPECIALITY_EQUIPMENT, flat);
hidden_flat[0..8].fill(0xff);
let mut hidden_solid = synth_record(1, OST_SPECIALITY_EQUIPMENT, solid);
hidden_solid[0..8].fill(0xff);
let buf = [nested(300, &hidden_flat), nested(301, &hidden_solid)].concat();
assert_eq!(
count_unattributed_frames(&buf, &[OST_SPECIALITY_EQUIPMENT]),
BTreeMap::from([(OST_SPECIALITY_EQUIPMENT, 1)])
);
}
}