use crate::{Result, RevitFile};
use std::collections::{BTreeMap, BTreeSet};
pub const COMPOUND_STRUCTURE_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2024, 2025];
pub const WALL_LINE_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2023, 2024, 2025];
pub fn scan_wall_lines(
rf: &mut RevitFile,
revit_version: u32,
walls: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, crate::partition_beam_axes::BoundedLine>> {
if !WALL_LINE_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) {
return Ok(BTreeMap::new());
}
crate::partition_beam_axes::scan_first_bounded_lines(rf, revit_version, walls)
}
pub fn scan_wall_arcs(
rf: &mut RevitFile,
revit_version: u32,
walls: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, crate::partition_beam_axes::BoundedArc>> {
if !WALL_LINE_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) {
return Ok(BTreeMap::new());
}
crate::partition_beam_axes::scan_first_bounded_arcs(rf, revit_version, walls)
}
pub const LAYER_RECORD_LEN: usize = 37;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LayerLayout {
pub record_len: usize,
pub function_at: usize,
pub material_at: usize,
pub deck_at: usize,
pub wide_ids: bool,
}
pub fn layer_layout(revit_version: u32) -> Option<LayerLayout> {
match revit_version {
2023 => Some(LayerLayout {
record_len: 29,
function_at: 8,
material_at: 16,
deck_at: 20,
wide_ids: false,
}),
2024 | 2025 => Some(LayerLayout {
record_len: LAYER_RECORD_LEN,
function_at: 24,
material_at: 8,
deck_at: 16,
wide_ids: true,
}),
_ => None,
}
}
pub const MAX_LAYERS: usize = 32;
pub const LAYER_WINDOW: usize = 0x2000;
pub const MAX_NAME_UNITS: usize = 256;
pub const WALL_FLIP_ANCHOR: [u8; 8] = [0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00];
pub fn layer_frame_tag(revit_version: u32) -> Option<[u8; 2]> {
match revit_version {
2023 => Some([0x6f, 0x10]),
2024 => Some([0xa6, 0x10]),
2025 => Some([0x0e, 0x11]),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CompoundLayer {
pub width_feet: f64,
pub material: Option<u32>,
pub function: u32,
}
fn u32_at(buf: &[u8], at: usize) -> Option<u32> {
buf.get(at..at.checked_add(4)?)
.map(|s| u32::from_le_bytes(s.try_into().expect("4 bytes")))
}
fn u64_at(buf: &[u8], at: usize) -> Option<u64> {
buf.get(at..at.checked_add(8)?)
.map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
}
pub fn layers_at(
buf: &[u8],
count_at: usize,
layout: LayerLayout,
materials: &BTreeSet<u32>,
declared: &BTreeSet<u32>,
) -> Option<Vec<CompoundLayer>> {
let id_at = |at: usize| -> Option<u64> {
if layout.wide_ids {
u64_at(buf, at)
} else {
u32_at(buf, at).map(|id| match id {
u32::MAX => u64::MAX,
id => u64::from(id),
})
}
};
let count = usize::try_from(u32_at(buf, count_at)?).ok()?;
if !(1..=MAX_LAYERS).contains(&count) {
return None;
}
let mut layers = Vec::with_capacity(count);
for index in 0..count {
let at = count_at + 4 + layout.record_len * index;
let width = f64::from_le_bytes(buf.get(at..at + 8)?.try_into().ok()?);
let material = id_at(at + layout.material_at)?;
let deck = id_at(at + layout.deck_at)?;
let function = u32_at(buf, at + layout.function_at)?;
let widthless = matches!(function, 100 | 200);
let width_ok =
width.is_finite() && width < 10.0 && (width >= 1e-3 || (widthless && width == 0.0));
let material = match material {
u64::MAX => None,
id => Some(u32::try_from(id).ok().filter(|id| materials.contains(id))?),
};
let deck_ok = deck == u64::MAX
|| u32::try_from(deck)
.ok()
.is_some_and(|id| declared.contains(&id));
if !width_ok || !deck_ok || !matches!(function, 0..=5 | 100 | 200) {
return None;
}
layers.push(CompoundLayer {
width_feet: width,
material,
function,
});
}
Some(layers)
}
fn framed(buf: &[u8], count_at: usize, tag: [u8; 2]) -> bool {
let tagged = count_at >= 6
&& buf.get(count_at - 6..count_at - 2) == Some(&[0xff; 4][..])
&& buf.get(count_at - 2..count_at) == Some(&tag[..]);
if tagged {
return true;
}
if count_at < 4 || u32_at(buf, count_at - 4) != Some(0) {
return false;
}
(1..=MAX_NAME_UNITS).any(|k: usize| {
let Some(start) = count_at.checked_sub(4 + 2 * k + 4) else {
return false;
};
u32_at(buf, start) == Some(k as u32)
&& (0..k).all(|i| {
buf.get(start + 4 + 2 * i..start + 6 + 2 * i)
.map(|unit| u16::from_le_bytes([unit[0], unit[1]]))
.is_some_and(|unit| unit >= 0x20)
})
})
}
pub fn find_layers(
data: &[u8],
tag: [u8; 2],
layout: LayerLayout,
materials: &BTreeSet<u32>,
declared: &BTreeSet<u32>,
) -> Option<Vec<CompoundLayer>> {
(6..data.len().saturating_sub(4))
.filter(|&at| framed(data, at, tag))
.find_map(|at| layers_at(data, at, layout, materials, declared))
}
pub fn scan_type_layers(
rf: &mut RevitFile,
revit_version: u32,
types: &BTreeSet<u32>,
materials: &BTreeSet<u32>,
declared: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, Vec<CompoundLayer>>> {
let header = match revit_version {
2023 => Some(crate::partition_names::ELEMENT_DATA_HEADER_2023),
_ => crate::partition_names::element_data_header(revit_version),
};
let (Some(header), Some(tag), Some(layout)) = (
header,
layer_frame_tag(revit_version),
layer_layout(revit_version),
) else {
return Ok(BTreeMap::new());
};
let mut found: BTreeMap<u32, Option<Vec<CompoundLayer>>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
let hits: Vec<usize> = memchr::memmem::find_iter(buf, &header).collect();
for (index, &hit) in hits.iter().enumerate() {
let id_at = hit + header.len();
let id = if layout.wide_ids {
u64_at(buf, id_at).and_then(|id| u32::try_from(id).ok())
} else {
u32_at(buf, id_at)
};
let Some(id) = id.filter(|id| types.contains(id)) else {
continue;
};
let end = hits
.get(index + 1)
.copied()
.unwrap_or(buf.len())
.min(hit.saturating_add(LAYER_WINDOW))
.min(buf.len());
let Some(layers) = buf
.get(id_at + 8..end)
.and_then(|data| find_layers(data, tag, layout, materials, declared))
else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(layers));
}
Some(held) => {
if held.as_ref() != Some(&layers) {
*held = None;
}
}
}
}
}
Ok(found
.into_iter()
.filter_map(|(id, layers)| layers.map(|l| (id, l)))
.collect())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WallOrientation {
pub location_line: u32,
pub word: u32,
pub flip: bool,
}
pub fn wall_orientation(data: &[u8]) -> Option<WallOrientation> {
let at = memchr::memmem::find(data, &WALL_FLIP_ANCHOR)? + WALL_FLIP_ANCHOR.len();
let location_line = u32_at(data, at)?;
let word = u32_at(data, at + 4)?;
let flip = match u32_at(data, at + 8)? {
0 => false,
1 => true,
_ => return None,
};
(location_line <= 5 && word <= 2).then_some(WallOrientation {
location_line,
word,
flip,
})
}
pub fn scan_wall_orientations(
rf: &mut RevitFile,
revit_version: u32,
walls: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, WallOrientation>> {
let Some(layout) = crate::partition_names::element_data_layout(revit_version) else {
return Ok(BTreeMap::new());
};
let header = layout.header;
let mut found: BTreeMap<u32, Option<WallOrientation>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
let hits: Vec<usize> = memchr::memmem::find_iter(buf, &header).collect();
for (index, &hit) in hits.iter().enumerate() {
let id_at = hit + header.len();
let Some(id) = layout.id_at(buf, id_at).filter(|id| walls.contains(id)) else {
continue;
};
let end = hits
.get(index + 1)
.copied()
.unwrap_or(buf.len())
.min(hit.saturating_add(LAYER_WINDOW))
.min(buf.len());
let Some(orientation) = buf.get(id_at + 8..end).and_then(wall_orientation) else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(orientation));
}
Some(held) => {
if *held != Some(orientation) {
*held = None;
}
}
}
}
}
Ok(found
.into_iter()
.filter_map(|(id, orientation)| orientation.map(|o| (id, o)))
.collect())
}
pub const WALL_FACE_ANGLES_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2024];
pub const WALL_FACE_ANGLES_FRAME: [u8; 24] = [
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0xe6, 0x3f, 0x03, 0x00, 0x00, 0x00,
];
pub fn wall_type_face_angles(data: &[u8]) -> Option<[f64; 3]> {
let mut frames = memchr::memmem::find_iter(data, &WALL_FACE_ANGLES_FRAME);
let at = frames.next()? + WALL_FACE_ANGLES_FRAME.len();
if frames.next().is_some() {
return None;
}
let angle = |index: usize| {
data.get(at + 8 * index..at + 8 * index + 8)
.map(|b| f64::from_le_bytes(b.try_into().expect("8 bytes")))
.filter(|value| {
*value == 0.0 || (1e-9..std::f64::consts::FRAC_PI_2).contains(&value.abs())
})
};
Some([angle(0)?, angle(1)?, angle(2)?])
}
pub fn scan_wall_type_face_angles(
rf: &mut RevitFile,
revit_version: u32,
types: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, [f64; 3]>> {
let Some(header) = crate::partition_names::element_data_header(revit_version)
.filter(|_| WALL_FACE_ANGLES_SUPPORTED_REVIT_VERSIONS.contains(&revit_version))
else {
return Ok(BTreeMap::new());
};
let mut found: BTreeMap<u32, Option<[f64; 3]>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
let hits: Vec<usize> = memchr::memmem::find_iter(buf, &header).collect();
for (index, &hit) in hits.iter().enumerate() {
let id_at = hit + header.len();
let Some(id) = u64_at(buf, id_at)
.and_then(|id| u32::try_from(id).ok())
.filter(|id| types.contains(id))
else {
continue;
};
let end = hits
.get(index + 1)
.copied()
.unwrap_or(buf.len())
.min(hit.saturating_add(LAYER_WINDOW))
.min(buf.len());
let Some(angles) = buf.get(id_at + 8..end).and_then(wall_type_face_angles) else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(angles));
}
Some(held) => {
if *held != Some(angles) {
*held = None;
}
}
}
}
}
Ok(found
.into_iter()
.filter_map(|(id, angles)| angles.map(|a| (id, a)))
.collect())
}
pub fn wall_join_count_offset(revit_version: u32) -> Option<usize> {
match revit_version {
2024 => Some(12),
2025 => Some(16),
_ => None,
}
}
pub const WALL_JOIN_LIST_WORD: [u8; 4] = [0x07, 0x00, 0x00, 0x00];
pub const WALL_JOIN_ENTRY_LEN: usize = 16;
pub const MAX_WALL_JOIN_ENTRIES: usize = 64;
pub const WALL_JOIN_WINDOW: usize = 0x1_0000;
pub type WallJoinLists = Vec<(u32, Vec<u32>)>;
pub fn wall_join_lists(
data: &[u8],
count_at: usize,
walls: &BTreeSet<u32>,
) -> Vec<(u32, Vec<u32>)> {
let mut out = Vec::new();
for at in memchr::memmem::find_iter(data, &WALL_JOIN_LIST_WORD) {
let (Some(tag), Some(2), Some(count)) = (
u32_at(data, at + 4),
u32_at(data, at + 8),
u32_at(data, at + count_at),
) else {
continue;
};
let count = count as usize;
if count == 0
|| count > MAX_WALL_JOIN_ENTRIES
|| (count_at > 12 && u32_at(data, at + 12) != Some(0))
{
continue;
}
let named: Option<Vec<u32>> = (0..count)
.map(|index| {
u64_at(data, at + count_at + 8 + index * WALL_JOIN_ENTRY_LEN)
.and_then(|id| u32::try_from(id).ok())
.filter(|id| walls.contains(id))
})
.collect();
if let Some(named) = named {
out.push((tag, named));
}
}
out
}
pub fn scan_wall_join_partners(
rf: &mut RevitFile,
revit_version: u32,
walls: &BTreeSet<u32>,
read: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, BTreeSet<u32>>> {
let (Some(header), Some(count_at)) = (
crate::partition_names::element_data_header(revit_version),
wall_join_count_offset(revit_version),
) else {
return Ok(BTreeMap::new());
};
let mut found: BTreeMap<u32, Option<WallJoinLists>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
let hits: Vec<usize> = memchr::memmem::find_iter(buf, &header).collect();
for (index, &hit) in hits.iter().enumerate() {
let id_at = hit + header.len();
let Some(id) = u64_at(buf, id_at)
.and_then(|id| u32::try_from(id).ok())
.filter(|id| read.contains(id))
else {
continue;
};
let end = hits
.get(index + 1)
.copied()
.unwrap_or(buf.len())
.min(hit.saturating_add(WALL_JOIN_WINDOW))
.min(buf.len());
let Some(data) = buf.get(id_at + 8..end) else {
continue;
};
let lists = wall_join_lists(data, count_at, walls);
match found.get_mut(&id) {
None => {
found.insert(id, Some(lists));
}
Some(held) => {
if held.as_ref() != Some(&lists) {
*held = None;
}
}
}
}
}
let tags: BTreeSet<u32> = found
.values()
.flatten()
.flatten()
.map(|(tag, _)| *tag)
.collect();
let [tag] = tags.into_iter().collect::<Vec<_>>()[..] else {
return Ok(BTreeMap::new());
};
Ok(found
.into_iter()
.filter_map(|(id, lists)| {
let named: BTreeSet<u32> = lists?
.into_iter()
.filter(|(list_tag, _)| *list_tag == tag)
.flat_map(|(_, named)| named)
.filter(|other| *other != id)
.collect();
Some((id, named))
})
.collect())
}