use super::level::normalise_field_name;
use crate::formats;
use crate::walker::{DecodedElement, ElementDecoder, HandleIndex, InstanceField};
use crate::{Error, Result};
pub struct CategoryDecoder;
impl ElementDecoder for CategoryDecoder {
fn class_name(&self) -> &'static str {
"Category"
}
fn decode(
&self,
bytes: &[u8],
schema: &formats::ClassEntry,
_index: &HandleIndex,
) -> Result<DecodedElement> {
if schema.name != "Category" {
return Err(Error::BasicFileInfo(format!(
"CategoryDecoder received wrong schema: {}",
schema.name
)));
}
Ok(crate::walker::decode_instance(bytes, 0, schema))
}
}
pub struct SubcategoryDecoder;
impl ElementDecoder for SubcategoryDecoder {
fn class_name(&self) -> &'static str {
"Subcategory"
}
fn decode(
&self,
bytes: &[u8],
schema: &formats::ClassEntry,
_index: &HandleIndex,
) -> Result<DecodedElement> {
if schema.name != "Subcategory" {
return Err(Error::BasicFileInfo(format!(
"SubcategoryDecoder received wrong schema: {}",
schema.name
)));
}
Ok(crate::walker::decode_instance(bytes, 0, schema))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Category {
pub name: Option<String>,
pub builtin_id: Option<i64>,
pub parent_category_id: Option<u32>,
pub is_cuttable: Option<bool>,
}
impl Category {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self {
name: None,
builtin_id: None,
parent_category_id: None,
is_cuttable: None,
};
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()),
("builtin" | "builtincategory", InstanceField::Integer { value, .. }) => {
out.builtin_id = Some(*value);
}
("parentid" | "parentcategoryid", InstanceField::ElementId { id, .. }) => {
out.parent_category_id = Some(*id);
}
("iscuttable", InstanceField::Bool(b)) => out.is_cuttable = Some(*b),
_ => {}
}
}
out
}
pub fn is_top_level(&self) -> bool {
matches!(self.parent_category_id, None | Some(0))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Subcategory {
pub name: Option<String>,
pub parent_category_id: Option<u32>,
pub line_color: Option<u32>,
pub line_weight_cut: Option<u32>,
pub line_weight_projection: Option<u32>,
pub line_pattern_id: Option<u32>,
pub material_id: Option<u32>,
}
impl Subcategory {
pub fn from_decoded(decoded: &DecodedElement) -> Self {
let mut out = Self {
name: None,
parent_category_id: None,
line_color: None,
line_weight_cut: None,
line_weight_projection: None,
line_pattern_id: None,
material_id: None,
};
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()),
("parentcategoryid" | "parentid", InstanceField::ElementId { id, .. }) => {
out.parent_category_id = Some(*id)
}
("linecolor", InstanceField::Integer { value, .. }) => {
out.line_color = Some(*value as u32);
}
("lineweightcut", InstanceField::Integer { value, .. }) => {
out.line_weight_cut = Some(*value as u32);
}
("lineweightprojection", InstanceField::Integer { value, .. }) => {
out.line_weight_projection = Some(*value as u32);
}
("linepatternid", InstanceField::ElementId { id, .. }) => {
out.line_pattern_id = Some(*id);
}
("materialid", InstanceField::ElementId { id, .. }) => {
out.material_id = Some(*id);
}
_ => {}
}
}
out
}
pub fn rgb(&self) -> Option<(u8, u8, u8)> {
let c = self.line_color?;
Some((
(c & 0xFF) as u8,
((c >> 8) & 0xFF) as u8,
((c >> 16) & 0xFF) as u8,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::formats::{ClassEntry, FieldEntry, FieldType};
fn synth_category_schema() -> ClassEntry {
ClassEntry {
name: "Category".to_string(),
offset: 0,
fields: vec![
FieldEntry {
name: "m_name".to_string(),
cpp_type: Some("String".into()),
field_type: Some(FieldType::String),
},
FieldEntry {
name: "m_builtin".to_string(),
cpp_type: Some("int".into()),
field_type: Some(FieldType::Primitive {
kind: 0x05,
size: 4,
}),
},
FieldEntry {
name: "m_parent_id".to_string(),
cpp_type: Some("ElementId".into()),
field_type: Some(FieldType::ElementId),
},
FieldEntry {
name: "m_is_cuttable".to_string(),
cpp_type: Some("bool".into()),
field_type: Some(FieldType::Primitive {
kind: 0x01,
size: 1,
}),
},
],
tag: Some(0x5678),
parent: None,
declared_field_count: Some(4),
was_parent_only: false,
ancestor_tag: None,
}
}
fn synth_category_bytes() -> Vec<u8> {
let mut b = Vec::new();
let name = "Walls";
b.extend_from_slice(&(name.chars().count() as u32).to_le_bytes());
for ch in name.encode_utf16() {
b.extend_from_slice(&ch.to_le_bytes());
}
b.extend_from_slice(&8u32.to_le_bytes());
b.extend_from_slice(&0u32.to_le_bytes());
b.extend_from_slice(&0u32.to_le_bytes());
b.push(1);
b
}
#[test]
fn category_decoder_decodes_all_fields() {
let schema = synth_category_schema();
let bytes = synth_category_bytes();
let decoded = CategoryDecoder
.decode(&bytes, &schema, &HandleIndex::new())
.unwrap();
assert_eq!(decoded.class, "Category");
assert_eq!(decoded.fields.len(), 4);
}
#[test]
fn category_from_decoded_projects_typed_view() {
let decoded = CategoryDecoder
.decode(
&synth_category_bytes(),
&synth_category_schema(),
&HandleIndex::new(),
)
.unwrap();
let cat = Category::from_decoded(&decoded);
assert_eq!(cat.name.as_deref(), Some("Walls"));
assert_eq!(cat.builtin_id, Some(8));
assert_eq!(cat.parent_category_id, Some(0));
assert_eq!(cat.is_cuttable, Some(true));
}
#[test]
fn category_top_level_detection() {
let top = Category {
name: Some("Walls".into()),
builtin_id: None,
parent_category_id: Some(0),
is_cuttable: None,
};
assert!(top.is_top_level());
let child = Category {
name: Some("Interior Walls".into()),
builtin_id: None,
parent_category_id: Some(42),
is_cuttable: None,
};
assert!(!child.is_top_level());
let missing = Category {
name: None,
builtin_id: None,
parent_category_id: None,
is_cuttable: None,
};
assert!(missing.is_top_level());
}
#[test]
fn category_decoder_rejects_wrong_schema() {
let wrong = ClassEntry {
name: "Wall".to_string(),
..synth_category_schema()
};
assert!(
CategoryDecoder
.decode(&[], &wrong, &HandleIndex::new())
.is_err()
);
}
#[test]
fn subcategory_rgb_decomposition() {
let s = Subcategory {
name: None,
parent_category_id: None,
line_color: Some(0x00336699), line_weight_cut: None,
line_weight_projection: None,
line_pattern_id: None,
material_id: None,
};
assert_eq!(s.rgb(), Some((0x99, 0x66, 0x33)));
let empty = Subcategory {
name: None,
parent_category_id: None,
line_color: None,
line_weight_cut: None,
line_weight_projection: None,
line_pattern_id: None,
material_id: None,
};
assert_eq!(empty.rgb(), None);
}
#[test]
fn subcategory_decoder_rejects_wrong_schema() {
let wrong = ClassEntry {
name: "Category".to_string(),
..synth_category_schema()
};
assert!(
SubcategoryDecoder
.decode(&[], &wrong, &HandleIndex::new())
.is_err()
);
}
#[test]
fn decoder_class_names_are_correct() {
assert_eq!(CategoryDecoder.class_name(), "Category");
assert_eq!(SubcategoryDecoder.class_name(), "Subcategory");
}
}