1use std::collections::{BTreeMap, BTreeSet};
14
15use cadmpeg_ir::codec::{CodecError, DecodeResult};
16use cadmpeg_ir::decode::{DecodeContext, View};
17use cadmpeg_ir::document::{CadIr, SourceMeta};
18use cadmpeg_ir::eval::{
19 analytic_surface_parameters, curve_point, model_surface_point_by_id, nurbs_surface_partials,
20 pcurve_uv, surface_point,
21};
22use cadmpeg_ir::features::{
23 BodyRetentionMode, BodySelection, BodyTrimSide, BooleanOp, ChamferSpec,
24 CurveProjectionDirection, CurveProjectionDirectionState, EdgeSelection, ExtrudeExtent,
25 FaceSelection, FeatureDefinition, HoleKind, Length, ParameterId, PathRef, PatternKind,
26 ProfileRef, RadiusSpec, RibConstruction, RibDraft, SketchSpace, Termination, TrimRegion,
27};
28use cadmpeg_ir::geometry::{
29 BlendCrossSection, BlendRadiusLaw, BlendSupport, Curve, CurveGeometry, IntcurveSupportContext,
30 IntcurveSupportSide, NurbsCurve, NurbsSurface, Pcurve, PcurveGeometry, ProceduralCurve,
31 ProceduralCurveDefinition, ProceduralSurface, ProceduralSurfaceDefinition, Surface,
32 SurfaceCurveFamily, SurfaceGeometry,
33};
34use cadmpeg_ir::hash::sha256_hex;
35use cadmpeg_ir::ids::{
36 BodyId, CoedgeId, CurveId, EdgeId, FaceId, LoopId, PcurveId, PointId, ProceduralCurveId,
37 ProceduralSurfaceId, RegionId, ShellId, SurfaceId, UnknownId, VertexId,
38};
39use cadmpeg_ir::math::{Point2, Point3, Vector3};
40use cadmpeg_ir::report::{DecodeReport, LossCategory, LossCode, LossNote, Severity};
41use cadmpeg_ir::topology::{
42 Body, BodyKind, Coedge, Edge, Face, Loop, Point, Region, Sense, Shell, Vertex,
43};
44use cadmpeg_ir::units::Units;
45use cadmpeg_ir::unknown::UnknownRecord;
46use cadmpeg_ir::{AnnotationBuilder, Exactness, SourceObjectAssociation};
47
48use crate::container::{self, Container};
49use crate::geometry;
50use crate::native::vector::{cross_vector, dot_vector, unit_vector};
51use crate::parasolid::{self, Stream, StreamKind};
52use crate::topology::{Graph, Node};
53
54pub(crate) const MISSING_TOLERANCE: f64 = -31_415_800_000_000.0;
55pub struct Scan {
57 pub container: Container,
59 pub streams: Vec<Stream>,
61}
62
63impl Scan {
64 pub fn count(&self, kind: StreamKind) -> usize {
66 self.streams.iter().filter(|s| s.kind == kind).count()
67 }
68
69 pub fn has_parasolid(&self) -> bool {
73 self.streams.iter().any(|s| s.kind.is_parasolid())
74 }
75}
76
77pub fn scan<'a>(ctx: &DecodeContext<'a>, root: View<'a>) -> Result<Scan, CodecError> {
79 let container = container::scan_bytes(root.window().to_vec())?;
80 let streams = parasolid::extract_streams(ctx, root, &container)?;
81 Ok(Scan { container, streams })
82}
83
84pub fn decode<'a>(ctx: &DecodeContext<'a>, root: View<'a>) -> Result<DecodeResult, CodecError> {
92 let scan = scan(ctx, root)?;
93
94 if ctx.container_only() {
95 let (ir, annotations, unknowns) = build_metadata_ir(&scan)?;
96 let mut report = build_container_report(&scan, true);
97 report_untransferred_streams(&scan, &mut report);
98 return decode_result(ir, report, annotations, &unknowns);
99 }
100
101 if let Some((ir, report, annotations, unknowns)) = try_decode_geometry(&scan) {
102 return decode_result(ir, report, annotations, &unknowns);
103 }
104
105 let (ir, annotations, unknowns) = build_metadata_ir(&scan)?;
106 let mut report = build_container_report(&scan, false);
107 report_untransferred_streams(&scan, &mut report);
108 decode_result(ir, report, annotations, &unknowns)
109}
110
111fn decode_result(
112 mut ir: CadIr,
113 report: DecodeReport,
114 annotations: cadmpeg_ir::Annotations,
115 unknowns: &[UnknownRecord],
116) -> Result<DecodeResult, CodecError> {
117 let mut source_fidelity = cadmpeg_ir::SourceFidelity {
118 annotations,
119 ..cadmpeg_ir::SourceFidelity::default()
120 };
121 source_fidelity.attach_native_unknown_records(&mut ir, "nx", unknowns)?;
122 Ok(DecodeResult::with_source_fidelity(
123 ir,
124 report,
125 source_fidelity,
126 ))
127}
128
129fn report_untransferred_streams(scan: &Scan, report: &mut DecodeReport) {
130 for (index, stream) in scan.streams.iter().enumerate() {
131 if !stream.kind.is_parasolid() {
132 report.losses.push(LossNote {
133 code: LossCode::PassthroughRecordOmitted,
134 category: LossCategory::Other,
135 severity: Severity::Info,
136 message: format!(
137 "Non-Parasolid {} stream #{index} was classified but not transferred.",
138 stream.kind.label()
139 ),
140 provenance: None,
141 });
142 }
143 }
144}
145
146#[derive(Debug, Default)]
148struct Counts {
149 points: usize,
150 planes: usize,
151 cylinders: usize,
152 cones: usize,
153 spheres: usize,
154 tori: usize,
155 nurbs_surfaces: usize,
156 offset_surfaces: usize,
157 blend_surfaces: usize,
158 lines: usize,
159 circles: usize,
160 ellipses: usize,
161 nurbs_curves: usize,
162 intersection_curves: usize,
163 intersection_rejections: crate::intersection::RejectionCounts,
164}
165
166impl Counts {
167 fn surfaces(&self) -> usize {
168 self.planes
169 + self.cylinders
170 + self.cones
171 + self.spheres
172 + self.tori
173 + self.nurbs_surfaces
174 + self.offset_surfaces
175 + self.blend_surfaces
176 }
177 fn curves(&self) -> usize {
178 self.lines + self.circles + self.ellipses + self.nurbs_curves + self.intersection_curves
179 }
180}
181
182pub(crate) fn ordered_point_candidates<'a>(
183 stream: &[u8],
184 graph: &'a Graph,
185) -> Vec<(usize, Point3, Option<&'a Node>)> {
186 ordered_fixed_candidates(
187 geometry::points(stream)
188 .into_iter()
189 .map(|point| (point.pos, point.position)),
190 graph,
191 29..=29,
192 Node::point_position,
193 )
194}
195
196pub(crate) fn ordered_surface_candidates<'a>(
197 stream: &[u8],
198 graph: &'a Graph,
199) -> Vec<(usize, SurfaceGeometry, Option<&'a Node>)> {
200 ordered_fixed_candidates(
201 geometry::surfaces(stream)
202 .into_iter()
203 .map(|surface| (surface.pos, surface.geometry)),
204 graph,
205 50..=54,
206 Node::surface_geometry,
207 )
208}
209
210pub(crate) fn ordered_curve_candidates<'a>(
211 stream: &[u8],
212 graph: &'a Graph,
213) -> Vec<(usize, CurveGeometry, Option<&'a Node>)> {
214 ordered_fixed_candidates(
215 geometry::curves(stream)
216 .into_iter()
217 .map(|curve| (curve.pos, curve.geometry)),
218 graph,
219 30..=32,
220 Node::curve_geometry,
221 )
222}
223
224fn ordered_fixed_candidates<T>(
225 fallback: impl IntoIterator<Item = (usize, T)>,
226 graph: &Graph,
227 kinds: std::ops::RangeInclusive<u8>,
228 graph_value: impl Fn(&Node) -> Option<T>,
229) -> Vec<(usize, T, Option<&Node>)> {
230 let mut candidates = BTreeMap::new();
231 for (offset, value) in fallback {
232 let node = graph
233 .at_pos(offset)
234 .filter(|node| graph_value(node).is_some());
235 candidates.insert(offset, (value, node));
236 }
237 for node in kinds.flat_map(|kind| graph.of_kind(kind)) {
238 if let Some(value) = graph_value(node) {
239 candidates.insert(node.pos, (value, Some(node)));
240 }
241 }
242 candidates
243 .into_iter()
244 .map(|(offset, (value, node))| (offset, value, node))
245 .collect()
246}
247
248fn try_decode_geometry(
251 scan: &Scan,
252) -> Option<(
253 CadIr,
254 DecodeReport,
255 cadmpeg_ir::Annotations,
256 Vec<UnknownRecord>,
257)> {
258 let mut ir = CadIr::empty(Units::default());
259 let mut annotations = AnnotationBuilder::new();
260 let mut unknowns = Vec::new();
261 ir.source = Some(source_meta(scan));
262 let mut counts = Counts::default();
263 let mut body_node_ids = BTreeMap::new();
264 let parsed = crate::native::ParsedStreams::parse(scan);
265
266 for (si, stream) in scan.streams.iter().enumerate() {
267 if !stream.kind.is_parasolid() {
268 continue;
269 }
270 let view = parsed.stream(si).view_for_geometry();
271 let semantic = parsed.semantic_bytes(si);
272 let stream_name = format!("parasolid#{si}:{}", stream.kind.label());
273 let source_stream = annotations.stream(format!("nx:{stream_name}"));
274 let graph = &view.graph;
275 body_node_ids.extend(topology_body_node_ids(si, graph));
276 let mut points_by_xmt = BTreeMap::new();
277 let mut surfaces_by_xmt = BTreeMap::new();
278 let mut curves_by_xmt = BTreeMap::new();
279 let mut pcurves_by_xmt = BTreeMap::new();
280 let mut pcurve_supports_by_xmt = BTreeMap::new();
281 let mut trim_ranges = BTreeMap::new();
282 let mut pending_blend_supports = Vec::new();
283 let mut pending_blend_spines = Vec::new();
284 let mut pending_ext11_support_uv = Vec::new();
285 let first_surface = ir.model.surfaces.len();
286 let first_curve = ir.model.curves.len();
287 for (pi, (position_offset, position, node)) in ordered_point_candidates(semantic, graph)
288 .into_iter()
289 .enumerate()
290 {
291 let pid = PointId(format!("nx:s{si}:pt#{pi}"));
292 let vid = VertexId(format!("nx:s{si}:v#{pi}"));
293 if let Some(node) = node {
294 annotate_node(&mut annotations, &pid, source_stream, node, "POINT");
295 } else {
296 annotations
297 .note(&pid, source_stream, position_offset as u64)
298 .tag("POINT");
299 }
300 annotations.derived(&pid, "position");
301 ir.model.points.push(Point {
302 id: pid.clone(),
303 position,
304 source_object: None,
305 });
306 ir.model.vertices.push(Vertex {
307 id: vid.clone(),
308 point: pid.clone(),
309 tolerance: None,
310 });
311 if let Some(node) = node {
312 points_by_xmt.insert(node.xmt, pid);
313 }
314 counts.points += 1;
315 }
316
317 for (fi, (offset, geometry, node)) in ordered_surface_candidates(semantic, graph)
318 .into_iter()
319 .enumerate()
320 {
321 match &geometry {
322 SurfaceGeometry::Plane { .. } => counts.planes += 1,
323 SurfaceGeometry::Cylinder { .. } => counts.cylinders += 1,
324 SurfaceGeometry::Cone { .. } => counts.cones += 1,
325 SurfaceGeometry::Sphere { .. } => counts.spheres += 1,
326 SurfaceGeometry::Torus { .. } => counts.tori += 1,
327 SurfaceGeometry::Nurbs(_)
328 | SurfaceGeometry::Procedural { .. }
329 | SurfaceGeometry::Polygonal { .. }
330 | SurfaceGeometry::Transformed { .. }
331 | SurfaceGeometry::Unknown { .. } => {}
332 }
333 let id = SurfaceId(format!("nx:s{si}:surf#{fi}"));
334 if let Some(node) = node {
335 annotate_node(
336 &mut annotations,
337 &id,
338 source_stream,
339 node,
340 surface_tag(&geometry),
341 );
342 } else {
343 annotations
344 .note(&id, source_stream, offset as u64)
345 .tag(surface_tag(&geometry));
346 }
347 annotations.derived(&id, "geometry");
348 ir.model.surfaces.push(Surface {
349 id: id.clone(),
350 geometry,
351 source_object: None,
352 });
353 if let Some(node) = node {
354 surfaces_by_xmt.insert(node.xmt, id);
355 }
356 }
357
358 for (fi, surf) in crate::nurbs::surfaces(semantic).into_iter().enumerate() {
359 counts.nurbs_surfaces += 1;
360 let id = SurfaceId(format!("nx:s{si}:nurbs-surf#{fi}"));
361 annotations
362 .note(&id, source_stream, surf.pos as u64)
363 .tag("B_SPLINE_SURFACE");
364 annotations.derived(&id, "geometry");
365 ir.model.surfaces.push(Surface {
366 id: id.clone(),
367 geometry: surf.geometry,
368 source_object: None,
369 });
370 if let Some(node) = graph.at_pos(surf.pos) {
371 surfaces_by_xmt.insert(node.xmt, id);
372 }
373 }
374
375 for (oi, offset) in view.offset_surfaces.iter().copied().enumerate() {
376 let Some(support) = surfaces_by_xmt.get(&offset.support).cloned() else {
377 continue;
378 };
379 let surface_id = SurfaceId(format!("nx:s{si}:offset-surf#{oi}"));
380 let procedural_id = ProceduralSurfaceId(format!("nx:s{si}:offset#{oi}"));
381 annotations
382 .note(&surface_id, source_stream, offset.pos as u64)
383 .tag("OFFSET_SURF");
384 annotations.derived(&surface_id, "geometry");
385 ir.model.surfaces.push(Surface {
386 id: surface_id.clone(),
387 geometry: SurfaceGeometry::Procedural {
388 construction: procedural_id.clone(),
389 },
390 source_object: Some(SourceObjectAssociation {
391 format: "nx".into(),
392 object_id: format!("nx:s{si}:offset-surface-record#{}", offset.xmt),
393 name: None,
394 color: None,
395 visible: None,
396 layer: None,
397 instance_path: Vec::new(),
398 }),
399 });
400 annotations
401 .note(&procedural_id, source_stream, offset.pos as u64)
402 .tag("OFFSET_SURF");
403 annotations.derived(&procedural_id, "definition");
404 ir.model.procedural_surfaces.push(ProceduralSurface {
405 id: procedural_id,
406 surface: surface_id.clone(),
407 definition: ProceduralSurfaceDefinition::Offset {
408 support,
409 distance: offset.distance,
410 u_sense: Some(0),
411 v_sense: Some(0),
412 extension_flags: Vec::new(),
413 revision_form: None,
414 },
415 cache_fit_tolerance: None,
416 record_bounds: None,
417 });
418 surfaces_by_xmt.insert(offset.xmt, surface_id);
419 counts.offset_surfaces += 1;
420 }
421
422 for (bi, blend) in view.blend_surfaces.iter().copied().enumerate() {
423 let surface_id = SurfaceId(format!("nx:s{si}:blend-surf#{bi}"));
424 let procedural_id = ProceduralSurfaceId(format!("nx:s{si}:blend#{bi}"));
425 annotations
426 .note(&surface_id, source_stream, blend.pos as u64)
427 .tag("BLEND_SURF");
428 annotations.derived(&surface_id, "geometry");
429 ir.model.surfaces.push(Surface {
430 id: surface_id.clone(),
431 geometry: SurfaceGeometry::Procedural {
432 construction: procedural_id.clone(),
433 },
434 source_object: Some(SourceObjectAssociation {
435 format: "nx".to_string(),
436 object_id: format!("nx:s{si}:blend-surface-record#{}", blend.xmt),
437 name: None,
438 color: None,
439 visible: None,
440 layer: None,
441 instance_path: Vec::new(),
442 }),
443 });
444 annotations
445 .note(&procedural_id, source_stream, blend.pos as u64)
446 .tag("BLEND_SURF");
447 annotations.derived(&procedural_id, "definition");
448 let procedural_index = ir.model.procedural_surfaces.len();
449 ir.model.procedural_surfaces.push(ProceduralSurface {
450 id: procedural_id,
451 surface: surface_id.clone(),
452 definition: ProceduralSurfaceDefinition::Blend {
453 supports: [None, None],
454 spine: None,
455 radius: BlendRadiusLaw::Constant {
456 signed_radius: blend.offsets[0],
457 },
458 cross_section: BlendCrossSection::Circular,
459 native: None,
460 },
461 cache_fit_tolerance: None,
462 record_bounds: None,
463 });
464 pending_blend_supports.push((procedural_index, blend.supports, blend.offsets));
465 if blend.spine > 1 {
466 pending_blend_spines.push((procedural_index, blend.spine));
467 }
468 surfaces_by_xmt.insert(blend.xmt, surface_id);
469 counts.blend_surfaces += 1;
470 }
471
472 for (procedural_index, support_xmts, offsets) in pending_blend_supports {
473 let supports = [0, 1].map(|side| {
474 surfaces_by_xmt
475 .get(&support_xmts[side])
476 .cloned()
477 .map(|surface| BlendSupport {
478 surface,
479 reversed: offsets[side].is_sign_negative(),
480 })
481 });
482 let Some(ProceduralSurface {
483 definition:
484 ProceduralSurfaceDefinition::Blend {
485 supports: slots, ..
486 },
487 ..
488 }) = ir.model.procedural_surfaces.get_mut(procedural_index)
489 else {
490 continue;
491 };
492 *slots = supports;
493 }
494
495 for (ci, (offset, geometry, node)) in ordered_curve_candidates(semantic, graph)
496 .into_iter()
497 .enumerate()
498 {
499 match &geometry {
500 CurveGeometry::Line { .. } => counts.lines += 1,
501 CurveGeometry::Circle { .. } => counts.circles += 1,
502 CurveGeometry::Ellipse { .. } => counts.ellipses += 1,
503 CurveGeometry::Parabola { .. }
504 | CurveGeometry::Hyperbola { .. }
505 | CurveGeometry::Degenerate { .. }
506 | CurveGeometry::Composite { .. }
507 | CurveGeometry::Nurbs(_)
508 | CurveGeometry::Procedural { .. }
509 | CurveGeometry::Polyline { .. }
510 | CurveGeometry::Transformed { .. }
511 | CurveGeometry::Unknown { .. } => {}
512 }
513 let id = CurveId(format!("nx:s{si}:crv#{ci}"));
514 if let Some(node) = node {
515 annotate_node(
516 &mut annotations,
517 &id,
518 source_stream,
519 node,
520 curve_tag(&geometry),
521 );
522 } else {
523 annotations
524 .note(&id, source_stream, offset as u64)
525 .tag(curve_tag(&geometry));
526 }
527 annotations.derived(&id, "geometry");
528 ir.model.curves.push(Curve {
529 id: id.clone(),
530 geometry,
531 source_object: None,
532 });
533 if let Some(node) = node {
534 curves_by_xmt.insert(node.xmt, id);
535 }
536 }
537
538 for (ci, crv) in crate::nurbs::curves(semantic).into_iter().enumerate() {
539 counts.nurbs_curves += 1;
540 let id = CurveId(format!("nx:s{si}:nurbs-crv#{ci}"));
541 annotations
542 .note(&id, source_stream, crv.pos as u64)
543 .tag("B_SPLINE_CURVE");
544 annotations.derived(&id, "geometry");
545 ir.model.curves.push(Curve {
546 id: id.clone(),
547 geometry: crv.geometry,
548 source_object: None,
549 });
550 if let Some(node) = graph.at_pos(crv.pos) {
551 curves_by_xmt.insert(node.xmt, id);
552 }
553 }
554
555 for (pi, pcurve) in crate::nurbs::pcurves(semantic).into_iter().enumerate() {
556 let id = PcurveId(format!("nx:s{si}:pcurve#{pi}"));
557 annotations
558 .note(&id, source_stream, pcurve.pos as u64)
559 .tag("B_CURVE_2D");
560 annotations.derived(&id, "geometry");
561 ir.model.pcurves.push(Pcurve {
562 id: id.clone(),
563 geometry: pcurve.geometry,
564 wrapper_reversed: None,
565 native_tail_flags: None,
566 parameter_range: None,
567 fit_tolerance: None,
568 });
569 if let Some(node) = graph.at_pos(pcurve.pos) {
570 pcurves_by_xmt.insert(node.xmt, id);
571 }
572 }
573
574 let intersection_scan = view.intersections.clone();
575 counts
576 .intersection_rejections
577 .extend(intersection_scan.rejected);
578 let intersection_constructions = intersection_scan.constructions;
579 let charted_intersections: BTreeMap<_, _> = intersection_scan
580 .curves
581 .into_iter()
582 .map(|curve| (curve.xmt, curve))
583 .collect();
584 for (ci, construction) in intersection_constructions.into_iter().enumerate() {
585 let curve_id = CurveId(format!("nx:s{si}:intersection-crv#{ci}"));
586 let procedural_id = ProceduralCurveId(format!("nx:s{si}:intersection#{ci}"));
587 let unknown_id = UnknownId(format!("nx:container:parasolid#{si}"));
588 let charted = charted_intersections.get(&construction.xmt);
589 if let Some(charted) = charted {
590 pending_ext11_support_uv.push((
591 procedural_id.clone(),
592 charted.points.clone(),
593 charted.parameters.clone(),
594 charted.fit_tolerance,
595 charted.ext_support_uv.clone(),
596 ));
597 }
598 annotations
599 .note(&curve_id, source_stream, construction.pos as u64)
600 .tag("INTERSECTION");
601 if charted.is_some() {
602 annotations.derived(&curve_id, "geometry");
603 } else {
604 annotations.exactness(&curve_id, Exactness::Unknown);
605 }
606 ir.model.curves.push(Curve {
607 id: curve_id.clone(),
608 geometry: charted.map_or_else(
609 || CurveGeometry::Unknown {
610 record: Some(unknown_id.clone()),
611 },
612 |charted| {
613 CurveGeometry::Nurbs(NurbsCurve {
614 degree: 1,
615 knots: linear_knots(&charted.parameters),
616 control_points: charted.points.clone(),
617 weights: None,
618 periodic: false,
619 })
620 },
621 ),
622 source_object: Some(SourceObjectAssociation {
623 format: "nx".into(),
624 object_id: format!("nx:s{si}:intersection-record#{}", construction.xmt),
625 name: None,
626 color: None,
627 visible: None,
628 layer: None,
629 instance_path: Vec::new(),
630 }),
631 });
632 annotations
633 .note(&procedural_id, source_stream, construction.pos as u64)
634 .tag("INTERSECTION");
635 if charted.is_some() {
636 annotations.derived(&procedural_id, "definition");
637 } else {
638 annotations.exactness(&procedural_id, Exactness::Unknown);
639 }
640 ir.model.procedural_curves.push(ProceduralCurve {
641 id: procedural_id,
642 curve: curve_id.clone(),
643 definition: charted.map_or_else(
644 || ProceduralCurveDefinition::Unknown {
645 native_kind: Some("nx:intersection".into()),
646 record: Some(unknown_id),
647 },
648 |charted| {
649 let mut support_uv = charted.support_uv.clone();
650 if let Some(ext_support_uv) = assign_ext11_support_uv(
651 &ir,
652 &surfaces_by_xmt,
653 charted.supports,
654 &charted.points,
655 charted.fit_tolerance,
656 &charted.ext_support_uv,
657 ) {
658 for side in 0..2 {
659 if support_uv[side].is_none() {
660 support_uv[side].clone_from(&ext_support_uv[side]);
661 }
662 }
663 }
664 let first = intersection_side(
665 &ir,
666 &surfaces_by_xmt,
667 charted.supports[0],
668 support_uv[0]
669 .as_deref()
670 .filter(|uv| uv.len() == charted.parameters.len())
671 .map(|uv| (uv, charted.parameters.as_slice())),
672 );
673 let second = intersection_side(
674 &ir,
675 &surfaces_by_xmt,
676 charted.supports[1],
677 support_uv[1]
678 .as_deref()
679 .filter(|uv| uv.len() == charted.parameters.len())
680 .map(|uv| (uv, charted.parameters.as_slice())),
681 );
682 ProceduralCurveDefinition::Intersection {
683 context: IntcurveSupportContext {
684 sides: [first, second],
685 parameter_range: [
686 charted.parameters[0],
687 *charted
688 .parameters
689 .last()
690 .expect("validated chart has points"),
691 ],
692 discontinuities: [Vec::new(), Vec::new(), Vec::new()],
693 },
694 discontinuity_flag: false,
695 }
696 },
697 ),
698 cache_fit_tolerance: charted.map(|charted| charted.fit_tolerance),
699 });
700 curves_by_xmt.insert(construction.xmt, curve_id);
701 counts.intersection_curves += 1;
702 }
703
704 for (procedural_index, spine_xmt) in pending_blend_spines {
705 let Some(spine) = curves_by_xmt.get(&spine_xmt).cloned() else {
706 continue;
707 };
708 let Some(ProceduralSurface {
709 definition: ProceduralSurfaceDefinition::Blend { spine: slot, .. },
710 ..
711 }) = ir.model.procedural_surfaces.get_mut(procedural_index)
712 else {
713 continue;
714 };
715 *slot = Some(spine);
716 }
717
718 let trimmed_curves = &view.trimmed_curves;
719 let mut normalized_pcurves = BTreeSet::new();
720 let surface_curves = &view.surface_curves;
721 loop {
722 let mapped = curves_by_xmt.len() + pcurves_by_xmt.len() + pcurve_supports_by_xmt.len();
723 for trim in trimmed_curves {
724 if let Some(basis) = curves_by_xmt.get(&trim.basis).cloned() {
725 let parameters = canonical_trim_range(&ir, &basis, trim.parameters);
726 curves_by_xmt.insert(trim.xmt, basis);
727 if let Some(parameters) = parameters {
728 trim_ranges.insert(trim.xmt, parameters);
729 }
730 }
731 if let Some(pcurve) = pcurves_by_xmt.get(&trim.basis).cloned() {
732 if let Some(carrier) = ir.model.pcurves.iter_mut().find(|p| p.id == pcurve) {
733 carrier.parameter_range = Some(trim.parameters);
734 }
735 pcurves_by_xmt.insert(trim.xmt, pcurve);
736 if let Some(support) = pcurve_supports_by_xmt.get(&trim.basis).cloned() {
737 pcurve_supports_by_xmt.insert(trim.xmt, support);
738 }
739 trim_ranges.insert(trim.xmt, trim.parameters);
740 }
741 }
742 for surface_curve in surface_curves {
743 if let Some(pcurve) = pcurves_by_xmt.get(&surface_curve.pcurve).cloned() {
744 if !normalized_pcurves.contains(&pcurve) {
745 let support = surfaces_by_xmt
746 .get(&surface_curve.surface)
747 .and_then(|id| {
748 ir.model.surfaces.iter().find(|surface| surface.id == *id)
749 })
750 .map(|surface| surface.geometry.clone());
751 let normalized = if let (Some(support), Some(carrier)) = (
752 support,
753 ir.model
754 .pcurves
755 .iter_mut()
756 .find(|candidate| candidate.id == pcurve),
757 ) {
758 normalize_pcurve_parameters(&mut carrier.geometry, &support).is_some()
759 } else {
760 false
761 };
762 if !normalized {
763 pcurves_by_xmt.remove(&surface_curve.pcurve);
764 ir.model.pcurves.retain(|candidate| candidate.id != pcurve);
765 continue;
766 }
767 normalized_pcurves.insert(pcurve.clone());
768 }
769 if let Some(carrier) = ir.model.pcurves.iter_mut().find(|p| p.id == pcurve) {
770 carrier.fit_tolerance = decoded_tolerance(surface_curve.tolerance);
771 }
772 pcurves_by_xmt.insert(surface_curve.xmt, pcurve);
773 if let Some(support) = surfaces_by_xmt.get(&surface_curve.surface).cloned() {
774 pcurve_supports_by_xmt.insert(surface_curve.xmt, support);
775 }
776 }
777 if let Some(original) = curves_by_xmt.get(&surface_curve.original).cloned() {
778 curves_by_xmt.insert(surface_curve.xmt, original);
779 }
780 }
781 if curves_by_xmt.len() + pcurves_by_xmt.len() + pcurve_supports_by_xmt.len() == mapped {
782 break;
783 }
784 }
785
786 retain_unresolved_topology_carriers(
787 &mut ir,
788 si,
789 graph,
790 &mut surfaces_by_xmt,
791 &mut curves_by_xmt,
792 &pcurves_by_xmt,
793 source_stream,
794 &mut annotations,
795 );
796
797 emit_topology(
798 &mut ir,
799 si,
800 graph,
801 &points_by_xmt,
802 &surfaces_by_xmt,
803 &curves_by_xmt,
804 &pcurves_by_xmt,
805 &pcurve_supports_by_xmt,
806 &trim_ranges,
807 source_stream,
808 &mut annotations,
809 );
810 complete_ext11_support_uv(&mut ir, &pending_ext11_support_uv);
811 complete_parameterization_equivalent_support_uv(&mut ir);
812 complete_support_uv(&mut ir, &pending_ext11_support_uv);
813 attach_completed_intersection_pcurves(
814 &mut ir,
815 graph,
816 &format!("nx:s{si}"),
817 source_stream,
818 &mut annotations,
819 );
820
821 let mut unknown = unknown_stream(si, stream);
823 unknown.links.extend(
824 ir.model.surfaces[first_surface..]
825 .iter()
826 .map(|surface| surface.id.0.clone()),
827 );
828 unknown.links.extend(
829 ir.model.curves[first_curve..]
830 .iter()
831 .map(|curve| curve.id.0.clone()),
832 );
833 let container_stream = annotations.stream("nx:container");
834 annotations
835 .note(&unknown.id, container_stream, stream.file_offset as u64)
836 .tag(stream.kind.label());
837 annotations.exactness(&unknown.id, Exactness::Derived);
838 if !unknown.links.is_empty() {
839 annotations.derived(&unknown.id, "links");
840 }
841 unknowns.push(unknown);
842 }
843
844 if counts.points == 0 && counts.surfaces() == 0 && counts.curves() == 0 {
845 return None;
846 }
847
848 let rmfastload_ids = scan
849 .container
850 .rmfastload_object_id_table()
851 .map(|(_, table)| {
852 table
853 .object_ids
854 .into_iter()
855 .map(|object_id| object_id.value)
856 .collect::<Vec<_>>()
857 })
858 .unwrap_or_default();
859 let model = crate::native::NativeModel::extract(&scan.container, &scan.streams, &parsed);
866 let mut active_body_selection = select_active_body(&mut ir, &body_node_ids, &rmfastload_ids);
867 if !active_body_selection {
868 active_body_selection = select_terminal_feature_bodies(&mut ir, &model);
869 }
870 classify_body_kinds(&mut ir);
871 crate::native::attach_annotations(&mut ir, &model, scan, &mut annotations, &mut unknowns)
872 .ok()?;
873 prune_unreferenced_unknown_carriers(&mut ir);
874 finalize_point_topology(&mut ir, &mut annotations);
875 let referenced_pcurves: BTreeSet<_> = ir
876 .model
877 .coedges
878 .iter()
879 .flat_map(|coedge| coedge.pcurves.iter().map(|pcurve| pcurve.pcurve.clone()))
880 .collect();
881 ir.model
882 .pcurves
883 .retain(|pcurve| referenced_pcurves.contains(&pcurve.id));
884 retain_live_unknown_links(&ir, &mut unknowns, &mut annotations);
885 let mut annotations = annotations.build();
886 retain_live_annotations(&ir, &unknowns, &mut annotations);
887 let mut report = build_geometry_report(
888 scan,
889 &ir,
890 &counts,
891 !ir.model.faces.is_empty(),
892 ir.model.bodies.len() > 1 && !active_body_selection,
893 ir.model.tessellations.len(),
894 );
895 report_untransferred_streams(scan, &mut report);
896 Some((ir, report, annotations, unknowns))
897}
898
899pub(crate) fn prune_unreferenced_unknown_carriers(ir: &mut CadIr) {
900 let mut used_surfaces: BTreeSet<_> = ir
901 .model
902 .faces
903 .iter()
904 .map(|face| face.surface.clone())
905 .collect();
906 let mut used_curves: BTreeSet<_> = ir
907 .model
908 .edges
909 .iter()
910 .filter_map(|edge| edge.curve.clone())
911 .collect();
912 loop {
913 let previous = (used_surfaces.len(), used_curves.len());
914 for procedural in &ir.model.procedural_surfaces {
915 if !used_surfaces.contains(&procedural.surface) {
916 continue;
917 }
918 match &procedural.definition {
919 ProceduralSurfaceDefinition::Offset { support, .. } => {
920 used_surfaces.insert(support.clone());
921 }
922 ProceduralSurfaceDefinition::Blend {
923 supports, spine, ..
924 } => {
925 used_surfaces.extend(
926 supports
927 .iter()
928 .flatten()
929 .map(|support| support.surface.clone()),
930 );
931 used_curves.extend(spine.iter().cloned());
932 }
933 _ => {}
934 }
935 }
936 for procedural in &ir.model.procedural_curves {
937 if !used_curves.contains(&procedural.curve) {
938 continue;
939 }
940 match &procedural.definition {
941 ProceduralCurveDefinition::Intersection { context, .. }
942 | ProceduralCurveDefinition::SurfaceCurve { context, .. } => {
943 used_surfaces
944 .extend(context.sides.iter().filter_map(|side| side.surface.clone()));
945 }
946 _ => {}
947 }
948 }
949 if previous == (used_surfaces.len(), used_curves.len()) {
950 break;
951 }
952 }
953 ir.model.surfaces.retain(|surface| {
954 !matches!(surface.geometry, SurfaceGeometry::Unknown { .. })
955 || used_surfaces.contains(&surface.id)
956 });
957 ir.model.curves.retain(|curve| {
958 !matches!(curve.geometry, CurveGeometry::Unknown { .. }) || used_curves.contains(&curve.id)
959 });
960}
961
962fn unmatched_delta_tombstone_count(scan: &Scan) -> usize {
963 let pairs = crate::native::paired_delta_streams(scan);
964 let mut current = pairs
965 .keys()
966 .map(|partition| (*partition, scan.streams[*partition].inflated.clone()))
967 .collect::<BTreeMap<_, _>>();
968 let paired_deltas = pairs.values().flatten().copied().collect::<BTreeSet<_>>();
969 let mut unmatched = 0usize;
970 for (delta, stream) in scan.streams.iter().enumerate() {
971 if stream.kind == StreamKind::Deltas && !paired_deltas.contains(&delta) {
972 unmatched += crate::deltas::unmatched_terminal_tombstones(&[], &stream.inflated);
973 }
974 }
975 for (partition, deltas) in pairs {
976 for delta in deltas {
977 let delta_bytes = &scan.streams[delta].inflated;
978 let partition_bytes = current
979 .get_mut(&partition)
980 .expect("paired partition was initialized");
981 unmatched += crate::deltas::unmatched_terminal_tombstones(partition_bytes, delta_bytes);
982 *partition_bytes = crate::deltas::merge_full_records(partition_bytes, delta_bytes);
983 }
984 }
985 unmatched
986}
987
988fn retain_live_annotations(
989 ir: &CadIr,
990 unknowns: &[UnknownRecord],
991 annotations: &mut cadmpeg_ir::Annotations,
992) {
993 let mut ids = BTreeSet::new();
994 macro_rules! add_ids {
995 ($($arena:expr),+ $(,)?) => {
996 $(ids.extend($arena.iter().map(|entity| entity.id.to_string()));)+
997 };
998 }
999 add_ids!(
1000 ir.model.bodies,
1001 ir.model.regions,
1002 ir.model.shells,
1003 ir.model.faces,
1004 ir.model.loops,
1005 ir.model.coedges,
1006 ir.model.edges,
1007 ir.model.vertices,
1008 ir.model.points,
1009 ir.model.surfaces,
1010 ir.model.curves,
1011 ir.model.pcurves,
1012 ir.model.procedural_surfaces,
1013 ir.model.procedural_curves,
1014 ir.model.features,
1015 );
1016 ids.extend(unknowns.iter().map(|unknown| unknown.id.to_string()));
1017 annotations.provenance.retain(|id, _| ids.contains(id));
1018 annotations.exactness.retain(|id, _| ids.contains(id));
1019}
1020
1021fn retain_live_unknown_links(
1022 ir: &CadIr,
1023 unknowns: &mut [UnknownRecord],
1024 annotations: &mut AnnotationBuilder,
1025) {
1026 let mut ids = BTreeSet::new();
1027 ids.extend(ir.model.surfaces.iter().map(|entity| entity.id.to_string()));
1028 ids.extend(ir.model.curves.iter().map(|entity| entity.id.to_string()));
1029 ids.extend(ir.model.pcurves.iter().map(|entity| entity.id.to_string()));
1030 ids.extend(
1031 ir.model
1032 .procedural_surfaces
1033 .iter()
1034 .map(|entity| entity.id.to_string()),
1035 );
1036 ids.extend(
1037 ir.model
1038 .procedural_curves
1039 .iter()
1040 .map(|entity| entity.id.to_string()),
1041 );
1042 let mut empty_links = Vec::new();
1043 for unknown in unknowns.iter_mut() {
1044 unknown.links.retain(|link| ids.contains(link));
1045 if unknown.links.is_empty() {
1046 empty_links.push(unknown.id.to_string());
1047 }
1048 }
1049 let _ = (empty_links, annotations);
1050}
1051
1052fn topology_body_node_ids(stream_index: usize, graph: &Graph) -> BTreeMap<BodyId, BTreeSet<u32>> {
1053 let prefix = format!("nx:s{stream_index}");
1054 let body_xmts: BTreeSet<_> = graph
1055 .body_shape_shells()
1056 .into_iter()
1057 .filter_map(|shell| shell.shell_fields().map(|fields| fields.body))
1058 .collect();
1059 body_xmts
1060 .into_iter()
1061 .map(|body_xmt| {
1062 let shells: BTreeSet<_> = graph
1063 .of_kind(13)
1064 .filter(|shell| {
1065 shell
1066 .shell_fields()
1067 .is_some_and(|fields| fields.body == body_xmt)
1068 })
1069 .map(|shell| shell.xmt)
1070 .collect();
1071 let faces: Vec<_> = graph
1072 .of_kind(14)
1073 .filter(|face| {
1074 face.face_fields()
1075 .is_some_and(|fields| shells.contains(&fields.shell))
1076 })
1077 .collect();
1078 let face_xmts: BTreeSet<_> = faces.iter().map(|face| face.xmt).collect();
1079 let loops: BTreeSet<_> = graph
1080 .of_kind(15)
1081 .filter(|loop_| {
1082 loop_
1083 .loop_fields()
1084 .is_some_and(|fields| face_xmts.contains(&fields.face))
1085 })
1086 .map(|loop_| loop_.xmt)
1087 .collect();
1088 let fins: Vec<_> = graph
1089 .of_kind(17)
1090 .filter(|fin| {
1091 fin.fin_fields()
1092 .is_some_and(|fields| loops.contains(&fields.loop_xmt))
1093 })
1094 .collect();
1095 let edge_xmts: BTreeSet<_> = fins
1096 .iter()
1097 .filter_map(|fin| fin.fin_fields().map(|fields| fields.edge))
1098 .collect();
1099 let vertex_xmts: BTreeSet<_> = fins
1100 .iter()
1101 .filter_map(|fin| fin.fin_fields().map(|fields| fields.vertex))
1102 .collect();
1103 let ids = faces
1104 .into_iter()
1105 .filter_map(|face| face.u32_at(4))
1106 .chain(
1107 graph
1108 .of_kind(16)
1109 .filter(|edge| edge_xmts.contains(&edge.xmt))
1110 .filter_map(|edge| edge.u32_at(4)),
1111 )
1112 .chain(
1113 graph
1114 .of_kind(18)
1115 .filter(|vertex| vertex_xmts.contains(&vertex.xmt))
1116 .filter_map(|vertex| vertex.u32_at(4)),
1117 )
1118 .collect();
1119 (BodyId(format!("{prefix}:body#{body_xmt}")), ids)
1120 })
1121 .collect()
1122}
1123
1124fn select_active_body(
1125 ir: &mut CadIr,
1126 body_node_ids: &BTreeMap<BodyId, BTreeSet<u32>>,
1127 rmfastload_ids: &[u32],
1128) -> bool {
1129 if rmfastload_ids.is_empty() || ir.model.bodies.len() <= 1 {
1130 return false;
1131 }
1132 let active: BTreeSet<_> = rmfastload_ids.iter().copied().collect();
1133 let mut scored: Vec<_> = ir
1134 .model
1135 .bodies
1136 .iter()
1137 .map(|body| {
1138 let ids = body_node_ids.get(&body.id);
1139 let count = ids.map_or(0, BTreeSet::len);
1140 let hits = ids.map_or(0, |ids| ids.intersection(&active).count());
1141 (hits, count, body.id.clone())
1142 })
1143 .collect();
1144 scored.sort_by(|first, second| second.0.cmp(&first.0).then(second.1.cmp(&first.1)));
1145 let Some(&(top_hits, top_count, ref top_body)) = scored.first() else {
1146 return false;
1147 };
1148 let next_hits = scored.get(1).map_or(0, |score| score.0);
1149 let mut selected: BTreeSet<_> = scored
1150 .iter()
1151 .filter(|(hits, count, _)| *hits > 0 && *count > 0 && (*hits as f64 / *count as f64) > 0.10)
1152 .map(|(_, _, body)| body.clone())
1153 .collect();
1154 let dominant = top_hits >= 5 * next_hits.max(1);
1155 if dominant {
1156 selected.retain(|body| body == top_body);
1157 }
1158 if top_count == 0
1159 || (top_hits as f64 / top_count as f64) <= 0.10
1160 || selected.is_empty()
1161 || (selected.len() == 1 && !dominant)
1162 {
1163 return false;
1164 }
1165 prune_inactive_topology(ir, &selected);
1166 if let Some(source) = &mut ir.source {
1167 source.attributes.insert(
1168 "active_body_selector".to_string(),
1169 "rmfastload_object_id_membership".to_string(),
1170 );
1171 source
1172 .attributes
1173 .insert("rmfastload_hits".to_string(), top_hits.to_string());
1174 source.attributes.insert(
1175 "rmfastload_active_body_count".to_string(),
1176 selected.len().to_string(),
1177 );
1178 }
1179 true
1180}
1181
1182fn select_terminal_feature_bodies(ir: &mut CadIr, model: &crate::native::NativeModel) -> bool {
1183 if ir.model.bodies.len() <= 1 {
1184 return false;
1185 }
1186 let labels = model.features.feature_operation_labels.as_slice();
1191 let body_references = model.features.feature_body_references.as_slice();
1192 let booleans = model.features.feature_boolean_operations.as_slice();
1193 let bindings = model.segments.segment_body_bindings.as_slice();
1194 let body_operands = model.features.feature_operation_body_operands.as_slice();
1195 if booleans.is_empty() && body_operands.is_empty() {
1196 return false;
1197 }
1198 let Some(statuses) = crate::native::segment_body_lineage_statuses(
1199 labels,
1200 body_references,
1201 booleans,
1202 body_operands,
1203 bindings,
1204 ) else {
1205 return false;
1206 };
1207 let mut mapped = BTreeSet::new();
1208 let mut selected = BTreeSet::new();
1209 for (binding, status) in bindings.iter().filter_map(|binding| {
1210 statuses
1211 .iter()
1212 .find(|status| status.segment_body_binding == binding.id)
1213 .map(|status| (binding, status))
1214 }) {
1215 let prefix = format!("nx:s{}:", binding.stream_ordinal);
1216 let stream_bodies = ir
1217 .model
1218 .bodies
1219 .iter()
1220 .filter(|body| body.id.0.starts_with(&prefix))
1221 .map(|body| body.id.clone())
1222 .collect::<Vec<_>>();
1223 if stream_bodies.is_empty() {
1224 continue;
1225 }
1226 mapped.extend(stream_bodies.iter().cloned());
1227 if status.terminal {
1228 selected.extend(stream_bodies);
1229 }
1230 }
1231 let emitted = ir
1232 .model
1233 .bodies
1234 .iter()
1235 .map(|body| body.id.clone())
1236 .collect::<BTreeSet<_>>();
1237 if mapped != emitted || selected.is_empty() || selected.len() == emitted.len() {
1238 return false;
1239 }
1240
1241 prune_inactive_topology(ir, &selected);
1242 if let Some(source) = &mut ir.source {
1243 source.attributes.insert(
1244 "active_body_selector".to_string(),
1245 "terminal_feature_body_lineage".to_string(),
1246 );
1247 source.attributes.insert(
1248 "feature_terminal_body_count".to_string(),
1249 selected.len().to_string(),
1250 );
1251 }
1252 true
1253}
1254
1255fn prune_inactive_topology(ir: &mut CadIr, selected: &BTreeSet<BodyId>) {
1256 ir.model.bodies.retain(|body| selected.contains(&body.id));
1257 ir.model
1258 .regions
1259 .retain(|region| selected.contains(®ion.body));
1260 let regions: BTreeSet<_> = ir
1261 .model
1262 .regions
1263 .iter()
1264 .map(|region| region.id.clone())
1265 .collect();
1266 ir.model
1267 .shells
1268 .retain(|shell| regions.contains(&shell.region));
1269 let shells: BTreeSet<_> = ir
1270 .model
1271 .shells
1272 .iter()
1273 .map(|shell| shell.id.clone())
1274 .collect();
1275 ir.model.faces.retain(|face| shells.contains(&face.shell));
1276 let faces: BTreeSet<_> = ir.model.faces.iter().map(|face| face.id.clone()).collect();
1277 ir.model.loops.retain(|loop_| faces.contains(&loop_.face));
1278 let loops: BTreeSet<_> = ir
1279 .model
1280 .loops
1281 .iter()
1282 .map(|loop_| loop_.id.clone())
1283 .collect();
1284 ir.model
1285 .coedges
1286 .retain(|coedge| loops.contains(&coedge.owner_loop));
1287 let edges: BTreeSet<_> = ir
1288 .model
1289 .coedges
1290 .iter()
1291 .map(|coedge| coedge.edge.clone())
1292 .chain(
1293 ir.model
1294 .shells
1295 .iter()
1296 .flat_map(|shell| shell.wire_edges.iter().cloned()),
1297 )
1298 .collect();
1299 ir.model.edges.retain(|edge| edges.contains(&edge.id));
1300 let vertices: BTreeSet<_> = ir
1301 .model
1302 .edges
1303 .iter()
1304 .flat_map(|edge| [edge.start.clone(), edge.end.clone()])
1305 .chain(
1306 ir.model
1307 .shells
1308 .iter()
1309 .flat_map(|shell| shell.free_vertices.iter().cloned()),
1310 )
1311 .collect();
1312 ir.model
1313 .vertices
1314 .retain(|vertex| vertices.contains(&vertex.id));
1315 let points: BTreeSet<_> = ir
1316 .model
1317 .vertices
1318 .iter()
1319 .map(|vertex| vertex.point.clone())
1320 .collect();
1321 ir.model.points.retain(|point| points.contains(&point.id));
1322 prune_inactive_geometry(ir);
1323}
1324
1325fn prune_inactive_geometry(ir: &mut CadIr) {
1326 let mut surfaces: BTreeSet<_> = ir
1327 .model
1328 .faces
1329 .iter()
1330 .map(|face| face.surface.clone())
1331 .collect();
1332 let mut curves: BTreeSet<_> = ir
1333 .model
1334 .edges
1335 .iter()
1336 .filter_map(|edge| edge.curve.clone())
1337 .collect();
1338 let pcurves: BTreeSet<_> = ir
1339 .model
1340 .coedges
1341 .iter()
1342 .flat_map(|coedge| coedge.pcurves.iter().map(|pcurve| pcurve.pcurve.clone()))
1343 .collect();
1344
1345 loop {
1346 let old_surface_count = surfaces.len();
1347 let old_curve_count = curves.len();
1348 for procedural in &ir.model.procedural_surfaces {
1349 if !surfaces.contains(&procedural.surface) {
1350 continue;
1351 }
1352 match &procedural.definition {
1353 ProceduralSurfaceDefinition::Offset { support, .. } => {
1354 surfaces.insert(support.clone());
1355 }
1356 ProceduralSurfaceDefinition::Blend {
1357 supports, spine, ..
1358 } => {
1359 surfaces.extend(
1360 supports
1361 .iter()
1362 .flatten()
1363 .map(|support| support.surface.clone()),
1364 );
1365 curves.extend(spine.iter().cloned());
1366 }
1367 _ => {}
1368 }
1369 }
1370 for procedural in &ir.model.procedural_curves {
1371 if !curves.contains(&procedural.curve) {
1372 continue;
1373 }
1374 match &procedural.definition {
1375 ProceduralCurveDefinition::Intersection { context, .. }
1376 | ProceduralCurveDefinition::SurfaceCurve { context, .. } => {
1377 surfaces.extend(context.sides.iter().filter_map(|side| side.surface.clone()));
1378 }
1379 _ => {}
1380 }
1381 }
1382 if surfaces.len() == old_surface_count && curves.len() == old_curve_count {
1383 break;
1384 }
1385 }
1386
1387 ir.model
1388 .procedural_surfaces
1389 .retain(|procedural| surfaces.contains(&procedural.surface));
1390 ir.model
1391 .procedural_curves
1392 .retain(|procedural| curves.contains(&procedural.curve));
1393 ir.model
1394 .surfaces
1395 .retain(|surface| surfaces.contains(&surface.id));
1396 ir.model.curves.retain(|curve| curves.contains(&curve.id));
1397 ir.model
1398 .pcurves
1399 .retain(|pcurve| pcurves.contains(&pcurve.id));
1400}
1401
1402fn finalize_point_topology(ir: &mut CadIr, annotations: &mut AnnotationBuilder) {
1403 let referenced_points: BTreeSet<_> = ir
1404 .model
1405 .vertices
1406 .iter()
1407 .map(|vertex| vertex.point.clone())
1408 .collect();
1409 if !ir.model.bodies.is_empty() {
1410 ir.model
1411 .points
1412 .retain(|point| referenced_points.contains(&point.id));
1413 return;
1414 }
1415
1416 if ir.model.points.is_empty() {
1417 return;
1418 }
1419
1420 let body_id = BodyId("nx:derived:point-body#0".to_string());
1421 let region_id = RegionId("nx:derived:point-region#0".to_string());
1422 let shell_id = ShellId("nx:derived:point-shell#0".to_string());
1423 let stream = annotations.stream("nx:container");
1424 for id in [&body_id.0, ®ion_id.0, &shell_id.0] {
1425 annotations
1426 .note(id, stream, 0)
1427 .tag("derived_point_topology");
1428 annotations.exactness(id, Exactness::Inferred);
1429 }
1430
1431 let mut free_vertices = Vec::with_capacity(ir.model.points.len());
1432 for (index, point) in ir.model.points.iter().enumerate() {
1433 let vertex_id = VertexId(format!("nx:derived:point-vertex#{index}"));
1434 annotations
1435 .note(&vertex_id, stream, 0)
1436 .tag("derived_point_topology");
1437 annotations.exactness(&vertex_id, Exactness::Inferred);
1438 ir.model.vertices.push(Vertex {
1439 id: vertex_id.clone(),
1440 point: point.id.clone(),
1441 tolerance: None,
1442 });
1443 free_vertices.push(vertex_id);
1444 }
1445 ir.model.shells.push(Shell {
1446 id: shell_id.clone(),
1447 region: region_id.clone(),
1448 faces: Vec::new(),
1449 wire_edges: Vec::new(),
1450 free_vertices,
1451 });
1452 ir.model.regions.push(Region {
1453 id: region_id.clone(),
1454 body: body_id.clone(),
1455 shells: vec![shell_id],
1456 });
1457 ir.model.bodies.push(Body {
1458 id: body_id,
1459 kind: BodyKind::General,
1460 regions: vec![region_id],
1461 transform: None,
1462 name: None,
1463 color: None,
1464 visible: None,
1465 });
1466}
1467
1468fn classify_body_kinds(ir: &mut CadIr) {
1469 let region_bodies: BTreeMap<_, _> = ir
1470 .model
1471 .regions
1472 .iter()
1473 .map(|region| (region.id.clone(), region.body.clone()))
1474 .collect();
1475 let shell_bodies: BTreeMap<_, _> = ir
1476 .model
1477 .shells
1478 .iter()
1479 .filter_map(|shell| {
1480 region_bodies
1481 .get(&shell.region)
1482 .cloned()
1483 .map(|body| (shell.id.clone(), body))
1484 })
1485 .collect();
1486 let face_bodies: BTreeMap<_, _> = ir
1487 .model
1488 .faces
1489 .iter()
1490 .filter_map(|face| {
1491 shell_bodies
1492 .get(&face.shell)
1493 .cloned()
1494 .map(|body| (face.id.clone(), body))
1495 })
1496 .collect();
1497 let loop_bodies: BTreeMap<_, _> = ir
1498 .model
1499 .loops
1500 .iter()
1501 .filter_map(|loop_| {
1502 face_bodies
1503 .get(&loop_.face)
1504 .cloned()
1505 .map(|body| (loop_.id.clone(), body))
1506 })
1507 .collect();
1508 let coedge_bodies: BTreeMap<_, _> = ir
1509 .model
1510 .coedges
1511 .iter()
1512 .filter_map(|coedge| {
1513 loop_bodies
1514 .get(&coedge.owner_loop)
1515 .cloned()
1516 .map(|body| (coedge.id.clone(), body))
1517 })
1518 .collect();
1519 let mut edge_uses = BTreeMap::<BodyId, BTreeMap<EdgeId, usize>>::new();
1520 for coedge in &ir.model.coedges {
1521 let Some(body) = coedge_bodies.get(&coedge.id) else {
1522 continue;
1523 };
1524 *edge_uses
1525 .entry(body.clone())
1526 .or_default()
1527 .entry(coedge.edge.clone())
1528 .or_default() += 1;
1529 }
1530 for body in &mut ir.model.bodies {
1531 body.kind = if edge_uses
1532 .get(&body.id)
1533 .is_some_and(|uses| !uses.is_empty() && uses.values().all(|use_count| *use_count == 2))
1534 {
1535 BodyKind::Solid
1536 } else {
1537 BodyKind::Sheet
1538 };
1539 }
1540}
1541
1542fn linear_knots(parameters: &[f64]) -> Vec<f64> {
1543 let mut knots = Vec::with_capacity(parameters.len() + 2);
1544 knots.push(parameters[0]);
1545 knots.extend_from_slice(parameters);
1546 knots.push(*parameters.last().expect("non-empty chart parameters"));
1547 knots
1548}
1549
1550pub(crate) fn assign_ext11_support_uv(
1551 ir: &CadIr,
1552 surfaces_by_xmt: &BTreeMap<u32, SurfaceId>,
1553 supports: [u32; 2],
1554 points: &[Point3],
1555 fit_tolerance: f64,
1556 lanes: &[Option<Vec<[f64; 2]>>; 2],
1557) -> Option<[Option<Vec<[f64; 2]>>; 2]> {
1558 let surface_ids = supports.map(|support| surfaces_by_xmt.get(&support).cloned());
1559 let [Some(first_surface), Some(second_surface)] = surface_ids else {
1560 return None;
1561 };
1562 assign_ext11_support_uv_to_surfaces(
1563 ir,
1564 [&first_surface, &second_surface],
1565 points,
1566 fit_tolerance,
1567 lanes,
1568 )
1569}
1570
1571pub(crate) fn assign_ext11_support_uv_to_surfaces(
1572 ir: &CadIr,
1573 surfaces: [&SurfaceId; 2],
1574 points: &[Point3],
1575 fit_tolerance: f64,
1576 lanes: &[Option<Vec<[f64; 2]>>; 2],
1577) -> Option<[Option<Vec<[f64; 2]>>; 2]> {
1578 let lane_matches_surface = |surface: &SurfaceId, lane: usize| {
1579 let Some(values) = lanes[lane]
1580 .as_deref()
1581 .filter(|values| values.len() == points.len())
1582 else {
1583 return false;
1584 };
1585 let Some(geometry) = ir
1586 .model
1587 .surfaces
1588 .iter()
1589 .find(|candidate| &candidate.id == surface)
1590 .map(|surface| &surface.geometry)
1591 else {
1592 return false;
1593 };
1594 values.iter().zip(points).all(|(uv, point)| {
1595 let Some(uv) = surface_parameters(geometry, *uv) else {
1596 return false;
1597 };
1598 model_surface_point_by_id(ir, surface, uv.u, uv.v)
1599 .is_some_and(|candidate| point_distance(candidate, *point) <= fit_tolerance)
1600 })
1601 };
1602 let matches = [
1603 [
1604 lane_matches_surface(surfaces[0], 0),
1605 lane_matches_surface(surfaces[0], 1),
1606 ],
1607 [
1608 lane_matches_surface(surfaces[1], 0),
1609 lane_matches_surface(surfaces[1], 1),
1610 ],
1611 ];
1612 let mut assigned = [None, None];
1613 let mut assigned_lanes = [None, None];
1614 for lane in 0..2 {
1615 let support_matches = [matches[0][lane], matches[1][lane]];
1616 let Some(support) = support_matches
1617 .iter()
1618 .position(|matches| *matches)
1619 .filter(|_| support_matches.iter().filter(|matches| **matches).count() == 1)
1620 else {
1621 continue;
1622 };
1623 if assigned[support].is_some() {
1624 return None;
1625 }
1626 assigned[support].clone_from(&lanes[lane]);
1627 assigned_lanes[support] = Some(lane);
1628 }
1629 if surfaces[0] != surfaces[1] && assigned.iter().filter(|lane| lane.is_some()).count() == 1 {
1630 let assigned_support = assigned.iter().position(Option::is_some)?;
1631 let assigned_lane = assigned_lanes[assigned_support]?;
1632 let other_support = 1 - assigned_support;
1633 let other_lane = 1 - assigned_lane;
1634 if lane_matches_surface(surfaces[other_support], other_lane) {
1635 assigned[other_support].clone_from(&lanes[other_lane]);
1636 }
1637 }
1638 assigned.iter().any(Option::is_some).then_some(assigned)
1639}
1640
1641pub(crate) type PendingExt11SupportUv = (
1642 ProceduralCurveId,
1643 Vec<Point3>,
1644 Vec<f64>,
1645 f64,
1646 [Option<Vec<[f64; 2]>>; 2],
1647);
1648
1649fn missing_support_parameter(value: f64) -> bool {
1650 value.to_bits() == MISSING_TOLERANCE.to_bits()
1651}
1652
1653fn pcurve_requires_completion(pcurve: Option<&PcurveGeometry>) -> bool {
1654 match pcurve {
1655 None => true,
1656 Some(PcurveGeometry::Nurbs { control_points, .. }) => control_points.iter().any(|point| {
1657 !point.u.is_finite()
1658 || !point.v.is_finite()
1659 || missing_support_parameter(point.u)
1660 || missing_support_parameter(point.v)
1661 }),
1662 Some(PcurveGeometry::Line { origin, direction }) => [origin, direction]
1663 .into_iter()
1664 .any(|point| !point.u.is_finite() || !point.v.is_finite()),
1665 Some(_) => false,
1666 }
1667}
1668
1669fn pcurve_control_point_seed(pcurve: Option<&PcurveGeometry>, index: usize) -> Option<Point2> {
1670 let PcurveGeometry::Nurbs { control_points, .. } = pcurve? else {
1671 return None;
1672 };
1673 control_points.get(index).copied().filter(|point| {
1674 point.u.is_finite()
1675 && point.v.is_finite()
1676 && !missing_support_parameter(point.u)
1677 && !missing_support_parameter(point.v)
1678 })
1679}
1680
1681pub(crate) fn complete_ext11_support_uv(ir: &mut CadIr, pending: &[PendingExt11SupportUv]) {
1682 for (procedural_id, points, parameters, fit_tolerance, lanes) in pending {
1683 let Some(procedural_index) = ir
1684 .model
1685 .procedural_curves
1686 .iter()
1687 .position(|procedural| &procedural.id == procedural_id)
1688 else {
1689 continue;
1690 };
1691 let (surfaces, missing) = match &ir.model.procedural_curves[procedural_index].definition {
1692 ProceduralCurveDefinition::Intersection { context, .. } => {
1693 let [Some(first), Some(second)] = &context.sides.clone().map(|side| side.surface)
1694 else {
1695 continue;
1696 };
1697 (
1698 [first.clone(), second.clone()],
1699 context
1700 .sides
1701 .each_ref()
1702 .map(|side| pcurve_requires_completion(side.pcurve.as_ref())),
1703 )
1704 }
1705 _ => continue,
1706 };
1707 if !missing.into_iter().any(|missing| missing) {
1708 continue;
1709 }
1710 let Some(assigned) = assign_ext11_support_uv_to_surfaces(
1711 ir,
1712 [&surfaces[0], &surfaces[1]],
1713 points,
1714 *fit_tolerance,
1715 lanes,
1716 ) else {
1717 continue;
1718 };
1719 let replacements: [Option<PcurveGeometry>; 2] = std::array::from_fn(|side| {
1720 if !missing[side] {
1721 return None;
1722 }
1723 let surface_geometry = ir
1724 .model
1725 .surfaces
1726 .iter()
1727 .find(|surface| surface.id == surfaces[side])
1728 .map(|surface| &surface.geometry)?;
1729 let values = assigned[side].as_ref()?;
1730 if values
1731 .iter()
1732 .flatten()
1733 .any(|value| !value.is_finite() || missing_support_parameter(*value))
1734 {
1735 return None;
1736 }
1737 let control_points = values
1738 .iter()
1739 .map(|uv| surface_parameters(surface_geometry, *uv))
1740 .collect::<Option<Vec<_>>>()?;
1741 Some(PcurveGeometry::Nurbs {
1742 degree: 1,
1743 knots: linear_knots(parameters),
1744 control_points,
1745 weights: None,
1746 periodic: false,
1747 })
1748 });
1749 let ProceduralCurveDefinition::Intersection { context, .. } =
1750 &mut ir.model.procedural_curves[procedural_index].definition
1751 else {
1752 unreachable!("definition checked above");
1753 };
1754 for (side, replacement) in replacements.into_iter().enumerate() {
1755 if let Some(replacement) = replacement {
1756 context.sides[side].pcurve = Some(replacement);
1757 }
1758 }
1759 }
1760}
1761
1762pub(crate) fn complete_support_uv(ir: &mut CadIr, pending: &[PendingExt11SupportUv]) {
1763 loop {
1764 let before = pending_support_lanes_requiring_completion(ir, pending);
1765 complete_support_uv_wave(ir, pending);
1766 let after = pending_support_lanes_requiring_completion(ir, pending);
1767 if after >= before {
1768 break;
1769 }
1770 }
1771}
1772
1773fn pending_support_lanes_requiring_completion(
1774 ir: &CadIr,
1775 pending: &[PendingExt11SupportUv],
1776) -> usize {
1777 pending
1778 .iter()
1779 .filter_map(|(procedural_id, ..)| {
1780 ir.model
1781 .procedural_curves
1782 .iter()
1783 .find(|procedural| &procedural.id == procedural_id)
1784 })
1785 .filter_map(|procedural| {
1786 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition
1787 else {
1788 return None;
1789 };
1790 Some(
1791 context
1792 .sides
1793 .iter()
1794 .filter(|side| pcurve_requires_completion(side.pcurve.as_ref()))
1795 .count(),
1796 )
1797 })
1798 .sum()
1799}
1800
1801fn complete_support_uv_wave(ir: &mut CadIr, pending: &[PendingExt11SupportUv]) {
1802 let mut replacements = Vec::new();
1803 let mut blend_parameter_grids = BTreeMap::<SurfaceId, Option<Vec<(Point2, Point3)>>>::new();
1804 for (procedural_id, points, parameters, fit_tolerance, _) in pending {
1805 let Some(procedural) = ir
1806 .model
1807 .procedural_curves
1808 .iter()
1809 .find(|procedural| &procedural.id == procedural_id)
1810 else {
1811 continue;
1812 };
1813 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition else {
1814 continue;
1815 };
1816 for side in 0..2 {
1817 if !pcurve_requires_completion(context.sides[side].pcurve.as_ref()) {
1818 continue;
1819 }
1820 let Some(surface_id) = &context.sides[side].surface else {
1821 continue;
1822 };
1823 let Some(surface) = ir
1824 .model
1825 .surfaces
1826 .iter()
1827 .find(|surface| &surface.id == surface_id)
1828 else {
1829 continue;
1830 };
1831 let effective_fit_tolerance =
1832 blend_spine_cache_fit_tolerance(ir, surface_id, *fit_tolerance);
1833 let mut uv = Vec::with_capacity(points.len());
1834 for (point_index, point) in points.iter().enumerate() {
1835 let seed =
1836 pcurve_control_point_seed(context.sides[side].pcurve.as_ref(), point_index)
1837 .or_else(|| uv.last().copied());
1838 let parameters = match &surface.geometry {
1839 SurfaceGeometry::Nurbs(nurbs) => nurbs_parameters(nurbs, *point, seed),
1840 SurfaceGeometry::Procedural { .. } => {
1841 let other_side = &context.sides[1 - side];
1842 other_side
1843 .surface
1844 .as_ref()
1845 .zip(other_side.pcurve.as_ref())
1846 .and_then(|(other_surface, other_pcurve)| {
1847 blend_boundary_parameter_from_support_pcurve(
1848 ir,
1849 surface_id,
1850 other_surface,
1851 other_pcurve,
1852 parameters[point_index],
1853 *point,
1854 effective_fit_tolerance,
1855 )
1856 })
1857 .or_else(|| {
1858 offset_surface_parameters_with_tolerance(
1859 ir,
1860 surface_id,
1861 *point,
1862 seed,
1863 Some(effective_fit_tolerance),
1864 )
1865 })
1866 .or_else(|| {
1867 blend_surface_parameters_for_fit_with_grid(
1868 ir,
1869 surface_id,
1870 *point,
1871 seed,
1872 effective_fit_tolerance,
1873 BlendParameterGrid::Disabled,
1874 )
1875 })
1876 .or_else(|| {
1877 let blend_grid = blend_parameter_grids
1878 .entry(surface_id.clone())
1879 .or_insert_with(|| {
1880 blend_surface_parameter_grid(ir, surface_id, 0)
1881 });
1882 blend_surface_parameters_from_grid_for_fit(
1883 ir,
1884 surface_id,
1885 *point,
1886 effective_fit_tolerance,
1887 blend_grid.as_deref()?,
1888 )
1889 })
1890 }
1891 geometry => analytic_surface_parameters(geometry, *point),
1892 };
1893 let Some(parameters) = parameters else {
1894 uv.clear();
1895 break;
1896 };
1897 uv.push(parameters);
1898 }
1899 if uv.len() != points.len() {
1900 continue;
1901 }
1902 if matches!(
1903 surface.geometry,
1904 SurfaceGeometry::Cylinder { .. }
1905 | SurfaceGeometry::Cone { .. }
1906 | SurfaceGeometry::Sphere { .. }
1907 | SurfaceGeometry::Torus { .. }
1908 ) {
1909 for index in 1..uv.len() {
1910 let turns = ((uv[index - 1].u - uv[index].u) / std::f64::consts::TAU).round();
1911 uv[index].u += turns * std::f64::consts::TAU;
1912 }
1913 }
1914 let reproduces_chart = uv.iter().zip(points).all(|(uv, point)| {
1915 decoded_surface_point(ir, surface_id, uv.u, uv.v)
1916 .is_some_and(|actual| point_distance(actual, *point) <= effective_fit_tolerance)
1917 });
1918 if reproduces_chart {
1919 replacements.push((
1920 procedural_id.clone(),
1921 side,
1922 PcurveGeometry::Nurbs {
1923 degree: 1,
1924 knots: linear_knots(parameters),
1925 control_points: uv,
1926 weights: None,
1927 periodic: false,
1928 },
1929 effective_fit_tolerance,
1930 ));
1931 }
1932 }
1933 }
1934 for (procedural_id, side, pcurve, effective_fit_tolerance) in replacements {
1935 let Some(procedural) = ir
1936 .model
1937 .procedural_curves
1938 .iter_mut()
1939 .find(|procedural| procedural.id == procedural_id)
1940 else {
1941 continue;
1942 };
1943 let ProceduralCurveDefinition::Intersection { context, .. } = &mut procedural.definition
1944 else {
1945 continue;
1946 };
1947 if pcurve_requires_completion(context.sides[side].pcurve.as_ref()) {
1948 context.sides[side].pcurve = Some(pcurve);
1949 procedural.cache_fit_tolerance = Some(
1950 procedural
1951 .cache_fit_tolerance
1952 .unwrap_or(0.0)
1953 .max(effective_fit_tolerance),
1954 );
1955 }
1956 }
1957 complete_coupled_support_uv(ir, pending);
1958}
1959
1960pub(crate) fn blend_spine_cache_fit_tolerance(
1961 ir: &CadIr,
1962 surface: &SurfaceId,
1963 fit_tolerance: f64,
1964) -> f64 {
1965 blend_surface_definition(ir, surface)
1966 .and_then(|(_, spine, _, _)| {
1967 ir.model
1968 .procedural_curves
1969 .iter()
1970 .find(|procedural| procedural.curve == spine)
1971 .and_then(|procedural| procedural.cache_fit_tolerance)
1972 })
1973 .filter(|tolerance| tolerance.is_finite() && *tolerance > 0.0)
1974 .map_or(fit_tolerance, |tolerance| fit_tolerance + tolerance)
1975}
1976
1977fn complete_coupled_support_uv(ir: &mut CadIr, pending: &[PendingExt11SupportUv]) {
1978 let mut replacements = Vec::new();
1979 for (procedural_id, points, parameters, fit_tolerance, _) in pending {
1980 let Some(procedural) = ir
1981 .model
1982 .procedural_curves
1983 .iter()
1984 .find(|procedural| &procedural.id == procedural_id)
1985 else {
1986 continue;
1987 };
1988 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition else {
1989 continue;
1990 };
1991 let missing = context
1992 .sides
1993 .each_ref()
1994 .map(|side| pcurve_requires_completion(side.pcurve.as_ref()));
1995 let [Some(first_surface), Some(second_surface)] =
1996 context.sides.each_ref().map(|side| side.surface.as_ref())
1997 else {
1998 continue;
1999 };
2000 let surfaces = [first_surface, second_surface];
2001 let unresolved_procedural_support = (0..2).any(|side| {
2002 missing[side]
2003 && pcurve_control_point_seed(context.sides[side].pcurve.as_ref(), 0).is_some()
2004 && ir.model.surfaces.iter().any(|surface| {
2005 &surface.id == surfaces[side]
2006 && matches!(surface.geometry, SurfaceGeometry::Procedural { .. })
2007 })
2008 });
2009 if !unresolved_procedural_support {
2010 continue;
2011 }
2012 let seeds = context
2013 .sides
2014 .each_ref()
2015 .map(|side| pcurve_control_point_seed(side.pcurve.as_ref(), 0));
2016 let Some(lanes) = continue_surface_intersection_parameters_with_seeds(
2017 ir,
2018 surfaces,
2019 points,
2020 *fit_tolerance,
2021 seeds,
2022 ) else {
2023 continue;
2024 };
2025 for side in 0..2 {
2026 if missing[side] {
2027 replacements.push((
2028 procedural_id.clone(),
2029 side,
2030 PcurveGeometry::Nurbs {
2031 degree: 1,
2032 knots: linear_knots(parameters),
2033 control_points: lanes[side].clone(),
2034 weights: None,
2035 periodic: false,
2036 },
2037 ));
2038 }
2039 }
2040 }
2041 for (procedural_id, side, pcurve) in replacements {
2042 let Some(procedural) = ir
2043 .model
2044 .procedural_curves
2045 .iter_mut()
2046 .find(|procedural| procedural.id == procedural_id)
2047 else {
2048 continue;
2049 };
2050 let ProceduralCurveDefinition::Intersection { context, .. } = &mut procedural.definition
2051 else {
2052 continue;
2053 };
2054 if pcurve_requires_completion(context.sides[side].pcurve.as_ref()) {
2055 context.sides[side].pcurve = Some(pcurve);
2056 }
2057 }
2058}
2059
2060pub(crate) fn complete_parameterization_equivalent_support_uv(ir: &mut CadIr) {
2061 let replacements = ir
2062 .model
2063 .procedural_curves
2064 .iter()
2065 .enumerate()
2066 .filter_map(|(procedural_index, procedural)| {
2067 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition
2068 else {
2069 return None;
2070 };
2071 let missing = context
2072 .sides
2073 .each_ref()
2074 .map(|side| pcurve_requires_completion(side.pcurve.as_ref()));
2075 let target = match missing {
2076 [true, false] => 0,
2077 [false, true] => 1,
2078 _ => return None,
2079 };
2080 let source = 1 - target;
2081 let (Some(target_surface), Some(source_surface), Some(source_pcurve)) = (
2082 context.sides[target].surface.as_ref(),
2083 context.sides[source].surface.as_ref(),
2084 context.sides[source].pcurve.as_ref(),
2085 ) else {
2086 return None;
2087 };
2088 parameterization_equivalent_surfaces(ir, target_surface, source_surface)
2089 .then(|| (procedural_index, target, source_pcurve.clone()))
2090 })
2091 .collect::<Vec<_>>();
2092 for (procedural_index, side, pcurve) in replacements {
2093 let ProceduralCurveDefinition::Intersection { context, .. } =
2094 &mut ir.model.procedural_curves[procedural_index].definition
2095 else {
2096 unreachable!("definition selected above");
2097 };
2098 if pcurve_requires_completion(context.sides[side].pcurve.as_ref()) {
2099 context.sides[side].pcurve = Some(pcurve);
2100 }
2101 }
2102}
2103
2104pub(crate) fn parameterization_equivalent_surfaces(
2105 ir: &CadIr,
2106 first: &SurfaceId,
2107 second: &SurfaceId,
2108) -> bool {
2109 fn equivalent(
2110 ir: &CadIr,
2111 first: &SurfaceId,
2112 second: &SurfaceId,
2113 visited: &mut BTreeSet<(SurfaceId, SurfaceId)>,
2114 ) -> bool {
2115 if first == second {
2116 return true;
2117 }
2118 if !visited.insert((first.clone(), second.clone())) {
2119 return false;
2120 }
2121 let geometry = |id: &SurfaceId| {
2122 ir.model
2123 .surfaces
2124 .iter()
2125 .find(|surface| &surface.id == id)
2126 .map(|surface| &surface.geometry)
2127 };
2128 let (Some(first_geometry), Some(second_geometry)) = (geometry(first), geometry(second))
2129 else {
2130 return false;
2131 };
2132 if first_geometry == second_geometry {
2133 return true;
2134 }
2135 let construction = |geometry: &SurfaceGeometry| {
2136 let SurfaceGeometry::Procedural { construction } = geometry else {
2137 return None;
2138 };
2139 ir.model
2140 .procedural_surfaces
2141 .iter()
2142 .find(|procedural| &procedural.id == construction)
2143 .map(|procedural| &procedural.definition)
2144 };
2145 let (
2146 Some(ProceduralSurfaceDefinition::Offset {
2147 support: first_support,
2148 distance: first_distance,
2149 u_sense: first_u_sense,
2150 v_sense: first_v_sense,
2151 extension_flags: first_extensions,
2152 ..
2153 }),
2154 Some(ProceduralSurfaceDefinition::Offset {
2155 support: second_support,
2156 distance: second_distance,
2157 u_sense: second_u_sense,
2158 v_sense: second_v_sense,
2159 extension_flags: second_extensions,
2160 ..
2161 }),
2162 ) = (construction(first_geometry), construction(second_geometry))
2163 else {
2164 return false;
2165 };
2166 first_distance.to_bits() == second_distance.to_bits()
2167 && first_u_sense == second_u_sense
2168 && first_v_sense == second_v_sense
2169 && first_extensions == second_extensions
2170 && equivalent(ir, first_support, second_support, visited)
2171 }
2172
2173 equivalent(ir, first, second, &mut BTreeSet::new())
2174}
2175
2176pub(crate) fn attach_completed_intersection_pcurves(
2177 ir: &mut CadIr,
2178 graph: &Graph,
2179 prefix: &str,
2180 source_stream: cadmpeg_ir::annotations::StreamHandle,
2181 annotations: &mut AnnotationBuilder,
2182) {
2183 let loop_faces = ir
2184 .model
2185 .loops
2186 .iter()
2187 .map(|loop_| (&loop_.id, &loop_.face))
2188 .collect::<BTreeMap<_, _>>();
2189 let face_surfaces = ir
2190 .model
2191 .faces
2192 .iter()
2193 .map(|face| (&face.id, &face.surface))
2194 .collect::<BTreeMap<_, _>>();
2195 let edge_curves = ir
2196 .model
2197 .edges
2198 .iter()
2199 .filter_map(|edge| Some((&edge.id, edge.curve.as_ref()?)))
2200 .collect::<BTreeMap<_, _>>();
2201 let mut candidates =
2202 BTreeMap::<(CurveId, SurfaceId), Vec<(PcurveGeometry, [f64; 2], Option<f64>)>>::new();
2203 for procedural in &ir.model.procedural_curves {
2204 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition else {
2205 continue;
2206 };
2207 for side in &context.sides {
2208 let (Some(surface), Some(pcurve)) = (&side.surface, &side.pcurve) else {
2209 continue;
2210 };
2211 let values = candidates
2212 .entry((procedural.curve.clone(), surface.clone()))
2213 .or_default();
2214 let candidate = (
2215 pcurve.clone(),
2216 context.parameter_range,
2217 procedural.cache_fit_tolerance,
2218 );
2219 if !values.contains(&candidate) {
2220 values.push(candidate);
2221 }
2222 }
2223 }
2224
2225 let replacements = ir
2226 .model
2227 .coedges
2228 .iter()
2229 .filter(|coedge| coedge.pcurves.is_empty() && coedge.id.0.starts_with(prefix))
2230 .filter_map(|coedge| {
2231 let surface = loop_faces
2232 .get(&coedge.owner_loop)
2233 .and_then(|face| face_surfaces.get(*face))?;
2234 let curve = edge_curves.get(&coedge.edge)?;
2235 let [candidate] = candidates
2236 .get(&((*curve).clone(), (*surface).clone()))?
2237 .as_slice()
2238 else {
2239 return None;
2240 };
2241 pcurve_matches_edge(ir, &coedge.edge, surface, &candidate.0, candidate.2)
2242 .then(|| (coedge.id.clone(), candidate.clone()))
2243 })
2244 .collect::<Vec<_>>();
2245 for (coedge_id, (geometry, parameter_range, fit_tolerance)) in replacements {
2246 let Some(fin_xmt) = coedge_id
2247 .0
2248 .rsplit_once('#')
2249 .and_then(|(_, value)| value.parse::<u32>().ok())
2250 else {
2251 continue;
2252 };
2253 let pcurve_id = PcurveId(format!("{prefix}:intersection-pcurve-completed#{fin_xmt}"));
2254 if ir.model.pcurves.iter().any(|pcurve| pcurve.id == pcurve_id) {
2255 continue;
2256 }
2257 let source_offset = graph.get(17, fin_xmt).map_or(0, |node| node.pos as u64);
2258 annotations
2259 .note(&pcurve_id, source_stream, source_offset)
2260 .tag("INTERSECTION_PCURVE");
2261 annotations.derived(&pcurve_id, "geometry");
2262 annotations.derived(&pcurve_id, "parameter_range");
2263 if fit_tolerance.is_some() {
2264 annotations.derived(&pcurve_id, "fit_tolerance");
2265 }
2266 ir.model.pcurves.push(Pcurve {
2267 id: pcurve_id.clone(),
2268 geometry,
2269 wrapper_reversed: None,
2270 native_tail_flags: None,
2271 parameter_range: Some(parameter_range),
2272 fit_tolerance,
2273 });
2274 if let Some(coedge) = ir
2275 .model
2276 .coedges
2277 .iter_mut()
2278 .find(|coedge| coedge.id == coedge_id && coedge.pcurves.is_empty())
2279 {
2280 coedge.pcurves.push(cadmpeg_ir::topology::PcurveUse {
2281 pcurve: pcurve_id,
2282 isoparametric: None,
2283 parameter_range: None,
2284 });
2285 }
2286 }
2287}
2288
2289fn decoded_surface_point(ir: &CadIr, surface: &SurfaceId, u: f64, v: f64) -> Option<Point3> {
2290 decoded_surface_point_inner(ir, surface, u, v, 0)
2291}
2292
2293fn decoded_surface_point_inner(
2294 ir: &CadIr,
2295 surface: &SurfaceId,
2296 u: f64,
2297 v: f64,
2298 depth: usize,
2299) -> Option<Point3> {
2300 (depth < 32).then_some(())?;
2301 model_surface_point_by_id(ir, surface, u, v)
2302 .or_else(|| blend_surface_point_inner(ir, surface, u, v, depth + 1))
2303}
2304
2305#[cfg(test)]
2306pub(crate) fn blend_surface_parameters(
2307 ir: &CadIr,
2308 surface: &SurfaceId,
2309 point: Point3,
2310 seed: Option<Point2>,
2311) -> Option<Point2> {
2312 blend_surface_parameters_inner(ir, surface, point, seed, None, BlendParameterGrid::Build, 0)
2313}
2314
2315pub(crate) fn blend_surface_parameters_for_fit(
2316 ir: &CadIr,
2317 surface: &SurfaceId,
2318 point: Point3,
2319 seed: Option<Point2>,
2320 fit_tolerance: f64,
2321) -> Option<Point2> {
2322 blend_surface_parameters_for_fit_with_grid(
2323 ir,
2324 surface,
2325 point,
2326 seed,
2327 fit_tolerance,
2328 BlendParameterGrid::Build,
2329 )
2330}
2331
2332#[derive(Clone, Copy)]
2333enum BlendParameterGrid {
2334 Build,
2335 Disabled,
2336}
2337
2338fn blend_surface_parameters_for_fit_with_grid(
2339 ir: &CadIr,
2340 surface: &SurfaceId,
2341 point: Point3,
2342 seed: Option<Point2>,
2343 fit_tolerance: f64,
2344 grid: BlendParameterGrid,
2345) -> Option<Point2> {
2346 blend_surface_parameters_inner(ir, surface, point, seed, Some(fit_tolerance), grid, 0)
2347}
2348
2349fn blend_surface_parameters_inner(
2350 ir: &CadIr,
2351 surface: &SurfaceId,
2352 point: Point3,
2353 seed: Option<Point2>,
2354 fit_tolerance: Option<f64>,
2355 grid: BlendParameterGrid,
2356 depth: usize,
2357) -> Option<Point2> {
2358 (depth < 32).then_some(())?;
2359 let (_, spine, _, _) = blend_surface_definition(ir, surface)?;
2360 if let (Some(seed), Some(fit_tolerance)) = (seed, fit_tolerance) {
2361 if let Some(parameters) =
2362 refine_blend_surface_parameters(ir, surface, point, seed, depth + 1).filter(
2363 |parameters| {
2364 blend_surface_point_inner(ir, surface, parameters.u, parameters.v, depth + 1)
2365 .is_some_and(|candidate| point_distance(candidate, point) <= fit_tolerance)
2366 },
2367 )
2368 {
2369 return Some(parameters);
2370 }
2371 }
2372 if let Some(fit_tolerance) = fit_tolerance {
2373 let boundary_parameters = [0usize, 1usize].map(|boundary| {
2374 blend_boundary_parameter(ir, surface, point, boundary, depth + 1).filter(|parameter| {
2375 blend_boundary_point(ir, surface, *parameter, boundary, depth + 1)
2376 .is_some_and(|candidate| point_distance(candidate, point) <= fit_tolerance)
2377 })
2378 });
2379 if let Some((parameter, boundary)) = match boundary_parameters {
2380 [Some(parameter), None] => Some((parameter, 0usize)),
2381 [None, Some(parameter)] => Some((parameter, 1usize)),
2382 _ => None,
2383 } {
2384 return Some(Point2::new(parameter, boundary as f64));
2385 }
2386 }
2387 let angular =
2388 closest_spine_parameter(ir, &spine, point, seed.map(|seed| seed.u)).and_then(|u| {
2389 let (center, tangent, first, second, _) =
2390 blend_surface_frame(ir, surface, u, depth + 1)?;
2391 let radial = unit_vector(Vector3::new(
2392 point.x - center.x,
2393 point.y - center.y,
2394 point.z - center.z,
2395 ))?;
2396 let alpha = signed_angle(first, second, tangent);
2397 if !alpha.is_finite() || alpha.abs() <= 1.0e-12 {
2398 return None;
2399 }
2400 let theta = signed_angle(first, radial, tangent);
2401 (-2..=2)
2402 .filter_map(|turn| {
2403 let v = (theta + f64::from(turn) * std::f64::consts::TAU) / alpha;
2404 let candidate = blend_surface_point_inner(ir, surface, u, v, depth + 1)?;
2405 let branch_distance = seed.map_or(v.abs(), |seed| (v - seed.v).abs());
2406 Some((
2407 Point2::new(u, v),
2408 point_distance(candidate, point),
2409 branch_distance,
2410 ))
2411 })
2412 .min_by(|first, second| {
2413 if (first.1 - second.1).abs() <= 1.0e-12 {
2414 first.2.total_cmp(&second.2)
2415 } else {
2416 first.1.total_cmp(&second.1)
2417 }
2418 })
2419 .map(|(parameters, _, _)| parameters)
2420 });
2421 if let Some(initial) = angular {
2422 let parameters = refine_blend_surface_parameters(ir, surface, point, initial, depth + 1)
2423 .unwrap_or(initial);
2424 if let Some(candidate) =
2425 blend_surface_point_inner(ir, surface, parameters.u, parameters.v, depth + 1)
2426 {
2427 let distance = point_distance(candidate, point);
2428 if fit_tolerance.is_none_or(|tolerance| distance <= tolerance) {
2429 return Some(parameters);
2430 }
2431 }
2432 }
2433 let initial = match grid {
2434 BlendParameterGrid::Build => coarse_blend_surface_parameters(ir, surface, point, depth + 1),
2435 BlendParameterGrid::Disabled => None,
2436 }?;
2437 let parameters =
2438 refine_blend_surface_parameters(ir, surface, point, initial, depth + 1).unwrap_or(initial);
2439 if !(0.0..=1.0).contains(¶meters.v) {
2440 return None;
2441 }
2442 let candidate = blend_surface_point_inner(ir, surface, parameters.u, parameters.v, depth + 1)?;
2443 let distance = point_distance(candidate, point);
2444 fit_tolerance
2445 .is_none_or(|tolerance| distance <= tolerance)
2446 .then_some(parameters)
2447}
2448
2449pub(crate) fn coarse_blend_surface_parameters(
2450 ir: &CadIr,
2451 surface: &SurfaceId,
2452 point: Point3,
2453 depth: usize,
2454) -> Option<Point2> {
2455 let grid = blend_surface_parameter_grid(ir, surface, depth)?;
2456 closest_blend_surface_grid_parameters(&grid, point)
2457}
2458
2459fn blend_surface_parameter_grid(
2460 ir: &CadIr,
2461 surface: &SurfaceId,
2462 depth: usize,
2463) -> Option<Vec<(Point2, Point3)>> {
2464 (depth < 32).then_some(())?;
2465 let (_, spine, _, _) = blend_surface_definition(ir, surface)?;
2466 let curve = ir.model.curves.iter().find(|curve| curve.id == spine)?;
2467 let CurveGeometry::Nurbs(nurbs) = &curve.geometry else {
2468 return None;
2469 };
2470 let degree = usize::try_from(nurbs.degree).ok()?;
2471 let count = nurbs.control_points.len();
2472 let domain = [*nurbs.knots.get(degree)?, *nurbs.knots.get(count)?];
2473 if !domain.into_iter().all(f64::is_finite) || domain[0] >= domain[1] {
2474 return None;
2475 }
2476 let mut grid = Vec::with_capacity(9 * 5);
2477 for u_index in 0..=8 {
2478 let u = domain[0] + (domain[1] - domain[0]) * f64::from(u_index) / 8.0;
2479 let frame = blend_surface_frame(ir, surface, u, depth + 1);
2480 for v_index in 0..=4 {
2481 let parameters = Point2::new(u, f64::from(v_index) / 4.0);
2482 let point = match v_index {
2483 0 => blend_boundary_point(ir, surface, u, 0, depth + 1),
2484 4 => blend_boundary_point(ir, surface, u, 1, depth + 1),
2485 _ => frame.map(|frame| blend_surface_point_from_frame(frame, parameters.v)),
2486 };
2487 let Some(point) = point else {
2488 continue;
2489 };
2490 grid.push((parameters, point));
2491 }
2492 }
2493 (!grid.is_empty()).then_some(grid)
2494}
2495
2496fn closest_blend_surface_grid_parameters(
2497 grid: &[(Point2, Point3)],
2498 point: Point3,
2499) -> Option<Point2> {
2500 grid.iter()
2501 .min_by(|(_, first), (_, second)| {
2502 point_distance(*first, point).total_cmp(&point_distance(*second, point))
2503 })
2504 .map(|(parameters, _)| *parameters)
2505}
2506
2507fn blend_surface_parameters_from_grid_for_fit(
2508 ir: &CadIr,
2509 surface: &SurfaceId,
2510 point: Point3,
2511 fit_tolerance: f64,
2512 grid: &[(Point2, Point3)],
2513) -> Option<Point2> {
2514 let initial = closest_blend_surface_grid_parameters(grid, point)?;
2515 let parameters =
2516 refine_blend_surface_parameters(ir, surface, point, initial, 0).unwrap_or(initial);
2517 (0.0..=1.0).contains(¶meters.v).then_some(())?;
2518 let candidate = blend_surface_point_inner(ir, surface, parameters.u, parameters.v, 0)?;
2519 (point_distance(candidate, point) <= fit_tolerance).then_some(parameters)
2520}
2521
2522pub(crate) fn refine_blend_surface_parameters(
2523 ir: &CadIr,
2524 surface: &SurfaceId,
2525 point: Point3,
2526 mut parameters: Point2,
2527 depth: usize,
2528) -> Option<Point2> {
2529 (depth < 32).then_some(())?;
2530 let (_, spine, _, _) = blend_surface_definition(ir, surface)?;
2531 let u_domain = ir
2532 .model
2533 .curves
2534 .iter()
2535 .find(|curve| curve.id == spine)
2536 .and_then(|curve| match &curve.geometry {
2537 CurveGeometry::Nurbs(nurbs) => {
2538 let degree = usize::try_from(nurbs.degree).ok()?;
2539 let count = nurbs.control_points.len();
2540 Some([*nurbs.knots.get(degree)?, *nurbs.knots.get(count)?])
2541 }
2542 _ => None,
2543 });
2544 if let Some(domain) = u_domain {
2545 parameters.u = parameters.u.clamp(domain[0], domain[1]);
2546 }
2547 let squared_distance = |candidate: Point3| {
2548 (candidate.x - point.x).powi(2)
2549 + (candidate.y - point.y).powi(2)
2550 + (candidate.z - point.z).powi(2)
2551 };
2552 for _ in 0..16 {
2553 let position =
2554 blend_surface_point_inner(ir, surface, parameters.u, parameters.v, depth + 1)?;
2555 let residual = Vector3::new(
2556 position.x - point.x,
2557 position.y - point.y,
2558 position.z - point.z,
2559 );
2560 let current_distance = squared_distance(position);
2561 let u_step = parameter_derivative_step(parameters.u, u_domain);
2562 let v_step = parameter_derivative_step(parameters.v, None);
2563 let derivative = |along_u: bool, step: f64| {
2564 let mut before = parameters;
2565 let mut after = parameters;
2566 if along_u {
2567 before.u -= step;
2568 after.u += step;
2569 if let Some(domain) = u_domain {
2570 before.u = before.u.clamp(domain[0], domain[1]);
2571 after.u = after.u.clamp(domain[0], domain[1]);
2572 }
2573 } else {
2574 before.v -= step;
2575 after.v += step;
2576 }
2577 let width = if along_u {
2578 after.u - before.u
2579 } else {
2580 after.v - before.v
2581 };
2582 if !width.is_finite() || width == 0.0 {
2583 return None;
2584 }
2585 let first = blend_surface_point_inner(ir, surface, before.u, before.v, depth + 1)?;
2586 let second = blend_surface_point_inner(ir, surface, after.u, after.v, depth + 1)?;
2587 Some(Vector3::new(
2588 (second.x - first.x) / width,
2589 (second.y - first.y) / width,
2590 (second.z - first.z) / width,
2591 ))
2592 };
2593 let du = derivative(true, u_step)?;
2594 let dv = derivative(false, v_step)?;
2595 let Some((step_u, step_v)) = least_squares_step(du, dv, residual) else {
2596 break;
2597 };
2598 let mut scale = 1.0;
2599 let mut accepted = None;
2600 for _ in 0..8 {
2601 let mut candidate =
2602 Point2::new(parameters.u - scale * step_u, parameters.v - scale * step_v);
2603 if let Some(domain) = u_domain {
2604 candidate.u = candidate.u.clamp(domain[0], domain[1]);
2605 }
2606 if let Some(position) =
2607 blend_surface_point_inner(ir, surface, candidate.u, candidate.v, depth + 1)
2608 {
2609 if squared_distance(position) < current_distance {
2610 accepted = Some(candidate);
2611 break;
2612 }
2613 }
2614 scale *= 0.5;
2615 }
2616 let Some(candidate) = accepted else {
2617 break;
2618 };
2619 let converged = (candidate.u - parameters.u).abs() <= 1.0e-12 * (1.0 + parameters.u.abs())
2620 && (candidate.v - parameters.v).abs() <= 1.0e-12 * (1.0 + parameters.v.abs());
2621 parameters = candidate;
2622 if converged {
2623 break;
2624 }
2625 }
2626 Some(parameters)
2627}
2628
2629#[cfg(test)]
2630pub(crate) fn blend_surface_point(
2631 ir: &CadIr,
2632 surface: &SurfaceId,
2633 u: f64,
2634 v: f64,
2635) -> Option<Point3> {
2636 blend_surface_point_inner(ir, surface, u, v, 0)
2637}
2638
2639fn blend_surface_point_inner(
2640 ir: &CadIr,
2641 surface: &SurfaceId,
2642 u: f64,
2643 v: f64,
2644 depth: usize,
2645) -> Option<Point3> {
2646 (depth < 32).then_some(())?;
2647 if v.to_bits() == 0.0f64.to_bits() {
2648 return blend_boundary_point(ir, surface, u, 0, depth + 1);
2649 }
2650 if v.to_bits() == 1.0f64.to_bits() {
2651 return blend_boundary_point(ir, surface, u, 1, depth + 1);
2652 }
2653 let frame = blend_surface_frame(ir, surface, u, depth + 1)?;
2654 Some(blend_surface_point_from_frame(frame, v))
2655}
2656
2657type BlendSurfaceFrame = (Point3, Vector3, Vector3, Vector3, f64);
2658
2659fn blend_surface_point_from_frame(
2660 (center, tangent, first, second, radius): BlendSurfaceFrame,
2661 v: f64,
2662) -> Point3 {
2663 let alpha = signed_angle(first, second, tangent);
2664 let radial = rodrigues_rotate(first, tangent, v * alpha);
2665 Point3::new(
2666 center.x + radius * radial.x,
2667 center.y + radius * radial.y,
2668 center.z + radius * radial.z,
2669 )
2670}
2671
2672fn blend_surface_frame(
2673 ir: &CadIr,
2674 surface: &SurfaceId,
2675 u: f64,
2676 depth: usize,
2677) -> Option<BlendSurfaceFrame> {
2678 (depth < 32).then_some(())?;
2679 let (supports, spine, radius, _) = blend_surface_definition(ir, surface)?;
2680 let center = model_curve_point(ir, &spine, u)?;
2681 let tangent = model_curve_tangent(ir, &spine, u)?;
2682 let first = spine_contact_direction(ir, &supports[0], &spine, u, center, radius, depth + 1)
2683 .or_else(|| surface_contact_direction(ir, &supports[0], center, depth + 1))?;
2684 let second = spine_contact_direction(ir, &supports[1], &spine, u, center, radius, depth + 1)
2685 .or_else(|| surface_contact_direction(ir, &supports[1], center, depth + 1))?;
2686 Some((center, tangent, first, second, radius))
2687}
2688
2689fn spine_contact_direction(
2690 ir: &CadIr,
2691 support: &SurfaceId,
2692 spine: &CurveId,
2693 parameter: f64,
2694 center: Point3,
2695 radius: f64,
2696 depth: usize,
2697) -> Option<Vector3> {
2698 let contact = spine_contact_point(ir, support, spine, parameter, radius, depth + 1)?;
2699 unit_vector(Vector3::new(
2700 contact.x - center.x,
2701 contact.y - center.y,
2702 contact.z - center.z,
2703 ))
2704}
2705
2706fn blend_boundary_point(
2707 ir: &CadIr,
2708 surface: &SurfaceId,
2709 parameter: f64,
2710 boundary: usize,
2711 depth: usize,
2712) -> Option<Point3> {
2713 (depth < 32).then_some(())?;
2714 let (supports, spine, radius, _) = blend_surface_definition(ir, surface)?;
2715 spine_contact_point(
2716 ir,
2717 supports.get(boundary)?,
2718 &spine,
2719 parameter,
2720 radius,
2721 depth + 1,
2722 )
2723}
2724
2725fn blend_boundary_parameter(
2726 ir: &CadIr,
2727 surface: &SurfaceId,
2728 point: Point3,
2729 boundary: usize,
2730 depth: usize,
2731) -> Option<f64> {
2732 (depth < 32).then_some(())?;
2733 let (supports, spine, radius, _) = blend_surface_definition(ir, surface)?;
2734 let support = supports.get(boundary)?;
2735 let carrier = ir
2736 .model
2737 .surfaces
2738 .iter()
2739 .find(|candidate| &candidate.id == support)?;
2740 let uv = match &carrier.geometry {
2741 SurfaceGeometry::Nurbs(nurbs) => nurbs_parameters(nurbs, point, None),
2742 SurfaceGeometry::Procedural { .. } => offset_surface_parameters(ir, support, point, None),
2743 geometry => analytic_surface_parameters(geometry, point),
2744 }?;
2745 let pcurve = spine_contact_pcurve(ir, support, &spine, radius, depth + 1)?;
2746 closest_pcurve_parameter(pcurve, uv)
2747}
2748
2749fn blend_boundary_parameter_from_support_pcurve(
2750 ir: &CadIr,
2751 blend: &SurfaceId,
2752 support: &SurfaceId,
2753 support_pcurve: &PcurveGeometry,
2754 curve_parameter: f64,
2755 point: Point3,
2756 fit_tolerance: f64,
2757) -> Option<Point2> {
2758 let (supports, spine, radius, _) = blend_surface_definition(ir, blend)?;
2759 let boundary = supports
2760 .iter()
2761 .position(|candidate| parameterization_equivalent_surfaces(ir, candidate, support))?;
2762 if supports
2763 .iter()
2764 .filter(|candidate| parameterization_equivalent_surfaces(ir, candidate, support))
2765 .count()
2766 != 1
2767 {
2768 return None;
2769 }
2770 let support_uv = pcurve_uv(support_pcurve, curve_parameter)?;
2771 let contact_pcurve = spine_contact_pcurve(ir, support, &spine, radius, 0)?;
2772 let parameter = closest_pcurve_parameter(contact_pcurve, support_uv)?;
2773 blend_boundary_point(ir, blend, parameter, boundary, 0)
2774 .filter(|candidate| point_distance(*candidate, point) <= fit_tolerance)
2775 .map(|_| Point2::new(parameter, boundary as f64))
2776}
2777
2778pub(crate) fn closest_pcurve_parameter(pcurve: &PcurveGeometry, point: Point2) -> Option<f64> {
2779 let PcurveGeometry::Nurbs {
2780 degree,
2781 knots,
2782 control_points,
2783 weights,
2784 ..
2785 } = pcurve
2786 else {
2787 return None;
2788 };
2789 let degree = usize::try_from(*degree).ok()?;
2790 let count = control_points.len();
2791 if count <= degree || knots.len() != count.checked_add(degree)?.checked_add(1)? {
2792 return None;
2793 }
2794 let domain = [*knots.get(degree)?, *knots.get(count)?];
2795 if !domain[0].is_finite() || !domain[1].is_finite() || domain[0] >= domain[1] {
2796 return None;
2797 }
2798 if degree != 1 || weights.is_some() {
2799 let squared_distance = |parameter| {
2800 let position = pcurve_uv(pcurve, parameter)?;
2801 Some((position.u - point.u).powi(2) + (position.v - point.v).powi(2))
2802 };
2803 let samples = knot_domain_samples(knots, degree, domain);
2804 let distances = samples
2805 .iter()
2806 .map(|parameter| squared_distance(*parameter))
2807 .collect::<Option<Vec<_>>>()?;
2808 let mut best = samples[0];
2809 let mut best_distance = distances[0];
2810 for (index, &distance) in distances.iter().enumerate() {
2811 if distance < best_distance {
2812 best = samples[index];
2813 best_distance = distance;
2814 }
2815 if index > 0
2816 && index + 1 < samples.len()
2817 && distance <= distances[index - 1]
2818 && distance <= distances[index + 1]
2819 {
2820 let (parameter, distance) = golden_section_minimum(
2821 samples[index - 1],
2822 samples[index + 1],
2823 &squared_distance,
2824 )?;
2825 if distance < best_distance {
2826 best = parameter;
2827 best_distance = distance;
2828 }
2829 }
2830 }
2831 return Some(best);
2832 }
2833 let mut candidates = control_points
2834 .windows(2)
2835 .enumerate()
2836 .filter_map(|(index, segment)| {
2837 let start = segment[0];
2838 let end = segment[1];
2839 let direction = Point2::new(end.u - start.u, end.v - start.v);
2840 let squared_length = direction.u * direction.u + direction.v * direction.v;
2841 if !squared_length.is_finite() || squared_length == 0.0 {
2842 return None;
2843 }
2844 let fraction = (((point.u - start.u) * direction.u
2845 + (point.v - start.v) * direction.v)
2846 / squared_length)
2847 .clamp(0.0, 1.0);
2848 let span_start = *knots.get(index + 1)?;
2849 let span_end = *knots.get(index + 2)?;
2850 if !span_start.is_finite() || !span_end.is_finite() || span_start >= span_end {
2851 return None;
2852 }
2853 let projected = Point2::new(
2854 start.u + fraction * direction.u,
2855 start.v + fraction * direction.v,
2856 );
2857 let squared_distance =
2858 (projected.u - point.u).powi(2) + (projected.v - point.v).powi(2);
2859 Some((
2860 span_start + fraction * (span_end - span_start),
2861 squared_distance,
2862 ))
2863 });
2864 let first = candidates.next()?;
2865 let best = candidates.fold(first, |best, candidate| {
2866 if candidate.1 < best.1 {
2867 candidate
2868 } else {
2869 best
2870 }
2871 });
2872 Some(best.0)
2873}
2874
2875fn spine_contact_point(
2876 ir: &CadIr,
2877 support: &SurfaceId,
2878 spine: &CurveId,
2879 parameter: f64,
2880 radius: f64,
2881 depth: usize,
2882) -> Option<Point3> {
2883 (depth < 32).then_some(())?;
2884 let pcurve = spine_contact_pcurve(ir, support, spine, radius, depth + 1)?;
2885 let uv = pcurve_uv(pcurve, parameter)?;
2886 decoded_surface_point_inner(ir, support, uv.u, uv.v, depth + 1)
2887}
2888
2889fn spine_contact_pcurve<'a>(
2890 ir: &'a CadIr,
2891 support: &SurfaceId,
2892 spine: &CurveId,
2893 radius: f64,
2894 depth: usize,
2895) -> Option<&'a PcurveGeometry> {
2896 (depth < 32).then_some(())?;
2897 let procedural = ir.model.procedural_curves.iter().find(|candidate| {
2898 candidate.curve == *spine
2899 && matches!(
2900 candidate.definition,
2901 ProceduralCurveDefinition::Intersection { .. }
2902 )
2903 })?;
2904 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition else {
2905 unreachable!("definition selected above");
2906 };
2907 let candidates = context.sides.iter().filter_map(|side| {
2908 let side_surface = side.surface.as_ref()?;
2909 let pcurve = side.pcurve.as_ref()?;
2910 let offset = constant_surface_offset_between(ir, support, side_surface, depth + 1)?;
2911 if !blend_contact_offset_matches(0.0, offset, radius) {
2912 return None;
2913 }
2914 Some(pcurve)
2915 });
2916 let candidates = candidates.collect::<Vec<_>>();
2917 let [pcurve] = candidates.as_slice() else {
2918 return None;
2919 };
2920 Some(*pcurve)
2921}
2922
2923pub(crate) fn constant_surface_offset_between(
2924 ir: &CadIr,
2925 support: &SurfaceId,
2926 offset_surface: &SurfaceId,
2927 depth: usize,
2928) -> Option<f64> {
2929 let (support_base, support_offset) = surface_offset_lineage(ir, support, depth + 1)?;
2930 let (offset_base, offset_distance) = surface_offset_lineage(ir, offset_surface, depth + 1)?;
2931 if support_base == offset_base {
2932 return Some(offset_distance - support_offset);
2933 }
2934 let support_geometry = &ir
2935 .model
2936 .surfaces
2937 .iter()
2938 .find(|surface| surface.id == support_base)?
2939 .geometry;
2940 let offset_geometry = &ir
2941 .model
2942 .surfaces
2943 .iter()
2944 .find(|surface| surface.id == offset_base)?
2945 .geometry;
2946 let base_offset = analytic_surface_offset(support_geometry, offset_geometry)
2947 .or_else(|| blend_surface_offset(ir, &support_base, &offset_base, depth + 1))?;
2948 Some(base_offset + offset_distance - support_offset)
2949}
2950
2951fn blend_surface_offset(
2952 ir: &CadIr,
2953 support: &SurfaceId,
2954 offset: &SurfaceId,
2955 depth: usize,
2956) -> Option<f64> {
2957 (depth < 32).then_some(())?;
2958 let (support_carriers, support_spine, support_radius, support_reversed) =
2959 blend_surface_definition(ir, support)?;
2960 let (offset_carriers, offset_spine, offset_radius, offset_reversed) =
2961 blend_surface_definition(ir, offset)?;
2962 (support_spine == offset_spine).then_some(())?;
2963
2964 let distance = offset_radius - support_radius;
2965 let magnitude = distance.abs();
2966 let matches = [[0usize, 1usize], [1usize, 0usize]]
2967 .into_iter()
2968 .filter(|permutation| {
2969 permutation
2970 .iter()
2971 .enumerate()
2972 .all(|(support_index, &offset_index)| {
2973 support_reversed[support_index] == offset_reversed[offset_index]
2974 && constant_surface_offset_between(
2975 ir,
2976 &support_carriers[support_index],
2977 &offset_carriers[offset_index],
2978 depth + 1,
2979 )
2980 .is_some_and(|carrier_distance| {
2981 blend_contact_offset_matches(0.0, carrier_distance, magnitude)
2982 })
2983 })
2984 })
2985 .count();
2986 (matches == 1).then_some(distance)
2987}
2988
2989pub(crate) fn analytic_surface_offset(
2990 support: &SurfaceGeometry,
2991 offset: &SurfaceGeometry,
2992) -> Option<f64> {
2993 match (support, offset) {
2994 (
2995 SurfaceGeometry::Plane {
2996 origin: support_origin,
2997 normal: support_normal,
2998 u_axis: support_u,
2999 },
3000 SurfaceGeometry::Plane {
3001 origin: offset_origin,
3002 normal: offset_normal,
3003 u_axis: offset_u,
3004 },
3005 ) if support_normal == offset_normal && support_u == offset_u => {
3006 let delta = Vector3::new(
3007 offset_origin.x - support_origin.x,
3008 offset_origin.y - support_origin.y,
3009 offset_origin.z - support_origin.z,
3010 );
3011 let distance = dot_vector(delta, *support_normal);
3012 let residual = Vector3::new(
3013 delta.x - distance * support_normal.x,
3014 delta.y - distance * support_normal.y,
3015 delta.z - distance * support_normal.z,
3016 );
3017 let scale = [
3018 support_origin.x,
3019 support_origin.y,
3020 support_origin.z,
3021 offset_origin.x,
3022 offset_origin.y,
3023 offset_origin.z,
3024 distance,
3025 ]
3026 .into_iter()
3027 .fold(1.0_f64, |scale, value| scale.max(value.abs()));
3028 let tolerance = 64.0 * f64::EPSILON * scale;
3029 (dot_vector(residual, residual) <= tolerance * tolerance).then_some(distance)
3030 }
3031 (
3032 SurfaceGeometry::Cylinder {
3033 origin: support_origin,
3034 axis: support_axis,
3035 ref_direction: support_ref,
3036 radius: support_radius,
3037 },
3038 SurfaceGeometry::Cylinder {
3039 origin: offset_origin,
3040 axis: offset_axis,
3041 ref_direction: offset_ref,
3042 radius: offset_radius,
3043 },
3044 ) if support_origin == offset_origin
3045 && support_axis == offset_axis
3046 && support_ref == offset_ref =>
3047 {
3048 Some(offset_radius - support_radius)
3049 }
3050 (
3051 SurfaceGeometry::Cone {
3052 origin: support_origin,
3053 axis: support_axis,
3054 ref_direction: support_ref,
3055 radius: support_radius,
3056 ratio: support_ratio,
3057 half_angle: support_angle,
3058 },
3059 SurfaceGeometry::Cone {
3060 origin: offset_origin,
3061 axis: offset_axis,
3062 ref_direction: offset_ref,
3063 radius: offset_radius,
3064 ratio: offset_ratio,
3065 half_angle: offset_angle,
3066 },
3067 ) if support_axis == offset_axis
3068 && support_ref == offset_ref
3069 && support_ratio.to_bits() == 1.0_f64.to_bits()
3070 && offset_ratio.to_bits() == 1.0_f64.to_bits()
3071 && support_angle.to_bits() == offset_angle.to_bits() =>
3072 {
3073 let delta = Vector3::new(
3074 offset_origin.x - support_origin.x,
3075 offset_origin.y - support_origin.y,
3076 offset_origin.z - support_origin.z,
3077 );
3078 let axial_delta = dot_vector(delta, *support_axis);
3079 let residual = Vector3::new(
3080 delta.x - axial_delta * support_axis.x,
3081 delta.y - axial_delta * support_axis.y,
3082 delta.z - axial_delta * support_axis.z,
3083 );
3084 let radial_delta = offset_radius - support_radius;
3085 let distance = radial_delta * support_angle.cos() - axial_delta * support_angle.sin();
3086 let tangent_residual =
3087 radial_delta * support_angle.sin() + axial_delta * support_angle.cos();
3088 let scale = [
3089 support_origin.x,
3090 support_origin.y,
3091 support_origin.z,
3092 offset_origin.x,
3093 offset_origin.y,
3094 offset_origin.z,
3095 *support_radius,
3096 *offset_radius,
3097 axial_delta,
3098 distance,
3099 tangent_residual,
3100 ]
3101 .into_iter()
3102 .fold(1.0_f64, |scale, value| scale.max(value.abs()));
3103 let tolerance = 64.0 * f64::EPSILON * scale;
3104 (distance.is_finite()
3105 && dot_vector(residual, residual) <= tolerance * tolerance
3106 && tangent_residual.abs() <= tolerance)
3107 .then_some(distance)
3108 }
3109 (
3110 SurfaceGeometry::Sphere {
3111 center: support_center,
3112 axis: support_axis,
3113 ref_direction: support_ref,
3114 radius: support_radius,
3115 },
3116 SurfaceGeometry::Sphere {
3117 center: offset_center,
3118 axis: offset_axis,
3119 ref_direction: offset_ref,
3120 radius: offset_radius,
3121 },
3122 ) if support_center == offset_center
3123 && support_axis == offset_axis
3124 && support_ref == offset_ref
3125 && support_radius.signum().to_bits() == offset_radius.signum().to_bits() =>
3126 {
3127 Some((offset_radius - support_radius) * support_radius.signum())
3128 }
3129 (
3130 SurfaceGeometry::Torus {
3131 center: support_center,
3132 axis: support_axis,
3133 ref_direction: support_ref,
3134 major_radius: support_major,
3135 minor_radius: support_minor,
3136 },
3137 SurfaceGeometry::Torus {
3138 center: offset_center,
3139 axis: offset_axis,
3140 ref_direction: offset_ref,
3141 major_radius: offset_major,
3142 minor_radius: offset_minor,
3143 },
3144 ) if support_center == offset_center
3145 && support_axis == offset_axis
3146 && support_ref == offset_ref
3147 && support_major.to_bits() == offset_major.to_bits()
3148 && support_minor.signum().to_bits() == offset_minor.signum().to_bits()
3149 && *support_major > support_minor.abs()
3150 && *offset_major > offset_minor.abs() =>
3151 {
3152 Some((offset_minor - support_minor) * support_minor.signum())
3153 }
3154 _ => None,
3155 }
3156}
3157
3158pub(crate) fn blend_contact_offset_matches(
3159 support_offset: f64,
3160 spine_side_offset: f64,
3161 radius: f64,
3162) -> bool {
3163 let actual = (spine_side_offset - support_offset).abs();
3164 let expected = radius.abs();
3165 let scale = actual.max(expected).max(1.0);
3166 actual.is_finite()
3167 && expected.is_finite()
3168 && (actual - expected).abs() <= 64.0 * f64::EPSILON * scale
3169}
3170
3171fn surface_offset_lineage(
3172 ir: &CadIr,
3173 surface: &SurfaceId,
3174 depth: usize,
3175) -> Option<(SurfaceId, f64)> {
3176 (depth < 32).then_some(())?;
3177 let carrier = ir
3178 .model
3179 .surfaces
3180 .iter()
3181 .find(|candidate| &candidate.id == surface)?;
3182 let SurfaceGeometry::Procedural { construction } = &carrier.geometry else {
3183 return Some((surface.clone(), 0.0));
3184 };
3185 let procedural = ir
3186 .model
3187 .procedural_surfaces
3188 .iter()
3189 .find(|candidate| candidate.id == *construction && candidate.surface == *surface)?;
3190 let ProceduralSurfaceDefinition::Offset {
3191 support, distance, ..
3192 } = &procedural.definition
3193 else {
3194 return Some((surface.clone(), 0.0));
3195 };
3196 let (base, accumulated) = surface_offset_lineage(ir, support, depth + 1)?;
3197 Some((base, accumulated + distance))
3198}
3199
3200fn blend_surface_definition(
3201 ir: &CadIr,
3202 surface: &SurfaceId,
3203) -> Option<([SurfaceId; 2], CurveId, f64, [bool; 2])> {
3204 let carrier = ir
3205 .model
3206 .surfaces
3207 .iter()
3208 .find(|candidate| &candidate.id == surface)?;
3209 let SurfaceGeometry::Procedural { construction } = &carrier.geometry else {
3210 return None;
3211 };
3212 let procedural = ir
3213 .model
3214 .procedural_surfaces
3215 .iter()
3216 .find(|candidate| &candidate.id == construction && &candidate.surface == surface)?;
3217 let ProceduralSurfaceDefinition::Blend {
3218 supports: [Some(first), Some(second)],
3219 spine: Some(spine),
3220 radius: BlendRadiusLaw::Constant { signed_radius },
3221 cross_section: BlendCrossSection::Circular,
3222 ..
3223 } = &procedural.definition
3224 else {
3225 return None;
3226 };
3227 let radius = signed_radius.abs();
3228 (radius.is_finite() && radius > 0.0).then(|| {
3229 (
3230 [first.surface.clone(), second.surface.clone()],
3231 spine.clone(),
3232 radius,
3233 [first.reversed, second.reversed],
3234 )
3235 })
3236}
3237
3238fn surface_contact_direction(
3239 ir: &CadIr,
3240 surface: &SurfaceId,
3241 center: Point3,
3242 depth: usize,
3243) -> Option<Vector3> {
3244 (depth < 32).then_some(())?;
3245 let carrier = ir
3246 .model
3247 .surfaces
3248 .iter()
3249 .find(|candidate| &candidate.id == surface)?;
3250 let parameters = match &carrier.geometry {
3251 SurfaceGeometry::Nurbs(nurbs) => nurbs_parameters(nurbs, center, None),
3252 SurfaceGeometry::Procedural { .. } => offset_surface_parameters(ir, surface, center, None)
3253 .or_else(|| {
3254 blend_surface_parameters_inner(
3255 ir,
3256 surface,
3257 center,
3258 None,
3259 None,
3260 BlendParameterGrid::Build,
3261 depth + 1,
3262 )
3263 }),
3264 geometry => analytic_surface_parameters(geometry, center),
3265 }?;
3266 let contact = decoded_surface_point_inner(ir, surface, parameters.u, parameters.v, depth + 1)?;
3267 unit_vector(Vector3::new(
3268 contact.x - center.x,
3269 contact.y - center.y,
3270 contact.z - center.z,
3271 ))
3272}
3273
3274fn model_curve_point(ir: &CadIr, curve: &CurveId, parameter: f64) -> Option<Point3> {
3275 let carrier = ir
3276 .model
3277 .curves
3278 .iter()
3279 .find(|candidate| &candidate.id == curve)?;
3280 curve_point(&carrier.geometry, parameter)
3281}
3282
3283fn model_curve_tangent(ir: &CadIr, curve: &CurveId, parameter: f64) -> Option<Vector3> {
3284 let step = 1.0e-6 * (1.0 + parameter.abs());
3285 let center = model_curve_point(ir, curve, parameter)?;
3286 let before = model_curve_point(ir, curve, parameter - step);
3287 let after = model_curve_point(ir, curve, parameter + step);
3288 let (before, after) = match (before, after) {
3289 (Some(before), Some(after)) => (before, after),
3290 (Some(before), None) => (before, center),
3291 (None, Some(after)) => (center, after),
3292 (None, None) => return None,
3293 };
3294 unit_vector(Vector3::new(
3295 after.x - before.x,
3296 after.y - before.y,
3297 after.z - before.z,
3298 ))
3299}
3300
3301pub(crate) fn closest_spine_parameter(
3302 ir: &CadIr,
3303 curve: &CurveId,
3304 point: Point3,
3305 seed: Option<f64>,
3306) -> Option<f64> {
3307 let carrier = ir
3308 .model
3309 .curves
3310 .iter()
3311 .find(|candidate| &candidate.id == curve)?;
3312 match &carrier.geometry {
3313 CurveGeometry::Line { origin, direction } => Some(
3314 (point.x - origin.x) * direction.x
3315 + (point.y - origin.y) * direction.y
3316 + (point.z - origin.z) * direction.z,
3317 ),
3318 CurveGeometry::Circle {
3319 center,
3320 axis,
3321 ref_direction,
3322 ..
3323 }
3324 | CurveGeometry::Ellipse {
3325 center,
3326 axis,
3327 major_direction: ref_direction,
3328 ..
3329 } => closest_periodic_analytic_curve_parameter(
3330 &carrier.geometry,
3331 *center,
3332 *axis,
3333 *ref_direction,
3334 point,
3335 seed,
3336 ),
3337 CurveGeometry::Nurbs(nurbs) => {
3338 let degree = usize::try_from(nurbs.degree).ok()?;
3339 let count = nurbs.control_points.len();
3340 let domain = [*nurbs.knots.get(degree)?, *nurbs.knots.get(count)?];
3341 if domain[0] >= domain[1] {
3342 return None;
3343 }
3344 closest_nurbs_curve_parameter(
3345 &carrier.geometry,
3346 &nurbs.knots,
3347 degree,
3348 domain,
3349 point,
3350 seed,
3351 )
3352 }
3353 _ => None,
3354 }
3355}
3356
3357fn closest_periodic_analytic_curve_parameter(
3358 geometry: &CurveGeometry,
3359 center: Point3,
3360 axis: Vector3,
3361 reference: Vector3,
3362 point: Point3,
3363 seed: Option<f64>,
3364) -> Option<f64> {
3365 let transverse = cross_vector(axis, reference);
3366 let delta = Vector3::new(point.x - center.x, point.y - center.y, point.z - center.z);
3367 let phase = dot_vector(delta, transverse).atan2(dot_vector(delta, reference));
3368 phase.is_finite().then_some(())?;
3369 let anchor = seed.map_or(phase, |seed| {
3370 phase + ((seed - phase) / std::f64::consts::TAU).round() * std::f64::consts::TAU
3371 });
3372 let lower = anchor - std::f64::consts::PI;
3373 let step = std::f64::consts::TAU / 64.0;
3374 let squared_distance = |parameter| {
3375 let position = curve_point(geometry, parameter)?;
3376 Some(
3377 (position.x - point.x).powi(2)
3378 + (position.y - point.y).powi(2)
3379 + (position.z - point.z).powi(2),
3380 )
3381 };
3382 let samples = (0..=64)
3383 .map(|index| lower + f64::from(index) * step)
3384 .collect::<Vec<_>>();
3385 let distances = samples
3386 .iter()
3387 .map(|parameter| squared_distance(*parameter))
3388 .collect::<Option<Vec<_>>>()?;
3389 let mut best_index = 0;
3390 for index in 1..distances.len() {
3391 if distances[index] < distances[best_index]
3392 || distances[index] == distances[best_index]
3393 && (samples[index] - anchor).abs() < (samples[best_index] - anchor).abs()
3394 {
3395 best_index = index;
3396 }
3397 }
3398 let bracket_center = match best_index {
3399 0 => samples[0] + std::f64::consts::TAU,
3400 64 => samples[64] - std::f64::consts::TAU,
3401 _ => samples[best_index],
3402 };
3403 let (parameter, _) = golden_section_minimum(
3404 bracket_center - step,
3405 bracket_center + step,
3406 &squared_distance,
3407 )?;
3408 Some(parameter + ((anchor - parameter) / std::f64::consts::TAU).round() * std::f64::consts::TAU)
3409}
3410
3411fn closest_nurbs_curve_parameter(
3412 geometry: &CurveGeometry,
3413 knots: &[f64],
3414 degree: usize,
3415 domain: [f64; 2],
3416 point: Point3,
3417 seed: Option<f64>,
3418) -> Option<f64> {
3419 let squared_distance = |parameter| {
3420 let position = curve_point(geometry, parameter)?;
3421 Some(
3422 (position.x - point.x).powi(2)
3423 + (position.y - point.y).powi(2)
3424 + (position.z - point.z).powi(2),
3425 )
3426 };
3427 let samples = knot_domain_samples(knots, degree, domain);
3428 let distances = samples
3429 .iter()
3430 .map(|parameter| squared_distance(*parameter))
3431 .collect::<Option<Vec<_>>>()?;
3432 let mut best = samples[0];
3433 let mut best_distance = distances[0];
3434 let mut best_seed_distance = seed.map_or(best.abs(), |seed| (best - seed).abs());
3435 let mut consider = |parameter: f64, distance: f64| {
3436 let seed_distance = seed.map_or(parameter.abs(), |seed| (parameter - seed).abs());
3437 let same_point = (distance - best_distance).abs()
3438 <= f64::EPSILON * 64.0 * distance.abs().max(best_distance.abs()).max(1.0);
3439 if distance < best_distance && !same_point
3440 || same_point && seed_distance < best_seed_distance
3441 {
3442 best = parameter;
3443 best_distance = distance;
3444 best_seed_distance = seed_distance;
3445 }
3446 };
3447 for (index, &distance) in distances.iter().enumerate() {
3448 consider(samples[index], distance);
3449 if index > 0
3450 && index + 1 < samples.len()
3451 && distance <= distances[index - 1]
3452 && distance <= distances[index + 1]
3453 {
3454 let (parameter, distance) =
3455 golden_section_minimum(samples[index - 1], samples[index + 1], &squared_distance)?;
3456 consider(parameter, distance);
3457 }
3458 }
3459 if let Some(seed) = seed {
3460 let seed = seed.clamp(domain[0], domain[1]);
3461 let insertion = samples.partition_point(|parameter| *parameter < seed);
3462 let lower = samples[insertion.saturating_sub(1)];
3463 let upper = samples[insertion.min(samples.len() - 1)];
3464 if lower < upper {
3465 let (parameter, distance) = golden_section_minimum(lower, upper, &squared_distance)?;
3466 consider(parameter, distance);
3467 } else {
3468 consider(seed, squared_distance(seed)?);
3469 }
3470 }
3471 Some(best)
3472}
3473
3474fn knot_domain_samples(knots: &[f64], degree: usize, domain: [f64; 2]) -> Vec<f64> {
3475 let subdivisions = 2 * (degree + 1).max(2);
3476 let mut samples = vec![domain[0]];
3477 for span in knots[degree..].windows(2) {
3478 let start = span[0].max(domain[0]);
3479 let end = span[1].min(domain[1]);
3480 if start >= end {
3481 continue;
3482 }
3483 for index in 1..=subdivisions {
3484 samples.push(start + (end - start) * index as f64 / subdivisions as f64);
3485 }
3486 if end >= domain[1] {
3487 break;
3488 }
3489 }
3490 samples.sort_by(f64::total_cmp);
3491 samples.dedup_by(|left, right| *left == *right);
3492 samples
3493}
3494
3495fn golden_section_minimum(
3496 mut lower: f64,
3497 mut upper: f64,
3498 value: &impl Fn(f64) -> Option<f64>,
3499) -> Option<(f64, f64)> {
3500 let ratio = (5.0_f64.sqrt() - 1.0) / 2.0;
3501 let mut left = upper - ratio * (upper - lower);
3502 let mut right = lower + ratio * (upper - lower);
3503 let mut left_value = value(left)?;
3504 let mut right_value = value(right)?;
3505 for _ in 0..64 {
3506 if left_value <= right_value {
3507 upper = right;
3508 right = left;
3509 right_value = left_value;
3510 left = upper - ratio * (upper - lower);
3511 left_value = value(left)?;
3512 } else {
3513 lower = left;
3514 left = right;
3515 left_value = right_value;
3516 right = lower + ratio * (upper - lower);
3517 right_value = value(right)?;
3518 }
3519 }
3520 if left_value <= right_value {
3521 Some((left, left_value))
3522 } else {
3523 Some((right, right_value))
3524 }
3525}
3526
3527fn signed_angle(first: Vector3, second: Vector3, axis: Vector3) -> f64 {
3528 dot_vector(cross_vector(first, second), axis).atan2(dot_vector(first, second))
3529}
3530
3531fn rodrigues_rotate(vector: Vector3, axis: Vector3, angle: f64) -> Vector3 {
3532 let cross = cross_vector(axis, vector);
3533 let dot = dot_vector(axis, vector);
3534 Vector3::new(
3535 vector.x * angle.cos() + cross.x * angle.sin() + axis.x * dot * (1.0 - angle.cos()),
3536 vector.y * angle.cos() + cross.y * angle.sin() + axis.y * dot * (1.0 - angle.cos()),
3537 vector.z * angle.cos() + cross.z * angle.sin() + axis.z * dot * (1.0 - angle.cos()),
3538 )
3539}
3540
3541pub(crate) fn offset_surface_parameters(
3542 ir: &CadIr,
3543 surface: &SurfaceId,
3544 point: Point3,
3545 seed: Option<Point2>,
3546) -> Option<Point2> {
3547 offset_surface_parameters_with_tolerance(ir, surface, point, seed, None)
3548}
3549
3550pub(crate) fn offset_surface_parameters_with_tolerance(
3551 ir: &CadIr,
3552 surface: &SurfaceId,
3553 point: Point3,
3554 seed: Option<Point2>,
3555 fit_tolerance: Option<f64>,
3556) -> Option<Point2> {
3557 let carrier = ir
3558 .model
3559 .surfaces
3560 .iter()
3561 .find(|candidate| &candidate.id == surface)?;
3562 let SurfaceGeometry::Procedural { construction } = &carrier.geometry else {
3563 return None;
3564 };
3565 let procedural = ir
3566 .model
3567 .procedural_surfaces
3568 .iter()
3569 .find(|candidate| &candidate.id == construction && &candidate.surface == surface)?;
3570 let ProceduralSurfaceDefinition::Offset { support, .. } = &procedural.definition else {
3571 return None;
3572 };
3573 let domain = surface_parameter_domain(ir, support);
3574 let mut parameters = seed
3575 .or_else(|| initial_surface_parameters(ir, support, point, None))
3576 .or_else(|| {
3577 domain.and_then(|domain| coarse_model_surface_parameters(ir, surface, point, domain))
3578 })?;
3579 clamp_surface_parameters(&mut parameters, domain);
3580 for _ in 0..32 {
3581 let position = model_surface_point_by_id(ir, surface, parameters.u, parameters.v)?;
3582 let residual = Vector3::new(
3583 position.x - point.x,
3584 position.y - point.y,
3585 position.z - point.z,
3586 );
3587 if fit_tolerance.is_some_and(|tolerance| {
3588 tolerance.is_finite()
3589 && tolerance >= 0.0
3590 && dot_vector(residual, residual) <= tolerance * tolerance
3591 }) {
3592 break;
3593 }
3594 let u_step = parameter_derivative_step(parameters.u, domain.map(|domain| domain.0));
3595 let v_step = parameter_derivative_step(parameters.v, domain.map(|domain| domain.1));
3596 let du =
3597 model_surface_derivative(ir, surface, parameters, u_step, true, domain, [None, None])?;
3598 let dv =
3599 model_surface_derivative(ir, surface, parameters, v_step, false, domain, [None, None])?;
3600 let Some((step_u, step_v)) = least_squares_step(du, dv, residual) else {
3601 break;
3602 };
3603 parameters.u -= step_u;
3604 parameters.v -= step_v;
3605 clamp_surface_parameters(&mut parameters, domain);
3606 if step_u.abs() <= 1.0e-12 * (1.0 + parameters.u.abs())
3607 && step_v.abs() <= 1.0e-12 * (1.0 + parameters.v.abs())
3608 {
3609 break;
3610 }
3611 }
3612 Some(parameters)
3613}
3614
3615fn coarse_model_surface_parameters(
3616 ir: &CadIr,
3617 surface: &SurfaceId,
3618 point: Point3,
3619 domain: ([f64; 2], [f64; 2]),
3620) -> Option<Point2> {
3621 let (u_domain, v_domain) = domain;
3622 let mut best = None;
3623 let mut best_distance = f64::INFINITY;
3624 for ui in 0..=8 {
3625 for vi in 0..=8 {
3626 let parameters = Point2::new(
3627 u_domain[0] + (u_domain[1] - u_domain[0]) * f64::from(ui) / 8.0,
3628 v_domain[0] + (v_domain[1] - v_domain[0]) * f64::from(vi) / 8.0,
3629 );
3630 let Some(candidate) =
3631 model_surface_point_by_id(ir, surface, parameters.u, parameters.v)
3632 else {
3633 continue;
3634 };
3635 let distance = (candidate.x - point.x).powi(2)
3636 + (candidate.y - point.y).powi(2)
3637 + (candidate.z - point.z).powi(2);
3638 if distance < best_distance {
3639 best = Some(parameters);
3640 best_distance = distance;
3641 }
3642 }
3643 }
3644 best
3645}
3646
3647fn initial_surface_parameters(
3648 ir: &CadIr,
3649 surface: &SurfaceId,
3650 point: Point3,
3651 seed: Option<Point2>,
3652) -> Option<Point2> {
3653 let carrier = ir
3654 .model
3655 .surfaces
3656 .iter()
3657 .find(|candidate| &candidate.id == surface)?;
3658 match &carrier.geometry {
3659 SurfaceGeometry::Nurbs(nurbs) => nurbs_parameters(nurbs, point, seed),
3660 SurfaceGeometry::Procedural { construction } => {
3661 let procedural =
3662 ir.model.procedural_surfaces.iter().find(|candidate| {
3663 &candidate.id == construction && &candidate.surface == surface
3664 })?;
3665 let ProceduralSurfaceDefinition::Offset { support, .. } = &procedural.definition else {
3666 return None;
3667 };
3668 initial_surface_parameters(ir, support, point, seed)
3669 }
3670 geometry => analytic_surface_parameters(geometry, point),
3671 }
3672}
3673
3674fn surface_parameter_domain(ir: &CadIr, surface: &SurfaceId) -> Option<([f64; 2], [f64; 2])> {
3675 let carrier = ir
3676 .model
3677 .surfaces
3678 .iter()
3679 .find(|candidate| &candidate.id == surface)?;
3680 match &carrier.geometry {
3681 SurfaceGeometry::Nurbs(nurbs) => {
3682 let u_degree = usize::try_from(nurbs.u_degree).ok()?;
3683 let v_degree = usize::try_from(nurbs.v_degree).ok()?;
3684 let u_count = usize::try_from(nurbs.u_count).ok()?;
3685 let v_count = usize::try_from(nurbs.v_count).ok()?;
3686 Some((
3687 [*nurbs.u_knots.get(u_degree)?, *nurbs.u_knots.get(u_count)?],
3688 [*nurbs.v_knots.get(v_degree)?, *nurbs.v_knots.get(v_count)?],
3689 ))
3690 }
3691 SurfaceGeometry::Procedural { construction } => {
3692 let procedural =
3693 ir.model.procedural_surfaces.iter().find(|candidate| {
3694 &candidate.id == construction && &candidate.surface == surface
3695 })?;
3696 let ProceduralSurfaceDefinition::Offset { support, .. } = &procedural.definition else {
3697 return None;
3698 };
3699 surface_parameter_domain(ir, support)
3700 }
3701 _ => None,
3702 }
3703}
3704
3705fn clamp_surface_parameters(parameters: &mut Point2, domain: Option<([f64; 2], [f64; 2])>) {
3706 if let Some((u_domain, v_domain)) = domain {
3707 parameters.u = parameters.u.clamp(u_domain[0], u_domain[1]);
3708 parameters.v = parameters.v.clamp(v_domain[0], v_domain[1]);
3709 }
3710}
3711
3712fn parameter_derivative_step(parameter: f64, domain: Option<[f64; 2]>) -> f64 {
3713 domain.map_or_else(
3714 || 1.0e-6 * (1.0 + parameter.abs()),
3715 |domain| 1.0e-6 * (domain[1] - domain[0]).abs().max(1.0),
3716 )
3717}
3718
3719fn model_surface_derivative(
3720 ir: &CadIr,
3721 surface: &SurfaceId,
3722 parameters: Point2,
3723 step: f64,
3724 along_u: bool,
3725 domain: Option<([f64; 2], [f64; 2])>,
3726 periods: [Option<f64>; 2],
3727) -> Option<Vector3> {
3728 let mut before = parameters;
3729 let mut after = parameters;
3730 if along_u {
3731 before.u -= step;
3732 after.u += step;
3733 } else {
3734 before.v -= step;
3735 after.v += step;
3736 }
3737 clamp_surface_parameters_with_periods(&mut before, domain, periods);
3738 clamp_surface_parameters_with_periods(&mut after, domain, periods);
3739 let width = if along_u {
3740 after.u - before.u
3741 } else {
3742 after.v - before.v
3743 };
3744 if !width.is_finite() || width == 0.0 {
3745 return None;
3746 }
3747 let first = model_surface_point_by_id(ir, surface, before.u, before.v)?;
3748 let second = model_surface_point_by_id(ir, surface, after.u, after.v)?;
3749 Some(Vector3::new(
3750 (second.x - first.x) / width,
3751 (second.y - first.y) / width,
3752 (second.z - first.z) / width,
3753 ))
3754}
3755
3756#[cfg(test)]
3760pub(crate) fn continue_surface_intersection_parameters(
3761 ir: &CadIr,
3762 surfaces: [&SurfaceId; 2],
3763 chart: &[Point3],
3764 fit_tolerance: f64,
3765) -> Option<[Vec<Point2>; 2]> {
3766 continue_surface_intersection_parameters_with_seeds(
3767 ir,
3768 surfaces,
3769 chart,
3770 fit_tolerance,
3771 [None, None],
3772 )
3773}
3774
3775fn continue_surface_intersection_parameters_with_seeds(
3776 ir: &CadIr,
3777 surfaces: [&SurfaceId; 2],
3778 chart: &[Point3],
3779 fit_tolerance: f64,
3780 seeds: [Option<Point2>; 2],
3781) -> Option<[Vec<Point2>; 2]> {
3782 if chart.len() < 2
3783 || surfaces[0] == surfaces[1]
3784 || !fit_tolerance.is_finite()
3785 || fit_tolerance <= 0.0
3786 {
3787 return None;
3788 }
3789 let fit_parameters = |surface: &SurfaceId, point: Point3, seed: Option<Point2>| {
3790 let geometry = &ir
3791 .model
3792 .surfaces
3793 .iter()
3794 .find(|candidate| &candidate.id == surface)?
3795 .geometry;
3796 match geometry {
3797 SurfaceGeometry::Nurbs(nurbs) => nurbs_parameters(nurbs, point, seed),
3798 SurfaceGeometry::Procedural { .. } => offset_surface_parameters_with_tolerance(
3799 ir,
3800 surface,
3801 point,
3802 seed,
3803 Some(fit_tolerance),
3804 )
3805 .or_else(|| blend_surface_parameters_for_fit(ir, surface, point, seed, fit_tolerance)),
3806 geometry => analytic_surface_parameters(geometry, point),
3807 }
3808 };
3809 let first = [
3810 fit_parameters(surfaces[0], chart[0], seeds[0])?,
3811 fit_parameters(surfaces[1], chart[0], seeds[1])?,
3812 ];
3813 let space = IntersectionParameterSpace {
3814 domains: surfaces.map(|surface| surface_parameter_domain(ir, surface)),
3815 periods: surfaces.map(|surface| surface_parameter_periods(ir, surface)),
3816 };
3817 let seed = [first[0].u, first[0].v, first[1].u, first[1].v];
3818 let first_chord = Vector3::new(
3819 chart[1].x - chart[0].x,
3820 chart[1].y - chart[0].y,
3821 chart[1].z - chart[0].z,
3822 );
3823 let seed_tangent = intersection_parameter_tangent(ir, surfaces, seed, space, first_chord)?;
3824 let mut current = correct_intersection_parameters(
3825 ir,
3826 surfaces,
3827 seed,
3828 seed_tangent,
3829 space,
3830 fit_tolerance,
3831 1.0,
3832 )?;
3833 let first_point = model_surface_point_by_id(ir, surfaces[0], current[0], current[1])?;
3834 if point_distance(first_point, chart[0]) > fit_tolerance {
3835 return None;
3836 }
3837 let mut lanes = [
3838 vec![Point2::new(current[0], current[1])],
3839 vec![Point2::new(current[2], current[3])],
3840 ];
3841
3842 for chart_pair in chart.windows(2) {
3843 let jacobian = intersection_parameter_jacobian(ir, surfaces, current, space)?;
3844 let chord = Vector3::new(
3845 chart_pair[1].x - chart_pair[0].x,
3846 chart_pair[1].y - chart_pair[0].y,
3847 chart_pair[1].z - chart_pair[0].z,
3848 );
3849 let tangent = intersection_parameter_tangent(ir, surfaces, current, space, chord)?;
3850 let spatial_tangent = Vector3::new(
3851 jacobian[0][0] * tangent[0] + jacobian[0][1] * tangent[1],
3852 jacobian[1][0] * tangent[0] + jacobian[1][1] * tangent[1],
3853 jacobian[2][0] * tangent[0] + jacobian[2][1] * tangent[1],
3854 );
3855 let target = [
3856 fit_parameters(
3857 surfaces[0],
3858 chart_pair[1],
3859 Some(Point2::new(current[0], current[1])),
3860 )?,
3861 fit_parameters(
3862 surfaces[1],
3863 chart_pair[1],
3864 Some(Point2::new(current[2], current[3])),
3865 )?,
3866 ];
3867 let mut predictor = [target[0].u, target[0].v, target[1].u, target[1].v];
3868 for (side, surface_periods) in space.periods.into_iter().enumerate() {
3869 for (coordinate, period) in surface_periods.into_iter().enumerate() {
3870 let index = side * 2 + coordinate;
3871 if let Some(period) = period {
3872 predictor[index] =
3873 lift_periodic_parameter(predictor[index], current[index], period);
3874 }
3875 }
3876 }
3877 let scale = (0..4)
3878 .map(|index| (predictor[index] - current[index]) * tangent[index])
3879 .sum::<f64>();
3880 if !scale.is_finite() || scale == 0.0 || dot_vector(spatial_tangent, chord) * scale <= 0.0 {
3881 return None;
3882 }
3883 let corrected = correct_intersection_parameters(
3884 ir,
3885 surfaces,
3886 predictor,
3887 tangent,
3888 space,
3889 fit_tolerance,
3890 scale,
3891 )?;
3892 let point = model_surface_point_by_id(ir, surfaces[0], corrected[0], corrected[1])?;
3893 if point_distance(point, chart_pair[1]) > fit_tolerance {
3894 return None;
3895 }
3896 current = corrected;
3897 lanes[0].push(Point2::new(current[0], current[1]));
3898 lanes[1].push(Point2::new(current[2], current[3]));
3899 }
3900 Some(lanes)
3901}
3902
3903fn lift_periodic_parameter(value: f64, reference: f64, period: f64) -> f64 {
3904 value + ((reference - value) / period).round() * period
3905}
3906
3907pub(crate) fn surface_parameter_periods(ir: &CadIr, surface: &SurfaceId) -> [Option<f64>; 2] {
3909 surface_parameter_periods_inner(ir, surface, &mut BTreeSet::new())
3910}
3911
3912fn surface_parameter_periods_inner(
3913 ir: &CadIr,
3914 surface: &SurfaceId,
3915 visiting: &mut BTreeSet<SurfaceId>,
3916) -> [Option<f64>; 2] {
3917 if !visiting.insert(surface.clone()) {
3918 return [None, None];
3919 }
3920 let Some(carrier) = ir
3921 .model
3922 .surfaces
3923 .iter()
3924 .find(|candidate| &candidate.id == surface)
3925 else {
3926 visiting.remove(surface);
3927 return [None, None];
3928 };
3929 let periods = match &carrier.geometry {
3930 SurfaceGeometry::Cylinder { .. }
3931 | SurfaceGeometry::Cone { .. }
3932 | SurfaceGeometry::Sphere { .. } => [Some(std::f64::consts::TAU), None],
3933 SurfaceGeometry::Torus { .. } => [Some(std::f64::consts::TAU), Some(std::f64::consts::TAU)],
3934 SurfaceGeometry::Nurbs(nurbs) => {
3935 let period = |periodic: bool, knots: &[f64], degree: u32, count: u32| {
3936 periodic.then(|| {
3937 let degree = usize::try_from(degree).ok()?;
3938 let count = usize::try_from(count).ok()?;
3939 let period = knots.get(count)? - knots.get(degree)?;
3940 (period.is_finite() && period > 0.0).then_some(period)
3941 })?
3942 };
3943 [
3944 period(
3945 nurbs.u_periodic,
3946 &nurbs.u_knots,
3947 nurbs.u_degree,
3948 nurbs.u_count,
3949 ),
3950 period(
3951 nurbs.v_periodic,
3952 &nurbs.v_knots,
3953 nurbs.v_degree,
3954 nurbs.v_count,
3955 ),
3956 ]
3957 }
3958 SurfaceGeometry::Procedural { construction } => ir
3959 .model
3960 .procedural_surfaces
3961 .iter()
3962 .find(|candidate| &candidate.id == construction && &candidate.surface == surface)
3963 .and_then(|procedural| match &procedural.definition {
3964 ProceduralSurfaceDefinition::Offset { support, .. } => {
3965 Some(surface_parameter_periods_inner(ir, support, visiting))
3966 }
3967 _ => None,
3968 })
3969 .unwrap_or([None, None]),
3970 _ => [None, None],
3971 };
3972 visiting.remove(surface);
3973 periods
3974}
3975
3976fn correct_intersection_parameters(
3977 ir: &CadIr,
3978 surfaces: [&SurfaceId; 2],
3979 predictor: [f64; 4],
3980 tangent: [f64; 4],
3981 space: IntersectionParameterSpace,
3982 fit_tolerance: f64,
3983 scale: f64,
3984) -> Option<[f64; 4]> {
3985 let mut corrected = predictor;
3986 clamp_intersection_parameters(&mut corrected, space);
3987 for _ in 0..32 {
3988 let first = model_surface_point_by_id(ir, surfaces[0], corrected[0], corrected[1])?;
3989 let second = model_surface_point_by_id(ir, surfaces[1], corrected[2], corrected[3])?;
3990 let residual = [
3991 first.x - second.x,
3992 first.y - second.y,
3993 first.z - second.z,
3994 (0..4)
3995 .map(|index| (corrected[index] - predictor[index]) * tangent[index])
3996 .sum(),
3997 ];
3998 let equality_error = residual[..3]
3999 .iter()
4000 .map(|value| value * value)
4001 .sum::<f64>()
4002 .sqrt();
4003 if equality_error <= fit_tolerance * 1.0e-6
4004 && residual[3].abs() <= 1.0e-11 * (1.0 + scale.abs())
4005 {
4006 return Some(corrected);
4007 }
4008 let jacobian = intersection_parameter_jacobian(ir, surfaces, corrected, space)?;
4009 let matrix = [jacobian[0], jacobian[1], jacobian[2], tangent];
4010 let step = solve_4x4(matrix, residual.map(|value| -value))?;
4011 for index in 0..4 {
4012 corrected[index] += step[index];
4013 }
4014 clamp_intersection_parameters(&mut corrected, space);
4015 }
4016 None
4017}
4018
4019#[derive(Clone, Copy)]
4020struct IntersectionParameterSpace {
4021 domains: [Option<([f64; 2], [f64; 2])>; 2],
4022 periods: [[Option<f64>; 2]; 2],
4023}
4024
4025fn intersection_parameter_tangent(
4026 ir: &CadIr,
4027 surfaces: [&SurfaceId; 2],
4028 parameters: [f64; 4],
4029 space: IntersectionParameterSpace,
4030 chord: Vector3,
4031) -> Option<[f64; 4]> {
4032 let jacobian = intersection_parameter_jacobian(ir, surfaces, parameters, space)?;
4033 if let Some(tangent) = null_vector_3x4(jacobian) {
4034 return Some(tangent);
4035 }
4036 let chord = unit_vector(chord)?;
4037 let derivatives = [
4038 [
4039 Vector3::new(jacobian[0][0], jacobian[1][0], jacobian[2][0]),
4040 Vector3::new(jacobian[0][1], jacobian[1][1], jacobian[2][1]),
4041 ],
4042 [
4043 Vector3::new(-jacobian[0][2], -jacobian[1][2], -jacobian[2][2]),
4044 Vector3::new(-jacobian[0][3], -jacobian[1][3], -jacobian[2][3]),
4045 ],
4046 ];
4047 let mut tangent = [0.0; 4];
4048 for side in 0..2 {
4049 let (u, v) = least_squares_step(derivatives[side][0], derivatives[side][1], chord)?;
4050 let mapped = unit_vector(Vector3::new(
4051 derivatives[side][0].x * u + derivatives[side][1].x * v,
4052 derivatives[side][0].y * u + derivatives[side][1].y * v,
4053 derivatives[side][0].z * u + derivatives[side][1].z * v,
4054 ))?;
4055 if dot_vector(mapped, chord) < 1.0 - 1.0e-8 {
4056 return None;
4057 }
4058 tangent[side * 2] = u;
4059 tangent[side * 2 + 1] = v;
4060 }
4061 let norm = tangent
4062 .iter()
4063 .map(|value| value * value)
4064 .sum::<f64>()
4065 .sqrt();
4066 (norm.is_finite() && norm > 1.0e-14).then(|| tangent.map(|value| value / norm))
4067}
4068
4069fn intersection_parameter_jacobian(
4070 ir: &CadIr,
4071 surfaces: [&SurfaceId; 2],
4072 parameters: [f64; 4],
4073 space: IntersectionParameterSpace,
4074) -> Option<[[f64; 4]; 3]> {
4075 let pairs = [
4076 Point2::new(parameters[0], parameters[1]),
4077 Point2::new(parameters[2], parameters[3]),
4078 ];
4079 let derivatives = std::array::from_fn(|side| {
4080 let u_step =
4081 parameter_derivative_step(pairs[side].u, space.domains[side].map(|value| value.0));
4082 let v_step =
4083 parameter_derivative_step(pairs[side].v, space.domains[side].map(|value| value.1));
4084 Some([
4085 model_surface_derivative(
4086 ir,
4087 surfaces[side],
4088 pairs[side],
4089 u_step,
4090 true,
4091 space.domains[side],
4092 space.periods[side],
4093 )?,
4094 model_surface_derivative(
4095 ir,
4096 surfaces[side],
4097 pairs[side],
4098 v_step,
4099 false,
4100 space.domains[side],
4101 space.periods[side],
4102 )?,
4103 ])
4104 });
4105 let [Some(first), Some(second)] = derivatives else {
4106 return None;
4107 };
4108 Some([
4109 [first[0].x, first[1].x, -second[0].x, -second[1].x],
4110 [first[0].y, first[1].y, -second[0].y, -second[1].y],
4111 [first[0].z, first[1].z, -second[0].z, -second[1].z],
4112 ])
4113}
4114
4115fn clamp_intersection_parameters(parameters: &mut [f64; 4], space: IntersectionParameterSpace) {
4116 for side in 0..2 {
4117 let mut pair = Point2::new(parameters[side * 2], parameters[side * 2 + 1]);
4118 clamp_surface_parameters_with_periods(&mut pair, space.domains[side], space.periods[side]);
4119 parameters[side * 2] = pair.u;
4120 parameters[side * 2 + 1] = pair.v;
4121 }
4122}
4123
4124fn clamp_surface_parameters_with_periods(
4125 parameters: &mut Point2,
4126 domain: Option<([f64; 2], [f64; 2])>,
4127 periods: [Option<f64>; 2],
4128) {
4129 if let Some((u_domain, v_domain)) = domain {
4130 if periods[0].is_none() {
4131 parameters.u = parameters.u.clamp(u_domain[0], u_domain[1]);
4132 }
4133 if periods[1].is_none() {
4134 parameters.v = parameters.v.clamp(v_domain[0], v_domain[1]);
4135 }
4136 }
4137}
4138
4139fn determinant_3x3(matrix: [[f64; 3]; 3]) -> f64 {
4140 matrix[0][0] * (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1])
4141 - matrix[0][1] * (matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0])
4142 + matrix[0][2] * (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0])
4143}
4144
4145fn null_vector_3x4(matrix: [[f64; 4]; 3]) -> Option<[f64; 4]> {
4146 let mut vector = [0.0; 4];
4147 for (omitted, component) in vector.iter_mut().enumerate() {
4148 let minor = std::array::from_fn(|row| {
4149 let mut column = 0;
4150 std::array::from_fn(|_| {
4151 while column == omitted {
4152 column += 1;
4153 }
4154 let value = matrix[row][column];
4155 column += 1;
4156 value
4157 })
4158 });
4159 *component = if omitted % 2 == 0 { 1.0 } else { -1.0 } * determinant_3x3(minor);
4160 }
4161 let norm = vector.iter().map(|value| value * value).sum::<f64>().sqrt();
4162 (norm.is_finite() && norm > 1.0e-14).then(|| vector.map(|value| value / norm))
4163}
4164
4165fn solve_4x4(mut matrix: [[f64; 4]; 4], mut rhs: [f64; 4]) -> Option<[f64; 4]> {
4166 for pivot in 0..4 {
4167 let row = (pivot..4).max_by(|first, second| {
4168 matrix[*first][pivot]
4169 .abs()
4170 .total_cmp(&matrix[*second][pivot].abs())
4171 })?;
4172 if !matrix[row][pivot].is_finite() || matrix[row][pivot].abs() <= 1.0e-14 {
4173 return None;
4174 }
4175 matrix.swap(pivot, row);
4176 rhs.swap(pivot, row);
4177 let pivot_row = matrix[pivot];
4178 for row in pivot + 1..4 {
4179 let factor = matrix[row][pivot] / matrix[pivot][pivot];
4180 for (value, pivot_value) in matrix[row][pivot..].iter_mut().zip(&pivot_row[pivot..]) {
4181 *value -= factor * pivot_value;
4182 }
4183 rhs[row] -= factor * rhs[pivot];
4184 }
4185 }
4186 let mut solution = [0.0; 4];
4187 for row in (0..4).rev() {
4188 let known = (row + 1..4)
4189 .map(|column| matrix[row][column] * solution[column])
4190 .sum::<f64>();
4191 solution[row] = (rhs[row] - known) / matrix[row][row];
4192 }
4193 solution
4194 .iter()
4195 .all(|value| value.is_finite())
4196 .then_some(solution)
4197}
4198
4199fn least_squares_step(du: Vector3, dv: Vector3, residual: Vector3) -> Option<(f64, f64)> {
4200 let dot =
4201 |left: Vector3, right: Vector3| left.x * right.x + left.y * right.y + left.z * right.z;
4202 let du_squared = dot(du, du);
4203 let mixed = dot(du, dv);
4204 let dv_squared = dot(dv, dv);
4205 let determinant = du_squared * dv_squared - mixed * mixed;
4206 if !determinant.is_finite()
4207 || determinant.abs() <= f64::EPSILON * du_squared.max(dv_squared).powi(2)
4208 {
4209 return None;
4210 }
4211 let du_residual = dot(du, residual);
4212 let dv_residual = dot(dv, residual);
4213 Some((
4214 (dv_squared * du_residual - mixed * dv_residual) / determinant,
4215 (du_squared * dv_residual - mixed * du_residual) / determinant,
4216 ))
4217}
4218
4219pub(crate) fn nurbs_parameters(
4220 surface: &NurbsSurface,
4221 point: Point3,
4222 seed: Option<Point2>,
4223) -> Option<Point2> {
4224 let seed = seed.filter(|seed| seed.u.is_finite() && seed.v.is_finite());
4225 let u_degree = usize::try_from(surface.u_degree).ok()?;
4226 let v_degree = usize::try_from(surface.v_degree).ok()?;
4227 let u_count = usize::try_from(surface.u_count).ok()?;
4228 let v_count = usize::try_from(surface.v_count).ok()?;
4229 let u_domain = [
4230 *surface.u_knots.get(u_degree)?,
4231 *surface.u_knots.get(u_count)?,
4232 ];
4233 let v_domain = [
4234 *surface.v_knots.get(v_degree)?,
4235 *surface.v_knots.get(v_count)?,
4236 ];
4237 if u_domain[0] >= u_domain[1] || v_domain[0] >= v_domain[1] {
4238 return None;
4239 }
4240 let squared_distance = |candidate: Point3| point_distance(candidate, point).powi(2);
4241 let mut coarse = vec![None; 81];
4242 for ui in 0..=8 {
4243 for vi in 0..=8 {
4244 let ui_value = f64::from(u32::try_from(ui).ok()?);
4245 let vi_value = f64::from(u32::try_from(vi).ok()?);
4246 let parameters = Point2::new(
4247 u_domain[0] + (u_domain[1] - u_domain[0]) * ui_value / 8.0,
4248 v_domain[0] + (v_domain[1] - v_domain[0]) * vi_value / 8.0,
4249 );
4250 let Some(position) =
4251 cadmpeg_ir::eval::nurbs_surface_point(surface, parameters.u, parameters.v)
4252 else {
4253 continue;
4254 };
4255 coarse[ui * 9 + vi] = Some((parameters, squared_distance(position)));
4256 }
4257 }
4258 let mut starts = Vec::new();
4259 if let Some(seed) = seed {
4260 starts.push(seed);
4261 }
4262 for ui in 0..=8 {
4263 for vi in 0..=8 {
4264 let index = ui * 9 + vi;
4265 let Some((parameters, distance)) = coarse[index] else {
4266 continue;
4267 };
4268 let local_minimum = ui.saturating_sub(1)..=(ui + 1).min(8);
4269 if local_minimum
4270 .flat_map(|neighbor_u| {
4271 (vi.saturating_sub(1)..=(vi + 1).min(8))
4272 .map(move |neighbor_v| neighbor_u * 9 + neighbor_v)
4273 })
4274 .all(|neighbor| coarse[neighbor].is_none_or(|(_, value)| distance <= value))
4275 {
4276 starts.push(parameters);
4277 }
4278 }
4279 }
4280 let mut best = None;
4281 let mut best_distance = f64::INFINITY;
4282 let mut best_seed_distance = f64::INFINITY;
4283 for start in starts {
4284 let Some(parameters) = refine_nurbs_surface_parameters(
4285 surface,
4286 point,
4287 start,
4288 u_domain,
4289 v_domain,
4290 &squared_distance,
4291 ) else {
4292 continue;
4293 };
4294 let Some(position) =
4295 cadmpeg_ir::eval::nurbs_surface_point(surface, parameters.u, parameters.v)
4296 else {
4297 continue;
4298 };
4299 let distance = squared_distance(position);
4300 let seed_distance = seed.map_or(parameters.u.abs() + parameters.v.abs(), |seed| {
4301 (parameters.u - seed.u).hypot(parameters.v - seed.v)
4302 });
4303 let same_point = (distance - best_distance).abs()
4304 <= f64::EPSILON * 64.0 * distance.abs().max(best_distance.abs()).max(1.0);
4305 if distance < best_distance && !same_point
4306 || same_point && seed_distance < best_seed_distance
4307 {
4308 best = Some(parameters);
4309 best_distance = distance;
4310 best_seed_distance = seed_distance;
4311 }
4312 }
4313 best
4314}
4315
4316fn refine_nurbs_surface_parameters(
4317 surface: &NurbsSurface,
4318 point: Point3,
4319 mut parameters: Point2,
4320 u_domain: [f64; 2],
4321 v_domain: [f64; 2],
4322 squared_distance: &impl Fn(Point3) -> f64,
4323) -> Option<Point2> {
4324 parameters.u = parameters.u.clamp(u_domain[0], u_domain[1]);
4325 parameters.v = parameters.v.clamp(v_domain[0], v_domain[1]);
4326 for _ in 0..32 {
4327 let position = cadmpeg_ir::eval::nurbs_surface_point(surface, parameters.u, parameters.v)?;
4328 let residual = Vector3::new(
4329 position.x - point.x,
4330 position.y - point.y,
4331 position.z - point.z,
4332 );
4333 let partials = nurbs_surface_partials(surface, parameters.u, parameters.v)?;
4334 let (du, dv) = (partials.du, partials.dv);
4335 let dot =
4336 |left: Vector3, right: Vector3| left.x * right.x + left.y * right.y + left.z * right.z;
4337 let du_squared = dot(du, du);
4338 let mixed = dot(du, dv);
4339 let dv_squared = dot(dv, dv);
4340 let determinant = du_squared * dv_squared - mixed * mixed;
4341 if !determinant.is_finite()
4342 || determinant.abs() <= f64::EPSILON * du_squared.max(dv_squared).powi(2)
4343 {
4344 break;
4345 }
4346 let du_residual = dot(du, residual);
4347 let dv_residual = dot(dv, residual);
4348 let step = Point2::new(
4349 (dv_squared * du_residual - mixed * dv_residual) / determinant,
4350 (du_squared * dv_residual - mixed * du_residual) / determinant,
4351 );
4352 let current_distance = squared_distance(position);
4353 let mut scale = 1.0;
4354 let mut accepted = None;
4355 for _ in 0..16 {
4356 let candidate = Point2::new(
4357 (parameters.u - scale * step.u).clamp(u_domain[0], u_domain[1]),
4358 (parameters.v - scale * step.v).clamp(v_domain[0], v_domain[1]),
4359 );
4360 let candidate_position =
4361 cadmpeg_ir::eval::nurbs_surface_point(surface, candidate.u, candidate.v)?;
4362 if squared_distance(candidate_position) <= current_distance {
4363 accepted = Some(candidate);
4364 break;
4365 }
4366 scale *= 0.5;
4367 }
4368 let Some(candidate) = accepted else {
4369 break;
4370 };
4371 parameters = candidate;
4372 if scale * step.u.abs() <= 1.0e-12 * (1.0 + parameters.u.abs())
4373 && scale * step.v.abs() <= 1.0e-12 * (1.0 + parameters.v.abs())
4374 {
4375 break;
4376 }
4377 }
4378 Some(parameters)
4379}
4380
4381fn point_distance(first: Point3, second: Point3) -> f64 {
4382 ((first.x - second.x).powi(2) + (first.y - second.y).powi(2) + (first.z - second.z).powi(2))
4383 .sqrt()
4384}
4385
4386fn intersection_side(
4387 ir: &CadIr,
4388 surfaces_by_xmt: &BTreeMap<u32, SurfaceId>,
4389 surface_xmt: u32,
4390 uv: Option<(&[[f64; 2]], &[f64])>,
4391) -> IntcurveSupportSide {
4392 let surface = surfaces_by_xmt.get(&surface_xmt).cloned();
4393 let pcurve = surface.as_ref().and_then(|surface_id| {
4394 let geometry = ir
4395 .model
4396 .surfaces
4397 .iter()
4398 .find(|candidate| &candidate.id == surface_id)
4399 .map(|surface| &surface.geometry)?;
4400 let (uv, parameters) = uv?;
4401 let control_points = uv
4402 .iter()
4403 .map(|pair| surface_parameters(geometry, *pair))
4404 .collect::<Option<Vec<_>>>()?;
4405 Some(PcurveGeometry::Nurbs {
4406 degree: 1,
4407 knots: linear_knots(parameters),
4408 control_points,
4409 weights: None,
4410 periodic: false,
4411 })
4412 });
4413 IntcurveSupportSide {
4414 surface,
4415 pcurve,
4416 pcurve_parameter_range: None,
4417 }
4418}
4419
4420fn surface_parameters(surface: &SurfaceGeometry, uv: [f64; 2]) -> Option<Point2> {
4421 let point = match surface {
4422 SurfaceGeometry::Plane { .. } => Point2::new(uv[0] * 1000.0, uv[1] * 1000.0),
4423 SurfaceGeometry::Cylinder { .. } | SurfaceGeometry::Cone { .. } => {
4424 Point2::new(uv[0], uv[1] * 1000.0)
4425 }
4426 SurfaceGeometry::Sphere { .. }
4427 | SurfaceGeometry::Torus { .. }
4428 | SurfaceGeometry::Nurbs(_)
4429 | SurfaceGeometry::Polygonal { .. }
4430 | SurfaceGeometry::Procedural { .. }
4431 | SurfaceGeometry::Unknown { .. } => Point2::new(uv[0], uv[1]),
4432 SurfaceGeometry::Transformed { basis, .. } => return surface_parameters(basis, uv),
4433 };
4434 [point.u, point.v]
4435 .into_iter()
4436 .all(f64::is_finite)
4437 .then_some(point)
4438}
4439
4440fn normalize_pcurve_parameters(
4441 pcurve: &mut PcurveGeometry,
4442 surface: &SurfaceGeometry,
4443) -> Option<()> {
4444 match pcurve {
4445 PcurveGeometry::Line { origin, direction } => {
4446 let end = Point2::new(origin.u + direction.u, origin.v + direction.v);
4447 let converted_origin = surface_parameters(surface, [origin.u, origin.v])?;
4448 let converted_end = surface_parameters(surface, [end.u, end.v])?;
4449 *origin = converted_origin;
4450 *direction = Point2::new(
4451 converted_end.u - converted_origin.u,
4452 converted_end.v - converted_origin.v,
4453 );
4454 }
4455 PcurveGeometry::Nurbs { control_points, .. } => {
4456 let converted = control_points
4457 .iter()
4458 .map(|point| surface_parameters(surface, [point.u, point.v]))
4459 .collect::<Option<Vec<_>>>()?;
4460 *control_points = converted;
4461 }
4462 _ => {}
4463 }
4464 Some(())
4465}
4466
4467#[allow(clippy::too_many_arguments)]
4470fn emit_topology(
4471 ir: &mut CadIr,
4472 stream_index: usize,
4473 graph: &Graph,
4474 points: &BTreeMap<u32, PointId>,
4475 surfaces: &BTreeMap<u32, SurfaceId>,
4476 curves: &BTreeMap<u32, CurveId>,
4477 pcurves: &BTreeMap<u32, PcurveId>,
4478 pcurve_supports: &BTreeMap<u32, SurfaceId>,
4479 trim_ranges: &BTreeMap<u32, [f64; 2]>,
4480 source_stream: cadmpeg_ir::annotations::StreamHandle,
4481 annotations: &mut AnnotationBuilder,
4482) {
4483 let prefix = format!("nx:s{stream_index}");
4484 let body_shape_shells = graph.body_shape_shells();
4485 let valid_face_xmts: BTreeSet<u32> = body_shape_shells
4486 .iter()
4487 .filter_map(|shell| graph.shell_face_xmts(shell))
4488 .flatten()
4489 .collect();
4490 let valid_loop_rings: BTreeMap<u32, Vec<u32>> = valid_face_xmts
4491 .iter()
4492 .filter_map(|face_xmt| graph.face_loop_rings(*face_xmt))
4493 .flatten()
4494 .collect();
4495 let valid_fin_xmts: BTreeSet<u32> = valid_loop_rings
4496 .values()
4497 .flat_map(|ring| ring.iter().copied())
4498 .collect();
4499 let valid_edge_xmts: BTreeSet<u32> = valid_fin_xmts
4500 .iter()
4501 .filter_map(|xmt| graph.get(17, *xmt)?.fin_fields().map(|fields| fields.edge))
4502 .collect();
4503 let valid_vertex_xmts: BTreeSet<u32> = valid_fin_xmts
4504 .iter()
4505 .flat_map(|xmt| {
4506 let fields = graph.get(17, *xmt).and_then(Node::fin_fields);
4507 let partner_vertex = fields
4508 .filter(|fields| fields.other > 1)
4509 .and_then(|fields| graph.get(17, fields.other))
4510 .and_then(Node::fin_fields)
4511 .map(|fields| fields.vertex);
4512 [fields.map(|fields| fields.vertex), partner_vertex]
4513 .into_iter()
4514 .flatten()
4515 })
4516 .filter(|xmt| *xmt > 1)
4517 .collect();
4518 let body_xmts: BTreeSet<_> = body_shape_shells
4519 .iter()
4520 .filter_map(|shell| shell.shell_fields().map(|fields| fields.body))
4521 .collect();
4522 let mut bodies = BTreeMap::new();
4523 for body_xmt in body_xmts {
4524 let id = BodyId(format!("{prefix}:body#{body_xmt}"));
4525 if let Some(node) = graph.get(12, body_xmt) {
4526 annotate_node(annotations, &id, source_stream, node, "BODY");
4527 } else if let Some(shell) = body_shape_shells.iter().find(|shell| {
4528 shell
4529 .shell_fields()
4530 .is_some_and(|fields| fields.body == body_xmt)
4531 }) {
4532 annotations
4533 .note(&id, source_stream, shell.pos as u64)
4534 .tag("UNRESOLVED_BODY_REFERENCE");
4535 annotations.exactness(&id, Exactness::Unknown);
4536 }
4537 bodies.insert(body_xmt, id.clone());
4538 ir.model.bodies.push(Body {
4539 id,
4540 kind: cadmpeg_ir::topology::BodyKind::Solid,
4541 regions: Vec::new(),
4542 transform: None,
4543 name: None,
4544 color: None,
4545 visible: None,
4546 });
4547 }
4548
4549 let mut regions: BTreeMap<u32, (RegionId, BodyId)> = BTreeMap::new();
4550 let mut shells = BTreeMap::new();
4551 for node in body_shape_shells {
4552 let Some(fields) = node.shell_fields() else {
4553 continue;
4554 };
4555 let Some(body) = bodies.get(&fields.body).cloned() else {
4556 continue;
4557 };
4558 let region_id = if let Some((region, owner)) = regions.get(&fields.region) {
4559 if owner != &body {
4560 continue;
4561 }
4562 region.clone()
4563 } else {
4564 let region = RegionId(format!("{prefix}:region#{}", fields.region));
4565 if let Some(region_node) = graph.get(19, fields.region) {
4566 annotate_node(annotations, ®ion, source_stream, region_node, "REGION");
4567 } else {
4568 annotations
4569 .note(®ion, source_stream, node.pos as u64)
4570 .tag("UNRESOLVED_REGION_REFERENCE");
4571 annotations.exactness(®ion, Exactness::Unknown);
4572 }
4573 annotations.derived(®ion, "body");
4574 ir.model.regions.push(Region {
4575 id: region.clone(),
4576 body: body.clone(),
4577 shells: Vec::new(),
4578 });
4579 if let Some(parent) = ir
4580 .model
4581 .bodies
4582 .iter_mut()
4583 .find(|candidate| candidate.id == body)
4584 {
4585 parent.regions.push(region.clone());
4586 }
4587 regions.insert(fields.region, (region.clone(), body.clone()));
4588 region
4589 };
4590 let shell_id = ShellId(format!("{prefix}:shell#{}", node.xmt));
4591 annotate_node(annotations, &shell_id, source_stream, node, "SHELL");
4592 ir.model.shells.push(Shell {
4593 id: shell_id.clone(),
4594 region: region_id.clone(),
4595 faces: Vec::new(),
4596 wire_edges: Vec::new(),
4597 free_vertices: Vec::new(),
4598 });
4599 if let Some(parent) = ir
4600 .model
4601 .regions
4602 .iter_mut()
4603 .find(|candidate| candidate.id == region_id)
4604 {
4605 parent.shells.push(shell_id.clone());
4606 }
4607 shells.insert(node.xmt, shell_id);
4608 }
4609
4610 let mut vertices = BTreeMap::new();
4611 for node in graph
4612 .of_kind(18)
4613 .filter(|node| valid_vertex_xmts.contains(&node.xmt))
4614 {
4615 let Some(fields) = node.vertex_fields() else {
4616 continue;
4617 };
4618 let Some(point) = points.get(&fields.point).cloned() else {
4619 continue;
4620 };
4621 let tolerance = decoded_tolerance(fields.tolerance);
4622 let vertex = VertexId(format!("{prefix}:vertex#{}", node.xmt));
4623 annotate_node(annotations, &vertex, source_stream, node, "VERTEX");
4624 if tolerance.is_some() {
4625 annotations.derived(&vertex, "tolerance");
4626 }
4627 ir.model.vertices.push(Vertex {
4628 id: vertex.clone(),
4629 point,
4630 tolerance,
4631 });
4632 vertices.insert(node.xmt, vertex.clone());
4633 }
4634
4635 let mut edges = BTreeMap::new();
4636 for node in graph
4637 .of_kind(16)
4638 .filter(|node| valid_edge_xmts.contains(&node.xmt))
4639 {
4640 let Some(fields) = node.edge_fields() else {
4641 continue;
4642 };
4643 let Some(fin) = graph.get(17, fields.fin) else {
4644 continue;
4645 };
4646 let Some(fin_fields) = fin.fin_fields() else {
4647 continue;
4648 };
4649 let curve_xmt = [fields.curve, fin_fields.curve_xmt]
4650 .into_iter()
4651 .find(|xmt| *xmt > 1);
4652 let mut curve = curve_xmt.and_then(|xmt| curves.get(&xmt)).cloned();
4653 let mut param_range = curve_xmt.and_then(|xmt| trim_ranges.get(&xmt)).copied();
4654 if curve.is_none() {
4655 let lifted = curve_xmt
4656 .and_then(|xmt| pcurves.get(&xmt))
4657 .and_then(|pcurve_id| {
4658 let pcurve = ir
4659 .model
4660 .pcurves
4661 .iter()
4662 .find(|pcurve| &pcurve.id == pcurve_id)?;
4663 let surface = pcurve_supports.get(&curve_xmt?)?.clone();
4664 let parameter_range = pcurve
4665 .parameter_range
4666 .or(param_range)
4667 .or_else(|| pcurve_parameter_range(&pcurve.geometry))?;
4668 let parameter_range = ordered_parameter_range(parameter_range)?;
4669 Some((
4670 surface,
4671 pcurve.geometry.clone(),
4672 parameter_range,
4673 pcurve.fit_tolerance,
4674 ))
4675 });
4676 if let Some((surface, pcurve, parameter_range, _fit_tolerance)) = lifted {
4677 let carrier = CurveId(format!("{prefix}:edge-parametric-curve#{}", node.xmt));
4678 let construction = ProceduralCurveId(format!(
4679 "{prefix}:edge-parametric-construction#{}",
4680 node.xmt
4681 ));
4682 annotations
4683 .note(&carrier, source_stream, node.pos as u64)
4684 .tag("PARAMETRIC_SURFACE_CURVE");
4685 annotations.derived(&carrier, "geometry");
4686 ir.model.curves.push(Curve {
4687 id: carrier.clone(),
4688 geometry: CurveGeometry::Procedural {
4689 construction: construction.clone(),
4690 },
4691 source_object: None,
4692 });
4693 ir.model.procedural_curves.push(ProceduralCurve {
4694 id: construction,
4695 curve: carrier.clone(),
4696 definition: ProceduralCurveDefinition::SurfaceCurve {
4697 family: SurfaceCurveFamily::Parametric,
4698 context: IntcurveSupportContext {
4699 sides: [
4700 IntcurveSupportSide {
4701 surface: Some(surface),
4702 pcurve: Some(pcurve),
4703 pcurve_parameter_range: None,
4704 },
4705 IntcurveSupportSide {
4706 surface: None,
4707 pcurve: None,
4708 pcurve_parameter_range: None,
4709 },
4710 ],
4711 parameter_range,
4712 discontinuities: [Vec::new(), Vec::new(), Vec::new()],
4713 },
4714 tail: None,
4715 },
4716 cache_fit_tolerance: None,
4719 });
4720 curve = Some(carrier);
4721 param_range = None;
4722 }
4723 }
4724 let start = vertices.get(&fin_fields.vertex).cloned().or_else(|| {
4725 (fin_fields.vertex == 1
4726 && fin_fields.forward == fin.xmt
4727 && fin_fields.backward == fin.xmt)
4728 .then(|| {
4729 synthesize_closed_edge_vertex(
4730 ir,
4731 annotations,
4732 &prefix,
4733 node,
4734 curve.as_ref()?,
4735 param_range,
4736 source_stream,
4737 decoded_tolerance(fields.tolerance),
4738 )
4739 })
4740 .flatten()
4741 });
4742 let Some(start) = start else {
4743 continue;
4744 };
4745 let end_fin = if fin_fields.other > 1 {
4746 fin_fields.other
4747 } else {
4748 fin_fields.forward
4749 };
4750 let end = graph
4751 .get(17, end_fin)
4752 .and_then(Node::fin_fields)
4753 .and_then(|next| vertices.get(&next.vertex))
4754 .cloned()
4755 .unwrap_or_else(|| start.clone());
4756 let (mut start, mut end) = (start, end);
4757 let id = EdgeId(format!("{prefix}:edge#{}", node.xmt));
4758 annotate_node(annotations, &id, source_stream, node, "EDGE");
4759 if decoded_tolerance(fields.tolerance).is_some() {
4760 annotations.derived(&id, "tolerance");
4761 }
4762 if let (Some(carrier), Some(range)) = (&curve, param_range) {
4763 match orient_edge_range(
4764 ir,
4765 carrier,
4766 range,
4767 &start,
4768 &end,
4769 decoded_tolerance(fields.tolerance),
4770 ) {
4771 Some((oriented, reverse_edge)) => {
4772 param_range = Some(oriented);
4773 if reverse_edge {
4774 std::mem::swap(&mut start, &mut end);
4775 }
4776 }
4777 None => {
4778 param_range = None;
4779 }
4780 }
4781 }
4782 ir.model.edges.push(Edge {
4783 id: id.clone(),
4784 curve,
4785 start,
4786 end,
4787 param_range,
4788 tolerance: decoded_tolerance(fields.tolerance),
4789 });
4790 edges.insert(node.xmt, id);
4791 }
4792
4793 let mut faces = BTreeMap::new();
4794 for node in graph
4795 .of_kind(14)
4796 .filter(|node| valid_face_xmts.contains(&node.xmt))
4797 {
4798 let Some(fields) = node.face_fields() else {
4799 continue;
4800 };
4801 let Some(shell) = shells.get(&fields.shell).cloned() else {
4802 continue;
4803 };
4804 let Some(surface) = surfaces.get(&fields.surface).cloned() else {
4805 continue;
4806 };
4807 let id = FaceId(format!("{prefix}:face#{}", node.xmt));
4808 annotate_node(annotations, &id, source_stream, node, "FACE");
4809 if decoded_tolerance(fields.tolerance).is_some() {
4810 annotations.derived(&id, "tolerance");
4811 }
4812 ir.model.faces.push(Face {
4813 id: id.clone(),
4814 shell: shell.clone(),
4815 surface,
4816 sense: sense(Some(fields.sense)),
4817 loops: Vec::new(),
4818 name: None,
4819 color: None,
4820 tolerance: decoded_tolerance(fields.tolerance),
4821 });
4822 if let Some(parent) = ir
4823 .model
4824 .shells
4825 .iter_mut()
4826 .find(|candidate| candidate.id == shell)
4827 {
4828 parent.faces.push(id.clone());
4829 }
4830 faces.insert(node.xmt, id);
4831 }
4832
4833 let mut loops = BTreeMap::new();
4834 for &loop_xmt in valid_loop_rings.keys() {
4835 let ring_resolves = valid_loop_rings[&loop_xmt].iter().all(|fin_xmt| {
4836 graph
4837 .get(17, *fin_xmt)
4838 .and_then(Node::fin_fields)
4839 .is_some_and(|fields| edges.contains_key(&fields.edge))
4840 });
4841 if !ring_resolves {
4842 continue;
4843 }
4844 let Some(node) = graph.get(15, loop_xmt) else {
4845 continue;
4846 };
4847 let Some(fields) = node.loop_fields() else {
4848 continue;
4849 };
4850 let Some(face) = faces.get(&fields.face).cloned() else {
4851 continue;
4852 };
4853 let id = LoopId(format!("{prefix}:loop#{}", node.xmt));
4854 annotate_node(annotations, &id, source_stream, node, "LOOP");
4855 ir.model.loops.push(Loop {
4856 id: id.clone(),
4857 face: face.clone(),
4858 boundary_role: cadmpeg_ir::topology::LoopBoundaryRole::Unspecified,
4859 coedges: Vec::new(),
4860 vertex_uses: Vec::new(),
4861 });
4862 if let Some(parent) = ir
4863 .model
4864 .faces
4865 .iter_mut()
4866 .find(|candidate| candidate.id == face)
4867 {
4868 parent.loops.push(id.clone());
4869 }
4870 loops.insert(node.xmt, id);
4871 }
4872
4873 let fin_ids: BTreeMap<u32, CoedgeId> = valid_fin_xmts
4874 .iter()
4875 .filter(|xmt| {
4876 graph
4877 .get(17, **xmt)
4878 .and_then(Node::fin_fields)
4879 .is_some_and(|fields| loops.contains_key(&fields.loop_xmt))
4880 })
4881 .map(|xmt| (*xmt, CoedgeId(format!("{prefix}:fin#{xmt}"))))
4882 .collect();
4883 let intersection_pcurves: BTreeMap<_, _> = ir
4884 .model
4885 .procedural_curves
4886 .iter()
4887 .filter_map(|procedural| {
4888 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition
4889 else {
4890 return None;
4891 };
4892 Some(context.sides.iter().filter_map(move |side| {
4893 Some((
4894 (procedural.curve.clone(), side.surface.clone()?),
4895 (
4896 side.pcurve.clone()?,
4897 context.parameter_range,
4898 procedural.cache_fit_tolerance,
4899 ),
4900 ))
4901 }))
4902 })
4903 .flatten()
4904 .collect();
4905 for &fin_xmt in fin_ids.keys() {
4906 let Some(node) = graph.get(17, fin_xmt) else {
4907 continue;
4908 };
4909 let Some(fields) = node.fin_fields() else {
4910 continue;
4911 };
4912 let Some(loop_id) = loops.get(&fields.loop_xmt).cloned() else {
4913 continue;
4914 };
4915 let Some(edge) = edges.get(&fields.edge).cloned() else {
4916 continue;
4917 };
4918 let id = fin_ids.get(&node.xmt).cloned().expect("filtered above");
4919 annotate_node(annotations, &id, source_stream, node, "FIN");
4920 let next = fin_ids
4921 .get(&fields.forward)
4922 .cloned()
4923 .expect("validated FIN ring resolves forward link");
4924 let previous = fin_ids
4925 .get(&fields.backward)
4926 .cloned()
4927 .expect("validated FIN ring resolves backward link");
4928 let partner = fin_ids.get(&fields.other).cloned();
4929 let radial_next = partner.clone().unwrap_or_else(|| id.clone());
4930 let support = graph
4931 .get(15, fields.loop_xmt)
4932 .and_then(Node::loop_fields)
4933 .and_then(|loop_| graph.get(14, loop_.face))
4934 .and_then(Node::face_fields)
4935 .and_then(|face| surfaces.get(&face.surface))
4936 .cloned();
4937 let mut pcurve = pcurves.get(&fields.curve_xmt).cloned().filter(|id| {
4938 let Some((carrier, support)) = ir
4939 .model
4940 .pcurves
4941 .iter()
4942 .find(|carrier| &carrier.id == id)
4943 .zip(support.as_ref())
4944 else {
4945 return false;
4946 };
4947 pcurve_matches_edge_range(
4948 ir,
4949 &edge,
4950 support,
4951 &carrier.geometry,
4952 carrier.parameter_range,
4953 carrier.fit_tolerance,
4954 )
4955 });
4956 if pcurve.is_none() {
4957 let carrier = ir
4958 .model
4959 .edges
4960 .iter()
4961 .find(|candidate| candidate.id == edge)
4962 .and_then(|edge| edge.curve.clone());
4963 if let Some((_support, geometry, parameter_range, fit_tolerance)) = carrier
4964 .zip(support)
4965 .and_then(|key| {
4966 intersection_pcurves
4967 .get(&key)
4968 .cloned()
4969 .map(|value| (key.1, value.0, value.1, value.2))
4970 })
4971 .filter(|(support, geometry, _, fit_tolerance)| {
4972 pcurve_matches_edge(ir, &edge, support, geometry, *fit_tolerance)
4973 })
4974 {
4975 let pcurve_id = PcurveId(format!("{prefix}:intersection-pcurve#{fin_xmt}"));
4976 annotations
4977 .note(&pcurve_id, source_stream, node.pos as u64)
4978 .tag("INTERSECTION_PCURVE");
4979 annotations.derived(&pcurve_id, "geometry");
4980 annotations.derived(&pcurve_id, "parameter_range");
4981 if fit_tolerance.is_some() {
4982 annotations.derived(&pcurve_id, "fit_tolerance");
4983 }
4984 ir.model.pcurves.push(Pcurve {
4985 id: pcurve_id.clone(),
4986 geometry,
4987 wrapper_reversed: None,
4988 native_tail_flags: None,
4989 parameter_range: Some(parameter_range),
4990 fit_tolerance,
4991 });
4992 pcurve = Some(pcurve_id);
4993 }
4994 }
4995 ir.model.coedges.push(Coedge {
4996 id: id.clone(),
4997 owner_loop: loop_id.clone(),
4998 edge,
4999 next,
5000 previous,
5001 radial_next,
5002 sense: sense(Some(fields.sense)),
5003 pcurves: pcurve
5004 .into_iter()
5005 .map(|pcurve| cadmpeg_ir::topology::PcurveUse {
5006 pcurve,
5007 isoparametric: None,
5008 parameter_range: None,
5009 })
5010 .collect(),
5011 use_curve: None,
5012 use_curve_parameter_range: None,
5013 });
5014 if let Some(parent) = ir
5015 .model
5016 .loops
5017 .iter_mut()
5018 .find(|candidate| candidate.id == loop_id)
5019 {
5020 parent.coedges.push(id);
5021 }
5022 }
5023
5024 attach_tolerant_edge_intersections(ir, graph, &edges, &prefix, source_stream, annotations);
5025 complete_intersection_supports_from_edge_incidence(ir);
5026 complete_intersection_pcurves_from_coedge_incidence(ir);
5027 complete_isoparametric_intersection_pcurves(ir);
5028 complete_intersection_pcurves_from_opposite_charts(ir);
5029
5030 let owned_edges: BTreeSet<_> = ir
5031 .model
5032 .coedges
5033 .iter()
5034 .map(|coedge| coedge.edge.clone())
5035 .collect();
5036 let candidate_edges: BTreeSet<_> = edges.into_values().collect();
5037 ir.model
5038 .edges
5039 .retain(|edge| !candidate_edges.contains(&edge.id) || owned_edges.contains(&edge.id));
5040 let retained_vertices: BTreeSet<_> = ir
5041 .model
5042 .edges
5043 .iter()
5044 .flat_map(|edge| [edge.start.clone(), edge.end.clone()])
5045 .collect();
5046 ir.model.vertices.retain(|vertex| {
5047 !vertex.id.0.starts_with(&prefix) || retained_vertices.contains(&vertex.id)
5048 });
5049}
5050
5051fn pcurve_parameter_range(geometry: &PcurveGeometry) -> Option<[f64; 2]> {
5052 let PcurveGeometry::Nurbs { knots, .. } = geometry else {
5053 return None;
5054 };
5055 ordered_parameter_range([*knots.first()?, *knots.last()?])
5056}
5057
5058fn ordered_parameter_range(mut range: [f64; 2]) -> Option<[f64; 2]> {
5059 if !range.iter().all(|value| value.is_finite()) || range[0] == range[1] {
5060 return None;
5061 }
5062 if range[0] > range[1] {
5063 range.swap(0, 1);
5064 }
5065 Some(range)
5066}
5067
5068pub(crate) fn complete_intersection_supports_from_edge_incidence(ir: &mut CadIr) {
5069 let loop_faces = ir
5070 .model
5071 .loops
5072 .iter()
5073 .map(|loop_| (loop_.id.clone(), loop_.face.clone()))
5074 .collect::<BTreeMap<_, _>>();
5075 let face_surfaces = ir
5076 .model
5077 .faces
5078 .iter()
5079 .map(|face| (face.id.clone(), face.surface.clone()))
5080 .collect::<BTreeMap<_, _>>();
5081 let edge_curves = ir
5082 .model
5083 .edges
5084 .iter()
5085 .filter_map(|edge| Some((edge.id.clone(), edge.curve.clone()?)))
5086 .collect::<BTreeMap<_, _>>();
5087 let mut incident_surfaces = BTreeMap::<CurveId, Vec<SurfaceId>>::new();
5088 for coedge in &ir.model.coedges {
5089 let Some(curve) = edge_curves.get(&coedge.edge) else {
5090 continue;
5091 };
5092 let Some(surface) = loop_faces
5093 .get(&coedge.owner_loop)
5094 .and_then(|face| face_surfaces.get(face))
5095 else {
5096 continue;
5097 };
5098 let surfaces = incident_surfaces.entry(curve.clone()).or_default();
5099 if !surfaces.contains(surface) {
5100 surfaces.push(surface.clone());
5101 }
5102 }
5103
5104 for procedural in &mut ir.model.procedural_curves {
5105 let ProceduralCurveDefinition::Intersection { context, .. } = &mut procedural.definition
5106 else {
5107 continue;
5108 };
5109 let missing = context
5110 .sides
5111 .iter()
5112 .enumerate()
5113 .filter_map(|(index, side)| side.surface.is_none().then_some(index))
5114 .collect::<Vec<_>>();
5115 if missing.len() != 1 {
5116 continue;
5117 }
5118 let Some(incident) = incident_surfaces.get(&procedural.curve) else {
5119 continue;
5120 };
5121 let candidates = incident
5122 .iter()
5123 .filter(|surface| {
5124 !context
5125 .sides
5126 .iter()
5127 .any(|side| side.surface.as_ref() == Some(surface))
5128 })
5129 .collect::<Vec<_>>();
5130 let [surface] = candidates.as_slice() else {
5131 continue;
5132 };
5133 context.sides[missing[0]].surface = Some((*surface).clone());
5134 }
5135}
5136
5137pub(crate) fn complete_intersection_pcurves_from_coedge_incidence(ir: &mut CadIr) {
5138 let loop_faces = ir
5139 .model
5140 .loops
5141 .iter()
5142 .map(|loop_| (loop_.id.clone(), loop_.face.clone()))
5143 .collect::<BTreeMap<_, _>>();
5144 let face_surfaces = ir
5145 .model
5146 .faces
5147 .iter()
5148 .map(|face| (face.id.clone(), face.surface.clone()))
5149 .collect::<BTreeMap<_, _>>();
5150 let edge_curves = ir
5151 .model
5152 .edges
5153 .iter()
5154 .filter_map(|edge| Some((edge.id.clone(), edge.curve.clone()?)))
5155 .collect::<BTreeMap<_, _>>();
5156 let mut incident_pcurves = BTreeMap::<(CurveId, SurfaceId), Vec<PcurveId>>::new();
5157 for coedge in &ir.model.coedges {
5158 let Some(curve) = edge_curves.get(&coedge.edge) else {
5159 continue;
5160 };
5161 let Some(surface) = loop_faces
5162 .get(&coedge.owner_loop)
5163 .and_then(|face| face_surfaces.get(face))
5164 else {
5165 continue;
5166 };
5167 let pcurves = incident_pcurves
5168 .entry((curve.clone(), surface.clone()))
5169 .or_default();
5170 for pcurve in &coedge.pcurves {
5171 if !pcurves.contains(&pcurve.pcurve) {
5172 pcurves.push(pcurve.pcurve.clone());
5173 }
5174 }
5175 }
5176
5177 for procedural in &mut ir.model.procedural_curves {
5178 let ProceduralCurveDefinition::Intersection { context, .. } = &mut procedural.definition
5179 else {
5180 continue;
5181 };
5182 for side in &mut context.sides {
5183 if side.pcurve.is_some() {
5184 continue;
5185 }
5186 let Some(surface) = &side.surface else {
5187 continue;
5188 };
5189 let Some([pcurve]) = incident_pcurves
5190 .get(&(procedural.curve.clone(), surface.clone()))
5191 .map(Vec::as_slice)
5192 else {
5193 continue;
5194 };
5195 let Some(carrier) = ir
5196 .model
5197 .pcurves
5198 .iter()
5199 .find(|carrier| &carrier.id == pcurve)
5200 else {
5201 continue;
5202 };
5203 side.pcurve = Some(carrier.geometry.clone());
5204 }
5205 }
5206}
5207
5208pub(crate) fn complete_intersection_pcurves_from_opposite_charts(ir: &mut CadIr) {
5209 let edge_tolerances = ir
5210 .model
5211 .edges
5212 .iter()
5213 .filter_map(|edge| {
5214 Some((
5215 edge.curve.clone()?,
5216 edge.tolerance
5217 .filter(|value| value.is_finite() && *value >= 0.0)?,
5218 ))
5219 })
5220 .fold(
5221 BTreeMap::<CurveId, f64>::new(),
5222 |mut values, (curve, tolerance)| {
5223 values
5224 .entry(curve)
5225 .and_modify(|current| *current = current.min(tolerance))
5226 .or_insert(tolerance);
5227 values
5228 },
5229 );
5230 let replacements = ir
5231 .model
5232 .procedural_curves
5233 .iter()
5234 .filter_map(|procedural| {
5235 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition
5236 else {
5237 return None;
5238 };
5239 let missing = context
5240 .sides
5241 .each_ref()
5242 .map(|side| pcurve_requires_completion(side.pcurve.as_ref()));
5243 let target = match missing {
5244 [true, false] => 0,
5245 [false, true] => 1,
5246 _ => return None,
5247 };
5248 let source = 1 - target;
5249 let source_surface = context.sides[source].surface.as_ref()?;
5250 let source_pcurve = context.sides[source].pcurve.as_ref()?;
5251 let target_surface = context.sides[target].surface.as_ref()?;
5252 let tolerance = procedural
5253 .cache_fit_tolerance
5254 .or_else(|| edge_tolerances.get(&procedural.curve).copied())?;
5255 let tolerance = blend_spine_cache_fit_tolerance(ir, target_surface, tolerance);
5256 let pcurve = transfer_intersection_pcurve(
5257 ir,
5258 &procedural.curve,
5259 source_surface,
5260 source_pcurve,
5261 target_surface,
5262 context.parameter_range,
5263 tolerance,
5264 )?;
5265 Some((procedural.id.clone(), target, pcurve, tolerance))
5266 })
5267 .collect::<Vec<_>>();
5268 for (procedural_id, side, pcurve, tolerance) in replacements {
5269 let Some(procedural) = ir
5270 .model
5271 .procedural_curves
5272 .iter_mut()
5273 .find(|procedural| procedural.id == procedural_id)
5274 else {
5275 continue;
5276 };
5277 let ProceduralCurveDefinition::Intersection { context, .. } = &mut procedural.definition
5278 else {
5279 continue;
5280 };
5281 if pcurve_requires_completion(context.sides[side].pcurve.as_ref()) {
5282 context.sides[side].pcurve = Some(pcurve);
5283 procedural.cache_fit_tolerance =
5284 Some(procedural.cache_fit_tolerance.unwrap_or(0.0).max(tolerance));
5285 }
5286 }
5287}
5288
5289pub(crate) fn complete_isoparametric_intersection_pcurves(ir: &mut CadIr) {
5290 let vertex_points = ir
5291 .model
5292 .vertices
5293 .iter()
5294 .filter_map(|vertex| {
5295 let point = ir
5296 .model
5297 .points
5298 .iter()
5299 .find(|point| point.id == vertex.point)?;
5300 Some((vertex.id.clone(), point.position))
5301 })
5302 .collect::<BTreeMap<_, _>>();
5303 let replacements = ir
5304 .model
5305 .procedural_curves
5306 .iter()
5307 .filter_map(|procedural| {
5308 let ProceduralCurveDefinition::Intersection { context, .. } = &procedural.definition
5309 else {
5310 return None;
5311 };
5312 if !context
5313 .sides
5314 .iter()
5315 .all(|side| pcurve_requires_completion(side.pcurve.as_ref()))
5316 {
5317 return None;
5318 }
5319 let [Some(first_surface), Some(second_surface)] =
5320 context.sides.each_ref().map(|side| side.surface.as_ref())
5321 else {
5322 return None;
5323 };
5324 let edges = ir
5325 .model
5326 .edges
5327 .iter()
5328 .filter(|edge| edge.curve.as_ref() == Some(&procedural.curve))
5329 .collect::<Vec<_>>();
5330 let [edge] = edges.as_slice() else {
5331 return None;
5332 };
5333 let tolerance = edge
5334 .tolerance
5335 .filter(|value| value.is_finite() && *value >= 0.0)?;
5336 let endpoints = [
5337 *vertex_points.get(&edge.start)?,
5338 *vertex_points.get(&edge.end)?,
5339 ];
5340 let candidates = [first_surface, second_surface].map(|surface| {
5341 isoparametric_boundary_pcurve(
5342 ir,
5343 surface,
5344 endpoints,
5345 context.parameter_range,
5346 tolerance,
5347 )
5348 });
5349 let pcurves = match candidates {
5350 [Some(first), Some(second)] => coincident_pcurve_pair(
5351 ir,
5352 [first_surface, second_surface],
5353 [&first, &second],
5354 context.parameter_range,
5355 tolerance,
5356 )
5357 .then_some([first, second])?,
5358 [Some(first), None] => [
5359 first.clone(),
5360 transfer_intersection_pcurve(
5361 ir,
5362 &procedural.curve,
5363 first_surface,
5364 &first,
5365 second_surface,
5366 context.parameter_range,
5367 tolerance,
5368 )?,
5369 ],
5370 [None, Some(second)] => [
5371 transfer_intersection_pcurve(
5372 ir,
5373 &procedural.curve,
5374 second_surface,
5375 &second,
5376 first_surface,
5377 context.parameter_range,
5378 tolerance,
5379 )?,
5380 second,
5381 ],
5382 [None, None] => return None,
5383 };
5384 Some((procedural.id.clone(), pcurves, tolerance))
5385 })
5386 .collect::<Vec<_>>();
5387 for (procedural_id, pcurves, tolerance) in replacements {
5388 let Some(procedural) = ir
5389 .model
5390 .procedural_curves
5391 .iter_mut()
5392 .find(|procedural| procedural.id == procedural_id)
5393 else {
5394 continue;
5395 };
5396 let ProceduralCurveDefinition::Intersection { context, .. } = &mut procedural.definition
5397 else {
5398 continue;
5399 };
5400 if context
5401 .sides
5402 .iter()
5403 .all(|side| pcurve_requires_completion(side.pcurve.as_ref()))
5404 {
5405 for (side, pcurve) in context.sides.iter_mut().zip(pcurves) {
5406 side.pcurve = Some(pcurve);
5407 }
5408 procedural.cache_fit_tolerance = Some(tolerance);
5409 }
5410 }
5411}
5412
5413fn isoparametric_boundary_pcurve(
5414 ir: &CadIr,
5415 surface: &SurfaceId,
5416 endpoints: [Point3; 2],
5417 range: [f64; 2],
5418 tolerance: f64,
5419) -> Option<PcurveGeometry> {
5420 (range[0].is_finite() && range[1].is_finite() && range[0] < range[1]).then_some(())?;
5421 let carrier = ir
5422 .model
5423 .surfaces
5424 .iter()
5425 .find(|candidate| &candidate.id == surface)?;
5426 let SurfaceGeometry::Nurbs(nurbs) = &carrier.geometry else {
5427 return None;
5428 };
5429 let domain = surface_parameter_domain(ir, surface)?;
5430 let parameters = [
5431 nurbs_parameters(nurbs, endpoints[0], None)?,
5432 nurbs_parameters(nurbs, endpoints[1], None)?,
5433 ];
5434 for index in 0..2 {
5435 let point =
5436 cadmpeg_ir::eval::nurbs_surface_point(nurbs, parameters[index].u, parameters[index].v)?;
5437 if point_distance(point, endpoints[index]) > tolerance {
5438 return None;
5439 }
5440 }
5441 let axes = [
5442 ([parameters[0].u, parameters[1].u], domain.0),
5443 ([parameters[0].v, parameters[1].v], domain.1),
5444 ];
5445 let candidates = axes
5446 .into_iter()
5447 .enumerate()
5448 .filter_map(|(constant_axis, (values, axis_domain))| {
5449 let scale = (axis_domain[1] - axis_domain[0]).abs().max(1.0);
5450 let parameter_tolerance = 1.0e-8 * scale;
5451 let boundary = axis_domain.into_iter().find(|boundary| {
5452 values
5453 .iter()
5454 .all(|value| (*value - *boundary).abs() <= parameter_tolerance)
5455 })?;
5456 let varying = if constant_axis == 0 {
5457 [parameters[0].v, parameters[1].v]
5458 } else {
5459 [parameters[0].u, parameters[1].u]
5460 };
5461 ((varying[1] - varying[0]).abs() > parameter_tolerance).then(|| {
5462 let delta = (varying[1] - varying[0]) / (range[1] - range[0]);
5463 let (origin, direction) = if constant_axis == 0 {
5464 (
5465 Point2::new(boundary, varying[0] - delta * range[0]),
5466 Point2::new(0.0, delta),
5467 )
5468 } else {
5469 (
5470 Point2::new(varying[0] - delta * range[0], boundary),
5471 Point2::new(delta, 0.0),
5472 )
5473 };
5474 PcurveGeometry::Line { origin, direction }
5475 })
5476 })
5477 .collect::<Vec<_>>();
5478 let [candidate] = candidates.as_slice() else {
5479 return None;
5480 };
5481 Some(candidate.clone())
5482}
5483
5484fn coincident_pcurve_pair(
5485 ir: &CadIr,
5486 surfaces: [&SurfaceId; 2],
5487 pcurves: [&PcurveGeometry; 2],
5488 range: [f64; 2],
5489 tolerance: f64,
5490) -> bool {
5491 (0..=32).all(|index| {
5492 let fraction = f64::from(index) / 32.0;
5493 let parameter = range[0] + fraction * (range[1] - range[0]);
5494 let points = [0usize, 1usize].map(|side| {
5495 let uv = pcurve_uv(pcurves[side], parameter)?;
5496 decoded_surface_point(ir, surfaces[side], uv.u, uv.v)
5497 });
5498 matches!(points, [Some(first), Some(second)] if point_distance(first, second) <= tolerance)
5499 })
5500}
5501
5502fn transfer_intersection_pcurve(
5503 ir: &CadIr,
5504 curve: &CurveId,
5505 source_surface: &SurfaceId,
5506 source_pcurve: &PcurveGeometry,
5507 target_surface: &SurfaceId,
5508 parameter_range: [f64; 2],
5509 tolerance: f64,
5510) -> Option<PcurveGeometry> {
5511 (parameter_range[0].is_finite()
5512 && parameter_range[1].is_finite()
5513 && parameter_range[0] < parameter_range[1]
5514 && tolerance.is_finite()
5515 && tolerance >= 0.0)
5516 .then_some(())?;
5517 let first = transferred_pcurve_sample(
5518 ir,
5519 curve,
5520 source_surface,
5521 source_pcurve,
5522 target_surface,
5523 parameter_range[0],
5524 None,
5525 tolerance,
5526 )?;
5527 let last = transferred_pcurve_sample(
5528 ir,
5529 curve,
5530 source_surface,
5531 source_pcurve,
5532 target_surface,
5533 parameter_range[1],
5534 Some(first.1),
5535 tolerance,
5536 )?;
5537 let mut samples = vec![first];
5538 append_transferred_pcurve_segment(
5539 ir,
5540 curve,
5541 source_surface,
5542 source_pcurve,
5543 target_surface,
5544 first,
5545 last,
5546 tolerance,
5547 0,
5548 &mut samples,
5549 )?;
5550 Some(PcurveGeometry::Nurbs {
5551 degree: 1,
5552 knots: linear_knots(&samples.iter().map(|sample| sample.0).collect::<Vec<_>>()),
5553 control_points: samples.iter().map(|sample| sample.1).collect(),
5554 weights: None,
5555 periodic: false,
5556 })
5557}
5558
5559type TransferredPcurveSample = (f64, Point2, Point3);
5560
5561#[allow(clippy::too_many_arguments)]
5562fn transferred_pcurve_sample(
5563 ir: &CadIr,
5564 curve: &CurveId,
5565 source_surface: &SurfaceId,
5566 source_pcurve: &PcurveGeometry,
5567 target_surface: &SurfaceId,
5568 parameter: f64,
5569 seed: Option<Point2>,
5570 tolerance: f64,
5571) -> Option<TransferredPcurveSample> {
5572 let source_uv = pcurve_uv(source_pcurve, parameter)?;
5573 let point = decoded_surface_point(ir, source_surface, source_uv.u, source_uv.v)
5574 .or_else(|| model_curve_point(ir, curve, parameter))?;
5575 let target_uv = blend_boundary_parameter_from_support_pcurve(
5576 ir,
5577 target_surface,
5578 source_surface,
5579 source_pcurve,
5580 parameter,
5581 point,
5582 tolerance,
5583 )
5584 .or_else(|| {
5585 blend_boundary_parameter_from_support_spine(
5586 ir,
5587 target_surface,
5588 source_surface,
5589 point,
5590 seed,
5591 tolerance,
5592 )
5593 })
5594 .or_else(|| surface_parameters_for_fit(ir, target_surface, point, seed, tolerance))?;
5595 (decoded_surface_point(ir, target_surface, target_uv.u, target_uv.v)
5596 .is_some_and(|candidate| point_distance(candidate, point) <= tolerance)
5597 || blend_boundary_spine_geometry_matches(ir, target_surface, target_uv, point, tolerance))
5598 .then_some((parameter, target_uv, point))
5599}
5600
5601pub(crate) fn blend_boundary_parameter_from_support_spine(
5602 ir: &CadIr,
5603 blend: &SurfaceId,
5604 support: &SurfaceId,
5605 point: Point3,
5606 seed: Option<Point2>,
5607 tolerance: f64,
5608) -> Option<Point2> {
5609 let (supports, spine, _, _) = blend_surface_definition(ir, blend)?;
5610 let matches = supports
5611 .iter()
5612 .enumerate()
5613 .filter(|(_, candidate)| parameterization_equivalent_surfaces(ir, candidate, support))
5614 .map(|(boundary, _)| boundary)
5615 .collect::<Vec<_>>();
5616 let [boundary] = matches.as_slice() else {
5617 return None;
5618 };
5619 let parameter = closest_spine_parameter(ir, &spine, point, seed.map(|seed| seed.u))?;
5620 let parameters = Point2::new(parameter, *boundary as f64);
5621 (blend_surface_point_inner(ir, blend, parameters.u, parameters.v, 0)
5622 .is_some_and(|candidate| point_distance(candidate, point) <= tolerance)
5623 || blend_boundary_spine_geometry_matches(ir, blend, parameters, point, tolerance))
5624 .then_some(parameters)
5625}
5626
5627fn blend_boundary_spine_geometry_matches(
5628 ir: &CadIr,
5629 blend: &SurfaceId,
5630 parameters: Point2,
5631 point: Point3,
5632 tolerance: f64,
5633) -> bool {
5634 if parameters.v.to_bits() != 0.0f64.to_bits() && parameters.v.to_bits() != 1.0f64.to_bits() {
5635 return false;
5636 }
5637 let Some((_, spine, radius, _)) = blend_surface_definition(ir, blend) else {
5638 return false;
5639 };
5640 let Some(center) = model_curve_point(ir, &spine, parameters.u) else {
5641 return false;
5642 };
5643 let radial = Vector3::new(point.x - center.x, point.y - center.y, point.z - center.z);
5644 let distance = (radial.x * radial.x + radial.y * radial.y + radial.z * radial.z).sqrt();
5645 if !distance.is_finite() || (distance - radius).abs() > tolerance {
5646 return false;
5647 }
5648 let Some(radial) = unit_vector(radial) else {
5649 return false;
5650 };
5651 let Some(tangent) = model_curve_tangent(ir, &spine, parameters.u) else {
5652 return false;
5653 };
5654 let angular_tolerance = (tolerance / radius).max(1.0e-8);
5655 (radial.x * tangent.x + radial.y * tangent.y + radial.z * tangent.z).abs() <= angular_tolerance
5656}
5657
5658#[allow(clippy::too_many_arguments)]
5659fn append_transferred_pcurve_segment(
5660 ir: &CadIr,
5661 curve: &CurveId,
5662 source_surface: &SurfaceId,
5663 source_pcurve: &PcurveGeometry,
5664 target_surface: &SurfaceId,
5665 first: TransferredPcurveSample,
5666 last: TransferredPcurveSample,
5667 tolerance: f64,
5668 depth: usize,
5669 samples: &mut Vec<TransferredPcurveSample>,
5670) -> Option<()> {
5671 let midpoint_parameter = f64::midpoint(first.0, last.0);
5672 let midpoint_seed = Point2::new(
5673 f64::midpoint(first.1.u, last.1.u),
5674 f64::midpoint(first.1.v, last.1.v),
5675 );
5676 let midpoint = transferred_pcurve_sample(
5677 ir,
5678 curve,
5679 source_surface,
5680 source_pcurve,
5681 target_surface,
5682 midpoint_parameter,
5683 Some(midpoint_seed),
5684 tolerance,
5685 )?;
5686 let fits = [0.25, 0.5, 0.75].into_iter().all(|fraction| {
5687 let parameter = first.0 + fraction * (last.0 - first.0);
5688 let uv = Point2::new(
5689 first.1.u + fraction * (last.1.u - first.1.u),
5690 first.1.v + fraction * (last.1.v - first.1.v),
5691 );
5692 let Some(source_uv) = pcurve_uv(source_pcurve, parameter) else {
5693 return false;
5694 };
5695 let Some(source_point) =
5696 decoded_surface_point(ir, source_surface, source_uv.u, source_uv.v)
5697 .or_else(|| model_curve_point(ir, curve, parameter))
5698 else {
5699 return false;
5700 };
5701 decoded_surface_point(ir, target_surface, uv.u, uv.v)
5702 .is_some_and(|target_point| point_distance(source_point, target_point) <= tolerance)
5703 || blend_boundary_spine_geometry_matches(
5704 ir,
5705 target_surface,
5706 uv,
5707 source_point,
5708 tolerance,
5709 )
5710 });
5711 if fits {
5712 samples.push(last);
5713 return Some(());
5714 }
5715 (depth < 16).then_some(())?;
5716 append_transferred_pcurve_segment(
5717 ir,
5718 curve,
5719 source_surface,
5720 source_pcurve,
5721 target_surface,
5722 first,
5723 midpoint,
5724 tolerance,
5725 depth + 1,
5726 samples,
5727 )?;
5728 append_transferred_pcurve_segment(
5729 ir,
5730 curve,
5731 source_surface,
5732 source_pcurve,
5733 target_surface,
5734 midpoint,
5735 last,
5736 tolerance,
5737 depth + 1,
5738 samples,
5739 )
5740}
5741
5742fn surface_parameters_for_fit(
5743 ir: &CadIr,
5744 surface: &SurfaceId,
5745 point: Point3,
5746 seed: Option<Point2>,
5747 tolerance: f64,
5748) -> Option<Point2> {
5749 let carrier = ir
5750 .model
5751 .surfaces
5752 .iter()
5753 .find(|candidate| &candidate.id == surface)?;
5754 match &carrier.geometry {
5755 SurfaceGeometry::Nurbs(nurbs) => nurbs_parameters(nurbs, point, seed),
5756 SurfaceGeometry::Procedural { .. } => {
5757 offset_surface_parameters_with_tolerance(ir, surface, point, seed, Some(tolerance))
5758 .or_else(|| blend_surface_parameters_for_fit(ir, surface, point, seed, tolerance))
5759 }
5760 geometry => analytic_surface_parameters(geometry, point),
5761 }
5762}
5763
5764pub(crate) fn attach_tolerant_edge_intersections(
5765 ir: &mut CadIr,
5766 graph: &Graph,
5767 edges: &BTreeMap<u32, EdgeId>,
5768 prefix: &str,
5769 source_stream: cadmpeg_ir::annotations::StreamHandle,
5770 annotations: &mut AnnotationBuilder,
5771) {
5772 let mut candidates = Vec::new();
5773 for (&xmt, edge_id) in edges {
5774 let Some(edge) = ir
5775 .model
5776 .edges
5777 .iter()
5778 .find(|candidate| &candidate.id == edge_id)
5779 else {
5780 continue;
5781 };
5782 if edge.curve.is_some() || edge.tolerance.is_none() {
5783 continue;
5784 }
5785 let mut supports = ir
5786 .model
5787 .coedges
5788 .iter()
5789 .filter(|coedge| &coedge.edge == edge_id)
5790 .filter_map(|coedge| {
5791 let face = ir
5792 .model
5793 .loops
5794 .iter()
5795 .find(|loop_| loop_.id == coedge.owner_loop)?
5796 .face
5797 .clone();
5798 ir.model
5799 .faces
5800 .iter()
5801 .find(|candidate| candidate.id == face)
5802 .map(|face| face.surface.clone())
5803 })
5804 .collect::<BTreeSet<_>>();
5805 if supports.len() != 2 {
5806 continue;
5807 }
5808 let second = supports.pop_last().expect("two supports");
5809 let first = supports.pop_first().expect("two supports");
5810 candidates.push((xmt, edge_id.clone(), [first, second]));
5811 }
5812
5813 for (xmt, edge_id, supports) in candidates {
5814 let curve_id = CurveId(format!("{prefix}:tolerant-curve#{xmt}"));
5815 let procedural_id = ProceduralCurveId(format!("{prefix}:tolerant-intersection#{xmt}"));
5816 let Some(edge) = ir
5817 .model
5818 .edges
5819 .iter_mut()
5820 .find(|candidate| candidate.id == edge_id)
5821 else {
5822 continue;
5823 };
5824 edge.curve = Some(curve_id.clone());
5825 edge.param_range = Some([0.0, 1.0]);
5826 annotations.derived(&edge_id, "curve");
5827 annotations.derived(&edge_id, "param_range");
5828 if let Some(node) = graph.get(16, xmt) {
5829 annotations
5830 .note(&curve_id, source_stream, node.pos as u64)
5831 .tag("TOLERANT_EDGE_INTERSECTION");
5832 annotations
5833 .note(&procedural_id, source_stream, node.pos as u64)
5834 .tag("TOLERANT_EDGE_INTERSECTION");
5835 }
5836 annotations.derived(&curve_id, "geometry");
5837 annotations.derived(&procedural_id, "definition");
5838 ir.model.curves.push(Curve {
5839 id: curve_id.clone(),
5840 geometry: CurveGeometry::Procedural {
5841 construction: procedural_id.clone(),
5842 },
5843 source_object: None,
5844 });
5845 ir.model.procedural_curves.push(ProceduralCurve {
5846 id: procedural_id,
5847 curve: curve_id,
5848 definition: ProceduralCurveDefinition::Intersection {
5849 context: IntcurveSupportContext {
5850 sides: supports.map(|surface| IntcurveSupportSide {
5851 surface: Some(surface),
5852 pcurve: None,
5853 pcurve_parameter_range: None,
5854 }),
5855 parameter_range: [0.0, 1.0],
5856 discontinuities: [Vec::new(), Vec::new(), Vec::new()],
5857 },
5858 discontinuity_flag: false,
5859 },
5860 cache_fit_tolerance: None,
5861 });
5862 }
5863}
5864
5865pub(crate) fn pcurve_matches_edge(
5866 ir: &CadIr,
5867 edge_id: &EdgeId,
5868 surface_id: &SurfaceId,
5869 geometry: &PcurveGeometry,
5870 fit_tolerance: Option<f64>,
5871) -> bool {
5872 pcurve_matches_edge_range(ir, edge_id, surface_id, geometry, None, fit_tolerance)
5873}
5874
5875fn pcurve_matches_edge_range(
5876 ir: &CadIr,
5877 edge_id: &EdgeId,
5878 surface_id: &SurfaceId,
5879 geometry: &PcurveGeometry,
5880 parameter_range: Option<[f64; 2]>,
5881 fit_tolerance: Option<f64>,
5882) -> bool {
5883 let Some(edge) = ir.model.edges.iter().find(|edge| &edge.id == edge_id) else {
5884 return false;
5885 };
5886 let Some(coincident_surface) = ir
5887 .model
5888 .surfaces
5889 .iter()
5890 .find(|surface| &surface.id == surface_id)
5891 .and_then(|surface| {
5892 let [t0, t1] = parameter_range.or_else(|| pcurve_parameter_range(geometry))?;
5893 let uv = [pcurve_uv(geometry, t0)?, pcurve_uv(geometry, t1)?];
5894 Some([
5895 surface_point(&surface.geometry, uv[0].u, uv[0].v)?,
5896 surface_point(&surface.geometry, uv[1].u, uv[1].v)?,
5897 ])
5898 })
5899 else {
5900 return false;
5901 };
5902 let vertex = |id: &VertexId| {
5903 let vertex = ir.model.vertices.iter().find(|vertex| &vertex.id == id)?;
5904 let point = ir
5905 .model
5906 .points
5907 .iter()
5908 .find(|point| point.id == vertex.point)?;
5909 Some((point.position, vertex.tolerance))
5910 };
5911 let (Some((start, start_tolerance)), Some((end, end_tolerance))) =
5912 (vertex(&edge.start), vertex(&edge.end))
5913 else {
5914 return false;
5915 };
5916 let allowance = [
5917 edge.tolerance,
5918 start_tolerance,
5919 end_tolerance,
5920 fit_tolerance,
5921 ]
5922 .into_iter()
5923 .flatten()
5924 .fold(0.01_f64, f64::max);
5925 let distance = |a: cadmpeg_ir::math::Point3, b: cadmpeg_ir::math::Point3| {
5926 ((a.x - b.x).powi(2) + (a.y - b.y).powi(2) + (a.z - b.z).powi(2)).sqrt()
5927 };
5928 (distance(coincident_surface[0], start) <= allowance
5929 && distance(coincident_surface[1], end) <= allowance)
5930 || (distance(coincident_surface[0], end) <= allowance
5931 && distance(coincident_surface[1], start) <= allowance)
5932}
5933
5934#[allow(clippy::too_many_arguments)]
5935fn retain_unresolved_topology_carriers(
5936 ir: &mut CadIr,
5937 stream_index: usize,
5938 graph: &Graph,
5939 surfaces: &mut BTreeMap<u32, SurfaceId>,
5940 curves: &mut BTreeMap<u32, CurveId>,
5941 pcurves: &BTreeMap<u32, PcurveId>,
5942 source_stream: cadmpeg_ir::annotations::StreamHandle,
5943 annotations: &mut AnnotationBuilder,
5944) {
5945 let unknown = UnknownId(format!("nx:container:parasolid#{stream_index}"));
5946 for face in graph.of_kind(14) {
5947 let Some(surface_xmt) = face.face_fields().map(|fields| fields.surface) else {
5948 continue;
5949 };
5950 if surface_xmt <= 1 || surfaces.contains_key(&surface_xmt) {
5951 continue;
5952 }
5953 let id = SurfaceId(format!("nx:s{stream_index}:surface#unknown-{surface_xmt}"));
5954 annotations
5955 .note(&id, source_stream, face.pos as u64)
5956 .tag("UNRESOLVED_SURFACE_REFERENCE");
5957 annotations.exactness(&id, Exactness::Unknown);
5958 ir.model.surfaces.push(Surface {
5959 id: id.clone(),
5960 geometry: SurfaceGeometry::Unknown {
5961 record: Some(unknown.clone()),
5962 },
5963 source_object: None,
5964 });
5965 surfaces.insert(surface_xmt, id);
5966 }
5967
5968 for edge in graph.of_kind(16) {
5969 let Some(curve_xmt) = edge.edge_fields().map(|fields| fields.curve) else {
5970 continue;
5971 };
5972 if curve_xmt <= 1 || curves.contains_key(&curve_xmt) || pcurves.contains_key(&curve_xmt) {
5973 continue;
5974 }
5975 let id = CurveId(format!("nx:s{stream_index}:curve#unknown-{curve_xmt}"));
5976 annotations
5977 .note(&id, source_stream, edge.pos as u64)
5978 .tag("UNRESOLVED_CURVE_REFERENCE");
5979 annotations.exactness(&id, Exactness::Unknown);
5980 ir.model.curves.push(Curve {
5981 id: id.clone(),
5982 geometry: CurveGeometry::Unknown {
5983 record: Some(unknown.clone()),
5984 },
5985 source_object: None,
5986 });
5987 curves.insert(curve_xmt, id);
5988 }
5989}
5990
5991fn annotate_node(
5992 annotations: &mut AnnotationBuilder,
5993 id: impl std::fmt::Display,
5994 stream: cadmpeg_ir::annotations::StreamHandle,
5995 node: &Node,
5996 tag: &str,
5997) {
5998 annotations.note(id, stream, node.pos as u64).tag(tag);
5999}
6000
6001fn surface_tag(geometry: &SurfaceGeometry) -> &'static str {
6002 match geometry {
6003 SurfaceGeometry::Plane { .. } => "PLANE",
6004 SurfaceGeometry::Cylinder { .. } => "CYLINDER",
6005 SurfaceGeometry::Cone { .. } => "CONE",
6006 SurfaceGeometry::Sphere { .. } => "SPHERE",
6007 SurfaceGeometry::Torus { .. } => "TORUS",
6008 SurfaceGeometry::Nurbs(_) => "B_SPLINE_SURFACE",
6009 SurfaceGeometry::Procedural { .. } => "PROCEDURAL_SURFACE",
6010 SurfaceGeometry::Polygonal { .. } => "POLYGONAL_SURFACE",
6011 SurfaceGeometry::Transformed { basis, .. } => surface_tag(basis),
6012 SurfaceGeometry::Unknown { .. } => "UNKNOWN_SURFACE",
6013 }
6014}
6015
6016fn curve_tag(geometry: &CurveGeometry) -> &'static str {
6017 match geometry {
6018 CurveGeometry::Line { .. } => "LINE",
6019 CurveGeometry::Circle { .. } => "CIRCLE",
6020 CurveGeometry::Ellipse { .. } => "ELLIPSE",
6021 CurveGeometry::Parabola { .. } => "PARABOLA",
6022 CurveGeometry::Hyperbola { .. } => "HYPERBOLA",
6023 CurveGeometry::Degenerate { .. } => "DEGENERATE_CURVE",
6024 CurveGeometry::Nurbs(_) => "B_SPLINE_CURVE",
6025 CurveGeometry::Procedural { .. } => "PROCEDURAL_CURVE",
6026 CurveGeometry::Composite { .. } => "COMPOSITE_CURVE",
6027 CurveGeometry::Polyline { .. } => "POLYLINE",
6028 CurveGeometry::Transformed { basis, .. } => curve_tag(basis),
6029 CurveGeometry::Unknown { .. } => "UNKNOWN_CURVE",
6030 }
6031}
6032
6033pub(crate) fn decoded_tolerance(value: f64) -> Option<f64> {
6034 match value {
6035 MISSING_TOLERANCE => None,
6036 value if value.is_finite() && value > 0.0 && (value * 1000.0).is_finite() => {
6037 Some(value * 1000.0)
6038 }
6039 _ => None,
6040 }
6041}
6042
6043#[allow(clippy::too_many_arguments)]
6044fn synthesize_closed_edge_vertex(
6045 ir: &mut CadIr,
6046 annotations: &mut AnnotationBuilder,
6047 prefix: &str,
6048 edge: &Node,
6049 curve: &CurveId,
6050 range: Option<[f64; 2]>,
6051 source_stream: cadmpeg_ir::annotations::StreamHandle,
6052 tolerance: Option<f64>,
6053) -> Option<VertexId> {
6054 let geometry = &ir
6055 .model
6056 .curves
6057 .iter()
6058 .find(|candidate| candidate.id == *curve)?
6059 .geometry;
6060 let parameter = range.map_or_else(
6061 || match geometry {
6062 CurveGeometry::Nurbs(nurbs) => nurbs.knots.first().copied().unwrap_or(0.0),
6063 _ => 0.0,
6064 },
6065 |range| range[0],
6066 );
6067 let position = curve_point(geometry, parameter)?;
6068 let point = PointId(format!("{prefix}:point#closed-edge-{}", edge.xmt));
6069 let vertex = VertexId(format!("{prefix}:vertex#closed-edge-{}", edge.xmt));
6070 annotations
6071 .note(&point, source_stream, edge.pos as u64)
6072 .tag("CLOSED_EDGE_POINT");
6073 annotations.exactness(&point, Exactness::Inferred);
6074 annotations
6075 .note(&vertex, source_stream, edge.pos as u64)
6076 .tag("CLOSED_EDGE_VERTEX");
6077 annotations.exactness(&vertex, Exactness::Inferred);
6078 ir.model.points.push(Point {
6079 id: point.clone(),
6080 position,
6081 source_object: None,
6082 });
6083 ir.model.vertices.push(Vertex {
6084 id: vertex.clone(),
6085 point,
6086 tolerance,
6087 });
6088 Some(vertex)
6089}
6090
6091fn canonical_trim_range(ir: &CadIr, basis: &CurveId, raw: [f64; 2]) -> Option<[f64; 2]> {
6092 let curve = ir.model.curves.iter().find(|curve| curve.id == *basis)?;
6093 match &curve.geometry {
6094 CurveGeometry::Line { .. } => {
6095 let range = [raw[0] * 1000.0, raw[1] * 1000.0];
6096 range.into_iter().all(f64::is_finite).then_some(range)
6097 }
6098 CurveGeometry::Nurbs(nurbs) => {
6099 let domain = [*nurbs.knots.first()?, *nurbs.knots.last()?];
6100 let epsilon = 1.0e-6 * (1.0 + domain[0].abs().max(domain[1].abs()));
6101 if raw
6102 .iter()
6103 .any(|value| *value < domain[0] - epsilon || *value > domain[1] + epsilon)
6104 {
6105 None
6106 } else {
6107 Some([
6108 raw[0].clamp(domain[0], domain[1]),
6109 raw[1].clamp(domain[0], domain[1]),
6110 ])
6111 }
6112 }
6113 _ => Some(raw),
6114 }
6115}
6116
6117fn orient_edge_range(
6118 ir: &CadIr,
6119 curve: &CurveId,
6120 range: [f64; 2],
6121 start: &VertexId,
6122 end: &VertexId,
6123 edge_tolerance: Option<f64>,
6124) -> Option<([f64; 2], bool)> {
6125 let geometry = &ir
6126 .model
6127 .curves
6128 .iter()
6129 .find(|candidate| candidate.id == *curve)?
6130 .geometry;
6131 let range = if range[0] <= range[1] {
6132 range
6133 } else {
6134 [range[1], range[0]]
6135 };
6136 let range = match geometry {
6137 CurveGeometry::Circle { .. } | CurveGeometry::Ellipse { .. } => {
6138 let sweep = range[1] - range[0];
6139 (0.0..=std::f64::consts::TAU)
6140 .contains(&sweep)
6141 .then_some(())?;
6142 let start = range[0].rem_euclid(std::f64::consts::TAU);
6143 [start, start + sweep]
6144 }
6145 _ => range,
6146 };
6147 let at = match (
6148 curve_point(geometry, range[0]),
6149 curve_point(geometry, range[1]),
6150 ) {
6151 (Some(start), Some(end)) => [start, end],
6152 _ if ir
6153 .model
6154 .procedural_curves
6155 .iter()
6156 .any(|procedural| procedural.curve == *curve) =>
6157 {
6158 return Some((range, false));
6159 }
6160 _ => return None,
6161 };
6162 let vertex_position = |vertex: &VertexId| {
6163 let vertex = ir
6164 .model
6165 .vertices
6166 .iter()
6167 .find(|candidate| candidate.id == *vertex)?;
6168 let point = ir
6169 .model
6170 .points
6171 .iter()
6172 .find(|candidate| candidate.id == vertex.point)?;
6173 Some((point.position, vertex.tolerance))
6174 };
6175 let (start_position, start_tolerance) = vertex_position(start)?;
6176 let (end_position, end_tolerance) = vertex_position(end)?;
6177 let cache_tolerance = ir
6178 .model
6179 .procedural_curves
6180 .iter()
6181 .find(|procedural| procedural.curve == *curve)
6182 .and_then(|procedural| procedural.cache_fit_tolerance);
6183 let allowance = [
6184 edge_tolerance,
6185 start_tolerance,
6186 end_tolerance,
6187 cache_tolerance,
6188 ]
6189 .into_iter()
6190 .flatten()
6191 .fold(0.01_f64, f64::max);
6192 let distance = |a: cadmpeg_ir::math::Point3, b: cadmpeg_ir::math::Point3| {
6193 ((a.x - b.x).powi(2) + (a.y - b.y).powi(2) + (a.z - b.z).powi(2)).sqrt()
6194 };
6195 if distance(at[0], start_position) <= allowance && distance(at[1], end_position) <= allowance {
6196 Some((range, false))
6197 } else if distance(at[1], start_position) <= allowance
6198 && distance(at[0], end_position) <= allowance
6199 {
6200 Some((range, true))
6201 } else {
6202 None
6203 }
6204}
6205
6206fn sense(byte: Option<u8>) -> Sense {
6207 if byte == Some(b'-') {
6208 Sense::Reversed
6209 } else {
6210 Sense::Forward
6211 }
6212}
6213
6214fn unknown_stream(si: usize, stream: &Stream) -> UnknownRecord {
6215 UnknownRecord {
6216 id: UnknownId(format!("nx:container:parasolid#{si}")),
6217 offset: stream.file_offset as u64,
6218 byte_len: stream.inflated.len() as u64,
6219 sha256: sha256_hex(&stream.inflated),
6220 data: Some(stream.inflated.clone()),
6221 links: Vec::new(),
6222 }
6223}
6224
6225fn source_meta(scan: &Scan) -> SourceMeta {
6226 let mut attributes = BTreeMap::new();
6227 attributes.insert(
6228 "file_size".to_string(),
6229 scan.container.data.len().to_string(),
6230 );
6231 attributes.insert(
6232 "footer_offset".to_string(),
6233 scan.container.footer_offset.to_string(),
6234 );
6235 attributes.insert(
6236 "directory_entries".to_string(),
6237 scan.container.entries.len().to_string(),
6238 );
6239 attributes.insert(
6240 "partition_streams".to_string(),
6241 scan.count(StreamKind::Partition).to_string(),
6242 );
6243 attributes.insert(
6244 "deltas_streams".to_string(),
6245 scan.count(StreamKind::Deltas).to_string(),
6246 );
6247 attributes.insert(
6248 "plain_streams".to_string(),
6249 scan.count(StreamKind::Plain).to_string(),
6250 );
6251 if let Some(schema) = scan.streams.iter().find_map(|s| s.schema.as_deref()) {
6252 attributes.insert("parasolid_schema".to_string(), schema.to_string());
6253 }
6254 for (index, path) in scan
6255 .container
6256 .external_reference_paths()
6257 .into_iter()
6258 .enumerate()
6259 {
6260 attributes.insert(format!("external_reference.{index}"), path);
6261 }
6262 if let Some((_, table)) = scan.container.rmfastload_object_id_table() {
6263 attributes.insert(
6264 "rmfastload_active_object_count".to_string(),
6265 table.object_ids.len().to_string(),
6266 );
6267 }
6268 let mut preview_count = 0usize;
6269 for entry in scan
6270 .container
6271 .entries
6272 .iter()
6273 .filter(|entry| entry.name == "/Root/images/preview")
6274 {
6275 let Some((offset, size)) = entry.file_span else {
6276 continue;
6277 };
6278 let (Ok(start), Ok(size)) = (usize::try_from(offset), usize::try_from(size)) else {
6279 continue;
6280 };
6281 let Some(payload) = scan.container.data.get(start..start.saturating_add(size)) else {
6282 continue;
6283 };
6284 let Some((width, height, precision, components)) = jpeg_dimensions(payload) else {
6285 continue;
6286 };
6287 let prefix = format!("jpeg_preview_{preview_count}");
6288 attributes.insert(format!("{prefix}_width"), width.to_string());
6289 attributes.insert(format!("{prefix}_height"), height.to_string());
6290 attributes.insert(format!("{prefix}_precision"), precision.to_string());
6291 attributes.insert(format!("{prefix}_components"), components.to_string());
6292 attributes.insert(format!("{prefix}_byte_len"), payload.len().to_string());
6293 attributes.insert(format!("{prefix}_sha256"), sha256_hex(payload));
6294 preview_count += 1;
6295 }
6296 attributes.insert("jpeg_preview_count".to_string(), preview_count.to_string());
6297 for (index, stream) in scan
6298 .streams
6299 .iter()
6300 .filter(|stream| stream.kind == StreamKind::Deltas)
6301 .enumerate()
6302 {
6303 let census = crate::deltas::walk(&stream.inflated);
6304 attributes.insert(
6305 format!("deltas.{index}.grammar"),
6306 "status_byte_framed_topology".to_string(),
6307 );
6308 attributes.insert(
6309 format!("deltas.{index}.bytes_decoded"),
6310 census.bytes_decoded.to_string(),
6311 );
6312 for (name, count) in census.full_counts {
6313 attributes.insert(format!("deltas.{index}.full.{name}"), count.to_string());
6314 }
6315 for (name, count) in census.tombstone_counts {
6316 attributes.insert(
6317 format!("deltas.{index}.tombstone.{name}"),
6318 count.to_string(),
6319 );
6320 }
6321 }
6322 SourceMeta {
6323 format: "nx".to_string(),
6324 attributes,
6325 }
6326}
6327
6328pub(crate) fn jpeg_dimensions(payload: &[u8]) -> Option<(u16, u16, u8, u8)> {
6329 if payload.get(..2)? != [0xff, 0xd8] {
6330 return None;
6331 }
6332 let mut offset = 2usize;
6333 while offset < payload.len() {
6334 while payload.get(offset) == Some(&0xff) {
6335 offset += 1;
6336 }
6337 let marker = *payload.get(offset)?;
6338 offset += 1;
6339 if marker == 0xd9 || marker == 0xda {
6340 return None;
6341 }
6342 if marker == 0x01 || (0xd0..=0xd7).contains(&marker) {
6343 continue;
6344 }
6345 let length = usize::from(u16::from_be_bytes([
6346 *payload.get(offset)?,
6347 *payload.get(offset + 1)?,
6348 ]));
6349 if length < 2 {
6350 return None;
6351 }
6352 let segment_start = offset + 2;
6353 let segment_end = offset.checked_add(length)?;
6354 let segment = payload.get(segment_start..segment_end)?;
6355 if matches!(marker, 0xc0..=0xc3 | 0xc5..=0xc7 | 0xc9..=0xcb | 0xcd..=0xcf) {
6356 let precision = *segment.first()?;
6357 let height = u16::from_be_bytes([*segment.get(1)?, *segment.get(2)?]);
6358 let width = u16::from_be_bytes([*segment.get(3)?, *segment.get(4)?]);
6359 let components = *segment.get(5)?;
6360 if width == 0
6361 || height == 0
6362 || components == 0
6363 || segment.len() != 6 + 3 * usize::from(components)
6364 {
6365 return None;
6366 }
6367 return Some((width, height, precision, components));
6368 }
6369 offset = segment_end;
6370 }
6371 None
6372}
6373
6374fn build_geometry_report(
6375 scan: &Scan,
6376 ir: &CadIr,
6377 counts: &Counts,
6378 has_topology: bool,
6379 has_unresolved_sub_bodies: bool,
6380 tessellation_count: usize,
6381) -> DecodeReport {
6382 let mut losses = Vec::new();
6383
6384 losses.push(LossNote {
6385 code: LossCode::CarrierSummary,
6386 category: LossCategory::Geometry,
6387 severity: Severity::Info,
6388 message: format!(
6389 "Decoded {} POINT carrier(s) verbatim from Parasolid POINT records (3×f64 big-endian, \
6390 metres → millimetres), {} analytic surface carrier(s) ({} plane, {} cylinder, {} \
6391 cone, {} sphere, {} torus), and {} analytic curve carrier(s) ({} line, {} circle, {} \
6392 ellipse). All parameters are byte-exact at the document's millimetre scale.",
6393 counts.points,
6394 counts.surfaces(),
6395 counts.planes,
6396 counts.cylinders,
6397 counts.cones,
6398 counts.spheres,
6399 counts.tori,
6400 counts.curves(),
6401 counts.lines,
6402 counts.circles,
6403 counts.ellipses,
6404 ),
6405 provenance: None,
6406 });
6407
6408 if tessellation_count != 0 {
6409 losses.push(LossNote {
6410 code: LossCode::CarrierSummary,
6411 category: LossCategory::Geometry,
6412 severity: Severity::Info,
6413 message: format!(
6414 "Decoded {tessellation_count} embedded JT display tessellation(s) with scene-node ownership, model-space coordinates, topological triangle connectivity, and corner normals when bound."
6415 ),
6416 provenance: None,
6417 });
6418 }
6419
6420 if !has_topology {
6421 losses.push(LossNote {
6422 code: LossCode::TopologyNotTransferred,
6423 category: LossCategory::Topology,
6424 severity: Severity::Blocking,
6425 message: "The B-rep topology graph (body→shell→face→loop→fin→edge→vertex) was not \
6426 reconstructed because the surviving typed records did not form a complete \
6427 connected ownership graph. Exact-key supported partition↔deltas replacements \
6428 and deletions were applied before graph construction. Required unresolved \
6429 records prevent their dependent incidence from being emitted; decoded geometry \
6430 then remains unattached."
6431 .to_string(),
6432 provenance: None,
6433 });
6434 }
6435
6436 if counts.intersection_rejections.total() > 0 {
6437 losses.push(LossNote {
6438 code: LossCode::ObjectRecordsUntransferred,
6439 category: LossCategory::Geometry,
6440 severity: Severity::Warning,
6441 message: format!(
6442 "{} surface-intersection record(s) without a complete validated CHART_s and \
6443 term-endpoint witness remain opaque constructions. Support-UV values govern \
6444 optional pcurve attachment and do not invalidate a witnessed 3D carrier. Each \
6445 Parasolid stream is preserved verbatim as an unknown passthrough record so the \
6446 unresolved source bytes remain available. Rejections: {} missing chart, {} missing \
6447 start term, {} missing end term, {} endpoint mismatch.",
6448 counts.intersection_rejections.total(),
6449 counts.intersection_rejections.missing_chart,
6450 counts.intersection_rejections.missing_start_term,
6451 counts.intersection_rejections.missing_end_term,
6452 counts.intersection_rejections.endpoint_mismatch,
6453 ),
6454 provenance: None,
6455 });
6456 }
6457
6458 if scan.count(StreamKind::Deltas) > 0 {
6459 let unmatched_tombstones = unmatched_delta_tombstone_count(scan);
6460 losses.push(LossNote {
6461 code: LossCode::DecodeDiagnostic,
6462 category: LossCategory::Topology,
6463 severity: if unmatched_tombstones == 0 {
6464 Severity::Info
6465 } else {
6466 Severity::Warning
6467 },
6468 message: if unmatched_tombstones == 0 {
6469 format!(
6470 "{} Parasolid deltas stream(s) were processed in validated UG_PART segment order. \
6471 Equal-schema deltas were paired with the preceding partition. Exact-key \
6472 BODY, SHELL, FACE, LOOP, FIN, EDGE, VERTEX, REGION, POINT, LINE, CIRCLE, ELLIPSE, PLANE, CYLINDER, CONE, SPHERE, TORUS, BLEND_SURF, OFFSET_SURF, B_SURFACE, TRIMMED_CURVE, B_CURVE, and SP_CURVE full records and compact \
6473 non-topology replacements and tombstones were applied using the last event for \
6474 each key. Validated partition topology remained authoritative, including any \
6475 point, curve, or surface carrier still referenced by surviving topology. Every \
6476 terminal tombstone resolved to an exact current or earlier-added key.",
6477 scan.count(StreamKind::Deltas)
6478 )
6479 } else {
6480 format!(
6481 "{} Parasolid deltas stream(s) were processed in validated UG_PART segment order. \
6482 Equal-schema deltas were paired with the preceding partition. Exact-key revisions were applied using the last \
6483 event for each key, but {unmatched_tombstones} terminal tombstone(s) have no exact \
6484 current or earlier-added key and remain unresolved.",
6485 scan.count(StreamKind::Deltas)
6486 )
6487 },
6488 provenance: None,
6489 });
6490 }
6491
6492 if has_unresolved_sub_bodies {
6493 losses.push(LossNote {
6494 code: LossCode::FeatureHistoryRetained,
6495 category: LossCategory::Topology,
6496 severity: Severity::Warning,
6497 message: format!(
6498 "This part is composed of {} sub-body partition(s); its decoded feature-history \
6499 Booleans do not resolve every intermediate body object to a partition image. \
6500 Carriers from all sub-bodies are emitted without the unresolved composition that \
6501 would remove interior/construction faces.",
6502 scan.count(StreamKind::Partition)
6503 ),
6504 provenance: None,
6505 });
6506 }
6507
6508 append_design_intent_losses(ir, &mut losses);
6509
6510 losses.push(LossNote {
6511 code: LossCode::AttributesNotTransferred,
6512 category: LossCategory::Attribute,
6513 severity: Severity::Warning,
6514 message: "Material and appearance assignment, class-specific entity attribute fields, and \
6515 assembly occurrence placements were not transferred: their remaining NX \
6516 object-model and Parasolid field serialization is not decoded."
6517 .to_string(),
6518 provenance: None,
6519 });
6520
6521 DecodeReport {
6522 format: "nx".to_string(),
6523 container_only: false,
6524 geometry_transferred: true,
6525 coverage: std::collections::BTreeMap::new(),
6526 losses,
6527 notes: summary_notes(scan),
6528 }
6529}
6530
6531pub(crate) fn append_design_intent_losses(ir: &CadIr, losses: &mut Vec<LossNote>) {
6532 let unresolved_suppression_count = ir
6533 .model
6534 .features
6535 .iter()
6536 .filter(|feature| feature.suppressed.is_none())
6537 .count();
6538 if unresolved_suppression_count != 0 {
6539 losses.push(LossNote {
6540 code: LossCode::FeatureHistoryRetained,
6541 category: LossCategory::DesignIntent,
6542 severity: Severity::Warning,
6543 message: format!(
6544 "Suppression state remains unresolved for {unresolved_suppression_count} NX \
6545 feature history operation(s)."
6546 ),
6547 provenance: None,
6548 });
6549 }
6550
6551 let active_configuration_count = ir
6552 .model
6553 .configurations
6554 .iter()
6555 .filter(|configuration| configuration.active)
6556 .count();
6557 let current_bodies = ir
6558 .model
6559 .bodies
6560 .iter()
6561 .map(|body| &body.id)
6562 .collect::<BTreeSet<_>>();
6563 let incomplete_configuration_count = ir
6564 .model
6565 .configurations
6566 .iter()
6567 .filter(|configuration| {
6568 configuration.bodies.is_unresolved()
6569 || active_configuration_count != 1
6570 || (configuration.active
6571 && configuration.bodies.resolved().is_none_or(|bodies| {
6572 bodies.len() != current_bodies.len()
6573 || bodies.iter().collect::<BTreeSet<_>>() != current_bodies
6574 }))
6575 })
6576 .count();
6577 if incomplete_configuration_count != 0 {
6578 losses.push(LossNote {
6579 code: LossCode::FeatureHistoryRetained,
6580 category: LossCategory::DesignIntent,
6581 severity: Severity::Warning,
6582 message: format!(
6583 "Activation or complete body membership remains unresolved for \
6584 {incomplete_configuration_count} NX design configuration(s)."
6585 ),
6586 provenance: None,
6587 });
6588 }
6589
6590 let incomplete_expression_count = incomplete_expression_parameters(ir).len();
6591 if incomplete_expression_count != 0 {
6592 losses.push(LossNote {
6593 code: LossCode::FeatureHistoryRetained,
6594 category: LossCategory::DesignIntent,
6595 severity: Severity::Warning,
6596 message: format!(
6597 "Neutral evaluation or dependency semantics remain incomplete for \
6598 {incomplete_expression_count} NX expression parameter(s)."
6599 ),
6600 provenance: None,
6601 });
6602 }
6603
6604 let mut native_feature_kinds = BTreeMap::<&str, usize>::new();
6605 for feature in &ir.model.features {
6606 if let FeatureDefinition::Native { kind, .. } = &feature.definition {
6607 *native_feature_kinds.entry(kind.as_str()).or_default() += 1;
6608 }
6609 }
6610 if !native_feature_kinds.is_empty() {
6611 let kinds = native_feature_kinds
6612 .into_iter()
6613 .map(|(kind, count)| format!("{kind} ({count})"))
6614 .collect::<Vec<_>>()
6615 .join(", ");
6616 losses.push(LossNote {
6617 code: LossCode::FeatureHistoryRetained,
6618 category: LossCategory::DesignIntent,
6619 severity: Severity::Warning,
6620 message: format!(
6621 "NX feature-history operation(s) remain native-only because their complete neutral \
6622 operation semantics are not decoded: {kinds}."
6623 ),
6624 provenance: None,
6625 });
6626 }
6627
6628 let mut unresolved_feature_families = BTreeMap::<&str, usize>::new();
6629 for feature in &ir.model.features {
6630 let family = match feature.definition {
6631 FeatureDefinition::DatumPlaneUnresolved => "datum plane",
6632 FeatureDefinition::DatumPointUnresolved => "datum point",
6633 FeatureDefinition::DatumCoordinateSystemUnresolved => "datum coordinate system",
6634 FeatureDefinition::LoftUnresolved => "loft",
6635 FeatureDefinition::FreeformSurfaceUnresolved => "freeform surface",
6636 FeatureDefinition::DraftUnresolved => "draft",
6637 _ => continue,
6638 };
6639 *unresolved_feature_families.entry(family).or_default() += 1;
6640 }
6641 if !unresolved_feature_families.is_empty() {
6642 let families = unresolved_feature_families
6643 .into_iter()
6644 .map(|(family, count)| format!("{family} ({count})"))
6645 .collect::<Vec<_>>()
6646 .join(", ");
6647 losses.push(LossNote {
6648 code: LossCode::FeatureHistoryRetained,
6649 category: LossCategory::DesignIntent,
6650 severity: Severity::Warning,
6651 message: format!(
6652 "NX feature family identities were transferred, but their neutral construction \
6653 semantics remain unresolved: {families}."
6654 ),
6655 provenance: None,
6656 });
6657 }
6658
6659 let mut incomplete_feature_families = BTreeMap::<&str, usize>::new();
6660 for feature in &ir.model.features {
6661 if feature.suppressed != Some(true) {
6662 if let Some(family) = body_output_feature_family(&feature.definition).filter(|_| {
6663 feature.outputs.is_empty()
6664 || feature.outputs.iter().collect::<BTreeSet<_>>().len()
6665 != feature.outputs.len()
6666 || feature
6667 .outputs
6668 .iter()
6669 .any(|output| !ir.model.bodies.iter().any(|body| body.id == *output))
6670 }) {
6671 *incomplete_feature_families.entry(family).or_default() += 1;
6672 continue;
6673 }
6674 }
6675 let family = match &feature.definition {
6676 FeatureDefinition::Block {
6677 dimensions,
6678 placement,
6679 } if dimensions.is_none() || placement.is_none() => "block",
6680 FeatureDefinition::DatumOffsetPlane {
6681 reference,
6682 distance,
6683 } if !distance.0.is_finite()
6684 || reference.as_ref().is_none_or(|reference| {
6685 ir.model
6686 .features
6687 .iter()
6688 .find(|candidate| candidate.id == *reference)
6689 .is_none_or(|source| source.ordinal >= feature.ordinal)
6690 || !feature.dependencies.contains(reference)
6691 }) =>
6692 {
6693 "datum plane"
6694 }
6695 FeatureDefinition::ExtractBody { source } if body_selection_is_incomplete(source) => {
6696 "extract body"
6697 }
6698 FeatureDefinition::Sketch { space, sketch }
6699 if !matches!(space, SketchSpace::Planar) || sketch.is_none() =>
6700 {
6701 "sketch"
6702 }
6703 FeatureDefinition::Loft {
6704 sections,
6705 guides,
6706 op,
6707 ..
6708 } if sections.len() < 2
6709 || sections.iter().any(|section| match section {
6710 cadmpeg_ir::features::LoftSection::Profile(profile) => {
6711 profile_ref_is_incomplete(profile)
6712 }
6713 cadmpeg_ir::features::LoftSection::Point { .. } => false,
6714 })
6715 || guides.iter().any(path_ref_is_incomplete)
6716 || matches!(op, BooleanOp::Unresolved) =>
6717 {
6718 "loft"
6719 }
6720 FeatureDefinition::ProjectedCurve {
6721 source,
6722 target_faces,
6723 direction,
6724 bidirectional,
6725 } if path_ref_is_incomplete(source)
6726 || face_selection_is_incomplete(target_faces)
6727 || matches!(
6728 direction,
6729 CurveProjectionDirection::State(CurveProjectionDirectionState::Unresolved)
6730 )
6731 || bidirectional.is_none() =>
6732 {
6733 "projected curve"
6734 }
6735 FeatureDefinition::TrimSurface { faces, tool, keep }
6736 if face_selection_is_incomplete(faces)
6737 || path_ref_is_incomplete(tool)
6738 || matches!(keep, TrimRegion::Unresolved) =>
6739 {
6740 "trim surface"
6741 }
6742 FeatureDefinition::ExtendSurface {
6743 faces,
6744 distance,
6745 method,
6746 } if face_selection_is_incomplete(faces)
6747 || distance.is_none()
6748 || matches!(method, cadmpeg_ir::features::SurfaceExtension::Unresolved) =>
6749 {
6750 "extend surface"
6751 }
6752 FeatureDefinition::Hole {
6753 profile,
6754 face,
6755 position,
6756 direction,
6757 kind,
6758 exit_kind,
6759 diameter,
6760 extent,
6761 ..
6762 } if hole_feature_is_incomplete(
6763 profile.as_ref(),
6764 face.as_ref(),
6765 *position,
6766 *direction,
6767 (kind, exit_kind.as_ref()),
6768 *diameter,
6769 extent.as_ref(),
6770 ) =>
6771 {
6772 "hole"
6773 }
6774 FeatureDefinition::Rib { construction, op }
6775 if rib_feature_is_incomplete(construction, *op) =>
6776 {
6777 "rib"
6778 }
6779 FeatureDefinition::Chamfer { groups, .. }
6780 if groups.is_empty()
6781 || groups.iter().any(|group| {
6782 edge_selection_is_incomplete(&group.edges)
6783 || matches!(group.spec, ChamferSpec::Unresolved { .. })
6784 }) =>
6785 {
6786 "chamfer"
6787 }
6788 FeatureDefinition::Fillet { groups }
6789 if groups.is_empty()
6790 || groups.iter().any(|group| {
6791 edge_selection_is_incomplete(&group.edges)
6792 || radius_spec_is_incomplete(&group.radius)
6793 }) =>
6794 {
6795 "fillet"
6796 }
6797 FeatureDefinition::FaceBlend {
6798 first_faces,
6799 second_faces,
6800 radius,
6801 } if face_selection_is_incomplete(first_faces)
6802 || face_selection_is_incomplete(second_faces)
6803 || face_selections_overlap(first_faces, second_faces)
6804 || radius_spec_is_incomplete(radius) =>
6805 {
6806 "face blend"
6807 }
6808 FeatureDefinition::SewBodies { bodies, .. }
6809 if body_selection_is_incomplete(bodies)
6810 || explicit_body_ids(bodies).is_some_and(|bodies| bodies.len() < 2) =>
6811 {
6812 "sew bodies"
6813 }
6814 FeatureDefinition::TrimBodies {
6815 targets,
6816 tools,
6817 keep,
6818 } if body_selection_is_incomplete(targets)
6819 || body_selection_is_incomplete(tools)
6820 || body_selections_overlap(targets, tools)
6821 || matches!(keep, BodyTrimSide::Unresolved) =>
6822 {
6823 "trim bodies"
6824 }
6825 FeatureDefinition::Extrude {
6826 profile,
6827 extent,
6828 op,
6829 ..
6830 } if profile_ref_is_incomplete(profile)
6831 || extrude_extent_is_incomplete(extent)
6832 || matches!(op, BooleanOp::Unresolved) =>
6833 {
6834 "extrude"
6835 }
6836 FeatureDefinition::OffsetSurface { faces, distance }
6837 if face_selection_is_incomplete(faces) || distance.is_none() =>
6838 {
6839 "offset surface"
6840 }
6841 FeatureDefinition::Thicken {
6842 faces,
6843 thickness,
6844 side,
6845 } if face_selection_is_incomplete(faces) || thickness.is_none() || side.is_none() => {
6846 "thicken"
6847 }
6848 FeatureDefinition::Draft {
6849 faces,
6850 neutral_plane,
6851 ..
6852 } if face_selection_is_incomplete(faces)
6853 || face_selection_is_incomplete(neutral_plane) =>
6854 {
6855 "draft"
6856 }
6857 FeatureDefinition::Pattern { seeds, pattern }
6858 if pattern_feature_is_incomplete(seeds, pattern) =>
6859 {
6860 "pattern"
6861 }
6862 FeatureDefinition::Combine { target, tools, op }
6863 if body_selection_is_incomplete(target)
6864 || body_selection_is_incomplete(tools)
6865 || body_selections_overlap(target, tools)
6866 || matches!(op, BooleanOp::Unresolved) =>
6867 {
6868 "body combine"
6869 }
6870 FeatureDefinition::DeleteBody { bodies, mode }
6871 if body_selection_is_incomplete(bodies)
6872 || matches!(mode, BodyRetentionMode::Unresolved) =>
6873 {
6874 "delete body"
6875 }
6876 _ => continue,
6877 };
6878 *incomplete_feature_families.entry(family).or_default() += 1;
6879 }
6880 if !incomplete_feature_families.is_empty() {
6881 let families = incomplete_feature_families
6882 .into_iter()
6883 .map(|(family, count)| format!("{family} ({count})"))
6884 .collect::<Vec<_>>()
6885 .join(", ");
6886 losses.push(LossNote {
6887 code: LossCode::FeatureHistoryRetained,
6888 category: LossCategory::DesignIntent,
6889 severity: Severity::Warning,
6890 message: format!(
6891 "NX feature families were transferred as typed neutral operations, but \
6892 construction fields or output lineage remain unresolved or native-only: \
6893 {families}."
6894 ),
6895 provenance: None,
6896 });
6897 }
6898
6899 let sketch_feature_count = ir
6900 .model
6901 .features
6902 .iter()
6903 .filter(|feature| matches!(feature.definition, FeatureDefinition::Sketch { .. }))
6904 .count();
6905 let unresolved_sketch_feature_count = ir
6906 .model
6907 .features
6908 .iter()
6909 .filter(|feature| {
6910 matches!(
6911 feature.definition,
6912 FeatureDefinition::Sketch { sketch: None, .. }
6913 )
6914 })
6915 .count();
6916 if unresolved_sketch_feature_count != 0 {
6917 losses.push(LossNote {
6918 code: LossCode::FeatureHistoryRetained,
6919 category: LossCategory::DesignIntent,
6920 severity: Severity::Warning,
6921 message: format!(
6922 "Decoded {sketch_feature_count} NX sketch history feature(s), of which \
6923 {unresolved_sketch_feature_count} have no neutral sketch graph because complete \
6924 sketch placement and entity semantics are unresolved."
6925 ),
6926 provenance: None,
6927 });
6928 } else if sketch_feature_count != 0 && ir.model.sketch_constraints.is_empty() {
6929 losses.push(LossNote {
6930 code: LossCode::FeatureHistoryRetained,
6931 category: LossCategory::DesignIntent,
6932 severity: Severity::Warning,
6933 message: format!(
6934 "Decoded {} NX sketch record(s), but no sketch constraints were transferred because \
6935 their object-model field serialization and operand roles are unresolved.",
6936 ir.model.sketches.len()
6937 ),
6938 provenance: None,
6939 });
6940 }
6941
6942 let native_sketch_entity_count = ir
6943 .model
6944 .sketch_entities
6945 .iter()
6946 .filter(|entity| {
6947 matches!(
6948 entity.geometry,
6949 cadmpeg_ir::sketches::SketchGeometry::Native { .. }
6950 )
6951 })
6952 .count();
6953 let native_sketch_constraint_count = ir
6954 .model
6955 .sketch_constraints
6956 .iter()
6957 .filter(|constraint| {
6958 matches!(
6959 constraint.definition,
6960 cadmpeg_ir::sketches::SketchConstraintDefinition::Native { .. }
6961 )
6962 })
6963 .count();
6964 if native_sketch_entity_count != 0 || native_sketch_constraint_count != 0 {
6965 losses.push(LossNote {
6966 code: LossCode::FeatureHistoryRetained,
6967 category: LossCategory::DesignIntent,
6968 severity: Severity::Warning,
6969 message: format!(
6970 "Neutral semantics remain unresolved for {native_sketch_entity_count} NX sketch \
6971 geometry record(s) and {native_sketch_constraint_count} sketch constraint \
6972 record(s)."
6973 ),
6974 provenance: None,
6975 });
6976 }
6977}
6978
6979pub(crate) fn body_output_feature_family(definition: &FeatureDefinition) -> Option<&'static str> {
6980 match definition {
6981 FeatureDefinition::Block { .. } => Some("block"),
6982 FeatureDefinition::ExtractBody { .. } => Some("extract body"),
6983 FeatureDefinition::Loft { .. } => Some("loft"),
6984 FeatureDefinition::TrimSurface { .. } => Some("trim surface"),
6985 FeatureDefinition::ExtendSurface { .. } => Some("extend surface"),
6986 FeatureDefinition::Hole { .. } => Some("hole"),
6987 FeatureDefinition::Rib { .. } => Some("rib"),
6988 FeatureDefinition::Chamfer { .. } => Some("chamfer"),
6989 FeatureDefinition::Fillet { .. } => Some("fillet"),
6990 FeatureDefinition::FaceBlend { .. } => Some("face blend"),
6991 FeatureDefinition::SewBodies { .. } => Some("sew bodies"),
6992 FeatureDefinition::TrimBodies { .. } => Some("trim bodies"),
6993 FeatureDefinition::Extrude { .. } => Some("extrude"),
6994 FeatureDefinition::OffsetSurface { .. } => Some("offset surface"),
6995 FeatureDefinition::Thicken { .. } => Some("thicken"),
6996 FeatureDefinition::Draft { .. } => Some("draft"),
6997 FeatureDefinition::Pattern { .. } => Some("pattern"),
6998 FeatureDefinition::Combine { .. } => Some("body combine"),
6999 _ => None,
7000 }
7001}
7002
7003pub(crate) fn incomplete_expression_parameters(ir: &CadIr) -> BTreeSet<ParameterId> {
7004 let parameter_owners = ir
7005 .model
7006 .parameters
7007 .iter()
7008 .map(|parameter| parameter.owner.clone())
7009 .collect::<BTreeSet<_>>();
7010 let mut incomplete = BTreeSet::new();
7011 for owner in parameter_owners {
7012 let parameters = ir
7013 .model
7014 .parameters
7015 .iter()
7016 .filter(|parameter| parameter.owner == owner)
7017 .collect::<Vec<_>>();
7018 let mut ids_by_name = BTreeMap::<&str, Vec<&ParameterId>>::new();
7019 for parameter in ¶meters {
7020 ids_by_name
7021 .entry(parameter.name.as_str())
7022 .or_default()
7023 .push(¶meter.id);
7024 }
7025 let expected = parameters
7026 .iter()
7027 .map(|parameter| {
7028 let [_] = ids_by_name.get(parameter.name.as_str())?.as_slice() else {
7029 return None;
7030 };
7031 let mut seen = BTreeSet::new();
7032 let dependencies = crate::native::expression_parameter_names(¶meter.expression)
7033 .into_iter()
7034 .map(|name| {
7035 let [dependency] = ids_by_name.get(name)?.as_slice() else {
7036 return None;
7037 };
7038 Some((*dependency).clone())
7039 })
7040 .collect::<Option<Vec<_>>>()?;
7041 Some(
7042 dependencies
7043 .into_iter()
7044 .filter(|dependency| seen.insert(dependency.clone()))
7045 .collect::<Vec<_>>(),
7046 )
7047 })
7048 .collect::<Vec<_>>();
7049 let indices = parameters
7050 .iter()
7051 .enumerate()
7052 .map(|(index, parameter)| (¶meter.id, index))
7053 .collect::<BTreeMap<_, _>>();
7054 let mut emitted = BTreeSet::new();
7055 while let Some(index) = (0..parameters.len()).find(|index| {
7056 !emitted.contains(index)
7057 && expected[*index].as_ref().is_some_and(|dependencies| {
7058 dependencies.iter().all(|dependency| {
7059 indices
7060 .get(dependency)
7061 .is_some_and(|dependency| emitted.contains(dependency))
7062 })
7063 })
7064 }) {
7065 emitted.insert(index);
7066 }
7067 for (index, parameter) in parameters.into_iter().enumerate() {
7068 if expected[index].as_ref() != Some(¶meter.dependencies)
7069 || !emitted.contains(&index)
7070 || parameter.value.is_none()
7071 {
7072 incomplete.insert(parameter.id.clone());
7073 }
7074 }
7075 }
7076 incomplete
7077}
7078
7079pub(crate) fn hole_feature_is_incomplete(
7080 profile: Option<&ProfileRef>,
7081 face: Option<&FaceSelection>,
7082 position: Option<Point3>,
7083 direction: Option<Vector3>,
7084 treatments: (&HoleKind, Option<&HoleKind>),
7085 diameter: Option<Length>,
7086 extent: Option<&Termination>,
7087) -> bool {
7088 let (kind, exit_kind) = treatments;
7089 let profile_incomplete = profile.is_some_and(profile_ref_is_incomplete);
7090 let face_incomplete = face.is_some_and(face_selection_is_incomplete);
7091 let location_unresolved = position.is_none() && profile.is_none_or(profile_ref_is_incomplete);
7092 let orientation_unresolved =
7093 direction.is_none() && face.is_none_or(face_selection_is_incomplete);
7094 profile_incomplete
7095 || face_incomplete
7096 || location_unresolved
7097 || orientation_unresolved
7098 || matches!(kind, HoleKind::Unresolved { .. })
7099 || exit_kind.is_some_and(|kind| matches!(kind, HoleKind::Unresolved { .. }))
7100 || diameter.is_none()
7101 || extent.is_none_or(termination_is_incomplete)
7102}
7103
7104pub(crate) fn extrude_extent_is_incomplete(extent: &ExtrudeExtent) -> bool {
7105 match extent {
7106 ExtrudeExtent::OneSided { side } | ExtrudeExtent::Symmetric { side } => {
7107 termination_is_incomplete(&side.termination)
7108 }
7109 ExtrudeExtent::TwoSided { first, second } => {
7110 termination_is_incomplete(&first.termination)
7111 || termination_is_incomplete(&second.termination)
7112 }
7113 }
7114}
7115
7116pub(crate) fn termination_is_incomplete(termination: &Termination) -> bool {
7117 match termination {
7118 Termination::Unresolved => true,
7119 Termination::ToFace { face, .. } => face_selection_is_incomplete(face),
7120 Termination::ToShape { target } => face_selection_is_incomplete(target),
7121 Termination::Blind { .. }
7122 | Termination::ThroughAll
7123 | Termination::ThroughNext
7124 | Termination::ToFirst
7125 | Termination::ToLast
7126 | Termination::ToVertex { .. }
7127 | Termination::OffsetFromFace { .. }
7128 | Termination::Angle { .. } => false,
7129 }
7130}
7131
7132pub(crate) fn rib_feature_is_incomplete(construction: &RibConstruction, op: BooleanOp) -> bool {
7133 construction
7134 .profile
7135 .as_ref()
7136 .is_none_or(profile_ref_is_incomplete)
7137 || construction.direction.is_none()
7138 || construction.thickness.is_none()
7139 || construction.side.is_none()
7140 || matches!(construction.draft, RibDraft::Unresolved)
7141 || matches!(op, BooleanOp::Unresolved)
7142}
7143
7144pub(crate) fn pattern_is_incomplete(pattern: &PatternKind) -> bool {
7145 match pattern {
7146 PatternKind::Unresolved { .. } => true,
7147 PatternKind::Linear { direction, .. } => direction.is_none(),
7148 PatternKind::LinearOffsets { direction, offsets } => {
7149 direction.is_none() || offsets.is_empty()
7150 }
7151 PatternKind::Circular { .. } | PatternKind::Mirror { .. } => false,
7152 PatternKind::CircularAngles { angles, .. } => angles.is_empty(),
7153 PatternKind::CurveDriven { path, .. } => path.as_ref().is_none_or(path_ref_is_incomplete),
7154 PatternKind::Scale { center, .. } => {
7155 matches!(center, cadmpeg_ir::features::PatternScaleCenter::Native(_))
7156 }
7157 PatternKind::Composite { stages } => {
7158 stages.is_empty()
7159 || stages
7160 .iter()
7161 .any(|stage| pattern_is_incomplete(&stage.pattern))
7162 }
7163 }
7164}
7165
7166pub(crate) fn pattern_feature_is_incomplete(
7167 seeds: &[cadmpeg_ir::features::PatternSeed],
7168 pattern: &PatternKind,
7169) -> bool {
7170 seeds.is_empty()
7171 || seeds
7172 .iter()
7173 .enumerate()
7174 .any(|(index, seed)| seeds[..index].contains(seed))
7175 || pattern_is_incomplete(pattern)
7176}
7177
7178pub(crate) fn radius_spec_is_incomplete(radius: &RadiusSpec) -> bool {
7179 match radius {
7180 RadiusSpec::Unresolved { .. } => true,
7181 RadiusSpec::Constant { .. } => false,
7182 RadiusSpec::Chordal { .. } => false,
7183 RadiusSpec::Variable { points } => points.len() < 2,
7184 }
7185}
7186
7187pub(crate) fn body_selection_is_incomplete(selection: &BodySelection) -> bool {
7188 explicit_body_ids(selection).is_none_or(selection_ids_are_incomplete)
7189}
7190
7191pub(crate) fn body_selections_overlap(first: &BodySelection, second: &BodySelection) -> bool {
7192 explicit_body_ids(first).is_some_and(|first| {
7193 explicit_body_ids(second)
7194 .is_some_and(|second| first.iter().any(|body| second.contains(body)))
7195 })
7196}
7197
7198fn explicit_body_ids(selection: &BodySelection) -> Option<&[BodyId]> {
7199 match selection {
7200 BodySelection::Bodies(bodies) | BodySelection::Resolved { bodies, .. } => Some(bodies),
7201 BodySelection::Unresolved
7202 | BodySelection::Historical { .. }
7203 | BodySelection::Generated { .. }
7204 | BodySelection::Local { .. }
7205 | BodySelection::Native(_) => None,
7206 }
7207}
7208
7209pub(crate) fn face_selection_is_incomplete(selection: &FaceSelection) -> bool {
7210 match selection {
7211 FaceSelection::Unresolved
7212 | FaceSelection::Generated { .. }
7213 | FaceSelection::Native(_)
7214 | FaceSelection::Historical { .. }
7215 | FaceSelection::HistoricalPartial { .. } => true,
7216 FaceSelection::Faces(faces) | FaceSelection::Resolved { faces, .. } => {
7217 selection_ids_are_incomplete(faces)
7218 }
7219 }
7220}
7221
7222pub(crate) fn face_selections_overlap(first: &FaceSelection, second: &FaceSelection) -> bool {
7223 let first = match first {
7224 FaceSelection::Faces(faces) | FaceSelection::Resolved { faces, .. } => faces,
7225 FaceSelection::Unresolved
7226 | FaceSelection::Generated { .. }
7227 | FaceSelection::Native(_)
7228 | FaceSelection::Historical { .. }
7229 | FaceSelection::HistoricalPartial { .. } => return false,
7230 };
7231 let second = match second {
7232 FaceSelection::Faces(faces) | FaceSelection::Resolved { faces, .. } => faces,
7233 FaceSelection::Unresolved
7234 | FaceSelection::Generated { .. }
7235 | FaceSelection::Native(_)
7236 | FaceSelection::Historical { .. }
7237 | FaceSelection::HistoricalPartial { .. } => return false,
7238 };
7239 first.iter().any(|face| second.contains(face))
7240}
7241
7242pub(crate) fn edge_selection_is_incomplete(selection: &EdgeSelection) -> bool {
7243 match selection {
7244 EdgeSelection::Unresolved
7245 | EdgeSelection::Generated { .. }
7246 | EdgeSelection::Native(_)
7247 | EdgeSelection::Historical { .. }
7248 | EdgeSelection::HistoricalPartial { .. } => true,
7249 EdgeSelection::All => false,
7250 EdgeSelection::Edges(edges) | EdgeSelection::Resolved { edges, .. } => {
7251 selection_ids_are_incomplete(edges)
7252 }
7253 }
7254}
7255
7256pub(crate) fn profile_ref_is_incomplete(profile: &ProfileRef) -> bool {
7257 match profile {
7258 ProfileRef::Unresolved(_) | ProfileRef::Native(_) => true,
7259 ProfileRef::Sketch(_) => false,
7260 ProfileRef::Feature(_) | ProfileRef::Generated { .. } => false,
7261 ProfileRef::Faces(faces) => selection_ids_are_incomplete(faces),
7262 ProfileRef::SketchProfiles { .. }
7263 | ProfileRef::SketchRegions { .. }
7264 | ProfileRef::SketchSelection { .. }
7265 | ProfileRef::SpatialSketchProfiles { .. }
7266 | ProfileRef::SpatialSketchSelection { .. }
7267 | ProfileRef::HistoricalFaces { .. } => false,
7268 }
7269}
7270
7271fn selection_ids_are_incomplete<T: Ord>(ids: &[T]) -> bool {
7272 ids.is_empty() || ids.iter().collect::<BTreeSet<_>>().len() != ids.len()
7273}
7274
7275pub(crate) fn path_ref_is_incomplete(path: &PathRef) -> bool {
7276 match path {
7277 PathRef::Unresolved(_) | PathRef::Native(_) => true,
7278 PathRef::HistoricalEdges { edges, .. } => selection_ids_are_incomplete(edges),
7279 PathRef::Sketch(_) => false,
7280 PathRef::SpatialSketchSelection { selections, .. } => {
7281 selection_ids_are_incomplete(selections)
7282 }
7283 PathRef::Edges(edges) => selection_ids_are_incomplete(edges),
7284 PathRef::Curves(curves) => selection_ids_are_incomplete(curves),
7285 }
7286}
7287
7288fn build_metadata_ir(
7289 scan: &Scan,
7290) -> Result<(CadIr, cadmpeg_ir::Annotations, Vec<UnknownRecord>), CodecError> {
7291 let mut ir = CadIr::empty(Units::default());
7292 let mut annotations = AnnotationBuilder::new();
7293 let mut unknowns = Vec::new();
7294 ir.source = Some(source_meta(scan));
7295 for (si, stream) in scan.streams.iter().enumerate() {
7296 if stream.kind.is_parasolid() {
7297 let unknown = unknown_stream(si, stream);
7298 let source_stream = annotations.stream("nx:container");
7299 annotations
7300 .note(&unknown.id, source_stream, stream.file_offset as u64)
7301 .tag(stream.kind.label());
7302 annotations.exactness(&unknown.id, Exactness::Derived);
7303 unknowns.push(unknown);
7304 }
7305 }
7306 let parsed = crate::native::ParsedStreams::parse(scan);
7307 let model = crate::native::NativeModel::extract(&scan.container, &scan.streams, &parsed);
7308 crate::native::attach_annotations(&mut ir, &model, scan, &mut annotations, &mut unknowns)
7309 .map_err(|error| CodecError::Malformed(error.to_string()))?;
7310 Ok((ir, annotations.build(), unknowns))
7311}
7312
7313fn build_container_report(scan: &Scan, container_only: bool) -> DecodeReport {
7314 let mut losses = Vec::new();
7315
7316 let assembly = scan
7317 .container
7318 .entries
7319 .iter()
7320 .any(|e| e.name.contains("ExternalReferences"))
7321 && !scan.has_parasolid();
7322
7323 if assembly {
7324 losses.push(LossNote {
7325 code: LossCode::AssemblyComponentsExternal,
7326 category: LossCategory::Geometry,
7327 severity: Severity::Blocking,
7328 message: "No inline Parasolid geometry: this is an assembly .prt. Component geometry \
7329 lives in external child .prt files named in EXTREFSTREAM, and the assembled \
7330 solid's inputs (child partitions + constraint solve) are absent from this \
7331 file. This is an external-dependency boundary, not a decode gap."
7332 .to_string(),
7333 provenance: None,
7334 });
7335 } else {
7336 losses.push(LossNote {
7337 code: LossCode::GeometryNotTransferred,
7338 category: LossCategory::Geometry,
7339 severity: Severity::Blocking,
7340 message: "No B-rep geometry was transferred: no gate-passing analytic carrier was found \
7341 in the embedded Parasolid streams (they may hold only B-spline/procedural \
7342 geometry this codec does not yet type). The streams are preserved verbatim as \
7343 unknown passthrough records."
7344 .to_string(),
7345 provenance: None,
7346 });
7347 }
7348
7349 if container_only {
7350 losses.push(LossNote {
7351 code: LossCode::ContainerOnly,
7352 category: LossCategory::Geometry,
7353 severity: Severity::Info,
7354 message: "Container-only decode requested; entity decode was not attempted."
7355 .to_string(),
7356 provenance: None,
7357 });
7358 }
7359
7360 DecodeReport {
7361 format: "nx".to_string(),
7362 container_only,
7363 geometry_transferred: false,
7364 coverage: std::collections::BTreeMap::new(),
7365 losses,
7366 notes: summary_notes(scan),
7367 }
7368}
7369
7370pub fn summary_notes(scan: &Scan) -> Vec<String> {
7372 let c = &scan.container;
7373 let mut notes = vec![format!(
7374 "SPLMSSTR container: version {:#04x}, file tag {}, footer offset {}, {} directory entry/ies",
7375 c.version,
7376 c.file_tag,
7377 c.footer_offset,
7378 c.entries.len()
7379 )];
7380 notes.push(format!(
7381 "embedded streams: {} partition, {} deltas, {} plain (cached body), {} preview/non-Parasolid",
7382 scan.count(StreamKind::Partition),
7383 scan.count(StreamKind::Deltas),
7384 scan.count(StreamKind::Plain),
7385 scan.count(StreamKind::Preview),
7386 ));
7387 if let Some(schema) = scan.streams.iter().find_map(|s| s.schema.as_deref()) {
7388 notes.push(format!("Parasolid schema: {schema}"));
7389 }
7390 let framed_om_sections = c.om_sections();
7391 if !framed_om_sections.is_empty() {
7392 let declarations = framed_om_sections
7393 .iter()
7394 .map(|(_, section)| section.types.len())
7395 .sum::<usize>();
7396 let fields = framed_om_sections
7397 .iter()
7398 .map(|(_, section)| section.fields.len())
7399 .sum::<usize>();
7400 notes.push(format!(
7401 "NX object model: {} size-framed section(s), {} class declaration(s), {} field declaration(s)",
7402 framed_om_sections.len(),
7403 declarations,
7404 fields
7405 ));
7406 }
7407 let om_sections = c.indexed_om_sections();
7408 if !om_sections.is_empty() {
7409 let entities = om_sections
7410 .iter()
7411 .filter(|(_, section)| {
7412 section
7413 .records
7414 .first()
7415 .is_some_and(|record| record.object_id.is_some())
7416 })
7417 .map(|(_, section)| section.records.len())
7418 .sum::<usize>();
7419 let blocks = om_sections
7420 .iter()
7421 .filter(|(_, section)| {
7422 section
7423 .records
7424 .first()
7425 .is_some_and(|record| record.object_id.is_none())
7426 })
7427 .map(|(_, section)| section.records.len() + usize::from(section.control.is_some()))
7428 .sum::<usize>();
7429 if blocks == 0 {
7430 notes.push(format!(
7431 "NX object model: {} indexed section(s), {} bounded entity record(s)",
7432 om_sections.len(),
7433 entities
7434 ));
7435 } else {
7436 notes.push(format!(
7437 "NX object model: {} indexed section(s), {} ID-bounded entity record(s), {} offset-only data block(s)",
7438 om_sections.len(),
7439 entities,
7440 blocks
7441 ));
7442 }
7443 }
7444 if !scan.has_parasolid()
7445 && c.entries
7446 .iter()
7447 .any(|e| e.name.contains("ExternalReferences"))
7448 {
7449 notes.push(
7450 "no inline Parasolid geometry (assembly .prt: geometry in external child parts)"
7451 .to_string(),
7452 );
7453 }
7454 notes
7455}