use super::level::normalise_field_name;
use crate::formats;
use crate::walker::{DecodedElement, ElementDecoder, HandleIndex, InstanceField};
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RoofKind {
#[default]
Footprint,
Extrusion,
}
impl RoofKind {
pub fn from_code(code: u32) -> Self {
if code == 1 {
Self::Extrusion
} else {
Self::Footprint
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RafterCut {
#[default]
PlumbCut,
TwoCutSquare,
TwoCutPlumb,
}
impl RafterCut {
pub fn from_code(code: u32) -> Self {
match code {
1 => Self::TwoCutSquare,
2 => Self::TwoCutPlumb,
_ => Self::PlumbCut,
}
}
}
pub struct RoofDecoder;
impl ElementDecoder for RoofDecoder {
fn class_name(&self) -> &'static str {
"Roof"
}
fn decode(
&self,
bytes: &[u8],
schema: &formats::ClassEntry,
_index: &HandleIndex,
) -> Result<DecodedElement> {
if schema.name != "Roof" {
return Err(Error::BasicFileInfo(format!(
"RoofDecoder received wrong schema: {}",
schema.name
)));
}
Ok(crate::walker::decode_instance(bytes, 0, schema))
}
}
pub struct RoofTypeDecoder;
impl ElementDecoder for RoofTypeDecoder {
fn class_name(&self) -> &'static str {
"RoofType"
}
fn decode(
&self,
bytes: &[u8],
schema: &formats::ClassEntry,
_index: &HandleIndex,
) -> Result<DecodedElement> {
if schema.name != "RoofType" {
return Err(Error::BasicFileInfo(format!(
"RoofTypeDecoder received wrong schema: {}",
schema.name
)));
}
Ok(crate::walker::decode_instance(bytes, 0, schema))
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Roof {
pub level_id: Option<u32>,
pub base_offset_feet: Option<f64>,
pub cutoff_level_id: Option<u32>,
pub cutoff_offset_feet: Option<f64>,
pub kind: Option<RoofKind>,
pub type_id: Option<u32>,
pub rafter_cut: Option<RafterCut>,
}
impl Roof {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self::default();
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
("levelid" | "baselevelid", InstanceField::ElementId { id, .. }) => {
out.level_id = Some(*id);
}
("baseoffset", InstanceField::Float { value, .. }) => {
out.base_offset_feet = Some(*value);
}
("cutofflevelid", InstanceField::ElementId { id, .. }) => {
out.cutoff_level_id = Some(*id);
}
("cutoffoffset", InstanceField::Float { value, .. }) => {
out.cutoff_offset_feet = Some(*value);
}
("rooftype" | "kind", InstanceField::Integer { value, .. }) => {
out.kind = Some(RoofKind::from_code(*value as u32));
}
("typeid", InstanceField::ElementId { id, .. }) => out.type_id = Some(*id),
("raftercut", InstanceField::Integer { value, .. }) => {
out.rafter_cut = Some(RafterCut::from_code(*value as u32));
}
_ => {}
}
}
out
}
pub fn has_cutoff(&self) -> bool {
!matches!(self.cutoff_level_id, None | Some(0))
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct RoofType {
pub name: Option<String>,
pub thickness_feet: Option<f64>,
pub is_exterior: Option<bool>,
}
impl RoofType {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self::default();
for (field_name, value) in &decoded.fields {
match (normalise_field_name(field_name).as_str(), value) {
("name", InstanceField::String(s)) => out.name = Some(s.clone()),
("thickness" | "width", InstanceField::Float { value, .. }) => {
out.thickness_feet = Some(*value);
}
("function", InstanceField::Integer { value, .. }) => {
out.is_exterior = Some(*value == 1);
}
_ => {}
}
}
out
}
pub fn thickness_inches(&self) -> Option<f64> {
self.thickness_feet.map(|ft| ft * 12.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::{ClassEntry, FieldEntry, FieldType};
fn synth_roof_schema() -> ClassEntry {
ClassEntry {
name: "Roof".into(),
offset: 0,
fields: vec![
FieldEntry {
name: "m_level_id".into(),
cpp_type: None,
field_type: Some(FieldType::ElementId),
},
FieldEntry {
name: "m_base_offset".into(),
cpp_type: None,
field_type: Some(FieldType::Primitive {
kind: 0x07,
size: 8,
}),
},
FieldEntry {
name: "m_roof_type".into(),
cpp_type: None,
field_type: Some(FieldType::Primitive {
kind: 0x05,
size: 4,
}),
},
FieldEntry {
name: "m_type_id".into(),
cpp_type: None,
field_type: Some(FieldType::ElementId),
},
],
tag: Some(1),
parent: None,
declared_field_count: Some(4),
was_parent_only: false,
ancestor_tag: None,
}
}
fn synth_roof_bytes() -> Vec<u8> {
let mut b = Vec::new();
b.extend_from_slice(&0u32.to_le_bytes());
b.extend_from_slice(&5u32.to_le_bytes());
b.extend_from_slice(&10.0_f64.to_le_bytes());
b.extend_from_slice(&1u32.to_le_bytes());
b.extend_from_slice(&0u32.to_le_bytes());
b.extend_from_slice(&12u32.to_le_bytes());
b
}
#[test]
fn roof_decoder_rejects_wrong_schema() {
let wrong = ClassEntry {
name: "Floor".into(),
..synth_roof_schema()
};
assert!(
RoofDecoder
.decode(&[], &wrong, &HandleIndex::new())
.is_err()
);
}
#[test]
fn roof_decodes_kind_and_levels() {
let decoded = RoofDecoder
.decode(
&synth_roof_bytes(),
&synth_roof_schema(),
&HandleIndex::new(),
)
.unwrap();
let r = Roof::from_decoded(&decoded);
assert_eq!(r.level_id, Some(5));
assert_eq!(r.base_offset_feet, Some(10.0));
assert_eq!(r.kind, Some(RoofKind::Extrusion));
assert_eq!(r.type_id, Some(12));
assert!(!r.has_cutoff());
}
#[test]
fn cutoff_detection() {
let with_cutoff = Roof {
cutoff_level_id: Some(99),
..Default::default()
};
let zero_cutoff = Roof {
cutoff_level_id: Some(0),
..Default::default()
};
let missing = Roof::default();
assert!(with_cutoff.has_cutoff());
assert!(!zero_cutoff.has_cutoff());
assert!(!missing.has_cutoff());
}
#[test]
fn enum_mappings() {
assert_eq!(RoofKind::from_code(0), RoofKind::Footprint);
assert_eq!(RoofKind::from_code(1), RoofKind::Extrusion);
assert_eq!(RoofKind::from_code(99), RoofKind::Footprint);
assert_eq!(RafterCut::from_code(0), RafterCut::PlumbCut);
assert_eq!(RafterCut::from_code(1), RafterCut::TwoCutSquare);
assert_eq!(RafterCut::from_code(2), RafterCut::TwoCutPlumb);
assert_eq!(RafterCut::from_code(5), RafterCut::PlumbCut);
}
#[test]
fn roof_type_thickness_conversion() {
let rt = RoofType {
thickness_feet: Some(0.75),
..Default::default()
};
assert_eq!(rt.thickness_inches(), Some(9.0));
assert_eq!(RoofType::default().thickness_inches(), None);
}
#[test]
fn class_names() {
assert_eq!(RoofDecoder.class_name(), "Roof");
assert_eq!(RoofTypeDecoder.class_name(), "RoofType");
}
}