use crate::{Result, RevitFile};
use std::collections::{BTreeMap, BTreeSet};
pub const MATERIALS_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2024, 2025];
pub const MATERIAL_TAG_OFFSET: usize = 0x47;
pub const APPEARANCE_WINDOW: usize = 0x2000;
pub fn material_object_tag(revit_version: u32) -> Option<[u8; 2]> {
match revit_version {
2024 => Some([0x28, 0x0a]),
2025 => Some([0x6b, 0x0a]),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MaterialAppearance {
pub rgb: [u8; 3],
pub transparency: f32,
pub shininess: u32,
}
impl MaterialAppearance {
pub fn color_packed(&self) -> u32 {
u32::from(self.rgb[0]) | (u32::from(self.rgb[1]) << 8) | (u32::from(self.rgb[2]) << 16)
}
}
fn f32_at(buf: &[u8], at: usize) -> Option<f32> {
buf.get(at..at.checked_add(4)?)
.map(|s| f32::from_le_bytes(s.try_into().expect("4 bytes")))
}
fn frame_at(buf: &[u8], at: usize) -> Option<MaterialAppearance> {
let start = at.checked_sub(32)?;
if buf.get(start..start + 8)? != [0xff; 8] {
return None;
}
let transparency = f32_at(buf, at - 24)?;
let second = f32_at(buf, at - 20)?;
let plausible = |v: f32| (0.0..=1.0).contains(&v) && (v == 0.0 || v.is_normal());
if !plausible(transparency) || !plausible(second) {
return None;
}
for high in [at - 13, at - 9, at - 5, at - 1, at + 3] {
if *buf.get(high)? != 0 {
return None;
}
}
let shininess = u32::from_le_bytes(buf.get(at + 4..at + 8)?.try_into().ok()?);
Some(MaterialAppearance {
rgb: [buf[at], buf[at + 1], buf[at + 2]],
transparency,
shininess,
})
}
fn frame_at_2023(buf: &[u8], at: usize) -> Option<MaterialAppearance> {
let transparency = f32_at(buf, at.checked_sub(40)?)?;
let second = f32_at(buf, at - 36)?;
let plausible = |v: f32| (0.0..=1.0).contains(&v) && (v == 0.0 || v.is_normal());
if !plausible(transparency) || second != 0.5 {
return None;
}
for slot in [at - 32, at - 24, at - 16, at - 8] {
let id = u32_at(buf, slot)?;
if id != u32::MAX && id >= 0x0100_0000 {
return None;
}
}
for high in [at - 25, at - 17, at - 9, at - 1, at + 3] {
if *buf.get(high)? != 0 {
return None;
}
}
let shininess = u32::from_le_bytes(buf.get(at + 4..at + 8)?.try_into().ok()?);
Some(MaterialAppearance {
rgb: [buf[at], buf[at + 1], buf[at + 2]],
transparency,
shininess,
})
}
pub fn appearance_at(buf: &[u8], id_at: usize) -> Option<MaterialAppearance> {
let from = id_at.checked_add(8 + 32)?;
let to = id_at
.saturating_add(APPEARANCE_WINDOW)
.min(buf.len().saturating_sub(8));
(from..to).find_map(|at| frame_at(buf, at))
}
pub fn scan_material_appearances(
rf: &mut RevitFile,
revit_version: u32,
declared: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, MaterialAppearance>> {
if revit_version == 2023 {
return Ok(scan_material_appearances_2023(rf, declared));
}
let Some(tag) = material_object_tag(revit_version) else {
return Ok(BTreeMap::new());
};
let mut found: BTreeMap<u32, Option<MaterialAppearance>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for at in memchr::memmem::find_iter(buf, &[1u8, 0, 0, 0]) {
let id_at = at + 4;
let Some(id) = buf
.get(id_at..id_at + 8)
.map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
.and_then(|id| u32::try_from(id).ok())
.filter(|id| declared.contains(id))
else {
continue;
};
let tag_at = id_at + MATERIAL_TAG_OFFSET;
if buf.get(tag_at..tag_at + 2) != Some(&tag[..]) {
continue;
}
let Some(appearance) = appearance_at(buf, id_at) else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(appearance));
}
Some(held) => {
if held.as_ref() != Some(&appearance) {
*held = None;
}
}
}
}
}
Ok(found
.into_iter()
.filter_map(|(id, appearance)| appearance.map(|a| (id, a)))
.collect())
}
pub const NAME_WINDOW: usize = 0x1000;
pub const NAME_PARAMETER: i64 = -1_001_203;
pub fn material_name_frame_tag(revit_version: u32) -> Option<[u8; 2]> {
match revit_version {
2024 => Some([0x49, 0x01]),
2025 => Some([0x5c, 0x01]),
_ => None,
}
}
fn utf16_name(buf: &[u8], at: usize, n: u32) -> Option<String> {
let n = usize::try_from(n).ok().filter(|n| (1..=256).contains(n))?;
let bytes = buf.get(at..at.checked_add(2 * n)?)?;
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|unit| u16::from_le_bytes([unit[0], unit[1]]))
.collect();
if units.iter().any(|u| !(0x20..0xfffe).contains(u)) {
return None;
}
String::from_utf16(&units).ok()
}
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 i64_at(buf: &[u8], at: usize) -> Option<i64> {
buf.get(at..at.checked_add(8)?)
.map(|s| i64::from_le_bytes(s.try_into().expect("8 bytes")))
}
fn is_builtin_parameter(id: i64) -> bool {
(-2_000_000..=-1_000_000).contains(&id)
}
pub fn material_name_end_tag(revit_version: u32) -> Option<[u8; 2]> {
match revit_version {
2024 => Some([0x17, 0x0c]),
2025 => Some([0x6b, 0x0c]),
_ => None,
}
}
fn terminated_name(object: &[u8], revit_version: u32) -> Option<String> {
let end_tag = material_name_end_tag(revit_version)?;
let terminator = [0xff, 0xff, 0xff, 0xff, end_tag[0], end_tag[1]];
let from = MATERIAL_TAG_OFFSET + 2;
let end = from + memchr::memmem::find(object.get(from..)?, &terminator)?;
let mut names = (1..=256u32).filter_map(|n| {
let at = end
.checked_sub(4 + 2 * n as usize)
.filter(|&at| at >= from)?;
(u32_at(object, at)? == n)
.then(|| utf16_name(object, at + 4, n))
.flatten()
});
let name = names.next()?;
names.next().is_none().then_some(name)
}
pub fn name_at(buf: &[u8], id_at: usize, revit_version: u32) -> Option<String> {
let end = id_at.saturating_add(NAME_WINDOW).min(buf.len());
let object = buf.get(id_at..end)?;
let mut at = MATERIAL_TAG_OFFSET + 2;
let mut zeros = 0;
while zeros < 4 && u32_at(object, at) == Some(0) {
at += 4;
zeros += 1;
}
if zeros >= 2 {
if let Some(n) = u32_at(object, at) {
if !i64_at(object, at + 4).is_some_and(is_builtin_parameter) {
if let Some(name) = utf16_name(object, at + 4, n) {
return Some(name);
}
}
}
}
if let Some(name) = terminated_name(object, revit_version) {
return Some(name);
}
if let Some(tag) = material_name_frame_tag(revit_version) {
let frame = [0xff, 0xff, 0xff, 0xff, tag[0], tag[1]];
let framed = memchr::memmem::find_iter(object, &frame).find_map(|hit| {
let n = u32_at(object, hit + frame.len())?;
utf16_name(object, hit + frame.len() + 4, n)
});
if framed.is_some() {
return framed;
}
}
let mut entry = NAME_PARAMETER.to_le_bytes().to_vec();
entry.extend_from_slice(&[0, 0, 0, 0]);
memchr::memmem::find_iter(object, &entry).find_map(|hit| {
let n = u32_at(object, hit + entry.len())?;
utf16_name(object, hit + entry.len() + 4, n)
})
}
pub fn scan_material_names(
rf: &mut RevitFile,
revit_version: u32,
declared: &BTreeSet<u32>,
) -> Result<BTreeMap<u32, String>> {
let (Some(tag), Some(_)) = (
material_object_tag(revit_version),
material_name_frame_tag(revit_version),
) else {
return Ok(BTreeMap::new());
};
let mut found: BTreeMap<u32, Option<String>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for at in memchr::memmem::find_iter(buf, &[1u8, 0, 0, 0]) {
let id_at = at + 4;
let Some(id) = buf
.get(id_at..id_at + 8)
.map(|s| u64::from_le_bytes(s.try_into().expect("8 bytes")))
.and_then(|id| u32::try_from(id).ok())
.filter(|id| declared.contains(id))
else {
continue;
};
let tag_at = id_at + MATERIAL_TAG_OFFSET;
if buf.get(tag_at..tag_at + 2) != Some(&tag[..]) {
continue;
}
let Some(name) = name_at(buf, id_at, revit_version) else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(name));
}
Some(held) => {
if held.as_deref() != Some(name.as_str()) {
*held = None;
}
}
}
}
}
let names: BTreeMap<u32, String> = found
.into_iter()
.filter_map(|(id, name)| name.map(|n| (id, n)))
.collect();
let mut uses: BTreeMap<&str, usize> = BTreeMap::new();
for name in names.values() {
*uses.entry(name.as_str()).or_default() += 1;
}
let shared: BTreeSet<String> = uses
.into_iter()
.filter(|(_, count)| *count > 1)
.map(|(name, _)| name.to_string())
.collect();
Ok(names
.into_iter()
.filter(|(_, name)| !shared.contains(name))
.collect())
}
pub const CATEGORY_MATERIAL_SUPPORTED_REVIT_VERSIONS: &[u32] = &[2023, 2024];
pub const CATEGORY_MATERIAL_FRAME_2023: [u8; 4] = [0xff, 0xff, 0xff, 0xff];
pub const CATEGORY_MATERIAL_MARK_2023: [u8; 4] = [0x36, 0x39, 0xd2, 0xff];
pub const CATEGORY_MATERIAL_FRAME: [u8; 20] = [
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff,
];
pub fn scan_category_material(
rf: &mut RevitFile,
revit_version: u32,
category: i64,
) -> Result<Option<u32>> {
if !CATEGORY_MATERIAL_SUPPORTED_REVIT_VERSIONS.contains(&revit_version) {
return Ok(None);
}
if revit_version == 2023 {
return scan_category_material_2023(rf, category);
}
let mut pattern = category.to_le_bytes().to_vec();
pattern.extend_from_slice(&CATEGORY_MATERIAL_FRAME);
let mut found: BTreeSet<u64> = BTreeSet::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for at in memchr::memmem::find_iter(buf, &pattern) {
if let Some(material) = buf
.get(at + pattern.len()..at + pattern.len() + 8)
.map(|b| u64::from_le_bytes(b.try_into().expect("8 bytes")))
{
found.insert(material);
}
}
}
let mut values = found.into_iter();
let (Some(material), None) = (values.next(), values.next()) else {
return Ok(None);
};
Ok(u32::try_from(material)
.ok()
.filter(|&id| id != 0 && id != u32::MAX))
}
pub const MATERIAL_TAG_2023: [u8; 2] = [0xfb, 0x09];
pub const MATERIAL_TAG_OFFSET_2023: usize = 0x27;
pub fn scan_materials_2023(
rf: &mut RevitFile,
declared: &BTreeSet<u32>,
) -> (BTreeSet<u32>, BTreeMap<u32, String>) {
const PREFIX: [u8; 7] = [0, 0, 0, 0xff, 0xff, 0xff, 0xff];
let mut entry = i32::try_from(NAME_PARAMETER)
.expect("a 32-bit BuiltInParameter")
.to_le_bytes()
.to_vec();
entry.extend_from_slice(&[0, 0, 0, 0]);
let mut materials = BTreeSet::new();
let mut found: BTreeMap<u32, Option<String>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for tag_at in memchr::memmem::find_iter(buf, &MATERIAL_TAG_2023) {
let Some(id_at) = tag_at.checked_sub(MATERIAL_TAG_OFFSET_2023) else {
continue;
};
if id_at < 4
|| buf.get(id_at - 4..id_at) != Some(&[1, 0, 0, 0][..])
|| buf.get(tag_at - PREFIX.len()..tag_at) != Some(&PREFIX[..])
{
continue;
}
let Some(id) = u32_at(buf, id_at).filter(|id| declared.contains(id)) else {
continue;
};
materials.insert(id);
let end = id_at.saturating_add(NAME_WINDOW).min(buf.len());
let object = &buf[tag_at + 2..end];
let name = own_name_field(object)
.or_else(|| terminated_name_2023(object))
.or_else(|| {
memchr::memmem::find_iter(object, &entry).find_map(|hit| {
let n = u32_at(object, hit + entry.len())?;
utf16_name(object, hit + entry.len() + 4, n)
})
});
let Some(name) = name else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(name));
}
Some(held) => {
if held.as_deref() != Some(name.as_str()) {
*held = None;
}
}
}
}
}
let mut names: BTreeMap<u32, String> = found
.into_iter()
.filter_map(|(id, name)| name.map(|n| (id, n)))
.collect();
let unnamed: BTreeSet<u32> = materials
.iter()
.copied()
.filter(|id| !names.contains_key(id))
.collect();
for stream in rf.partition_stream_names() {
if let Ok(inflated) = rf.inflated_partition(&stream) {
for (id, name) in
crate::partition_names::find_element_data_names_2023(inflated.bytes(), &unnamed)
{
names.entry(id).or_insert(name);
}
}
}
let mut uses: BTreeMap<&str, usize> = BTreeMap::new();
for name in names.values() {
*uses.entry(name.as_str()).or_default() += 1;
}
let shared: BTreeSet<String> = uses
.into_iter()
.filter(|(_, count)| *count > 1)
.map(|(name, _)| name.to_string())
.collect();
let names = names
.into_iter()
.filter(|(_, name)| !shared.contains(name))
.collect();
(materials, names)
}
fn scan_category_material_2023(rf: &mut RevitFile, category: i64) -> Result<Option<u32>> {
let Ok(category) = i32::try_from(category) else {
return Ok(None);
};
let mut pattern = category.to_le_bytes().to_vec();
pattern.extend_from_slice(&CATEGORY_MATERIAL_FRAME_2023);
let mut found: BTreeSet<u32> = BTreeSet::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for at in memchr::memmem::find_iter(buf, &pattern) {
let entry = |offset: usize, len: usize| buf.get(at + offset..at + offset + len);
let word = entry(8, 4).map(|b| u32::from_le_bytes(b.try_into().expect("4 bytes")));
if !matches!(word, Some(1 | 2))
|| entry(12, 4) != Some(&[1, 0, 0, 0][..])
|| entry(16, 6) != Some(&[0xff, 0xff, 0xff, 0xff, 0x3f, 0x01][..])
|| entry(30, 4) != Some(&CATEGORY_MATERIAL_MARK_2023[..])
{
continue;
}
if let Some(material) = u32_at(buf, at + 34) {
found.insert(material);
}
}
}
let mut values = found.into_iter();
let (Some(material), None) = (values.next(), values.next()) else {
return Ok(None);
};
Ok(Some(material).filter(|&id| id != 0 && id != u32::MAX))
}
fn scan_material_appearances_2023(
rf: &mut RevitFile,
declared: &BTreeSet<u32>,
) -> BTreeMap<u32, MaterialAppearance> {
const PREFIX: [u8; 7] = [0, 0, 0, 0xff, 0xff, 0xff, 0xff];
let mut found: BTreeMap<u32, Option<MaterialAppearance>> = BTreeMap::new();
for stream in rf.partition_stream_names() {
let Ok(inflated) = rf.inflated_partition(&stream) else {
continue;
};
let buf = inflated.bytes();
for tag_at in memchr::memmem::find_iter(buf, &MATERIAL_TAG_2023) {
let Some(id_at) = tag_at.checked_sub(MATERIAL_TAG_OFFSET_2023) else {
continue;
};
if id_at < 4
|| buf.get(id_at - 4..id_at) != Some(&[1, 0, 0, 0][..])
|| buf.get(tag_at - PREFIX.len()..tag_at) != Some(&PREFIX[..])
{
continue;
}
let Some(id) = u32_at(buf, id_at).filter(|id| declared.contains(id)) else {
continue;
};
let to = id_at
.saturating_add(APPEARANCE_WINDOW)
.min(buf.len().saturating_sub(8));
let Some(appearance) = (tag_at + 2 + 40..to).find_map(|at| frame_at_2023(buf, at))
else {
continue;
};
match found.get_mut(&id) {
None => {
found.insert(id, Some(appearance));
}
Some(held) => {
if held.as_ref() != Some(&appearance) {
*held = None;
}
}
}
}
}
found
.into_iter()
.filter_map(|(id, appearance)| appearance.map(|a| (id, a)))
.collect()
}
fn terminated_name_2023(object: &[u8]) -> Option<String> {
const TERMINATOR: [u8; 6] = [0xff, 0xff, 0xff, 0xff, 0xeb, 0x0b];
let end = memchr::memmem::find(object, &TERMINATOR)?;
let mut names = (1..=256u32).filter_map(|n| {
let at = end.checked_sub(4 + 2 * n as usize)?;
(u32_at(object, at)? == n)
.then(|| utf16_name(object, at + 4, n))
.flatten()
});
let name = names.next()?;
names.next().is_none().then_some(name)
}
fn own_name_field(object: &[u8]) -> Option<String> {
let mut at = 0;
let mut zeros = 0;
while zeros < 4 && u32_at(object, at) == Some(0) {
at += 4;
zeros += 1;
}
if zeros < 2 {
return None;
}
let n = u32_at(object, at)?;
let parameter = i32::from_le_bytes(object.get(at + 4..at + 8)?.try_into().ok()?);
if is_builtin_parameter(i64::from(parameter)) {
return None;
}
utf16_name(object, at + 4, n).filter(|name| {
!name
.chars()
.any(|c| matches!(c, '\u{e000}'..='\u{f8ff}' | '\u{fff0}'..='\u{ffff}'))
})
}