use std::collections::{HashMap, HashSet};
use cadmpeg_ir::attributes::{AttributeTarget, AttributeValue, SourceAttribute};
use cadmpeg_ir::design::{PersistentDesignLink, SketchCurveLink};
use cadmpeg_ir::geometry::{
BlendSupport, Curve, CurveGeometry, Pcurve, PcurveGeometry, ProceduralCurve, ProceduralSurface,
ProceduralSurfaceDefinition, Surface, SurfaceGeometry,
};
use cadmpeg_ir::ids::{
AttributeId, BodyId, CoedgeId, CurveId, EdgeId, FaceId, LoopId, PcurveId, PointId, RegionId,
ShellId, SurfaceId, UnknownId, VertexId,
};
use cadmpeg_ir::math::{Point3, Vector3};
use cadmpeg_ir::topology::{
Body, Coedge, Color, Edge, Face, Loop, Point, Region, Sense, Shell, Vertex,
};
use cadmpeg_ir::unknown::UnknownRecord;
use crate::asm_header;
use crate::nurbs;
use crate::sab::{Record, Token};
const LEN_TO_MM: f64 = 10.0;
#[derive(Default)]
pub struct Brep {
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 sketch_curve_links: Vec<SketchCurveLink>,
pub persistent_design_links: Vec<PersistentDesignLink>,
pub body_keys: HashMap<BodyId, u64>,
pub attributes: Vec<SourceAttribute>,
pub unknowns: Vec<UnknownRecord>,
pub stats: Stats,
pub annotation_records: Vec<AnnotationRecord>,
}
pub struct AnnotationRecord {
pub id: String,
pub offset: u64,
pub tag: String,
pub derived_fields: Vec<&'static str>,
}
#[derive(Default)]
pub struct Stats {
pub unknown_surface_faces: usize,
pub nurbs_surfaces: usize,
pub nurbs_curves: usize,
pub procedural_curve_edges: usize,
pub undecoded_pcurve_refs: usize,
pub partial_procedural_supports: usize,
pub other_records: usize,
pub other_record_kinds: std::collections::BTreeMap<String, usize>,
}
struct Carrier {
positions: Vec<[f64; 3]>,
vectors: Vec<[f64; 3]>,
doubles: Vec<f64>,
}
fn collect_carrier(rec: &Record) -> Carrier {
let mut c = Carrier {
positions: Vec::new(),
vectors: Vec::new(),
doubles: Vec::new(),
};
for t in &rec.tokens {
match t {
Token::Position(p) => c.positions.push(*p),
Token::Vector3(v) => c.vectors.push(*v),
Token::Double(d) => c.doubles.push(*d),
_ => {}
}
}
c
}
fn scale_point(p: [f64; 3]) -> Point3 {
Point3::new(p[0] * LEN_TO_MM, p[1] * LEN_TO_MM, p[2] * LEN_TO_MM)
}
fn vec3(v: [f64; 3]) -> Vector3 {
Vector3::new(v[0], v[1], v[2])
}
fn norm3(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn unit(v: [f64; 3]) -> Vector3 {
let n = norm3(v);
if n > f64::EPSILON {
Vector3::new(v[0] / n, v[1] / n, v[2] / n)
} else {
vec3(v)
}
}
fn is_analytic_surface(head: &str) -> bool {
matches!(head, "plane" | "cone" | "sphere" | "torus")
}
fn is_analytic_curve(head: &str) -> bool {
matches!(head, "straight" | "ellipse")
}
pub(crate) fn decode_surface(rec: &Record) -> Option<(SurfaceGeometry, bool)> {
let c = collect_carrier(rec);
let origin = *c.positions.first()?;
match rec.head.as_str() {
"plane" => {
let normal = *c.vectors.first()?;
let normal = unit(normal);
let u_axis = c
.vectors
.get(1)
.map_or_else(|| deterministic_ref_direction(normal), |axis| unit(*axis));
Some((
SurfaceGeometry::Plane {
origin: scale_point(origin),
normal,
u_axis,
},
false,
))
}
"cone" => {
let axis = *c.vectors.first()?;
let axis = unit(axis);
let major = c.vectors.get(1).copied();
let sine = *c.doubles.get(1).unwrap_or(&0.0);
let r1 = c.doubles.get(3).copied();
let radius = r1
.map(|r| r * LEN_TO_MM)
.or_else(|| major.map(|vector| norm3(vector) * LEN_TO_MM))?;
let ref_direction = major.map_or_else(|| deterministic_ref_direction(axis), unit);
if sine.abs() <= f64::EPSILON {
Some((
SurfaceGeometry::Cylinder {
origin: scale_point(origin),
axis,
ref_direction,
radius,
},
false,
))
} else {
Some((
SurfaceGeometry::Cone {
origin: scale_point(origin),
axis,
ref_direction,
radius,
half_angle: sine.abs().asin(),
},
false,
))
}
}
"sphere" => {
let signed = *c.doubles.first()?;
let polar_axis = c.vectors.get(1).or_else(|| c.vectors.first()).copied()?;
let polar_axis = unit(polar_axis);
let equator = c
.vectors
.first()
.filter(|_| c.vectors.len() > 1)
.map_or_else(
|| deterministic_ref_direction(polar_axis),
|direction| unit(*direction),
);
Some((
SurfaceGeometry::Sphere {
center: scale_point(origin),
axis: polar_axis,
ref_direction: equator,
radius: signed * LEN_TO_MM,
},
false,
))
}
"torus" => {
let axis = *c.vectors.first()?;
let axis = unit(axis);
let ref_direction = c.vectors.get(1).map_or_else(
|| deterministic_ref_direction(axis),
|direction| unit(*direction),
);
let major = *c.doubles.first()?;
let minor = *c.doubles.get(1)?;
Some((
SurfaceGeometry::Torus {
center: scale_point(origin),
axis,
ref_direction,
major_radius: major * LEN_TO_MM,
minor_radius: minor * LEN_TO_MM,
},
false,
))
}
_ => None,
}
}
fn deterministic_ref_direction(axis: Vector3) -> Vector3 {
let candidates = [
Vector3::new(1.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(0.0, 0.0, 1.0),
];
let basis = candidates
.into_iter()
.min_by(|a, b| {
let a_dot = (a.x * axis.x + a.y * axis.y + a.z * axis.z).abs();
let b_dot = (b.x * axis.x + b.y * axis.y + b.z * axis.z).abs();
a_dot.total_cmp(&b_dot)
})
.expect("fixed candidate set is non-empty");
let dot = basis.x * axis.x + basis.y * axis.y + basis.z * axis.z;
let projected = Vector3::new(
basis.x - dot * axis.x,
basis.y - dot * axis.y,
basis.z - dot * axis.z,
);
let length = projected.norm();
Vector3::new(
projected.x / length,
projected.y / length,
projected.z / length,
)
}
pub(crate) fn decode_curve(rec: &Record) -> Option<CurveGeometry> {
let carrier = collect_carrier(rec);
let base = *carrier.positions.first()?;
match rec.head.as_str() {
"straight" => Some(CurveGeometry::Line {
origin: scale_point(base),
direction: unit(*carrier.vectors.first()?),
}),
"ellipse" => {
let axis = *carrier.vectors.first()?;
let reference = *carrier.vectors.get(1)?;
let ratio = *carrier.doubles.first()?;
let major_radius = norm3(reference) * LEN_TO_MM;
if (ratio.abs() - 1.0).abs() <= f64::EPSILON {
Some(CurveGeometry::Circle {
center: scale_point(base),
axis: unit(axis),
ref_direction: unit(reference),
radius: major_radius,
})
} else {
Some(CurveGeometry::Ellipse {
center: scale_point(base),
axis: unit(axis),
major_direction: unit(reference),
major_radius,
minor_radius: major_radius * ratio.abs(),
})
}
}
_ => None,
}
}
fn sense_at(rec: &Record, i: usize) -> Sense {
match rec.chunk(i) {
Some(Token::True) => Sense::Reversed,
_ => Sense::Forward,
}
}
fn double_at(rec: &Record, i: usize) -> Option<f64> {
match rec.chunk(i) {
Some(Token::Double(d)) => Some(*d),
_ => None,
}
}
pub fn decode(records: &[Record], bytes: &[u8], _stream: &str) -> Brep {
let mut out = Brep::default();
let id = |i: i64| format!("f3d:brep:entity#{i}");
let by_index: HashMap<i64, &Record> = records.iter().map(|r| (r.index as i64, r)).collect();
let header_scale = asm_header::parse(bytes)
.and_then(|header| header.scale)
.unwrap_or(1.0);
let attribute_color = |entity: &Record| attribute_chain_color(entity, &by_index);
let mut surface_geo: HashMap<i64, (SurfaceGeometry, bool)> = HashMap::new();
let mut procedural_surface_defs = HashMap::new();
let mut curve_geo: HashMap<i64, CurveGeometry> = HashMap::new();
let mut procedural_curve_defs = HashMap::new();
for r in records {
if is_analytic_surface(&r.head) {
if let Some(g) = decode_surface(r) {
surface_geo.insert(r.index as i64, g);
}
} else if is_analytic_curve(&r.head) {
if let Some(g) = decode_curve(r) {
curve_geo.insert(r.index as i64, g);
}
}
}
let mut kept_faces: HashSet<i64> = HashSet::new();
let mut kept_loops: HashSet<i64> = HashSet::new();
let mut kept_coedges: HashSet<i64> = HashSet::new();
let mut kept_edges: HashSet<i64> = HashSet::new();
let mut kept_vertices: HashSet<i64> = HashSet::new();
let mut kept_points: HashSet<i64> = HashSet::new();
let mut kept_surfaces: HashSet<i64> = HashSet::new();
let mut unknown_surface_records: HashSet<i64> = HashSet::new();
let mut kept_curves: HashSet<i64> = HashSet::new();
let mut kept_pcurves: HashSet<i64> = HashSet::new();
let mut pcurve_geo: HashMap<i64, PcurveGeometry> = HashMap::new();
let mut undecoded_carriers: HashSet<i64> = HashSet::new();
for r in records {
if r.head != "face" {
continue;
}
let Some(surf_ref) = r.ref_at(7) else {
continue;
};
let Some(surf_rec) = by_index.get(&surf_ref) else {
continue;
};
kept_faces.insert(r.index as i64);
if let std::collections::hash_map::Entry::Vacant(e) = surface_geo.entry(surf_ref) {
if let Some(ns) =
nurbs::decode_surface_cache_resolving_refs(record_slice(surf_rec, bytes), bytes)
{
e.insert((SurfaceGeometry::Nurbs(ns), false));
out.stats.nurbs_surfaces += 1;
if let Some(procedural) = nurbs::decode_procedural_surface_resolving_refs(
record_slice(surf_rec, bytes),
bytes,
) {
procedural_surface_defs.insert(surf_ref, procedural);
}
}
}
if surface_geo.contains_key(&surf_ref) {
kept_surfaces.insert(surf_ref);
} else {
unknown_surface_records.insert(surf_ref);
undecoded_carriers.insert(surf_ref);
out.stats.unknown_surface_faces += 1;
}
}
for &face_idx in &kept_faces.iter().copied().collect::<Vec<_>>() {
let Some(face) = by_index.get(&face_idx) else {
continue;
};
let mut loop_ref = face.ref_at(4);
let mut loop_guard = HashSet::new();
while let Some(li) = loop_ref {
if !loop_guard.insert(li) {
break;
}
let Some(lp) = by_index.get(&li) else { break };
if lp.head != "loop" {
break;
}
kept_loops.insert(li);
if let Some(first_ce) = lp.ref_at(4) {
let mut ce_ref = Some(first_ce);
let mut ce_guard = HashSet::new();
while let Some(ci) = ce_ref {
if !ce_guard.insert(ci) {
break;
}
let Some(ce) = by_index.get(&ci) else { break };
if ce.head != "coedge" {
break;
}
kept_coedges.insert(ci);
if let Some(pc) = ce.ref_at(10) {
if let Some(prec) = by_index.get(&pc) {
let decoded = nurbs::decode_pcurve_cache_resolving_refs(
record_slice(prec, bytes),
bytes,
)
.or_else(|| {
prec.ref_at(4)
.and_then(|reference| by_index.get(&reference))
.and_then(|intcurve| {
nurbs::decode_intcurve_pcurve_cache_resolving_refs(
record_slice(intcurve, bytes),
bytes,
)
})
});
if let Some(decoded) = decoded {
pcurve_geo.insert(
pc,
PcurveGeometry::Nurbs {
degree: decoded.degree,
knots: decoded.knots,
control_points: decoded.control_points,
weights: None,
},
);
kept_pcurves.insert(pc);
} else {
out.stats.undecoded_pcurve_refs += 1;
}
} else {
out.stats.undecoded_pcurve_refs += 1;
}
}
if let Some(ei) = ce.ref_at(6) {
if let Some(edge) = by_index.get(&ei) {
if edge.head == "edge" && kept_edges.insert(ei) {
for slot in [3usize, 5] {
if let Some(vi) = edge.ref_at(slot) {
if let Some(v) = by_index.get(&vi) {
if v.head == "vertex" {
kept_vertices.insert(vi);
if let Some(pi) = v.ref_at(5) {
kept_points.insert(pi);
}
}
}
}
}
match edge.ref_at(8) {
Some(cv) if curve_geo.contains_key(&cv) => {
kept_curves.insert(cv);
}
Some(cv) => {
if let Some(crec) = by_index.get(&cv) {
if let Some(decoded) =
nurbs::decode_procedural_curve_resolving_refs(
record_slice(crec, bytes),
bytes,
)
{
curve_geo.insert(
cv,
CurveGeometry::Nurbs(decoded.curve),
);
procedural_curve_defs.insert(
cv,
(
decoded.native_kind,
decoded.cache_fit_tolerance,
),
);
out.stats.nurbs_curves += 1;
kept_curves.insert(cv);
} else {
undecoded_carriers.insert(cv);
out.stats.procedural_curve_edges += 1;
}
}
}
_ => {}
}
}
}
}
ce_ref = ce.ref_at(3);
if ce_ref == Some(first_ce) {
break;
}
}
}
loop_ref = lp.ref_at(3);
}
}
for r in records {
let i = r.index as i64;
match r.head.as_str() {
_ if kept_surfaces.contains(&i) => {
let Some((geometry, _)) = surface_geo.remove(&i) else {
continue;
};
out.surfaces.push(Surface {
id: SurfaceId(id(i)),
geometry,
});
if let Some(procedural) = procedural_surface_defs.remove(&i) {
let definition = match procedural.definition {
nurbs::DecodedProceduralSurfaceDefinition::Extrusion {
directrix,
direction,
} => {
let directrix_id =
CurveId(format!("f3d:brep:procedural_surface#{i}:directrix"));
out.curves.push(Curve {
id: directrix_id.clone(),
geometry: CurveGeometry::Nurbs(directrix),
});
ProceduralSurfaceDefinition::Extrusion {
directrix: directrix_id,
direction,
}
}
nurbs::DecodedProceduralSurfaceDefinition::Blend {
supports,
spine,
radius,
cross_section,
} => {
let mut resolved_supports = [None, None];
for (side, support) in supports.into_iter().enumerate() {
if let Some(support) = support {
let support_id = SurfaceId(format!(
"f3d:brep:procedural_surface#{i}:support#{side}"
));
out.surfaces.push(Surface {
id: support_id.clone(),
geometry: SurfaceGeometry::Nurbs(support),
});
resolved_supports[side] = Some(BlendSupport {
surface: support_id,
reversed: false,
});
}
}
let spine = spine.map(|spine| {
let spine_id =
CurveId(format!("f3d:brep:procedural_surface#{i}:spine"));
out.curves.push(Curve {
id: spine_id.clone(),
geometry: CurveGeometry::Nurbs(spine),
});
spine_id
});
if resolved_supports
.iter()
.filter(|support| support.is_some())
.count()
== 1
{
out.stats.partial_procedural_supports += 1;
}
ProceduralSurfaceDefinition::Blend {
supports: resolved_supports,
spine,
radius,
cross_section,
}
}
};
out.procedural_surfaces.push(ProceduralSurface {
id: format!("f3d:brep:procedural_surface#{i}").into(),
surface: SurfaceId(id(i)),
definition,
cache_fit_tolerance: procedural.cache_fit_tolerance,
});
}
}
_ if unknown_surface_records.contains(&i) => {
out.surfaces.push(Surface {
id: SurfaceId(id(i)),
geometry: SurfaceGeometry::Unknown {
record: Some(UnknownId(unknown_record_id(r))),
},
});
}
_ if kept_curves.contains(&i) => {
let Some(geometry) = curve_geo.remove(&i) else {
continue;
};
out.curves.push(Curve {
id: CurveId(id(i)),
geometry,
});
if let Some(procedural) = procedural_curve_defs.remove(&i) {
out.procedural_curves.push(ProceduralCurve {
id: format!("f3d:brep:procedural_curve#{i}").into(),
curve: CurveId(id(i)),
definition: cadmpeg_ir::geometry::ProceduralCurveDefinition::Unknown {
record: None,
},
cache_fit_tolerance: procedural.1,
});
}
}
_ => {}
}
}
for r in records {
let i = r.index as i64;
if kept_pcurves.contains(&i) {
if let Some(geometry) = pcurve_geo.remove(&i) {
out.pcurves.push(Pcurve {
id: PcurveId(id(i)),
geometry,
});
}
}
}
for r in records {
let i = r.index as i64;
if r.head == "point" && kept_points.contains(&i) {
let c = collect_carrier(r);
if let Some(p) = c.positions.first() {
out.points.push(Point {
id: PointId(id(i)),
position: scale_point(*p),
});
}
}
}
for r in records {
let i = r.index as i64;
if r.head == "vertex" && kept_vertices.contains(&i) {
if let Some(pi) = r.ref_at(5) {
if kept_points.contains(&pi) {
out.vertices.push(Vertex {
id: VertexId(id(i)),
point: PointId(id(pi)),
tolerance: None,
});
}
}
}
}
for r in records {
let i = r.index as i64;
if r.head == "edge" && kept_edges.contains(&i) {
let (Some(start), Some(end)) = (r.ref_at(3), r.ref_at(5)) else {
continue;
};
if !kept_vertices.contains(&start) || !kept_vertices.contains(&end) {
continue;
}
let curve = r.ref_at(8).filter(|c| kept_curves.contains(c));
let param_range = match (double_at(r, 4), double_at(r, 6)) {
(Some(mut a), Some(mut b)) => {
if let Some(curve_record) = curve.and_then(|curve| by_index.get(&curve)) {
if curve_record.head == "ellipse" {
let carrier = collect_carrier(curve_record);
let ratio = carrier.doubles.first().copied().unwrap_or(1.0);
if ratio > 0.0 {
a += std::f64::consts::FRAC_PI_2;
b += std::f64::consts::FRAC_PI_2;
}
if (b - a).abs() >= std::f64::consts::TAU - 1.0e-12 {
a = 0.0;
b = std::f64::consts::TAU;
}
}
}
Some([a, b])
}
_ => None,
};
out.edges.push(Edge {
id: EdgeId(id(i)),
curve: curve.map(|c| CurveId(id(c))),
start: VertexId(id(start)),
end: VertexId(id(end)),
param_range,
tolerance: None,
});
}
}
for r in records {
let i = r.index as i64;
if r.head == "coedge" && kept_coedges.contains(&i) {
let (Some(next), Some(prev), Some(edge), Some(owner)) =
(r.ref_at(3), r.ref_at(4), r.ref_at(6), r.ref_at(8))
else {
continue;
};
if !kept_coedges.contains(&next)
|| !kept_coedges.contains(&prev)
|| !kept_edges.contains(&edge)
|| !kept_loops.contains(&owner)
{
continue;
}
let partner = r.ref_at(5).filter(|p| kept_coedges.contains(p));
out.coedges.push(Coedge {
id: CoedgeId(id(i)),
owner_loop: LoopId(id(owner)),
edge: EdgeId(id(edge)),
next: CoedgeId(id(next)),
previous: CoedgeId(id(prev)),
radial_next: partner.map_or_else(|| CoedgeId(id(i)), |p| CoedgeId(id(p))),
sense: sense_at(r, 7),
pcurve: r
.ref_at(10)
.filter(|p| kept_pcurves.contains(p))
.map(|p| PcurveId(id(p))),
});
}
}
for r in records {
let i = r.index as i64;
if r.head == "loop" && kept_loops.contains(&i) {
let Some(owner) = r.ref_at(5) else { continue };
let coedges = ring_coedges(r, &by_index, &kept_coedges);
out.loops.push(Loop {
id: LoopId(id(i)),
face: FaceId(id(owner)),
coedges,
});
}
}
for r in records {
let i = r.index as i64;
if r.head == "face" && kept_faces.contains(&i) {
let (Some(surface), Some(owner)) = (r.ref_at(7), r.ref_at(5)) else {
continue;
};
let loops = loop_chain(r, &by_index, &kept_loops);
out.faces.push(Face {
id: FaceId(id(i)),
shell: ShellId(id(owner)),
surface: SurfaceId(id(surface)),
sense: sense_at(r, 8),
loops,
name: None,
color: attribute_color(r),
tolerance: None,
});
}
}
for r in records {
let i = r.index as i64;
match r.head.as_str() {
"shell" => {
let Some(owner) = r.ref_at(7) else { continue };
let faces = face_chain(r, &by_index, &kept_faces);
out.shells.push(Shell {
id: ShellId(id(i)),
region: RegionId(id(owner)),
faces,
wire_edges: Vec::new(),
free_vertices: Vec::new(),
});
}
"region" => {
let Some(owner) = r.ref_at(5) else { continue };
let shells = shell_chain(r, &by_index);
out.regions.push(Region {
id: RegionId(id(i)),
body: BodyId(id(owner)),
shells,
});
}
"body" => {
let regions = region_chain(r, &by_index);
let body_id = BodyId(id(i));
if let Some(Token::Long(key)) = r.chunk(1) {
if *key >= 0 {
out.body_keys.insert(body_id.clone(), *key as u64);
}
}
out.bodies.push(Body {
id: body_id,
kind: cadmpeg_ir::topology::BodyKind::Solid,
regions,
transform: r
.ref_at(5)
.and_then(|reference| by_index.get(&reference))
.and_then(|transform| decode_transform(transform, header_scale)),
name: None,
color: attribute_color(r),
});
}
_ => {}
}
}
let mut emitted_attributes = HashSet::new();
for record in records {
let index = record.index as i64;
let target = match record.head.as_str() {
"body" if out.bodies.iter().any(|entity| entity.id.0 == id(index)) => {
Some(AttributeTarget::Body(BodyId(id(index))))
}
"face" if kept_faces.contains(&index) => Some(AttributeTarget::Face(FaceId(id(index)))),
"coedge" if kept_coedges.contains(&index) => {
Some(AttributeTarget::Coedge(CoedgeId(id(index))))
}
"edge" if kept_edges.contains(&index) => Some(AttributeTarget::Edge(EdgeId(id(index)))),
"vertex" if kept_vertices.contains(&index) => {
Some(AttributeTarget::Vertex(VertexId(id(index))))
}
_ => None,
};
if let Some(target) = target {
collect_attributes(
record,
&target,
&by_index,
&mut emitted_attributes,
&mut out.attributes,
);
}
}
for record in records {
let index = record.index as i64;
if record.name != "ATTRIB_CUSTOM-attrib" || emitted_attributes.contains(&index) {
continue;
}
let Some(owner) = record.ref_at(4).and_then(|owner| by_index.get(&owner)) else {
continue;
};
let owner_index = owner.index as i64;
let target = match owner.head.as_str() {
"body" => Some(AttributeTarget::Body(BodyId(id(owner_index)))),
"face" if kept_faces.contains(&owner_index) => {
Some(AttributeTarget::Face(FaceId(id(owner_index))))
}
"coedge" if kept_coedges.contains(&owner_index) => {
Some(AttributeTarget::Coedge(CoedgeId(id(owner_index))))
}
"edge" if kept_edges.contains(&owner_index) => {
Some(AttributeTarget::Edge(EdgeId(id(owner_index))))
}
"vertex" if kept_vertices.contains(&owner_index) => {
Some(AttributeTarget::Vertex(VertexId(id(owner_index))))
}
_ => None,
};
if let Some(target) = target {
emitted_attributes.insert(index);
out.attributes.push(source_attribute(record, target));
}
}
out.sketch_curve_links = out
.attributes
.iter()
.filter_map(sketch_curve_link)
.collect();
out.persistent_design_links = out
.attributes
.iter()
.flat_map(persistent_design_links)
.collect();
for r in records {
let i = r.index as i64;
if undecoded_carriers.contains(&i) {
out.unknowns.push(UnknownRecord {
id: UnknownId(unknown_record_id(r)),
offset: r.offset as u64,
byte_len: r.len as u64,
sha256: sha256_hex(&bytes[r.offset..(r.offset + r.len).min(bytes.len())]),
data: Some(bytes[r.offset..(r.offset + r.len).min(bytes.len())].to_vec()),
links: Vec::new(),
});
}
}
let kept_transforms: HashSet<i64> = records
.iter()
.filter(|record| record.head == "body")
.filter_map(|record| record.ref_at(5))
.collect();
let pcurve_intcurves: HashSet<i64> = records
.iter()
.filter(|record| kept_pcurves.contains(&(record.index as i64)))
.filter_map(|record| record.ref_at(4))
.collect();
let known_head = |h: &str| {
matches!(
h,
"body" | "region" | "shell" | "face" | "loop" | "coedge" | "edge" | "vertex" | "point"
) || is_analytic_surface(h)
|| is_analytic_curve(h)
|| h == "asmheader"
};
for r in records {
let i = r.index as i64;
let transferred = kept_surfaces.contains(&i)
|| kept_curves.contains(&i)
|| kept_pcurves.contains(&i)
|| kept_transforms.contains(&i)
|| emitted_attributes.contains(&i)
|| pcurve_intcurves.contains(&i);
if !known_head(&r.head)
&& r.name != "Begin-of-ASM-History-Data"
&& !undecoded_carriers.contains(&i)
&& !transferred
{
out.stats.other_records += 1;
*out.stats
.other_record_kinds
.entry(r.name.clone())
.or_default() += 1;
}
}
let emitted_ids = out
.bodies
.iter()
.map(|entity| entity.id.0.as_str())
.chain(out.regions.iter().map(|entity| entity.id.0.as_str()))
.chain(out.shells.iter().map(|entity| entity.id.0.as_str()))
.chain(out.faces.iter().map(|entity| entity.id.0.as_str()))
.chain(out.loops.iter().map(|entity| entity.id.0.as_str()))
.chain(out.coedges.iter().map(|entity| entity.id.0.as_str()))
.chain(out.edges.iter().map(|entity| entity.id.0.as_str()))
.chain(out.vertices.iter().map(|entity| entity.id.0.as_str()))
.chain(out.points.iter().map(|entity| entity.id.0.as_str()))
.chain(out.surfaces.iter().map(|entity| entity.id.0.as_str()))
.chain(out.curves.iter().map(|entity| entity.id.0.as_str()))
.chain(out.pcurves.iter().map(|entity| entity.id.0.as_str()))
.collect::<HashSet<_>>();
for record in records {
let entity_id = id(record.index as i64);
if emitted_ids.contains(entity_id.as_str()) {
let mut derived_fields = Vec::new();
match record.head.as_str() {
"plane" => {
derived_fields.extend(["geometry.normal", "geometry.u_axis"]);
}
"cone" => {
derived_fields.extend(["geometry.axis", "geometry.ref_direction"]);
}
"sphere" => {
derived_fields.extend(["geometry.axis", "geometry.ref_direction"]);
}
"torus" => {
derived_fields.extend(["geometry.axis", "geometry.ref_direction"]);
}
"straight" => derived_fields.push("geometry.direction"),
"ellipse" => {
derived_fields.extend(["geometry.axis", "geometry.major_direction"]);
}
_ => {}
}
if record.head == "edge" {
if let Some(curve) = record
.ref_at(8)
.and_then(|reference| by_index.get(&reference))
{
if curve.head == "ellipse" {
derived_fields.push("param_range");
}
}
}
out.annotation_records.push(AnnotationRecord {
id: entity_id,
offset: record.offset as u64,
tag: record.name.clone(),
derived_fields,
});
}
let attribute_id = format!("f3d:attribute#{}", record.index);
if out
.attributes
.iter()
.any(|attribute| attribute.id.0 == attribute_id)
{
out.annotation_records.push(AnnotationRecord {
id: attribute_id,
offset: record.offset as u64,
tag: record.name.clone(),
derived_fields: Vec::new(),
});
}
let unknown_id = unknown_record_id(record);
if out
.unknowns
.iter()
.any(|unknown| unknown.id.0 == unknown_id)
{
out.annotation_records.push(AnnotationRecord {
id: unknown_id,
offset: record.offset as u64,
tag: record.name.clone(),
derived_fields: Vec::new(),
});
}
for (synthetic_id, tag) in [
(
format!("f3d:brep:procedural_surface#{}", record.index),
"procedural_surface",
),
(
format!("f3d:brep:procedural_curve#{}", record.index),
"procedural_curve",
),
] {
if out
.procedural_surfaces
.iter()
.any(|entity| entity.id.0 == synthetic_id)
|| out
.procedural_curves
.iter()
.any(|entity| entity.id.0 == synthetic_id)
{
out.annotation_records.push(AnnotationRecord {
id: synthetic_id,
offset: record.offset as u64,
tag: tag.into(),
derived_fields: Vec::new(),
});
}
}
}
for (entity_id, tag) in out
.surfaces
.iter()
.map(|entity| (entity.id.0.as_str(), "procedural_support"))
.chain(
out.curves
.iter()
.map(|entity| (entity.id.0.as_str(), "procedural_curve_child")),
)
{
if !entity_id.starts_with("f3d:brep:procedural_surface#") {
continue;
}
let Some(index) = entity_id
.split_once('#')
.and_then(|(_, suffix)| suffix.split(':').next())
.and_then(|value| value.parse::<usize>().ok())
else {
continue;
};
let Some(record) = records.get(index) else {
continue;
};
out.annotation_records.push(AnnotationRecord {
id: entity_id.to_owned(),
offset: record.offset as u64,
tag: tag.into(),
derived_fields: Vec::new(),
});
}
out
}
fn sketch_curve_link(attribute: &SourceAttribute) -> Option<SketchCurveLink> {
let AttributeTarget::Coedge(coedge) = &attribute.target else {
return None;
};
let family = attribute.values.iter().position(
|value| matches!(value, AttributeValue::String(name) if name == "sketch_attrib_def"),
)?;
let fields = attribute.values[family + 1..]
.iter()
.filter_map(|value| match value {
AttributeValue::String(payload) => Some(
payload
.split_ascii_whitespace()
.map(str::parse::<i64>)
.collect::<Result<Vec<_>, _>>()
.ok(),
),
_ => None,
})
.flatten()
.find(|values| values.len() == 6)
.unwrap_or_else(|| {
attribute.values[family + 1..]
.iter()
.filter_map(|value| match value {
AttributeValue::Integer(value) => Some(*value),
_ => None,
})
.take(6)
.collect()
});
let [sketch_curve_id, 0, signed_reference, 0, role, closure] = fields.as_slice() else {
return None;
};
Some(SketchCurveLink {
id: format!(
"f3d:design:sketch-curve-link#{}:{sketch_curve_id}",
coedge.0
),
coedge: coedge.clone(),
sketch_curve_id: *sketch_curve_id,
signed_reference: (*signed_reference != -1).then_some(*signed_reference),
role: *role,
closure: *closure,
})
}
fn persistent_design_links(attribute: &SourceAttribute) -> Vec<PersistentDesignLink> {
let Some(family) = attribute.values.iter().position(
|value| matches!(value, AttributeValue::String(name) if name == "generic_tag_attrib_def"),
) else {
return Vec::new();
};
let ids: Vec<String> = attribute.values[family + 1..]
.iter()
.filter_map(|value| match value {
AttributeValue::String(value)
if value.trim() != "generic_tag_attrib_def"
&& !value.is_empty()
&& value.bytes().all(|byte| byte.is_ascii_digit()) =>
{
Some(value.clone())
}
_ => None,
})
.collect();
let last = ids.len().saturating_sub(1);
ids.into_iter()
.enumerate()
.map(|(ordinal, design_id)| PersistentDesignLink {
id: format!(
"f3d:persistent-design-link:{:?}:{}:{ordinal}",
attribute.target, design_id
),
target: attribute.target.clone(),
design_id,
ordinal: ordinal as u32,
is_current: ordinal == last,
})
.collect()
}
fn collect_attributes(
entity: &Record,
target: &AttributeTarget,
by_index: &HashMap<i64, &Record>,
emitted: &mut HashSet<i64>,
out: &mut Vec<SourceAttribute>,
) {
let mut current = entity.ref_at(0);
let mut chain = HashSet::new();
while let Some(index) = current.filter(|index| chain.insert(*index)) {
let Some(record) = by_index.get(&index) else {
break;
};
if emitted.insert(index) {
out.push(source_attribute(record, target.clone()));
}
current = record.ref_at(0);
}
}
fn source_attribute(record: &Record, target: AttributeTarget) -> SourceAttribute {
SourceAttribute {
id: AttributeId(format!("f3d:attribute#{}", record.index)),
target,
name: record.name.clone(),
values: record.tokens.iter().map(attribute_value).collect(),
}
}
fn attribute_value(token: &Token) -> AttributeValue {
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) => AttributeValue::Float(f64::from(*value)),
Token::Double(value) => AttributeValue::Float(*value),
Token::Str(value) => AttributeValue::String(value.clone()),
Token::True => AttributeValue::Boolean(true),
Token::False => AttributeValue::Boolean(false),
Token::Ref(value) => AttributeValue::Reference(format!("f3d:brep:entity#{value}")),
Token::SubtypeOpen => AttributeValue::String("subtype_open".into()),
Token::SubtypeClose => AttributeValue::String("subtype_close".into()),
Token::Position(value) | Token::Vector3(value) => AttributeValue::Vector(value.to_vec()),
Token::Vector2(value) => AttributeValue::Vector(value.to_vec()),
}
}
pub(crate) fn decode_transform(
record: &Record,
header_scale: f64,
) -> Option<cadmpeg_ir::transform::Transform> {
let vectors: Vec<[f64; 3]> = record
.tokens
.iter()
.filter_map(|token| match token {
Token::Position(value) | Token::Vector3(value) => Some(*value),
_ => None,
})
.collect();
let scale = record
.tokens
.iter()
.filter_map(|token| match token {
Token::Double(value) => Some(*value),
_ => None,
})
.next_back()?;
let [x, y, z, translation] = vectors.as_slice() else {
return None;
};
Some(cadmpeg_ir::transform::Transform {
rows: [
[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],
[0.0, 0.0, 0.0, scale],
],
})
}
pub(crate) fn attribute_chain_color(
entity: &Record,
by_index: &HashMap<i64, &Record>,
) -> Option<Color> {
let mut current = entity.ref_at(0)?;
let mut seen = HashSet::new();
while seen.insert(current) {
let record = by_index.get(¤t)?;
if record.name.contains("rgb_color") {
let values: Vec<f64> = record
.tokens
.iter()
.filter_map(|t| match t {
Token::Double(value) => Some(*value),
_ => None,
})
.collect();
if let [r, g, b, ..] = values.as_slice() {
if [*r, *g, *b].iter().all(|value| (0.0..=1.0).contains(value)) {
return Some(Color {
r: *r as f32,
g: *g as f32,
b: *b as f32,
a: 1.0,
});
}
}
} else if record.name.contains("truecolor") {
let packed = record.tokens.iter().find_map(|token| match token {
Token::Int64(value) | Token::Long(value) => Some(*value as u32),
_ => None,
})?;
return Some(Color {
r: ((packed >> 16) & 0xff) as f32 / 255.0,
g: ((packed >> 8) & 0xff) as f32 / 255.0,
b: (packed & 0xff) as f32 / 255.0,
a: ((packed >> 24) & 0xff) as f32 / 255.0,
});
}
current = record.ref_at(0)?;
}
None
}
fn record_slice<'a>(rec: &Record, bytes: &'a [u8]) -> &'a [u8] {
let end = (rec.offset + rec.len).min(bytes.len());
&bytes[rec.offset..end]
}
fn unknown_record_id(rec: &Record) -> String {
format!("f3d:brep:{}#{}", rec.head, rec.index)
}
fn ring_coedges(
loop_rec: &Record,
by_index: &HashMap<i64, &Record>,
kept: &HashSet<i64>,
) -> Vec<CoedgeId> {
let id = |i: i64| CoedgeId(format!("f3d:brep:entity#{i}"));
let mut out = Vec::new();
let Some(first) = loop_rec.ref_at(4) else {
return out;
};
let mut cur = Some(first);
let mut guard = HashSet::new();
while let Some(ci) = cur {
if !guard.insert(ci) || !kept.contains(&ci) {
break;
}
out.push(id(ci));
let Some(ce) = by_index.get(&ci) else { break };
cur = ce.ref_at(3);
if cur == Some(first) {
break;
}
}
out
}
fn loop_chain(
face_rec: &Record,
by_index: &HashMap<i64, &Record>,
kept: &HashSet<i64>,
) -> Vec<LoopId> {
let id = |i: i64| LoopId(format!("f3d:brep:entity#{i}"));
let mut out = Vec::new();
let mut cur = face_rec.ref_at(4);
let mut guard = HashSet::new();
while let Some(li) = cur {
if !guard.insert(li) {
break;
}
if kept.contains(&li) {
out.push(id(li));
}
let Some(lp) = by_index.get(&li) else { break };
cur = lp.ref_at(3);
}
out
}
fn face_chain(
shell_rec: &Record,
by_index: &HashMap<i64, &Record>,
kept: &HashSet<i64>,
) -> Vec<FaceId> {
let id = |i: i64| FaceId(format!("f3d:brep:entity#{i}"));
let mut out = Vec::new();
let mut cur = shell_rec.ref_at(5);
let mut guard = HashSet::new();
while let Some(fi) = cur {
if !guard.insert(fi) {
break;
}
if kept.contains(&fi) {
out.push(id(fi));
}
let Some(f) = by_index.get(&fi) else { break };
cur = f.ref_at(3);
}
out
}
fn shell_chain(region_rec: &Record, by_index: &HashMap<i64, &Record>) -> Vec<ShellId> {
let id = |i: i64| ShellId(format!("f3d:brep:entity#{i}"));
let mut out = Vec::new();
let mut cur = region_rec.ref_at(4);
let mut guard = HashSet::new();
while let Some(si) = cur {
if !guard.insert(si) {
break;
}
out.push(id(si));
let Some(s) = by_index.get(&si) else { break };
cur = s.ref_at(0);
}
out
}
fn region_chain(body_rec: &Record, by_index: &HashMap<i64, &Record>) -> Vec<RegionId> {
let id = |i: i64| RegionId(format!("f3d:brep:entity#{i}"));
let mut out = Vec::new();
let mut cur = body_rec.ref_at(3);
let mut guard = HashSet::new();
while let Some(li) = cur {
if !guard.insert(li) {
break;
}
out.push(id(li));
let Some(l) = by_index.get(&li) else { break };
cur = l.ref_at(0);
}
out
}
fn sha256_hex(bytes: &[u8]) -> String {
use std::fmt::Write as _;
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(bytes);
let digest = h.finalize();
let mut s = String::with_capacity(digest.len() * 2);
for b in digest {
let _ = write!(s, "{b:02x}");
}
s
}