1use super::parse::{Arg, Exchange, Instance};
22use ogeom_algo::{
23 make_edge_between, make_face_on, make_shell, make_solid, make_vertex, make_wire,
24 project_on_curve,
25};
26use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
27use ogeom_geom::Curve2d as _;
28use ogeom_geom::Curve3d as _;
29use ogeom_geom::Surface as _;
30use ogeom_geom::Transformable as _;
31use ogeom_geom::{
32 BSplineCurve, CircleCurve, ConeSurface, Curve, CylinderSurface, EllipseCurve, ExtrusionSurface,
33 HyperbolaCurve, LineCurve, ParabolaCurve, PlanarCurve, PlaneSurface, SphereSurface,
34 SurfaceGeometry, TorusSurface,
35};
36use ogeom_math::{
37 Axis, Blend as _, Circle, Cone, Cylinder, Direction, Ellipse, Frame, Hyperbola, KnotVector,
38 Matrix3, Parabola, Plane, Point, Sphere, Torus, Transform, Vector, Weighted,
39};
40use ogeom_topo::{Location, Model, Shape};
41use std::collections::{BTreeMap, HashMap, HashSet};
42
43type BoundUse = (Shape, Shape, u64, Curve, (f64, f64));
46
47type ChartReading = dyn Fn(Point) -> (Option<f64>, Option<f64>);
50
51const SURFACE_EXTENT: f64 = 1e5;
55
56#[derive(Debug, Default)]
58pub struct StepReport {
59 pub scale_mm: f64,
61 pub skipped: BTreeMap<String, usize>,
65 pub warnings: Vec<String>,
67 pub summary: Vec<WarningSummary>,
73 pub untrimmed_faces: Vec<UntrimmedFace>,
82}
83
84#[derive(Debug, Clone)]
86pub struct WarningSummary {
87 pub kind: &'static str,
90 pub count: usize,
92 pub worst: f64,
95 pub exemplar: u64,
97}
98
99#[derive(Debug, Clone)]
101pub struct UntrimmedFace {
102 pub entity: u64,
104 pub face: Shape,
107}
108
109#[derive(Debug)]
111pub struct StepImport {
112 pub document: ogeom_doc::Document,
115 pub solids: Vec<Shape>,
118 pub shells: Vec<Shape>,
126 pub report: StepReport,
128}
129
130pub fn read_step(text: &str, tol: Tolerances) -> OgeomResult<StepImport> {
139 let exchange = super::parse::parse(text)?;
140 let mut reader = Reader {
141 exchange: &exchange,
142 model: Model::new(),
143 report: StepReport {
144 scale_mm: 1.0,
145 ..StepReport::default()
146 },
147 angle_scale: 1.0,
148 visited: {
149 let top = exchange.data.keys().max().copied().unwrap_or(0);
150 let top = usize::try_from(top).unwrap_or(usize::MAX - 1);
151 vec![false; top + 1]
152 },
153 vertices: HashMap::new(),
154 edges: HashMap::new(),
155 faces: HashMap::new(),
156 callout_index: HashMap::new(),
157 pcurves: HashMap::new(),
158 untrimmed_ids: Vec::new(),
159 tallies: HashMap::new(),
160 cdsr_of_nauo: None,
161 properties_of_definition: None,
162 sdrs_of_property: None,
163 tol,
164 };
165 reader.report.scale_mm = reader.unit_scale();
166 reader.angle_scale = reader.angle_unit_scale();
167
168 let mut solids = Vec::new();
169 let mut shells = Vec::new();
170 let mut by_item: HashMap<u64, Shape> = HashMap::new();
171 let mut ids: Vec<(u64, bool)> = exchange
174 .data
175 .iter()
176 .filter_map(|(id, inst)| {
177 if inst.part("MANIFOLD_SOLID_BREP").is_some() || inst.part("BREP_WITH_VOIDS").is_some()
178 {
179 Some((*id, true))
180 } else if inst.part("SHELL_BASED_SURFACE_MODEL").is_some() {
181 Some((*id, false))
182 } else {
183 None
184 }
185 })
186 .collect();
187 ids.sort_unstable();
188 let total = ids.len() as u64;
189 for (done, (id, is_solid)) in ids.into_iter().enumerate() {
190 ogeom_core::progress::checkpoint()?;
191 ogeom_core::progress::stage_at("step: solid", done as u64 + 1, total);
192 if is_solid {
193 let solid = reader.solid(id)?;
194 by_item.insert(id, solid.clone());
195 solids.push(solid);
196 } else {
197 let shell = reader.surface_model(id)?;
198 by_item.insert(id, shell.clone());
199 shells.push(shell);
200 }
201 }
202 for body in solids.iter().chain(shells.iter()) {
207 ogeom_algo::restore_containment(&mut reader.model, body)?;
208 }
209 {
212 let mut entries: Vec<WarningSummary> = reader
213 .tallies
214 .drain()
215 .map(|(kind, (count, worst, exemplar))| WarningSummary {
216 kind,
217 count,
218 worst,
219 exemplar,
220 })
221 .collect();
222 entries.sort_by(|a, b| b.count.cmp(&a.count).then(a.kind.cmp(b.kind)));
223 reader.report.summary = entries;
224 }
225 for id in std::mem::take(&mut reader.untrimmed_ids) {
229 if let Some(face) = reader.faces.get(&id) {
230 reader.report.untrimmed_faces.push(UntrimmedFace {
231 entity: id,
232 face: face.clone(),
233 });
234 }
235 }
236 if solids.is_empty() && shells.is_empty() {
237 ogeom_bail!(
238 Construction,
239 "the exchange file contains no MANIFOLD_SOLID_BREP, \
240 BREP_WITH_VOIDS or SHELL_BASED_SURFACE_MODEL to read"
241 );
242 }
243 let document = reader.document(&by_item, &solids, &shells)?;
244
245 for (id, instance) in &exchange.data {
247 if !usize::try_from(*id)
248 .ok()
249 .and_then(|i| reader.visited.get(i).copied())
250 .unwrap_or(false)
251 {
252 *reader
253 .report
254 .skipped
255 .entry(instance.keyword().to_owned())
256 .or_default() += 1;
257 }
258 }
259
260 Ok(StepImport {
261 document,
262 solids,
263 shells,
264 report: reader.report,
265 })
266}
267
268type BuiltEdge = (Shape, Curve, (f64, f64), bool);
271
272enum PreparedPcurve {
279 Exact(PlanarCurve),
281 Fitted {
283 curve: PlanarCurve,
284 error: f64,
285 met: bool,
286 worst_off: f64,
287 warning: Option<String>,
288 },
289 Refused(String),
291}
292
293struct Reader<'a> {
294 exchange: &'a Exchange,
295 model: Model,
296 report: StepReport,
297 angle_scale: f64,
299 visited: Vec<bool>,
302 vertices: HashMap<u64, Shape>,
303 edges: HashMap<u64, BuiltEdge>,
304 faces: HashMap<u64, Shape>,
305 callout_index: HashMap<u64, usize>,
307 pcurves: HashMap<(u64, u64), PreparedPcurve>,
310 untrimmed_ids: Vec<u64>,
313 tallies: HashMap<&'static str, (usize, f64, u64)>,
315 cdsr_of_nauo: Option<HashMap<u64, u64>>,
320 properties_of_definition: Option<HashMap<u64, Vec<u64>>>,
327 sdrs_of_property: Option<HashMap<u64, Vec<u64>>>,
328 tol: Tolerances,
329}
330
331impl Reader<'_> {
332 fn instance(&mut self, id: u64) -> OgeomResult<&'_ Instance> {
333 if let Ok(i) = usize::try_from(id)
334 && let Some(slot) = self.visited.get_mut(i)
335 {
336 *slot = true;
337 }
338 self.exchange.data.get(&id).ok_or_else(|| {
339 ogeom_core::ogeom_err!(
340 Construction,
341 "the file references #{id}, which does not exist"
342 )
343 })
344 }
345
346 fn face_args(&mut self, id: u64) -> OgeomResult<Vec<Arg>> {
350 let instance = self.instance(id)?;
351 if let Some(args) = instance
352 .part("ADVANCED_FACE")
353 .or_else(|| instance.part("FACE_SURFACE"))
354 {
355 return Ok(args.to_vec());
356 }
357 ogeom_bail!(
358 Construction,
359 "#{id} is {}, where a face (ADVANCED_FACE or FACE_SURFACE) was needed",
360 instance.keyword()
361 );
362 }
363
364 fn args(&mut self, id: u64, keyword: &str) -> OgeomResult<Vec<Arg>> {
365 let instance = self.instance(id)?;
366 let Some(args) = instance.part(keyword) else {
367 ogeom_bail!(
368 Construction,
369 "#{id} is {}, where {keyword} was needed",
370 instance.keyword()
371 );
372 };
373 Ok(args.to_vec())
374 }
375
376 fn assigned_units(&self) -> Vec<u64> {
384 let mut cited: Vec<u64> = self
390 .exchange
391 .data
392 .values()
393 .filter(|inst| {
394 inst.parts
395 .iter()
396 .any(|(k, _)| k.ends_with("SHAPE_REPRESENTATION"))
397 })
398 .filter_map(|inst| {
399 inst.parts
400 .iter()
401 .find(|(k, _)| k.ends_with("SHAPE_REPRESENTATION"))
402 .and_then(|(_, args)| args.last())
403 .and_then(Arg::reference)
404 })
405 .collect();
406 cited.sort_unstable();
407 cited.dedup();
408
409 let mut contexts: Vec<u64> = self
410 .exchange
411 .data
412 .iter()
413 .filter(|(_, inst)| inst.part("GLOBAL_UNIT_ASSIGNED_CONTEXT").is_some())
414 .map(|(id, _)| *id)
415 .collect();
416 contexts.sort_unstable();
417 contexts.sort_by_cached_key(|id| !cited.contains(id));
418
419 let mut out = Vec::new();
420 for id in contexts {
421 if let Some(instance) = self.exchange.data.get(&id)
422 && let Some(args) = instance.part("GLOBAL_UNIT_ASSIGNED_CONTEXT")
423 {
424 for arg in args.iter().filter_map(Arg::list).flatten() {
425 if let Some(r) = arg.reference() {
426 out.push(r);
427 }
428 }
429 }
430 }
431 out
432 }
433
434 fn unit_scale(&mut self) -> f64 {
436 let assigned = self.assigned_units();
437 for id in assigned {
438 let Some(instance) = self.exchange.data.get(&id) else {
439 continue;
440 };
441 let instance = instance.clone();
442 if instance.part("LENGTH_UNIT").is_none() {
443 continue;
444 }
445 if let Some(args) = instance.part("SI_UNIT") {
446 return match args.first() {
447 Some(Arg::Enum(prefix)) => match prefix.as_str() {
448 "MILLI" => 1.0,
449 "CENTI" => 10.0,
450 "DECI" => 100.0,
451 "KILO" => 1e6,
452 "MICRO" => 1e-3,
453 other => {
454 self.report
455 .warnings
456 .push(format!("#{id}: unknown SI prefix {other}; taking metres"));
457 1000.0
458 }
459 },
460 _ => 1000.0,
461 };
462 }
463 if let Some(args) = instance.part("CONVERSION_BASED_UNIT")
464 && let Some(measure) = args.get(1).and_then(Arg::reference)
465 && let Some(inner) = self.exchange.data.get(&measure)
466 {
467 let factor = inner
468 .parts
469 .iter()
470 .flat_map(|(_, a)| a.iter())
471 .find_map(|a| match a {
472 Arg::Typed(k, v) if k == "LENGTH_MEASURE" => {
473 v.first().and_then(Arg::number)
474 }
475 _ => None,
476 });
477 let base = inner
478 .parts
479 .iter()
480 .flat_map(|(_, a)| a.iter())
481 .find_map(Arg::reference)
482 .and_then(|b| self.exchange.data.get(&b))
483 .and_then(|u| u.part("SI_UNIT"))
484 .map_or(1000.0, |args| match args.first() {
485 Some(Arg::Enum(p)) if p == "MILLI" => 1.0,
486 Some(Arg::Enum(p)) if p == "CENTI" => 10.0,
487 _ => 1000.0,
488 });
489 if let Some(f) = factor {
490 return f * base;
491 }
492 }
493 }
494 self.report
495 .warnings
496 .push("no length unit found; taking millimetres".to_owned());
497 1.0
498 }
499
500 fn angle_unit_scale(&mut self) -> f64 {
503 let assigned = self.assigned_units();
504 for id in assigned {
505 let Some(instance) = self.exchange.data.get(&id) else {
506 continue;
507 };
508 let instance = instance.clone();
509 if instance.part("PLANE_ANGLE_UNIT").is_none() {
510 continue;
511 }
512 if instance.part("SI_UNIT").is_some() {
513 return 1.0;
514 }
515 if let Some(args) = instance.part("CONVERSION_BASED_UNIT")
516 && let Some(measure) = args.get(1).and_then(Arg::reference)
517 && let Some(inner) = self.exchange.data.get(&measure)
518 {
519 let factor = inner
520 .parts
521 .iter()
522 .flat_map(|(_, a)| a.iter())
523 .find_map(|a| match a {
524 Arg::Typed(k, v) if k == "PLANE_ANGLE_MEASURE" => {
525 v.first().and_then(Arg::number)
526 }
527 _ => None,
528 });
529 if let Some(f) = factor {
530 return f;
531 }
532 }
533 }
534 1.0
535 }
536
537 fn point(&mut self, id: u64) -> OgeomResult<Point> {
540 let args = self.args(id, "CARTESIAN_POINT")?;
541 let Some(coords) = args.get(1).and_then(Arg::list) else {
542 ogeom_bail!(Construction, "#{id}: a point without coordinates");
543 };
544 let scale = self.report.scale_mm;
545 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0) * scale;
546 Ok(Point::new(value(0), value(1), value(2)))
547 }
548
549 fn direction(&mut self, id: u64) -> OgeomResult<Direction> {
550 let args = self.args(id, "DIRECTION")?;
551 let Some(coords) = args.get(1).and_then(Arg::list) else {
552 ogeom_bail!(Construction, "#{id}: a direction without components");
553 };
554 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0);
555 Direction::new(Vector::new(value(0), value(1), value(2)), self.tol)
556 }
557
558 fn frame(&mut self, id: u64) -> OgeomResult<Frame> {
561 let args = self.args(id, "AXIS2_PLACEMENT_3D")?;
562 let origin = args
563 .get(1)
564 .and_then(Arg::reference)
565 .map(|r| self.point(r))
566 .transpose()?
567 .unwrap_or(Point::ORIGIN);
568 let z = args
569 .get(2)
570 .and_then(Arg::reference)
571 .map(|r| self.direction(r))
572 .transpose()?
573 .unwrap_or(Direction::Z);
574 let x = match args.get(3).and_then(Arg::reference) {
575 Some(r) => self.direction(r)?,
576 None => Direction::from_cross(z.vector(), Vector::new(0.31, 0.52, 0.8), self.tol)?,
577 };
578 Frame::new(origin, z, x, self.tol)
579 }
580
581 fn surface(&mut self, id: u64) -> OgeomResult<Option<SurfaceGeometry>> {
582 let (keyword, args) = {
583 let instance = self.instance(id)?;
584 (
585 instance.keyword().to_owned(),
586 instance
587 .parts
588 .first()
589 .map(|(_, a)| a.clone())
590 .unwrap_or_default(),
591 )
592 };
593 let scale = self.report.scale_mm;
594 let radius_arg = |args: &[Arg], i: usize| args.get(i).and_then(Arg::number);
595 {
599 let (base, knots_part, weights) = {
600 let instance = self.instance(id)?;
601 (
602 instance.part("B_SPLINE_SURFACE").map(<[Arg]>::to_vec),
603 instance
604 .part("B_SPLINE_SURFACE_WITH_KNOTS")
605 .map(<[Arg]>::to_vec),
606 instance
607 .part("RATIONAL_B_SPLINE_SURFACE")
608 .map(<[Arg]>::to_vec),
609 )
610 };
611 if let Some(kp) = knots_part {
612 let (degrees, grid_arg, mults_knots) = if let Some(base) = base {
613 (
614 (base.first().cloned(), base.get(1).cloned()),
615 base.get(2).cloned(),
616 (
617 kp.first().cloned(),
618 kp.get(1).cloned(),
619 kp.get(2).cloned(),
620 kp.get(3).cloned(),
621 ),
622 )
623 } else {
624 (
625 (kp.get(1).cloned(), kp.get(2).cloned()),
626 kp.get(3).cloned(),
627 (
628 kp.get(8).cloned(),
629 kp.get(9).cloned(),
630 kp.get(10).cloned(),
631 kp.get(11).cloned(),
632 ),
633 )
634 };
635 return self
636 .bspline_surface(id, degrees, grid_arg, mults_knots, weights, None)
637 .map(Some);
638 }
639 }
640 {
643 let (base, weights, form) = {
644 let instance = self.instance(id)?;
645 (
646 instance.part("B_SPLINE_SURFACE").map(<[Arg]>::to_vec),
647 instance
648 .part("RATIONAL_B_SPLINE_SURFACE")
649 .map(<[Arg]>::to_vec),
650 ["BEZIER_SURFACE", "UNIFORM_SURFACE", "QUASI_UNIFORM_SURFACE"]
651 .into_iter()
652 .find(|k| instance.part(k).is_some()),
653 )
654 };
655 if let (Some(base), Some(form)) = (base, form) {
656 return self
657 .bspline_surface(
658 id,
659 (base.first().cloned(), base.get(1).cloned()),
660 base.get(2).cloned(),
661 (None, None, None, None),
662 weights,
663 Some(form),
664 )
665 .map(Some);
666 }
667 if let Some(form) = ["BEZIER_SURFACE", "UNIFORM_SURFACE", "QUASI_UNIFORM_SURFACE"]
668 .into_iter()
669 .find(|k| *k == keyword)
670 {
671 return self
672 .bspline_surface(
673 id,
674 (args.get(1).cloned(), args.get(2).cloned()),
675 args.get(3).cloned(),
676 (None, None, None, None),
677 None,
678 Some(form),
679 )
680 .map(Some);
681 }
682 }
683 let out = match keyword.as_str() {
684 "PLANE" => {
685 let frame = self.frame(args[1].reference().unwrap_or(0))?;
686 Some(
687 PlaneSurface::over(
688 Plane::new(frame),
689 (-SURFACE_EXTENT, SURFACE_EXTENT),
690 (-SURFACE_EXTENT, SURFACE_EXTENT),
691 )?
692 .into(),
693 )
694 }
695 "CYLINDRICAL_SURFACE" => {
696 let frame = self.frame(args[1].reference().unwrap_or(0))?;
697 let radius = radius_arg(&args, 2).unwrap_or(0.0) * scale;
698 Some(
699 CylinderSurface::new(
700 Cylinder::new(frame, radius, self.tol)?,
701 (-SURFACE_EXTENT, SURFACE_EXTENT),
702 )?
703 .into(),
704 )
705 }
706 "CONICAL_SURFACE" => {
707 let frame = self.frame(args[1].reference().unwrap_or(0))?;
708 let radius = radius_arg(&args, 2).unwrap_or(0.0) * scale;
709 let angle = radius_arg(&args, 3).unwrap_or(0.0) * self.angle_scale;
710 Some(
711 ConeSurface::new(
712 Cone::new(frame, radius, angle, self.tol)?,
713 (-SURFACE_EXTENT, SURFACE_EXTENT),
714 )?
715 .into(),
716 )
717 }
718 "SPHERICAL_SURFACE" => {
719 let frame = self.frame(args[1].reference().unwrap_or(0))?;
720 let radius = radius_arg(&args, 2).unwrap_or(0.0) * scale;
721 Some(SphereSurface::new(Sphere::new(frame, radius, self.tol)?).into())
722 }
723 "TOROIDAL_SURFACE" => {
724 let frame = self.frame(args[1].reference().unwrap_or(0))?;
725 let major = radius_arg(&args, 2).unwrap_or(0.0) * scale;
726 let minor = radius_arg(&args, 3).unwrap_or(0.0) * scale;
727 Some(TorusSurface::new(Torus::new(frame, major, minor, self.tol)?).into())
728 }
729 "SURFACE_OF_LINEAR_EXTRUSION" => {
730 let Some(curve) = self.curve(args[1].reference().unwrap_or(0))? else {
738 self.report.warnings.push(format!(
739 "#{id}: an extrusion's swept curve is not read; its face is skipped"
740 ));
741 return Ok(None);
742 };
743 let vector = self.args(args[2].reference().unwrap_or(0), "VECTOR")?;
744 let direction = self.direction(vector[1].reference().unwrap_or(0))?;
745 let parallel =
749 |axis: Direction| axis.vector().cross(direction.vector()).magnitude() <= 1e-6;
750 match &curve {
751 Curve::Circle(c) if parallel(c.circle().frame().z()) => Some(
752 CylinderSurface::new(
753 Cylinder::new(c.circle().frame(), c.circle().radius(), self.tol)?,
754 (-SURFACE_EXTENT, SURFACE_EXTENT),
755 )?
756 .into(),
757 ),
758 Curve::Line(l) if !parallel(l.axis().direction) => {
759 let axis = l.axis();
760 let normal = Direction::from_cross(
761 axis.direction.vector(),
762 direction.vector(),
763 self.tol,
764 )?;
765 let frame = Frame::new(axis.location, normal, axis.direction, self.tol)?;
766 Some(
767 PlaneSurface::over(
768 Plane::new(frame),
769 (-SURFACE_EXTENT, SURFACE_EXTENT),
770 (-SURFACE_EXTENT, SURFACE_EXTENT),
771 )?
772 .into(),
773 )
774 }
775 _ => Some(
776 ExtrusionSurface::over(
777 curve,
778 direction,
779 (-SURFACE_EXTENT, SURFACE_EXTENT),
780 )?
781 .into(),
782 ),
783 }
784 }
785 "SURFACE_OF_REVOLUTION" => {
786 let Some(curve) = self.curve(args[1].reference().unwrap_or(0))? else {
787 self.report.warnings.push(format!(
788 "#{id}: a revolution's swept curve is not read; its face is skipped"
789 ));
790 return Ok(None);
791 };
792 let placement = self.args(args[2].reference().unwrap_or(0), "AXIS1_PLACEMENT")?;
793 let location = self.point(placement[1].reference().unwrap_or(0))?;
794 let direction = match placement.get(2).and_then(Arg::reference) {
795 Some(r) => self.direction(r)?,
796 None => Direction::Z,
797 };
798 Some(
799 ogeom_geom::RevolutionSurface::new(
800 curve,
801 Axis {
802 location,
803 direction,
804 },
805 core::f64::consts::TAU,
806 )?
807 .into(),
808 )
809 }
810 "OFFSET_SURFACE" => {
811 let Some(basis) = self.surface(args[1].reference().unwrap_or(0))? else {
812 return Ok(None);
813 };
814 let distance = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
815 if distance == 0.0 {
816 return Ok(Some(basis));
817 }
818 let offset = ogeom_geom::OffsetSurface::new(basis, distance)?;
821 Some(match offset.analytic(self.tol)? {
822 Some(analytic) => analytic,
823 None => SurfaceGeometry::Offset(Box::new(offset)),
824 })
825 }
826 "RECTANGULAR_TRIMMED_SURFACE" | "CURVE_BOUNDED_SURFACE" => {
829 self.surface(args[1].reference().unwrap_or(0))?
830 }
831 "DEGENERATE_TOROIDAL_SURFACE" => {
832 let frame = self.frame(args[1].reference().unwrap_or(0))?;
833 let major = radius_arg(&args, 2).unwrap_or(0.0) * scale;
834 let minor = radius_arg(&args, 3).unwrap_or(0.0) * scale;
835 Some(TorusSurface::new(Torus::new(frame, major, minor, self.tol)?).into())
836 }
837 "RECTANGULAR_COMPOSITE_SURFACE" => Some(self.composite_surface(id, &args)?),
838 "SURFACE_REPLICA" => {
839 let Some(parent) = self.surface(args[1].reference().unwrap_or(0))? else {
840 return Ok(None);
841 };
842 let motion = self.transformation_operator(args[2].reference().unwrap_or(0))?;
843 Some(parent.transformed(&motion, self.tol)?)
844 }
845 other => {
846 self.report.warnings.push(format!(
847 "#{id}: surface kind {other} is not read yet; its face is skipped"
848 ));
849 None
850 }
851 };
852 Ok(out)
853 }
854
855 fn expand_knots(mults: Option<Arg>, knots: Option<Arg>) -> Vec<f64> {
857 let mut out = Vec::new();
858 let (Some(Arg::List(mults)), Some(Arg::List(knots))) = (mults, knots) else {
859 return out;
860 };
861 for (m, k) in mults.iter().zip(&knots) {
862 let count = m.number().unwrap_or(1.0);
863 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
864 let count = count as usize;
865 for _ in 0..count {
866 out.push(k.number().unwrap_or(0.0));
867 }
868 }
869 out
870 }
871
872 #[allow(clippy::type_complexity)]
873 fn bspline_surface(
874 &mut self,
875 id: u64,
876 degrees: (Option<Arg>, Option<Arg>),
877 grid_arg: Option<Arg>,
878 mults_knots: (Option<Arg>, Option<Arg>, Option<Arg>, Option<Arg>),
879 weights: Option<Vec<Arg>>,
880 form: Option<&str>,
881 ) -> OgeomResult<SurfaceGeometry> {
882 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
883 let deg = |a: Option<Arg>| a.and_then(|x| x.number()).unwrap_or(1.0) as usize;
884 let (u_degree, v_degree) = (deg(degrees.0), deg(degrees.1));
885 let Some(Arg::List(rows)) = grid_arg else {
886 ogeom_bail!(
887 Construction,
888 "#{id}: a b-spline surface without control points"
889 );
890 };
891 let mut points = Vec::new();
892 let (mut u_count, mut v_count) = (0, 0);
893 for row in &rows {
894 let Some(cells) = row.list() else { continue };
895 u_count += 1;
896 v_count = cells.len();
897 for cell in cells {
898 points.push(self.point(cell.reference().unwrap_or(0))?);
899 }
900 }
901 let (u_raw, v_raw) = match form {
902 Some(form) => (
903 implied_knots(form, u_degree, u_count),
904 implied_knots(form, v_degree, v_count),
905 ),
906 None => (
907 Self::expand_knots(mults_knots.0, mults_knots.2),
908 Self::expand_knots(mults_knots.1, mults_knots.3),
909 ),
910 };
911 let u_knots = KnotVector::new(u_raw, u_degree)?;
912 let v_knots = KnotVector::new(v_raw, v_degree)?;
913 let surface = if let Some(weights) = weights {
914 let flat: Vec<f64> = weights
915 .first()
916 .and_then(Arg::list)
917 .unwrap_or(&[])
918 .iter()
919 .filter_map(Arg::list)
920 .flatten()
921 .filter_map(Arg::number)
922 .collect();
923 let weighted: Vec<ogeom_math::Weighted<Point>> = points
924 .iter()
925 .zip(flat.iter().chain(std::iter::repeat(&1.0)))
926 .map(|(p, w)| ogeom_math::Weighted::new(*p, *w, self.tol))
927 .collect::<OgeomResult<_>>()?;
928 ogeom_geom::BSplineSurface::rational(
929 u_knots,
930 v_knots,
931 ogeom_math::ControlGrid::new(weighted, u_count, v_count)?,
932 )?
933 } else {
934 ogeom_geom::BSplineSurface::new(
935 u_knots,
936 v_knots,
937 &ogeom_math::ControlGrid::new(points, u_count, v_count)?,
938 self.tol,
939 )?
940 };
941 Ok(surface.into())
942 }
943
944 fn curve(&mut self, id: u64) -> OgeomResult<Option<Curve>> {
945 let (keyword, args) = {
946 let instance = self.instance(id)?;
947 (
948 instance.keyword().to_owned(),
949 instance
950 .parts
951 .first()
952 .map(|(_, a)| a.clone())
953 .unwrap_or_default(),
954 )
955 };
956 let scale = self.report.scale_mm;
957 {
958 let (base, kp, weights) = {
959 let instance = self.instance(id)?;
960 (
961 instance.part("B_SPLINE_CURVE").map(<[Arg]>::to_vec),
962 instance
963 .part("B_SPLINE_CURVE_WITH_KNOTS")
964 .map(<[Arg]>::to_vec),
965 instance
966 .part("RATIONAL_B_SPLINE_CURVE")
967 .map(<[Arg]>::to_vec),
968 )
969 };
970 let form = {
975 let instance = self.instance(id)?;
976 ["BEZIER_CURVE", "UNIFORM_CURVE", "QUASI_UNIFORM_CURVE"]
977 .into_iter()
978 .find(|k| instance.part(k).is_some())
979 };
980 if let Some(base) = base
981 && (kp.is_some() || form.is_some())
982 {
983 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
984 let degree = base.first().and_then(Arg::number).unwrap_or(1.0) as usize;
985 let control: Vec<Point> = base
986 .get(1)
987 .and_then(Arg::list)
988 .unwrap_or(&[])
989 .iter()
990 .filter_map(Arg::reference)
991 .map(|r| self.point(r))
992 .collect::<OgeomResult<_>>()?;
993 let knots = match (&kp, form) {
994 (Some(kp), _) => Self::expand_knots(kp.first().cloned(), kp.get(1).cloned()),
995 (None, Some(form)) => implied_knots(form, degree, control.len()),
996 (None, None) => unreachable!("guarded above"),
997 };
998 let knots = KnotVector::new(knots, degree)?;
999 let flat: Vec<f64> = weights
1000 .as_ref()
1001 .and_then(|w| w.first())
1002 .and_then(Arg::list)
1003 .unwrap_or(&[])
1004 .iter()
1005 .filter_map(Arg::number)
1006 .collect();
1007 let weighted: Vec<ogeom_math::Weighted<Point>> = control
1008 .iter()
1009 .zip(flat.iter().chain(std::iter::repeat(&1.0)))
1010 .map(|(p, w)| ogeom_math::Weighted::new(*p, *w, self.tol))
1011 .collect::<OgeomResult<_>>()?;
1012 return Ok(Some(BSplineCurve::rational(knots, weighted)?.into()));
1013 }
1014 }
1015 let out: Option<Curve> = match keyword.as_str() {
1016 "LINE" => {
1017 let through = self.point(args[1].reference().unwrap_or(0))?;
1018 let vector = self.args(args[2].reference().unwrap_or(0), "VECTOR")?;
1022 let direction = self.direction(vector[1].reference().unwrap_or(0))?;
1023 Some(
1024 LineCurve::new(Axis {
1025 location: through,
1026 direction,
1027 })
1028 .into(),
1029 )
1030 }
1031 "CIRCLE" => {
1032 let frame = self.frame(args[1].reference().unwrap_or(0))?;
1033 let radius = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1034 Some(CircleCurve::new(Circle::new(frame, radius, self.tol)?).into())
1035 }
1036 "ELLIPSE" => {
1037 let frame = self.frame(args[1].reference().unwrap_or(0))?;
1038 let a = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1039 let b = args.get(3).and_then(Arg::number).unwrap_or(0.0) * scale;
1040 Some(EllipseCurve::new(Ellipse::new(frame, a, b, self.tol)?).into())
1041 }
1042 "B_SPLINE_CURVE_WITH_KNOTS" => {
1043 let degree = args.get(1).and_then(Arg::number).unwrap_or(0.0);
1044 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1045 let degree = degree as usize;
1046 let control: Vec<Point> = args
1047 .get(2)
1048 .and_then(Arg::list)
1049 .unwrap_or(&[])
1050 .iter()
1051 .filter_map(Arg::reference)
1052 .map(|r| self.point(r))
1053 .collect::<OgeomResult<_>>()?;
1054 let mults = args.get(6).and_then(Arg::list).unwrap_or(&[]).to_vec();
1055 let knots = args.get(7).and_then(Arg::list).unwrap_or(&[]).to_vec();
1056 let mut expanded = Vec::new();
1057 for (m, k) in mults.iter().zip(&knots) {
1058 let count = m.number().unwrap_or(1.0);
1059 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1060 let count = count as usize;
1061 for _ in 0..count {
1062 expanded.push(k.number().unwrap_or(0.0));
1063 }
1064 }
1065 Some(
1066 BSplineCurve::new(KnotVector::new(expanded, degree)?, control, self.tol)?
1067 .into(),
1068 )
1069 }
1070 "SURFACE_CURVE" | "SEAM_CURVE" | "INTERSECTION_CURVE" => {
1071 self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1077 }
1078 "BEZIER_CURVE" | "UNIFORM_CURVE" | "QUASI_UNIFORM_CURVE" => {
1079 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1080 let degree = args.get(1).and_then(Arg::number).unwrap_or(1.0) as usize;
1081 let control: Vec<Point> = args
1082 .get(2)
1083 .and_then(Arg::list)
1084 .unwrap_or(&[])
1085 .iter()
1086 .filter_map(Arg::reference)
1087 .map(|r| self.point(r))
1088 .collect::<OgeomResult<_>>()?;
1089 let knots = implied_knots(&keyword, degree, control.len());
1090 Some(BSplineCurve::new(KnotVector::new(knots, degree)?, control, self.tol)?.into())
1091 }
1092 "HYPERBOLA" => {
1093 let frame = self.frame(args[1].reference().unwrap_or(0))?;
1094 let a = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1095 let b = args.get(3).and_then(Arg::number).unwrap_or(0.0) * scale;
1096 Some(HyperbolaCurve::new(Hyperbola::new(frame, a, b, self.tol)?, 20.0)?.into())
1099 }
1100 "PARABOLA" => {
1101 let frame = self.frame(args[1].reference().unwrap_or(0))?;
1102 let focal = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1103 Some(
1104 ParabolaCurve::new(Parabola::new(frame, focal, self.tol)?, SURFACE_EXTENT)?
1105 .into(),
1106 )
1107 }
1108 "TRIMMED_CURVE" => {
1109 self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1111 }
1112 "OFFSET_CURVE_3D" => {
1113 let Some(basis) = self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1114 else {
1115 return Ok(None);
1116 };
1117 let distance = args.get(2).and_then(Arg::number).unwrap_or(0.0) * scale;
1118 let reference =
1119 self.direction(args.get(4).and_then(Arg::reference).unwrap_or(0))?;
1120 Some(Curve::Offset(Box::new(ogeom_geom::OffsetCurve::new(
1121 basis, distance, reference,
1122 )?)))
1123 }
1124 "POLYLINE" => {
1125 let points: Vec<Point> = args
1126 .get(1)
1127 .and_then(Arg::list)
1128 .unwrap_or(&[])
1129 .iter()
1130 .filter_map(Arg::reference)
1131 .map(|r| self.point(r))
1132 .collect::<OgeomResult<_>>()?;
1133 if points.len() < 2 {
1134 ogeom_bail!(Construction, "#{id}: a polyline of fewer than two points");
1135 }
1136 let mut knots = vec![0.0];
1138 #[allow(clippy::cast_precision_loss)]
1139 knots.extend((0..points.len()).map(|i| i as f64));
1140 #[allow(clippy::cast_precision_loss)]
1141 knots.push((points.len() - 1) as f64);
1142 Some(BSplineCurve::new(KnotVector::new(knots, 1)?, points, self.tol)?.into())
1143 }
1144 "COMPOSITE_CURVE" => {
1145 let mut joined: Option<(KnotVector, Vec<ogeom_math::Weighted<Point>>)> = None;
1148 let segments: Vec<u64> = args
1149 .get(1)
1150 .and_then(Arg::list)
1151 .unwrap_or(&[])
1152 .iter()
1153 .filter_map(Arg::reference)
1154 .collect();
1155 for segment in segments {
1156 let sargs = self.args(segment, "COMPOSITE_CURVE_SEGMENT")?;
1157 let same_sense = !sargs.get(1).is_some_and(|a| a.is_enum("F"));
1158 let parent = sargs.get(2).and_then(Arg::reference).unwrap_or(0);
1159 let Some((curve, range)) = self.bounded_curve(parent)? else {
1160 return Ok(None);
1161 };
1162 let mut spline = curve.to_bspline_over(range, self.tol)?;
1163 if !same_sense {
1164 let (knots, control) =
1165 ogeom_math::bspline::reverse(spline.knots(), spline.control_points());
1166 spline = BSplineCurve::rational(knots, control)?;
1167 }
1168 joined = Some(match joined {
1169 None => (spline.knots().clone(), spline.control_points().to_vec()),
1170 Some(held) => join_splines(held, &spline, self.tol)?,
1171 });
1172 }
1173 let Some((knots, control)) = joined else {
1174 ogeom_bail!(Construction, "#{id}: a composite curve with no segments");
1175 };
1176 Some(BSplineCurve::rational(knots, control)?.into())
1177 }
1178 "CURVE_REPLICA" => {
1179 let Some(parent) = self.curve(args.get(1).and_then(Arg::reference).unwrap_or(0))?
1180 else {
1181 return Ok(None);
1182 };
1183 let motion = self
1184 .transformation_operator(args.get(2).and_then(Arg::reference).unwrap_or(0))?;
1185 Some(parent.transformed(&motion, self.tol)?)
1186 }
1187 other => {
1188 self.report.warnings.push(format!(
1189 "#{id}: curve kind {other} is not read yet; its edge is skipped"
1190 ));
1191 None
1192 }
1193 };
1194 Ok(out)
1195 }
1196
1197 fn composite_surface(&mut self, id: u64, args: &[Arg]) -> OgeomResult<SurfaceGeometry> {
1205 let rows: Vec<Vec<u64>> = args
1206 .get(1)
1207 .and_then(Arg::list)
1208 .unwrap_or(&[])
1209 .iter()
1210 .map(|row| {
1211 row.list()
1212 .unwrap_or(&[])
1213 .iter()
1214 .filter_map(Arg::reference)
1215 .collect()
1216 })
1217 .collect();
1218 if rows.is_empty()
1219 || rows
1220 .iter()
1221 .any(|r| r.len() != rows[0].len() || r.is_empty())
1222 {
1223 ogeom_bail!(
1224 Construction,
1225 "#{id}: a composite surface's grid is not rectangular"
1226 );
1227 }
1228 let mut grid: Vec<Vec<Patch>> = Vec::with_capacity(rows.len());
1229 for row in &rows {
1230 let mut out = Vec::with_capacity(row.len());
1231 for &patch in row {
1232 let pargs = self.args(patch, "SURFACE_PATCH")?;
1233 let parent = pargs.get(1).and_then(Arg::reference).unwrap_or(0);
1234 let u_sense = !pargs.get(4).is_some_and(|a| a.is_enum("F"));
1235 let v_sense = !pargs.get(5).is_some_and(|a| a.is_enum("F"));
1236 let spline = self.bounded_surface_spline(parent)?;
1237 let mut p = Patch::of(&spline)?;
1238 if !u_sense {
1239 p = p.reversed_u();
1240 }
1241 if !v_sense {
1242 p = p.transposed().reversed_u().transposed();
1243 }
1244 out.push(p);
1245 }
1246 grid.push(out);
1247 }
1248 join_patches(grid, self.tol).map_err(|e| {
1249 ogeom_core::ogeom_err!(
1250 Construction,
1251 "#{id}: a composite surface's patches do not join: {e}"
1252 )
1253 })
1254 }
1255
1256 fn bounded_surface_spline(&mut self, id: u64) -> OgeomResult<ogeom_geom::BSplineSurface> {
1260 let keyword = self.instance(id)?.keyword().to_owned();
1261 if keyword == "RECTANGULAR_TRIMMED_SURFACE" {
1262 let args = self.args(id, "RECTANGULAR_TRIMMED_SURFACE")?;
1263 let basis_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
1264 let Some(basis) = self.surface(basis_id)? else {
1265 ogeom_bail!(Construction, "#{id}: a patch's basis is not read");
1266 };
1267 let angular = matches!(
1268 self.instance(basis_id)?.keyword(),
1269 "CYLINDRICAL_SURFACE"
1270 | "CONICAL_SURFACE"
1271 | "SPHERICAL_SURFACE"
1272 | "TOROIDAL_SURFACE"
1273 );
1274 let n = |i: usize| args.get(i).and_then(Arg::number).unwrap_or(0.0);
1275 let (u_scale, v_scale) = if angular {
1276 (self.angle_scale, self.report.scale_mm)
1277 } else {
1278 (self.report.scale_mm, self.report.scale_mm)
1279 };
1280 let (u1, u2) = (n(2) * u_scale, n(3) * u_scale);
1281 let (v1, v2) = (n(4) * v_scale, n(5) * v_scale);
1282 let window = ((u1.min(u2), u1.max(u2)), (v1.min(v2), v1.max(v2)));
1283 let trimmed = ogeom_geom::TrimmedSurface::new(basis, window.0, window.1, self.tol)?;
1284 return SurfaceGeometry::Trimmed(Box::new(trimmed)).to_bspline(self.tol);
1285 }
1286 let Some(surface) = self.surface(id)? else {
1287 ogeom_bail!(Construction, "#{id}: a patch's surface is not read");
1288 };
1289 surface.to_bspline(self.tol)
1290 }
1291
1292 fn bounded_curve(&mut self, id: u64) -> OgeomResult<Option<(Curve, (f64, f64))>> {
1297 let keyword = self.instance(id)?.keyword().to_owned();
1298 if keyword != "TRIMMED_CURVE" {
1299 let Some(curve) = self.curve(id)? else {
1300 return Ok(None);
1301 };
1302 let (lo, hi) = curve.domain();
1303 if !lo.is_finite() || !hi.is_finite() || hi - lo > SURFACE_EXTENT {
1304 ogeom_bail!(
1305 Construction,
1306 "#{id}: an unbounded curve cannot stand as a composite's segment"
1307 );
1308 }
1309 return Ok(Some((curve, (lo, hi))));
1310 }
1311 let args = self.args(id, "TRIMMED_CURVE")?;
1312 let basis_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
1313 let Some(basis) = self.curve(basis_id)? else {
1314 return Ok(None);
1315 };
1316 let basis_keyword = self.instance(basis_id)?.keyword().to_owned();
1317 let forward = !args.get(4).is_some_and(|a| a.is_enum("F"));
1318 let mut ends = Vec::with_capacity(2);
1319 for trim in [args.get(2), args.get(3)] {
1320 let items = trim.and_then(Arg::list).unwrap_or(&[]).to_vec();
1321 let mut at: Option<Point> = None;
1322 for item in &items {
1323 if let Some(r) = item.reference() {
1324 at = Some(self.point(r)?);
1325 }
1326 }
1327 if at.is_none() {
1328 for item in &items {
1329 if let Arg::Typed(name, values) = item
1330 && name == "PARAMETER_VALUE"
1331 && let Some(v) = values.first().and_then(Arg::number)
1332 {
1333 at =
1334 Some(self.step_parameter_point(&basis, &basis_keyword, basis_id, v)?);
1335 }
1336 }
1337 }
1338 let Some(point) = at else {
1339 ogeom_bail!(
1340 Construction,
1341 "#{id}: a trim names neither point nor parameter"
1342 );
1343 };
1344 let t = match self.parameter_of(&basis, point) {
1345 Some(t) => t,
1346 None => project_on_curve(&basis, point, 256, self.tol)?.parameter,
1347 };
1348 ends.push(t);
1349 }
1350 let (mut a, mut b) = (ends[0], ends[1]);
1351 if !forward {
1352 core::mem::swap(&mut a, &mut b);
1353 }
1354 if basis.is_periodic() && b <= a {
1355 let (lo, hi) = basis.domain();
1356 b += hi - lo;
1357 }
1358 let range = if a <= b { (a, b) } else { (b, a) };
1359 let mut curve = basis;
1360 if !forward || a > b {
1361 let spline = curve.to_bspline_over(range, self.tol)?;
1363 let (knots, control) =
1364 ogeom_math::bspline::reverse(spline.knots(), spline.control_points());
1365 curve = BSplineCurve::rational(knots, control)?.into();
1366 let domain = curve.domain();
1367 return Ok(Some((curve, domain)));
1368 }
1369 Ok(Some((curve, range)))
1370 }
1371
1372 fn step_parameter_point(
1376 &mut self,
1377 basis: &Curve,
1378 keyword: &str,
1379 basis_id: u64,
1380 v: f64,
1381 ) -> OgeomResult<Point> {
1382 match keyword {
1383 "LINE" => {
1384 let args = self.args(basis_id, "LINE")?;
1385 let through = self.point(args[1].reference().unwrap_or(0))?;
1386 let vector = self.args(args[2].reference().unwrap_or(0), "VECTOR")?;
1387 let direction = self.direction(vector[1].reference().unwrap_or(0))?;
1388 let magnitude = vector.get(2).and_then(Arg::number).unwrap_or(1.0);
1389 Ok(through + direction.vector() * (v * magnitude * self.report.scale_mm))
1390 }
1391 "CIRCLE" | "ELLIPSE" => basis.point_at(v * self.angle_scale, self.tol),
1392 _ => basis.point_at(v, self.tol),
1393 }
1394 }
1395
1396 fn transformation_operator(&mut self, id: u64) -> OgeomResult<Transform> {
1400 let args = self.args(id, "CARTESIAN_TRANSFORMATION_OPERATOR_3D")?;
1401 let direction_at = |this: &mut Self, i: usize| -> OgeomResult<Option<Vector>> {
1402 match args.get(i).and_then(Arg::reference) {
1403 Some(r) => Ok(Some(this.direction(r)?.vector())),
1404 None => Ok(None),
1405 }
1406 };
1407 let (axis1, axis2, axis3) = (
1408 direction_at(self, 1)?,
1409 direction_at(self, 2)?,
1410 direction_at(self, 5)?,
1411 );
1412 let origin = self.point(args.get(3).and_then(Arg::reference).unwrap_or(0))?;
1413 let scale = args.get(4).and_then(Arg::number).unwrap_or(1.0);
1414 let z = match (axis3, axis1, axis2) {
1415 (Some(z), _, _) => z,
1416 (None, Some(x), Some(y)) => x.cross(y),
1417 _ => Vector::Z,
1418 };
1419 let z = Direction::new(z, self.tol)?.vector();
1420 let x = match axis1 {
1421 Some(x) => x - z * x.dot(z),
1422 None => {
1423 let seed = if z.x.abs() < 0.9 {
1424 Vector::X
1425 } else {
1426 Vector::Y
1427 };
1428 seed - z * seed.dot(z)
1429 }
1430 };
1431 let x = Direction::new(x, self.tol)?.vector();
1432 let y = z.cross(x);
1433 let linear = Matrix3::new([[x.x, y.x, z.x], [x.y, y.y, z.y], [x.z, y.z, z.z]]);
1434 Transform::from_parts(linear, scale, origin.to_vector(), self.tol.angular())
1435 }
1436
1437 fn parameter_of(&self, curve: &Curve, p: Point) -> Option<f64> {
1439 crate::inversion::parameter_on(curve, p)
1440 }
1441
1442 fn vertex(&mut self, id: u64) -> OgeomResult<Shape> {
1445 if let Some(shape) = self.vertices.get(&id) {
1446 return Ok(shape.clone());
1447 }
1448 let args = self.args(id, "VERTEX_POINT")?;
1449 let point = self.point(args[1].reference().unwrap_or(0))?;
1450 let shape = make_vertex(&mut self.model, point).shape;
1451 self.vertices.insert(id, shape.clone());
1452 Ok(shape)
1453 }
1454
1455 fn edge(&mut self, id: u64) -> OgeomResult<Option<BuiltEdge>> {
1461 if let Some(found) = self.edges.get(&id) {
1462 return Ok(Some(found.clone()));
1463 }
1464 let args = self.args(id, "EDGE_CURVE")?;
1465 let v1 = args[1].reference().unwrap_or(0);
1466 let v2 = args[2].reference().unwrap_or(0);
1467 let Some(mut curve) = self.curve(args[3].reference().unwrap_or(0))? else {
1468 return Ok(None);
1469 };
1470 let same_sense = !args.get(4).is_some_and(|a| a.is_enum("F"));
1471
1472 let p1 = {
1473 let vargs = self.args(v1, "VERTEX_POINT")?;
1474 self.point(vargs[1].reference().unwrap_or(0))?
1475 };
1476 let p2 = {
1477 let vargs = self.args(v2, "VERTEX_POINT")?;
1478 self.point(vargs[1].reference().unwrap_or(0))?
1479 };
1480
1481 let (start, end, mut flipped) = if same_sense {
1485 (p1, p2, false)
1486 } else {
1487 (p2, p1, true)
1488 };
1489 let (range, closed) = match (
1490 self.parameter_of(&curve, start),
1491 self.parameter_of(&curve, end),
1492 ) {
1493 (Some(a), Some(b)) => {
1494 let period = if curve.is_periodic() {
1495 let (lo, hi) = curve.domain();
1496 hi - lo
1497 } else {
1498 0.0
1499 };
1500 if v1 == v2 {
1501 ((a, a + if period > 0.0 { period } else { 0.0 }), true)
1502 } else if period > 0.0 && b <= a + self.tol.parametric() {
1503 ((a, b + period), false)
1504 } else if b < a - self.tol.parametric() {
1505 flipped = !flipped;
1511 self.report.warnings.push(format!(
1512 "#{id}: the edge's vertices run against its curve's \
1513 parameter despite its sense flag; the edge was reversed"
1514 ));
1515 self.tally("edge-against-curve", a - b, id);
1516 ((b, a), false)
1517 } else {
1518 ((a, b), false)
1519 }
1520 }
1521 _ => {
1522 if v1 == v2
1533 && let Curve::BSpline(spline) = &curve
1534 {
1535 let (lo, hi) = curve.domain();
1536 let gap = curve.point_at(lo, self.tol)?.distance(start);
1537 if gap > self.tol.confusion() * 10.0 {
1538 let samples = (spline.control_points().len() * 8).max(64);
1539 if let Ok(found) = project_on_curve(&curve, start, samples, self.tol)
1540 && found.distance <= self.tol.confusion() * 1e4
1541 && found.parameter > lo + self.tol.parametric()
1542 && found.parameter < hi - self.tol.parametric()
1543 && let Ok(moved) = spline.reseamed_at(found.parameter, self.tol)
1544 {
1545 curve = Curve::BSpline(moved);
1546 self.report.warnings.push(format!(
1547 "#{id}: a closed edge's vertex sits {gap:.2e} along its \
1548 curve from the seam; the seam was moved to the vertex"
1549 ));
1550 self.tally("reseamed", gap, id);
1551 }
1552 }
1553 }
1554 let (lo, hi) = curve.domain();
1555 let head = curve.point_at(lo, self.tol)?;
1556 let tail = curve.point_at(hi, self.tol)?;
1557 let (head_miss, tail_miss) = (head.distance(start), tail.distance(end));
1558 if head_miss > self.tol.confusion() * 10.0
1559 || tail_miss > self.tol.confusion() * 10.0
1560 {
1561 let window = if v1 == v2 {
1569 None
1570 } else {
1571 window_between_feet(&curve, start, end, self.tol)
1572 };
1573 if let Some(window) = window {
1574 self.report.warnings.push(format!(
1575 "#{id}: edge endpoints sit {head_miss:.2e} and {tail_miss:.2e} \
1576 from its curve's ends; the window between the vertices' \
1577 feet on the curve was taken"
1578 ));
1579 self.tally("vertex-window", head_miss.max(tail_miss), id);
1580 (window, false)
1581 } else {
1582 self.report.warnings.push(format!(
1583 "#{id}: edge endpoints sit {head_miss:.2e} and {tail_miss:.2e} \
1584 from its curve's ends; the curve's own domain was taken"
1585 ));
1586 ((lo, hi), v1 == v2)
1587 }
1588 } else {
1589 ((lo, hi), v1 == v2)
1590 }
1591 }
1592 };
1593 let _ = closed;
1594
1595 let (vlo, vhi) = if flipped {
1596 (self.vertex(v2)?, self.vertex(v1)?)
1597 } else {
1598 (self.vertex(v1)?, self.vertex(v2)?)
1599 };
1600 for (vertex, t) in [(&vlo, range.0), (&vhi, range.1)] {
1606 let end = curve.point_at(t, self.tol)?;
1607 let stated = {
1608 let Some(node) = self.model.node(vertex) else {
1609 continue;
1610 };
1611 let Some(data) = node.data().as_vertex() else {
1612 continue;
1613 };
1614 (data.point, data.tolerance)
1615 };
1616 let gap = end.distance(stated.0);
1617 if gap > stated.1.get() {
1618 if let Some(node) = self.model.node_mut(vertex)
1619 && let ogeom_topo::NodeData::Vertex(data) = node.data_mut()
1620 {
1621 data.tolerance = data.tolerance.widen_to(gap + self.tol.confusion());
1622 }
1623 self.warn_vertex_miss(id, gap);
1624 }
1625 }
1626 let shape =
1627 make_edge_between(&mut self.model, curve.clone(), range, &vlo, &vhi, self.tol)?.shape;
1628 let entry = (shape, curve, range, flipped);
1629 self.edges.insert(id, entry.clone());
1630 Ok(Some(entry))
1631 }
1632
1633 fn face(&mut self, id: u64) -> OgeomResult<Option<Shape>> {
1634 if let Some(shape) = self.faces.get(&id) {
1635 return Ok(Some(shape.clone()));
1636 }
1637 let args = self.face_args(id)?;
1638 let bounds: Vec<u64> = args
1639 .get(1)
1640 .and_then(Arg::list)
1641 .unwrap_or(&[])
1642 .iter()
1643 .filter_map(Arg::reference)
1644 .collect();
1645 let Some(surface) = self.surface(args[2].reference().unwrap_or(0))? else {
1646 return Ok(None);
1647 };
1648 let face_forward = !args.get(3).is_some_and(|a| a.is_enum("F"));
1649 let surface_id = self.model.geometry_mut().add_surface(surface.clone());
1650
1651 let mut ordered = bounds.clone();
1653 ordered.sort_by_key(|b| {
1654 self.exchange
1655 .data
1656 .get(b)
1657 .map_or(1, |i| i32::from(i.part("FACE_OUTER_BOUND").is_none()))
1658 });
1659
1660 let mut edge_uses: HashMap<u64, usize> = HashMap::new();
1663 for &bound in &ordered {
1664 let bargs = self.bound_args(bound)?;
1665 if self.instance(bargs.0)?.part("VERTEX_LOOP").is_some() {
1666 continue;
1667 }
1668 let loop_args = self.args(bargs.0, "EDGE_LOOP")?;
1669 for oe in loop_args.get(1).and_then(Arg::list).unwrap_or(&[]) {
1670 if let Some(oe_id) = oe.reference() {
1671 let oargs = self.args(oe_id, "ORIENTED_EDGE")?;
1672 if let Some(e) = oargs.get(3).and_then(Arg::reference) {
1673 *edge_uses.entry(e).or_default() += 1;
1674 }
1675 }
1676 }
1677 }
1678
1679 let mut wires = Vec::new();
1680 let mut annotated: HashSet<u64> = HashSet::new();
1681 for &bound in &ordered {
1682 let (loop_id, bound_forward) = self.bound_args(bound)?;
1683 if let Some(vertex_loop) = {
1684 let instance = self.instance(loop_id)?;
1685 instance.part("VERTEX_LOOP").map(<[Arg]>::to_vec)
1686 } {
1687 let vertex_id = vertex_loop.get(1).and_then(Arg::reference).unwrap_or(0);
1693 let vargs = self.args(vertex_id, "VERTEX_POINT")?;
1694 let at = self.point(vargs[1].reference().unwrap_or(0))?;
1695 let vertex = self.vertex(vertex_id)?;
1696 let projection = ogeom_algo::project_on_surface(&surface, at, 32, self.tol)?;
1697 let ((ua, ub), _) = surface.domain();
1698 let row = projection.parameters.1;
1699 let mut data = ogeom_topo::EdgeData::new();
1700 data.degenerate = true;
1701 let edge = self
1702 .model
1703 .add_edge(data, &[vertex.clone(), vertex.clone()])?;
1704 let pcurve: PlanarCurve = ogeom_geom::Line2d::segment(
1705 ogeom_math::Point2::new(ua, row),
1706 ogeom_math::Point2::new(ub, row),
1707 self.tol,
1708 )?
1709 .into();
1710 ogeom_algo::attach_pcurve(
1711 &mut self.model,
1712 &edge,
1713 pcurve,
1714 surface_id,
1715 Location::identity(),
1716 (0.0, ub - ua),
1717 )?;
1718 wires.push(make_wire(&mut self.model, &[edge], self.tol)?.shape);
1719 continue;
1720 }
1721 let loop_args = self.args(loop_id, "EDGE_LOOP")?;
1722 let mut uses: Vec<BoundUse> = Vec::new();
1723 for oe in loop_args.get(1).and_then(Arg::list).unwrap_or(&[]) {
1724 let Some(oe_id) = oe.reference() else {
1725 continue;
1726 };
1727 let oargs = self.args(oe_id, "ORIENTED_EDGE")?;
1728 let Some(edge_id) = oargs.get(3).and_then(Arg::reference) else {
1729 continue;
1730 };
1731 let forward = !oargs.get(4).is_some_and(|a| a.is_enum("F"));
1732 let Some((shape, curve, range, flipped)) = self.edge(edge_id)? else {
1733 self.report.warnings.push(format!(
1734 "#{id}: a bound references unreadable edge #{edge_id}; \
1735 the face is skipped"
1736 ));
1737 return Ok(None);
1738 };
1739 let mut use_forward = forward == bound_forward;
1742 if flipped {
1743 use_forward = !use_forward;
1744 }
1745 let placed = if use_forward {
1746 shape.clone()
1747 } else {
1748 shape.reversed()
1749 };
1750 uses.push((placed, shape, edge_id, curve, range));
1751 }
1752 if !bound_forward {
1753 uses.reverse();
1754 }
1755 self.widen_window(surface_id, &uses)?;
1758 let widened = self
1759 .model
1760 .geometry()
1761 .surface(surface_id)
1762 .cloned()
1763 .unwrap_or_else(|| surface.clone());
1764 self.chart_bound(id, &uses, &widened, surface_id, &edge_uses, &mut annotated)?;
1765 let edges: Vec<Shape> = uses.into_iter().map(|(placed, ..)| placed).collect();
1766 if edges.is_empty() {
1767 continue;
1768 }
1769 wires.push(make_wire(&mut self.model, &edges, self.tol)?.shape);
1770 }
1771 if wires.is_empty() {
1772 self.report
1773 .warnings
1774 .push(format!("#{id}: a face with no readable bounds is skipped"));
1775 return Ok(None);
1776 }
1777 let surface = self
1781 .model
1782 .geometry()
1783 .surface(surface_id)
1784 .cloned()
1785 .unwrap_or(surface);
1786
1787 if wires.len() == 2
1793 && surface.is_periodic_u()
1794 && let [(e_lo, _v_lo), (e_hi, _v_hi)] =
1795 closed_ring_edges(&self.model, &wires)?.as_slice()
1796 && ogeom_algo::rings_are_parallels(&self.model, &surface, &[e_lo, e_hi], self.tol)?
1802 {
1803 {
1804 match ogeom_algo::make_revolution_band(
1810 &mut self.model,
1811 &surface,
1812 e_lo,
1813 e_hi,
1814 self.tol,
1815 ) {
1816 Ok(built) => {
1817 let shape = if face_forward {
1818 built
1819 } else {
1820 built.reversed()
1821 };
1822 self.faces.insert(id, shape.clone());
1823 return Ok(Some(shape));
1824 }
1825 Err(e) => {
1826 self.report.warnings.push(format!(
1827 "#{id}: no seam could be synthesised ({e}); the \
1828 face may not triangulate"
1829 ));
1830 }
1831 }
1832 }
1833 }
1834
1835 if wires.len() == 1
1841 && matches!(surface, SurfaceGeometry::Cone(_))
1842 && let [(ring, _)] = closed_ring_edges(&self.model, &wires)?.as_slice()
1843 {
1844 match ogeom_algo::make_apex_band(&mut self.model, &surface, ring, self.tol) {
1845 Ok(built) => {
1846 let shape = if face_forward {
1847 built
1848 } else {
1849 built.reversed()
1850 };
1851 self.faces.insert(id, shape.clone());
1852 return Ok(Some(shape));
1853 }
1854 Err(e) => {
1855 self.report.warnings.push(format!(
1856 "#{id}: no apex could be synthesised ({e}); the face \
1857 may not triangulate"
1858 ));
1859 }
1860 }
1861 }
1862
1863 ogeom_algo::chain_wire_branches(&mut self.model, surface_id, &wires, self.tol)?;
1867 let built = make_face_on(&mut self.model, surface_id, &wires, self.tol)?.shape;
1868 let shape = if face_forward {
1869 built
1870 } else {
1871 built.reversed()
1872 };
1873 self.faces.insert(id, shape.clone());
1874 Ok(Some(shape))
1875 }
1876
1877 fn bound_args(&mut self, id: u64) -> OgeomResult<(u64, bool)> {
1879 let instance = self.instance(id)?;
1880 let args = instance
1881 .part("FACE_OUTER_BOUND")
1882 .or_else(|| instance.part("FACE_BOUND"))
1883 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "#{id} is not a face bound"))?
1884 .to_vec();
1885 let loop_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
1886 let forward = !args.get(2).is_some_and(|a| a.is_enum("F"));
1887 Ok((loop_id, forward))
1888 }
1889
1890 #[allow(clippy::too_many_arguments)]
1892 fn widen_window(
1910 &mut self,
1911 surface_id: ogeom_topo::SurfaceId,
1912 uses: &[BoundUse],
1913 ) -> OgeomResult<()> {
1914 use ogeom_geom::Curve3d as _;
1915 let Some(surface) = self.model.geometry().surface(surface_id).cloned() else {
1916 return Ok(());
1917 };
1918 let along: Box<ChartReading> = match &surface {
1921 SurfaceGeometry::Plane(p) => {
1922 let frame = p.plane().frame();
1923 let (origin, x, y) = (frame.origin(), frame.x().vector(), frame.y().vector());
1924 Box::new(move |at: Point| (Some((at - origin).dot(x)), Some((at - origin).dot(y))))
1925 }
1926 SurfaceGeometry::Cylinder(c) => {
1927 let axis = c.cylinder().axis();
1928 let (origin, direction) = (axis.location, axis.direction.vector());
1929 Box::new(move |at: Point| (None, Some((at - origin).dot(direction))))
1930 }
1931 SurfaceGeometry::Cone(c) => {
1932 let axis = c.cone().axis();
1933 let (origin, direction) = (axis.location, axis.direction.vector());
1934 Box::new(move |at: Point| (None, Some((at - origin).dot(direction))))
1935 }
1936 SurfaceGeometry::Extrusion(e) => {
1941 let direction = e.direction().vector();
1942 let (lo, hi) = e.curve().domain();
1943 let (mut least, mut most) = (f64::INFINITY, f64::NEG_INFINITY);
1944 for k in 0..=32 {
1945 let t = lo + (hi - lo) * f64::from(k) / 32.0;
1946 if let Ok(p) = e.curve().point_at(t, self.tol) {
1947 let reach = p.to_vector().dot(direction);
1948 least = least.min(reach);
1949 most = most.max(reach);
1950 }
1951 }
1952 let middle = f64::midpoint(least, most);
1953 Box::new(move |at: Point| (None, Some(at.to_vector().dot(direction) - middle)))
1954 }
1955 _ => return Ok(()),
1956 };
1957 const STATIONS: usize = 4;
1958 let (mut ua, mut ub) = (-SURFACE_EXTENT, SURFACE_EXTENT);
1959 let (mut va, mut vb) = (-SURFACE_EXTENT, SURFACE_EXTENT);
1960 for (_, _, _, curve, range) in uses {
1961 for step in 0..=STATIONS {
1962 #[allow(clippy::cast_precision_loss)]
1963 let t = range.0 + (range.1 - range.0) * (step as f64 / STATIONS as f64);
1964 let Ok(at) = curve.point_at(t, self.tol) else {
1965 continue;
1966 };
1967 let (u, v) = along(at);
1968 if let Some(u) = u {
1969 ua = ua.min(u);
1970 ub = ub.max(u);
1971 }
1972 if let Some(v) = v {
1973 va = va.min(v);
1974 vb = vb.max(v);
1975 }
1976 }
1977 }
1978 let ((was_u, was_v), grown) = (surface.domain(), (ub - ua) * 0.05);
1979 if ua >= was_u.0 && ub <= was_u.1 && va >= was_v.0 && vb <= was_v.1 {
1980 return Ok(());
1981 }
1982 let (u, v) = (
1983 (ua.min(was_u.0) - grown, ub.max(was_u.1) + grown),
1984 (va.min(was_v.0) - grown, vb.max(was_v.1) + grown),
1985 );
1986 let wider = match surface {
1987 SurfaceGeometry::Plane(p) => PlaneSurface::over(p.plane(), u, v)?.into(),
1988 SurfaceGeometry::Cylinder(c) => CylinderSurface::new(c.cylinder(), v)?.into(),
1989 SurfaceGeometry::Cone(c) => ConeSurface::new(c.cone(), v)?.into(),
1990 SurfaceGeometry::Extrusion(e) => {
1991 ExtrusionSurface::over(e.curve().clone(), e.direction(), v)?.into()
1992 }
1993 other => other,
1994 };
1995 if let Some(held) = self.model.geometry_mut().surface_mut(surface_id) {
1996 *held = wider;
1997 }
1998 Ok(())
1999 }
2000
2001 fn chart_bound(
2019 &mut self,
2020 face_id: u64,
2021 uses: &[BoundUse],
2022 surface: &SurfaceGeometry,
2023 surface_id: ogeom_topo::SurfaceId,
2024 edge_uses: &HashMap<u64, usize>,
2025 annotated: &mut HashSet<u64>,
2026 ) -> OgeomResult<()> {
2027 use ogeom_geom::Curve2d as _;
2028 let mut images: HashMap<u64, PlanarCurve> = HashMap::new();
2029 let mut sides: HashMap<u64, (Option<PlanarCurve>, Option<PlanarCurve>)> = HashMap::new();
2030 let mut order: Vec<u64> = Vec::new();
2031 let mut previous: Option<ogeom_math::Point2> = None;
2032 for (placed, shape, edge_id, curve, range) in uses {
2033 if !images.contains_key(edge_id) {
2034 let Some(image) =
2035 self.image_for(face_id, *edge_id, shape, curve, *range, surface)?
2036 else {
2037 continue;
2038 };
2039 images.insert(*edge_id, image);
2040 order.push(*edge_id);
2041 }
2042 let Some(image) = images.get(edge_id).cloned() else {
2043 continue;
2044 };
2045 let backwards = placed.orientation() == ogeom_topo::Orientation::Reversed;
2046 let (start, end) = if backwards {
2047 (range.1, range.0)
2048 } else {
2049 (range.0, range.1)
2050 };
2051 let image =
2052 crate::pcurves::shifted_to_meet(&image, start, previous, surface, self.tol)?;
2053 previous = Some(image.point_at(end, self.tol)?);
2054 let walked = sides.entry(*edge_id).or_default();
2055 if backwards {
2056 walked.1 = Some(image);
2057 } else {
2058 walked.0 = Some(image);
2059 }
2060 }
2061 for edge_id in order {
2062 if !annotated.insert(edge_id) {
2063 continue;
2064 }
2065 let Some((shape, range)) = uses
2066 .iter()
2067 .find(|(_, _, id, _, _)| *id == edge_id)
2068 .map(|(_, shape, _, _, range)| (shape.clone(), *range))
2069 else {
2070 continue;
2071 };
2072 let Some(image) = images.get(&edge_id).cloned() else {
2073 continue;
2074 };
2075 let walked = sides.remove(&edge_id).unwrap_or((None, None));
2076 let seam = edge_uses.get(&edge_id).copied().unwrap_or(1) > 1;
2077 let columns = match &walked {
2078 (Some(forward), Some(reversed)) => {
2079 let at = forward.point_at(range.0, self.tol)?;
2080 let other = reversed.point_at(range.0, self.tol)?;
2081 (at.distance(other) > self.tol.confusion())
2082 .then(|| (forward.clone(), reversed.clone()))
2083 }
2084 _ => None,
2085 };
2086 match (seam, columns) {
2087 (true, Some((forward, reversed))) => {
2088 self.record_columns(&shape, forward, reversed, surface_id, range)?;
2089 }
2090 (true, None) => {
2091 self.record_pcurve(&shape, image, surface_id, true, range)?;
2092 }
2093 (false, _) => {
2094 let (forward, reversed) = walked;
2095 let placed = forward.or(reversed).unwrap_or(image);
2096 self.record_pcurve(&shape, placed, surface_id, false, range)?;
2097 }
2098 }
2099 }
2100 Ok(())
2101 }
2102
2103 fn image_for(
2104 &mut self,
2105 face_id: u64,
2106 edge_id: u64,
2107 edge: &Shape,
2108 curve: &Curve,
2109 range: (f64, f64),
2110 surface: &SurfaceGeometry,
2111 ) -> OgeomResult<Option<PlanarCurve>> {
2112 let widen = |p: PlanarCurve| -> PlanarCurve {
2113 if let PlanarCurve::Line(l) = &p {
2117 let (lo, hi) = (l.domain().0.min(range.0), l.domain().1.max(range.1));
2118 if let Ok(wider) = ogeom_geom::Line2d::over(l.axis(), lo, hi) {
2119 return wider.into();
2120 }
2121 }
2122 p
2123 };
2124 if let Some(prepared) = self.pcurves.remove(&(face_id, edge_id)) {
2129 match prepared {
2130 PreparedPcurve::Exact(exact) => {
2131 return Ok(Some(widen(exact)));
2132 }
2133 PreparedPcurve::Fitted {
2134 curve: fitted,
2135 error,
2136 met,
2137 worst_off,
2138 warning,
2139 } => {
2140 if let Some(w) = warning {
2141 self.warn_slop(w, worst_off, face_id);
2142 }
2143 if !met {
2144 self.warn_fit_short(face_id, error);
2145 }
2146 if worst_off > self.tol.confusion()
2147 && let Some(node) = self.model.node_mut(edge)
2148 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
2149 {
2150 data.tolerance = data.tolerance.widen_to(worst_off + self.tol.confusion());
2151 }
2152 return Ok(Some(fitted));
2153 }
2154 PreparedPcurve::Refused(why) => {
2155 self.report.warnings.push(why);
2156 self.note_untrimmed(face_id);
2157 return Ok(None);
2158 }
2159 }
2160 }
2161 let pcurve =
2162 match ogeom_intersect::exact_pcurve_over(curve, range, surface, self.tol).map(widen) {
2163 Some(exact) => exact,
2164 None => {
2165 match crate::pcurves::fit_projected_pcurve(curve, range, surface, self.tol) {
2172 Ok((fitted, error, met, worst_off, slop_warning)) => {
2173 if let Some(w) = slop_warning {
2174 self.warn_slop(w, worst_off, face_id);
2175 }
2176 if !met {
2177 self.warn_fit_short(face_id, error);
2178 }
2179 if worst_off > self.tol.confusion()
2183 && let Some(node) = self.model.node_mut(edge)
2184 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
2185 {
2186 data.tolerance =
2187 data.tolerance.widen_to(worst_off + self.tol.confusion());
2188 }
2189 fitted
2190 }
2191 Err(e) => {
2192 self.report.warnings.push(format!(
2193 "face #{face_id}: no pcurve for an edge on this \
2194 surface ({e}); the face may not triangulate"
2195 ));
2196 self.note_untrimmed(face_id);
2197 return Ok(None);
2198 }
2199 }
2200 }
2201 };
2202 Ok(Some(pcurve))
2203 }
2204
2205 fn tally(&mut self, kind: &'static str, measured: f64, exemplar: u64) {
2207 let entry = self.tallies.entry(kind).or_insert((0, 0.0, exemplar));
2208 entry.0 += 1;
2209 if measured > entry.1 {
2210 entry.1 = measured;
2211 entry.2 = exemplar;
2212 }
2213 }
2214
2215 fn warn_vertex_miss(&mut self, id: u64, gap: f64) {
2217 self.report.warnings.push(format!(
2218 "#{id}: a curve end misses its vertex by {gap:.2e}; the vertex \
2219 tolerance grew to say so"
2220 ));
2221 self.tally("vertex-miss", gap, id);
2222 }
2223
2224 fn warn_fit_short(&mut self, face_id: u64, error: f64) {
2226 self.report.warnings.push(format!(
2227 "face #{face_id}: a projected pcurve fit stopped at {error:.2e}; \
2228 the face's mesh may sit that far off along this edge"
2229 ));
2230 self.tally("fit-short", error, face_id);
2231 }
2232
2233 fn warn_slop(&mut self, prose: String, worst: f64, exemplar: u64) {
2235 self.report.warnings.push(prose);
2236 self.tally("boundary-slop", worst, exemplar);
2237 }
2238
2239 fn note_untrimmed(&mut self, face_id: u64) {
2241 if self.untrimmed_ids.last() != Some(&face_id) {
2242 self.untrimmed_ids.push(face_id);
2243 self.tally("untrimmed", 0.0, face_id);
2244 }
2245 }
2246
2247 fn record_columns(
2249 &mut self,
2250 edge: &Shape,
2251 forward: PlanarCurve,
2252 reversed: PlanarCurve,
2253 surface_id: ogeom_topo::SurfaceId,
2254 range: (f64, f64),
2255 ) -> OgeomResult<()> {
2256 ogeom_algo::attach_seam(
2257 &mut self.model,
2258 edge,
2259 forward,
2260 reversed,
2261 surface_id,
2262 Location::identity(),
2263 range,
2264 )
2265 }
2266
2267 fn record_pcurve(
2270 &mut self,
2271 edge: &Shape,
2272 pcurve: PlanarCurve,
2273 surface_id: ogeom_topo::SurfaceId,
2274 seam: bool,
2275 range: (f64, f64),
2276 ) -> OgeomResult<()> {
2277 if seam {
2278 let Some(surface) = self.model.geometry().surface(surface_id).cloned() else {
2279 ogeom_bail!(Dangling, "the surface is not in this model");
2280 };
2281 let other = crate::pcurves::seam_other_side(&pcurve, range, &surface, self.tol)?;
2284 ogeom_algo::attach_seam(
2285 &mut self.model,
2286 edge,
2287 pcurve,
2288 other,
2289 surface_id,
2290 Location::identity(),
2291 range,
2292 )?;
2293 } else {
2294 ogeom_algo::attach_pcurve(
2295 &mut self.model,
2296 edge,
2297 pcurve,
2298 surface_id,
2299 Location::identity(),
2300 range,
2301 )?;
2302 }
2303 Ok(())
2304 }
2305
2306 fn prepare_pcurves(&mut self, face_ids: &[u64]) {
2322 struct Job {
2323 face: u64,
2324 edge: u64,
2325 curve: Curve,
2326 range: (f64, f64),
2327 surface: usize,
2332 }
2333 let mut surfaces: Vec<SurfaceGeometry> = Vec::new();
2334 let mut jobs: Vec<Job> = Vec::new();
2335 let mut seen: HashSet<(u64, u64)> = HashSet::new();
2336 for &fid in face_ids {
2337 let Ok(args) = self.face_args(fid) else {
2338 continue;
2339 };
2340 let Some(surface) = args
2341 .get(2)
2342 .and_then(Arg::reference)
2343 .and_then(|sid| self.surface(sid).ok().flatten())
2344 else {
2345 continue;
2346 };
2347 surfaces.push(surface);
2348 let at = surfaces.len() - 1;
2349 let bounds: Vec<u64> = args
2350 .get(1)
2351 .and_then(Arg::list)
2352 .unwrap_or(&[])
2353 .iter()
2354 .filter_map(Arg::reference)
2355 .collect();
2356 for bound in bounds {
2357 let Ok((loop_id, _)) = self.bound_args(bound) else {
2358 continue;
2359 };
2360 let Ok(loop_args) = self.args(loop_id, "EDGE_LOOP") else {
2361 continue;
2362 };
2363 let uses: Vec<u64> = loop_args
2364 .get(1)
2365 .and_then(Arg::list)
2366 .unwrap_or(&[])
2367 .iter()
2368 .filter_map(Arg::reference)
2369 .collect();
2370 for oe_id in uses {
2371 let Ok(oargs) = self.args(oe_id, "ORIENTED_EDGE") else {
2372 continue;
2373 };
2374 let Some(edge_id) = oargs.get(3).and_then(Arg::reference) else {
2375 continue;
2376 };
2377 if !seen.insert((fid, edge_id)) {
2378 continue;
2379 }
2380 if let Ok(Some((_, curve, range, _))) = self.edge(edge_id) {
2381 jobs.push(Job {
2382 face: fid,
2383 edge: edge_id,
2384 curve,
2385 range,
2386 surface: at,
2387 });
2388 }
2389 }
2390 }
2391 }
2392 if jobs.len() < 16 {
2396 return;
2397 }
2398 let tol = self.tol;
2399 let derived = ogeom_core::parallel::map_ordered(&jobs, |_, job| {
2400 let surface = &surfaces[job.surface];
2401 match ogeom_intersect::exact_pcurve_over(&job.curve, job.range, surface, tol) {
2402 Some(exact) => PreparedPcurve::Exact(exact),
2403 None => {
2404 match crate::pcurves::fit_projected_pcurve(&job.curve, job.range, surface, tol)
2405 {
2406 Ok((curve, error, met, worst_off, warning)) => PreparedPcurve::Fitted {
2407 curve,
2408 error,
2409 met,
2410 worst_off,
2411 warning,
2412 },
2413 Err(e) => PreparedPcurve::Refused(format!(
2414 "face #{}: no pcurve for an edge on this surface ({e}); \
2415 the face may not triangulate",
2416 job.face
2417 )),
2418 }
2419 }
2420 }
2421 });
2422 for (job, pcurve) in jobs.iter().zip(derived) {
2423 self.pcurves.insert((job.face, job.edge), pcurve);
2424 }
2425 }
2426
2427 fn solid(&mut self, id: u64) -> OgeomResult<Shape> {
2439 let instance = self.instance(id)?;
2440 let args = instance
2441 .part("MANIFOLD_SOLID_BREP")
2442 .or_else(|| instance.part("BREP_WITH_VOIDS"))
2443 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "#{id} is not a solid"))?
2444 .to_vec();
2445 let shell_id = args.get(1).and_then(Arg::reference).unwrap_or(0);
2446 let Some(shell) = self.shell(shell_id)? else {
2447 ogeom_bail!(Construction, "#{id}: a solid with no readable faces");
2448 };
2449 if !ogeom_algo::is_shell_closed(&self.model, &shell)? {
2450 self.report.warnings.push(format!(
2451 "#{id}: the shell does not close as read; measures needing an \
2452 inside will refuse it"
2453 ));
2454 }
2455 let mut shells = vec![shell];
2456 for void_id in args
2457 .get(2)
2458 .and_then(Arg::list)
2459 .unwrap_or(&[])
2460 .iter()
2461 .filter_map(Arg::reference)
2462 .collect::<Vec<u64>>()
2463 {
2464 match self.shell(void_id) {
2465 Ok(Some(void)) => shells.push(void),
2466 Ok(None) => self.report.warnings.push(format!(
2467 "#{id}: void shell #{void_id} has no readable faces; the \
2468 cavity is missing from the solid"
2469 )),
2470 Err(refusal) => self.report.warnings.push(format!(
2471 "#{id}: void shell #{void_id} could not be read ({refusal}); \
2472 the cavity is missing from the solid"
2473 )),
2474 }
2475 }
2476 Ok(make_solid(&mut self.model, &shells)?.shape)
2477 }
2478
2479 fn surface_model(&mut self, id: u64) -> OgeomResult<Shape> {
2489 let args = self.args(id, "SHELL_BASED_SURFACE_MODEL")?;
2490 let shell_ids: Vec<u64> = args
2491 .get(1)
2492 .and_then(Arg::list)
2493 .unwrap_or(&[])
2494 .iter()
2495 .filter_map(Arg::reference)
2496 .collect();
2497 let mut shells = Vec::new();
2498 for shell_id in shell_ids {
2499 if let Some(shell) = self.shell(shell_id)? {
2500 shells.push(shell);
2501 }
2502 }
2503 match shells.len() {
2504 0 => ogeom_bail!(
2505 Construction,
2506 "#{id}: a surface model with no readable faces"
2507 ),
2508 1 => Ok(shells.remove(0)),
2509 _ => Ok(ogeom_algo::build::make_compound(&mut self.model, &shells)?.shape),
2510 }
2511 }
2512
2513 fn shell(&mut self, shell_id: u64) -> OgeomResult<Option<Shape>> {
2520 let shell_instance = self.instance(shell_id)?;
2521 if let Some(oriented) = shell_instance
2522 .part("ORIENTED_CLOSED_SHELL")
2523 .map(<[Arg]>::to_vec)
2524 {
2525 let Some(base) = oriented.get(2).and_then(Arg::reference) else {
2526 ogeom_bail!(Construction, "#{shell_id}: an oriented shell names none");
2527 };
2528 let forward = !oriented.get(3).is_some_and(|a| a.is_enum("F"));
2529 return Ok(self
2530 .shell(base)?
2531 .map(|shell| if forward { shell } else { shell.reversed() }));
2532 }
2533 let shell_args = shell_instance
2534 .part("CLOSED_SHELL")
2535 .or_else(|| shell_instance.part("OPEN_SHELL"))
2536 .ok_or_else(|| ogeom_core::ogeom_err!(Construction, "#{shell_id} is not a shell"))?
2537 .to_vec();
2538 let face_ids: Vec<u64> = shell_args
2539 .get(1)
2540 .and_then(Arg::list)
2541 .unwrap_or(&[])
2542 .iter()
2543 .filter_map(Arg::reference)
2544 .collect();
2545 self.prepare_pcurves(&face_ids);
2546 let mut faces = Vec::new();
2547 for fid in face_ids {
2548 if let Some(face) = self.face(fid)? {
2549 faces.push(face);
2550 }
2551 }
2552 if faces.is_empty() {
2553 return Ok(None);
2554 }
2555 Ok(Some(make_shell(&mut self.model, &faces)?.shape))
2556 }
2557
2558 fn document(
2568 &mut self,
2569 by_item: &HashMap<u64, Shape>,
2570 solids: &[Shape],
2571 shells: &[Shape],
2572 ) -> OgeomResult<ogeom_doc::Document> {
2573 let structure = self.product_structure(by_item);
2576 let colours = self.colours(by_item);
2577 let pmi = self.pmi_of();
2578
2579 let mut document = ogeom_doc::Document::over(std::mem::take(&mut self.model));
2580 match structure {
2581 Some(products) => self.build_products(&mut document, products),
2582 None => {
2583 for (i, solid) in solids.iter().enumerate() {
2584 document.add_part(format!("solid-{i}"), solid.clone());
2585 }
2586 for (i, shell) in shells.iter().enumerate() {
2587 document.add_part(format!("shell-{i}"), shell.clone());
2588 }
2589 }
2590 }
2591 for (shape, colour) in colours {
2592 document.set_colour(&shape, colour);
2593 }
2594 *document.pmi_mut() = pmi;
2595 for view in self.views() {
2596 document.add_view(view);
2597 }
2598 Ok(document)
2599 }
2600
2601 fn product_structure(&mut self, by_item: &HashMap<u64, Shape>) -> Option<Vec<PdEntry>> {
2603 let mut pds: Vec<u64> = self
2605 .exchange
2606 .data
2607 .iter()
2608 .filter(|(_, inst)| inst.part("PRODUCT_DEFINITION").is_some())
2609 .filter(|(_, inst)| inst.part("PRODUCT_DEFINITION_RELATIONSHIP").is_none())
2610 .map(|(id, _)| *id)
2611 .collect();
2612 pds.sort_unstable();
2613 if pds.is_empty() {
2614 return None;
2615 }
2616
2617 let mut sr_of_pd: HashMap<u64, u64> = HashMap::new();
2620 let sdrs: Vec<(u64, u64)> = self
2621 .exchange
2622 .data
2623 .iter()
2624 .filter(|(_, inst)| inst.part("SHAPE_DEFINITION_REPRESENTATION").is_some())
2625 .filter_map(|(id, _)| {
2626 let args = self.args(*id, "SHAPE_DEFINITION_REPRESENTATION").ok()?;
2627 Some((args.first()?.reference()?, args.get(1)?.reference()?))
2628 })
2629 .collect();
2630 for (pds_id, sr) in sdrs {
2631 if let Some(definition) = self.definition_of_shape(pds_id) {
2632 sr_of_pd.insert(definition, sr);
2633 }
2634 }
2635
2636 let mut linked: HashMap<u64, Vec<u64>> = HashMap::new();
2643 let mut srrs: Vec<u64> = self
2644 .exchange
2645 .data
2646 .iter()
2647 .filter(|(_, inst)| {
2648 inst.part("SHAPE_REPRESENTATION_RELATIONSHIP").is_some()
2649 && inst
2650 .part("REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION")
2651 .is_none()
2652 })
2653 .map(|(id, _)| *id)
2654 .collect();
2655 srrs.sort_unstable();
2656 for srr in srrs {
2657 let args = {
2658 let Ok(instance) = self.instance(srr) else {
2659 continue;
2660 };
2661 let Some(args) = instance
2662 .part("SHAPE_REPRESENTATION_RELATIONSHIP")
2663 .or_else(|| instance.part("REPRESENTATION_RELATIONSHIP"))
2664 else {
2665 continue;
2666 };
2667 args.to_vec()
2668 };
2669 if let (Some(a), Some(b)) = (
2670 args.get(2).and_then(Arg::reference),
2671 args.get(3).and_then(Arg::reference),
2672 ) {
2673 linked.entry(a).or_default().push(b);
2674 linked.entry(b).or_default().push(a);
2675 }
2676 }
2677
2678 let mut entries = Vec::new();
2679 for pd in pds {
2680 let name = self.product_name(pd).unwrap_or_else(|| format!("#{pd}"));
2681 let mut shapes: Vec<Shape> = Vec::new();
2682 if let Some(&sr) = sr_of_pd.get(&pd) {
2683 let mut reps = vec![sr];
2684 reps.extend(linked.get(&sr).into_iter().flatten().copied());
2685 for rep in reps {
2686 if let Some(items) = self.representation_items(rep) {
2687 shapes.extend(items.iter().filter_map(|item| by_item.get(item).cloned()));
2688 }
2689 }
2690 }
2691 entries.push(PdEntry {
2692 pd,
2693 name,
2694 shapes,
2695 children: Vec::new(),
2696 });
2697 }
2698
2699 let mut nauos: Vec<u64> = self
2702 .exchange
2703 .data
2704 .iter()
2705 .filter(|(_, inst)| inst.part("NEXT_ASSEMBLY_USAGE_OCCURRENCE").is_some())
2706 .map(|(id, _)| *id)
2707 .collect();
2708 nauos.sort_unstable();
2709 let entry_of_pd: HashMap<u64, usize> =
2710 entries.iter().enumerate().map(|(i, e)| (e.pd, i)).collect();
2711 for nauo in nauos {
2712 let Ok(args) = self.args(nauo, "NEXT_ASSEMBLY_USAGE_OCCURRENCE") else {
2713 continue;
2714 };
2715 let (Some(parent), Some(child)) = (
2716 args.get(3).and_then(Arg::reference),
2717 args.get(4).and_then(Arg::reference),
2718 ) else {
2719 continue;
2720 };
2721 let name = args
2722 .get(5)
2723 .and_then(|a| match a {
2724 Arg::Str(s) if !s.is_empty() => Some(s.clone()),
2725 _ => None,
2726 })
2727 .or_else(|| {
2728 self.args(nauo, "NEXT_ASSEMBLY_USAGE_OCCURRENCE")
2729 .ok()
2730 .and_then(|args| match args.get(1) {
2731 Some(Arg::Str(s)) if !s.is_empty() => Some(s.clone()),
2732 _ => None,
2733 })
2734 });
2735 let child_sr = sr_of_pd.get(&child).copied();
2736 let at = self
2737 .usage_transform(nauo, child_sr)
2738 .unwrap_or(Transform::IDENTITY);
2739 if let Some(&at_index) = entry_of_pd.get(&parent) {
2740 entries[at_index].children.push((child, at, name));
2741 }
2742 }
2743 Some(entries)
2744 }
2745
2746 fn definition_of_shape(&mut self, pds_id: u64) -> Option<u64> {
2748 let args = self.args(pds_id, "PRODUCT_DEFINITION_SHAPE").ok()?;
2749 args.get(2).and_then(Arg::reference)
2750 }
2751
2752 fn product_name(&mut self, pd: u64) -> Option<String> {
2754 let formation = self
2755 .args(pd, "PRODUCT_DEFINITION")
2756 .ok()?
2757 .get(2)
2758 .and_then(Arg::reference)?;
2759 let product = [
2764 "PRODUCT_DEFINITION_FORMATION",
2765 "PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE",
2766 ]
2767 .into_iter()
2768 .find_map(|keyword| {
2769 self.instance(formation)
2770 .ok()?
2771 .part(keyword)
2772 .map(<[Arg]>::to_vec)
2773 .or_else(|| self.args(formation, keyword).ok())?
2774 .get(2)
2775 .and_then(Arg::reference)
2776 })?;
2777 let args = self.args(product, "PRODUCT").ok()?;
2780 [args.get(1), args.first()]
2781 .into_iter()
2782 .find_map(|arg| match arg {
2783 Some(Arg::Str(name)) if !name.is_empty() => Some(name.clone()),
2784 _ => None,
2785 })
2786 }
2787
2788 fn representation_items(&mut self, sr: u64) -> Option<Vec<u64>> {
2790 let instance = self.instance(sr).ok()?;
2791 let args = [
2795 "SHAPE_REPRESENTATION",
2796 "ADVANCED_BREP_SHAPE_REPRESENTATION",
2797 "MANIFOLD_SURFACE_SHAPE_REPRESENTATION",
2798 "FACETED_BREP_SHAPE_REPRESENTATION",
2799 "GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION",
2800 "GEOMETRICALLY_BOUNDED_WIREFRAME_SHAPE_REPRESENTATION",
2801 "REPRESENTATION",
2802 ]
2803 .into_iter()
2804 .find_map(|keyword| instance.part(keyword))?
2805 .to_vec();
2806 Some(
2807 args.get(1)?
2808 .list()?
2809 .iter()
2810 .filter_map(Arg::reference)
2811 .collect(),
2812 )
2813 }
2814
2815 fn usage_transform(&mut self, nauo: u64, child_sr: Option<u64>) -> Option<Transform> {
2823 if self.cdsr_of_nauo.is_none() {
2824 let mut cdsrs: Vec<u64> = self
2828 .exchange
2829 .data
2830 .iter()
2831 .filter(|(_, inst)| {
2832 inst.part("CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")
2833 .is_some()
2834 })
2835 .map(|(id, _)| *id)
2836 .collect();
2837 cdsrs.sort_unstable();
2838 let mut index: HashMap<u64, u64> = HashMap::new();
2839 for id in cdsrs {
2840 let Some(owner) = self
2841 .args(id, "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")
2842 .ok()
2843 .and_then(|args| args.get(1).and_then(Arg::reference))
2844 .and_then(|pds| self.definition_of_shape(pds))
2845 else {
2846 continue;
2847 };
2848 index.entry(owner).or_insert(id);
2849 }
2850 self.cdsr_of_nauo = Some(index);
2851 }
2852 let cdsr = *self.cdsr_of_nauo.as_ref()?.get(&nauo)?;
2853 let rr = self
2854 .args(cdsr, "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")
2855 .ok()?
2856 .first()
2857 .and_then(Arg::reference)?;
2858 let (rep_1, rep_2) = {
2859 let args = self.args(rr, "REPRESENTATION_RELATIONSHIP").ok()?;
2860 (
2861 args.get(2).and_then(Arg::reference)?,
2862 args.get(3).and_then(Arg::reference)?,
2863 )
2864 };
2865 let idt = self
2866 .args(rr, "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION")
2867 .ok()?
2868 .first()
2869 .and_then(Arg::reference)?;
2870 let (item_1, item_2) = {
2871 let args = self.args(idt, "ITEM_DEFINED_TRANSFORMATION").ok()?;
2872 (
2873 args.get(2).and_then(Arg::reference)?,
2874 args.get(3).and_then(Arg::reference)?,
2875 )
2876 };
2877 let frame_1 = self.frame(item_1).ok()?;
2878 let frame_2 = self.frame(item_2).ok()?;
2879 let child_first = match child_sr {
2882 Some(sr) => rep_1 == sr || rep_2 != sr,
2883 None => true,
2884 };
2885 Some(if child_first {
2886 Transform::from_frame(&frame_2) * Transform::to_frame(&frame_1)
2887 } else {
2888 Transform::from_frame(&frame_1) * Transform::to_frame(&frame_2)
2889 })
2890 }
2891
2892 fn build_products(&mut self, document: &mut ogeom_doc::Document, entries: Vec<PdEntry>) {
2895 let mut ids: HashMap<u64, ogeom_doc::ProductId> = HashMap::new();
2896 for entry in &entries {
2897 let shape = match entry.shapes.len() {
2898 0 => None,
2899 1 => Some(entry.shapes[0].clone()),
2900 _ => match ogeom_algo::build::make_compound(document.model_mut(), &entry.shapes) {
2901 Ok(built) => Some(built.shape),
2902 Err(_) => Some(entry.shapes[0].clone()),
2903 },
2904 };
2905 if entry.children.is_empty() {
2906 if let Some(shape) = shape {
2907 ids.insert(entry.pd, document.add_part(&entry.name, shape));
2908 }
2909 } else {
2912 let assembly = document.add_assembly(&entry.name);
2913 ids.insert(entry.pd, assembly);
2914 if let Some(shape) = shape {
2917 let body = document.add_part(format!("{}-body", entry.name), shape);
2918 let _ = document.add_instance(assembly, body, Transform::IDENTITY, None);
2919 }
2920 }
2921 }
2922 for entry in &entries {
2923 let Some(&parent) = ids.get(&entry.pd) else {
2924 continue;
2925 };
2926 for (child, at, name) in &entry.children {
2927 let Some(&child_id) = ids.get(child) else {
2928 self.report.warnings.push(format!(
2929 "#{child}: an assembly child holds nothing readable"
2930 ));
2931 continue;
2932 };
2933 if let Err(e) = document.add_instance(parent, child_id, *at, name.clone()) {
2934 self.report
2935 .warnings
2936 .push(format!("assembly edge #{} -> #{child}: {e}", entry.pd));
2937 }
2938 }
2939 }
2940 }
2941
2942 fn colours(&mut self, by_item: &HashMap<u64, Shape>) -> Vec<(Shape, ogeom_doc::Colour)> {
2944 let mut styled: Vec<u64> = self
2945 .exchange
2946 .data
2947 .iter()
2948 .filter(|(_, inst)| {
2949 inst.part("STYLED_ITEM").is_some() || inst.part("OVER_RIDING_STYLED_ITEM").is_some()
2950 })
2951 .map(|(id, _)| *id)
2952 .collect();
2953 styled.sort_unstable();
2957 let mut out = Vec::new();
2958 for id in styled {
2959 let args = {
2960 let Ok(instance) = self.instance(id) else {
2961 continue;
2962 };
2963 let Some(args) = instance
2964 .part("STYLED_ITEM")
2965 .or_else(|| instance.part("OVER_RIDING_STYLED_ITEM"))
2966 else {
2967 continue;
2968 };
2969 args.to_vec()
2970 };
2971 let Some(item) = args.get(2).and_then(Arg::reference) else {
2972 continue;
2973 };
2974 let Some(shape) = by_item.get(&item).or_else(|| self.faces.get(&item)) else {
2975 continue;
2976 };
2977 let styles: Vec<u64> = args
2978 .get(1)
2979 .and_then(Arg::list)
2980 .map(|list| list.iter().filter_map(Arg::reference).collect())
2981 .unwrap_or_default();
2982 let shape = shape.clone();
2983 if let Some(colour) = styles.iter().find_map(|&style| self.colour_in(style, 0)) {
2984 out.push((shape, colour));
2985 }
2986 }
2987 out
2988 }
2989
2990 fn colour_in(&mut self, id: u64, depth: usize) -> Option<ogeom_doc::Colour> {
2995 if depth > 6 {
2996 return None;
2997 }
2998 let (keyword, args) = {
2999 let instance = self.instance(id).ok()?;
3000 let all: Vec<Arg> = instance
3001 .parts()
3002 .flat_map(|(_, args)| args.iter().cloned())
3003 .collect();
3004 (instance.keyword().to_owned(), all)
3005 };
3006 if keyword == "COLOUR_RGB" {
3007 let channel = |i: usize| args.get(i).and_then(Arg::number);
3008 return Some(ogeom_doc::Colour::rgb(
3009 channel(1)?,
3010 channel(2)?,
3011 channel(3)?,
3012 ));
3013 }
3014 let mut refs: Vec<u64> = Vec::new();
3015 collect_refs(&args, &mut refs);
3016 refs.into_iter()
3017 .find_map(|next| self.colour_in(next, depth + 1))
3018 }
3019
3020 fn pmi_of(&mut self) -> ogeom_doc::Pmi {
3027 let mut pmi = ogeom_doc::Pmi::new();
3028 let mut annotation_ids: HashMap<u64, ogeom_doc::Annotated> = HashMap::new();
3032
3033 let mut aspect_items: HashMap<u64, Vec<ogeom_topo::TShapeId>> = HashMap::new();
3037 let mut gisus = self.ids_with("GEOMETRIC_ITEM_SPECIFIC_USAGE");
3038 gisus.sort_unstable();
3039 for id in gisus {
3040 let Ok(args) = self.args(id, "GEOMETRIC_ITEM_SPECIFIC_USAGE") else {
3041 continue;
3042 };
3043 let (Some(aspect), Some(item)) = (
3044 args.get(2).and_then(Arg::reference),
3045 args.get(4).and_then(Arg::reference),
3046 ) else {
3047 continue;
3048 };
3049 let node = self
3050 .faces
3051 .get(&item)
3052 .map(Shape::node)
3053 .or_else(|| self.edges.get(&item).map(|(shape, ..)| shape.node()));
3054 if let Some(node) = node {
3055 aspect_items.entry(aspect).or_default().push(node);
3056 }
3057 }
3058 let mut adjacency: HashMap<u64, Vec<u64>> = HashMap::new();
3059 for id in self.ids_with("SHAPE_ASPECT_RELATIONSHIP") {
3060 let Ok(args) = self.args(id, "SHAPE_ASPECT_RELATIONSHIP") else {
3061 continue;
3062 };
3063 if let (Some(a), Some(b)) = (
3064 args.get(2).and_then(Arg::reference),
3065 args.get(3).and_then(Arg::reference),
3066 ) {
3067 adjacency.entry(a).or_default().push(b);
3068 adjacency.entry(b).or_default().push(a);
3069 }
3070 }
3071 let items_for = |aspect: u64| -> Vec<ogeom_topo::TShapeId> {
3072 let mut reach = vec![aspect];
3076 for _ in 0..3 {
3077 let mut next = reach.clone();
3078 for a in &reach {
3079 next.extend(adjacency.get(a).into_iter().flatten().copied());
3080 }
3081 next.sort_unstable();
3082 next.dedup();
3083 reach = next;
3084 }
3085 let mut out: Vec<ogeom_topo::TShapeId> = Vec::new();
3086 for a in reach {
3087 out.extend(aspect_items.get(&a).into_iter().flatten().copied());
3088 }
3089 out.sort_unstable();
3090 out.dedup();
3091 out
3092 };
3093
3094 let mut plus_minus: HashMap<u64, (Option<f64>, Option<f64>)> = HashMap::new();
3098 for id in self.ids_with("PLUS_MINUS_TOLERANCE") {
3099 let Ok(args) = self.args(id, "PLUS_MINUS_TOLERANCE") else {
3100 continue;
3101 };
3102 let (Some(tv), Some(dim)) = (
3103 args.first().and_then(Arg::reference),
3104 args.get(1).and_then(Arg::reference),
3105 ) else {
3106 continue;
3107 };
3108 let Ok(tv_args) = self.args(tv, "TOLERANCE_VALUE") else {
3109 continue;
3110 };
3111 let lower = tv_args
3112 .first()
3113 .and_then(Arg::reference)
3114 .and_then(|r| self.measure_value(r))
3115 .map(|(v, _)| v);
3116 let upper = tv_args
3117 .get(1)
3118 .and_then(Arg::reference)
3119 .and_then(|r| self.measure_value(r))
3120 .map(|(v, _)| v);
3121 plus_minus.insert(dim, (lower, upper));
3122 }
3123 let mut dcrs = self.ids_with("DIMENSIONAL_CHARACTERISTIC_REPRESENTATION");
3124 dcrs.sort_unstable();
3125 for id in dcrs {
3126 let Ok(args) = self.args(id, "DIMENSIONAL_CHARACTERISTIC_REPRESENTATION") else {
3127 continue;
3128 };
3129 let (Some(dim), Some(sdr)) = (
3130 args.first().and_then(Arg::reference),
3131 args.get(1).and_then(Arg::reference),
3132 ) else {
3133 continue;
3134 };
3135 let mut values = Vec::new();
3136 let mut kind = ogeom_doc::MeasureKind::Length;
3137 if let Ok(sdr_args) = self.args(sdr, "SHAPE_DIMENSION_REPRESENTATION") {
3138 for item in sdr_args.get(1).and_then(Arg::list).unwrap_or(&[]) {
3139 if let Some(r) = item.reference()
3140 && let Some((v, k)) = self.measure_value(r)
3141 {
3142 values.push(v);
3143 kind = k;
3144 }
3145 }
3146 }
3147 let (name, location, aspects) = self.dimension_shape(dim);
3148 let features: Vec<Vec<ogeom_topo::TShapeId>> =
3149 aspects.into_iter().map(&items_for).collect();
3150 let (minus, plus) = plus_minus.get(&dim).copied().unwrap_or((None, None));
3151 annotation_ids.insert(dim, ogeom_doc::Annotated::Dimension(pmi.dimensions.len()));
3152 pmi.dimensions.push(ogeom_doc::Dimension {
3153 name,
3154 values,
3155 kind,
3156 plus,
3157 minus,
3158 features,
3159 location,
3160 });
3161 }
3162
3163 let is_subtype = |k: &str| {
3167 k.ends_with("_TOLERANCE")
3168 && !matches!(
3169 k,
3170 "GEOMETRIC_TOLERANCE"
3171 | "GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE"
3172 | "GEOMETRIC_TOLERANCE_WITH_DEFINED_UNIT"
3173 | "GEOMETRIC_TOLERANCE_WITH_MODIFIERS"
3174 | "GEOMETRIC_TOLERANCE_WITH_MAXIMUM_TOLERANCE"
3175 | "PLUS_MINUS_TOLERANCE"
3176 | "TOLERANCE_VALUE"
3177 )
3178 };
3179 let mut gts: Vec<u64> = self
3180 .exchange
3181 .data
3182 .iter()
3183 .filter(|(_, inst)| inst.parts().any(|(k, _)| is_subtype(k)))
3184 .map(|(id, _)| *id)
3185 .collect();
3186 gts.sort_unstable();
3187 for id in gts {
3188 let (name, magnitude_ref, aspect, kind, modifiers, datum_refs) = {
3189 let Ok(instance) = self.instance(id) else {
3190 continue;
3191 };
3192 let subtype = instance
3193 .parts()
3194 .map(|(k, _)| k.to_owned())
3195 .find(|k| is_subtype(k));
3196 let Some(subtype) = subtype else {
3197 continue;
3198 };
3199 let base = instance
3200 .part("GEOMETRIC_TOLERANCE")
3201 .or_else(|| instance.part(&subtype))
3202 .unwrap_or(&[])
3203 .to_vec();
3204 let name = match base.first() {
3205 Some(Arg::Str(s)) => s.clone(),
3206 _ => String::new(),
3207 };
3208 let magnitude_ref = base.get(2).and_then(Arg::reference);
3209 let aspect = base.get(3).and_then(Arg::reference);
3210 let kind = Some(subtype.trim_end_matches("_TOLERANCE").to_lowercase());
3211 let modifiers: Vec<String> = instance
3214 .part("GEOMETRIC_TOLERANCE_WITH_MODIFIERS")
3215 .and_then(|args| args.first())
3216 .and_then(Arg::list)
3217 .map(|list| {
3218 list.iter()
3219 .filter_map(|arg| match arg {
3220 Arg::Enum(word) => Some(word.to_lowercase()),
3221 _ => None,
3222 })
3223 .collect()
3224 })
3225 .unwrap_or_default();
3226 let datum_refs: Vec<u64> = instance
3229 .part("GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE")
3230 .and_then(|args| args.first())
3231 .and_then(Arg::list)
3232 .or_else(|| base.get(4).and_then(Arg::list))
3233 .map(|list| list.iter().filter_map(Arg::reference).collect())
3234 .unwrap_or_default();
3235 (name, magnitude_ref, aspect, kind, modifiers, datum_refs)
3236 };
3237 let Some(kind) = kind else {
3238 continue;
3239 };
3240 let magnitude = magnitude_ref
3241 .and_then(|r| self.measure_value(r))
3242 .map_or(0.0, |(v, _)| v);
3243 let datums: Vec<String> = datum_refs
3244 .iter()
3245 .filter_map(|&r| self.datum_letter(r, 0))
3246 .collect();
3247 let items = aspect.map(items_for).unwrap_or_default();
3248 annotation_ids.insert(id, ogeom_doc::Annotated::Tolerance(pmi.tolerances.len()));
3249 pmi.tolerances.push(ogeom_doc::GeometricTolerance {
3250 kind,
3251 name,
3252 magnitude,
3253 modifiers,
3254 datums,
3255 items,
3256 });
3257 }
3258
3259 let mut datums = self.ids_with("DATUM");
3262 datums.sort_unstable();
3263 for id in datums {
3264 let letter = {
3265 let Ok(instance) = self.instance(id) else {
3266 continue;
3267 };
3268 if instance.part("DATUM_FEATURE").is_some()
3269 || instance.part("DATUM_REFERENCE").is_some()
3270 || instance.part("DATUM_REFERENCE_COMPARTMENT").is_some()
3271 || instance.part("DATUM_SYSTEM").is_some()
3272 {
3273 continue;
3274 }
3275 let Some(args) = instance.part("DATUM") else {
3276 continue;
3277 };
3278 match args.get(4) {
3279 Some(Arg::Str(s)) if !s.is_empty() => s.clone(),
3280 _ => continue,
3281 }
3282 };
3283 annotation_ids.insert(id, ogeom_doc::Annotated::Datum(pmi.datums.len()));
3284 pmi.datums.push(ogeom_doc::Datum {
3285 label: letter,
3286 items: items_for(id),
3287 });
3288 }
3289
3290 let mut targets = self.ids_with("PLACED_DATUM_TARGET_FEATURE");
3295 targets.sort_unstable();
3296 for id in targets {
3297 if let Some(target) = self.datum_target(id, &items_for) {
3298 pmi.targets.push(target);
3299 }
3300 }
3301
3302 let (callouts, callout_index) = self.callouts(&annotation_ids);
3308 pmi.callouts = callouts;
3309 self.callout_index = callout_index;
3310 pmi
3311 }
3312
3313 fn views(&mut self) -> Vec<ogeom_doc::View> {
3317 let mut out = Vec::new();
3318 for id in self.ids_with("DRAUGHTING_MODEL") {
3319 let Ok(args) = self.args(id, "DRAUGHTING_MODEL") else {
3320 continue;
3321 };
3322 let name = match args.first() {
3323 Some(Arg::Str(text)) => text.clone(),
3324 _ => String::new(),
3325 };
3326 if name.is_empty() {
3327 continue;
3328 }
3329 let mut frame = None;
3330 let mut callouts = Vec::new();
3331 for item in args.get(1).and_then(Arg::list).unwrap_or(&[]) {
3332 let Some(item) = item.reference() else {
3333 continue;
3334 };
3335 if let Ok(cam) = self.args(item, "CAMERA_MODEL_D3") {
3336 if let Some(placement) = cam.get(1).and_then(Arg::reference) {
3337 frame = self.frame(placement).ok();
3338 }
3339 continue;
3340 }
3341 if let Some(&index) = self.callout_index.get(&item) {
3342 callouts.push(index);
3343 }
3344 }
3345 let Some(frame) = frame else { continue };
3346 out.push(ogeom_doc::View {
3347 name,
3348 frame,
3349 clipping: None,
3350 callouts,
3351 });
3352 }
3353 out
3354 }
3355
3356 fn datum_target(
3358 &mut self,
3359 id: u64,
3360 items_for: &dyn Fn(u64) -> Vec<ogeom_topo::TShapeId>,
3361 ) -> Option<ogeom_doc::DatumTarget> {
3362 let (target_id, description) = {
3363 let instance = self.instance(id).ok()?;
3364 let args = instance.part("PLACED_DATUM_TARGET_FEATURE")?;
3365 let target = match args.get(4) {
3366 Some(Arg::Str(s)) if !s.is_empty() => s.clone(),
3367 _ => return None,
3368 };
3369 let description = match args.get(1) {
3370 Some(Arg::Str(s)) => s.to_ascii_lowercase(),
3371 _ => String::new(),
3372 };
3373 (target, description)
3374 };
3375 let split = target_id
3377 .find(|c: char| c.is_ascii_digit())
3378 .unwrap_or(target_id.len());
3379 let (letter, number) = target_id.split_at(split);
3380 let index = number.parse::<u32>().unwrap_or(1);
3381 let datum = if letter.is_empty() {
3382 self.datum_letter(id, 0).unwrap_or_default()
3383 } else {
3384 letter.to_owned()
3385 };
3386
3387 self.ensure_property_indexes();
3393 let mut frame = None;
3394 let mut lengths: Vec<f64> = Vec::new();
3395 let properties: Vec<u64> = self
3396 .properties_of_definition
3397 .as_ref()
3398 .and_then(|m| m.get(&id).cloned())
3399 .unwrap_or_default();
3400 for property in properties {
3401 let sdrs: Vec<u64> = self
3402 .sdrs_of_property
3403 .as_ref()
3404 .and_then(|m| m.get(&property).cloned())
3405 .unwrap_or_default();
3406 for sdr in sdrs {
3407 let Ok(args) = self.args(sdr, "SHAPE_DEFINITION_REPRESENTATION") else {
3408 continue;
3409 };
3410 let Some(rep) = args.get(1).and_then(Arg::reference) else {
3411 continue;
3412 };
3413 for item in self.representation_items(rep).unwrap_or_default() {
3414 let keyword = self
3415 .instance(item)
3416 .map(|i| i.keyword().to_owned())
3417 .unwrap_or_default();
3418 match keyword.as_str() {
3419 "AXIS2_PLACEMENT_3D" => frame = self.frame(item).ok(),
3420 "LENGTH_MEASURE_WITH_UNIT" | "MEASURE_REPRESENTATION_ITEM" => {
3421 if let Some((value, _)) = self.measure_value(item) {
3422 lengths.push(value);
3423 }
3424 }
3425 _ => {}
3426 }
3427 }
3428 }
3429 }
3430
3431 let kind = if description.contains("circle") || description.contains("circular") {
3435 ogeom_doc::DatumTargetKind::Circle {
3436 diameter: lengths.first().copied().unwrap_or(0.0),
3437 }
3438 } else if description.contains("rectangle") || lengths.len() >= 2 {
3439 ogeom_doc::DatumTargetKind::Rectangle {
3440 length: lengths.first().copied().unwrap_or(0.0),
3441 width: lengths.get(1).copied().unwrap_or(0.0),
3442 }
3443 } else if description.contains("line") || lengths.len() == 1 {
3444 ogeom_doc::DatumTargetKind::Line {
3445 length: lengths.first().copied().unwrap_or(0.0),
3446 }
3447 } else {
3448 ogeom_doc::DatumTargetKind::Point
3449 };
3450 Some(ogeom_doc::DatumTarget {
3451 datum,
3452 index,
3453 kind,
3454 at: frame.map_or(Point::ORIGIN, |f: Frame| f.origin()),
3455 frame,
3456 items: items_for(id),
3457 })
3458 }
3459
3460 fn callouts(
3463 &mut self,
3464 annotation_ids: &HashMap<u64, ogeom_doc::Annotated>,
3465 ) -> (Vec<ogeom_doc::Callout>, HashMap<u64, usize>) {
3466 let mut plane_of: HashMap<u64, Frame> = HashMap::new();
3468 for id in self.ids_with("ANNOTATION_PLANE") {
3469 let Ok(args) = self.args(id, "ANNOTATION_PLANE") else {
3470 continue;
3471 };
3472 let frame = args
3473 .get(2)
3474 .and_then(Arg::reference)
3475 .and_then(|plane| {
3476 let inner = self.args(plane, "PLANE").ok()?;
3477 inner.get(1).and_then(Arg::reference)
3478 })
3479 .and_then(|placement| self.frame(placement).ok());
3480 let Some(frame) = frame else { continue };
3481 for element in args.get(3).and_then(Arg::list).unwrap_or(&[]) {
3482 if let Some(callout) = element.reference() {
3483 plane_of.insert(callout, frame);
3484 }
3485 }
3486 }
3487
3488 let mut draws: HashMap<u64, Vec<u64>> = HashMap::new();
3496 let mut associations = self.ids_with("DRAUGHTING_MODEL_ITEM_ASSOCIATION");
3497 associations.sort_unstable();
3498 for id in associations {
3499 let Ok(args) = self.args(id, "DRAUGHTING_MODEL_ITEM_ASSOCIATION") else {
3500 continue;
3501 };
3502 if let (Some(definition), Some(item)) = (
3503 args.get(2).and_then(Arg::reference),
3504 args.get(4).and_then(Arg::reference),
3505 ) {
3506 draws.entry(item).or_default().push(definition);
3507 }
3508 }
3509
3510 let mut out = Vec::new();
3511 let mut index_of: HashMap<u64, usize> = HashMap::new();
3512 let mut callouts = self.ids_with("DRAUGHTING_CALLOUT");
3513 callouts.sort_unstable();
3514 for id in callouts {
3515 let Ok(args) = self.args(id, "DRAUGHTING_CALLOUT") else {
3516 continue;
3517 };
3518 let name = match args.first() {
3519 Some(Arg::Str(s)) => s.clone(),
3520 _ => String::new(),
3521 };
3522 let mut polylines = Vec::new();
3523 for element in args.get(1).and_then(Arg::list).unwrap_or(&[]).to_vec() {
3524 let Some(occurrence) = element.reference() else {
3525 continue;
3526 };
3527 polylines.extend(self.annotation_polylines(occurrence));
3528 }
3529 if polylines.is_empty() && !plane_of.contains_key(&id) {
3530 continue;
3531 }
3532 index_of.insert(id, out.len());
3533 out.push(ogeom_doc::Callout {
3534 name,
3535 plane: plane_of.get(&id).copied(),
3536 polylines,
3537 annotates: draws
3538 .get(&id)
3539 .cloned()
3540 .unwrap_or_default()
3541 .into_iter()
3542 .find_map(|d| annotation_ids.get(&d).copied()),
3543 });
3544 }
3545 (out, index_of)
3546 }
3547
3548 fn annotation_polylines(&mut self, occurrence: u64) -> Vec<Vec<Point>> {
3555 let item = {
3556 let Ok(instance) = self.instance(occurrence) else {
3557 return Vec::new();
3558 };
3559 let args = instance
3560 .part("TESSELLATED_ANNOTATION_OCCURRENCE")
3561 .or_else(|| instance.part("ANNOTATION_OCCURRENCE"))
3562 .or_else(|| instance.part("STYLED_ITEM"));
3563 match args.and_then(|a| a.get(2).and_then(Arg::reference)) {
3564 Some(item) => item,
3565 None => return Vec::new(),
3566 }
3567 };
3568 self.tessellated_polylines(item, 0)
3569 }
3570
3571 fn tessellated_polylines(&mut self, item: u64, depth: usize) -> Vec<Vec<Point>> {
3579 if depth > 4 {
3580 return Vec::new();
3581 }
3582 let (children, curve_set, placement) = {
3583 let Ok(instance) = self.instance(item) else {
3584 return Vec::new();
3585 };
3586 let children: Vec<u64> = instance
3587 .part("TESSELLATED_GEOMETRIC_SET")
3588 .and_then(|args| args.first().and_then(Arg::list))
3589 .map(|list| list.iter().filter_map(Arg::reference).collect())
3590 .unwrap_or_default();
3591 let curve_set = instance.part("TESSELLATED_CURVE_SET").map(<[Arg]>::to_vec);
3592 let placement = instance
3593 .part("REPOSITIONED_TESSELLATED_ITEM")
3594 .and_then(|args| args.first().and_then(Arg::reference));
3595 (children, curve_set, placement)
3596 };
3597
3598 let mut out = Vec::new();
3599 for child in children {
3600 out.extend(self.tessellated_polylines(child, depth + 1));
3601 }
3602 if let Some(args) = curve_set
3603 && let Some(list) = args.get(1).and_then(Arg::reference)
3604 {
3605 let points = self.coordinates_list(list);
3606 for line in args.get(2).and_then(Arg::list).unwrap_or(&[]) {
3607 let Some(indices) = line.list() else { continue };
3608 let mut polyline = Vec::with_capacity(indices.len());
3609 for index in indices {
3610 let Some(k) = index.number() else { continue };
3612 #[expect(
3613 clippy::cast_possible_truncation,
3614 clippy::cast_sign_loss,
3615 reason = "an index into a list the file itself sized"
3616 )]
3617 let k = k as usize;
3618 if k >= 1 && k <= points.len() {
3619 polyline.push(points[k - 1]);
3620 }
3621 }
3622 if polyline.len() >= 2 {
3623 out.push(polyline);
3624 }
3625 }
3626 }
3627 if let Some(placement) = placement
3628 && let Ok(frame) = self.frame(placement)
3629 {
3630 for polyline in &mut out {
3631 for p in polyline.iter_mut() {
3632 *p = frame.to_world(*p);
3633 }
3634 }
3635 }
3636 out
3637 }
3638
3639 fn coordinates_list(&mut self, id: u64) -> Vec<Point> {
3641 let Ok(args) = self.args(id, "COORDINATES_LIST") else {
3642 return Vec::new();
3643 };
3644 let scale = self.report.scale_mm;
3645 args.get(2)
3646 .and_then(Arg::list)
3647 .unwrap_or(&[])
3648 .iter()
3649 .filter_map(|entry| {
3650 let coords = entry.list()?;
3651 let value = |i: usize| coords.get(i).and_then(Arg::number).unwrap_or(0.0) * scale;
3652 Some(Point::new(value(0), value(1), value(2)))
3653 })
3654 .collect()
3655 }
3656
3657 fn ids_with(&self, keyword: &str) -> Vec<u64> {
3659 self.exchange
3660 .data
3661 .iter()
3662 .filter(|(_, inst)| inst.part(keyword).is_some())
3663 .map(|(id, _)| *id)
3664 .collect()
3665 }
3666
3667 fn ensure_property_indexes(&mut self) {
3673 if self.properties_of_definition.is_some() {
3674 return;
3675 }
3676 let mut properties: HashMap<u64, Vec<u64>> = HashMap::new();
3677 let mut sdrs: HashMap<u64, Vec<u64>> = HashMap::new();
3678 let exchange = self.exchange;
3679 for (id, instance) in &exchange.data {
3680 if let Some(args) = instance.part("PROPERTY_DEFINITION") {
3681 if let Ok(i) = usize::try_from(*id)
3684 && let Some(slot) = self.visited.get_mut(i)
3685 {
3686 *slot = true;
3687 }
3688 if let Some(definition) = args.get(2).and_then(Arg::reference) {
3689 properties.entry(definition).or_default().push(*id);
3690 }
3691 }
3692 if let Some(args) = instance.part("SHAPE_DEFINITION_REPRESENTATION") {
3693 if let Ok(i) = usize::try_from(*id)
3694 && let Some(slot) = self.visited.get_mut(i)
3695 {
3696 *slot = true;
3697 }
3698 if let Some(property) = args.first().and_then(Arg::reference) {
3699 sdrs.entry(property).or_default().push(*id);
3700 }
3701 }
3702 }
3703 for list in properties.values_mut().chain(sdrs.values_mut()) {
3704 list.sort_unstable();
3705 }
3706 self.properties_of_definition = Some(properties);
3707 self.sdrs_of_property = Some(sdrs);
3708 }
3709
3710 fn measure_value(&mut self, id: u64) -> Option<(f64, ogeom_doc::MeasureKind)> {
3712 let args = {
3713 let instance = self.instance(id).ok()?;
3714 instance.part("MEASURE_WITH_UNIT")?.to_vec()
3715 };
3716 match args.first() {
3717 Some(Arg::Typed(kind, inner)) => {
3718 let value = inner.first().and_then(Arg::number)?;
3719 if kind.contains("ANGLE") {
3720 Some((value * self.angle_scale, ogeom_doc::MeasureKind::Angle))
3721 } else {
3722 Some((value * self.report.scale_mm, ogeom_doc::MeasureKind::Length))
3723 }
3724 }
3725 _ => None,
3726 }
3727 }
3728
3729 fn dimension_shape(&mut self, dim: u64) -> (String, bool, Vec<u64>) {
3735 let size = {
3736 let Ok(instance) = self.instance(dim) else {
3737 return (String::new(), false, Vec::new());
3738 };
3739 instance
3740 .part("DIMENSIONAL_SIZE")
3741 .or_else(|| instance.part("ANGULAR_SIZE"))
3742 .map(<[Arg]>::to_vec)
3743 };
3744 if let Some(args) = size {
3745 let name = match args.get(1) {
3746 Some(Arg::Str(s)) => s.clone(),
3747 _ => String::new(),
3748 };
3749 let aspects = args.first().and_then(Arg::reference).into_iter().collect();
3750 return (name, false, aspects);
3751 }
3752 let location = {
3753 let Ok(instance) = self.instance(dim) else {
3754 return (String::new(), false, Vec::new());
3755 };
3756 instance
3757 .part("DIMENSIONAL_LOCATION")
3758 .or_else(|| instance.part("ANGULAR_LOCATION"))
3759 .map(<[Arg]>::to_vec)
3760 };
3761 if let Some(args) = location {
3762 let name = match args.first() {
3763 Some(Arg::Str(s)) => s.clone(),
3764 _ => String::new(),
3765 };
3766 let aspects = [args.get(2), args.get(3)]
3767 .into_iter()
3768 .flatten()
3769 .filter_map(Arg::reference)
3770 .collect();
3771 return (name, true, aspects);
3772 }
3773 (String::new(), false, Vec::new())
3774 }
3775
3776 fn datum_letter(&mut self, id: u64, depth: usize) -> Option<String> {
3780 if depth > 4 {
3781 return None;
3782 }
3783 let (letter, members, refs) = {
3784 let instance = self.instance(id).ok()?;
3785 let letter = instance.part("DATUM").and_then(|args| match args.get(4) {
3786 Some(Arg::Str(s)) if !s.is_empty() => Some(s.clone()),
3787 _ => None,
3788 });
3789 let members: Vec<u64> = ["DATUM_REFERENCE_COMPARTMENT", "COMMON_DATUM"]
3793 .iter()
3794 .find_map(|k| instance.part(k))
3795 .into_iter()
3796 .flatten()
3797 .find_map(Arg::list)
3798 .map(|list| list.iter().filter_map(Arg::reference).collect())
3799 .unwrap_or_default();
3800 let mut refs = Vec::new();
3801 for (_, args) in instance.parts() {
3802 collect_refs(args, &mut refs);
3803 }
3804 (letter, members, refs)
3805 };
3806 if let Some(letter) = letter {
3807 return Some(letter);
3808 }
3809 if !members.is_empty() {
3810 let letters: Vec<String> = members
3811 .into_iter()
3812 .filter_map(|r| self.datum_letter(r, depth + 1))
3813 .collect();
3814 if !letters.is_empty() {
3815 return Some(letters.join("-"));
3816 }
3817 }
3818 refs.into_iter()
3819 .find_map(|r| self.datum_letter(r, depth + 1))
3820 }
3821}
3822
3823struct PdEntry {
3826 pd: u64,
3827 name: String,
3828 shapes: Vec<Shape>,
3829 children: Vec<(u64, Transform, Option<String>)>,
3830}
3831
3832fn collect_refs(args: &[Arg], out: &mut Vec<u64>) {
3834 for arg in args {
3835 match arg {
3836 Arg::Ref(id) => out.push(*id),
3837 Arg::List(inner) | Arg::Typed(_, inner) => collect_refs(inner, out),
3838 _ => {}
3839 }
3840 }
3841}
3842
3843fn closed_ring_edges(model: &Model, wires: &[Shape]) -> OgeomResult<Vec<(Shape, Shape)>> {
3848 let mut out = Vec::new();
3849 for wire in wires {
3850 let edges = ogeom_topo::explore(
3851 model,
3852 wire,
3853 ogeom_topo::Filter::OfType(ogeom_topo::ShapeType::Edge),
3854 )?;
3855 if edges.len() != 1 {
3856 return Ok(Vec::new());
3857 }
3858 let edge = edges[0].clone();
3859 let Some((a, b)) = ogeom_algo::edge_vertices(model, &edge)? else {
3860 return Ok(Vec::new());
3861 };
3862 if !a.is_same(&b) {
3863 return Ok(Vec::new());
3864 }
3865 out.push((edge, a));
3866 }
3867 Ok(out)
3868}
3869
3870#[allow(dead_code)]
3872fn _keep(p: PlanarCurve) -> PlanarCurve {
3873 p
3874}
3875
3876fn window_between_feet(
3881 curve: &Curve,
3882 start: Point,
3883 end: Point,
3884 tol: Tolerances,
3885) -> Option<(f64, f64)> {
3886 let (lo, hi) = curve.domain();
3887 let samples = match curve {
3888 Curve::BSpline(spline) => (spline.control_points().len() * 8).max(64),
3889 _ => 64,
3890 };
3891 let foot = |p: Point| -> Option<f64> {
3892 let found = project_on_curve(curve, p, samples, tol).ok()?;
3893 (found.distance <= tol.confusion() * 1e4).then_some(found.parameter)
3894 };
3895 let (a, b) = (foot(start)?, foot(end)?);
3896 let slack = tol.parametric().max((hi - lo) * 1e-9);
3897 if b <= a + slack {
3898 return None;
3899 }
3900 if (a - lo).abs() <= slack && (b - hi).abs() <= slack {
3901 return None;
3902 }
3903 Some((a, b))
3904}
3905
3906fn implied_knots(form: &str, degree: usize, count: usize) -> Vec<f64> {
3910 let p = degree.max(1);
3911 #[allow(clippy::cast_precision_loss)]
3912 let at = |i: usize| i as f64;
3913 if form.starts_with("BEZIER") {
3914 let pieces = (count.saturating_sub(1) / p).max(1);
3915 let mut out = vec![0.0; p + 1];
3916 for s in 1..pieces {
3917 out.extend(std::iter::repeat_n(at(s), p));
3918 }
3919 out.extend(std::iter::repeat_n(at(pieces), p + 1));
3920 out
3921 } else if form.starts_with("QUASI_UNIFORM") {
3922 let interior = count.saturating_sub(p + 1);
3923 let mut out = vec![0.0; p + 1];
3924 out.extend((1..=interior).map(at));
3925 out.extend(std::iter::repeat_n(at(interior + 1), p + 1));
3926 out
3927 } else {
3928 #[allow(clippy::cast_precision_loss)]
3929 (0..count + p + 1).map(|i| i as f64 - p as f64).collect()
3930 }
3931}
3932
3933fn join_splines(
3938 held: (KnotVector, Vec<Weighted<Point>>),
3939 next: &BSplineCurve,
3940 tol: Tolerances,
3941) -> OgeomResult<(KnotVector, Vec<Weighted<Point>>)> {
3942 let (mut knots, mut control) = held;
3943 let mut next = next.clone();
3944 while next.degree() < knots.degree() {
3945 next = next.elevated(tol)?;
3946 }
3947 while knots.degree() < next.degree() {
3948 (knots, control) = ogeom_math::bspline::elevate_degree(&knots, &control, tol)?;
3949 }
3950 let (Some(end), Some(start)) = (control.last(), next.control_points().first()) else {
3951 ogeom_bail!(Construction, "a composite segment has no control points");
3952 };
3953 let factor = end.weight / start.weight;
3954 let scaled: Vec<Weighted<Point>> = next
3955 .control_points()
3956 .iter()
3957 .map(|w| Weighted {
3958 scaled: w.scaled.scale(factor),
3959 weight: w.weight * factor,
3960 })
3961 .collect();
3962 ogeom_math::bspline::join(&(knots, control), &(next.knots().clone(), scaled))
3963}
3964
3965#[derive(Debug, Clone)]
3968struct Patch {
3969 u: KnotVector,
3970 v: KnotVector,
3971 net: Vec<Vec<Weighted<Point>>>,
3972}
3973
3974impl Patch {
3975 fn of(surface: &ogeom_geom::BSplineSurface) -> OgeomResult<Self> {
3976 let grid = surface.grid();
3977 let (nu, nv) = (grid.u_count(), grid.v_count());
3978 let points = grid.points();
3979 let net = (0..nu)
3980 .map(|i| (0..nv).map(|j| points[i * nv + j]).collect())
3981 .collect();
3982 Ok(Self {
3983 u: surface.u_knots().reparameterized(0.0, 1.0)?,
3984 v: surface.v_knots().reparameterized(0.0, 1.0)?,
3985 net,
3986 })
3987 }
3988
3989 fn transposed(self) -> Self {
3990 let (nu, nv) = (self.net.len(), self.net[0].len());
3991 let net = (0..nv)
3992 .map(|j| (0..nu).map(|i| self.net[i][j]).collect())
3993 .collect();
3994 Self {
3995 u: self.v,
3996 v: self.u,
3997 net,
3998 }
3999 }
4000
4001 fn reversed_u(mut self) -> Self {
4002 self.net.reverse();
4003 self.u = self.u.reversed();
4004 self
4005 }
4006
4007 fn along_u(
4009 self,
4010 op: impl Fn(&KnotVector, &[Weighted<Point>]) -> OgeomResult<(KnotVector, Vec<Weighted<Point>>)>,
4011 ) -> OgeomResult<Self> {
4012 let nv = self.net[0].len();
4013 let mut knots = self.u.clone();
4014 let mut columns = Vec::with_capacity(nv);
4015 for j in 0..nv {
4016 let column: Vec<Weighted<Point>> = self.net.iter().map(|row| row[j]).collect();
4017 let (k, c) = op(&self.u, &column)?;
4018 knots = k;
4019 columns.push(c);
4020 }
4021 let nu = columns[0].len();
4022 let net = (0..nu)
4023 .map(|i| (0..nv).map(|j| columns[j][i]).collect())
4024 .collect();
4025 Ok(Self {
4026 u: knots,
4027 v: self.v,
4028 net,
4029 })
4030 }
4031}
4032
4033fn join_patches(mut grid: Vec<Vec<Patch>>, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
4035 let (m, n) = (grid.len(), grid[0].len());
4036 let pu = grid
4038 .iter()
4039 .flatten()
4040 .map(|p| p.u.degree())
4041 .max()
4042 .unwrap_or(1);
4043 let pv = grid
4044 .iter()
4045 .flatten()
4046 .map(|p| p.v.degree())
4047 .max()
4048 .unwrap_or(1);
4049 for patch in grid.iter_mut().flatten() {
4050 let mut p = patch.clone();
4051 while p.u.degree() < pu {
4052 p = p.along_u(|k, c| ogeom_math::bspline::elevate_degree(k, c, tol))?;
4053 }
4054 p = p.transposed();
4055 while p.u.degree() < pv {
4056 p = p.along_u(|k, c| ogeom_math::bspline::elevate_degree(k, c, tol))?;
4057 }
4058 *patch = p.transposed();
4059 }
4060 let unify = |patches: Vec<Patch>, along_v: bool| -> OgeomResult<Vec<Patch>> {
4064 let turned: Vec<Patch> = if along_v {
4065 patches.into_iter().map(Patch::transposed).collect()
4066 } else {
4067 patches
4068 };
4069 let mut wanted: Vec<(f64, usize)> = Vec::new();
4070 for p in &turned {
4071 for (value, count) in p.u.distinct() {
4072 if value <= 1e-12 || value >= 1.0 - 1e-12 {
4073 continue;
4074 }
4075 match wanted.iter_mut().find(|(v, _)| (v - value).abs() <= 1e-12) {
4076 Some(slot) => slot.1 = slot.1.max(count),
4077 None => wanted.push((value, count)),
4078 }
4079 }
4080 }
4081 let mut out = Vec::with_capacity(turned.len());
4082 for mut p in turned {
4083 for &(value, count) in &wanted {
4084 let have = p.u.multiplicity_of(value);
4085 if have < count {
4086 p = p.along_u(|k, c| {
4087 ogeom_math::bspline::insert_knot(k, c, value, count - have, tol)
4088 })?;
4089 }
4090 }
4091 out.push(if along_v { p.transposed() } else { p });
4092 }
4093 Ok(out)
4094 };
4095 for row in &mut grid {
4096 *row = unify(std::mem::take(row), true)?;
4097 }
4098 for j in 0..n {
4099 let column: Vec<Patch> = grid.iter().map(|row| row[j].clone()).collect();
4100 for (row, p) in grid.iter_mut().zip(unify(column, false)?) {
4101 row[j] = p;
4102 }
4103 }
4104 let near = |a: &Weighted<Point>, b: &Weighted<Point>| {
4106 a.point().distance(b.point()) <= tol.confusion() * 100.0
4107 && (a.weight - b.weight).abs() <= 1e-9 * a.weight.abs().max(1.0)
4108 };
4109 let mut rows: Vec<Vec<Weighted<Point>>> = Vec::new();
4110 for (i, grid_row) in grid.iter().enumerate() {
4111 let nu = grid_row[0].net.len();
4112 for a in 0..nu {
4113 if i > 0 && a == 0 {
4114 let prev = rows.len() - 1;
4116 let mut shared = 0;
4117 for (j, patch) in grid_row.iter().enumerate() {
4118 for (b, cell) in patch.net[0].iter().enumerate() {
4119 if j > 0 && b == 0 {
4120 continue;
4121 }
4122 if !near(&rows[prev][shared], cell) {
4123 ogeom_bail!(Construction, "neighbouring patches part along u");
4124 }
4125 shared += 1;
4126 }
4127 }
4128 continue;
4129 }
4130 let mut row = Vec::new();
4131 for (j, patch) in grid_row.iter().enumerate() {
4132 for (b, cell) in patch.net[a].iter().enumerate() {
4133 if j > 0 && b == 0 {
4134 if !near(row.last().unwrap_or(cell), cell) {
4135 ogeom_bail!(Construction, "neighbouring patches part along v");
4136 }
4137 continue;
4138 }
4139 row.push(*cell);
4140 }
4141 }
4142 rows.push(row);
4143 }
4144 }
4145 let joined_knots = |parts: Vec<&KnotVector>, degree: usize| -> OgeomResult<KnotVector> {
4149 let mut out: Vec<f64> = Vec::new();
4150 for (k, knots) in parts.iter().enumerate() {
4151 #[allow(clippy::cast_precision_loss)]
4152 let shift = k as f64;
4153 let values: Vec<f64> = knots.knots().iter().map(|x| x + shift).collect();
4154 if k == 0 {
4155 out.extend(values);
4156 } else {
4157 out.pop();
4158 out.extend_from_slice(&values[degree + 1..]);
4159 }
4160 }
4161 KnotVector::new(out, degree)
4162 };
4163 let u = joined_knots((0..m).map(|i| &grid[i][0].u).collect(), pu)?;
4164 let v = joined_knots((0..n).map(|j| &grid[0][j].v).collect(), pv)?;
4165 let (nu, nv) = (rows.len(), rows[0].len());
4166 let cells: Vec<Weighted<Point>> = rows.into_iter().flatten().collect();
4167 Ok(
4168 ogeom_geom::BSplineSurface::rational(u, v, ogeom_math::ControlGrid::new(cells, nu, nv)?)?
4169 .into(),
4170 )
4171}
4172
4173#[cfg(test)]
4174#[allow(clippy::unwrap_used)]
4175mod tests {
4176 use super::*;
4177
4178 const T: Tolerances = Tolerances::millimetres();
4179
4180 #[test]
4184 fn an_edges_window_is_taken_between_its_vertices_feet() {
4185 let spline: Curve = BSplineCurve::new(
4186 KnotVector::clamped_uniform(3, 6).unwrap(),
4187 vec![
4188 Point::new(0.0, 0.0, 0.0),
4189 Point::new(1.0, 2.0, 0.5),
4190 Point::new(2.5, 1.0, -0.5),
4191 Point::new(4.0, 3.0, 1.0),
4192 Point::new(5.0, 0.5, 0.0),
4193 Point::new(6.0, 2.0, 2.0),
4194 ],
4195 T,
4196 )
4197 .unwrap()
4198 .into();
4199 let at = |t: f64| spline.point_at(t, T).unwrap();
4200 let (a, b) = window_between_feet(&spline, at(0.3), at(0.7), T).unwrap();
4201 assert!(
4202 (a - 0.3).abs() < 1e-9 && (b - 0.7).abs() < 1e-9,
4203 "{a} .. {b}"
4204 );
4205 let (a, b) = window_between_feet(&spline, at(0.0), at(0.7), T).unwrap();
4206 assert!(a.abs() < 1e-9 && (b - 0.7).abs() < 1e-9, "{a} .. {b}");
4207 assert!(window_between_feet(&spline, at(0.0), at(1.0), T).is_none());
4208 assert!(window_between_feet(&spline, at(0.7), at(0.3), T).is_none());
4209 let off = at(0.3) + Vector::new(0.0, 0.0, 0.5);
4210 assert!(window_between_feet(&spline, off, at(0.7), T).is_none());
4211 }
4212}