pub mod attributes;
mod emit;
pub mod geometry;
pub mod records;
mod topology;
use crate::asm_header;
use crate::ids::IdFormat;
use crate::nurbs;
use crate::nurbs::proc_curve::{
CompoundDefinition, EmbeddedDeformable, EmbeddedIntersection, EmbeddedLawCurve,
EmbeddedProjection, EmbeddedSilhouette, EmbeddedSpring, EmbeddedSurfaceOffset,
EmbeddedThreeSurfaceIntersection, EmbeddedTwoSidedOffset, SubsetDefinition,
VectorOffsetDefinition,
};
use crate::nurbs::proc_surface::DecodedProceduralSurface;
use crate::sab::Record;
use cadmpeg_ir::attributes::{AttributeTarget, SourceAttribute};
use cadmpeg_ir::geometry::{
Curve, CurveGeometry, Pcurve, PcurveGeometry, ProceduralCurve, ProceduralSurface, Surface,
SurfaceGeometry,
};
use cadmpeg_ir::ids::BodyId;
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::emit::{
count_other_records, emit_annotation_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, project_subshell_faces,
};
use self::geometry::{clamp_edge_ranges_to_carrier_domains, classify_body_kinds};
use self::records::{
BodyNativeKey, EdgeContinuity, EdgeOwnership, 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,
};
pub(crate) fn embedded_pcurve_geometry(pcurve: nurbs::pcurve::NurbsPcurve) -> PcurveGeometry {
PcurveGeometry::Nurbs {
degree: pcurve.degree,
knots: pcurve.knots,
control_points: pcurve.control_points,
weights: pcurve.weights,
periodic: pcurve.periodic,
}
}
#[derive(Default, Serialize, Deserialize)]
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<ProceduralSurface>,
pub procedural_curves: Vec<ProceduralCurve>,
pub edge_continuities: Vec<EdgeContinuity>,
pub edge_ownerships: Vec<EdgeOwnership>,
pub vertex_ownerships: Vec<VertexOwnership>,
pub face_sidedness: Vec<FaceSidedness>,
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_keys: HashMap<BodyId, u64>,
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>,
}
pub struct AnnotationRecord {
pub id: String,
pub stream: String,
pub offset: u64,
pub tag: String,
pub derived_fields: Vec<&'static str>,
}
#[derive(Default, Serialize, Deserialize)]
pub struct Stats {
pub missing_face_surfaces: usize,
pub missing_face_surface_kinds: std::collections::BTreeMap<String, usize>,
pub unknown_surface_faces: usize,
pub unknown_surface_kinds: std::collections::BTreeMap<String, usize>,
pub mesh_surface_faces: usize,
pub nurbs_surfaces: usize,
pub nurbs_curves: usize,
pub procedural_curve_edges: usize,
pub procedural_curve_kinds: std::collections::BTreeMap<String, usize>,
pub undecoded_pcurve_refs: usize,
pub undecoded_pcurve_kinds: std::collections::BTreeMap<String, usize>,
pub partial_procedural_supports: usize,
pub other_records: usize,
pub other_record_kinds: std::collections::BTreeMap<String, usize>,
}
impl AsmBrep {
pub fn append(&mut self, mut other: Self) {
macro_rules! append_vecs {
($($field:ident),+ $(,)?) => {
$(self.$field.append(&mut other.$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,
tolerant_coedge_parameters,
tolerant_edge_tails,
tolerant_vertex_tails,
mesh_surface_sentinels,
transform_hints,
body_native_keys,
wire_topologies,
attributes,
unknowns,
annotation_records,
);
self.body_keys.extend(other.body_keys);
self.stats.merge(other.stats);
}
}
impl Stats {
fn merge(&mut self, other: Self) {
macro_rules! add_counts {
($($field:ident),+ $(,)?) => {
$(self.$field += other.$field;)+
};
}
add_counts!(
missing_face_surfaces,
unknown_surface_faces,
mesh_surface_faces,
nurbs_surfaces,
nurbs_curves,
procedural_curve_edges,
undecoded_pcurve_refs,
partial_procedural_supports,
other_records,
);
for (target, source) in [
(
&mut self.missing_face_surface_kinds,
other.missing_face_surface_kinds,
),
(&mut self.unknown_surface_kinds, other.unknown_surface_kinds),
(
&mut self.procedural_curve_kinds,
other.procedural_curve_kinds,
),
(
&mut self.undecoded_pcurve_kinds,
other.undecoded_pcurve_kinds,
),
(&mut self.other_record_kinds, other.other_record_kinds),
] {
for (kind, count) in source {
*target.entry(kind).or_default() += count;
}
}
}
}
#[allow(
clippy::implicit_hasher,
reason = "Callers pass default-hasher collections; a hasher parameter adds generic noise for one call shape."
)]
pub fn collect_owned_ids(value: &Value, out: &mut HashSet<String>) {
match value {
Value::Map(fields) => {
if let Some(id) = fields
.get(&Value::String("id".into()))
.and_then(value_string)
{
out.insert(id.to_owned());
}
for (key, value) in fields {
collect_owned_ids(key, out);
collect_owned_ids(value, out);
}
}
Value::Seq(items) => {
for item in items {
collect_owned_ids(item, out);
}
}
Value::Option(Some(value)) | Value::Newtype(value) => collect_owned_ids(value, out),
_ => {}
}
}
pub fn value_string(value: &Value) -> Option<&str> {
match value {
Value::String(value) => Some(value),
Value::Newtype(value) => value_string(value),
_ => None,
}
}
#[allow(
clippy::implicit_hasher,
reason = "Callers pass default-hasher collections; a hasher parameter adds generic noise for one call shape."
)]
pub fn collect_entity_adjacency(
value: &Value,
owned: &HashSet<String>,
out: &mut HashMap<String, HashSet<String>>,
) {
let Value::Map(fields) = value else {
return;
};
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(item, owned, &mut references);
references.remove(id);
for reference in references {
out.entry(id.to_owned())
.or_default()
.insert(reference.clone());
out.entry(reference).or_default().insert(id.to_owned());
}
}
}
}
pub fn entity_id(value: &Value) -> Option<&str> {
let Value::Map(fields) = value else {
return None;
};
fields
.get(&Value::String("id".into()))
.and_then(value_string)
}
#[allow(
clippy::implicit_hasher,
reason = "Callers pass default-hasher collections; a hasher parameter adds generic noise for one call shape."
)]
pub fn collect_references(value: &Value, owned: &HashSet<String>, out: &mut HashSet<String>) {
match value {
Value::String(id) if owned.contains(id) => {
out.insert(id.clone());
}
Value::Seq(items) => {
for item in items {
collect_references(item, owned, out);
}
}
Value::Map(fields) => {
for (key, value) in fields {
collect_references(key, owned, out);
collect_references(value, owned, out);
}
}
Value::Option(Some(value)) | Value::Newtype(value) => {
collect_references(value, owned, out);
}
_ => {}
}
}
#[allow(
clippy::implicit_hasher,
reason = "Callers pass default-hasher collections; a hasher parameter adds generic noise for one call shape."
)]
pub fn retain_root_entities(value: &mut Value, reachable: &HashSet<String>) {
let Value::Map(fields) = value else {
return;
};
for value in fields.values_mut() {
let Value::Seq(items) = value else {
continue;
};
items.retain(|item| entity_id(item).is_none_or(|id| reachable.contains(id)));
}
}
#[allow(
clippy::implicit_hasher,
reason = "Callers pass default-hasher collections; a hasher parameter adds generic noise for one call shape."
)]
pub fn remap_owned_ids(value: &mut Value, replacements: &HashMap<String, String>) {
match value {
Value::String(id) => {
if let Some(replacement) = replacements.get(id) {
id.clone_from(replacement);
}
}
Value::Seq(items) => {
for item in items {
remap_owned_ids(item, replacements);
}
}
Value::Map(fields) => {
let entries = std::mem::take(fields);
for (mut key, mut item) in entries {
remap_owned_ids(&mut key, replacements);
remap_owned_ids(&mut item, replacements);
fields.insert(key, item);
}
}
Value::Option(Some(value)) | Value::Newtype(value) => {
remap_owned_ids(value, replacements);
}
_ => {}
}
}
pub(crate) fn count_kind(counts: &mut std::collections::BTreeMap<String, usize>, kind: &str) {
*counts.entry(kind.to_owned()).or_default() += 1;
}
pub fn id(format: IdFormat<'_>, index: i64) -> String {
format!("{format}:brep:entity#{index}")
}
type ProceduralCurveTail = (
String,
Option<cadmpeg_ir::geometry::ProceduralCurveDefinition>,
Option<VectorOffsetDefinition>,
Option<SubsetDefinition>,
Option<CompoundDefinition>,
Option<EmbeddedTwoSidedOffset>,
Option<(EmbeddedIntersection, bool)>,
Option<EmbeddedThreeSurfaceIntersection>,
Option<(
cadmpeg_ir::geometry::SurfaceCurveFamily,
EmbeddedIntersection,
Option<cadmpeg_ir::geometry::SurfaceCurveTail>,
)>,
Option<EmbeddedSilhouette>,
Option<EmbeddedSurfaceOffset>,
Option<EmbeddedSpring>,
Option<EmbeddedDeformable>,
Option<EmbeddedProjection>,
Option<EmbeddedLawCurve>,
Option<f64>,
);
#[derive(Default)]
pub(crate) struct Carriers {
surface_geo: HashMap<i64, (SurfaceGeometry, bool)>,
procedural_surface_defs: HashMap<i64, DecodedProceduralSurface>,
curve_geo: HashMap<i64, CurveGeometry>,
procedural_curve_defs: HashMap<i64, ProceduralCurveTail>,
cacheless_procedural_curve_defs:
HashMap<i64, (String, cadmpeg_ir::geometry::ProceduralCurveDefinition)>,
pcurve_geo: HashMap<i64, PcurveGeometry>,
pcurve_parameter_ranges: HashMap<i64, [f64; 2]>,
}
#[derive(Default)]
pub(crate) 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)]
pub(crate) 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(
records: &[Record],
bytes: &[u8],
stream: &str,
format: IdFormat<'_>,
purpose: DecodePurpose,
) -> AsmBrep {
let header = asm_header::parse(bytes);
decode_with_header(records, bytes, header, stream, format, purpose)
}
pub fn decode_with_header(
records: &[Record],
bytes: &[u8],
header: Option<crate::asm_header::AsmHeader>,
stream: &str,
format: IdFormat<'_>,
purpose: DecodePurpose,
) -> AsmBrep {
let mut out = AsmBrep::default();
let by_index: HashMap<i64, &Record> = records.iter().map(|r| (r.index as i64, r)).collect();
let token_table = nurbs::toks::SubtypeTable::from_records(records).with_save_format_version(
header
.as_ref()
.and_then(|header| header.save_format_version),
);
let save_format_major = header
.as_ref()
.and_then(crate::asm_header::AsmHeader::save_format_major);
let saved_entity_limit = header
.as_ref()
.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(records);
let mut reach = Reachable::default();
keep_faces_and_carriers(
&mut out,
records,
&by_index,
&token_table,
&mut carriers,
&mut reach,
purpose,
format,
);
walk_reachable_topology(
&mut out,
&by_index,
&token_table,
&mut carriers,
&mut reach,
purpose,
format,
);
let wire = collect_wire_topology(
&mut out,
records,
&by_index,
saved_entity_limit,
&token_table,
&mut carriers,
&mut reach,
purpose,
format,
);
let (reversed_curve_refs, forward_curve_refs) = classify_edge_curve_senses(records, &reach);
emit_carrier_records(
&mut out,
records,
&mut carriers,
&reach,
&reversed_curve_refs,
&forward_curve_refs,
format,
);
emit_pcurves(&mut out, records, &mut carriers, &reach, format);
emit_points(&mut out, records, &reach, format);
emit_vertices(&mut out, records, &by_index, &reach, format);
emit_edges(
&mut out,
records,
&by_index,
&reach,
&reversed_curve_refs,
&forward_curve_refs,
format,
);
emit_coedges(
&mut out,
records,
&token_table,
save_format_major,
&carriers,
&reach,
format,
);
emit_loops(&mut out, records, &by_index, &reach, format);
emit_faces(
&mut out,
records,
&by_index,
&reach,
&inward_normal_surfaces,
format,
);
emit_containers(
&mut out,
records,
&by_index,
&reach,
&wire,
stream,
header_scale,
format,
);
project_subshell_faces(&mut out, records, &by_index, format);
if purpose == DecodePurpose::Model {
let emitted_attributes = emit_attributes(&mut out, records, &by_index, &reach, format);
emit_passthrough_unknowns(&mut out, records, bytes, &reach, format);
count_other_records(&mut out, records, &reach, &emitted_attributes);
emit_annotation_records(&mut out, records, &by_index, stream, format);
classify_body_kinds(&mut out);
clamp_edge_ranges_to_carrier_domains(&mut out);
}
out
}
pub(crate) fn inherited_attribute_target(
mut owner: i64,
by_index: &HashMap<i64, &Record>,
targets: &HashMap<i64, AttributeTarget>,
) -> Option<AttributeTarget> {
let mut visited = HashSet::new();
while visited.insert(owner) {
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.ref_at(4)?;
}
None
}
#[cfg(test)]
mod tests;