1use super::parse::{Arg, Instance, Instances};
24use ogeom_algo::{
25 make_edge_between, make_face_on, make_shell, make_solid, make_vertex, make_wire,
26 project_on_curve,
27};
28use ogeom_core::{FastMap, FastSet};
29use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
30use ogeom_doc::ConstructionGeometry;
31use ogeom_geom::Curve2d as _;
32use ogeom_geom::Curve3d as _;
33use ogeom_geom::Surface as _;
34use ogeom_geom::Transformable as _;
35use ogeom_geom::{
36 BSplineCurve, CircleCurve, ConeSurface, Curve, CylinderSurface, EllipseCurve, ExtrusionSurface,
37 HyperbolaCurve, LineCurve, ParabolaCurve, PlanarCurve, PlaneSurface, SphereSurface,
38 SurfaceGeometry, TorusSurface,
39};
40use ogeom_math::{
41 Axis, Blend as _, Circle, Cone, Cylinder, Direction, Ellipse, Frame, Hyperbola, KnotVector,
42 Matrix3, Parabola, Plane, Point, Sphere, Torus, Transform, Vector, Weighted,
43};
44use ogeom_topo::{Location, Model, Shape};
45use std::collections::BTreeMap;
46
47type BoundUse = (Shape, Shape, u64, Curve, (f64, f64));
50
51type ChartReading = dyn Fn(Point) -> (Option<f64>, Option<f64>);
54
55const SURFACE_EXTENT: f64 = 1e5;
59
60#[derive(Debug, Default)]
62pub struct StepReport {
63 pub scale_mm: f64,
65 pub skipped: BTreeMap<String, usize>,
69 pub warnings: Vec<String>,
71 pub summary: Vec<WarningSummary>,
77 pub untrimmed_faces: Vec<UntrimmedFace>,
86 pub pcurves_kept: usize,
90 pub pcurves_derived: usize,
93}
94
95#[derive(Debug, Clone)]
97pub struct WarningSummary {
98 pub kind: &'static str,
101 pub count: usize,
103 pub worst: f64,
106 pub exemplar: u64,
108}
109
110#[derive(Debug, Clone)]
112pub struct UntrimmedFace {
113 pub entity: u64,
115 pub face: Shape,
118}
119
120#[derive(Debug)]
122pub struct StepImport {
123 pub document: ogeom_doc::Document,
126 pub solids: Vec<Shape>,
129 pub shells: Vec<Shape>,
137 pub faces: Vec<(u64, Shape)>,
142 pub report: StepReport,
144}
145
146pub fn read_step(text: &str, tol: Tolerances) -> OgeomResult<StepImport> {
159 let (_, instances) = super::parse::parse_instances(text)?;
160 let mut reader = Reader {
161 instances: &instances,
162 model: Model::new(),
163 report: StepReport {
164 scale_mm: 1.0,
165 ..StepReport::default()
166 },
167 angle_scale: 1.0,
168 visited: Visited::for_instances(&instances),
169 vertices: FastMap::default(),
170 edges: FastMap::default(),
171 faces: FastMap::default(),
172 callout_index: FastMap::default(),
173 pcurves: FastMap::default(),
174 pieces: FastMap::default(),
175 band_faces: FastSet::default(),
176 unnamed_colours: FastSet::default(),
177 pole_vertices: Vec::new(),
178 piece_key: u64::MAX,
179 untrimmed_ids: Vec::new(),
180 tallies: FastMap::default(),
181 cdsr_of_nauo: None,
182 properties_of_definition: None,
183 sdrs_of_property: None,
184 depth: 0,
185 tol,
186 };
187 reader.report.scale_mm = reader.unit_scale();
188 reader.angle_scale = reader.angle_unit_scale();
189
190 let mut solids = Vec::new();
191 let mut shells = Vec::new();
192 let mut by_item: FastMap<u64, Shape> = FastMap::default();
193 let mut ids: Vec<(u64, bool)> = instances
196 .iter()
197 .filter_map(|(id, inst)| {
198 if inst.part("MANIFOLD_SOLID_BREP").is_some() || inst.part("BREP_WITH_VOIDS").is_some()
199 {
200 Some((id, true))
201 } else if inst.part("SHELL_BASED_SURFACE_MODEL").is_some() {
202 Some((id, false))
203 } else {
204 None
205 }
206 })
207 .collect();
208 ids.sort_unstable();
209 let total = ids.len() as u64;
210 for (done, (id, is_solid)) in ids.into_iter().enumerate() {
211 ogeom_core::progress::checkpoint()?;
212 ogeom_core::progress::stage_at("step: solid", done as u64 + 1, total);
213 if is_solid {
214 let solid = reader.solid(id)?;
215 by_item.insert(id, solid.clone());
216 solids.push(solid);
217 } else {
218 let shell = reader.surface_model(id)?;
219 by_item.insert(id, shell.clone());
220 shells.push(shell);
221 }
222 }
223 for body in solids.iter().chain(shells.iter()) {
228 ogeom_algo::restore_containment(&mut reader.model, body)?;
229 }
230 {
233 let mut entries: Vec<WarningSummary> = reader
234 .tallies
235 .drain()
236 .map(|(kind, (count, worst, exemplar))| WarningSummary {
237 kind,
238 count,
239 worst,
240 exemplar,
241 })
242 .collect();
243 entries.sort_by(|a, b| b.count.cmp(&a.count).then(a.kind.cmp(b.kind)));
244 reader.report.summary = entries;
245 }
246 for id in std::mem::take(&mut reader.untrimmed_ids) {
250 if let Some(face) = reader.faces.get(&id) {
251 reader.report.untrimmed_faces.push(UntrimmedFace {
252 entity: id,
253 face: face.clone(),
254 });
255 }
256 }
257 crate::bodies::rechart_beside_poles(
258 &mut reader.model,
259 solids.iter().chain(shells.iter()),
260 &mut reader.report.warnings,
261 tol,
262 )?;
263 let construction = reader.construction_sets()?;
264 if solids.is_empty() && shells.is_empty() && construction.is_empty() {
265 ogeom_bail!(
266 Construction,
267 "the exchange file contains no MANIFOLD_SOLID_BREP, \
268 BREP_WITH_VOIDS, SHELL_BASED_SURFACE_MODEL or geometric set to read"
269 );
270 }
271 let mut held: FastMap<ogeom_topo::TShapeId, Shape> = FastMap::default();
275 for body in solids.iter().chain(shells.iter()) {
276 for face in ogeom_topo::explore_unique(&reader.model, body, ogeom_topo::ShapeType::Face)? {
277 held.entry(face.node()).or_insert(face);
278 }
279 }
280 let mut faces: Vec<(u64, Shape)> = reader
281 .faces
282 .iter()
283 .filter_map(|(&id, face)| Some((id, held.get(&face.node())?.clone())))
284 .collect();
285 faces.sort_unstable_by_key(|&(id, _)| id);
286 let document = reader.document(&by_item, &solids, &shells, construction)?;
287
288 for (id, instance) in instances.iter() {
290 if !reader.visited.seen(id) {
291 *reader
292 .report
293 .skipped
294 .entry(instance.keyword().to_owned())
295 .or_default() += 1;
296 }
297 }
298
299 Ok(StepImport {
300 document,
301 solids,
302 shells,
303 faces,
304 report: reader.report,
305 })
306}
307
308type ConstructionSets = Vec<(u64, Vec<(String, ConstructionGeometry)>)>;
311
312type BuiltEdge = (Shape, Curve, (f64, f64), bool);
315
316enum PreparedPcurve {
324 Stated { curve: PlanarCurve, off: f64 },
327 Exact { curve: PlanarCurve, off: f64 },
330 Fitted {
333 curve: PlanarCurve,
334 error: f64,
335 met: bool,
336 worst_off: f64,
337 off: f64,
338 warning: Option<String>,
339 },
340 Refused(String),
342}
343
344struct Reader<'a> {
345 instances: &'a Instances,
346 model: Model,
347 report: StepReport,
348 angle_scale: f64,
350 visited: Visited,
352 vertices: FastMap<u64, Shape>,
353 edges: FastMap<u64, BuiltEdge>,
354 faces: FastMap<u64, Shape>,
355 callout_index: FastMap<u64, usize>,
357 pcurves: FastMap<(u64, u64), (PreparedPcurve, bool)>,
361 pieces: FastMap<u64, Vec<(u64, BuiltEdge)>>,
365 band_faces: FastSet<u64>,
369 unnamed_colours: FastSet<u64>,
372 pole_vertices: Vec<Shape>,
374 piece_key: u64,
377 untrimmed_ids: Vec<u64>,
380 tallies: FastMap<&'static str, (usize, f64, u64)>,
382 cdsr_of_nauo: Option<FastMap<u64, u64>>,
386 properties_of_definition: Option<FastMap<u64, Vec<u64>>>,
392 sdrs_of_property: Option<FastMap<u64, Vec<u64>>>,
393 depth: u32,
398 tol: Tolerances,
399}
400
401const MOST_NESTING: u32 = 64;
405
406impl<'a> Reader<'a> {
407 fn nested<T>(&mut self, build: impl FnOnce(&mut Self) -> OgeomResult<T>) -> OgeomResult<T> {
409 if self.depth >= MOST_NESTING {
410 ogeom_bail!(
411 Construction,
412 "entities refer to one another more than {MOST_NESTING} deep; \
413 the file refers in a circle"
414 );
415 }
416 self.depth += 1;
417 let built = build(self);
418 self.depth -= 1;
419 built
420 }
421
422 fn instance(&mut self, id: u64) -> OgeomResult<&'a Instance> {
425 self.visited.mark(id);
426 let instances = self.instances;
427 instances.get(id).ok_or_else(|| {
428 ogeom_core::ogeom_err!(
429 Construction,
430 "the file references #{id}, which does not exist"
431 )
432 })
433 }
434
435 fn face_args(&mut self, id: u64) -> OgeomResult<&'a [Arg]> {
439 let instance = self.instance(id)?;
440 if let Some(args) = instance
441 .part("ADVANCED_FACE")
442 .or_else(|| instance.part("FACE_SURFACE"))
443 {
444 return Ok(args);
445 }
446 ogeom_bail!(
447 Construction,
448 "#{id} is {}, where a face (ADVANCED_FACE or FACE_SURFACE) was needed",
449 instance.keyword()
450 );
451 }
452
453 fn args(&mut self, id: u64, keyword: &str) -> OgeomResult<&'a [Arg]> {
454 let instance = self.instance(id)?;
455 let Some(args) = instance.part(keyword) else {
456 ogeom_bail!(
457 Construction,
458 "#{id} is {}, where {keyword} was needed",
459 instance.keyword()
460 );
461 };
462 Ok(args)
463 }
464
465 fn assigned_units(&self) -> Vec<u64> {
473 let mut cited: Vec<u64> = self
479 .instances
480 .values()
481 .filter(|inst| {
482 inst.parts
483 .iter()
484 .any(|(k, _)| k.ends_with("SHAPE_REPRESENTATION"))
485 })
486 .filter_map(|inst| {
487 inst.parts
488 .iter()
489 .find(|(k, _)| k.ends_with("SHAPE_REPRESENTATION"))
490 .and_then(|(_, args)| args.last())
491 .and_then(Arg::reference)
492 })
493 .collect();
494 cited.sort_unstable();
495 cited.dedup();
496
497 let mut contexts: Vec<u64> = self
498 .instances
499 .iter()
500 .filter(|(_, inst)| inst.part("GLOBAL_UNIT_ASSIGNED_CONTEXT").is_some())
501 .map(|(id, _)| id)
502 .collect();
503 contexts.sort_unstable();
504 contexts.sort_by_cached_key(|id| !cited.contains(id));
505
506 let mut out = Vec::new();
507 for id in contexts {
508 if let Some(instance) = self.instances.get(id)
509 && let Some(args) = instance.part("GLOBAL_UNIT_ASSIGNED_CONTEXT")
510 {
511 for arg in args.iter().filter_map(Arg::list).flatten() {
512 if let Some(r) = arg.reference() {
513 out.push(r);
514 }
515 }
516 }
517 }
518 out
519 }
520
521 fn unit_scale(&mut self) -> f64 {
523 let assigned = self.assigned_units();
524 for id in assigned {
525 let Some(instance) = self.instances.get(id) else {
526 continue;
527 };
528 let instance = instance.clone();
529 if instance.part("LENGTH_UNIT").is_none() {
530 continue;
531 }
532 if let Some(args) = instance.part("SI_UNIT") {
533 return match args.first() {
534 Some(Arg::Enum(prefix)) => match prefix.as_str() {
535 "MILLI" => 1.0,
536 "CENTI" => 10.0,
537 "DECI" => 100.0,
538 "KILO" => 1e6,
539 "MICRO" => 1e-3,
540 other => {
541 self.report
542 .warnings
543 .push(format!("#{id}: unknown SI prefix {other}; taking metres"));
544 1000.0
545 }
546 },
547 _ => 1000.0,
548 };
549 }
550 if let Some(args) = instance.part("CONVERSION_BASED_UNIT")
551 && let Some(measure) = args.get(1).and_then(Arg::reference)
552 && let Some(inner) = self.instances.get(measure)
553 {
554 let factor = inner
555 .parts
556 .iter()
557 .flat_map(|(_, a)| a.iter())
558 .find_map(|a| match a {
559 Arg::Typed(typed) if typed.0 == "LENGTH_MEASURE" => {
560 typed.1.first().and_then(Arg::number)
561 }
562 _ => None,
563 });
564 let base = inner
565 .parts
566 .iter()
567 .flat_map(|(_, a)| a.iter())
568 .find_map(Arg::reference)
569 .and_then(|b| self.instances.get(b))
570 .and_then(|u| u.part("SI_UNIT"))
571 .map_or(1000.0, |args| match args.first() {
572 Some(Arg::Enum(p)) if p == "MILLI" => 1.0,
573 Some(Arg::Enum(p)) if p == "CENTI" => 10.0,
574 _ => 1000.0,
575 });
576 if let Some(f) = factor {
577 return f * base;
578 }
579 }
580 }
581 self.report
582 .warnings
583 .push("no length unit found; taking millimetres".to_owned());
584 1.0
585 }
586
587 fn angle_unit_scale(&mut self) -> f64 {
590 let assigned = self.assigned_units();
591 for id in assigned {
592 let Some(instance) = self.instances.get(id) else {
593 continue;
594 };
595 let instance = instance.clone();
596 if instance.part("PLANE_ANGLE_UNIT").is_none() {
597 continue;
598 }
599 if instance.part("SI_UNIT").is_some() {
600 return 1.0;
601 }
602 if let Some(args) = instance.part("CONVERSION_BASED_UNIT")
603 && let Some(measure) = args.get(1).and_then(Arg::reference)
604 && let Some(inner) = self.instances.get(measure)
605 {
606 let factor = inner
607 .parts
608 .iter()
609 .flat_map(|(_, a)| a.iter())
610 .find_map(|a| match a {
611 Arg::Typed(typed) if typed.0 == "PLANE_ANGLE_MEASURE" => {
612 typed.1.first().and_then(Arg::number)
613 }
614 _ => None,
615 });
616 if let Some(f) = factor {
617 return f;
618 }
619 }
620 }
621 1.0
622 }
623
624 fn point(&mut self, id: u64) -> OgeomResult<Point> {
627 let args = self.args(id, "CARTESIAN_POINT")?;
628 let Some(coords) = args.get(1).and_then(Arg::list) else {
629 ogeom_bail!(Construction, "#{id}: a point without coordinates");
630 };
631 let scale = self.report.scale_mm;
632 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0) * scale;
633 Ok(Point::new(value(0), value(1), value(2)))
634 }
635
636 fn direction(&mut self, id: u64) -> OgeomResult<Direction> {
637 let args = self.args(id, "DIRECTION")?;
638 let Some(coords) = args.get(1).and_then(Arg::list) else {
639 ogeom_bail!(Construction, "#{id}: a direction without components");
640 };
641 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0);
642 Direction::new(Vector::new(value(0), value(1), value(2)), self.tol)
643 }
644
645 fn frame(&mut self, id: u64) -> OgeomResult<Frame> {
648 let args = self.args(id, "AXIS2_PLACEMENT_3D")?;
649 let origin = args
650 .get(1)
651 .and_then(Arg::reference)
652 .map(|r| self.point(r))
653 .transpose()?
654 .unwrap_or(Point::ORIGIN);
655 let z = args
656 .get(2)
657 .and_then(Arg::reference)
658 .map(|r| self.direction(r))
659 .transpose()?
660 .unwrap_or(Direction::Z);
661 let x = match args.get(3).and_then(Arg::reference) {
662 Some(r) => self.direction(r)?,
663 None => Direction::from_cross(z.vector(), Vector::new(0.31, 0.52, 0.8), self.tol)?,
664 };
665 Frame::new(origin, z, x, self.tol)
666 }
667
668 fn surface(&mut self, id: u64) -> OgeomResult<Option<SurfaceGeometry>> {
669 self.nested(|reader| reader.surface_at(id))
670 }
671
672 fn surface_at(&mut self, id: u64) -> OgeomResult<Option<SurfaceGeometry>> {
673 let (keyword, args) = {
674 let instance = self.instance(id)?;
675 (
676 instance.keyword(),
677 instance
678 .parts
679 .first()
680 .map_or(&[][..], |(_, a)| a.as_slice()),
681 )
682 };
683 let scale = self.report.scale_mm;
684 let radius_arg = |args: &[Arg], i: usize| args.get(i).and_then(Arg::number);
685 {
689 let (base, knots_part, weights) = {
690 let instance = self.instance(id)?;
691 (
692 instance.part("B_SPLINE_SURFACE"),
693 instance.part("B_SPLINE_SURFACE_WITH_KNOTS"),
694 instance.part("RATIONAL_B_SPLINE_SURFACE"),
695 )
696 };
697 if let Some(kp) = knots_part {
698 let (degrees, grid_arg, mults_knots) = if let Some(base) = base {
699 (
700 (base.first().cloned(), base.get(1).cloned()),
701 base.get(2).cloned(),
702 (
703 kp.first().cloned(),
704 kp.get(1).cloned(),
705 kp.get(2).cloned(),
706 kp.get(3).cloned(),
707 ),
708 )
709 } else {
710 (
711 (kp.get(1).cloned(), kp.get(2).cloned()),
712 kp.get(3).cloned(),
713 (
714 kp.get(8).cloned(),
715 kp.get(9).cloned(),
716 kp.get(10).cloned(),
717 kp.get(11).cloned(),
718 ),
719 )
720 };
721 return self
722 .bspline_surface(id, degrees, grid_arg, mults_knots, weights, None)
723 .map(Some);
724 }
725 }
726 {
729 let (base, weights, form) = {
730 let instance = self.instance(id)?;
731 (
732 instance.part("B_SPLINE_SURFACE"),
733 instance.part("RATIONAL_B_SPLINE_SURFACE"),
734 ["BEZIER_SURFACE", "UNIFORM_SURFACE", "QUASI_UNIFORM_SURFACE"]
735 .into_iter()
736 .find(|k| instance.part(k).is_some()),
737 )
738 };
739 if let (Some(base), Some(form)) = (base, form) {
740 return self
741 .bspline_surface(
742 id,
743 (base.first().cloned(), base.get(1).cloned()),
744 base.get(2).cloned(),
745 (None, None, None, None),
746 weights,
747 Some(form),
748 )
749 .map(Some);
750 }
751 if let Some(form) = ["BEZIER_SURFACE", "UNIFORM_SURFACE", "QUASI_UNIFORM_SURFACE"]
752 .into_iter()
753 .find(|k| *k == keyword)
754 {
755 return self
756 .bspline_surface(
757 id,
758 (args.get(1).cloned(), args.get(2).cloned()),
759 args.get(3).cloned(),
760 (None, None, None, None),
761 None,
762 Some(form),
763 )
764 .map(Some);
765 }
766 }
767 let out = match keyword {
768 "PLANE" => {
769 let frame = self.frame(ref_at(args, 1))?;
770 Some(
771 PlaneSurface::over(
772 Plane::new(frame),
773 (-SURFACE_EXTENT, SURFACE_EXTENT),
774 (-SURFACE_EXTENT, SURFACE_EXTENT),
775 )?
776 .into(),
777 )
778 }
779 "CYLINDRICAL_SURFACE" => {
780 let frame = self.frame(ref_at(args, 1))?;
781 let radius = radius_arg(args, 2).unwrap_or(0.0) * scale;
782 Some(
783 CylinderSurface::new(
784 Cylinder::new(frame, radius, self.tol)?,
785 (-SURFACE_EXTENT, SURFACE_EXTENT),
786 )?
787 .into(),
788 )
789 }
790 "CONICAL_SURFACE" => {
791 let frame = self.frame(ref_at(args, 1))?;
792 let radius = radius_arg(args, 2).unwrap_or(0.0) * scale;
793 let angle = radius_arg(args, 3).unwrap_or(0.0) * self.angle_scale;
794 Some(
795 ConeSurface::new(
796 Cone::new(frame, radius, angle, self.tol)?,
797 (-SURFACE_EXTENT, SURFACE_EXTENT),
798 )?
799 .into(),
800 )
801 }
802 "SPHERICAL_SURFACE" => {
803 let frame = self.frame(ref_at(args, 1))?;
804 let radius = radius_arg(args, 2).unwrap_or(0.0) * scale;
805 Some(SphereSurface::new(Sphere::new(frame, radius, self.tol)?).into())
806 }
807 "TOROIDAL_SURFACE" => {
808 let frame = self.frame(ref_at(args, 1))?;
809 let major = radius_arg(args, 2).unwrap_or(0.0) * scale;
810 let minor = radius_arg(args, 3).unwrap_or(0.0) * scale;
811 Some(TorusSurface::new(Torus::new(frame, major, minor, self.tol)?).into())
812 }
813 "SURFACE_OF_LINEAR_EXTRUSION" => {
814 let Some(curve) = self.curve(ref_at(args, 1))? else {
822 self.report.warnings.push(format!(
823 "#{id}: an extrusion's swept curve is not read; its face is skipped"
824 ));
825 return Ok(None);
826 };
827 let vector = self.args(ref_at(args, 2), "VECTOR")?;
828 let direction = self.direction(vector[1].reference().unwrap_or(0))?;
829 let parallel =
833 |axis: Direction| axis.vector().cross(direction.vector()).magnitude() <= 1e-6;
834 match &curve {
835 Curve::Circle(c) if parallel(c.circle().frame().z()) => Some(
836 CylinderSurface::new(
837 Cylinder::new(c.circle().frame(), c.circle().radius(), self.tol)?,
838 (-SURFACE_EXTENT, SURFACE_EXTENT),
839 )?
840 .into(),
841 ),
842 Curve::Line(l) if !parallel(l.axis().direction) => {
843 let axis = l.axis();
844 let normal = Direction::from_cross(
845 axis.direction.vector(),
846 direction.vector(),
847 self.tol,
848 )?;
849 let frame = Frame::new(axis.location, normal, axis.direction, self.tol)?;
850 Some(
851 PlaneSurface::over(
852 Plane::new(frame),
853 (-SURFACE_EXTENT, SURFACE_EXTENT),
854 (-SURFACE_EXTENT, SURFACE_EXTENT),
855 )?
856 .into(),
857 )
858 }
859 _ => Some(
860 ExtrusionSurface::over(
861 curve,
862 direction,
863 (-SURFACE_EXTENT, SURFACE_EXTENT),
864 )?
865 .into(),
866 ),
867 }
868 }
869 "SURFACE_OF_REVOLUTION" => {
870 let Some(curve) = self.curve(ref_at(args, 1))? else {
871 self.report.warnings.push(format!(
872 "#{id}: a revolution's swept curve is not read; its face is skipped"
873 ));
874 return Ok(None);
875 };
876 let placement = self.args(ref_at(args, 2), "AXIS1_PLACEMENT")?;
877 let location = self.point(placement[1].reference().unwrap_or(0))?;
878 let direction = match placement.get(2).and_then(Arg::reference) {
879 Some(r) => self.direction(r)?,
880 None => Direction::Z,
881 };
882 Some(
883 ogeom_geom::RevolutionSurface::new(
884 curve,
885 Axis {
886 location,
887 direction,
888 },
889 core::f64::consts::TAU,
890 )?
891 .into(),
892 )
893 }
894 "OFFSET_SURFACE" => {
895 let Some(basis) = self.surface(ref_at(args, 1))? else {
896 return Ok(None);
897 };
898 let distance = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
899 if distance == 0.0 {
900 return Ok(Some(basis));
901 }
902 let offset = ogeom_geom::OffsetSurface::new(basis, distance)?;
905 Some(match offset.analytic(self.tol)? {
906 Some(analytic) => analytic,
907 None => SurfaceGeometry::Offset(Box::new(offset)),
908 })
909 }
910 "RECTANGULAR_TRIMMED_SURFACE" | "CURVE_BOUNDED_SURFACE" => {
913 self.surface(ref_at(args, 1))?
914 }
915 "DEGENERATE_TOROIDAL_SURFACE" => {
916 let frame = self.frame(ref_at(args, 1))?;
917 let major = radius_arg(args, 2).unwrap_or(0.0) * scale;
918 let minor = radius_arg(args, 3).unwrap_or(0.0) * scale;
919 Some(TorusSurface::new(Torus::new(frame, major, minor, self.tol)?).into())
920 }
921 "RECTANGULAR_COMPOSITE_SURFACE" => Some(self.composite_surface(id, args)?),
922 "SURFACE_REPLICA" => {
923 let Some(parent) = self.surface(ref_at(args, 1))? else {
924 return Ok(None);
925 };
926 let motion = self.transformation_operator(ref_at(args, 2))?;
927 Some(parent.transformed(&motion, self.tol)?)
928 }
929 other => {
930 self.report.warnings.push(format!(
931 "#{id}: surface kind {other} is not read yet; its face is skipped"
932 ));
933 None
934 }
935 };
936 Ok(out)
937 }
938
939 fn expand_knots(mults: Option<Arg>, knots: Option<Arg>) -> OgeomResult<Vec<f64>> {
941 let (Some(Arg::List(mults)), Some(Arg::List(knots))) = (mults, knots) else {
942 return Ok(Vec::new());
943 };
944 expanded_knots(&mults, &knots)
945 }
946
947 #[allow(clippy::type_complexity)]
948 fn bspline_surface(
949 &mut self,
950 id: u64,
951 degrees: (Option<Arg>, Option<Arg>),
952 grid_arg: Option<Arg>,
953 mults_knots: (Option<Arg>, Option<Arg>, Option<Arg>, Option<Arg>),
954 weights: Option<&[Arg]>,
955 form: Option<&str>,
956 ) -> OgeomResult<SurfaceGeometry> {
957 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
958 let (u_degree, v_degree) = (
959 file_degree(degrees.0.as_ref(), 1.0)?,
960 file_degree(degrees.1.as_ref(), 1.0)?,
961 );
962 let Some(Arg::List(rows)) = grid_arg else {
963 ogeom_bail!(
964 Construction,
965 "#{id}: a b-spline surface without control points"
966 );
967 };
968 let mut points = Vec::new();
969 let (mut u_count, mut v_count) = (0, 0);
970 for row in &rows {
971 let Some(cells) = row.list() else { continue };
972 u_count += 1;
973 v_count = cells.len();
974 for cell in cells {
975 points.push(self.point(cell.reference().unwrap_or(0))?);
976 }
977 }
978 let (u_raw, v_raw) = match form {
979 Some(form) => (
980 implied_knots(form, u_degree, u_count),
981 implied_knots(form, v_degree, v_count),
982 ),
983 None => (
984 Self::expand_knots(mults_knots.0, mults_knots.2)?,
985 Self::expand_knots(mults_knots.1, mults_knots.3)?,
986 ),
987 };
988 let u_knots = KnotVector::new(u_raw, u_degree)?;
989 let v_knots = KnotVector::new(v_raw, v_degree)?;
990 let surface = if let Some(weights) = weights {
991 let flat: Vec<f64> = weights
992 .first()
993 .and_then(Arg::list)
994 .unwrap_or(&[])
995 .iter()
996 .filter_map(Arg::list)
997 .flatten()
998 .filter_map(Arg::number)
999 .collect();
1000 let weighted: Vec<ogeom_math::Weighted<Point>> = points
1001 .iter()
1002 .zip(flat.iter().chain(std::iter::repeat(&1.0)))
1003 .map(|(p, w)| ogeom_math::Weighted::new(*p, *w, self.tol))
1004 .collect::<OgeomResult<_>>()?;
1005 ogeom_geom::BSplineSurface::rational(
1006 u_knots,
1007 v_knots,
1008 ogeom_math::ControlGrid::new(weighted, u_count, v_count)?,
1009 )?
1010 } else {
1011 ogeom_geom::BSplineSurface::new(
1012 u_knots,
1013 v_knots,
1014 &ogeom_math::ControlGrid::new(points, u_count, v_count)?,
1015 self.tol,
1016 )?
1017 };
1018 Ok(surface.into())
1019 }
1020
1021 fn curve(&mut self, id: u64) -> OgeomResult<Option<Curve>> {
1022 self.nested(|reader| reader.curve_at(id))
1023 }
1024
1025 fn curve_at(&mut self, id: u64) -> OgeomResult<Option<Curve>> {
1026 let (keyword, args) = {
1027 let instance = self.instance(id)?;
1028 (
1029 instance.keyword(),
1030 instance
1031 .parts
1032 .first()
1033 .map_or(&[][..], |(_, a)| a.as_slice()),
1034 )
1035 };
1036 let scale = self.report.scale_mm;
1037 {
1038 let (base, kp, weights) = {
1039 let instance = self.instance(id)?;
1040 (
1041 instance.part("B_SPLINE_CURVE"),
1042 instance.part("B_SPLINE_CURVE_WITH_KNOTS"),
1043 instance.part("RATIONAL_B_SPLINE_CURVE"),
1044 )
1045 };
1046 let form = {
1051 let instance = self.instance(id)?;
1052 ["BEZIER_CURVE", "UNIFORM_CURVE", "QUASI_UNIFORM_CURVE"]
1053 .into_iter()
1054 .find(|k| instance.part(k).is_some())
1055 };
1056 if let Some(base) = base
1057 && (kp.is_some() || form.is_some())
1058 {
1059 let degree = file_degree(base.first(), 1.0)?;
1060 let control: Vec<Point> = base
1061 .get(1)
1062 .and_then(Arg::list)
1063 .unwrap_or(&[])
1064 .iter()
1065 .filter_map(Arg::reference)
1066 .map(|r| self.point(r))
1067 .collect::<OgeomResult<_>>()?;
1068 let knots = match (&kp, form) {
1069 (Some(kp), _) => Self::expand_knots(kp.first().cloned(), kp.get(1).cloned())?,
1070 (None, Some(form)) => implied_knots(form, degree, control.len()),
1071 (None, None) => unreachable!("guarded above"),
1072 };
1073 let knots = KnotVector::new(knots, degree)?;
1074 let flat: Vec<f64> = weights
1075 .as_ref()
1076 .and_then(|w| w.first())
1077 .and_then(Arg::list)
1078 .unwrap_or(&[])
1079 .iter()
1080 .filter_map(Arg::number)
1081 .collect();
1082 let weighted: Vec<ogeom_math::Weighted<Point>> = control
1083 .iter()
1084 .zip(flat.iter().chain(std::iter::repeat(&1.0)))
1085 .map(|(p, w)| ogeom_math::Weighted::new(*p, *w, self.tol))
1086 .collect::<OgeomResult<_>>()?;
1087 return Ok(Some(BSplineCurve::rational(knots, weighted)?.into()));
1088 }
1089 }
1090 let out: Option<Curve> = match keyword {
1091 "LINE" => {
1092 let through = self.point(ref_at(args, 1))?;
1093 let vector = self.args(ref_at(args, 2), "VECTOR")?;
1097 let direction = self.direction(vector[1].reference().unwrap_or(0))?;
1098 Some(
1099 LineCurve::new(Axis {
1100 location: through,
1101 direction,
1102 })
1103 .into(),
1104 )
1105 }
1106 "CIRCLE" => {
1107 let frame = self.frame(ref_at(args, 1))?;
1108 let radius = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1109 Some(CircleCurve::new(Circle::new(frame, radius, self.tol)?).into())
1110 }
1111 "ELLIPSE" => {
1112 let frame = self.frame(ref_at(args, 1))?;
1113 let a = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1114 let b = args.get(3).and_then(Arg::number).unwrap_or(0.0) * scale;
1115 Some(EllipseCurve::new(Ellipse::new(frame, a, b, self.tol)?).into())
1116 }
1117 "B_SPLINE_CURVE_WITH_KNOTS" => {
1118 let degree = file_degree(args.get(1), 0.0)?;
1119 let control: Vec<Point> = args
1120 .get(2)
1121 .and_then(Arg::list)
1122 .unwrap_or(&[])
1123 .iter()
1124 .filter_map(Arg::reference)
1125 .map(|r| self.point(r))
1126 .collect::<OgeomResult<_>>()?;
1127 let mults = args.get(6).and_then(Arg::list).unwrap_or(&[]).to_vec();
1128 let knots = args.get(7).and_then(Arg::list).unwrap_or(&[]).to_vec();
1129 let expanded = expanded_knots(&mults, &knots)?;
1130 Some(
1131 BSplineCurve::new(KnotVector::new(expanded, degree)?, control, self.tol)?
1132 .into(),
1133 )
1134 }
1135 "SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" => {
1136 self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1140 }
1141 "BEZIER_CURVE" | "UNIFORM_CURVE" | "QUASI_UNIFORM_CURVE" => {
1142 let degree = file_degree(args.get(1), 1.0)?;
1143 let control: Vec<Point> = args
1144 .get(2)
1145 .and_then(Arg::list)
1146 .unwrap_or(&[])
1147 .iter()
1148 .filter_map(Arg::reference)
1149 .map(|r| self.point(r))
1150 .collect::<OgeomResult<_>>()?;
1151 let knots = implied_knots(keyword, degree, control.len());
1152 Some(BSplineCurve::new(KnotVector::new(knots, degree)?, control, self.tol)?.into())
1153 }
1154 "HYPERBOLA" => {
1155 let frame = self.frame(ref_at(args, 1))?;
1156 let a = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1157 let b = args.get(3).and_then(Arg::number).unwrap_or(0.0) * scale;
1158 Some(HyperbolaCurve::new(Hyperbola::new(frame, a, b, self.tol)?, 20.0)?.into())
1161 }
1162 "PARABOLA" => {
1163 let frame = self.frame(ref_at(args, 1))?;
1164 let focal = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1165 Some(
1166 ParabolaCurve::new(Parabola::new(frame, focal, self.tol)?, SURFACE_EXTENT)?
1167 .into(),
1168 )
1169 }
1170 "TRIMMED_CURVE" => {
1171 self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1173 }
1174 "OFFSET_CURVE_3D" => {
1175 let Some(basis) = self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1176 else {
1177 return Ok(None);
1178 };
1179 let distance = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1180 let reference =
1181 self.direction(args.get(4).and_then(Arg::reference).unwrap_or(0))?;
1182 Some(Curve::Offset(Box::new(ogeom_geom::OffsetCurve::new(
1183 basis, distance, reference,
1184 )?)))
1185 }
1186 "POLYLINE" => {
1187 let points: Vec<Point> = args
1188 .get(1)
1189 .and_then(Arg::list)
1190 .unwrap_or(&[])
1191 .iter()
1192 .filter_map(Arg::reference)
1193 .map(|r| self.point(r))
1194 .collect::<OgeomResult<_>>()?;
1195 if points.len() < 2 {
1196 ogeom_bail!(Construction, "#{id}: a polyline of fewer than two points");
1197 }
1198 let mut knots = vec![0.0];
1200 #[allow(clippy::cast_precision_loss)]
1201 knots.extend((0..points.len()).map(|i| i as f64));
1202 #[allow(clippy::cast_precision_loss)]
1203 knots.push((points.len() - 1) as f64);
1204 Some(BSplineCurve::new(KnotVector::new(knots, 1)?, points, self.tol)?.into())
1205 }
1206 "COMPOSITE_CURVE" => {
1207 let mut joined: Option<(KnotVector, Vec<ogeom_math::Weighted<Point>>)> = None;
1210 let segments: Vec<u64> = args
1211 .get(1)
1212 .and_then(Arg::list)
1213 .unwrap_or(&[])
1214 .iter()
1215 .filter_map(Arg::reference)
1216 .collect();
1217 for segment in segments {
1218 let sargs = self.args(segment, "COMPOSITE_CURVE_SEGMENT")?;
1219 let same_sense = !sargs.get(1).is_some_and(|a| a.is_enum("F"));
1220 let parent = sargs.get(2).and_then(Arg::reference).unwrap_or(0);
1221 let Some((curve, range)) = self.bounded_curve(parent)? else {
1222 return Ok(None);
1223 };
1224 let mut spline = curve.to_bspline_over(range, self.tol)?;
1225 if !same_sense {
1226 let (knots, control) =
1227 ogeom_math::bspline::reverse(spline.knots(), spline.control_points());
1228 spline = BSplineCurve::rational(knots, control)?;
1229 }
1230 joined = Some(match joined {
1231 None => (spline.knots().clone(), spline.control_points().to_vec()),
1232 Some(held) => join_splines(held, &spline, self.tol)?,
1233 });
1234 }
1235 let Some((knots, control)) = joined else {
1236 ogeom_bail!(Construction, "#{id}: a composite curve with no segments");
1237 };
1238 Some(BSplineCurve::rational(knots, control)?.into())
1239 }
1240 "CURVE_REPLICA" => {
1241 let Some(parent) = self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1242 else {
1243 return Ok(None);
1244 };
1245 let motion = self
1246 .transformation_operator(args.get(2).and_then(Arg::reference).unwrap_or(0))?;
1247 Some(parent.transformed(&motion, self.tol)?)
1248 }
1249 other => {
1250 self.report.warnings.push(format!(
1251 "#{id}: curve kind {other} is not read yet; its edge is skipped"
1252 ));
1253 None
1254 }
1255 };
1256 Ok(out)
1257 }
1258
1259 fn composite_surface(&mut self, id: u64, args: &[Arg]) -> OgeomResult<SurfaceGeometry> {
1267 let rows: Vec<Vec<u64>> = args
1268 .get(1)
1269 .and_then(Arg::list)
1270 .unwrap_or(&[])
1271 .iter()
1272 .map(|row| {
1273 row.list()
1274 .unwrap_or(&[])
1275 .iter()
1276 .filter_map(Arg::reference)
1277 .collect()
1278 })
1279 .collect();
1280 if rows.is_empty()
1281 || rows
1282 .iter()
1283 .any(|r| r.len() != rows[0].len() || r.is_empty())
1284 {
1285 ogeom_bail!(
1286 Construction,
1287 "#{id}: a composite surface's grid is not rectangular"
1288 );
1289 }
1290 let mut grid: Vec<Vec<Patch>> = Vec::with_capacity(rows.len());
1291 for row in &rows {
1292 let mut out = Vec::with_capacity(row.len());
1293 for &patch in row {
1294 let pargs = self.args(patch, "SURFACE_PATCH")?;
1295 let parent = pargs.get(1).and_then(Arg::reference).unwrap_or(0);
1296 let u_sense = !pargs.get(4).is_some_and(|a| a.is_enum("F"));
1297 let v_sense = !pargs.get(5).is_some_and(|a| a.is_enum("F"));
1298 let spline = self.bounded_surface_spline(parent)?;
1299 let mut p = Patch::of(&spline)?;
1300 if !u_sense {
1301 p = p.reversed_u();
1302 }
1303 if !v_sense {
1304 p = p.transposed().reversed_u().transposed();
1305 }
1306 out.push(p);
1307 }
1308 grid.push(out);
1309 }
1310 join_patches(grid, self.tol).map_err(|e| {
1311 ogeom_core::ogeom_err!(
1312 Construction,
1313 "#{id}: a composite surface's patches do not join: {e}"
1314 )
1315 })
1316 }
1317
1318 fn bounded_surface_spline(&mut self, id: u64) -> OgeomResult<ogeom_geom::BSplineSurface> {
1322 let keyword = self.instance(id)?.keyword().to_owned();
1323 if keyword == "RECTANGULAR_TRIMMED_SURFACE" {
1324 let args = self.args(id, "RECTANGULAR_TRIMMED_SURFACE")?;
1325 let basis_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
1326 let Some(basis) = self.surface(basis_id)? else {
1327 ogeom_bail!(Construction, "#{id}: a patch's basis is not read");
1328 };
1329 let angular = matches!(
1330 self.instance(basis_id)?.keyword(),
1331 "CYLINDRICAL_SURFACE"
1332 | "CONICAL_SURFACE"
1333 | "SPHERICAL_SURFACE"
1334 | "TOROIDAL_SURFACE"
1335 );
1336 let n = |i: usize| args.get(i).and_then(Arg::number).unwrap_or(0.0);
1337 let (u_scale, v_scale) = if angular {
1338 (self.angle_scale, self.report.scale_mm)
1339 } else {
1340 (self.report.scale_mm, self.report.scale_mm)
1341 };
1342 let (u1, u2) = (n(2) * u_scale, n(3) * u_scale);
1343 let (v1, v2) = (n(4) * v_scale, n(5) * v_scale);
1344 let window = ((u1.min(u2), u1.max(u2)), (v1.min(v2), v1.max(v2)));
1345 let trimmed = ogeom_geom::TrimmedSurface::new(basis, window.0, window.1, self.tol)?;
1346 return SurfaceGeometry::Trimmed(Box::new(trimmed)).to_bspline(self.tol);
1347 }
1348 let Some(surface) = self.surface(id)? else {
1349 ogeom_bail!(Construction, "#{id}: a patch's surface is not read");
1350 };
1351 surface.to_bspline(self.tol)
1352 }
1353
1354 fn bounded_curve(&mut self, id: u64) -> OgeomResult<Option<(Curve, (f64, f64))>> {
1359 self.nested(|reader| reader.bounded_curve_at(id))
1360 }
1361
1362 fn bounded_curve_at(&mut self, id: u64) -> OgeomResult<Option<(Curve, (f64, f64))>> {
1363 let keyword = self.instance(id)?.keyword().to_owned();
1364 if keyword != "TRIMMED_CURVE" {
1365 let Some(curve) = self.curve(id)? else {
1366 return Ok(None);
1367 };
1368 let (lo, hi) = curve.domain();
1369 if !lo.is_finite() || !hi.is_finite() || hi - lo > SURFACE_EXTENT {
1370 ogeom_bail!(
1371 Construction,
1372 "#{id}: an unbounded curve cannot stand as a composite's segment"
1373 );
1374 }
1375 return Ok(Some((curve, (lo, hi))));
1376 }
1377 let args = self.args(id, "TRIMMED_CURVE")?;
1378 let basis_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
1379 let Some(basis) = self.curve(basis_id)? else {
1380 return Ok(None);
1381 };
1382 let basis_keyword = self.instance(basis_id)?.keyword().to_owned();
1383 let forward = !args.get(4).is_some_and(|a| a.is_enum("F"));
1384 let mut ends = Vec::with_capacity(2);
1385 for trim in [args.get(2), args.get(3)] {
1386 let items = trim.and_then(Arg::list).unwrap_or(&[]).to_vec();
1387 let mut at: Option<Point> = None;
1388 for item in &items {
1389 if let Some(r) = item.reference() {
1390 at = Some(self.point(r)?);
1391 }
1392 }
1393 if at.is_none() {
1394 for item in &items {
1395 if let Arg::Typed(typed) = item
1396 && typed.0 == "PARAMETER_VALUE"
1397 && let Some(v) = typed.1.first().and_then(Arg::number)
1398 {
1399 at =
1400 Some(self.step_parameter_point(&basis, &basis_keyword, basis_id, v)?);
1401 }
1402 }
1403 }
1404 let Some(point) = at else {
1405 ogeom_bail!(
1406 Construction,
1407 "#{id}: a trim names neither point nor parameter"
1408 );
1409 };
1410 let t = match self.parameter_of(&basis, point) {
1411 Some(t) => t,
1412 None => project_on_curve(&basis, point, 256, self.tol)?.parameter,
1413 };
1414 ends.push(t);
1415 }
1416 let (mut a, mut b) = (ends[0], ends[1]);
1417 if !forward {
1418 core::mem::swap(&mut a, &mut b);
1419 }
1420 if basis.is_periodic() && b <= a {
1421 let (lo, hi) = basis.domain();
1422 b += hi - lo;
1423 }
1424 let range = if a <= b { (a, b) } else { (b, a) };
1425 let mut curve = basis;
1426 if !forward || a > b {
1427 let spline = curve.to_bspline_over(range, self.tol)?;
1429 let (knots, control) =
1430 ogeom_math::bspline::reverse(spline.knots(), spline.control_points());
1431 curve = BSplineCurve::rational(knots, control)?.into();
1432 let domain = curve.domain();
1433 return Ok(Some((curve, domain)));
1434 }
1435 Ok(Some((curve, range)))
1436 }
1437
1438 fn step_parameter_point(
1442 &mut self,
1443 basis: &Curve,
1444 keyword: &str,
1445 basis_id: u64,
1446 v: f64,
1447 ) -> OgeomResult<Point> {
1448 match keyword {
1449 "LINE" => {
1450 let args = self.args(basis_id, "LINE")?;
1451 let through = self.point(ref_at(args, 1))?;
1452 let vector = self.args(ref_at(args, 2), "VECTOR")?;
1453 let direction = self.direction(vector[1].reference().unwrap_or(0))?;
1454 let magnitude = vector.get(2).and_then(Arg::number).unwrap_or(1.0);
1455 Ok(through + direction.vector() * (v * magnitude * self.report.scale_mm))
1456 }
1457 "CIRCLE" | "ELLIPSE" => basis.point_at(v * self.angle_scale, self.tol),
1458 _ => basis.point_at(v, self.tol),
1459 }
1460 }
1461
1462 fn transformation_operator(&mut self, id: u64) -> OgeomResult<Transform> {
1466 let args = self.args(id, "CARTESIAN_TRANSFORMATION_OPERATOR_3D")?;
1467 let direction_at = |this: &mut Self, i: usize| -> OgeomResult<Option<Vector>> {
1468 match args.get(i).and_then(Arg::reference) {
1469 Some(r) => Ok(Some(this.direction(r)?.vector())),
1470 None => Ok(None),
1471 }
1472 };
1473 let (axis1, axis2, axis3) = (
1474 direction_at(self, 1)?,
1475 direction_at(self, 2)?,
1476 direction_at(self, 5)?,
1477 );
1478 let origin = self.point(args.get(3).and_then(Arg::reference).unwrap_or(0))?;
1479 let scale = args.get(4).and_then(Arg::number).unwrap_or(1.0);
1480 let z = match (axis3, axis1, axis2) {
1481 (Some(z), _, _) => z,
1482 (None, Some(x), Some(y)) => x.cross(y),
1483 _ => Vector::Z,
1484 };
1485 let z = Direction::new(z, self.tol)?.vector();
1486 let x = match axis1 {
1487 Some(x) => x - z * x.dot(z),
1488 None => {
1489 let seed = if z.x.abs() < 0.9 {
1490 Vector::X
1491 } else {
1492 Vector::Y
1493 };
1494 seed - z * seed.dot(z)
1495 }
1496 };
1497 let x = Direction::new(x, self.tol)?.vector();
1498 let y = z.cross(x);
1499 let linear = Matrix3::new([[x.x, y.x, z.x], [x.y, y.y, z.y], [x.z, y.z, z.z]]);
1500 Transform::from_parts(linear, scale, origin.to_vector(), self.tol.angular())
1501 }
1502
1503 fn parameter_of(&self, curve: &Curve, p: Point) -> Option<f64> {
1505 crate::inversion::parameter_on(curve, p)
1506 }
1507
1508 fn file_pcurves(&mut self, edge_id: u64, surface_id: u64) -> Vec<PlanarCurve> {
1513 let mut out = Vec::new();
1514 let Ok(args) = self.args(edge_id, "EDGE_CURVE") else {
1515 return out;
1516 };
1517 let curve_id = ref_at(args, 3);
1518 let Ok(instance) = self.instance(curve_id) else {
1519 return out;
1520 };
1521 let Some(dressed) = instance
1522 .part("SURFACE_CURVE")
1523 .or_else(|| instance.part("SEAM_CURVE"))
1524 .or_else(|| instance.part("INTERSECTION_CURVE"))
1525 else {
1526 return out;
1527 };
1528 let uses: Vec<u64> = dressed
1529 .get(2)
1530 .and_then(Arg::list)
1531 .unwrap_or(&[])
1532 .iter()
1533 .filter_map(Arg::reference)
1534 .collect();
1535 for id in uses {
1536 let Ok(pargs) = self.args(id, "PCURVE") else {
1537 continue;
1538 };
1539 if pargs.get(1).and_then(Arg::reference) != Some(surface_id) {
1540 continue;
1541 }
1542 let Ok(dargs) = self.args(ref_at(pargs, 2), "DEFINITIONAL_REPRESENTATION") else {
1543 continue;
1544 };
1545 let Some(item) = dargs
1546 .get(1)
1547 .and_then(Arg::list)
1548 .and_then(|items| items.first())
1549 .and_then(Arg::reference)
1550 else {
1551 continue;
1552 };
1553 if let Ok(Some(curve)) = self.nested(|reader| reader.planar_curve(item)) {
1554 out.push(curve);
1555 }
1556 }
1557 if out.is_empty() {
1561 return out;
1562 }
1563 if let (Some(chart), Some(pace)) =
1564 (self.chart_units(surface_id), self.curve_units(curve_id))
1565 {
1566 for pcurve in &mut out {
1567 if let Some(restated) =
1568 in_kernel_units(pcurve, chart, pace, self.angle_scale, self.tol)
1569 {
1570 *pcurve = restated;
1571 }
1572 }
1573 } else if let (Some(map), Some(pace)) = (
1574 self.swept_line_chart(surface_id),
1575 self.curve_units(curve_id),
1576 ) {
1577 for pcurve in &mut out {
1578 if let Some(restated) = through_map(pcurve, map, pace, self.tol) {
1579 *pcurve = restated;
1580 }
1581 }
1582 }
1583 out
1584 }
1585
1586 fn swept_line_chart(&mut self, id: u64) -> Option<ChartMap> {
1593 let args = self.args(id, "SURFACE_OF_LINEAR_EXTRUSION").ok()?.to_vec();
1594 let Some(SurfaceGeometry::Plane(plane)) = self.surface(id).ok()? else {
1595 return None;
1596 };
1597 let line_id = ref_at(&args, 1);
1598 let Some(Curve::Line(line)) = self.curve(line_id).ok()? else {
1599 return None;
1600 };
1601 let pace = self.curve_units(line_id)?;
1602 let vector = self.args(ref_at(&args, 2), "VECTOR").ok()?.to_vec();
1603 let across = vector.get(2).and_then(Arg::number).unwrap_or(1.0) * self.report.scale_mm;
1604 let sweep = self.direction(vector.get(1)?.reference()?).ok()?.vector();
1605 let frame = plane.plane().frame();
1606 let (x, y) = (frame.x().vector(), frame.y().vector());
1607 let (along, from) = (
1608 line.axis().direction.vector(),
1609 line.axis().location - frame.origin(),
1610 );
1611 Some(ChartMap {
1612 linear: [
1613 [pace * along.dot(x), across * sweep.dot(x)],
1614 [pace * along.dot(y), across * sweep.dot(y)],
1615 ],
1616 offset: (from.dot(x), from.dot(y)),
1617 })
1618 }
1619
1620 fn chart_units(&mut self, id: u64) -> Option<(f64, f64)> {
1633 let (keyword, args) = {
1634 let instance = self.instance(id).ok()?;
1635 if [
1636 "B_SPLINE_SURFACE",
1637 "B_SPLINE_SURFACE_WITH_KNOTS",
1638 "BEZIER_SURFACE",
1639 "UNIFORM_SURFACE",
1640 "QUASI_UNIFORM_SURFACE",
1641 ]
1642 .into_iter()
1643 .any(|k| instance.part(k).is_some())
1644 {
1645 return Some((1.0, 1.0));
1646 }
1647 (
1648 instance.keyword(),
1649 instance
1650 .parts
1651 .first()
1652 .map_or(&[][..], |(_, a)| a.as_slice()),
1653 )
1654 };
1655 let (length, angle) = (self.report.scale_mm, self.angle_scale);
1656 match keyword {
1657 "PLANE" => Some((length, length)),
1658 "CYLINDRICAL_SURFACE" | "CONICAL_SURFACE" => Some((angle, length)),
1659 "SPHERICAL_SURFACE" | "TOROIDAL_SURFACE" | "DEGENERATE_TOROIDAL_SURFACE" => {
1660 Some((angle, angle))
1661 }
1662 "OFFSET_SURFACE" | "RECTANGULAR_TRIMMED_SURFACE" | "CURVE_BOUNDED_SURFACE" => {
1663 self.chart_units(ref_at(args, 1))
1664 }
1665 "SURFACE_OF_REVOLUTION" => Some((angle, self.curve_units(ref_at(args, 1))?)),
1666 "SURFACE_REPLICA" => {
1669 let parent = ref_at(args, 1);
1670 let scale = self
1671 .args(ref_at(args, 2), "CARTESIAN_TRANSFORMATION_OPERATOR_3D")
1672 .ok()?
1673 .get(4)
1674 .and_then(Arg::number)
1675 .unwrap_or(1.0);
1676 if !(scale > 0.0 && scale.is_finite()) {
1677 return None;
1678 }
1679 let (u, v) = self.chart_units(parent)?;
1680 let (lu, lv) = self.length_parameters(parent)?;
1681 Some((
1682 if lu { u * scale } else { u },
1683 if lv { v * scale } else { v },
1684 ))
1685 }
1686 "SURFACE_OF_LINEAR_EXTRUSION" => {
1687 let along = self.curve_units(ref_at(args, 1))?;
1688 let vector = self.args(ref_at(args, 2), "VECTOR").ok()?;
1689 let magnitude = vector.get(2).and_then(Arg::number).unwrap_or(1.0);
1690 let direction = self.direction(vector[1].reference()?).ok()?;
1691 let across = magnitude * length;
1692 match self.surface(id).ok()?? {
1696 SurfaceGeometry::Cylinder(c) => {
1697 let z = c.cylinder().frame().z();
1698 Some((along, across.copysign(direction.vector().dot(z.vector()))))
1699 }
1700 SurfaceGeometry::Extrusion(_) => Some((along, across)),
1701 _ => None,
1702 }
1703 }
1704 _ => None,
1705 }
1706 }
1707
1708 fn length_parameters(&mut self, id: u64) -> Option<(bool, bool)> {
1716 let (keyword, args) = {
1717 let instance = self.instance(id).ok()?;
1718 if [
1719 "B_SPLINE_SURFACE",
1720 "B_SPLINE_SURFACE_WITH_KNOTS",
1721 "BEZIER_SURFACE",
1722 "UNIFORM_SURFACE",
1723 "QUASI_UNIFORM_SURFACE",
1724 ]
1725 .into_iter()
1726 .any(|k| instance.part(k).is_some())
1727 {
1728 return Some((false, false));
1729 }
1730 (
1731 instance.keyword().to_owned(),
1732 instance
1733 .parts
1734 .first()
1735 .map_or_else(Vec::new, |(_, a)| a.clone()),
1736 )
1737 };
1738 match keyword.as_str() {
1739 "PLANE" => Some((true, true)),
1740 "CYLINDRICAL_SURFACE" | "CONICAL_SURFACE" => Some((false, true)),
1741 "SPHERICAL_SURFACE" | "TOROIDAL_SURFACE" | "DEGENERATE_TOROIDAL_SURFACE" => {
1742 Some((false, false))
1743 }
1744 "OFFSET_SURFACE"
1745 | "RECTANGULAR_TRIMMED_SURFACE"
1746 | "CURVE_BOUNDED_SURFACE"
1747 | "SURFACE_REPLICA" => self.length_parameters(ref_at(&args, 1)),
1748 "SURFACE_OF_REVOLUTION" => Some((false, self.length_parameter(ref_at(&args, 1))?)),
1749 "SURFACE_OF_LINEAR_EXTRUSION" => match self.surface(id).ok()?? {
1750 SurfaceGeometry::Cylinder(_) => Some((false, true)),
1751 SurfaceGeometry::Extrusion(_) => {
1752 Some((self.length_parameter(ref_at(&args, 1))?, true))
1753 }
1754 _ => None,
1755 },
1756 _ => None,
1757 }
1758 }
1759
1760 fn length_parameter(&mut self, id: u64) -> Option<bool> {
1765 let (keyword, inner) = {
1766 let instance = self.instance(id).ok()?;
1767 if [
1768 "B_SPLINE_CURVE",
1769 "B_SPLINE_CURVE_WITH_KNOTS",
1770 "BEZIER_CURVE",
1771 "UNIFORM_CURVE",
1772 "QUASI_UNIFORM_CURVE",
1773 ]
1774 .into_iter()
1775 .any(|k| instance.part(k).is_some())
1776 {
1777 return Some(false);
1778 }
1779 let args = instance
1780 .parts
1781 .first()
1782 .map_or(&[][..], |(_, a)| a.as_slice());
1783 (instance.keyword().to_owned(), ref_at(args, 1))
1784 };
1785 match keyword.as_str() {
1786 "LINE" | "PARABOLA" => Some(true),
1787 "CIRCLE" | "ELLIPSE" | "HYPERBOLA" => Some(false),
1788 "SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" | "TRIMMED_CURVE"
1789 | "OFFSET_CURVE_3D" => self.length_parameter(inner),
1790 _ => None,
1791 }
1792 }
1793
1794 fn curve_units(&mut self, id: u64) -> Option<f64> {
1801 let (keyword, args) = {
1802 let instance = self.instance(id).ok()?;
1803 if [
1804 "B_SPLINE_CURVE",
1805 "B_SPLINE_CURVE_WITH_KNOTS",
1806 "BEZIER_CURVE",
1807 "UNIFORM_CURVE",
1808 "QUASI_UNIFORM_CURVE",
1809 ]
1810 .into_iter()
1811 .any(|k| instance.part(k).is_some())
1812 {
1813 return Some(1.0);
1814 }
1815 (
1816 instance.keyword(),
1817 instance
1818 .parts
1819 .first()
1820 .map_or(&[][..], |(_, a)| a.as_slice()),
1821 )
1822 };
1823 let length = self.report.scale_mm;
1824 match keyword {
1825 "LINE" => {
1826 let vector = self.args(ref_at(args, 2), "VECTOR").ok()?;
1827 Some(vector.get(2).and_then(Arg::number).unwrap_or(1.0) * length)
1828 }
1829 "CIRCLE" | "ELLIPSE" => Some(self.angle_scale),
1830 "HYPERBOLA" => Some(1.0),
1831 "PARABOLA" => Some(2.0 * args.get(2).and_then(Arg::number)? * length),
1832 "SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" | "TRIMMED_CURVE"
1833 | "OFFSET_CURVE_3D" => self.curve_units(ref_at(args, 1)),
1834 _ => None,
1835 }
1836 }
1837
1838 fn planar_curve(&mut self, id: u64) -> OgeomResult<Option<PlanarCurve>> {
1842 let instance = self.instance(id)?;
1843 let (base, knotted, weights) = (
1844 instance.part("B_SPLINE_CURVE"),
1845 instance.part("B_SPLINE_CURVE_WITH_KNOTS"),
1846 instance.part("RATIONAL_B_SPLINE_CURVE"),
1847 );
1848 let spline = match (base, knotted) {
1851 (Some(base), Some(knotted)) => {
1852 Some((base.first(), base.get(1), knotted.first(), knotted.get(1)))
1853 }
1854 (None, Some(knotted)) => Some((
1855 knotted.get(1),
1856 knotted.get(2),
1857 knotted.get(6),
1858 knotted.get(7),
1859 )),
1860 _ => None,
1861 };
1862 if let Some((degree, control, mults, knots)) = spline {
1863 let degree = file_degree(degree, 0.0)?;
1864 let mut points = Vec::new();
1865 for r in control
1866 .and_then(Arg::list)
1867 .unwrap_or(&[])
1868 .iter()
1869 .filter_map(Arg::reference)
1870 {
1871 points.push(self.point2(r)?);
1872 }
1873 let expanded = expanded_knots(
1874 mults.and_then(Arg::list).unwrap_or(&[]),
1875 knots.and_then(Arg::list).unwrap_or(&[]),
1876 )?;
1877 let flat: Vec<f64> = weights
1878 .and_then(|w| w.first())
1879 .and_then(Arg::list)
1880 .unwrap_or(&[])
1881 .iter()
1882 .filter_map(Arg::number)
1883 .collect();
1884 let control = points
1885 .into_iter()
1886 .zip(flat.iter().chain(std::iter::repeat(&1.0)))
1887 .map(|(p, w)| Weighted::new(p, *w, self.tol))
1888 .collect::<OgeomResult<Vec<_>>>()?;
1889 let knots = KnotVector::new(expanded, degree)?;
1890 return Ok(Some(
1891 ogeom_geom::BSpline2d::rational(knots, control)?.into(),
1892 ));
1893 }
1894 let args = instance
1895 .parts
1896 .first()
1897 .map_or(&[][..], |(_, a)| a.as_slice());
1898 let out: PlanarCurve = match instance.keyword() {
1899 "LINE" => {
1900 let through = self.point2(ref_at(args, 1))?;
1901 let vector = self.args(ref_at(args, 2), "VECTOR")?;
1902 let direction = self.direction2(vector[1].reference().unwrap_or(0))?;
1903 let magnitude = vector.get(2).and_then(Arg::number).unwrap_or(1.0);
1907 if magnitude == 1.0 {
1908 let axis = ogeom_math::Axis2 {
1909 location: through,
1910 direction,
1911 };
1912 ogeom_geom::Line2d::over(axis, -SURFACE_EXTENT, SURFACE_EXTENT)?.into()
1913 } else {
1914 let step = direction.vector() * magnitude;
1915 let ends = vec![
1916 through - step * SURFACE_EXTENT,
1917 through + step * SURFACE_EXTENT,
1918 ];
1919 let knots = vec![
1920 -SURFACE_EXTENT,
1921 -SURFACE_EXTENT,
1922 SURFACE_EXTENT,
1923 SURFACE_EXTENT,
1924 ];
1925 ogeom_geom::BSpline2d::new(KnotVector::new(knots, 1)?, ends, self.tol)?.into()
1926 }
1927 }
1928 "CIRCLE" => {
1929 let frame = self.frame2(ref_at(args, 1))?;
1930 let radius = args.get(2).and_then(Arg::number).unwrap_or(0.0);
1931 ogeom_geom::Circle2d::new(ogeom_math::Circle2::new(frame, radius, self.tol)?).into()
1932 }
1933 "ELLIPSE" => {
1934 let frame = self.frame2(ref_at(args, 1))?;
1935 let a = args.get(2).and_then(Arg::number).unwrap_or(0.0);
1936 let b = args.get(3).and_then(Arg::number).unwrap_or(0.0);
1937 ogeom_geom::Ellipse2d::new(ogeom_math::Ellipse2::new(frame, a, b, self.tol)?).into()
1938 }
1939 "TRIMMED_CURVE" => {
1940 return self.planar_curve(ref_at(args, 1));
1943 }
1944 _ => return Ok(None),
1945 };
1946 Ok(Some(out))
1947 }
1948
1949 fn point2(&mut self, id: u64) -> OgeomResult<ogeom_math::Point2> {
1951 let args = self.args(id, "CARTESIAN_POINT")?;
1952 let Some(coords) = args.get(1).and_then(Arg::list) else {
1953 ogeom_bail!(Construction, "#{id}: a point without coordinates");
1954 };
1955 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0);
1956 Ok(ogeom_math::Point2::new(value(0), value(1)))
1957 }
1958
1959 fn direction2(&mut self, id: u64) -> OgeomResult<ogeom_math::Direction2> {
1960 let args = self.args(id, "DIRECTION")?;
1961 let Some(coords) = args.get(1).and_then(Arg::list) else {
1962 ogeom_bail!(Construction, "#{id}: a direction without components");
1963 };
1964 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0);
1965 ogeom_math::Direction2::new(ogeom_math::Vector2::new(value(0), value(1)), self.tol)
1966 }
1967
1968 fn frame2(&mut self, id: u64) -> OgeomResult<ogeom_math::Frame2> {
1971 let args = self.args(id, "AXIS2_PLACEMENT_2D")?;
1972 let origin = self.point2(ref_at(args, 1))?;
1973 let x = match args.get(2).and_then(Arg::reference) {
1974 Some(r) => self.direction2(r)?,
1975 None => ogeom_math::Direction2::X,
1976 };
1977 Ok(ogeom_math::Frame2::new(origin, x))
1978 }
1979
1980 fn vertex(&mut self, id: u64) -> OgeomResult<Shape> {
1983 if let Some(shape) = self.vertices.get(&id) {
1984 return Ok(shape.clone());
1985 }
1986 let args = self.args(id, "VERTEX_POINT")?;
1987 let point = self.point(ref_at(args, 1))?;
1988 let shape = make_vertex(&mut self.model, point).shape;
1989 self.vertices.insert(id, shape.clone());
1990 Ok(shape)
1991 }
1992
1993 fn edge(&mut self, id: u64) -> OgeomResult<Option<BuiltEdge>> {
1999 if let Some(found) = self.edges.get(&id) {
2000 return Ok(Some(found.clone()));
2001 }
2002 let args = self.args(id, "EDGE_CURVE")?;
2003 let v1 = ref_at(args, 1);
2004 let v2 = ref_at(args, 2);
2005 let Some(mut curve) = self.curve(ref_at(args, 3))? else {
2006 return Ok(None);
2007 };
2008 let same_sense = !args.get(4).is_some_and(|a| a.is_enum("F"));
2009
2010 let p1 = {
2011 let vargs = self.args(v1, "VERTEX_POINT")?;
2012 self.point(ref_at(vargs, 1))?
2013 };
2014 let p2 = {
2015 let vargs = self.args(v2, "VERTEX_POINT")?;
2016 self.point(ref_at(vargs, 1))?
2017 };
2018
2019 let (start, end, mut flipped) = if same_sense {
2023 (p1, p2, false)
2024 } else {
2025 (p2, p1, true)
2026 };
2027 let (range, closed) = match (
2028 self.parameter_of(&curve, start),
2029 self.parameter_of(&curve, end),
2030 ) {
2031 (Some(a), Some(b)) => {
2032 let period = if curve.is_periodic() {
2033 let (lo, hi) = curve.domain();
2034 hi - lo
2035 } else {
2036 0.0
2037 };
2038 if v1 == v2 {
2039 ((a, a + if period > 0.0 { period } else { 0.0 }), true)
2040 } else if period > 0.0 && b <= a + self.tol.parametric() {
2041 ((a, b + period), false)
2042 } else if b < a - self.tol.parametric() {
2043 flipped = !flipped;
2049 self.report.warnings.push(format!(
2050 "#{id}: the edge's vertices run against its curve's \
2051 parameter despite its sense flag; the edge was reversed"
2052 ));
2053 self.tally("edge-against-curve", a - b, id);
2054 ((b, a), false)
2055 } else {
2056 ((a, b), false)
2057 }
2058 }
2059 _ => {
2060 if v1 == v2
2071 && let Curve::BSpline(spline) = &curve
2072 {
2073 let (lo, hi) = curve.domain();
2074 let gap = curve.point_at(lo, self.tol)?.distance(start);
2075 if gap > self.tol.confusion() * 10.0 {
2076 let samples = (spline.control_points().len() * 8).max(64);
2077 if let Ok(found) = project_on_curve(&curve, start, samples, self.tol)
2078 && found.distance <= self.tol.confusion() * 1e4
2079 && found.parameter > lo + self.tol.parametric()
2080 && found.parameter < hi - self.tol.parametric()
2081 && let Ok(moved) = spline.reseamed_at(found.parameter, self.tol)
2082 {
2083 curve = Curve::BSpline(moved);
2084 self.report.warnings.push(format!(
2085 "#{id}: a closed edge's vertex sits {gap:.2e} along its \
2086 curve from the seam; the seam was moved to the vertex"
2087 ));
2088 self.tally("reseamed", gap, id);
2089 }
2090 }
2091 }
2092 let (lo, hi) = curve.domain();
2093 let head = curve.point_at(lo, self.tol)?;
2094 let tail = curve.point_at(hi, self.tol)?;
2095 let (head_miss, tail_miss) = (head.distance(start), tail.distance(end));
2096 if head_miss > self.tol.confusion() * 10.0
2097 || tail_miss > self.tol.confusion() * 10.0
2098 {
2099 let window = if v1 == v2 {
2107 None
2108 } else {
2109 window_between_feet(&curve, start, end, self.tol)
2110 };
2111 if let Some(window) = window {
2112 self.report.warnings.push(format!(
2113 "#{id}: edge endpoints sit {head_miss:.2e} and {tail_miss:.2e} \
2114 from its curve's ends; the window between the vertices' \
2115 feet on the curve was taken"
2116 ));
2117 self.tally("vertex-window", head_miss.max(tail_miss), id);
2118 (window, false)
2119 } else {
2120 self.report.warnings.push(format!(
2121 "#{id}: edge endpoints sit {head_miss:.2e} and {tail_miss:.2e} \
2122 from its curve's ends; the curve's own domain was taken"
2123 ));
2124 ((lo, hi), v1 == v2)
2125 }
2126 } else {
2127 ((lo, hi), v1 == v2)
2128 }
2129 }
2130 };
2131 let _ = closed;
2132
2133 let (vlo, vhi) = if flipped {
2134 (self.vertex(v2)?, self.vertex(v1)?)
2135 } else {
2136 (self.vertex(v1)?, self.vertex(v2)?)
2137 };
2138 for (vertex, t) in [(&vlo, range.0), (&vhi, range.1)] {
2144 let end = curve.point_at(t, self.tol)?;
2145 let stated = {
2146 let Some(node) = self.model.node(vertex) else {
2147 continue;
2148 };
2149 let Some(data) = node.data().as_vertex() else {
2150 continue;
2151 };
2152 (data.point, data.tolerance)
2153 };
2154 let gap = end.distance(stated.0);
2155 if gap > stated.1.get() {
2156 if let Some(node) = self.model.node_mut(vertex)
2157 && let ogeom_topo::NodeData::Vertex(data) = node.data_mut()
2158 {
2159 data.tolerance = data.tolerance.widen_to(gap + self.tol.confusion());
2160 }
2161 self.warn_vertex_miss(id, gap);
2162 }
2163 }
2164 let shape =
2165 make_edge_between(&mut self.model, curve.clone(), range, &vlo, &vhi, self.tol)?.shape;
2166 let entry = (shape, curve, range, flipped);
2167 self.edges.insert(id, entry.clone());
2168 Ok(Some(entry))
2169 }
2170
2171 fn edge_pieces(&mut self, id: u64) -> OgeomResult<Option<Vec<(u64, BuiltEdge)>>> {
2175 if let Some(pieces) = self.pieces.get(&id) {
2176 return Ok(Some(pieces.clone()));
2177 }
2178 Ok(self.edge(id)?.map(|built| vec![(id, built)]))
2179 }
2180
2181 fn piece_keys(&self, id: u64) -> Vec<u64> {
2183 self.pieces.get(&id).map_or_else(
2184 || vec![id],
2185 |pieces| pieces.iter().map(|(k, _)| *k).collect(),
2186 )
2187 }
2188
2189 fn edge_shapes(&self, id: u64) -> Vec<Shape> {
2195 if let Some(pieces) = self.pieces.get(&id) {
2196 return pieces
2197 .iter()
2198 .map(|(_, (piece, ..))| piece.clone())
2199 .collect();
2200 }
2201 self.edges
2202 .get(&id)
2203 .map(|(shape, ..)| shape.clone())
2204 .into_iter()
2205 .collect()
2206 }
2207
2208 fn cut_at_poles(&mut self, face_ids: &[u64]) {
2223 let mut crossings: BTreeMap<u64, Vec<crate::pcurves::Crossing>> = BTreeMap::new();
2224 for &fid in face_ids {
2225 let Ok(args) = self.face_args(fid) else {
2226 continue;
2227 };
2228 let Some(surface) = args
2229 .get(2)
2230 .and_then(Arg::reference)
2231 .and_then(|sid| self.surface(sid).ok().flatten())
2232 else {
2233 continue;
2234 };
2235 let poles = crate::pcurves::chart_poles(&surface, self.tol);
2236 if poles.is_empty() {
2237 continue;
2238 }
2239 let bounds: Vec<u64> = args
2240 .get(1)
2241 .and_then(Arg::list)
2242 .unwrap_or(&[])
2243 .iter()
2244 .filter_map(Arg::reference)
2245 .collect();
2246 for bound in bounds {
2247 let Ok((loop_id, _)) = self.bound_args(bound) else {
2248 continue;
2249 };
2250 let Ok(loop_args) = self.args(loop_id, "EDGE_LOOP") else {
2251 continue;
2252 };
2253 let edge_ids: Vec<u64> = loop_args
2254 .get(1)
2255 .and_then(Arg::list)
2256 .unwrap_or(&[])
2257 .iter()
2258 .filter_map(Arg::reference)
2259 .filter_map(|oe| {
2260 self.args(oe, "ORIENTED_EDGE")
2261 .ok()
2262 .and_then(|a| a.get(3).and_then(Arg::reference))
2263 })
2264 .collect();
2265 for edge_id in edge_ids {
2266 if self.pieces.contains_key(&edge_id) {
2267 continue;
2268 }
2269 let Ok(Some((_, curve, range, _))) = self.edge(edge_id) else {
2270 continue;
2271 };
2272 let found = crate::pcurves::pole_crossings(&curve, range, &poles, self.tol);
2273 if found.is_empty() {
2274 continue;
2275 }
2276 let entry = crossings.entry(edge_id).or_default();
2277 for crossing in found {
2278 if entry.iter().all(|(t, ..)| (t - crossing.0).abs() > 1e-9) {
2279 entry.push(crossing);
2280 }
2281 }
2282 }
2283 }
2284 }
2285 for (edge_id, mut at) in crossings {
2286 at.sort_by(|x, y| x.0.total_cmp(&y.0));
2287 let Some((shape, curve, range, flipped)) = self.edges.get(&edge_id).cloned() else {
2288 continue;
2289 };
2290 let Ok(cut) = crate::pcurves::split_at_poles(
2291 &mut self.model,
2292 &shape,
2293 &curve,
2294 range,
2295 &at,
2296 &mut self.pole_vertices,
2297 self.tol,
2298 ) else {
2299 continue;
2300 };
2301 let pieces = cut
2302 .into_iter()
2303 .map(|(piece, window)| {
2304 self.piece_key -= 1;
2305 (self.piece_key, (piece, curve.clone(), window, flipped))
2306 })
2307 .collect();
2308 self.pieces.insert(edge_id, pieces);
2309 }
2310 }
2311
2312 fn split_band_rims(&mut self, face_ids: &[u64]) {
2332 loop {
2333 let before = (self.pieces.len(), self.band_faces.len());
2334 for &fid in face_ids {
2335 let _ = self.split_band_rim(fid);
2336 }
2337 if (self.pieces.len(), self.band_faces.len()) == before {
2338 break;
2339 }
2340 }
2341 }
2342
2343 fn split_band_rim(&mut self, fid: u64) -> OgeomResult<()> {
2345 let args = self.face_args(fid)?.to_vec();
2346 let Some(surface) = args
2347 .get(2)
2348 .and_then(Arg::reference)
2349 .and_then(|sid| self.surface(sid).ok().flatten())
2350 else {
2351 return Ok(());
2352 };
2353 if !surface.is_periodic_u() {
2354 return Ok(());
2355 }
2356 let bounds: Vec<u64> = args
2357 .get(1)
2358 .and_then(Arg::list)
2359 .unwrap_or(&[])
2360 .iter()
2361 .filter_map(Arg::reference)
2362 .collect();
2363 let [first, second] = bounds[..] else {
2364 return Ok(());
2365 };
2366 let mut rims = Vec::with_capacity(2);
2368 for bound in [first, second] {
2369 let (loop_id, _) = self.bound_args(bound)?;
2370 let Ok(loop_args) = self.args(loop_id, "EDGE_LOOP") else {
2371 return Ok(());
2372 };
2373 let edge_ids: Vec<u64> = loop_args
2374 .get(1)
2375 .and_then(Arg::list)
2376 .unwrap_or(&[])
2377 .iter()
2378 .filter_map(Arg::reference)
2379 .filter_map(|oe| {
2380 self.args(oe, "ORIENTED_EDGE")
2381 .ok()
2382 .and_then(|a| a.get(3).and_then(Arg::reference))
2383 })
2384 .collect();
2385 let [edge_id] = edge_ids[..] else {
2386 return Ok(());
2387 };
2388 rims.push(edge_id);
2389 }
2390 let mut rings: Vec<(Vec<Shape>, ogeom_math::Circle)> = Vec::with_capacity(2);
2392 for &edge_id in &rims {
2393 let Some(pieces) = self.edge_pieces(edge_id)? else {
2394 return Ok(());
2395 };
2396 let Curve::Circle(c) = &pieces[0].1.1 else {
2397 return Ok(());
2398 };
2399 let circle = c.circle();
2400 let shapes: Vec<Shape> = pieces.into_iter().map(|(_, (s, ..))| s).collect();
2401 if shapes.len() == 1 {
2402 let Some((a, b)) = ogeom_algo::edge_vertices(&self.model, &shapes[0])? else {
2403 return Ok(());
2404 };
2405 if !a.is_same(&b) {
2406 return Ok(());
2407 }
2408 }
2409 rings.push((shapes, circle));
2410 }
2411 let all: Vec<&Shape> = rings.iter().flat_map(|(s, _)| s.iter()).collect();
2412 if !ogeom_algo::rings_are_parallels(&self.model, &surface, &all, self.tol)? {
2413 return Ok(());
2414 }
2415 let starts = |model: &Model, ring: &[Shape]| -> OgeomResult<Vec<(Shape, Point)>> {
2416 let mut out = Vec::with_capacity(ring.len());
2417 for edge in ring {
2418 if let Some((v, _)) = ogeom_algo::edge_vertices(model, edge)?
2419 && let Some(data) = model.node(&v).and_then(|n| n.data().as_vertex())
2420 {
2421 let at = data.point;
2422 out.push((v, at));
2423 }
2424 }
2425 Ok(out)
2426 };
2427 let starts_a = starts(&self.model, &rings[0].0)?;
2428 let starts_b = starts(&self.model, &rings[1].0)?;
2429 let shared = starts_a.iter().any(|(_, p)| {
2430 starts_b
2431 .iter()
2432 .any(|(_, q)| same_column(&rings[1].1, *p, *q, self.tol))
2433 });
2434 let split_any = rings.iter().any(|(s, _)| s.len() > 1);
2435 if shared {
2436 if split_any {
2437 self.band_faces.insert(fid);
2438 }
2439 return Ok(());
2440 }
2441 let (target, anchor) = if rings[1].0.len() == 1 {
2443 (1, &starts_a[0].0)
2444 } else if rings[0].0.len() == 1 {
2445 (0, &starts_b[0].0)
2446 } else {
2447 return Ok(());
2448 };
2449 let edge_id = rims[target];
2450 let Some((shape, curve, _, flipped)) = self.edges.get(&edge_id).cloned() else {
2451 return Ok(());
2452 };
2453 let Some(split) =
2454 ogeom_algo::split_ring_on_column(&mut self.model, &surface, &shape, anchor, self.tol)?
2455 else {
2456 return Ok(());
2457 };
2458 let mut pieces = Vec::with_capacity(2);
2459 for piece in split.history.modified(&shape) {
2460 let Some(ogeom_topo::EdgeRepr::Curve3d { range, .. }) = self
2461 .model
2462 .node(piece)
2463 .and_then(|n| n.data().as_edge())
2464 .and_then(|d| d.curve3d())
2465 else {
2466 return Ok(());
2467 };
2468 self.piece_key -= 1;
2469 pieces.push((
2470 self.piece_key,
2471 (piece.clone(), curve.clone(), *range, flipped),
2472 ));
2473 }
2474 self.pieces.insert(edge_id, pieces);
2475 self.band_faces.insert(fid);
2476 Ok(())
2477 }
2478
2479 fn band_of_split_rims(
2484 &mut self,
2485 surface: &SurfaceGeometry,
2486 wires: &[Shape],
2487 ) -> OgeomResult<Shape> {
2488 let [wire_a, wire_b] = wires else {
2489 ogeom_bail!(Construction, "a band has two rims");
2490 };
2491 let ring_a = self.model.ordered_children_of(wire_a)?;
2492 let ring_b = self.model.ordered_children_of(wire_b)?;
2493 let circle = {
2494 let Some(ogeom_topo::EdgeRepr::Curve3d { curve, .. }) = self
2495 .model
2496 .node(&ring_b[0])
2497 .and_then(|n| n.data().as_edge())
2498 .and_then(|d| d.curve3d())
2499 else {
2500 ogeom_bail!(Construction, "a band rim has no curve");
2501 };
2502 let Some(Curve::Circle(c)) = self.model.geometry().curve(*curve) else {
2503 ogeom_bail!(Construction, "a band rim is not a circle");
2504 };
2505 c.circle()
2506 };
2507 let start = |model: &Model, edge: &Shape| -> OgeomResult<Point> {
2508 let Some((v, _)) = ogeom_algo::edge_vertices(model, edge)? else {
2509 ogeom_bail!(Construction, "a band rim has no vertex");
2510 };
2511 let Some(data) = model.node(&v).and_then(|n| n.data().as_vertex()) else {
2512 ogeom_bail!(Construction, "a band rim's vertex holds no point");
2513 };
2514 Ok(data.point)
2515 };
2516 for i in 0..ring_a.len() {
2517 let p = start(&self.model, &ring_a[i])?;
2518 for j in 0..ring_b.len() {
2519 let q = start(&self.model, &ring_b[j])?;
2520 if !same_column(&circle, p, q, self.tol) {
2521 continue;
2522 }
2523 let mut a = ring_a.clone();
2524 a.rotate_left(i);
2525 let mut b = ring_b.clone();
2526 b.rotate_left(j);
2527 return ogeom_algo::make_band_of_rings(&mut self.model, surface, &a, &b, self.tol);
2528 }
2529 }
2530 ogeom_bail!(Construction, "the band's rims share no vertex column")
2531 }
2532
2533 fn face(&mut self, id: u64) -> OgeomResult<Option<Shape>> {
2534 if let Some(shape) = self.faces.get(&id) {
2535 return Ok(Some(shape.clone()));
2536 }
2537 let args = self.face_args(id)?;
2538 let bounds: Vec<u64> = args
2539 .get(1)
2540 .and_then(Arg::list)
2541 .unwrap_or(&[])
2542 .iter()
2543 .filter_map(Arg::reference)
2544 .collect();
2545 let Some(surface) = self.surface(ref_at(args, 2))? else {
2546 return Ok(None);
2547 };
2548 let face_forward = !args.get(3).is_some_and(|a| a.is_enum("F"));
2549 let surface_id = self.model.geometry_mut().add_surface(surface.clone());
2550
2551 let mut ordered = bounds.clone();
2553 ordered.sort_by_key(|b| {
2554 self.instances
2555 .get(*b)
2556 .map_or(1, |i| i32::from(i.part("FACE_OUTER_BOUND").is_none()))
2557 });
2558
2559 let mut edge_uses: FastMap<u64, usize> = FastMap::default();
2562 for &bound in &ordered {
2563 let bargs = self.bound_args(bound)?;
2564 if self.instance(bargs.0)?.part("VERTEX_LOOP").is_some() {
2565 continue;
2566 }
2567 let loop_args = self.args(bargs.0, "EDGE_LOOP")?;
2568 for oe in loop_args.get(1).and_then(Arg::list).unwrap_or(&[]) {
2569 if let Some(oe_id) = oe.reference() {
2570 let oargs = self.args(oe_id, "ORIENTED_EDGE")?;
2571 if let Some(e) = oargs.get(3).and_then(Arg::reference) {
2572 for key in self.piece_keys(e) {
2573 *edge_uses.entry(key).or_default() += 1;
2574 }
2575 }
2576 }
2577 }
2578 }
2579
2580 let mut wires = Vec::new();
2581 let mut annotated: FastSet<u64> = FastSet::default();
2582 for &bound in &ordered {
2583 let (loop_id, bound_forward) = self.bound_args(bound)?;
2584 if let Some(vertex_loop) = {
2585 let instance = self.instance(loop_id)?;
2586 instance.part("VERTEX_LOOP")
2587 } {
2588 let vertex_id = vertex_loop.get(1).and_then(Arg::reference).unwrap_or(0);
2594 let vargs = self.args(vertex_id, "VERTEX_POINT")?;
2595 let at = self.point(ref_at(vargs, 1))?;
2596 let vertex = self.vertex(vertex_id)?;
2597 let projection = ogeom_algo::project_on_surface(&surface, at, 32, self.tol)?;
2598 let ((ua, ub), _) = surface.domain();
2599 let row = projection.parameters.1;
2600 let mut data = ogeom_topo::EdgeData::new();
2601 data.degenerate = true;
2602 let edge = self
2603 .model
2604 .add_edge(data, &[vertex.clone(), vertex.clone()])?;
2605 let pcurve: PlanarCurve = ogeom_geom::Line2d::segment(
2606 ogeom_math::Point2::new(ua, row),
2607 ogeom_math::Point2::new(ub, row),
2608 self.tol,
2609 )?
2610 .into();
2611 ogeom_algo::attach_pcurve(
2612 &mut self.model,
2613 &edge,
2614 pcurve,
2615 surface_id,
2616 Location::identity(),
2617 (0.0, ub - ua),
2618 )?;
2619 wires.push(make_wire(&mut self.model, &[edge], self.tol)?.shape);
2620 continue;
2621 }
2622 let loop_args = self.args(loop_id, "EDGE_LOOP")?;
2623 let mut uses: Vec<BoundUse> = Vec::new();
2624 for oe in loop_args.get(1).and_then(Arg::list).unwrap_or(&[]) {
2625 let Some(oe_id) = oe.reference() else {
2626 continue;
2627 };
2628 let oargs = self.args(oe_id, "ORIENTED_EDGE")?;
2629 let Some(edge_id) = oargs.get(3).and_then(Arg::reference) else {
2630 continue;
2631 };
2632 let forward = !oargs.get(4).is_some_and(|a| a.is_enum("F"));
2633 let Some(pieces) = self.edge_pieces(edge_id)? else {
2634 self.report.warnings.push(format!(
2635 "#{id}: a bound references unreadable edge #{edge_id}; \
2636 the face is skipped"
2637 ));
2638 return Ok(None);
2639 };
2640 let flipped = pieces.first().is_some_and(|(_, built)| built.3);
2641 let mut use_forward = forward == bound_forward;
2646 if flipped {
2647 use_forward = !use_forward;
2648 }
2649 let mut walked: Vec<BoundUse> = pieces
2650 .into_iter()
2651 .map(|(key, (shape, curve, range, _))| {
2652 let placed = if use_forward {
2653 shape.clone()
2654 } else {
2655 shape.reversed()
2656 };
2657 (placed, shape, key, curve, range)
2658 })
2659 .collect();
2660 if forward == flipped {
2661 walked.reverse();
2662 }
2663 uses.extend(walked);
2664 }
2665 if !bound_forward {
2666 uses.reverse();
2667 }
2668 self.widen_window(surface_id, &uses)?;
2671 let widened = self
2672 .model
2673 .geometry()
2674 .surface(surface_id)
2675 .cloned()
2676 .unwrap_or_else(|| surface.clone());
2677 self.chart_bound(id, &uses, &widened, surface_id, &edge_uses, &mut annotated)?;
2678 let edges: Vec<Shape> = uses.into_iter().map(|(placed, ..)| placed).collect();
2679 if edges.is_empty() {
2680 continue;
2681 }
2682 wires.push(make_wire(&mut self.model, &edges, self.tol)?.shape);
2683 }
2684 if wires.is_empty() {
2685 self.report
2686 .warnings
2687 .push(format!("#{id}: a face with no readable bounds is skipped"));
2688 return Ok(None);
2689 }
2690 let surface = self
2694 .model
2695 .geometry()
2696 .surface(surface_id)
2697 .cloned()
2698 .unwrap_or(surface);
2699
2700 if wires.len() == 2 && self.band_faces.contains(&id) {
2704 match self.band_of_split_rims(&surface, &wires) {
2705 Ok(shape) => {
2706 self.faces.insert(id, shape.clone());
2707 return Ok(Some(shape));
2708 }
2709 Err(e) => {
2710 self.report.warnings.push(format!(
2711 "#{id}: no seam could be synthesised ({e}); the \
2712 face may not triangulate"
2713 ));
2714 }
2715 }
2716 }
2717
2718 if wires.len() == 2
2724 && surface.is_periodic_u()
2725 && let [(e_lo, _v_lo), (e_hi, _v_hi)] =
2726 closed_ring_edges(&self.model, &wires)?.as_slice()
2727 && ogeom_algo::rings_are_parallels(&self.model, &surface, &[e_lo, e_hi], self.tol)?
2733 {
2734 {
2735 match ogeom_algo::make_revolution_band(
2741 &mut self.model,
2742 &surface,
2743 e_lo,
2744 e_hi,
2745 self.tol,
2746 ) {
2747 Ok(built) => {
2748 let shape = if face_forward {
2749 built
2750 } else {
2751 built.reversed()
2752 };
2753 self.faces.insert(id, shape.clone());
2754 return Ok(Some(shape));
2755 }
2756 Err(e) => {
2757 self.report.warnings.push(format!(
2758 "#{id}: no seam could be synthesised ({e}); the \
2759 face may not triangulate"
2760 ));
2761 }
2762 }
2763 }
2764 }
2765
2766 if wires.len() == 1
2772 && matches!(surface, SurfaceGeometry::Cone(_))
2773 && let [(ring, _)] = closed_ring_edges(&self.model, &wires)?.as_slice()
2774 {
2775 match ogeom_algo::make_apex_band(&mut self.model, &surface, ring, self.tol) {
2776 Ok(built) => {
2777 let shape = if face_forward {
2778 built
2779 } else {
2780 built.reversed()
2781 };
2782 self.faces.insert(id, shape.clone());
2783 return Ok(Some(shape));
2784 }
2785 Err(e) => {
2786 self.report.warnings.push(format!(
2787 "#{id}: no apex could be synthesised ({e}); the face \
2788 may not triangulate"
2789 ));
2790 }
2791 }
2792 }
2793
2794 if wires.len() == 1
2800 && let SurfaceGeometry::Sphere(ball) = &surface
2801 && let [(ring, at)] = closed_ring_edges(&self.model, &wires)?.as_slice()
2802 && ogeom_algo::rings_are_parallels(&self.model, &surface, &[ring], self.tol)?
2803 && let Some(pole) = pole_on_the_left(
2804 &self.model,
2805 &surface,
2806 ball.sphere(),
2807 ring,
2808 at,
2809 face_forward,
2810 self.tol,
2811 )?
2812 {
2813 let vertex = self.model.add_vertex(ogeom_topo::VertexData::new(pole));
2814 let mut data = ogeom_topo::EdgeData::new();
2815 data.degenerate = true;
2816 let collapsed = self.model.add_edge(data, &[vertex.clone(), vertex])?;
2817 match ogeom_algo::make_revolution_band(
2818 &mut self.model,
2819 &surface,
2820 ring,
2821 &collapsed,
2822 self.tol,
2823 ) {
2824 Ok(built) => {
2825 let shape = if face_forward {
2826 built
2827 } else {
2828 built.reversed()
2829 };
2830 self.faces.insert(id, shape.clone());
2831 return Ok(Some(shape));
2832 }
2833 Err(e) => {
2834 self.report.warnings.push(format!(
2835 "#{id}: no pole could be synthesised ({e}); the face \
2836 may not triangulate"
2837 ));
2838 }
2839 }
2840 }
2841
2842 if let Some(belt) = ogeom_algo::belted_wire(&mut self.model, surface_id, &wires, self.tol)?
2845 {
2846 wires = vec![belt];
2847 }
2848 ogeom_algo::chain_wire_branches(&mut self.model, surface_id, &wires, self.tol)?;
2852 for wire in &mut wires {
2855 let edges = self.model.ordered_children_of(wire)?;
2856 let closed =
2857 ogeom_algo::closed_at_poles(&mut self.model, surface_id, &edges, self.tol)?;
2858 if closed.len() != edges.len() {
2859 *wire = make_wire(&mut self.model, &closed, self.tol)?.shape;
2860 }
2861 }
2862 if !face_forward {
2867 wires = walked_back(&mut self.model, &wires, self.tol)?;
2868 }
2869 let mut built = make_face_on(&mut self.model, surface_id, &wires, self.tol)?.shape;
2870 let crossed = ogeom_algo::pole_crossings(&mut self.model, &built, self.tol)?;
2874 if !crossed.is_empty() {
2875 for wire in &mut wires {
2876 if let Some((_, new)) = crossed.iter().find(|(old, _)| old.node() == wire.node()) {
2877 *wire = new.clone();
2878 }
2879 }
2880 built = make_face_on(&mut self.model, surface_id, &wires, self.tol)?.shape;
2881 self.report.warnings.push(format!(
2882 "#{id}: the boundary crosses a pole of its surface a whole turn \
2883 round; a degenerate edge was laid across it"
2884 ));
2885 self.tally("pole-crossed", 0.0, id);
2886 }
2887 let shape = if face_forward {
2888 built
2889 } else {
2890 built.reversed()
2891 };
2892 self.faces.insert(id, shape.clone());
2893 Ok(Some(shape))
2894 }
2895
2896 fn bound_args(&mut self, id: u64) -> OgeomResult<(u64, bool)> {
2898 let instance = self.instance(id)?;
2899 let args = instance
2900 .part("FACE_OUTER_BOUND")
2901 .or_else(|| instance.part("FACE_BOUND"))
2902 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "#{id} is not a face bound"))?
2903 .to_vec();
2904 let loop_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
2905 let forward = !args.get(2).is_some_and(|a| a.is_enum("F"));
2906 Ok((loop_id, forward))
2907 }
2908
2909 fn widen_window(
2924 &mut self,
2925 surface_id: ogeom_topo::SurfaceId,
2926 uses: &[BoundUse],
2927 ) -> OgeomResult<()> {
2928 use ogeom_geom::Curve3d as _;
2929 let Some(surface) = self.model.geometry().surface(surface_id).cloned() else {
2930 return Ok(());
2931 };
2932 let along: Box<ChartReading> = match &surface {
2935 SurfaceGeometry::Plane(p) => {
2936 let frame = p.plane().frame();
2937 let (origin, x, y) = (frame.origin(), frame.x().vector(), frame.y().vector());
2938 Box::new(move |at: Point| (Some((at - origin).dot(x)), Some((at - origin).dot(y))))
2939 }
2940 SurfaceGeometry::Cylinder(c) => {
2941 let axis = c.cylinder().axis();
2942 let (origin, direction) = (axis.location, axis.direction.vector());
2943 Box::new(move |at: Point| (None, Some((at - origin).dot(direction))))
2944 }
2945 SurfaceGeometry::Cone(c) => {
2946 let axis = c.cone().axis();
2947 let (origin, direction) = (axis.location, axis.direction.vector());
2948 Box::new(move |at: Point| (None, Some((at - origin).dot(direction))))
2949 }
2950 SurfaceGeometry::Extrusion(e) => {
2955 let direction = e.direction().vector();
2956 let (lo, hi) = e.curve().domain();
2957 let (mut least, mut most) = (f64::INFINITY, f64::NEG_INFINITY);
2958 for k in 0..=32 {
2959 let t = lo + (hi - lo) * f64::from(k) / 32.0;
2960 if let Ok(p) = e.curve().point_at(t, self.tol) {
2961 let reach = p.to_vector().dot(direction);
2962 least = least.min(reach);
2963 most = most.max(reach);
2964 }
2965 }
2966 let middle = f64::midpoint(least, most);
2967 Box::new(move |at: Point| (None, Some(at.to_vector().dot(direction) - middle)))
2968 }
2969 _ => return Ok(()),
2970 };
2971 const STATIONS: usize = 4;
2972 let (mut ua, mut ub) = (-SURFACE_EXTENT, SURFACE_EXTENT);
2973 let (mut va, mut vb) = (-SURFACE_EXTENT, SURFACE_EXTENT);
2974 for (_, _, _, curve, range) in uses {
2975 for step in 0..=STATIONS {
2976 #[allow(clippy::cast_precision_loss)]
2977 let t = range.0 + (range.1 - range.0) * (step as f64 / STATIONS as f64);
2978 let Ok(at) = curve.point_at(t, self.tol) else {
2979 continue;
2980 };
2981 let (u, v) = along(at);
2982 if let Some(u) = u {
2983 ua = ua.min(u);
2984 ub = ub.max(u);
2985 }
2986 if let Some(v) = v {
2987 va = va.min(v);
2988 vb = vb.max(v);
2989 }
2990 }
2991 }
2992 let ((was_u, was_v), grown) = (surface.domain(), (ub - ua) * 0.05);
2993 if ua >= was_u.0 && ub <= was_u.1 && va >= was_v.0 && vb <= was_v.1 {
2994 return Ok(());
2995 }
2996 let (u, v) = (
2997 (ua.min(was_u.0) - grown, ub.max(was_u.1) + grown),
2998 (va.min(was_v.0) - grown, vb.max(was_v.1) + grown),
2999 );
3000 let wider = match surface {
3001 SurfaceGeometry::Plane(p) => PlaneSurface::over(p.plane(), u, v)?.into(),
3002 SurfaceGeometry::Cylinder(c) => CylinderSurface::new(c.cylinder(), v)?.into(),
3003 SurfaceGeometry::Cone(c) => ConeSurface::new(c.cone(), v)?.into(),
3004 SurfaceGeometry::Extrusion(e) => {
3005 ExtrusionSurface::over(e.curve().clone(), e.direction(), v)?.into()
3006 }
3007 other => other,
3008 };
3009 if let Some(held) = self.model.geometry_mut().surface_mut(surface_id) {
3010 *held = wider;
3011 }
3012 Ok(())
3013 }
3014
3015 fn chart_bound(
3033 &mut self,
3034 face_id: u64,
3035 uses: &[BoundUse],
3036 surface: &SurfaceGeometry,
3037 surface_id: ogeom_topo::SurfaceId,
3038 edge_uses: &FastMap<u64, usize>,
3039 annotated: &mut FastSet<u64>,
3040 ) -> OgeomResult<()> {
3041 use ogeom_geom::Curve2d as _;
3042 let mut images: FastMap<u64, PlanarCurve> = FastMap::default();
3043 let mut sides: FastMap<u64, (Option<PlanarCurve>, Option<PlanarCurve>)> =
3044 FastMap::default();
3045 let mut order: Vec<u64> = Vec::new();
3046 let mut previous: Option<ogeom_math::Point2> = None;
3047 let mut opening: Option<(u64, bool, f64)> = None;
3048 for (placed, shape, edge_id, curve, range) in uses {
3049 if !images.contains_key(edge_id) {
3050 let Some(image) =
3051 self.image_for(face_id, *edge_id, shape, curve, *range, surface)?
3052 else {
3053 continue;
3054 };
3055 images.insert(*edge_id, image);
3056 order.push(*edge_id);
3057 }
3058 let Some(image) = images.get(edge_id).cloned() else {
3059 continue;
3060 };
3061 let backwards = placed.orientation() == ogeom_topo::Orientation::Reversed;
3062 let (start, end) = if backwards {
3063 (range.1, range.0)
3064 } else {
3065 (range.0, range.1)
3066 };
3067 if previous.is_none() && edge_uses.get(edge_id).copied().unwrap_or(1) == 1 {
3068 opening = Some((*edge_id, backwards, start));
3069 }
3070 let mut image =
3071 crate::pcurves::shifted_to_meet(&image, start, previous, surface, self.tol)?;
3072 let walked = sides.entry(*edge_id).or_default();
3073 let first = if backwards { &walked.0 } else { &walked.1 };
3074 if edge_uses.get(edge_id).copied().unwrap_or(1) > 1
3075 && let Some(first) = first
3076 {
3077 image = crate::pcurves::across_from(image, first, *range, surface, self.tol)?;
3078 }
3079 previous = Some(image.point_at(end, self.tol)?);
3080 if backwards {
3081 walked.1 = Some(image);
3082 } else {
3083 walked.0 = Some(image);
3084 }
3085 }
3086 if let (Some((edge_id, backwards, start)), Some(end)) = (opening, previous)
3089 && let Some(walked) = sides.get_mut(&edge_id)
3090 {
3091 let side = if backwards {
3092 &mut walked.1
3093 } else {
3094 &mut walked.0
3095 };
3096 if let Some(image) = side.as_ref() {
3097 *side = Some(crate::pcurves::onto_closing_side(
3098 image, start, end, surface, self.tol,
3099 )?);
3100 }
3101 }
3102 for edge_id in order {
3103 if !annotated.insert(edge_id) {
3104 continue;
3105 }
3106 let Some((shape, range)) = uses
3107 .iter()
3108 .find(|(_, _, id, _, _)| *id == edge_id)
3109 .map(|(_, shape, _, _, range)| (shape.clone(), *range))
3110 else {
3111 continue;
3112 };
3113 let Some(image) = images.get(&edge_id).cloned() else {
3114 continue;
3115 };
3116 let walked = sides.remove(&edge_id).unwrap_or((None, None));
3117 let seam = edge_uses.get(&edge_id).copied().unwrap_or(1) > 1;
3118 let columns = match &walked {
3119 (Some(forward), Some(reversed)) => {
3120 let at = forward.point_at(range.0, self.tol)?;
3121 let other = reversed.point_at(range.0, self.tol)?;
3122 (at.distance(other) > self.tol.confusion())
3123 .then(|| (forward.clone(), reversed.clone()))
3124 }
3125 _ => None,
3126 };
3127 match (seam, columns) {
3128 (true, Some((forward, reversed))) => {
3129 self.record_columns(&shape, forward, reversed, surface_id, range)?;
3130 }
3131 (true, None) => {
3132 self.record_pcurve(&shape, image, surface_id, true, range)?;
3133 }
3134 (false, _) => {
3135 let (forward, reversed) = walked;
3136 let placed = forward.or(reversed).unwrap_or(image);
3137 self.record_pcurve(&shape, placed, surface_id, false, range)?;
3138 }
3139 }
3140 }
3141 Ok(())
3142 }
3143
3144 fn image_for(
3145 &mut self,
3146 face_id: u64,
3147 edge_id: u64,
3148 edge: &Shape,
3149 curve: &Curve,
3150 range: (f64, f64),
3151 surface: &SurfaceGeometry,
3152 ) -> OgeomResult<Option<PlanarCurve>> {
3153 let (prepared, offered) = match self.pcurves.remove(&(face_id, edge_id)) {
3158 Some(prepared) => prepared,
3159 None => {
3160 let stated = self.stated_pcurves(face_id, edge_id);
3161 let offered = !stated.is_empty();
3162 (
3163 derive_pcurve(curve, range, surface, &stated, self.tol),
3164 offered,
3165 )
3166 }
3167 };
3168 if matches!(prepared, PreparedPcurve::Stated { .. }) {
3169 self.report.pcurves_kept += 1;
3170 } else if offered {
3171 self.report.pcurves_derived += 1;
3172 }
3173 let (pcurve, off) = match prepared {
3174 PreparedPcurve::Exact { curve, off } | PreparedPcurve::Stated { curve, off } => {
3175 (crate::pcurves::over_range(&curve, range), off)
3176 }
3177 PreparedPcurve::Fitted {
3178 curve: fitted,
3179 error,
3180 met,
3181 worst_off,
3182 off,
3183 warning,
3184 } => {
3185 if let Some(w) = warning {
3186 self.warn_slop(w, worst_off, face_id);
3187 }
3188 if !met {
3189 self.warn_fit_short(face_id, error);
3190 }
3191 (fitted, off.max(worst_off))
3192 }
3193 PreparedPcurve::Refused(why) => {
3194 self.report.warnings.push(format!(
3195 "face #{face_id}: no pcurve for an edge on this surface ({why}); \
3196 the face may not triangulate"
3197 ));
3198 self.note_untrimmed(face_id);
3199 return Ok(None);
3200 }
3201 };
3202 if off > self.tol.confusion() * 1e7 {
3212 self.warn_fit_short(face_id, off);
3213 } else if off > self.tol.confusion()
3214 && let Some(node) = self.model.node_mut(edge)
3215 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
3216 {
3217 data.tolerance = data.tolerance.widen_to(off + self.tol.confusion());
3218 }
3219 Ok(Some(pcurve))
3220 }
3221
3222 fn stated_pcurves(&mut self, face_id: u64, edge_id: u64) -> Vec<PlanarCurve> {
3226 if self.pieces.contains_key(&edge_id) || !self.instances.contains(edge_id) {
3227 return Vec::new();
3228 }
3229 let Some(surface_id) = self
3230 .face_args(face_id)
3231 .ok()
3232 .and_then(|args| args.get(2).and_then(Arg::reference))
3233 else {
3234 return Vec::new();
3235 };
3236 self.file_pcurves(edge_id, surface_id)
3237 }
3238
3239 fn tally(&mut self, kind: &'static str, measured: f64, exemplar: u64) {
3241 let entry = self.tallies.entry(kind).or_insert((0, 0.0, exemplar));
3242 entry.0 += 1;
3243 if measured > entry.1 {
3244 entry.1 = measured;
3245 entry.2 = exemplar;
3246 }
3247 }
3248
3249 fn warn_vertex_miss(&mut self, id: u64, gap: f64) {
3251 self.report.warnings.push(format!(
3252 "#{id}: a curve end misses its vertex by {gap:.2e}; the vertex \
3253 tolerance grew to say so"
3254 ));
3255 self.tally("vertex-miss", gap, id);
3256 }
3257
3258 fn warn_fit_short(&mut self, face_id: u64, error: f64) {
3260 self.report.warnings.push(format!(
3261 "face #{face_id}: a projected pcurve fit stopped at {error:.2e}; \
3262 the face's mesh may sit that far off along this edge"
3263 ));
3264 self.tally("fit-short", error, face_id);
3265 }
3266
3267 fn warn_slop(&mut self, prose: String, worst: f64, exemplar: u64) {
3269 self.report.warnings.push(prose);
3270 self.tally("boundary-slop", worst, exemplar);
3271 }
3272
3273 fn note_untrimmed(&mut self, face_id: u64) {
3275 if self.untrimmed_ids.last() != Some(&face_id) {
3276 self.untrimmed_ids.push(face_id);
3277 self.tally("untrimmed", 0.0, face_id);
3278 }
3279 }
3280
3281 fn record_columns(
3283 &mut self,
3284 edge: &Shape,
3285 forward: PlanarCurve,
3286 reversed: PlanarCurve,
3287 surface_id: ogeom_topo::SurfaceId,
3288 range: (f64, f64),
3289 ) -> OgeomResult<()> {
3290 ogeom_algo::attach_seam(
3291 &mut self.model,
3292 edge,
3293 forward,
3294 reversed,
3295 surface_id,
3296 Location::identity(),
3297 range,
3298 )
3299 }
3300
3301 fn record_pcurve(
3304 &mut self,
3305 edge: &Shape,
3306 pcurve: PlanarCurve,
3307 surface_id: ogeom_topo::SurfaceId,
3308 seam: bool,
3309 range: (f64, f64),
3310 ) -> OgeomResult<()> {
3311 if seam {
3312 let Some(surface) = self.model.geometry().surface(surface_id).cloned() else {
3313 ogeom_bail!(Dangling, "the surface is not in this model");
3314 };
3315 let other = crate::pcurves::seam_other_side(&pcurve, range, &surface, self.tol)?;
3318 ogeom_algo::attach_seam(
3319 &mut self.model,
3320 edge,
3321 pcurve,
3322 other,
3323 surface_id,
3324 Location::identity(),
3325 range,
3326 )?;
3327 } else {
3328 ogeom_algo::attach_pcurve(
3329 &mut self.model,
3330 edge,
3331 pcurve,
3332 surface_id,
3333 Location::identity(),
3334 range,
3335 )?;
3336 }
3337 Ok(())
3338 }
3339
3340 fn prepare_pcurves(&mut self, face_ids: &[u64]) {
3355 struct Job {
3356 face: u64,
3357 edge: u64,
3358 curve: Curve,
3359 range: (f64, f64),
3360 stated: Vec<PlanarCurve>,
3362 surface: usize,
3367 }
3368 let mut surfaces: Vec<SurfaceGeometry> = Vec::new();
3369 let mut jobs: Vec<Job> = Vec::new();
3370 let mut seen: FastSet<(u64, u64)> = FastSet::default();
3371 for &fid in face_ids {
3372 let Ok(args) = self.face_args(fid) else {
3373 continue;
3374 };
3375 let Some(surface) = args
3376 .get(2)
3377 .and_then(Arg::reference)
3378 .and_then(|sid| self.surface(sid).ok().flatten())
3379 else {
3380 continue;
3381 };
3382 surfaces.push(surface);
3383 let at = surfaces.len() - 1;
3384 let bounds: Vec<u64> = args
3385 .get(1)
3386 .and_then(Arg::list)
3387 .unwrap_or(&[])
3388 .iter()
3389 .filter_map(Arg::reference)
3390 .collect();
3391 for bound in bounds {
3392 let Ok((loop_id, _)) = self.bound_args(bound) else {
3393 continue;
3394 };
3395 let Ok(loop_args) = self.args(loop_id, "EDGE_LOOP") else {
3396 continue;
3397 };
3398 let uses: Vec<u64> = loop_args
3399 .get(1)
3400 .and_then(Arg::list)
3401 .unwrap_or(&[])
3402 .iter()
3403 .filter_map(Arg::reference)
3404 .collect();
3405 for oe_id in uses {
3406 let Ok(oargs) = self.args(oe_id, "ORIENTED_EDGE") else {
3407 continue;
3408 };
3409 let Some(edge_id) = oargs.get(3).and_then(Arg::reference) else {
3410 continue;
3411 };
3412 if !seen.insert((fid, edge_id)) {
3413 continue;
3414 }
3415 if let Ok(Some(pieces)) = self.edge_pieces(edge_id) {
3416 for (key, (_, curve, range, _)) in pieces {
3417 let stated = self.stated_pcurves(fid, key);
3418 jobs.push(Job {
3419 face: fid,
3420 edge: key,
3421 curve,
3422 range,
3423 stated,
3424 surface: at,
3425 });
3426 }
3427 }
3428 }
3429 }
3430 }
3431 if jobs.len() < 16 {
3435 return;
3436 }
3437 let tol = self.tol;
3438 let derived = ogeom_core::parallel::map_ordered(&jobs, |_, job| {
3439 derive_pcurve(
3440 &job.curve,
3441 job.range,
3442 &surfaces[job.surface],
3443 &job.stated,
3444 tol,
3445 )
3446 });
3447 for (job, pcurve) in jobs.iter().zip(derived) {
3448 self.pcurves
3449 .insert((job.face, job.edge), (pcurve, !job.stated.is_empty()));
3450 }
3451 }
3452
3453 fn solid(&mut self, id: u64) -> OgeomResult<Shape> {
3465 let instance = self.instance(id)?;
3466 let args = instance
3467 .part("MANIFOLD_SOLID_BREP")
3468 .or_else(|| instance.part("BREP_WITH_VOIDS"))
3469 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "#{id} is not a solid"))?
3470 .to_vec();
3471 let shell_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
3472 let Some(shell) = self.shell(shell_id)? else {
3473 ogeom_bail!(Construction, "#{id}: a solid with no readable faces");
3474 };
3475 if !ogeom_algo::is_shell_closed(&self.model, &shell)? {
3476 self.report.warnings.push(format!(
3477 "#{id}: the shell does not close as read; measures needing an \
3478 inside will refuse it"
3479 ));
3480 }
3481 let mut shells = vec![shell];
3482 for void_id in args
3483 .get(2)
3484 .and_then(Arg::list)
3485 .unwrap_or(&[])
3486 .iter()
3487 .filter_map(Arg::reference)
3488 .collect::<Vec<u64>>()
3489 {
3490 match self.shell(void_id) {
3491 Ok(Some(void)) => {
3492 let encloses = ogeom_mesh::triangulate(
3497 &self.model,
3498 &void,
3499 ogeom_mesh::Deflection::default(),
3500 self.tol,
3501 )
3502 .ok()
3503 .filter(ogeom_topo::Triangulation::is_closed)
3504 .map(|mesh| mesh.volume());
3505 if encloses.is_some_and(|v| v > 0.0) {
3506 self.report.warnings.push(format!(
3507 "#{id}: void shell #{void_id} was written the wrong way \
3508 out; it is read as the cavity it bounds"
3509 ));
3510 shells.push(void.reversed());
3511 } else {
3512 shells.push(void);
3513 }
3514 }
3515 Ok(None) => self.report.warnings.push(format!(
3516 "#{id}: void shell #{void_id} has no readable faces; the \
3517 cavity is missing from the solid"
3518 )),
3519 Err(refusal) => self.report.warnings.push(format!(
3520 "#{id}: void shell #{void_id} could not be read ({refusal}); \
3521 the cavity is missing from the solid"
3522 )),
3523 }
3524 }
3525 Ok(make_solid(&mut self.model, &shells)?.shape)
3526 }
3527
3528 fn surface_model(&mut self, id: u64) -> OgeomResult<Shape> {
3538 let args = self.args(id, "SHELL_BASED_SURFACE_MODEL")?;
3539 let shell_ids: Vec<u64> = args
3540 .get(1)
3541 .and_then(Arg::list)
3542 .unwrap_or(&[])
3543 .iter()
3544 .filter_map(Arg::reference)
3545 .collect();
3546 let mut shells = Vec::new();
3547 for shell_id in shell_ids {
3548 if let Some(shell) = self.shell(shell_id)? {
3549 shells.push(shell);
3550 }
3551 }
3552 match shells.len() {
3553 0 => ogeom_bail!(
3554 Construction,
3555 "#{id}: a surface model with no readable faces"
3556 ),
3557 1 => Ok(shells.remove(0)),
3558 _ => Ok(ogeom_algo::build::make_compound(&mut self.model, &shells)?.shape),
3559 }
3560 }
3561
3562 fn shell(&mut self, shell_id: u64) -> OgeomResult<Option<Shape>> {
3569 self.nested(|reader| reader.shell_at(shell_id))
3570 }
3571
3572 fn shell_at(&mut self, shell_id: u64) -> OgeomResult<Option<Shape>> {
3573 let shell_instance = self.instance(shell_id)?;
3574 if let Some(oriented) = shell_instance.part("ORIENTED_CLOSED_SHELL") {
3575 let Some(base) = oriented.get(2).and_then(Arg::reference) else {
3576 ogeom_bail!(Construction, "#{shell_id}: an oriented shell names none");
3577 };
3578 let forward = !oriented.get(3).is_some_and(|a| a.is_enum("F"));
3579 return Ok(self
3580 .shell(base)?
3581 .map(|shell| if forward { shell } else { shell.reversed() }));
3582 }
3583 let shell_args = shell_instance
3584 .part("CLOSED_SHELL")
3585 .or_else(|| shell_instance.part("OPEN_SHELL"))
3586 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "#{shell_id} is not a shell"))?
3587 .to_vec();
3588 let face_ids: Vec<u64> = shell_args
3589 .get(1)
3590 .and_then(Arg::list)
3591 .unwrap_or(&[])
3592 .iter()
3593 .filter_map(Arg::reference)
3594 .collect();
3595 self.cut_at_poles(&face_ids);
3596 self.split_band_rims(&face_ids);
3597 self.prepare_pcurves(&face_ids);
3598 let mut read = Vec::new();
3599 for fid in face_ids {
3600 if let Some(face) = self.face(fid)? {
3601 read.push((fid, face));
3602 }
3603 }
3604 if read.is_empty() {
3605 return Ok(None);
3606 }
3607 self.turn_backward_loops(shell_id, &mut read)?;
3608 let faces: Vec<Shape> = read.into_iter().map(|(_, face)| face).collect();
3609 Ok(Some(make_shell(&mut self.model, &faces)?.shape))
3610 }
3611
3612 fn turn_backward_loops(&mut self, shell_id: u64, read: &mut [(u64, Shape)]) -> OgeomResult<()> {
3625 let mut uses: FastMap<(ogeom_topo::TShapeId, Location), Vec<(usize, bool)>> =
3627 FastMap::default();
3628 for (i, (_, face)) in read.iter().enumerate() {
3629 for wire in self.model.children_of(face)? {
3630 for edge in self.model.children_of(&wire)? {
3631 if self
3632 .model
3633 .node(&edge)
3634 .and_then(|n| n.data().as_edge())
3635 .is_some_and(|d| d.degenerate)
3636 {
3637 continue;
3638 }
3639 uses.entry((edge.node(), edge.location().clone()))
3640 .or_default()
3641 .push((i, edge.orientation() == ogeom_topo::Orientation::Forward));
3642 }
3643 }
3644 }
3645 let pairs: Vec<(usize, usize, bool)> = uses
3650 .values()
3651 .filter_map(|u| match u.as_slice() {
3652 [(a, x), (b, y)] if a != b => Some((*a, *b, x == y)),
3653 _ => None,
3654 })
3655 .collect();
3656 if !pairs.iter().any(|&(_, _, same)| same) {
3657 return Ok(());
3658 }
3659 let mut beside: Vec<Vec<(usize, bool)>> = vec![Vec::new(); read.len()];
3660 for &(a, b, same) in &pairs {
3661 beside[a].push((b, same));
3662 beside[b].push((a, same));
3663 }
3664 let mut turn: Vec<Option<bool>> = vec![None; read.len()];
3665 let mut groups: Vec<Vec<usize>> = Vec::new();
3666 for start in 0..read.len() {
3667 if turn[start].is_some() {
3668 continue;
3669 }
3670 turn[start] = Some(false);
3671 let mut group = vec![start];
3672 let mut k = 0;
3673 while k < group.len() {
3674 let f = group[k];
3675 k += 1;
3676 let mine = turn[f] == Some(true);
3677 for &(g, same) in &beside[f] {
3678 let want = mine ^ same;
3679 match turn[g] {
3680 None => {
3681 turn[g] = Some(want);
3682 group.push(g);
3683 }
3684 Some(had) if had != want => return Ok(()),
3685 Some(_) => {}
3686 }
3687 }
3688 }
3689 groups.push(group);
3690 }
3691 let backward = |i: usize| read[i].1.orientation() == ogeom_topo::Orientation::Reversed;
3692 let mut walk_back = Vec::new();
3693 for group in groups {
3694 let (marked, unmarked): (Vec<usize>, Vec<usize>) =
3695 group.iter().partition(|&&f| turn[f] == Some(true));
3696 let candidates = [marked, unmarked];
3697 let Some(chosen) = candidates
3698 .iter()
3699 .filter(|set| set.iter().all(|&f| backward(f)))
3700 .min_by_key(|set| set.len())
3701 else {
3702 return Ok(());
3703 };
3704 walk_back.extend(chosen.iter().copied());
3705 }
3706 if walk_back.is_empty() {
3707 return Ok(());
3708 }
3709 for &i in &walk_back {
3710 let (fid, face) = read[i].clone();
3711 let built = face.oriented(ogeom_topo::Orientation::Forward);
3712 let Some(surface_id) = self
3713 .model
3714 .node(&built)
3715 .and_then(|n| n.data().as_face())
3716 .map(|d| d.surface)
3717 else {
3718 ogeom_bail!(Construction, "#{fid}: a read face holds no face data");
3719 };
3720 let wires = self.model.ordered_children_of(&built)?;
3721 let wires = walked_back(&mut self.model, &wires, self.tol)?;
3722 let rebuilt = make_face_on(&mut self.model, surface_id, &wires, self.tol)?.shape;
3723 let turned = rebuilt.oriented(face.orientation());
3724 self.faces.insert(fid, turned.clone());
3725 read[i].1 = turned;
3726 }
3727 self.report.warnings.push(format!(
3728 "#{shell_id}: {} face(s) turned against their surfaces walked their \
3729 loops backward; their loops were walked back",
3730 walk_back.len()
3731 ));
3732 Ok(())
3733 }
3734
3735 fn construction_sets(&mut self) -> OgeomResult<ConstructionSets> {
3744 let mut reps: Vec<u64> = self
3745 .instances
3746 .iter()
3747 .filter(|(_, inst)| {
3748 inst.part("GEOMETRICALLY_BOUNDED_WIREFRAME_SHAPE_REPRESENTATION")
3749 .is_some()
3750 || inst
3751 .part("GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION")
3752 .is_some()
3753 })
3754 .map(|(id, _)| id)
3755 .collect();
3756 reps.sort_unstable();
3757 let mut out = Vec::new();
3758 for rep in reps {
3759 let items: Vec<u64> = {
3760 let Some(inst) = self.instances.get(rep) else {
3761 continue;
3762 };
3763 let args = inst
3764 .part("GEOMETRICALLY_BOUNDED_WIREFRAME_SHAPE_REPRESENTATION")
3765 .or_else(|| inst.part("GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION"))
3766 .unwrap_or(&[]);
3767 args.get(1)
3768 .and_then(Arg::list)
3769 .unwrap_or(&[])
3770 .iter()
3771 .filter_map(Arg::reference)
3772 .collect()
3773 };
3774 let mut read = Vec::new();
3775 for item in items {
3776 ogeom_core::progress::checkpoint()?;
3777 let Some(inst) = self.instances.get(item) else {
3778 continue;
3779 };
3780 let set = inst
3781 .part("GEOMETRIC_CURVE_SET")
3782 .or_else(|| inst.part("GEOMETRIC_SET"));
3783 if let Some(args) = set {
3784 let elements: Vec<u64> = args
3785 .get(1)
3786 .and_then(Arg::list)
3787 .unwrap_or(&[])
3788 .iter()
3789 .filter_map(Arg::reference)
3790 .collect();
3791 self.visited.mark(item);
3792 for element in elements {
3793 if let Some(found) = self.construction_item(element) {
3794 read.push(found);
3795 }
3796 }
3797 } else if inst.part("AXIS2_PLACEMENT_3D").is_some() {
3798 let name = item_name(inst);
3799 match self.frame(item) {
3800 Ok(frame) => read.push((name, ConstructionGeometry::Axis(frame))),
3801 Err(e) => self
3802 .report
3803 .warnings
3804 .push(format!("#{item}: a construction axis is not read: {e}")),
3805 }
3806 }
3807 }
3808 if !read.is_empty() {
3809 self.visited.mark(rep);
3810 out.push((rep, read));
3811 }
3812 }
3813 Ok(out)
3814 }
3815
3816 fn construction_item(&mut self, id: u64) -> Option<(String, ConstructionGeometry)> {
3822 let inst = self.instances.get(id)?;
3823 let name = item_name(inst);
3824 let keyword = inst.keyword().to_owned();
3825 let surface = inst.parts.iter().any(|(k, _)| {
3826 k == "PLANE"
3827 || k.ends_with("_SURFACE")
3828 || (k.starts_with("SURFACE_") && !k.ends_with("_CURVE"))
3829 });
3830 let bounded = |(lo, hi): (f64, f64)| {
3831 lo.is_finite() && hi.is_finite() && lo < hi && hi - lo <= SURFACE_EXTENT
3832 };
3833 let read: OgeomResult<Option<ConstructionGeometry>> =
3834 if inst.part("CARTESIAN_POINT").is_some() {
3835 self.point(id).map(|p| Some(ConstructionGeometry::Point(p)))
3836 } else if surface {
3837 self.construction_surface(id, &keyword).map(|found| {
3838 found
3839 .filter(|(_, u, v)| bounded(*u) && bounded(*v))
3840 .map(|(surface, u, v)| ConstructionGeometry::Surface { surface, u, v })
3841 })
3842 } else if keyword == "TRIMMED_CURVE" {
3843 self.construction_trim(id).map(|found| {
3844 found.map(|(curve, range)| ConstructionGeometry::Curve { curve, range })
3845 })
3846 } else {
3847 self.curve(id).map(|found| {
3848 found
3849 .map(|curve| (curve.domain(), curve))
3850 .filter(|(range, _)| bounded(*range))
3851 .map(|(range, curve)| ConstructionGeometry::Curve { curve, range })
3852 })
3853 };
3854 match read {
3855 Ok(Some(geometry)) => Some((name, geometry)),
3856 Ok(None) => {
3857 self.report.warnings.push(format!(
3858 "#{id}: a geometric set's {keyword} is not read: it is not bounded, \
3859 or not a kind read yet"
3860 ));
3861 None
3862 }
3863 Err(e) => {
3864 self.report.warnings.push(format!(
3865 "#{id}: a geometric set's {keyword} is not read: {e}"
3866 ));
3867 None
3868 }
3869 }
3870 }
3871
3872 fn construction_trim(&mut self, id: u64) -> OgeomResult<Option<(Curve, (f64, f64))>> {
3878 let args = self.args(id, "TRIMMED_CURVE")?;
3879 let basis_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
3880 let forward = !args.get(4).is_some_and(|a| a.is_enum("F"));
3881 let by_parameter = args.get(5).is_some_and(|a| a.is_enum("PARAMETER"));
3882 let parameter = |trim: Option<&Arg>| {
3883 trim.and_then(Arg::list)?
3884 .iter()
3885 .find_map(|item| match item {
3886 Arg::Typed(typed) if typed.0 == "PARAMETER_VALUE" => {
3887 typed.1.first().and_then(Arg::number)
3888 }
3889 _ => None,
3890 })
3891 };
3892 let (Some(v0), Some(v1)) = (parameter(args.get(2)), parameter(args.get(3))) else {
3893 return self.bounded_curve(id);
3894 };
3895 if !forward || !by_parameter {
3896 return self.bounded_curve(id);
3897 }
3898 let basis_keyword = self.instance(basis_id)?.keyword().to_owned();
3899 let scale = match basis_keyword.as_str() {
3900 "LINE" => {
3901 let line = self.args(basis_id, "LINE")?;
3902 let vector = self.args(ref_at(line, 2), "VECTOR")?;
3903 vector.get(2).and_then(Arg::number).unwrap_or(1.0) * self.report.scale_mm
3904 }
3905 "CIRCLE" | "ELLIPSE" => self.angle_scale,
3906 "B_SPLINE_CURVE_WITH_KNOTS" => 1.0,
3907 _ if self
3908 .instance(basis_id)?
3909 .part("B_SPLINE_CURVE_WITH_KNOTS")
3910 .is_some() =>
3911 {
3912 1.0
3913 }
3914 _ => return self.bounded_curve(id),
3915 };
3916 let Some(basis) = self.curve(basis_id)? else {
3917 return Ok(None);
3918 };
3919 let (a, mut b) = (v0 * scale, v1 * scale);
3920 if basis.is_periodic() && b <= a {
3921 let (lo, hi) = basis.domain();
3922 b += hi - lo;
3923 }
3924 if a >= b {
3925 return self.bounded_curve(id);
3926 }
3927 Ok(Some((basis, (a, b))))
3928 }
3929
3930 #[allow(clippy::type_complexity)]
3935 fn construction_surface(
3936 &mut self,
3937 id: u64,
3938 keyword: &str,
3939 ) -> OgeomResult<Option<(SurfaceGeometry, (f64, f64), (f64, f64))>> {
3940 if keyword != "RECTANGULAR_TRIMMED_SURFACE" {
3941 return Ok(self.surface(id)?.map(|surface| {
3942 let (u, v) = surface.domain();
3943 (surface, u, v)
3944 }));
3945 }
3946 let args = self.args(id, "RECTANGULAR_TRIMMED_SURFACE")?;
3947 let basis_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
3948 let Some(basis) = self.surface(basis_id)? else {
3949 return Ok(None);
3950 };
3951 let (length, angle) = (self.report.scale_mm, self.angle_scale);
3952 let (u_scale, v_scale) = match self.instance(basis_id)?.keyword() {
3956 "PLANE" => (length, length),
3957 "CYLINDRICAL_SURFACE" | "CONICAL_SURFACE" => (angle, length),
3958 "SPHERICAL_SURFACE" | "TOROIDAL_SURFACE" | "DEGENERATE_TOROIDAL_SURFACE" => {
3959 (angle, angle)
3960 }
3961 "SURFACE_OF_LINEAR_EXTRUSION" => (1.0, length),
3962 "SURFACE_OF_REVOLUTION" => (1.0, angle),
3963 _ => (1.0, 1.0),
3964 };
3965 let n = |i: usize| args.get(i).and_then(Arg::number).unwrap_or(0.0);
3966 let (u1, u2) = (n(2) * u_scale, n(3) * u_scale);
3967 let (v1, v2) = (n(4) * v_scale, n(5) * v_scale);
3968 Ok(Some((
3969 basis,
3970 (u1.min(u2), u1.max(u2)),
3971 (v1.min(v2), v1.max(v2)),
3972 )))
3973 }
3974
3975 fn add_construction(
3978 &mut self,
3979 document: &mut ogeom_doc::Document,
3980 product: ogeom_doc::ProductId,
3981 items: Vec<(String, ConstructionGeometry)>,
3982 ) {
3983 for (name, geometry) in items {
3984 if let Err(e) = document.add_construction(product, &name, geometry) {
3985 self.report
3986 .warnings
3987 .push(format!("construction item '{name}' is not kept: {e}"));
3988 }
3989 }
3990 }
3991
3992 fn document(
4000 &mut self,
4001 by_item: &FastMap<u64, Shape>,
4002 solids: &[Shape],
4003 shells: &[Shape],
4004 construction: ConstructionSets,
4005 ) -> OgeomResult<ogeom_doc::Document> {
4006 let mut sets: FastMap<u64, Vec<(String, ConstructionGeometry)>> = FastMap::default();
4009 let order: Vec<u64> = construction.iter().map(|(rep, _)| *rep).collect();
4010 sets.extend(construction);
4011 let structure = self.product_structure(by_item, &mut sets);
4012 let colours = self.colours(by_item);
4013 let pmi = self.pmi_of();
4014
4015 let mut document = ogeom_doc::Document::over(std::mem::take(&mut self.model));
4016 match structure {
4017 Some(products) => self.build_products(&mut document, products),
4018 None => {
4019 for (i, solid) in solids.iter().enumerate() {
4020 document.add_part(format!("solid-{i}"), solid.clone());
4021 }
4022 for (i, shell) in shells.iter().enumerate() {
4023 document.add_part(format!("shell-{i}"), shell.clone());
4024 }
4025 }
4026 }
4027 for (i, rep) in order.into_iter().enumerate() {
4030 let Some(items) = sets.remove(&rep) else {
4031 continue;
4032 };
4033 let name = self
4034 .instances
4035 .get(rep)
4036 .and_then(|inst| inst.parts.first())
4037 .and_then(|(_, args)| match args.first() {
4038 Some(Arg::Str(name)) if !name.is_empty() => Some(name.clone()),
4039 _ => None,
4040 })
4041 .unwrap_or_else(|| format!("construction-{i}"));
4042 let empty = ogeom_algo::build::make_compound(document.model_mut(), &[])?.shape;
4043 let part = document.add_part(name, empty);
4044 self.add_construction(&mut document, part, items);
4045 }
4046 for (shape, colour) in colours {
4047 document.set_colour(&shape, colour);
4048 }
4049 *document.pmi_mut() = pmi;
4050 for view in self.views() {
4051 document.add_view(view);
4052 }
4053 Ok(document)
4054 }
4055
4056 fn product_structure(
4058 &mut self,
4059 by_item: &FastMap<u64, Shape>,
4060 sets: &mut FastMap<u64, Vec<(String, ConstructionGeometry)>>,
4061 ) -> Option<Vec<PdEntry>> {
4062 let mut pds: Vec<u64> = self
4064 .instances
4065 .iter()
4066 .filter(|(_, inst)| inst.part("PRODUCT_DEFINITION").is_some())
4067 .filter(|(_, inst)| inst.part("PRODUCT_DEFINITION_RELATIONSHIP").is_none())
4068 .map(|(id, _)| id)
4069 .collect();
4070 pds.sort_unstable();
4071 if pds.is_empty() {
4072 return None;
4073 }
4074
4075 let mut sr_of_pd: FastMap<u64, u64> = FastMap::default();
4078 let sdrs: Vec<(u64, u64)> = self
4079 .instances
4080 .iter()
4081 .filter(|(_, inst)| inst.part("SHAPE_DEFINITION_REPRESENTATION").is_some())
4082 .filter_map(|(id, _)| {
4083 let args = self.args(id, "SHAPE_DEFINITION_REPRESENTATION").ok()?;
4084 Some((args.first()?.reference()?, args.get(1)?.reference()?))
4085 })
4086 .collect();
4087 for (pds_id, sr) in sdrs {
4088 if let Some(definition) = self.definition_of_shape(pds_id) {
4089 sr_of_pd.insert(definition, sr);
4090 }
4091 }
4092
4093 let mut linked: FastMap<u64, Vec<u64>> = FastMap::default();
4100 let mut srrs: Vec<u64> = self
4101 .instances
4102 .iter()
4103 .filter(|(_, inst)| {
4104 inst.part("SHAPE_REPRESENTATION_RELATIONSHIP").is_some()
4105 && inst
4106 .part("REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION")
4107 .is_none()
4108 })
4109 .map(|(id, _)| id)
4110 .collect();
4111 srrs.sort_unstable();
4112 for srr in srrs {
4113 let args = {
4114 let Ok(instance) = self.instance(srr) else {
4115 continue;
4116 };
4117 let Some(args) = instance
4118 .part("SHAPE_REPRESENTATION_RELATIONSHIP")
4119 .or_else(|| instance.part("REPRESENTATION_RELATIONSHIP"))
4120 else {
4121 continue;
4122 };
4123 args.to_vec()
4124 };
4125 if let (Some(a), Some(b)) = (
4126 args.get(2).and_then(Arg::reference),
4127 args.get(3).and_then(Arg::reference),
4128 ) {
4129 linked.entry(a).or_default().push(b);
4130 linked.entry(b).or_default().push(a);
4131 }
4132 }
4133
4134 let mut entries = Vec::new();
4135 for pd in pds {
4136 let name = self.product_name(pd).unwrap_or_else(|| format!("#{pd}"));
4137 let mut shapes: Vec<Shape> = Vec::new();
4138 let mut construction = Vec::new();
4139 if let Some(&sr) = sr_of_pd.get(&pd) {
4140 let mut reps = vec![sr];
4141 reps.extend(linked.get(&sr).into_iter().flatten().copied());
4142 for rep in reps {
4143 if let Some(items) = self.representation_items(rep) {
4144 shapes.extend(items.iter().filter_map(|item| by_item.get(item).cloned()));
4145 }
4146 construction.extend(sets.remove(&rep).into_iter().flatten());
4147 }
4148 }
4149 entries.push(PdEntry {
4150 pd,
4151 name,
4152 shapes,
4153 construction,
4154 children: Vec::new(),
4155 });
4156 }
4157
4158 let mut nauos: Vec<u64> = self
4161 .instances
4162 .iter()
4163 .filter(|(_, inst)| inst.part("NEXT_ASSEMBLY_USAGE_OCCURRENCE").is_some())
4164 .map(|(id, _)| id)
4165 .collect();
4166 nauos.sort_unstable();
4167 let entry_of_pd: FastMap<u64, usize> =
4168 entries.iter().enumerate().map(|(i, e)| (e.pd, i)).collect();
4169 for nauo in nauos {
4170 let Ok(args) = self.args(nauo, "NEXT_ASSEMBLY_USAGE_OCCURRENCE") else {
4171 continue;
4172 };
4173 let (Some(parent), Some(child)) = (
4174 args.get(3).and_then(Arg::reference),
4175 args.get(4).and_then(Arg::reference),
4176 ) else {
4177 continue;
4178 };
4179 let name = args
4180 .get(5)
4181 .and_then(|a| match a {
4182 Arg::Str(s) if !s.is_empty() => Some(s.clone()),
4183 _ => None,
4184 })
4185 .or_else(|| {
4186 self.args(nauo, "NEXT_ASSEMBLY_USAGE_OCCURRENCE")
4187 .ok()
4188 .and_then(|args| match args.get(1) {
4189 Some(Arg::Str(s)) if !s.is_empty() => Some(s.clone()),
4190 _ => None,
4191 })
4192 });
4193 let child_sr = sr_of_pd.get(&child).copied();
4194 let at = self
4195 .usage_transform(nauo, child_sr)
4196 .unwrap_or(Transform::IDENTITY);
4197 if let Some(&at_index) = entry_of_pd.get(&parent) {
4198 entries[at_index].children.push((child, at, name));
4199 }
4200 }
4201 Some(entries)
4202 }
4203
4204 fn definition_of_shape(&mut self, pds_id: u64) -> Option<u64> {
4206 let args = self.args(pds_id, "PRODUCT_DEFINITION_SHAPE").ok()?;
4207 args.get(2).and_then(Arg::reference)
4208 }
4209
4210 fn product_name(&mut self, pd: u64) -> Option<String> {
4212 let formation = self
4213 .args(pd, "PRODUCT_DEFINITION")
4214 .ok()?
4215 .get(2)
4216 .and_then(Arg::reference)?;
4217 let product = [
4222 "PRODUCT_DEFINITION_FORMATION",
4223 "PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE",
4224 ]
4225 .into_iter()
4226 .find_map(|keyword| {
4227 self.instance(formation)
4228 .ok()?
4229 .part(keyword)
4230 .or_else(|| self.args(formation, keyword).ok())?
4231 .get(2)
4232 .and_then(Arg::reference)
4233 })?;
4234 let args = self.args(product, "PRODUCT").ok()?;
4237 [args.get(1), args.first()]
4238 .into_iter()
4239 .find_map(|arg| match arg {
4240 Some(Arg::Str(name)) if !name.is_empty() => Some(name.clone()),
4241 _ => None,
4242 })
4243 }
4244
4245 fn representation_items(&mut self, sr: u64) -> Option<Vec<u64>> {
4247 let instance = self.instance(sr).ok()?;
4248 let args = [
4252 "SHAPE_REPRESENTATION",
4253 "ADVANCED_BREP_SHAPE_REPRESENTATION",
4254 "MANIFOLD_SURFACE_SHAPE_REPRESENTATION",
4255 "FACETED_BREP_SHAPE_REPRESENTATION",
4256 "GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION",
4257 "GEOMETRICALLY_BOUNDED_WIREFRAME_SHAPE_REPRESENTATION",
4258 "REPRESENTATION",
4259 ]
4260 .into_iter()
4261 .find_map(|keyword| instance.part(keyword))?
4262 .to_vec();
4263 Some(
4264 args.get(1)?
4265 .list()?
4266 .iter()
4267 .filter_map(Arg::reference)
4268 .collect(),
4269 )
4270 }
4271
4272 fn usage_transform(&mut self, nauo: u64, child_sr: Option<u64>) -> Option<Transform> {
4280 if self.cdsr_of_nauo.is_none() {
4281 let mut cdsrs: Vec<u64> = self
4285 .instances
4286 .iter()
4287 .filter(|(_, inst)| {
4288 inst.part("CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")
4289 .is_some()
4290 })
4291 .map(|(id, _)| id)
4292 .collect();
4293 cdsrs.sort_unstable();
4294 let mut index: FastMap<u64, u64> = FastMap::default();
4295 for id in cdsrs {
4296 let Some(owner) = self
4297 .args(id, "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")
4298 .ok()
4299 .and_then(|args| args.get(1).and_then(Arg::reference))
4300 .and_then(|pds| self.definition_of_shape(pds))
4301 else {
4302 continue;
4303 };
4304 index.entry(owner).or_insert(id);
4305 }
4306 self.cdsr_of_nauo = Some(index);
4307 }
4308 let cdsr = *self.cdsr_of_nauo.as_ref()?.get(&nauo)?;
4309 let rr = self
4310 .args(cdsr, "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")
4311 .ok()?
4312 .first()
4313 .and_then(Arg::reference)?;
4314 let (rep_1, rep_2) = {
4315 let args = self.args(rr, "REPRESENTATION_RELATIONSHIP").ok()?;
4316 (
4317 args.get(2).and_then(Arg::reference)?,
4318 args.get(3).and_then(Arg::reference)?,
4319 )
4320 };
4321 let idt = self
4322 .args(rr, "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION")
4323 .ok()?
4324 .first()
4325 .and_then(Arg::reference)?;
4326 let (item_1, item_2) = {
4327 let args = self.args(idt, "ITEM_DEFINED_TRANSFORMATION").ok()?;
4328 (
4329 args.get(2).and_then(Arg::reference)?,
4330 args.get(3).and_then(Arg::reference)?,
4331 )
4332 };
4333 let frame_1 = self.frame(item_1).ok()?;
4334 let frame_2 = self.frame(item_2).ok()?;
4335 let child_first = match child_sr {
4338 Some(sr) => rep_1 == sr || rep_2 != sr,
4339 None => true,
4340 };
4341 Some(if child_first {
4342 Transform::from_frame(&frame_2) * Transform::to_frame(&frame_1)
4343 } else {
4344 Transform::from_frame(&frame_1) * Transform::to_frame(&frame_2)
4345 })
4346 }
4347
4348 fn build_products(&mut self, document: &mut ogeom_doc::Document, mut entries: Vec<PdEntry>) {
4351 let mut ids: FastMap<u64, ogeom_doc::ProductId> = FastMap::default();
4352 for entry in &mut entries {
4353 let shape = match entry.shapes.len() {
4354 0 if !entry.construction.is_empty() && entry.children.is_empty() => {
4357 match ogeom_algo::build::make_compound(document.model_mut(), &[]) {
4358 Ok(built) => Some(built.shape),
4359 Err(_) => None,
4360 }
4361 }
4362 0 => None,
4363 1 => Some(entry.shapes[0].clone()),
4364 _ => match ogeom_algo::build::make_compound(document.model_mut(), &entry.shapes) {
4365 Ok(built) => Some(built.shape),
4366 Err(_) => Some(entry.shapes[0].clone()),
4367 },
4368 };
4369 if entry.children.is_empty() {
4370 if let Some(shape) = shape {
4371 ids.insert(entry.pd, document.add_part(&entry.name, shape));
4372 }
4373 } else {
4376 let assembly = document.add_assembly(&entry.name);
4377 ids.insert(entry.pd, assembly);
4378 if let Some(shape) = shape {
4381 let body = document.add_part(format!("{}-body", entry.name), shape);
4382 let _ = document.add_instance(assembly, body, Transform::IDENTITY, None);
4383 }
4384 }
4385 if let Some(&id) = ids.get(&entry.pd) {
4386 let items = std::mem::take(&mut entry.construction);
4387 self.add_construction(document, id, items);
4388 }
4389 }
4390 for entry in &entries {
4391 let Some(&parent) = ids.get(&entry.pd) else {
4392 continue;
4393 };
4394 for (child, at, name) in &entry.children {
4395 let Some(&child_id) = ids.get(child) else {
4396 self.report.warnings.push(format!(
4397 "#{child}: an assembly child holds nothing readable"
4398 ));
4399 continue;
4400 };
4401 if let Err(e) = document.add_instance(parent, child_id, *at, name.clone()) {
4402 self.report
4403 .warnings
4404 .push(format!("assembly edge #{} -> #{child}: {e}", entry.pd));
4405 }
4406 }
4407 }
4408 }
4409
4410 fn colours(&mut self, by_item: &FastMap<u64, Shape>) -> Vec<(Shape, ogeom_doc::Colour)> {
4412 let mut styled: Vec<u64> = self
4413 .instances
4414 .iter()
4415 .filter(|(_, inst)| {
4416 inst.part("STYLED_ITEM").is_some() || inst.part("OVER_RIDING_STYLED_ITEM").is_some()
4417 })
4418 .map(|(id, _)| id)
4419 .collect();
4420 styled.sort_unstable();
4424 let mut out = Vec::new();
4425 for id in styled {
4426 let args = {
4427 let Ok(instance) = self.instance(id) else {
4428 continue;
4429 };
4430 let Some(args) = instance
4431 .part("STYLED_ITEM")
4432 .or_else(|| instance.part("OVER_RIDING_STYLED_ITEM"))
4433 else {
4434 continue;
4435 };
4436 args.to_vec()
4437 };
4438 let Some(item) = args.get(2).and_then(Arg::reference) else {
4439 continue;
4440 };
4441 let shapes = match by_item.get(&item).or_else(|| self.faces.get(&item)) {
4442 Some(shape) => vec![shape.clone()],
4443 None => self.edge_shapes(item),
4444 };
4445 if shapes.is_empty() {
4446 continue;
4447 }
4448 let styles: Vec<u64> = args
4449 .get(1)
4450 .and_then(Arg::list)
4451 .map(|list| list.iter().filter_map(Arg::reference).collect())
4452 .unwrap_or_default();
4453 if let Some(colour) = styles.iter().find_map(|&style| self.colour_in(style, 0)) {
4454 out.extend(shapes.into_iter().map(|shape| (shape, colour)));
4455 }
4456 }
4457 out
4458 }
4459
4460 fn colour_in(&mut self, id: u64, depth: usize) -> Option<ogeom_doc::Colour> {
4465 if depth > 6 {
4466 return None;
4467 }
4468 let (keyword, args) = {
4469 let instance = self.instance(id).ok()?;
4470 let all: Vec<Arg> = instance
4471 .parts()
4472 .flat_map(|(_, args)| args.iter().cloned())
4473 .collect();
4474 (instance.keyword().to_owned(), all)
4475 };
4476 if keyword == "COLOUR_RGB" {
4477 let channel = |i: usize| args.get(i).and_then(Arg::number);
4478 return Some(ogeom_doc::Colour::rgb(
4479 channel(1)?,
4480 channel(2)?,
4481 channel(3)?,
4482 ));
4483 }
4484 if keyword == "DRAUGHTING_PRE_DEFINED_COLOUR" {
4489 let name = match args.first() {
4490 Some(Arg::Str(name)) => name.as_str(),
4491 _ => "",
4492 };
4493 let channels = match name.trim().to_ascii_lowercase().as_str() {
4494 "black" => Some((0.0, 0.0, 0.0)),
4495 "red" => Some((1.0, 0.0, 0.0)),
4496 "green" => Some((0.0, 1.0, 0.0)),
4497 "blue" => Some((0.0, 0.0, 1.0)),
4498 "yellow" => Some((1.0, 1.0, 0.0)),
4499 "magenta" => Some((1.0, 0.0, 1.0)),
4500 "cyan" => Some((0.0, 1.0, 1.0)),
4501 "white" => Some((1.0, 1.0, 1.0)),
4502 _ => None,
4503 };
4504 if channels.is_none() && self.unnamed_colours.insert(id) {
4505 self.report.warnings.push(format!(
4506 "#{id}: DRAUGHTING_PRE_DEFINED_COLOUR '{name}' names none of the eight \
4507 predefined colours; the item is left uncoloured"
4508 ));
4509 }
4510 return channels.map(|(r, g, b)| ogeom_doc::Colour::rgb(r, g, b));
4511 }
4512 let mut refs: Vec<u64> = Vec::new();
4513 collect_refs(&args, &mut refs);
4514 refs.into_iter()
4515 .find_map(|next| self.colour_in(next, depth + 1))
4516 }
4517
4518 fn pmi_of(&mut self) -> ogeom_doc::Pmi {
4525 let mut pmi = ogeom_doc::Pmi::new();
4526 let mut annotation_ids: FastMap<u64, ogeom_doc::Annotated> = FastMap::default();
4530
4531 let mut aspect_items: FastMap<u64, Vec<ogeom_topo::TShapeId>> = FastMap::default();
4535 let mut gisus = self.ids_with("GEOMETRIC_ITEM_SPECIFIC_USAGE");
4536 gisus.sort_unstable();
4537 for id in gisus {
4538 let Ok(args) = self.args(id, "GEOMETRIC_ITEM_SPECIFIC_USAGE") else {
4539 continue;
4540 };
4541 let (Some(aspect), Some(item)) = (
4542 args.get(2).and_then(Arg::reference),
4543 args.get(4).and_then(Arg::reference),
4544 ) else {
4545 continue;
4546 };
4547 let nodes = match self.faces.get(&item) {
4548 Some(face) => vec![face.node()],
4549 None => self.edge_shapes(item).iter().map(Shape::node).collect(),
4550 };
4551 if !nodes.is_empty() {
4552 aspect_items.entry(aspect).or_default().extend(nodes);
4553 }
4554 }
4555 let mut adjacency: FastMap<u64, Vec<u64>> = FastMap::default();
4556 for id in self.ids_with("SHAPE_ASPECT_RELATIONSHIP") {
4557 let Ok(args) = self.args(id, "SHAPE_ASPECT_RELATIONSHIP") else {
4558 continue;
4559 };
4560 if let (Some(a), Some(b)) = (
4561 args.get(2).and_then(Arg::reference),
4562 args.get(3).and_then(Arg::reference),
4563 ) {
4564 adjacency.entry(a).or_default().push(b);
4565 adjacency.entry(b).or_default().push(a);
4566 }
4567 }
4568 let items_for = |aspect: u64| -> Vec<ogeom_topo::TShapeId> {
4569 let mut reach = vec![aspect];
4573 for _ in 0..3 {
4574 let mut next = reach.clone();
4575 for a in &reach {
4576 next.extend(adjacency.get(a).into_iter().flatten().copied());
4577 }
4578 next.sort_unstable();
4579 next.dedup();
4580 reach = next;
4581 }
4582 let mut out: Vec<ogeom_topo::TShapeId> = Vec::new();
4583 for a in reach {
4584 out.extend(aspect_items.get(&a).into_iter().flatten().copied());
4585 }
4586 out.sort_unstable();
4587 out.dedup();
4588 out
4589 };
4590
4591 let mut plus_minus: FastMap<u64, (Option<f64>, Option<f64>)> = FastMap::default();
4595 for id in self.ids_with("PLUS_MINUS_TOLERANCE") {
4596 let Ok(args) = self.args(id, "PLUS_MINUS_TOLERANCE") else {
4597 continue;
4598 };
4599 let (Some(tv), Some(dim)) = (
4600 args.first().and_then(Arg::reference),
4601 args.get(1).and_then(Arg::reference),
4602 ) else {
4603 continue;
4604 };
4605 let Ok(tv_args) = self.args(tv, "TOLERANCE_VALUE") else {
4606 continue;
4607 };
4608 let lower = tv_args
4609 .first()
4610 .and_then(Arg::reference)
4611 .and_then(|r| self.measure_value(r))
4612 .map(|(v, _)| v);
4613 let upper = tv_args
4614 .get(1)
4615 .and_then(Arg::reference)
4616 .and_then(|r| self.measure_value(r))
4617 .map(|(v, _)| v);
4618 plus_minus.insert(dim, (lower, upper));
4619 }
4620 let mut dcrs = self.ids_with("DIMENSIONAL_CHARACTERISTIC_REPRESENTATION");
4621 dcrs.sort_unstable();
4622 for id in dcrs {
4623 let Ok(args) = self.args(id, "DIMENSIONAL_CHARACTERISTIC_REPRESENTATION") else {
4624 continue;
4625 };
4626 let (Some(dim), Some(sdr)) = (
4627 args.first().and_then(Arg::reference),
4628 args.get(1).and_then(Arg::reference),
4629 ) else {
4630 continue;
4631 };
4632 let mut values = Vec::new();
4633 let mut kind = ogeom_doc::MeasureKind::Length;
4634 if let Ok(sdr_args) = self.args(sdr, "SHAPE_DIMENSION_REPRESENTATION") {
4635 for item in sdr_args.get(1).and_then(Arg::list).unwrap_or(&[]) {
4636 if let Some(r) = item.reference()
4637 && let Some((v, k)) = self.measure_value(r)
4638 {
4639 values.push(v);
4640 kind = k;
4641 }
4642 }
4643 }
4644 let (name, location, aspects) = self.dimension_shape(dim);
4645 let features: Vec<Vec<ogeom_topo::TShapeId>> =
4646 aspects.into_iter().map(&items_for).collect();
4647 let (minus, plus) = plus_minus.get(&dim).copied().unwrap_or((None, None));
4648 annotation_ids.insert(dim, ogeom_doc::Annotated::Dimension(pmi.dimensions.len()));
4649 pmi.dimensions.push(ogeom_doc::Dimension {
4650 name,
4651 values,
4652 kind,
4653 plus,
4654 minus,
4655 features,
4656 location,
4657 });
4658 }
4659
4660 let is_subtype = |k: &str| {
4664 k.ends_with("_TOLERANCE")
4665 && !matches!(
4666 k,
4667 "GEOMETRIC_TOLERANCE"
4668 | "GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE"
4669 | "GEOMETRIC_TOLERANCE_WITH_DEFINED_UNIT"
4670 | "GEOMETRIC_TOLERANCE_WITH_MODIFIERS"
4671 | "GEOMETRIC_TOLERANCE_WITH_MAXIMUM_TOLERANCE"
4672 | "PLUS_MINUS_TOLERANCE"
4673 | "TOLERANCE_VALUE"
4674 )
4675 };
4676 let mut gts: Vec<u64> = self
4677 .instances
4678 .iter()
4679 .filter(|(_, inst)| inst.parts().any(|(k, _)| is_subtype(k)))
4680 .map(|(id, _)| id)
4681 .collect();
4682 gts.sort_unstable();
4683 for id in gts {
4684 let (name, magnitude_ref, aspect, kind, modifiers, datum_refs) = {
4685 let Ok(instance) = self.instance(id) else {
4686 continue;
4687 };
4688 let subtype = instance
4689 .parts()
4690 .map(|(k, _)| k.to_owned())
4691 .find(|k| is_subtype(k));
4692 let Some(subtype) = subtype else {
4693 continue;
4694 };
4695 let base = instance
4696 .part("GEOMETRIC_TOLERANCE")
4697 .or_else(|| instance.part(&subtype))
4698 .unwrap_or(&[])
4699 .to_vec();
4700 let name = match base.first() {
4701 Some(Arg::Str(s)) => s.clone(),
4702 _ => String::new(),
4703 };
4704 let magnitude_ref = base.get(2).and_then(Arg::reference);
4705 let aspect = base.get(3).and_then(Arg::reference);
4706 let kind = Some(subtype.trim_end_matches("_TOLERANCE").to_lowercase());
4707 let modifiers: Vec<String> = instance
4710 .part("GEOMETRIC_TOLERANCE_WITH_MODIFIERS")
4711 .and_then(|args| args.first())
4712 .and_then(Arg::list)
4713 .map(|list| {
4714 list.iter()
4715 .filter_map(|arg| match arg {
4716 Arg::Enum(word) => Some(word.to_lowercase()),
4717 _ => None,
4718 })
4719 .collect()
4720 })
4721 .unwrap_or_default();
4722 let datum_refs: Vec<u64> = instance
4725 .part("GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE")
4726 .and_then(|args| args.first())
4727 .and_then(Arg::list)
4728 .or_else(|| base.get(4).and_then(Arg::list))
4729 .map(|list| list.iter().filter_map(Arg::reference).collect())
4730 .unwrap_or_default();
4731 (name, magnitude_ref, aspect, kind, modifiers, datum_refs)
4732 };
4733 let Some(kind) = kind else {
4734 continue;
4735 };
4736 let magnitude = magnitude_ref
4737 .and_then(|r| self.measure_value(r))
4738 .map_or(0.0, |(v, _)| v);
4739 let mut datums: Vec<ogeom_doc::DatumReference> = Vec::new();
4743 let mut datum_system = false;
4744 for &r in &datum_refs {
4745 if let Some(frame) = self.datum_system(r) {
4746 datums.extend(frame);
4747 datum_system = true;
4748 } else if let Some(letter) = self.datum_letter(r, 0) {
4749 datums.push(letter.into());
4750 }
4751 }
4752 let items = aspect.map(items_for).unwrap_or_default();
4753 annotation_ids.insert(id, ogeom_doc::Annotated::Tolerance(pmi.tolerances.len()));
4754 pmi.tolerances.push(ogeom_doc::GeometricTolerance {
4755 kind,
4756 name,
4757 magnitude,
4758 modifiers,
4759 datums,
4760 datum_system,
4761 items,
4762 });
4763 }
4764
4765 let mut datums = self.ids_with("DATUM");
4768 datums.sort_unstable();
4769 for id in datums {
4770 let letter = {
4771 let Ok(instance) = self.instance(id) else {
4772 continue;
4773 };
4774 if instance.part("DATUM_FEATURE").is_some()
4775 || instance.part("DATUM_REFERENCE").is_some()
4776 || instance.part("DATUM_REFERENCE_COMPARTMENT").is_some()
4777 || instance.part("DATUM_SYSTEM").is_some()
4778 {
4779 continue;
4780 }
4781 let Some(args) = instance.part("DATUM") else {
4782 continue;
4783 };
4784 match args.get(4) {
4785 Some(Arg::Str(s)) if !s.is_empty() => s.clone(),
4786 _ => continue,
4787 }
4788 };
4789 annotation_ids.insert(id, ogeom_doc::Annotated::Datum(pmi.datums.len()));
4790 pmi.datums.push(ogeom_doc::Datum {
4791 label: letter,
4792 items: items_for(id),
4793 });
4794 }
4795
4796 let mut targets = self.ids_with("PLACED_DATUM_TARGET_FEATURE");
4801 targets.sort_unstable();
4802 for id in targets {
4803 if let Some(target) = self.datum_target(id, &items_for) {
4804 pmi.targets.push(target);
4805 }
4806 }
4807
4808 let (callouts, callout_index) = self.callouts(&annotation_ids, &aspect_items);
4814 pmi.callouts = callouts;
4815 self.callout_index = callout_index;
4816 pmi
4817 }
4818
4819 fn views(&mut self) -> Vec<ogeom_doc::View> {
4823 let mut out = Vec::new();
4824 for id in self.ids_with("DRAUGHTING_MODEL") {
4825 let Ok(args) = self.args(id, "DRAUGHTING_MODEL") else {
4826 continue;
4827 };
4828 let name = match args.first() {
4829 Some(Arg::Str(text)) => text.clone(),
4830 _ => String::new(),
4831 };
4832 if name.is_empty() {
4833 continue;
4834 }
4835 let mut frame = None;
4836 let mut callouts = Vec::new();
4837 for item in args.get(1).and_then(Arg::list).unwrap_or(&[]) {
4838 let Some(item) = item.reference() else {
4839 continue;
4840 };
4841 if let Ok(cam) = self.args(item, "CAMERA_MODEL_D3") {
4842 if let Some(placement) = cam.get(1).and_then(Arg::reference) {
4843 frame = self.frame(placement).ok();
4844 }
4845 continue;
4846 }
4847 if let Some(&index) = self.callout_index.get(&item) {
4848 callouts.push(index);
4849 }
4850 }
4851 let Some(frame) = frame else { continue };
4852 out.push(ogeom_doc::View {
4853 name,
4854 frame,
4855 clipping: None,
4856 callouts,
4857 });
4858 }
4859 out
4860 }
4861
4862 fn datum_target(
4864 &mut self,
4865 id: u64,
4866 items_for: &dyn Fn(u64) -> Vec<ogeom_topo::TShapeId>,
4867 ) -> Option<ogeom_doc::DatumTarget> {
4868 let (target_id, description) = {
4869 let instance = self.instance(id).ok()?;
4870 let args = instance.part("PLACED_DATUM_TARGET_FEATURE")?;
4871 let target = match args.get(4) {
4872 Some(Arg::Str(s)) if !s.is_empty() => s.clone(),
4873 _ => return None,
4874 };
4875 let description = match args.get(1) {
4876 Some(Arg::Str(s)) => s.to_ascii_lowercase(),
4877 _ => String::new(),
4878 };
4879 (target, description)
4880 };
4881 let split = target_id
4883 .find(|c: char| c.is_ascii_digit())
4884 .unwrap_or(target_id.len());
4885 let (letter, number) = target_id.split_at(split);
4886 let index = number.parse::<u32>().unwrap_or(1);
4887 let datum = if letter.is_empty() {
4888 self.datum_letter(id, 0).unwrap_or_default()
4889 } else {
4890 letter.to_owned()
4891 };
4892
4893 self.ensure_property_indexes();
4899 let mut frame = None;
4900 let mut lengths: Vec<f64> = Vec::new();
4901 let properties: Vec<u64> = self
4902 .properties_of_definition
4903 .as_ref()
4904 .and_then(|m| m.get(&id).cloned())
4905 .unwrap_or_default();
4906 for property in properties {
4907 let sdrs: Vec<u64> = self
4908 .sdrs_of_property
4909 .as_ref()
4910 .and_then(|m| m.get(&property).cloned())
4911 .unwrap_or_default();
4912 for sdr in sdrs {
4913 let Ok(args) = self.args(sdr, "SHAPE_DEFINITION_REPRESENTATION") else {
4914 continue;
4915 };
4916 let Some(rep) = args.get(1).and_then(Arg::reference) else {
4917 continue;
4918 };
4919 for item in self.representation_items(rep).unwrap_or_default() {
4920 let keyword = self
4921 .instance(item)
4922 .map(|i| i.keyword().to_owned())
4923 .unwrap_or_default();
4924 match keyword.as_str() {
4925 "AXIS2_PLACEMENT_3D" => frame = self.frame(item).ok(),
4926 "LENGTH_MEASURE_WITH_UNIT" | "MEASURE_REPRESENTATION_ITEM" => {
4927 if let Some((value, _)) = self.measure_value(item) {
4928 lengths.push(value);
4929 }
4930 }
4931 _ => {}
4932 }
4933 }
4934 }
4935 }
4936
4937 let kind = if description.contains("circle") || description.contains("circular") {
4941 ogeom_doc::DatumTargetKind::Circle {
4942 diameter: lengths.first().copied().unwrap_or(0.0),
4943 }
4944 } else if description.contains("rectangle") || lengths.len() >= 2 {
4945 ogeom_doc::DatumTargetKind::Rectangle {
4946 length: lengths.first().copied().unwrap_or(0.0),
4947 width: lengths.get(1).copied().unwrap_or(0.0),
4948 }
4949 } else if description.contains("line") || lengths.len() == 1 {
4950 ogeom_doc::DatumTargetKind::Line {
4951 length: lengths.first().copied().unwrap_or(0.0),
4952 }
4953 } else {
4954 ogeom_doc::DatumTargetKind::Point
4955 };
4956 Some(ogeom_doc::DatumTarget {
4957 datum,
4958 index,
4959 kind,
4960 at: frame.map_or(Point::ORIGIN, |f: Frame| f.origin()),
4961 frame,
4962 items: items_for(id),
4963 })
4964 }
4965
4966 fn callouts(
4969 &mut self,
4970 annotation_ids: &FastMap<u64, ogeom_doc::Annotated>,
4971 aspect_items: &FastMap<u64, Vec<ogeom_topo::TShapeId>>,
4972 ) -> (Vec<ogeom_doc::Callout>, FastMap<u64, usize>) {
4973 let mut plane_of: FastMap<u64, Frame> = FastMap::default();
4975 for id in self.ids_with("ANNOTATION_PLANE") {
4976 let Ok(args) = self.args(id, "ANNOTATION_PLANE") else {
4977 continue;
4978 };
4979 let frame = args
4980 .get(2)
4981 .and_then(Arg::reference)
4982 .and_then(|plane| {
4983 let inner = self.args(plane, "PLANE").ok()?;
4984 inner.get(1).and_then(Arg::reference)
4985 })
4986 .and_then(|placement| self.frame(placement).ok());
4987 let Some(frame) = frame else { continue };
4988 for element in args.get(3).and_then(Arg::list).unwrap_or(&[]) {
4989 if let Some(callout) = element.reference() {
4990 plane_of.insert(callout, frame);
4991 }
4992 }
4993 }
4994
4995 let mut draws: FastMap<u64, Vec<u64>> = FastMap::default();
5003 let mut associations = self.ids_with("DRAUGHTING_MODEL_ITEM_ASSOCIATION");
5004 associations.sort_unstable();
5005 for id in associations {
5006 let Ok(args) = self.args(id, "DRAUGHTING_MODEL_ITEM_ASSOCIATION") else {
5007 continue;
5008 };
5009 if let (Some(definition), Some(item)) = (
5010 args.get(2).and_then(Arg::reference),
5011 args.get(4).and_then(Arg::reference),
5012 ) {
5013 draws.entry(item).or_default().push(definition);
5014 }
5015 }
5016
5017 let mut out = Vec::new();
5018 let mut index_of: FastMap<u64, usize> = FastMap::default();
5019 let mut callouts = self.ids_with("DRAUGHTING_CALLOUT");
5020 callouts.sort_unstable();
5021 for id in callouts {
5022 let Ok(args) = self.args(id, "DRAUGHTING_CALLOUT") else {
5023 continue;
5024 };
5025 let name = match args.first() {
5026 Some(Arg::Str(s)) => s.clone(),
5027 _ => String::new(),
5028 };
5029 let mut polylines = Vec::new();
5030 let mut text: Option<String> = None;
5031 for element in args.get(1).and_then(Arg::list).unwrap_or(&[]).to_vec() {
5032 let Some(occurrence) = element.reference() else {
5033 continue;
5034 };
5035 if let Some(words) = self.annotation_text(occurrence) {
5036 text.get_or_insert(words);
5037 continue;
5038 }
5039 polylines.extend(self.annotation_polylines(occurrence));
5040 }
5041 if polylines.is_empty() && text.is_none() && !plane_of.contains_key(&id) {
5042 continue;
5043 }
5044 index_of.insert(id, out.len());
5045 let definitions = draws.get(&id).cloned().unwrap_or_default();
5046 let annotates = definitions
5047 .iter()
5048 .find_map(|d| annotation_ids.get(d).copied());
5049 let mut items: Vec<ogeom_topo::TShapeId> = Vec::new();
5052 if annotates.is_none() {
5053 for d in &definitions {
5054 for &node in aspect_items.get(d).into_iter().flatten() {
5055 if !items.contains(&node) {
5056 items.push(node);
5057 }
5058 }
5059 }
5060 }
5061 out.push(ogeom_doc::Callout {
5062 name,
5063 plane: plane_of.get(&id).copied(),
5064 polylines,
5065 annotates,
5066 items,
5067 text,
5068 });
5069 }
5070 (out, index_of)
5071 }
5072
5073 fn annotation_text(&mut self, occurrence: u64) -> Option<String> {
5075 let literal = {
5076 let instance = self.instance(occurrence).ok()?;
5077 let args = instance.part("ANNOTATION_TEXT_OCCURRENCE")?;
5078 args.get(2).and_then(Arg::reference)?
5079 };
5080 let args = self.args(literal, "TEXT_LITERAL").ok()?;
5081 match args.get(1) {
5082 Some(Arg::Str(words)) => Some(words.clone()),
5083 _ => None,
5084 }
5085 }
5086
5087 fn annotation_polylines(&mut self, occurrence: u64) -> Vec<Vec<Point>> {
5094 let item = {
5095 let Ok(instance) = self.instance(occurrence) else {
5096 return Vec::new();
5097 };
5098 let args = instance
5099 .part("TESSELLATED_ANNOTATION_OCCURRENCE")
5100 .or_else(|| instance.part("ANNOTATION_OCCURRENCE"))
5101 .or_else(|| instance.part("STYLED_ITEM"));
5102 match args.and_then(|a| a.get(2).and_then(Arg::reference)) {
5103 Some(item) => item,
5104 None => return Vec::new(),
5105 }
5106 };
5107 self.tessellated_polylines(item, 0)
5108 }
5109
5110 fn tessellated_polylines(&mut self, item: u64, depth: usize) -> Vec<Vec<Point>> {
5118 if depth > 4 {
5119 return Vec::new();
5120 }
5121 let (children, curve_set, placement) = {
5122 let Ok(instance) = self.instance(item) else {
5123 return Vec::new();
5124 };
5125 let children: Vec<u64> = instance
5126 .part("TESSELLATED_GEOMETRIC_SET")
5127 .and_then(|args| args.first().and_then(Arg::list))
5128 .map(|list| list.iter().filter_map(Arg::reference).collect())
5129 .unwrap_or_default();
5130 let curve_set = instance.part("TESSELLATED_CURVE_SET");
5131 let placement = instance
5132 .part("REPOSITIONED_TESSELLATED_ITEM")
5133 .and_then(|args| args.first().and_then(Arg::reference));
5134 (children, curve_set, placement)
5135 };
5136
5137 let mut out = Vec::new();
5138 for child in children {
5139 out.extend(self.tessellated_polylines(child, depth + 1));
5140 }
5141 if let Some(args) = curve_set
5142 && let Some(list) = args.get(1).and_then(Arg::reference)
5143 {
5144 let points = self.coordinates_list(list);
5145 for line in args.get(2).and_then(Arg::list).unwrap_or(&[]) {
5146 let Some(indices) = line.list() else { continue };
5147 let mut polyline = Vec::with_capacity(indices.len());
5148 for index in indices {
5149 let Some(k) = index.number() else { continue };
5151 #[expect(
5152 clippy::cast_possible_truncation,
5153 clippy::cast_sign_loss,
5154 reason = "an index into a list the file itself sized"
5155 )]
5156 let k = k as usize;
5157 if k >= 1 && k <= points.len() {
5158 polyline.push(points[k - 1]);
5159 }
5160 }
5161 if polyline.len() >= 2 {
5162 out.push(polyline);
5163 }
5164 }
5165 }
5166 if let Some(placement) = placement
5167 && let Ok(frame) = self.frame(placement)
5168 {
5169 for polyline in &mut out {
5170 for p in polyline.iter_mut() {
5171 *p = frame.to_world(*p);
5172 }
5173 }
5174 }
5175 out
5176 }
5177
5178 fn coordinates_list(&mut self, id: u64) -> Vec<Point> {
5180 let Ok(args) = self.args(id, "COORDINATES_LIST") else {
5181 return Vec::new();
5182 };
5183 let scale = self.report.scale_mm;
5184 args.get(2)
5185 .and_then(Arg::list)
5186 .unwrap_or(&[])
5187 .iter()
5188 .filter_map(|entry| {
5189 let coords = entry.list()?;
5190 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0) * scale;
5191 Some(Point::new(value(0), value(1), value(2)))
5192 })
5193 .collect()
5194 }
5195
5196 fn ids_with(&self, keyword: &str) -> Vec<u64> {
5198 self.instances
5199 .iter()
5200 .filter(|(_, inst)| inst.part(keyword).is_some())
5201 .map(|(id, _)| id)
5202 .collect()
5203 }
5204
5205 fn ensure_property_indexes(&mut self) {
5211 if self.properties_of_definition.is_some() {
5212 return;
5213 }
5214 let mut properties: FastMap<u64, Vec<u64>> = FastMap::default();
5215 let mut sdrs: FastMap<u64, Vec<u64>> = FastMap::default();
5216 let instances = self.instances;
5217 for (id, instance) in instances.iter() {
5218 if let Some(args) = instance.part("PROPERTY_DEFINITION") {
5219 self.visited.mark(id);
5222 if let Some(definition) = args.get(2).and_then(Arg::reference) {
5223 properties.entry(definition).or_default().push(id);
5224 }
5225 }
5226 if let Some(args) = instance.part("SHAPE_DEFINITION_REPRESENTATION") {
5227 self.visited.mark(id);
5228 if let Some(property) = args.first().and_then(Arg::reference) {
5229 sdrs.entry(property).or_default().push(id);
5230 }
5231 }
5232 }
5233 for list in properties.values_mut().chain(sdrs.values_mut()) {
5234 list.sort_unstable();
5235 }
5236 self.properties_of_definition = Some(properties);
5237 self.sdrs_of_property = Some(sdrs);
5238 }
5239
5240 fn measure_value(&mut self, id: u64) -> Option<(f64, ogeom_doc::MeasureKind)> {
5242 let args = {
5243 let instance = self.instance(id).ok()?;
5244 instance.part("MEASURE_WITH_UNIT")?.to_vec()
5245 };
5246 match args.first() {
5247 Some(Arg::Typed(typed)) => {
5248 let (kind, inner) = (&typed.0, &typed.1);
5249 let value = inner.first().and_then(Arg::number)?;
5250 if kind.contains("ANGLE") {
5251 Some((value * self.angle_scale, ogeom_doc::MeasureKind::Angle))
5252 } else {
5253 Some((value * self.report.scale_mm, ogeom_doc::MeasureKind::Length))
5254 }
5255 }
5256 _ => None,
5257 }
5258 }
5259
5260 fn dimension_shape(&mut self, dim: u64) -> (String, bool, Vec<u64>) {
5266 let size = {
5267 let Ok(instance) = self.instance(dim) else {
5268 return (String::new(), false, Vec::new());
5269 };
5270 instance
5271 .part("DIMENSIONAL_SIZE")
5272 .or_else(|| instance.part("ANGULAR_SIZE"))
5273 };
5274 if let Some(args) = size {
5275 let name = match args.get(1) {
5276 Some(Arg::Str(s)) => s.clone(),
5277 _ => String::new(),
5278 };
5279 let aspects = args.first().and_then(Arg::reference).into_iter().collect();
5280 return (name, false, aspects);
5281 }
5282 let location = {
5283 let Ok(instance) = self.instance(dim) else {
5284 return (String::new(), false, Vec::new());
5285 };
5286 instance
5287 .part("DIMENSIONAL_LOCATION")
5288 .or_else(|| instance.part("ANGULAR_LOCATION"))
5289 };
5290 if let Some(args) = location {
5291 let name = match args.first() {
5292 Some(Arg::Str(s)) => s.clone(),
5293 _ => String::new(),
5294 };
5295 let aspects = [args.get(2), args.get(3)]
5296 .into_iter()
5297 .flatten()
5298 .filter_map(Arg::reference)
5299 .collect();
5300 return (name, true, aspects);
5301 }
5302 (String::new(), false, Vec::new())
5303 }
5304
5305 fn datum_system(&mut self, id: u64) -> Option<Vec<ogeom_doc::DatumReference>> {
5310 let compartments: Vec<u64> = {
5311 let instance = self.instance(id).ok()?;
5312 instance
5313 .part("DATUM_SYSTEM")?
5314 .get(4)
5315 .and_then(Arg::list)?
5316 .iter()
5317 .filter_map(Arg::reference)
5318 .collect()
5319 };
5320 let mut out = Vec::with_capacity(compartments.len());
5321 for compartment in compartments {
5322 let args = self.args(compartment, "DATUM_REFERENCE_COMPARTMENT").ok()?;
5323 let label = match args.get(4)? {
5324 Arg::Ref(base) => self.datum_letter(*base, 0)?,
5325 Arg::List(elements) => {
5326 let mut letters = Vec::with_capacity(elements.len());
5329 for element in elements {
5330 let element = element.reference()?;
5331 let datum = match self.args(element, "DATUM_REFERENCE_ELEMENT") {
5332 Ok(args) => args.get(4).and_then(Arg::reference)?,
5333 Err(_) => element,
5334 };
5335 letters.push(self.datum_letter(datum, 0)?);
5336 }
5337 letters.join("-")
5338 }
5339 _ => return None,
5340 };
5341 let mut modifiers = Vec::new();
5342 for modifier in args.get(5).and_then(Arg::list).unwrap_or(&[]) {
5343 match modifier {
5344 Arg::Typed(typed) => {
5345 if let Some(Arg::Enum(word)) = typed.1.first() {
5346 modifiers.push(ogeom_doc::DatumModifier::Simple(word.to_lowercase()));
5347 }
5348 }
5349 Arg::Enum(word) => {
5350 modifiers.push(ogeom_doc::DatumModifier::Simple(word.to_lowercase()));
5351 }
5352 Arg::Ref(valued) => {
5353 if let Some(modifier) = self.datum_modifier_with_value(*valued) {
5354 modifiers.push(modifier);
5355 }
5356 }
5357 _ => {}
5358 }
5359 }
5360 out.push(ogeom_doc::DatumReference { label, modifiers });
5361 }
5362 Some(out)
5363 }
5364
5365 fn datum_modifier_with_value(&mut self, id: u64) -> Option<ogeom_doc::DatumModifier> {
5367 let args = self.args(id, "DATUM_REFERENCE_MODIFIER_WITH_VALUE").ok()?;
5368 let kind = match args.first()? {
5369 Arg::Enum(word) => ogeom_doc::DatumModifierKind::from_word(word)?,
5370 _ => return None,
5371 };
5372 let value = self.length_value(args.get(1)?.reference()?)?;
5373 Some(ogeom_doc::DatumModifier::WithValue { kind, value })
5374 }
5375
5376 fn length_value(&mut self, id: u64) -> Option<f64> {
5379 if let Some((value, ogeom_doc::MeasureKind::Length)) = self.measure_value(id) {
5380 return Some(value);
5381 }
5382 let args = self.args(id, "LENGTH_MEASURE_WITH_UNIT").ok()?;
5383 match args.first()? {
5384 Arg::Typed(typed) => {
5385 Some(typed.1.first().and_then(Arg::number)? * self.report.scale_mm)
5386 }
5387 _ => None,
5388 }
5389 }
5390
5391 fn datum_letter(&mut self, id: u64, depth: usize) -> Option<String> {
5395 if depth > 4 {
5396 return None;
5397 }
5398 let (letter, members, refs) = {
5399 let instance = self.instance(id).ok()?;
5400 let letter = instance.part("DATUM").and_then(|args| match args.get(4) {
5401 Some(Arg::Str(s)) if !s.is_empty() => Some(s.clone()),
5402 _ => None,
5403 });
5404 let members: Vec<u64> = ["DATUM_REFERENCE_COMPARTMENT", "COMMON_DATUM"]
5408 .iter()
5409 .find_map(|k| instance.part(k))
5410 .into_iter()
5411 .flatten()
5412 .find_map(Arg::list)
5413 .map(|list| list.iter().filter_map(Arg::reference).collect())
5414 .unwrap_or_default();
5415 let mut refs = Vec::new();
5416 for (_, args) in instance.parts() {
5417 collect_refs(args, &mut refs);
5418 }
5419 (letter, members, refs)
5420 };
5421 if let Some(letter) = letter {
5422 return Some(letter);
5423 }
5424 if !members.is_empty() {
5425 let letters: Vec<String> = members
5426 .into_iter()
5427 .filter_map(|r| self.datum_letter(r, depth + 1))
5428 .collect();
5429 if !letters.is_empty() {
5430 return Some(letters.join("-"));
5431 }
5432 }
5433 refs.into_iter()
5434 .find_map(|r| self.datum_letter(r, depth + 1))
5435 }
5436}
5437
5438struct PdEntry {
5441 pd: u64,
5442 name: String,
5443 shapes: Vec<Shape>,
5444 construction: Vec<(String, ConstructionGeometry)>,
5446 children: Vec<(u64, Transform, Option<String>)>,
5447}
5448
5449fn walked_back(model: &mut Model, wires: &[Shape], tol: Tolerances) -> OgeomResult<Vec<Shape>> {
5452 wires
5453 .iter()
5454 .map(|wire| {
5455 let back = model.ordered_children_of(&wire.reversed())?;
5456 Ok(make_wire(model, &back, tol)?.shape)
5457 })
5458 .collect()
5459}
5460
5461fn derive_pcurve(
5466 curve: &Curve,
5467 range: (f64, f64),
5468 surface: &SurfaceGeometry,
5469 stated: &[PlanarCurve],
5470 tol: Tolerances,
5471) -> PreparedPcurve {
5472 let lifted = |pcurve: &PlanarCurve| {
5478 let gap = if stated.is_empty() {
5479 ogeom_algo::pcurve_fit::lifted_gap_past_confusion
5480 } else {
5481 ogeom_algo::pcurve_fit::lifted_gap
5482 };
5483 gap((curve, range), (pcurve, range), surface, false, tol).unwrap_or(0.0)
5484 };
5485 let nearest = nearest_stated(curve, range, surface, stated, tol);
5486 if let Some((curve, off)) = &nearest
5488 && *off <= tol.confusion()
5489 {
5490 return PreparedPcurve::Stated {
5491 curve: curve.clone(),
5492 off: *off,
5493 };
5494 }
5495 let derived =
5496 if let Some(exact) = ogeom_intersect::exact_pcurve_over(curve, range, surface, tol) {
5497 let exact = crate::pcurves::over_range(&exact, range);
5498 let off = lifted(&exact);
5499 PreparedPcurve::Exact { curve: exact, off }
5500 } else {
5501 match crate::pcurves::fit_projected_pcurve(curve, range, surface, tol) {
5505 Ok((fitted, error, met, worst_off, warning)) => {
5506 let off = lifted(&fitted);
5507 PreparedPcurve::Fitted {
5508 curve: fitted,
5509 error,
5510 met,
5511 worst_off,
5512 off,
5513 warning,
5514 }
5515 }
5516 Err(e) => PreparedPcurve::Refused(e.to_string()),
5517 }
5518 };
5519 let allowed = match &derived {
5525 PreparedPcurve::Exact { off, .. } => Some(*off),
5526 PreparedPcurve::Fitted { off, worst_off, .. } => Some(off.max(*worst_off)),
5527 PreparedPcurve::Stated { .. } | PreparedPcurve::Refused(_) => None,
5528 };
5529 let Some(allowed) = allowed else {
5530 return derived;
5531 };
5532 let bound = 2.0f64.mul_add(allowed, tol.confusion());
5533 if let Some((curve, off)) = nearest
5534 && off <= bound
5535 {
5536 return PreparedPcurve::Stated { curve, off };
5537 }
5538 let ends = match &derived {
5544 PreparedPcurve::Exact { curve: image, .. }
5545 | PreparedPcurve::Fitted { curve: image, .. } => image
5546 .point_at(range.0, tol)
5547 .ok()
5548 .zip(image.point_at(range.1, tol).ok()),
5549 PreparedPcurve::Stated { .. } | PreparedPcurve::Refused(_) => None,
5550 };
5551 if let Some(ends) = ends {
5552 let repaced: Vec<PlanarCurve> = stated
5553 .iter()
5554 .flat_map(|pcurve| repaced(pcurve, range, ends, tol))
5555 .collect();
5556 if let Some((curve, off)) = nearest_stated(curve, range, surface, &repaced, tol)
5557 && off <= bound
5558 {
5559 return PreparedPcurve::Stated { curve, off };
5560 }
5561 }
5562 derived
5563}
5564
5565fn nearest_stated(
5577 curve: &Curve,
5578 range: (f64, f64),
5579 surface: &SurfaceGeometry,
5580 stated: &[PlanarCurve],
5581 tol: Tolerances,
5582) -> Option<(PlanarCurve, f64)> {
5583 let period = curve.is_periodic().then(|| {
5584 let (lo, hi) = curve.domain();
5585 hi - lo
5586 });
5587 let turns: &[f64] = if period.is_some() {
5588 &[0.0, 1.0, -1.0]
5589 } else {
5590 &[0.0]
5591 };
5592 let mut best: Option<(PlanarCurve, f64)> = None;
5593 for pcurve in stated {
5594 for turn in turns {
5595 let Some(shifted) = period_shifted(pcurve, turn * period.unwrap_or(0.0)) else {
5596 continue;
5597 };
5598 let shifted = crate::pcurves::over_range(&shifted, range);
5599 if !within_chart(&shifted, range, surface, tol) {
5600 continue;
5601 }
5602 let Ok(off) = ogeom_algo::pcurve_fit::lifted_gap_past_confusion(
5603 (curve, range),
5604 (&shifted, range),
5605 surface,
5606 false,
5607 tol,
5608 ) else {
5609 continue;
5610 };
5611 if best.as_ref().is_none_or(|(_, held)| off < *held) {
5612 best = Some((shifted, off));
5613 }
5614 if off <= tol.confusion() {
5615 return best;
5616 }
5617 }
5618 }
5619 best
5620}
5621
5622fn period_shifted(pcurve: &PlanarCurve, delta: f64) -> Option<PlanarCurve> {
5625 if delta == 0.0 {
5626 return Some(pcurve.clone());
5627 }
5628 match pcurve {
5629 PlanarCurve::Line(l) => {
5630 let axis = l.axis();
5631 let moved = ogeom_math::Axis2 {
5632 location: axis.location - axis.direction.vector() * delta,
5633 direction: axis.direction,
5634 };
5635 let (lo, hi) = l.domain();
5636 ogeom_geom::Line2d::over(moved, lo + delta, hi + delta)
5637 .ok()
5638 .map(Into::into)
5639 }
5640 PlanarCurve::BSpline(b) => {
5641 let knots: Vec<f64> = b.knots().knots().iter().map(|k| k + delta).collect();
5642 let knots = KnotVector::new(knots, b.knots().degree()).ok()?;
5643 ogeom_geom::BSpline2d::rational(knots, b.control_points().to_vec())
5644 .ok()
5645 .map(Into::into)
5646 }
5647 PlanarCurve::Circle(_) | PlanarCurve::Ellipse(_)
5649 if (delta / core::f64::consts::TAU).fract().abs() < 1e-12 =>
5650 {
5651 Some(pcurve.clone())
5652 }
5653 _ => None,
5654 }
5655}
5656
5657fn in_kernel_units(
5665 pcurve: &PlanarCurve,
5666 (su, sv): (f64, f64),
5667 pace: f64,
5668 angle: f64,
5669 tol: Tolerances,
5670) -> Option<PlanarCurve> {
5671 let own = match pcurve {
5672 PlanarCurve::Circle(_) | PlanarCurve::Ellipse(_) => angle,
5673 _ => 1.0,
5674 };
5675 if su == 1.0 && sv == 1.0 && own == pace {
5676 return Some(pcurve.clone());
5677 }
5678 match pcurve {
5679 PlanarCurve::Line(l) => {
5680 let axis = l.axis();
5686 let d = axis.direction.vector();
5687 let along = ogeom_math::Vector2::new(d.x * su, d.y * sv);
5688 let speed = if su == sv || d.y == 0.0 {
5689 su.abs()
5690 } else if d.x == 0.0 {
5691 sv.abs()
5692 } else {
5693 along.magnitude()
5694 };
5695 let origin = ogeom_math::Point2::new(axis.location.x * su, axis.location.y * sv);
5696 let line =
5697 ogeom_math::Axis2::new(origin, ogeom_math::Direction2::new(along, tol).ok()?);
5698 let (lo, hi) = l.domain();
5699 let stretched: PlanarCurve = ogeom_geom::Line2d::over(line, lo * speed, hi * speed)
5700 .ok()?
5701 .into();
5702 paced(&stretched, speed * own / pace, 0.0, tol)
5703 }
5704 PlanarCurve::Circle(_) | PlanarCurve::Ellipse(_) => {
5705 if own != pace {
5706 return None;
5707 }
5708 pcurve.stretched(su, sv, tol).ok()
5709 }
5710 PlanarCurve::BSpline(_) => {
5711 let stretched = pcurve.stretched(su, sv, tol).ok()?;
5712 paced(&stretched, own / pace, 0.0, tol)
5713 }
5714 _ => None,
5715 }
5716}
5717
5718#[derive(Debug, Clone, Copy)]
5721struct ChartMap {
5722 linear: [[f64; 2]; 2],
5723 offset: (f64, f64),
5724}
5725
5726impl ChartMap {
5727 fn point(&self, p: ogeom_math::Point2) -> ogeom_math::Point2 {
5728 let [[a, b], [c, d]] = self.linear;
5729 ogeom_math::Point2::new(
5730 a.mul_add(p.x, b * p.y) + self.offset.0,
5731 c.mul_add(p.x, d * p.y) + self.offset.1,
5732 )
5733 }
5734
5735 fn vector(&self, v: ogeom_math::Vector2) -> ogeom_math::Vector2 {
5736 let [[a, b], [c, d]] = self.linear;
5737 ogeom_math::Vector2::new(a.mul_add(v.x, b * v.y), c.mul_add(v.x, d * v.y))
5738 }
5739}
5740
5741fn through_map(
5749 pcurve: &PlanarCurve,
5750 map: ChartMap,
5751 pace: f64,
5752 tol: Tolerances,
5753) -> Option<PlanarCurve> {
5754 match pcurve {
5755 PlanarCurve::Line(l) => {
5756 let axis = l.axis();
5757 let along = map.vector(axis.direction.vector());
5758 let speed = along.magnitude();
5759 let line = ogeom_math::Axis2::new(
5760 map.point(axis.location),
5761 ogeom_math::Direction2::new(along, tol).ok()?,
5762 );
5763 let (lo, hi) = l.domain();
5764 let mapped: PlanarCurve = ogeom_geom::Line2d::over(line, lo * speed, hi * speed)
5765 .ok()?
5766 .into();
5767 paced(&mapped, speed / pace, 0.0, tol)
5768 }
5769 PlanarCurve::BSpline(b) => {
5770 let control = b
5771 .control_points()
5772 .iter()
5773 .map(|w| ogeom_math::Weighted::new(map.point(w.point()), w.weight, tol))
5774 .collect::<OgeomResult<Vec<_>>>()
5775 .ok()?;
5776 let mapped: PlanarCurve = ogeom_geom::BSpline2d::rational(b.knots().clone(), control)
5777 .ok()?
5778 .into();
5779 paced(&mapped, 1.0 / pace, 0.0, tol)
5780 }
5781 _ => None,
5782 }
5783}
5784
5785fn paced(pcurve: &PlanarCurve, a: f64, b: f64, tol: Tolerances) -> Option<PlanarCurve> {
5793 use ogeom_geom::Reversible as _;
5794 if !a.is_finite() || !b.is_finite() || a == 0.0 {
5795 return None;
5796 }
5797 if a == 1.0 && b == 0.0 {
5798 return Some(pcurve.clone());
5799 }
5800 let back = |lo: f64, hi: f64| {
5802 let (x, y) = ((lo - b) / a, (hi - b) / a);
5803 (x.min(y), x.max(y))
5804 };
5805 match pcurve {
5806 PlanarCurve::Line(l) => {
5807 let axis = l.axis();
5808 let (lo, hi) = l.domain();
5809 let (from, to) = back(lo, hi);
5810 if a.abs() == 1.0 {
5811 let direction = if a > 0.0 {
5812 axis.direction
5813 } else {
5814 axis.direction.reversed()
5815 };
5816 let line = ogeom_math::Axis2::new(axis.point_at(b), direction);
5817 return ogeom_geom::Line2d::over(line, from, to)
5818 .ok()
5819 .map(Into::into);
5820 }
5821 let at = |t: f64| axis.point_at(a.mul_add(t, b));
5822 let knots = KnotVector::new(vec![from, from, to, to], 1).ok()?;
5823 ogeom_geom::BSpline2d::new(knots, vec![at(from), at(to)], tol)
5824 .ok()
5825 .map(Into::into)
5826 }
5827 PlanarCurve::BSpline(spline) => {
5828 let mut knots: Vec<f64> = spline.knots().knots().iter().map(|k| (k - b) / a).collect();
5829 let mut control = spline.control_points().to_vec();
5830 if a < 0.0 {
5831 knots.reverse();
5832 control.reverse();
5833 }
5834 let knots = KnotVector::new(knots, spline.knots().degree()).ok()?;
5835 ogeom_geom::BSpline2d::rational(knots, control)
5836 .ok()
5837 .map(Into::into)
5838 }
5839 PlanarCurve::Circle(c) if a.abs() == 1.0 => {
5840 let sense = if c.is_reversed() { -1.0 } else { 1.0 };
5843 let circle = c.circle();
5844 let frame = circle.frame();
5845 let (sin, cos) = (sense * b).sin_cos();
5846 let (x, y) = (frame.x().vector(), frame.y().vector());
5847 let turned = ogeom_math::Frame2::from_axes(
5848 frame.origin(),
5849 ogeom_math::Direction2::new(x * cos + y * sin, tol).ok()?,
5850 ogeom_math::Direction2::new(y * cos - x * sin, tol).ok()?,
5851 tol,
5852 )
5853 .ok()?;
5854 let out: PlanarCurve = ogeom_geom::Circle2d::new(
5855 ogeom_math::Circle2::new(turned, circle.radius(), tol).ok()?,
5856 )
5857 .into();
5858 Some(if sense * a < 0.0 { out.reversed() } else { out })
5859 }
5860 PlanarCurve::Ellipse(e) if a.abs() == 1.0 => {
5861 let half_turns = (b / core::f64::consts::PI).round();
5862 if core::f64::consts::PI * half_turns != b {
5863 return None;
5864 }
5865 let sense = if e.is_reversed() { -1.0 } else { 1.0 };
5866 let ellipse = e.ellipse();
5867 let frame = ellipse.frame();
5868 let frame = if half_turns.rem_euclid(2.0) == 0.0 {
5869 frame
5870 } else {
5871 ogeom_math::Frame2::from_axes(
5872 frame.origin(),
5873 frame.x().reversed(),
5874 frame.y().reversed(),
5875 tol,
5876 )
5877 .ok()?
5878 };
5879 let out: PlanarCurve = ogeom_geom::Ellipse2d::new(
5880 ogeom_math::Ellipse2::new(
5881 frame,
5882 ellipse.major_radius(),
5883 ellipse.minor_radius(),
5884 tol,
5885 )
5886 .ok()?,
5887 )
5888 .into();
5889 Some(if sense * a < 0.0 { out.reversed() } else { out })
5890 }
5891 _ => None,
5892 }
5893}
5894
5895fn repaced(
5902 pcurve: &PlanarCurve,
5903 range: (f64, f64),
5904 ends: (ogeom_math::Point2, ogeom_math::Point2),
5905 tol: Tolerances,
5906) -> Vec<PlanarCurve> {
5907 let straight = match pcurve {
5910 PlanarCurve::BSpline(spline) if spline.knots().degree() == 1 && !spline.is_rational() => {
5911 match spline.control_points() {
5912 [from, to] => Some((from.point(), to.point())),
5913 _ => None,
5914 }
5915 }
5916 _ => None,
5917 };
5918 let line;
5919 let pcurve = match straight {
5920 Some((from, to)) => {
5921 let Ok(segment) = ogeom_geom::Line2d::segment(from, to, tol) else {
5922 return Vec::new();
5923 };
5924 line = PlanarCurve::from(segment);
5925 &line
5926 }
5927 None => pcurve,
5928 };
5929 let span = range.1 - range.0;
5930 let foot = |w: ogeom_math::Point2| -> Option<f64> {
5933 match pcurve {
5934 PlanarCurve::Line(l) => {
5935 let axis = l.axis();
5936 Some((w - axis.location).dot(axis.direction.vector()))
5937 }
5938 PlanarCurve::Circle(c) => {
5939 let frame = c.circle().frame();
5940 let r = w - frame.origin();
5941 let angle = r.dot(frame.y().vector()).atan2(r.dot(frame.x().vector()));
5942 Some(if c.is_reversed() { -angle } else { angle })
5943 }
5944 PlanarCurve::Ellipse(e) => {
5945 let ellipse = e.ellipse();
5946 let frame = ellipse.frame();
5947 let r = w - frame.origin();
5948 let angle = (r.dot(frame.y().vector()) / ellipse.minor_radius())
5949 .atan2(r.dot(frame.x().vector()) / ellipse.major_radius());
5950 Some(if e.is_reversed() { -angle } else { angle })
5951 }
5952 _ => None,
5953 }
5954 };
5955 let mut maps: Vec<(f64, f64)> = Vec::new();
5956 match pcurve {
5957 PlanarCurve::BSpline(spline) => {
5958 let (lo, hi) = spline.domain();
5959 let rate = (hi - lo) / span;
5960 maps.push((rate, rate.mul_add(-range.0, lo)));
5961 maps.push((-rate, rate.mul_add(range.0, hi)));
5962 }
5963 PlanarCurve::Line(_) | PlanarCurve::Circle(_) | PlanarCurve::Ellipse(_) => {
5964 let Some(start) = foot(ends.0) else {
5965 return Vec::new();
5966 };
5967 for a in [1.0_f64, -1.0] {
5968 let b = a.mul_add(-range.0, start);
5969 let b = if matches!(pcurve, PlanarCurve::Ellipse(_)) {
5971 core::f64::consts::PI * (b / core::f64::consts::PI).round()
5972 } else {
5973 b
5974 };
5975 maps.push((a, b));
5976 }
5977 if let PlanarCurve::Line(_) = pcurve
5978 && let Some(end) = foot(ends.1)
5979 {
5980 let rate = (end - start) / span;
5981 maps.push((rate, rate.mul_add(-range.0, start)));
5982 }
5983 }
5984 _ => {}
5985 }
5986 maps.into_iter()
5987 .filter_map(|(a, b)| paced(pcurve, a, b, tol))
5988 .collect()
5989}
5990
5991fn within_chart(
5994 pcurve: &PlanarCurve,
5995 range: (f64, f64),
5996 surface: &SurfaceGeometry,
5997 tol: Tolerances,
5998) -> bool {
5999 let ((u0, u1), (v0, v1)) = surface.domain();
6000 let slack = tol.parametric();
6001 (0..=4).all(|i| {
6002 let t = (range.1 - range.0).mul_add(f64::from(i) / 4.0, range.0);
6003 pcurve.point_at(t, tol).is_ok_and(|p| {
6004 (surface.is_periodic_u() || (p.x >= u0 - slack && p.x <= u1 + slack))
6005 && (surface.is_periodic_v() || (p.y >= v0 - slack && p.y <= v1 + slack))
6006 })
6007 })
6008}
6009
6010fn collect_refs(args: &[Arg], out: &mut Vec<u64>) {
6012 for arg in args {
6013 match arg {
6014 Arg::Ref(id) => out.push(*id),
6015 Arg::List(inner) => collect_refs(inner, out),
6016 Arg::Typed(typed) => collect_refs(&typed.1, out),
6017 _ => {}
6018 }
6019 }
6020}
6021
6022fn same_column(circle: &ogeom_math::Circle, p: Point, q: Point, tol: Tolerances) -> bool {
6026 let (lp, lq) = (circle.frame().to_local(p), circle.frame().to_local(q));
6027 let apart = (lp.y.atan2(lp.x) - lq.y.atan2(lq.x)).rem_euclid(core::f64::consts::TAU);
6028 let turn = apart.min(core::f64::consts::TAU - apart);
6029 turn * lq.x.hypot(lq.y) <= tol.confusion()
6030}
6031
6032fn pole_on_the_left(
6040 model: &Model,
6041 surface: &SurfaceGeometry,
6042 ball: ogeom_math::Sphere,
6043 ring: &Shape,
6044 at: &Shape,
6045 face_forward: bool,
6046 tol: Tolerances,
6047) -> OgeomResult<Option<Point>> {
6048 let Some(data) = model.node(ring).and_then(|n| n.data().as_edge()) else {
6049 return Ok(None);
6050 };
6051 let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
6052 return Ok(None);
6053 };
6054 let Some(curve) = model.geometry().curve(*curve) else {
6055 return Ok(None);
6056 };
6057 let Some(start) = model
6058 .node(at)
6059 .and_then(|n| n.data().as_vertex())
6060 .map(|v| v.point)
6061 else {
6062 return Ok(None);
6063 };
6064 let reversed = ring.orientation() == ogeom_topo::Orientation::Reversed;
6065 let t = if reversed { range.1 } else { range.0 };
6066 let along = curve.d1_at(t, tol)? * if reversed { -1.0 } else { 1.0 };
6067 let local = ball.frame().to_local(start);
6068 let (u, v) = (
6069 local.y.atan2(local.x),
6070 (local.z / ball.radius()).clamp(-1.0, 1.0).asin(),
6071 );
6072 let (du, dv) = surface.d1_at(u, v, tol)?;
6073 let own = du.cross(dv);
6074 let normal = if face_forward { own } else { -own };
6075 let left = normal.cross(along);
6076 let axis = ball.frame().z().vector() * ball.radius();
6077 let lean = left.dot(axis);
6078
6079 if lean.abs() <= tol.confusion() * left.magnitude() {
6080 return Ok(None);
6081 }
6082 Ok(Some(if lean > 0.0 {
6083 ball.centre() + axis
6084 } else {
6085 ball.centre() - axis
6086 }))
6087}
6088
6089fn closed_ring_edges(model: &Model, wires: &[Shape]) -> OgeomResult<Vec<(Shape, Shape)>> {
6090 let mut out = Vec::new();
6091 for wire in wires {
6092 let edges = ogeom_topo::explore(
6093 model,
6094 wire,
6095 ogeom_topo::Filter::OfType(ogeom_topo::ShapeType::Edge),
6096 )?;
6097 if edges.len() != 1 {
6098 return Ok(Vec::new());
6099 }
6100 let edge = edges[0].clone();
6101 let Some((a, b)) = ogeom_algo::edge_vertices(model, &edge)? else {
6102 return Ok(Vec::new());
6103 };
6104 if !a.is_same(&b) {
6105 return Ok(Vec::new());
6106 }
6107 out.push((edge, a));
6108 }
6109 Ok(out)
6110}
6111
6112fn window_between_feet(
6117 curve: &Curve,
6118 start: Point,
6119 end: Point,
6120 tol: Tolerances,
6121) -> Option<(f64, f64)> {
6122 let (lo, hi) = curve.domain();
6123 let samples = match curve {
6124 Curve::BSpline(spline) => (spline.control_points().len() * 8).max(64),
6125 _ => 64,
6126 };
6127 let foot = |p: Point| -> Option<f64> {
6128 let found = project_on_curve(curve, p, samples, tol).ok()?;
6129 (found.distance <= tol.confusion() * 1e4).then_some(found.parameter)
6130 };
6131 let (a, b) = (foot(start)?, foot(end)?);
6132 let slack = tol.parametric().max((hi - lo) * 1e-9);
6133 if b <= a + slack {
6134 return None;
6135 }
6136 if (a - lo).abs() <= slack && (b - hi).abs() <= slack {
6137 return None;
6138 }
6139 Some((a, b))
6140}
6141
6142fn implied_knots(form: &str, degree: usize, count: usize) -> Vec<f64> {
6146 let p = degree.max(1);
6147 #[allow(clippy::cast_precision_loss)]
6148 let at = |i: usize| i as f64;
6149 if form.starts_with("BEZIER") {
6150 let pieces = (count.saturating_sub(1) / p).max(1);
6151 let mut out = vec![0.0; p + 1];
6152 for s in 1..pieces {
6153 out.extend(std::iter::repeat_n(at(s), p));
6154 }
6155 out.extend(std::iter::repeat_n(at(pieces), p + 1));
6156 out
6157 } else if form.starts_with("QUASI_UNIFORM") {
6158 let interior = count.saturating_sub(p + 1);
6159 let mut out = vec![0.0; p + 1];
6160 out.extend((1..=interior).map(at));
6161 out.extend(std::iter::repeat_n(at(interior + 1), p + 1));
6162 out
6163 } else {
6164 #[allow(clippy::cast_precision_loss)]
6165 (0..count + p + 1).map(|i| i as f64 - p as f64).collect()
6166 }
6167}
6168
6169fn join_splines(
6174 held: (KnotVector, Vec<Weighted<Point>>),
6175 next: &BSplineCurve,
6176 tol: Tolerances,
6177) -> OgeomResult<(KnotVector, Vec<Weighted<Point>>)> {
6178 let (mut knots, mut control) = held;
6179 let mut next = next.clone();
6180 while next.degree() < knots.degree() {
6181 next = next.elevated(tol)?;
6182 }
6183 while knots.degree() < next.degree() {
6184 (knots, control) = ogeom_math::bspline::elevate_degree(&knots, &control, tol)?;
6185 }
6186 let (Some(end), Some(start)) = (control.last(), next.control_points().first()) else {
6187 ogeom_bail!(Construction, "a composite segment has no control points");
6188 };
6189 let factor = end.weight / start.weight;
6190 let scaled: Vec<Weighted<Point>> = next
6191 .control_points()
6192 .iter()
6193 .map(|w| Weighted {
6194 scaled: w.scaled.scale(factor),
6195 weight: w.weight * factor,
6196 })
6197 .collect();
6198 ogeom_math::bspline::join(&(knots, control), &(next.knots().clone(), scaled))
6199}
6200
6201#[derive(Debug, Clone)]
6204struct Patch {
6205 u: KnotVector,
6206 v: KnotVector,
6207 net: Vec<Vec<Weighted<Point>>>,
6208}
6209
6210impl Patch {
6211 fn of(surface: &ogeom_geom::BSplineSurface) -> OgeomResult<Self> {
6212 let grid = surface.grid();
6213 let (nu, nv) = (grid.u_count(), grid.v_count());
6214 let points = grid.points();
6215 let net = (0..nu)
6216 .map(|i| (0..nv).map(|j| points[i * nv + j]).collect())
6217 .collect();
6218 Ok(Self {
6219 u: surface.u_knots().reparameterized(0.0, 1.0)?,
6220 v: surface.v_knots().reparameterized(0.0, 1.0)?,
6221 net,
6222 })
6223 }
6224
6225 fn transposed(self) -> Self {
6226 let (nu, nv) = (self.net.len(), self.net[0].len());
6227 let net = (0..nv)
6228 .map(|j| (0..nu).map(|i| self.net[i][j]).collect())
6229 .collect();
6230 Self {
6231 u: self.v,
6232 v: self.u,
6233 net,
6234 }
6235 }
6236
6237 fn reversed_u(mut self) -> Self {
6238 self.net.reverse();
6239 self.u = self.u.reversed();
6240 self
6241 }
6242
6243 fn along_u(
6245 self,
6246 op: impl Fn(&KnotVector, &[Weighted<Point>]) -> OgeomResult<(KnotVector, Vec<Weighted<Point>>)>,
6247 ) -> OgeomResult<Self> {
6248 let nv = self.net[0].len();
6249 let mut knots = self.u.clone();
6250 let mut columns = Vec::with_capacity(nv);
6251 for j in 0..nv {
6252 let column: Vec<Weighted<Point>> = self.net.iter().map(|row| row[j]).collect();
6253 let (k, c) = op(&self.u, &column)?;
6254 knots = k;
6255 columns.push(c);
6256 }
6257 let nu = columns[0].len();
6258 let net = (0..nu)
6259 .map(|i| (0..nv).map(|j| columns[j][i]).collect())
6260 .collect();
6261 Ok(Self {
6262 u: knots,
6263 v: self.v,
6264 net,
6265 })
6266 }
6267}
6268
6269fn join_patches(mut grid: Vec<Vec<Patch>>, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
6271 let (m, n) = (grid.len(), grid[0].len());
6272 let pu = grid
6274 .iter()
6275 .flatten()
6276 .map(|p| p.u.degree())
6277 .max()
6278 .unwrap_or(1);
6279 let pv = grid
6280 .iter()
6281 .flatten()
6282 .map(|p| p.v.degree())
6283 .max()
6284 .unwrap_or(1);
6285 for patch in grid.iter_mut().flatten() {
6286 let mut p = patch.clone();
6287 while p.u.degree() < pu {
6288 p = p.along_u(|k, c| ogeom_math::bspline::elevate_degree(k, c, tol))?;
6289 }
6290 p = p.transposed();
6291 while p.u.degree() < pv {
6292 p = p.along_u(|k, c| ogeom_math::bspline::elevate_degree(k, c, tol))?;
6293 }
6294 *patch = p.transposed();
6295 }
6296 let unify = |patches: Vec<Patch>, along_v: bool| -> OgeomResult<Vec<Patch>> {
6300 let turned: Vec<Patch> = if along_v {
6301 patches.into_iter().map(Patch::transposed).collect()
6302 } else {
6303 patches
6304 };
6305 let mut wanted: Vec<(f64, usize)> = Vec::new();
6306 for p in &turned {
6307 for (value, count) in p.u.distinct() {
6308 if value <= 1e-12 || value >= 1.0 - 1e-12 {
6309 continue;
6310 }
6311 match wanted.iter_mut().find(|(v, _)| (v - value).abs() <= 1e-12) {
6312 Some(slot) => slot.1 = slot.1.max(count),
6313 None => wanted.push((value, count)),
6314 }
6315 }
6316 }
6317 let mut out = Vec::with_capacity(turned.len());
6318 for mut p in turned {
6319 for &(value, count) in &wanted {
6320 let have = p.u.multiplicity_of(value);
6321 if have < count {
6322 p = p.along_u(|k, c| {
6323 ogeom_math::bspline::insert_knot(k, c, value, count - have, tol)
6324 })?;
6325 }
6326 }
6327 out.push(if along_v { p.transposed() } else { p });
6328 }
6329 Ok(out)
6330 };
6331 for row in &mut grid {
6332 *row = unify(std::mem::take(row), true)?;
6333 }
6334 for j in 0..n {
6335 let column: Vec<Patch> = grid.iter().map(|row| row[j].clone()).collect();
6336 for (row, p) in grid.iter_mut().zip(unify(column, false)?) {
6337 row[j] = p;
6338 }
6339 }
6340 let near = |a: &Weighted<Point>, b: &Weighted<Point>| {
6342 a.point().distance(b.point()) <= tol.confusion() * 100.0
6343 && (a.weight - b.weight).abs() <= 1e-9 * a.weight.abs().max(1.0)
6344 };
6345 let mut rows: Vec<Vec<Weighted<Point>>> = Vec::new();
6346 for (i, grid_row) in grid.iter().enumerate() {
6347 let nu = grid_row[0].net.len();
6348 for a in 0..nu {
6349 if i > 0 && a == 0 {
6350 let prev = rows.len() - 1;
6352 let mut shared = 0;
6353 for (j, patch) in grid_row.iter().enumerate() {
6354 for (b, cell) in patch.net[0].iter().enumerate() {
6355 if j > 0 && b == 0 {
6356 continue;
6357 }
6358 if !near(&rows[prev][shared], cell) {
6359 ogeom_bail!(Construction, "neighbouring patches part along u");
6360 }
6361 shared += 1;
6362 }
6363 }
6364 continue;
6365 }
6366 let mut row = Vec::new();
6367 for (j, patch) in grid_row.iter().enumerate() {
6368 for (b, cell) in patch.net[a].iter().enumerate() {
6369 if j > 0 && b == 0 {
6370 if !near(row.last().unwrap_or(cell), cell) {
6371 ogeom_bail!(Construction, "neighbouring patches part along v");
6372 }
6373 continue;
6374 }
6375 row.push(*cell);
6376 }
6377 }
6378 rows.push(row);
6379 }
6380 }
6381 let joined_knots = |parts: Vec<&KnotVector>, degree: usize| -> OgeomResult<KnotVector> {
6385 let mut out: Vec<f64> = Vec::new();
6386 for (k, knots) in parts.iter().enumerate() {
6387 #[allow(clippy::cast_precision_loss)]
6388 let shift = k as f64;
6389 let values: Vec<f64> = knots.knots().iter().map(|x| x + shift).collect();
6390 if k == 0 {
6391 out.extend(values);
6392 } else {
6393 out.pop();
6394 out.extend_from_slice(&values[degree + 1..]);
6395 }
6396 }
6397 KnotVector::new(out, degree)
6398 };
6399 let u = joined_knots((0..m).map(|i| &grid[i][0].u).collect(), pu)?;
6400 let v = joined_knots((0..n).map(|j| &grid[0][j].v).collect(), pv)?;
6401 let (nu, nv) = (rows.len(), rows[0].len());
6402 let cells: Vec<Weighted<Point>> = rows.into_iter().flatten().collect();
6403 Ok(
6404 ogeom_geom::BSplineSurface::rational(u, v, ogeom_math::ControlGrid::new(cells, nu, nv)?)?
6405 .into(),
6406 )
6407}
6408
6409const MOST_DEGREE: usize = 64;
6413
6414fn file_degree(arg: Option<&Arg>, missing: f64) -> OgeomResult<usize> {
6417 let degree = arg.and_then(Arg::number).unwrap_or(missing);
6418 #[allow(clippy::cast_precision_loss)]
6419 if !degree.is_finite() || degree < 0.0 || degree > MOST_DEGREE as f64 {
6420 ogeom_bail!(Construction, "a spline of degree {degree}");
6421 }
6422 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
6423 Ok(degree as usize)
6424}
6425
6426fn expanded_knots(mults: &[Arg], knots: &[Arg]) -> OgeomResult<Vec<f64>> {
6430 let mut out = Vec::new();
6431 for (m, k) in mults.iter().zip(knots) {
6432 let count = m.number().unwrap_or(1.0);
6433 #[allow(clippy::cast_precision_loss)]
6434 if !count.is_finite() || count < 0.0 || count > (MOST_DEGREE + 1) as f64 {
6435 ogeom_bail!(Construction, "a knot of multiplicity {count}");
6436 }
6437 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
6438 out.extend(std::iter::repeat_n(
6439 k.number().unwrap_or(0.0),
6440 count as usize,
6441 ));
6442 }
6443 Ok(out)
6444}
6445
6446enum Visited {
6454 Dense(Vec<bool>),
6455 Sparse(ogeom_core::FastSet<u64>),
6456}
6457
6458impl Visited {
6459 fn for_instances(instances: &Instances) -> Self {
6460 match instances {
6461 Instances::Dense(slots) => Self::Dense(vec![false; slots.len()]),
6462 Instances::Sparse(_) => Self::Sparse(ogeom_core::FastSet::default()),
6463 }
6464 }
6465
6466 fn mark(&mut self, id: u64) {
6467 match self {
6468 Self::Dense(slots) => {
6469 if let Some(slot) = usize::try_from(id).ok().and_then(|i| slots.get_mut(i)) {
6470 *slot = true;
6471 }
6472 }
6473 Self::Sparse(seen) => {
6474 seen.insert(id);
6475 }
6476 }
6477 }
6478
6479 fn seen(&self, id: u64) -> bool {
6480 match self {
6481 Self::Dense(slots) => usize::try_from(id)
6482 .ok()
6483 .and_then(|i| slots.get(i).copied())
6484 .unwrap_or(false),
6485 Self::Sparse(seen) => seen.contains(&id),
6486 }
6487 }
6488}
6489
6490fn item_name(instance: &Instance) -> String {
6495 instance
6496 .part("REPRESENTATION_ITEM")
6497 .or_else(|| instance.parts.first().map(|(_, args)| args.as_slice()))
6498 .and_then(<[Arg]>::first)
6499 .and_then(|arg| match arg {
6500 Arg::Str(name) => Some(name.clone()),
6501 _ => None,
6502 })
6503 .unwrap_or_default()
6504}
6505
6506fn ref_at(args: &[Arg], i: usize) -> u64 {
6507 args.get(i).and_then(Arg::reference).unwrap_or(0)
6508}
6509
6510#[cfg(test)]
6511#[allow(clippy::unwrap_used)]
6512mod tests {
6513 use super::*;
6514
6515 const T: Tolerances = Tolerances::millimetres();
6516
6517 #[test]
6523 fn a_pcurve_paced_is_the_same_curve_at_the_mapped_parameter() {
6524 use ogeom_math::{Direction2, Frame2, Point2};
6525 let frame = Frame2::new(Point2::new(1.0, -2.0), Direction2::from_angle(0.3));
6526 let curves: Vec<PlanarCurve> = vec![
6527 ogeom_geom::Line2d::over(
6528 ogeom_math::Axis2::new(Point2::new(0.5, 1.0), Direction2::from_angle(1.1)),
6529 -10.0,
6530 10.0,
6531 )
6532 .unwrap()
6533 .into(),
6534 ogeom_geom::Circle2d::new(ogeom_math::Circle2::new(frame, 2.0, T).unwrap()).into(),
6535 ogeom_geom::Ellipse2d::new(ogeom_math::Ellipse2::new(frame, 3.0, 1.0, T).unwrap())
6536 .into(),
6537 ogeom_geom::BSpline2d::new(
6538 KnotVector::new(vec![0.0, 0.0, 0.0, 2.0, 5.0, 5.0, 5.0], 2).unwrap(),
6539 vec![
6540 Point2::new(0.0, 0.0),
6541 Point2::new(1.0, 3.0),
6542 Point2::new(4.0, 2.0),
6543 Point2::new(6.0, -1.0),
6544 ],
6545 T,
6546 )
6547 .unwrap()
6548 .into(),
6549 ];
6550 let pi = core::f64::consts::PI;
6551 let maps: [&[(f64, f64)]; 4] = [
6552 &[(1.0, 2.5), (-1.0, 0.75), (0.4, -1.0)],
6553 &[(1.0, 0.7), (-1.0, 0.7), (-1.0, -2.0)],
6554 &[(1.0, pi), (-1.0, pi), (-1.0, 0.0)],
6555 &[(0.5, 1.0), (-2.0, 4.0)],
6556 ];
6557 for (curve, maps) in curves.iter().zip(maps) {
6558 for &(a, b) in maps {
6559 let paced = paced(curve, a, b, T).unwrap();
6560 let (lo, hi) = curve.domain();
6561 let ((from, to), at) = if curve.is_periodic() {
6562 ((0.0, 6.0), 6)
6563 } else {
6564 let (x, y) = ((lo - b) / a, (hi - b) / a);
6565 ((x.min(y), x.max(y)), 8)
6566 };
6567 for i in 0..=at {
6568 let t = from + (to - from) * f64::from(i) / f64::from(at);
6569 let want = curve.point_at(a.mul_add(t, b), T).unwrap();
6570 let got = paced.point_at(t, T).unwrap();
6571 assert!(
6572 got.distance(want) <= 1e-12,
6573 "{curve:?} at {a}·t + {b}, t = {t}: {got:?} against {want:?}"
6574 );
6575 }
6576 }
6577 }
6578 assert!(paced(&curves[2], 1.0, 0.5, T).is_none());
6581 assert!(paced(&curves[1], 2.0, 0.0, T).is_none());
6582 }
6583
6584 #[test]
6588 fn an_edges_window_is_taken_between_its_vertices_feet() {
6589 let spline: Curve = BSplineCurve::new(
6590 KnotVector::clamped_uniform(3, 6).unwrap(),
6591 vec![
6592 Point::new(0.0, 0.0, 0.0),
6593 Point::new(1.0, 2.0, 0.5),
6594 Point::new(2.5, 1.0, -0.5),
6595 Point::new(4.0, 3.0, 1.0),
6596 Point::new(5.0, 0.5, 0.0),
6597 Point::new(6.0, 2.0, 2.0),
6598 ],
6599 T,
6600 )
6601 .unwrap()
6602 .into();
6603 let at = |t: f64| spline.point_at(t, T).unwrap();
6604 let (a, b) = window_between_feet(&spline, at(0.3), at(0.7), T).unwrap();
6605 assert!(
6606 (a - 0.3).abs() < 1e-9 && (b - 0.7).abs() < 1e-9,
6607 "{a} .. {b}"
6608 );
6609 let (a, b) = window_between_feet(&spline, at(0.0), at(0.7), T).unwrap();
6610 assert!(a.abs() < 1e-9 && (b - 0.7).abs() < 1e-9, "{a} .. {b}");
6611 assert!(window_between_feet(&spline, at(0.0), at(1.0), T).is_none());
6612 assert!(window_between_feet(&spline, at(0.7), at(0.3), T).is_none());
6613 let off = at(0.3) + Vector::new(0.0, 0.0, 0.5);
6614 assert!(window_between_feet(&spline, off, at(0.7), T).is_none());
6615 }
6616
6617 #[test]
6622 fn a_wrapped_parallel_states_its_gap() {
6623 let cone: SurfaceGeometry = ConeSurface::new(
6624 Cone::new(Frame::WORLD, 2.0, core::f64::consts::FRAC_PI_4, T).unwrap(),
6625 (-10.0, 10.0),
6626 )
6627 .unwrap()
6628 .into();
6629 let wider = 3e-7;
6630 let circle: Curve =
6631 CircleCurve::new(Circle::new(Frame::WORLD, 2.0 + wider, T).unwrap()).into();
6632 let range = (3.0, 8.5);
6633 let PreparedPcurve::Exact { curve, off } = derive_pcurve(&circle, range, &cone, &[], T)
6634 else {
6635 panic!("a parallel has an exact pcurve");
6636 };
6637 assert!(curve.point_at(range.1, T).is_ok(), "{curve:?}");
6638 assert!((off - wider).abs() < wider * 1e-3, "{off}");
6639 }
6640}