pub mod annotations;
pub mod attributes;
mod emit;
pub mod geometry;
pub mod key_maps;
pub mod records;
pub mod stats;
use stats::Stats;
mod topology;
pub mod transfer;
use crate::asm_header;
use crate::ids::IdFormat;
use crate::nurbs;
use crate::nurbs::proc_curve::ProceduralCurveConstruction;
use crate::nurbs::proc_surface::DecodedProceduralSurface;
use crate::sab::Record;
use cadmpeg_ir::attributes::{AttributeTarget, SourceAttribute};
use cadmpeg_ir::geometry::{
pcurve::{Pcurve, PcurveGeometry},
Curve, CurveGeometry, ProceduralCurve, ProceduralSurface, Surface, SurfaceGeometry,
};
use cadmpeg_ir::ids::Identity;
use cadmpeg_ir::ids::{CurveId, SurfaceId};
use cadmpeg_ir::topology::{Body, Coedge, Edge, Face, Loop, Point, Region, Shell, Vertex};
use cadmpeg_ir::unknown::UnknownRecord;
use serde::{Deserialize, Serialize};
use serde_value::Value;
use std::collections::{HashMap, HashSet};
use self::annotations::{emit_annotation_records, AnnotationRecord};
use self::attributes::attribute_owner;
use self::emit::{
count_other_records, emit_attributes, emit_carrier_records, emit_coedges, emit_containers,
emit_edges, emit_faces, emit_loops, emit_passthrough_unknowns, emit_pcurves, emit_points,
emit_vertices, CoedgeDecodeInputs, ContainerInputs, CurveSenseRefs,
};
use self::geometry::{clamp_edge_ranges_to_carrier_domains, classify_body_kinds};
use self::records::{
BodyNativeKey, EdgeContinuity, EdgeOwnership, FaceNativeKey, FaceSidedness,
MeshSurfaceSentinel, TolerantCoedgeParameters, TolerantEdgeTail, TolerantVertexTail,
TransformHints, VertexOwnership, WireTopology,
};
use self::topology::{
classify_edge_curve_senses, collect_wire_topology, decode_analytic_carriers,
keep_faces_and_carriers, walk_reachable_topology, TopologyContext,
};
#[derive(Default, Serialize)]
pub struct AsmBrep {
pub bodies: Vec<Body>,
pub regions: Vec<Region>,
pub shells: Vec<Shell>,
pub faces: Vec<Face>,
pub loops: Vec<Loop>,
pub coedges: Vec<Coedge>,
pub edges: Vec<Edge>,
pub vertices: Vec<Vertex>,
pub points: Vec<Point>,
pub surfaces: Vec<Surface>,
pub curves: Vec<Curve>,
pub pcurves: Vec<Pcurve>,
pub procedural_surfaces: Vec<(SurfaceId, ProceduralSurface)>,
pub procedural_curves: Vec<(CurveId, ProceduralCurve)>,
pub edge_continuities: Vec<EdgeContinuity>,
pub edge_ownerships: Vec<EdgeOwnership>,
pub vertex_ownerships: Vec<VertexOwnership>,
pub face_sidedness: Vec<FaceSidedness>,
pub face_native_keys: Vec<FaceNativeKey>,
pub tolerant_coedge_parameters: Vec<TolerantCoedgeParameters>,
pub tolerant_edge_tails: Vec<TolerantEdgeTail>,
pub tolerant_vertex_tails: Vec<TolerantVertexTail>,
pub mesh_surface_sentinels: Vec<MeshSurfaceSentinel>,
pub transform_hints: Vec<TransformHints>,
pub body_native_keys: Vec<BodyNativeKey>,
pub wire_topologies: Vec<WireTopology>,
pub attributes: Vec<SourceAttribute>,
pub unknowns: Vec<UnknownRecord>,
pub stats: Stats,
#[serde(skip)]
pub annotation_records: Vec<AnnotationRecord>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct AsmBrepWire {
bodies: Vec<Body>,
regions: Vec<Region>,
shells: Vec<Shell>,
faces: Vec<Face>,
loops: Vec<Loop>,
coedges: Vec<Coedge>,
edges: Vec<Edge>,
vertices: Vec<Vertex>,
points: Vec<Point>,
surfaces: Vec<Surface>,
curves: Vec<Curve>,
pcurves: Vec<Pcurve>,
procedural_surfaces: Vec<(SurfaceId, ProceduralSurface)>,
procedural_curves: Vec<(CurveId, ProceduralCurve)>,
edge_continuities: Vec<EdgeContinuity>,
edge_ownerships: Vec<EdgeOwnership>,
vertex_ownerships: Vec<VertexOwnership>,
face_sidedness: Vec<FaceSidedness>,
face_native_keys: Vec<FaceNativeKey>,
tolerant_coedge_parameters: Vec<TolerantCoedgeParameters>,
tolerant_edge_tails: Vec<TolerantEdgeTail>,
tolerant_vertex_tails: Vec<TolerantVertexTail>,
mesh_surface_sentinels: Vec<MeshSurfaceSentinel>,
transform_hints: Vec<TransformHints>,
body_native_keys: Vec<BodyNativeKey>,
wire_topologies: Vec<WireTopology>,
attributes: Vec<SourceAttribute>,
unknowns: Vec<UnknownRecord>,
stats: Stats,
}
impl<'de> Deserialize<'de> for AsmBrep {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = AsmBrepWire::deserialize(deserializer)?;
Ok(Self {
bodies: wire.bodies,
regions: wire.regions,
shells: wire.shells,
faces: wire.faces,
loops: wire.loops,
coedges: wire.coedges,
edges: wire.edges,
vertices: wire.vertices,
points: wire.points,
surfaces: wire.surfaces,
curves: wire.curves,
pcurves: wire.pcurves,
procedural_surfaces: wire.procedural_surfaces,
procedural_curves: wire.procedural_curves,
edge_continuities: wire.edge_continuities,
edge_ownerships: wire.edge_ownerships,
vertex_ownerships: wire.vertex_ownerships,
face_sidedness: wire.face_sidedness,
face_native_keys: wire.face_native_keys,
tolerant_coedge_parameters: wire.tolerant_coedge_parameters,
tolerant_edge_tails: wire.tolerant_edge_tails,
tolerant_vertex_tails: wire.tolerant_vertex_tails,
mesh_surface_sentinels: wire.mesh_surface_sentinels,
transform_hints: wire.transform_hints,
body_native_keys: wire.body_native_keys,
wire_topologies: wire.wire_topologies,
attributes: wire.attributes,
unknowns: wire.unknowns,
stats: wire.stats,
annotation_records: Vec::new(),
})
}
}
impl AsmBrep {
pub fn append(
&mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
mut other: Self,
) -> Result<(), cadmpeg_core::CodecError> {
macro_rules! append_vecs {
($($field:ident),+ $(,)?) => {
$(ctx.append_vec(&mut self.$field, &mut other.$field, concat!("ASM append ", stringify!($field)))?;)+
};
}
append_vecs!(
bodies,
regions,
shells,
faces,
loops,
coedges,
edges,
vertices,
points,
surfaces,
curves,
pcurves,
procedural_surfaces,
procedural_curves,
edge_continuities,
edge_ownerships,
vertex_ownerships,
face_sidedness,
face_native_keys,
tolerant_coedge_parameters,
tolerant_edge_tails,
tolerant_vertex_tails,
mesh_surface_sentinels,
transform_hints,
body_native_keys,
wire_topologies,
attributes,
unknowns,
annotation_records,
);
self.stats.merge(ctx, other.stats)?;
Ok(())
}
}
pub fn collect_owned_ids(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
value: &Value,
out: &mut HashSet<String, std::collections::hash_map::RandomState>,
) -> Result<(), cadmpeg_core::CodecError> {
let _depth = ctx.enter_nested("collect ASM owned ids")?;
match value {
Value::Map(fields) => {
if let Some(id) = fields
.iter()
.find(|(key, _)| matches!(key, Value::String(name) if name == "id"))
.map(|(_, value)| value)
.and_then(value_string)
{
ctx.insert_string_set(out, id, "ASM owned ids")?;
}
for (key, value) in fields {
collect_owned_ids(ctx, key, out)?;
collect_owned_ids(ctx, value, out)?;
}
}
Value::Seq(items) => {
for item in items {
collect_owned_ids(ctx, item, out)?;
}
}
Value::Option(Some(value)) | Value::Newtype(value) => collect_owned_ids(ctx, value, out)?,
_ => {}
}
Ok(())
}
pub fn value_string(value: &Value) -> Option<&str> {
match value {
Value::String(value) => Some(value),
Value::Newtype(value) => value_string(value),
_ => None,
}
}
pub fn collect_entity_adjacency(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
value: &Value,
owned: &HashSet<String, std::collections::hash_map::RandomState>,
out: &mut HashMap<
String,
HashSet<String, std::collections::hash_map::RandomState>,
std::collections::hash_map::RandomState,
>,
) -> Result<(), cadmpeg_core::CodecError> {
let Value::Map(fields) = value else {
return Ok(());
};
for value in fields.values() {
let Value::Seq(items) = value else {
continue;
};
for item in items {
let Some(id) = entity_id(item) else {
continue;
};
let mut references = HashSet::new();
collect_references(ctx, item, owned, &mut references)?;
references.remove(id);
for reference in references {
insert_adjacency(ctx, out, id, &reference)?;
insert_adjacency(ctx, out, &reference, id)?;
}
}
}
Ok(())
}
fn insert_adjacency(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
out: &mut HashMap<String, HashSet<String>>,
owner: &str,
reference: &str,
) -> Result<(), cadmpeg_core::CodecError> {
if !out.contains_key(owner) {
let key = ctx.copy_retained_text(owner, "ASM adjacency owner")?;
ctx.reserve_map(out, 1, "ASM adjacency owners")?;
out.insert(key, HashSet::new());
}
if let Some(references) = out.get_mut(owner) {
ctx.insert_string_set(references, reference, "ASM adjacency references")?;
}
Ok(())
}
pub fn entity_id(value: &Value) -> Option<&str> {
let Value::Map(fields) = value else {
return None;
};
fields
.iter()
.find(|(key, _)| matches!(key, Value::String(name) if name == "id"))
.map(|(_, value)| value)
.and_then(value_string)
}
pub fn collect_references(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
value: &Value,
owned: &HashSet<String, std::collections::hash_map::RandomState>,
out: &mut HashSet<String, std::collections::hash_map::RandomState>,
) -> Result<(), cadmpeg_core::CodecError> {
let _depth = ctx.enter_nested("collect ASM references")?;
match value {
Value::String(id) if owned.contains(id) => {
ctx.insert_string_set(out, id, "ASM references")?;
}
Value::Seq(items) => {
for item in items {
collect_references(ctx, item, owned, out)?;
}
}
Value::Map(fields) => {
for (key, value) in fields {
collect_references(ctx, key, owned, out)?;
collect_references(ctx, value, owned, out)?;
}
}
Value::Option(Some(value)) | Value::Newtype(value) => {
collect_references(ctx, value, owned, out)?;
}
_ => {}
}
Ok(())
}
pub fn retain_root_entities(
value: &mut Value,
reachable: &HashSet<String, std::collections::hash_map::RandomState>,
) {
let Value::Map(fields) = value else {
return;
};
for value in fields.values_mut() {
if let Value::Seq(items) = value {
items.retain(|item| entity_id(item).is_none_or(|id| reachable.contains(id)));
}
}
}
pub fn remap_owned_ids(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
value: &mut Value,
replacements: &HashMap<String, String, std::collections::hash_map::RandomState>,
) -> Result<(), cadmpeg_core::CodecError> {
let _depth = ctx.enter_nested("remap ASM owned ids")?;
match value {
Value::String(id) => {
if let Some(replacement) = replacements.get(id) {
*id = ctx.copy_retained_text(replacement, "ASM remapped id")?;
}
}
Value::Seq(items) => {
for item in items {
remap_owned_ids(ctx, item, replacements)?;
}
}
Value::Map(fields) => {
let entries = std::mem::take(fields);
for (mut key, mut item) in entries {
remap_owned_ids(ctx, &mut key, replacements)?;
remap_owned_ids(ctx, &mut item, replacements)?;
ctx.insert_btree_map(fields, key, item, "ASM remapped fields")?;
}
}
Value::Option(Some(value)) | Value::Newtype(value) => {
remap_owned_ids(ctx, value, replacements)?;
}
_ => {}
}
Ok(())
}
fn count_kind(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
counts: &mut std::collections::BTreeMap<String, usize>,
kind: &str,
) -> Result<(), cadmpeg_core::CodecError> {
if let Some(count) = counts.get_mut(kind) {
*count += 1;
return Ok(());
}
let key = ctx.copy_retained_text(kind, "ASM loss kind")?;
ctx.insert_btree_map(counts, key, 1, "ASM loss kind")?;
Ok(())
}
pub fn id(format: IdFormat, index: i64) -> Identity {
format.brep_identity(&cadmpeg_ir::identity_component!("entity"), index)
}
enum ProceduralCurveSource {
Cached {
construction: Box<ProceduralCurveConstruction>,
cache_fit_tolerance: Option<f64>,
parsed_domain: Option<[f64; 2]>,
},
Cacheless(Box<cadmpeg_ir::geometry::ProceduralCurveDefinition>),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct PcurveRecordIndex(i64);
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct CoedgeRecordIndex(i64);
#[derive(Default)]
struct Carriers {
procedural_support_sources: Vec<(i64, SurfaceId)>,
procedural_curve_child_sources: Vec<(i64, CurveId)>,
surface_geo: HashMap<i64, SurfaceGeometry>,
procedural_surface_defs: HashMap<i64, DecodedProceduralSurface>,
curve_geo: HashMap<i64, CurveGeometry>,
procedural_curve_defs: HashMap<i64, ProceduralCurveSource>,
pcurve_geo: HashMap<PcurveRecordIndex, PcurveGeometry>,
pcurve_parameter_ranges: HashMap<CoedgeRecordIndex, [f64; 2]>,
}
#[derive(Default)]
struct Reachable {
faces: HashSet<i64>,
loops: HashSet<i64>,
coedges: HashSet<i64>,
edges: HashSet<i64>,
vertices: HashSet<i64>,
points: HashSet<i64>,
surfaces: HashSet<i64>,
curves: HashSet<i64>,
pcurves: HashSet<i64>,
unknown_surface_records: HashSet<i64>,
cached_unknown_procedural_surfaces: HashSet<i64>,
undecoded_carriers: HashSet<i64>,
}
#[derive(Default)]
struct WireShellTopology {
wire_edges_by_shell: HashMap<i64, Vec<i64>>,
free_vertices_by_shell: HashMap<i64, Vec<i64>>,
saved_free_edges: Vec<i64>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum DecodePurpose {
Model,
History,
}
pub fn decode_with_purpose(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
records: &[Record],
bytes: &[u8],
stream: &str,
format: IdFormat,
purpose: DecodePurpose,
) -> Result<AsmBrep, cadmpeg_core::CodecError> {
let header = asm_header::parse(ctx, bytes)?.map(|header| header.metadata);
decode_with_header(
ctx,
records,
bytes,
header.as_ref(),
stream,
format,
purpose,
)
}
pub fn decode_with_header(
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
records: &[Record],
bytes: &[u8],
header: Option<&crate::kernel_header::KernelHeader>,
stream: &str,
format: IdFormat,
purpose: DecodePurpose,
) -> Result<AsmBrep, cadmpeg_core::CodecError> {
let mut out = AsmBrep::default();
let record_slots = cadmpeg_core::decode::u64_from_index(records.len());
let record_slot_bytes =
cadmpeg_core::decode::u64_from_index(std::mem::size_of::<(i64, &Record)>());
let record_index_bytes = record_slots
.checked_mul(record_slot_bytes)
.ok_or_else(|| ctx.refuse_codec_limit("ASM record index bytes", u64::MAX, u64::MAX))?;
let _record_index_reservation = ctx.reserve_scoped(record_index_bytes, "index ASM records")?;
let mut by_index: HashMap<i64, &Record> = HashMap::new();
ctx.reserve_map(&mut by_index, records.len(), "index ASM records")?;
for record in records {
by_index.insert(
i64::try_from(record.index).map_err(|_| {
ctx.refuse_codec_limit(
"ASM record index",
9_223_372_036_854_775_807,
cadmpeg_core::decode::u64_from_index(record.index),
)
})?,
record,
);
}
let token_count = records.iter().try_fold(0_u64, |count, record| {
let record_tokens = cadmpeg_core::decode::u64_from_index(record.tokens.len());
count
.checked_add(record_tokens)
.ok_or_else(|| ctx.refuse_codec_limit("ASM subtype scan work", u64::MAX, u64::MAX))
})?;
ctx.charge_work(token_count, "scan ASM subtype definitions")?;
let definition_count = records
.iter()
.flat_map(|record| {
record
.tokens
.iter()
.enumerate()
.filter_map(move |(position, token)| {
if !matches!(token, crate::sab::Token::SubtypeOpen) {
return None;
}
match record.tokens.get(position + 1) {
Some(
crate::sab::Token::Ident(name) | crate::sab::Token::SubIdent(name),
) if name != "ref" => Some(()),
_ => None,
}
})
})
.count();
let definition_slots = cadmpeg_core::decode::u64_from_index(definition_count);
let definition_slot_bytes = cadmpeg_core::decode::u64_from_index(std::mem::size_of::<(
std::sync::Arc<[crate::sab::Token]>,
usize,
)>());
let definition_bytes = definition_slots
.checked_mul(definition_slot_bytes)
.ok_or_else(|| ctx.refuse_codec_limit("ASM subtype index bytes", u64::MAX, u64::MAX))?;
let _subtype_index_reservation =
ctx.reserve_scoped(definition_bytes, "index ASM subtype definitions")?;
let token_table = nurbs::toks::SubtypeTable::from_records(ctx, records)?
.with_save_format_version(header.and_then(|header| header.save_format_version));
nurbs::toks::admit_subtype_references(ctx, records, &token_table)?;
let save_format_major = header.and_then(crate::kernel_header::KernelHeader::save_format_major);
let saved_entity_limit = header
.and_then(|header| header.entity_count)
.and_then(|count| i64::try_from(count).ok());
let header_scale = header.and_then(|header| header.scale).unwrap_or(1.0);
let (mut carriers, inward_normal_surfaces) = decode_analytic_carriers(ctx, records)?;
let mut reach = Reachable::default();
let topology_context = TopologyContext {
ctx,
by_index: &by_index,
token_table: &token_table,
purpose,
format,
};
keep_faces_and_carriers(
topology_context,
&mut out,
records,
&mut carriers,
&mut reach,
)?;
walk_reachable_topology(topology_context, &mut out, &mut carriers, &mut reach)?;
let wire = collect_wire_topology(
topology_context,
&mut out,
records,
saved_entity_limit,
&mut carriers,
&mut reach,
)?;
let (reversed_curve_refs, forward_curve_refs) =
classify_edge_curve_senses(ctx, records, &reach)?;
emit_carrier_records(
ctx,
&mut out,
records,
&mut carriers,
&reach,
CurveSenseRefs {
reversed_curve_refs: &reversed_curve_refs,
forward_curve_refs: &forward_curve_refs,
},
format,
)?;
emit_pcurves(ctx, &mut out, records, &mut carriers, &reach, format)?;
emit_points(ctx, &mut out, records, &reach, format)?;
emit_vertices(ctx, &mut out, records, &by_index, &reach, format)?;
emit_edges(
ctx,
&mut out,
records,
&by_index,
&reach,
CurveSenseRefs {
reversed_curve_refs: &reversed_curve_refs,
forward_curve_refs: &forward_curve_refs,
},
format,
)?;
emit_coedges(
ctx,
&mut out,
records,
CoedgeDecodeInputs {
token_table: &token_table,
save_format_major,
},
&carriers,
&reach,
format,
)?;
emit_loops(ctx, &mut out, records, &by_index, &reach, format)?;
emit_faces(
ctx,
&mut out,
records,
&by_index,
&reach,
&inward_normal_surfaces,
format,
)?;
emit_containers(
ctx,
&mut out,
ContainerInputs {
records,
bytes,
by_index: &by_index,
reach: &reach,
wire: &wire,
stream,
header_scale,
format,
},
)?;
let emitted_attributes = emit_attributes(ctx, &mut out, records, &by_index, &reach, format)?;
if purpose == DecodePurpose::Model {
emit_passthrough_unknowns(ctx, &mut out, records, bytes, &reach, format)?;
count_other_records(ctx, &mut out, records, &reach, &emitted_attributes)?;
emit_annotation_records(ctx, &mut out, records, &by_index, &carriers, stream, format)?;
classify_body_kinds(ctx, &mut out)?;
clamp_edge_ranges_to_carrier_domains(ctx, &mut out)?;
}
Ok(out)
}
fn inherited_attribute_target(
mut owner: i64,
by_index: &HashMap<i64, &Record>,
targets: &HashMap<i64, AttributeTarget>,
) -> Option<AttributeTarget> {
for _ in 0..=by_index.len() {
if let Some(target) = targets.get(&owner) {
return Some(target.clone());
}
let attribute = by_index.get(&owner)?;
if !attribute.name.ends_with("-attrib") {
return None;
}
owner = attribute_owner(attribute)?;
}
None
}
#[cfg(test)]
mod tests;
mod identity_rewrite;