use crate::ids::{brep_id, IdFormat};
use crate::nurbs::reader::LEN_TO_MM;
use crate::sab::{Record, Token};
use cadmpeg_ir::attributes::{AttributeTarget, AttributeValue, SourceAttribute};
use cadmpeg_ir::ids::{AttributeId, Identity, IdentityComponent, UnknownId};
use cadmpeg_ir::topology::Color;
use std::collections::{hash_map::RandomState, HashMap, HashSet};
pub fn collect_attributes(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
entity: &Record,
target: &AttributeTarget,
by_index: &HashMap<i64, &Record, RandomState>,
emitted: &mut HashSet<i64, RandomState>,
out: &mut Vec<SourceAttribute>,
format: IdFormat,
) -> Result<(), cadmpeg_core::CodecError> {
let mut current = entity.ref_at(0);
let mut chain = HashSet::new();
while let Some(index) = current {
if !ctx.insert_hash_set(&mut chain, index, "ASM attribute chain")? {
break;
}
let Some(record) = by_index.get(&index) else {
break;
};
if ctx.insert_hash_set(emitted, index, "ASM emitted attributes")? {
ctx.reserve_vec(out, 1, "ASM source attributes")?;
out.push(source_attribute(
ctx,
record,
target.try_clone_for_decode(ctx, "ASM source attribute target")?,
format,
)?);
}
current = attribute_next(record);
}
Ok(())
}
fn is_integer(token: Option<&Token>) -> bool {
matches!(
token,
Some(Token::Char(_) | Token::Short(_) | Token::Long(_) | Token::Enum(_) | Token::Int64(_))
)
}
#[derive(Clone, Copy)]
enum AttributeBase {
Current,
Legacy,
Compact,
}
impl AttributeBase {
fn next(self) -> usize {
match self {
Self::Current => 2,
Self::Legacy => 1,
Self::Compact => 0,
}
}
fn owner(self) -> Option<usize> {
match self {
Self::Current => Some(4),
Self::Legacy => Some(3),
Self::Compact => None,
}
}
fn payload(self) -> usize {
match self {
Self::Current => 5,
Self::Legacy => 4,
Self::Compact => 1,
}
}
}
fn attribute_base(record: &Record) -> Option<AttributeBase> {
let current = matches!(
(
record.chunk(0),
record.chunk(2),
record.chunk(3),
record.chunk(4),
),
(
Some(Token::Ref(_)),
Some(Token::Ref(_)),
Some(Token::Ref(_)),
Some(Token::Ref(_)),
)
) && is_integer(record.chunk(1));
if current {
return Some(AttributeBase::Current);
}
let legacy = matches!(
(
record.chunk(0),
record.chunk(1),
record.chunk(2),
record.chunk(3),
),
(
Some(Token::Ref(_)),
Some(Token::Ref(_)),
Some(Token::Ref(_)),
Some(Token::Ref(_)),
)
);
if legacy {
return Some(AttributeBase::Legacy);
}
matches!(record.chunk(0), Some(Token::Ref(_))).then_some(AttributeBase::Compact)
}
fn attribute_next(record: &Record) -> Option<i64> {
record.ref_at(attribute_base(record)?.next())
}
pub(super) fn attribute_owner(record: &Record) -> Option<i64> {
record.ref_at(attribute_base(record)?.owner()?)
}
pub fn attribute_key(attribute: &SourceAttribute) -> &str {
attribute
.id
.as_str()
.rsplit('#')
.next()
.unwrap_or(attribute.id.as_str())
}
pub fn source_attribute(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
record: &Record,
target: AttributeTarget,
format: IdFormat,
) -> Result<SourceAttribute, cadmpeg_core::CodecError> {
let mut values = Vec::new();
for token in record.chunks() {
ctx.reserve_vec(&mut values, 1, "ASM attribute values")?;
let value = attribute_value(ctx, token, format)?.ok_or_else(|| {
cadmpeg_core::CodecError::malformed(format_args!(
"attribute record {} ({}) holds a non-finite number",
record.index, record.name
))
})?;
values.push(value);
}
Ok(SourceAttribute {
id: brep_id!(format, AttributeId, "attribute", record.index),
target,
name: ctx.copy_retained_text(&record.name, "ASM attribute record name")?,
values,
})
}
fn attribute_value(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
token: &Token,
format: IdFormat,
) -> Result<Option<AttributeValue>, cadmpeg_core::CodecError> {
Ok(Some(match token {
Token::Char(value) => AttributeValue::Integer(i64::from(*value)),
Token::Short(value) => AttributeValue::Integer(i64::from(*value)),
Token::Long(value) | Token::Enum(value) | Token::Int64(value) => {
AttributeValue::Integer(*value)
}
Token::Float(value) => match AttributeValue::float(f64::from(*value)) {
Some(value) => value,
None => return Ok(None),
},
Token::Double(value) => match AttributeValue::float(*value) {
Some(value) => value,
None => return Ok(None),
},
Token::Str(value) => {
AttributeValue::String(ctx.copy_retained_text(value, "ASM attribute string")?)
}
Token::True => AttributeValue::Boolean(true),
Token::False => AttributeValue::Boolean(false),
Token::Ref(value) => {
AttributeValue::Reference(brep_id!(format, Identity, "entity", value).into_string())
}
Token::SubtypeOpen => AttributeValue::String("subtype_open".into()),
Token::SubtypeClose => AttributeValue::String("subtype_close".into()),
Token::Position(value) | Token::Vector3(value) => match AttributeValue::vector(*value) {
Some(value) => value,
None => return Ok(None),
},
Token::Vector2(value) => match AttributeValue::vector(*value) {
Some(value) => value,
None => return Ok(None),
},
Token::Ident(value) | Token::SubIdent(value) => {
AttributeValue::String(ctx.copy_retained_text(value, "ASM attribute identifier")?)
}
}))
}
pub fn decode_transform(
record: &Record,
header_scale: f64,
) -> Option<cadmpeg_ir::transform::Transform> {
let mut vectors = record.tokens.iter().filter_map(|token| match token {
Token::Position(value) | Token::Vector3(value) => Some(*value),
_ => None,
});
let scale = record
.tokens
.iter()
.filter_map(|token| match token {
Token::Double(value) => Some(*value),
_ => None,
})
.next_back()?;
let (Some(x), Some(y), Some(z), Some(translation), None) = (
vectors.next(),
vectors.next(),
vectors.next(),
vectors.next(),
vectors.next(),
) else {
return None;
};
if scale != 1.0 {
return None;
}
cadmpeg_ir::transform::Transform::affine([
[x[0], y[0], z[0], translation[0] * header_scale * LEN_TO_MM],
[x[1], y[1], z[1], translation[1] * header_scale * LEN_TO_MM],
[x[2], y[2], z[2], translation[2] * header_scale * LEN_TO_MM],
])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectColorCarrier {
NormalizedRgb {
fields: [usize; 3],
},
AutodeskTrueColor {
field: usize,
},
DecimalRgb {
field: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DirectAttributeColor {
pub color: Color,
pub carrier: DirectColorCarrier,
}
fn packed_u32(value: i64) -> Option<u32> {
u32::try_from(value)
.ok()
.or_else(|| i32::try_from(value).ok().map(i32::cast_unsigned))
}
fn packed_rgb(packed: u32) -> Option<Color> {
let red = u8::try_from((packed >> 16) & 0xff).ok()?;
let green = u8::try_from((packed >> 8) & 0xff).ok()?;
let blue = u8::try_from(packed & 0xff).ok()?;
Some(Color::from_rgba8(red, green, blue, 255))
}
fn direct_attribute_color(record: &Record) -> Option<DirectAttributeColor> {
let payload = attribute_base(record)?.payload();
match record.name.as_str() {
"rgb_color-st-attrib" => {
let mut channels = record.chunks().enumerate().skip(payload).filter_map(
|(field, token)| match token {
Token::Double(value) => Some((field, *value)),
_ => None,
},
);
let channels = [
channels.next(),
channels.next(),
channels.next(),
channels.next(),
channels.next(),
];
let [(r_field, r), (g_field, g), (b_field, b)] = match channels {
[Some(red), Some(green), Some(blue), None, None] => [red, green, blue],
[Some(red), Some(green), Some(blue), Some((_, 1.0)), None] => [red, green, blue],
_ => return None,
};
if ![r, g, b]
.into_iter()
.all(|value| (0.0..=1.0).contains(&value))
{
return None;
}
Some(DirectAttributeColor {
color: Color::new(
cadmpeg_core::convert::f32_from_f64(r)?,
cadmpeg_core::convert::f32_from_f64(g)?,
cadmpeg_core::convert::f32_from_f64(b)?,
1.0,
)?,
carrier: DirectColorCarrier::NormalizedRgb {
fields: [r_field, g_field, b_field],
},
})
}
"truecolor-adesk-attrib" => {
let (field, packed) = record
.chunks()
.enumerate()
.skip(payload)
.filter_map(|(field, token)| match token {
Token::Int64(value) | Token::Long(value) => Some((field, *value)),
_ => None,
})
.last()?;
let packed = packed_u32(packed)?;
if packed >> 24 != 0xc2 {
return None;
}
Some(DirectAttributeColor {
color: packed_rgb(packed)?,
carrier: DirectColorCarrier::AutodeskTrueColor { field },
})
}
"entatt_color-bt-attrib" => {
let (field, text) = record
.chunks()
.enumerate()
.skip(payload)
.filter_map(|(field, token)| match token {
Token::Str(value) => Some((field, value.as_str())),
_ => None,
})
.last()?;
if text.is_empty() || !text.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let packed = text
.parse::<u32>()
.ok()
.filter(|value| *value <= 0xff_ffff)?;
Some(DirectAttributeColor {
color: packed_rgb(packed)?,
carrier: DirectColorCarrier::DecimalRgb { field },
})
}
_ => None,
}
}
pub fn attribute_chain_color_carrier<'a>(
entity: &Record,
max_steps: usize,
mut by_index: impl FnMut(i64) -> Option<&'a Record>,
) -> Option<(&'a Record, DirectAttributeColor)> {
let mut current = entity.ref_at(0)?;
for _ in 0..max_steps {
let record = by_index(current)?;
if let Some(color) = direct_attribute_color(record) {
return Some((record, color));
}
current = attribute_next(record)?;
}
None
}
pub fn attribute_chain_color(
entity: &Record,
by_index: &HashMap<i64, &Record, RandomState>,
) -> Option<Color> {
attribute_chain_color_carrier(entity, by_index.len(), |index| {
by_index.get(&index).copied()
})
.map(|(_, decoded)| decoded.color)
}
pub fn attribute_chain_name(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
entity: &Record,
by_index: &HashMap<i64, &Record, RandomState>,
) -> Result<Option<String>, cadmpeg_core::CodecError> {
let Some(mut current) = entity.ref_at(0) else {
return Ok(None);
};
for _ in 0..by_index.len() {
let Some(record) = by_index.get(¤t) else {
return Ok(None);
};
if record.name == "string_attrib-name_attrib-gen-attrib" {
let mut values = record.tokens.iter().filter_map(|token| match token {
Token::Str(value) => Some(value.as_str()),
_ => None,
});
let mut previous = None;
let mut last = None;
for value in &mut values {
previous = last;
last = Some(value);
}
if let (Some("name"), Some(value)) = (previous, last) {
if !value.is_empty() {
let name = ctx.copy_retained_text(value, "ASM attribute name")?;
return Ok(Some(name));
}
}
}
let Some(next) = attribute_next(record) else {
return Ok(None);
};
current = next;
}
Ok(None)
}
pub fn unknown_record_id(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
rec: &Record,
format: IdFormat,
) -> Result<UnknownId, cadmpeg_core::CodecError> {
let head = if rec.head().is_empty() {
"untyped-record"
} else {
rec.head()
};
let name = ctx.copy_retained_text(head, "ASM unknown record kind")?;
let kind = IdentityComponent::try_new(name).map_err(|error| {
cadmpeg_core::CodecError::malformed(format_args!(
"invalid ASM source identity component: {error}"
))
})?;
Ok(UnknownId::from(
format.try_brep_identity(ctx, &kind, rec.index)?,
))
}
#[cfg(test)]
mod tests {
use super::decode_transform;
use crate::sab::{Record, Token};
#[test]
fn unknown_record_identity_refuses_retained_limit() {
use cadmpeg_core::decode::{DecodeArena, DecodeContext, DecodePolicy, ResourceDimension};
use cadmpeg_core::CodecError;
let record = Record {
index: 1,
name: "mystery".into(),
tokens: std::sync::Arc::from([]),
offset: 0,
len: 0,
};
let expected = "f3d:brep:mystery#1";
let arena = DecodeArena::new();
let mut policy = DecodePolicy::service();
policy.limits.max_retained_bytes =
7 + cadmpeg_core::decode::u64_from_index(expected.len()) - 1;
let (ctx, _) = DecodeContext::from_root_bytes(&[], &arena, &policy)
.expect("empty test input fits input limit");
let error = super::unknown_record_id(&ctx, &record, crate::asm_format!("f3d"))
.expect_err("dynamic identity exceeds retained limit");
let CodecError::ResourceLimit(refusal) = error else {
panic!("expected resource refusal, got {error:?}");
};
assert_eq!(refusal.dimension, ResourceDimension::RetainedBytes);
assert_eq!(refusal.operation, "ASM unknown record identity");
}
fn transform_record(scale: f64, x: [f64; 3]) -> Record {
Record {
index: 0,
name: "transform".into(),
tokens: std::sync::Arc::from([
Token::Vector3(x),
Token::Vector3([0.0, 1.0, 0.0]),
Token::Vector3([0.0, 0.0, 1.0]),
Token::Position([0.0, 0.0, 0.0]),
Token::Double(scale),
]),
offset: 0,
len: 0,
}
}
#[test]
fn transform_decode_propagates_affine_constructor_rejection() {
let identity = transform_record(1.0, [1.0, 0.0, 0.0]);
assert_eq!(
decode_transform(&identity, 1.0),
Some(cadmpeg_ir::transform::Transform::identity())
);
for scale in [0.0, 2.0, f64::NAN, f64::INFINITY] {
assert!(decode_transform(&transform_record(scale, [1.0, 0.0, 0.0]), 1.0).is_none());
}
assert!(decode_transform(&transform_record(1.0, [f64::NAN, 0.0, 0.0]), 1.0).is_none());
assert!(decode_transform(&identity, f64::INFINITY).is_none());
}
}