use crate::element_record_plan_profiles::{PlanProfile, plan_profile_from_lines};
use crate::{Result, RevitFile};
pub const ROOM_OUTLINE_SOURCE: &str = "partition_room_solid_faces";
pub const ROOM_SOLID_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2023, 2024, 2025];
pub const ROOM_SOLID_TAG: [u8; 4] = [0x04, 0x90, 0x08, 0x00];
pub const FACE_TAG: [u8; 4] = [0x04, 0x00, 0x08, 0x00];
pub const FACE_GAP: usize = 0x200;
pub const ROOM_SOLID_WINDOW: usize = 0x1_0000;
pub const BOX_TOLERANCE_FEET: f64 = 1e-3;
pub const ROOM_VERTEX_TOLERANCE_FEET: f64 = 1e-4;
const FACE_LEN: usize = 158;
const UNIT_EPS: f64 = 1e-6;
const AXIS_EPS: f64 = 1e-9;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RoomFace {
pub uv_box: [f64; 4],
pub origin: [f64; 3],
pub u: [f64; 3],
pub v: [f64; 3],
}
impl RoomFace {
pub fn plan_edge(&self) -> Option<([f64; 4], f64)> {
let vertical = |d: [f64; 3]| (d[2].abs() - 1.0).abs() < AXIS_EPS;
let horizontal = |d: [f64; 3]| d[2].abs() < AXIS_EPS;
let [u_min, v_min, u_max, v_max] = self.uv_box;
let ((h, lo, hi), (up, down_lo, down_hi)) = if vertical(self.u) && horizontal(self.v) {
((self.v, v_min, v_max), (self.u[2], u_min, u_max))
} else if vertical(self.v) && horizontal(self.u) {
((self.u, u_min, u_max), (self.v[2], v_min, v_max))
} else {
return None;
};
let [ox, oy, oz] = self.origin;
let bottom = (oz + up * down_lo).min(oz + up * down_hi);
Some((
[
ox + h[0] * lo,
oy + h[1] * lo,
ox + h[0] * hi,
oy + h[1] * hi,
],
bottom,
))
}
}
fn read_f64s<const N: usize>(buf: &[u8], at: usize) -> Option<[f64; N]> {
let mut out = [0.0; N];
for (index, value) in out.iter_mut().enumerate() {
let start = at.checked_add(8 * index)?;
let bytes = buf.get(start..start.checked_add(8)?)?;
*value = f64::from_le_bytes(bytes.try_into().ok()?);
if !value.is_finite() {
return None;
}
}
Some(out)
}
pub fn face_at(buf: &[u8], at: usize) -> Option<RoomFace> {
if buf.get(at..at.checked_add(FACE_LEN)?)?[..4] != FACE_TAG || buf[at + 85] != 1 {
return None;
}
let uv_box: [f64; 4] = read_f64s(buf, at + 20)?;
let origin = read_f64s(buf, at + 86)?;
let u = read_f64s(buf, at + 110)?;
let v = read_f64s(buf, at + 134)?;
let unit = |d: [f64; 3]| (d.iter().map(|c| c * c).sum::<f64>() - 1.0).abs() < UNIT_EPS;
if !unit(u) || !unit(v) || uv_box[0] > uv_box[2] || uv_box[1] > uv_box[3] {
return None;
}
Some(RoomFace {
uv_box,
origin,
u,
v,
})
}
fn solid_start(buf: &[u8], header: usize) -> Option<usize> {
let after_id = header.checked_add(8)?;
if buf.get(after_id..after_id + 4)? == ROOM_SOLID_TAG {
return Some(after_id + 4);
}
(buf.get(after_id..after_id + 8)? == [0xff, 0xff, 0xff, 0xff, 0x04, 0x90, 0x08, 0x00])
.then_some(after_id + 8)
}
pub fn solid_faces(buf: &[u8], header: usize) -> Vec<RoomFace> {
let Some(start) = solid_start(buf, header) else {
return Vec::new();
};
let end = buf.len().min(start + ROOM_SOLID_WINDOW);
let Some(window) = buf.get(start..end) else {
return Vec::new();
};
let Some(first) = memchr::memmem::find_iter(window, &FACE_TAG)
.map(|offset| start + offset)
.find(|&at| face_at(buf, at).is_some())
else {
return Vec::new();
};
let mut faces = Vec::new();
let mut at = first;
while let Some(face) = face_at(buf, at) {
faces.push(face);
let from = at + FACE_TAG.len();
let Some(window) = buf.get(from..buf.len().min(at + FACE_GAP)) else {
break;
};
let Some(next) = memchr::memmem::find_iter(window, &FACE_TAG)
.map(|offset| from + offset)
.find(|&next| face_at(buf, next).is_some())
else {
break;
};
at = next;
}
faces
}
pub fn room_outline_at(buf: &[u8], header: usize, bbox: [f64; 6]) -> Option<PlanProfile> {
let faces = solid_faces(buf, header);
let mut lines = Vec::new();
for face in &faces {
if let Some((edge, bottom)) = face.plan_edge() {
if (bottom - bbox[2]).abs() <= BOX_TOLERANCE_FEET {
lines.push(edge);
}
}
}
snap_ends(&mut lines);
let profile = plan_profile_from_lines(&lines)?;
let extent = profile.plan_extent_feet()?;
let wanted = [bbox[0], bbox[1], bbox[3], bbox[4]];
extent
.iter()
.zip(wanted)
.all(|(got, want)| (got - want).abs() <= BOX_TOLERANCE_FEET)
.then_some(profile)
}
pub fn solid_headers(buf: &[u8], room_id: u32) -> Vec<usize> {
let id = u64::from(room_id).to_le_bytes();
memchr::memmem::find_iter(buf, &id)
.filter(|&at| solid_start(buf, at).is_some())
.collect()
}
fn snap_ends(lines: &mut [[f64; 4]]) {
let mut held: Vec<(f64, f64)> = Vec::new();
for line in lines.iter_mut() {
for end in [0, 2] {
let point = (line[end], line[end + 1]);
let near = held.iter().copied().find(|q| {
(q.0 - point.0).abs() <= ROOM_VERTEX_TOLERANCE_FEET
&& (q.1 - point.1).abs() <= ROOM_VERTEX_TOLERANCE_FEET
});
match near {
Some(q) => (line[end], line[end + 1]) = q,
None => held.push(point),
}
}
}
}
pub fn room_outline(
rf: &mut RevitFile,
stream: &str,
room_id: u32,
bbox: [f64; 6],
) -> Result<Option<PlanProfile>> {
let inflated = rf.inflated_partition(stream)?;
let buf = inflated.bytes();
let mut found: Option<PlanProfile> = None;
for at in solid_headers(buf, room_id) {
let Some(profile) = room_outline_at(buf, at, bbox) else {
continue;
};
match &found {
None => found = Some(profile),
Some(held) if *held == profile => {}
Some(_) => return Ok(None),
}
}
Ok(found)
}