1use std::collections::HashMap;
46use std::fmt::Write as _;
47
48use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail};
49use ogeom_geom::{
50 BSplineCurve, BSplineSurface, Circle2d, CircleCurve, ConeSurface, Curve, Curve2d as _,
51 Curve3d as _, CylinderSurface, Ellipse2d, EllipseCurve, ExtrusionSurface, HyperbolaCurve,
52 Line2d, LineCurve, ParabolaCurve, PlanarCurve, PlaneSurface, RevolutionSurface, SphereSurface,
53 Surface as _, SurfaceGeometry, TorusSurface, TrimmedCurve, TrimmedSurface,
54};
55use ogeom_math::{
56 Axis, Axis2, Circle, Circle2, Cone, ControlGrid, Cylinder, Direction, Direction2, Ellipse,
57 Ellipse2, Frame, Frame2, Hyperbola, KnotVector, Matrix3, Parabola, Plane, Point, Point2,
58 Sphere, Torus, Transform, Vector, Vector2, Weighted,
59};
60use ogeom_topo::{
61 CurveId, EdgeData, EdgeRepr, FaceData, Location, Model, NodeData, Orientation, PCurveId, Shape,
62 ShapeType, SurfaceId, TShapeId, VertexData,
63};
64
65const CONTENT_TYPE: &str = "DBRep_DrawableShape";
68const VERSION: &str = "CASCADE Topology V1, (c) Matra-Datavision";
69
70pub fn write(model: &Model, root: &Shape, tol: Tolerances) -> OgeomResult<String> {
82 let mut tables = Tables::default();
83 tables.gather(model, root)?;
84
85 let mut out = String::new();
86 let _ = writeln!(out, "{CONTENT_TYPE}\n");
87 let _ = writeln!(out, "{VERSION}");
88
89 let _ = writeln!(out, "Locations {}", tables.locations.len());
94 for location in &tables.locations {
95 let transform = location.composed(model.datums())?;
96 let linear = transform.linear();
97 let scale = transform.scale_factor();
98 let translation = transform.translation_vector();
99 for row in 0..3 {
100 let mut line = String::new();
101 for column in 0..3 {
102 let _ = write!(line, " {}", real(linear.get(row, column)? * scale));
103 }
104 let _ = write!(
105 line,
106 " {}",
107 real(match row {
108 0 => translation.x,
109 1 => translation.y,
110 _ => translation.z,
111 })
112 );
113 let _ = writeln!(out, "1{line}");
114 }
115 }
116
117 let _ = writeln!(out, "Curve2ds {}", tables.pcurves.len());
118 for id in &tables.pcurves {
119 let Some(geometry) = model.geometry().pcurve(*id) else {
120 ogeom_bail!(Dangling, "a pcurve is not in this model");
121 };
122 write_pcurve(&mut out, geometry)?;
123 }
124
125 let _ = writeln!(out, "Curves {}", tables.curves.len());
126 for id in &tables.curves {
127 let Some(geometry) = model.geometry().curve(*id) else {
128 ogeom_bail!(Dangling, "a curve is not in this model");
129 };
130 write_curve(&mut out, geometry)?;
131 }
132
133 let _ = writeln!(out, "Polygon3D 0");
136 let _ = writeln!(out, "PolygonOnTriangulations 0");
137
138 let _ = writeln!(out, "Surfaces {}", tables.surfaces.len());
139 for id in &tables.surfaces {
140 let Some(geometry) = model.geometry().surface(*id) else {
141 ogeom_bail!(Dangling, "a surface is not in this model");
142 };
143 write_surface(&mut out, geometry)?;
144 }
145 let _ = writeln!(out, "Triangulations 0");
146
147 let total = tables.order.len();
150 let _ = writeln!(out, "\nTShapes {total}");
151 let number_of = |node: TShapeId| -> OgeomResult<usize> {
152 let Some(position) = tables.index_of.get(&node) else {
153 ogeom_bail!(Construction, "a subshape was never written");
154 };
155 Ok(total - position)
156 };
157
158 for node_id in &tables.order {
159 let shape = Shape::of(*node_id);
160 let Some(node) = model.node(&shape) else {
161 ogeom_bail!(Dangling, "a shape is not in this model");
162 };
163 write_record(&mut out, model, &shape, &tables, tol)?;
164
165 let _ = writeln!(out, "\n0001000");
171 let mut line = String::new();
172 for child in node.children() {
173 let _ = write!(
174 line,
175 "{}{} {} ",
176 match child.orientation() {
177 Orientation::Forward => "+",
178 Orientation::Reversed => "-",
179 Orientation::Internal => "i",
180 Orientation::External => "e",
181 },
182 number_of(child.node())?,
183 tables.location_number(child.location()),
184 );
185 }
186 let _ = writeln!(out, "{line}*");
187 }
188
189 let _ = writeln!(
191 out,
192 "\n{}{} {}",
193 match root.orientation() {
194 Orientation::Forward => "+",
195 Orientation::Reversed => "-",
196 Orientation::Internal => "i",
197 Orientation::External => "e",
198 },
199 number_of(root.node())?,
200 tables.location_number(root.location()),
201 );
202 Ok(out)
203}
204
205#[derive(Default)]
208struct Tables {
209 curves: Vec<CurveId>,
210 pcurves: Vec<PCurveId>,
211 surfaces: Vec<SurfaceId>,
212 locations: Vec<Location>,
213 order: Vec<TShapeId>,
214 index_of: HashMap<TShapeId, usize>,
215 curve_at: HashMap<CurveId, usize>,
216 pcurve_at: HashMap<PCurveId, usize>,
217 surface_at: HashMap<SurfaceId, usize>,
218 location_at: HashMap<Location, usize>,
219}
220
221impl Tables {
222 fn gather(&mut self, model: &Model, root: &Shape) -> OgeomResult<()> {
224 self.visit(model, root)
225 }
226
227 fn visit(&mut self, model: &Model, shape: &Shape) -> OgeomResult<()> {
228 if self.index_of.contains_key(&shape.node()) {
229 return Ok(());
230 }
231 let Some(node) = model.node(shape) else {
232 ogeom_bail!(Dangling, "shape is not in this model");
233 };
234 for child in node.children() {
237 self.visit(model, child)?;
238 self.take_location(child.location());
239 }
240 match node.data() {
241 NodeData::Edge(data) => {
242 for representation in &data.representations {
243 match representation {
244 EdgeRepr::Curve3d { curve, .. } => self.take_curve(*curve),
245 EdgeRepr::PCurve {
246 curve,
247 surface,
248 location,
249 ..
250 } => {
251 self.take_pcurve(*curve);
252 self.take_surface(*surface);
253 self.take_location(location);
254 }
255 EdgeRepr::Seam {
256 forward,
257 reversed,
258 surface,
259 location,
260 ..
261 } => {
262 self.take_pcurve(*forward);
263 self.take_pcurve(*reversed);
264 self.take_surface(*surface);
265 self.take_location(location);
266 }
267 _ => {}
268 }
269 }
270 }
271 NodeData::Face(data) => {
272 self.take_surface(data.surface);
273 self.take_location(&data.location);
274 }
275 NodeData::Vertex(_) | NodeData::Container => {}
276 }
277 self.index_of.insert(shape.node(), self.order.len());
278 self.order.push(shape.node());
279 Ok(())
280 }
281
282 fn take_curve(&mut self, id: CurveId) {
283 if !self.curve_at.contains_key(&id) {
284 self.curve_at.insert(id, self.curves.len());
285 self.curves.push(id);
286 }
287 }
288
289 fn take_pcurve(&mut self, id: PCurveId) {
290 if !self.pcurve_at.contains_key(&id) {
291 self.pcurve_at.insert(id, self.pcurves.len());
292 self.pcurves.push(id);
293 }
294 }
295
296 fn take_surface(&mut self, id: SurfaceId) {
297 if !self.surface_at.contains_key(&id) {
298 self.surface_at.insert(id, self.surfaces.len());
299 self.surfaces.push(id);
300 }
301 }
302
303 fn take_location(&mut self, location: &Location) {
304 if location.is_identity() || self.location_at.contains_key(location) {
305 return;
306 }
307 self.location_at
308 .insert(location.clone(), self.locations.len());
309 self.locations.push(location.clone());
310 }
311
312 fn location_number(&self, location: &Location) -> usize {
315 if location.is_identity() {
316 return 0;
317 }
318 self.location_at.get(location).map_or(0, |at| at + 1)
319 }
320
321 fn curve_number(&self, id: CurveId) -> usize {
322 self.curve_at.get(&id).map_or(0, |at| at + 1)
323 }
324
325 fn pcurve_number(&self, id: PCurveId) -> usize {
326 self.pcurve_at.get(&id).map_or(0, |at| at + 1)
327 }
328
329 fn surface_number(&self, id: SurfaceId) -> usize {
330 self.surface_at.get(&id).map_or(0, |at| at + 1)
331 }
332}
333
334fn write_record(
336 out: &mut String,
337 model: &Model,
338 shape: &Shape,
339 tables: &Tables,
340 tol: Tolerances,
341) -> OgeomResult<()> {
342 let Some(node) = model.node(shape) else {
343 ogeom_bail!(Dangling, "a shape is not in this model");
344 };
345 match node.data() {
346 NodeData::Vertex(data) => {
347 let _ = writeln!(out, "Ve");
348 let _ = writeln!(out, "{}", real(data.tolerance.get()));
349 let _ = writeln!(
350 out,
351 "{} {} {}",
352 real(data.point.x),
353 real(data.point.y),
354 real(data.point.z)
355 );
356 let _ = writeln!(out, "0 0");
359 }
360 NodeData::Edge(data) => {
361 let _ = writeln!(out, "Ed");
362 let _ = writeln!(
363 out,
364 " {} {} {} {}",
365 real(data.tolerance.get()),
366 usize::from(data.same_parameter()),
367 1,
368 usize::from(data.degenerate)
369 );
370 for representation in &data.representations {
371 match representation {
372 EdgeRepr::Curve3d { curve, range, .. } => {
373 let _ = writeln!(
374 out,
375 "1 {} 0 {} {}",
376 tables.curve_number(*curve),
377 real(range.0),
378 real(range.1)
379 );
380 }
381 EdgeRepr::PCurve {
382 curve,
383 range,
384 surface,
385 location,
386 } => {
387 let _ = writeln!(
388 out,
389 "2 {} {} {} {} {}",
390 tables.pcurve_number(*curve),
391 tables.surface_number(*surface),
392 tables.location_number(location),
393 real(range.0),
394 real(range.1)
395 );
396 }
397 EdgeRepr::Seam {
398 forward,
399 reversed,
400 range,
401 surface,
402 location,
403 } => {
404 let _ = writeln!(
405 out,
406 "3 {} {} C0 {} {} {} {}",
407 tables.pcurve_number(*forward),
408 tables.pcurve_number(*reversed),
409 tables.surface_number(*surface),
410 tables.location_number(location),
411 real(range.0),
412 real(range.1)
413 );
414 }
415 other => ogeom_bail!(
416 Construction,
417 "an edge carries a representation this format has no \
418 record for: {other:?}"
419 ),
420 }
421 }
422 let _ = writeln!(out, "0");
423 }
424 NodeData::Face(data) => {
425 let _ = writeln!(out, "Fa");
426 let _ = writeln!(
427 out,
428 "0 {} {} {}",
429 real(data.tolerance.get()),
430 tables.surface_number(data.surface),
431 tables.location_number(&data.location)
432 );
433 }
434 NodeData::Container => {
435 let kind = match model.kind_of(shape)? {
436 ShapeType::Wire => "Wi",
437 ShapeType::Shell => "Sh",
438 ShapeType::Solid => "So",
439 ShapeType::CompSolid => "CS",
440 ShapeType::Compound => "Co",
441 other => ogeom_bail!(Construction, "a {other:?} has no record in this format"),
442 };
443 let _ = writeln!(out, "{kind}\n");
444 }
445 }
446 let _ = tol;
447 Ok(())
448}
449
450fn write_curve(out: &mut String, curve: &Curve) -> OgeomResult<()> {
451 match curve {
452 Curve::Line(l) => {
453 let axis = l.axis();
454 let _ = writeln!(
455 out,
456 "1 {} {}",
457 point(axis.location),
458 direction(axis.direction.vector())
459 );
460 }
461 Curve::Circle(c) => {
462 let circle = c.circle();
463 let frame = circle.frame();
464 let _ = writeln!(
465 out,
466 "2 {} {} {} {} {}",
467 point(frame.origin()),
468 direction(frame.z().vector()),
469 direction(frame.x().vector()),
470 direction(frame.y().vector()),
471 real(circle.radius())
472 );
473 }
474 Curve::Ellipse(e) => {
475 let ellipse = e.ellipse();
476 let frame = ellipse.frame();
477 let _ = writeln!(
478 out,
479 "3 {} {} {} {} {} {}",
480 point(frame.origin()),
481 direction(frame.z().vector()),
482 direction(frame.x().vector()),
483 direction(frame.y().vector()),
484 real(ellipse.major_radius()),
485 real(ellipse.minor_radius())
486 );
487 }
488 Curve::Parabola(p) => {
489 let parabola = p.parabola();
490 let frame = parabola.frame();
491 let _ = writeln!(
492 out,
493 "4 {} {} {} {} {}",
494 point(frame.origin()),
495 direction(frame.z().vector()),
496 direction(frame.x().vector()),
497 direction(frame.y().vector()),
498 real(parabola.focal())
499 );
500 }
501 Curve::Hyperbola(h) => {
502 let hyperbola = h.hyperbola();
503 let frame = hyperbola.frame();
504 let _ = writeln!(
505 out,
506 "5 {} {} {} {} {} {}",
507 point(frame.origin()),
508 direction(frame.z().vector()),
509 direction(frame.x().vector()),
510 direction(frame.y().vector()),
511 real(hyperbola.major_radius()),
512 real(hyperbola.minor_radius())
513 );
514 }
515 Curve::BSpline(b) => {
516 let rational = b
517 .control_points()
518 .iter()
519 .any(|c| (c.weight - 1.0).abs() > 0.0);
520 let distinct = b.knots().distinct();
521 let mut line = format!(
522 "7 {} 0 {} {} {}",
523 usize::from(rational),
524 b.knots().degree(),
525 b.control_points().len(),
526 distinct.len()
527 );
528 for control in b.control_points() {
529 let at = control.point();
530 let _ = write!(line, " {}", point(at));
531 if rational {
532 let _ = write!(line, " {}", real(control.weight));
533 }
534 }
535 let _ = writeln!(out, "{line}");
536 let mut knot_line = String::new();
537 for (value, multiplicity) in distinct {
538 let _ = write!(knot_line, " {} {multiplicity}", real(value));
539 }
540 let _ = writeln!(out, "{knot_line}");
541 }
542 Curve::Trimmed(t) => {
543 let (lo, hi) = t.domain();
544 let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
545 write_curve(out, t.basis())?;
546 }
547 Curve::Offset(o) => {
548 let _ = writeln!(
549 out,
550 "9 {} {}",
551 real(o.distance()),
552 direction(o.reference().vector())
553 );
554 write_curve(out, o.basis())?;
555 }
556 Curve::Helix(_) | Curve::OnSurface(_) => ogeom_bail!(
557 Construction,
558 "the format has no record for a helix or a curve carried on a \
559 surface, and writing one as anything else would be writing a \
560 different curve"
561 ),
562 }
563 Ok(())
564}
565
566fn write_pcurve(out: &mut String, curve: &PlanarCurve) -> OgeomResult<()> {
567 match curve {
568 PlanarCurve::Line(l) => {
569 let axis = l.axis();
570 let _ = writeln!(
571 out,
572 "1 {} {}",
573 point2(axis.location),
574 direction2(axis.direction.vector())
575 );
576 }
577 PlanarCurve::Circle(c) => {
578 let circle = c.circle();
579 let frame = circle.frame();
580 let _ = writeln!(
581 out,
582 "2 {} {} {} {}",
583 point2(frame.origin()),
584 direction2(frame.x().vector()),
585 direction2(frame.y().vector()),
586 real(circle.radius())
587 );
588 }
589 PlanarCurve::Ellipse(e) => {
590 let ellipse = e.ellipse();
591 let frame = ellipse.frame();
592 let _ = writeln!(
593 out,
594 "3 {} {} {} {} {}",
595 point2(frame.origin()),
596 direction2(frame.x().vector()),
597 direction2(frame.y().vector()),
598 real(ellipse.major_radius()),
599 real(ellipse.minor_radius())
600 );
601 }
602 PlanarCurve::BSpline(b) => {
603 let rational = b
604 .control_points()
605 .iter()
606 .any(|c| (c.weight - 1.0).abs() > 0.0);
607 let distinct = b.knots().distinct();
608 let mut line = format!(
609 "7 {} 0 {} {} {}",
610 usize::from(rational),
611 b.knots().degree(),
612 b.control_points().len(),
613 distinct.len()
614 );
615 for control in b.control_points() {
616 let at = control.point();
617 let _ = write!(line, " {}", point2(at));
618 if rational {
619 let _ = write!(line, " {}", real(control.weight));
620 }
621 }
622 let _ = writeln!(out, "{line}");
623 let mut knot_line = String::new();
624 for (value, multiplicity) in distinct {
625 let _ = write!(knot_line, " {} {multiplicity}", real(value));
626 }
627 let _ = writeln!(out, "{knot_line}");
628 }
629 PlanarCurve::Trimmed(t) => {
630 let (lo, hi) = t.domain();
631 let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
632 write_pcurve(out, t.basis())?;
633 }
634 PlanarCurve::Offset(o) => {
635 let _ = writeln!(out, "9 {}", real(o.distance()));
636 write_pcurve(out, o.basis())?;
637 }
638 PlanarCurve::Trig(_) => ogeom_bail!(
639 Construction,
640 "the format has no record for the sinusoidal chart curve an \
641 oblique section leaves on a cylinder; writing it as a spline \
642 would be writing a fit and calling it exact"
643 ),
644 }
645 Ok(())
646}
647
648fn write_surface(out: &mut String, surface: &SurfaceGeometry) -> OgeomResult<()> {
649 let seat = |frame: &Frame| {
652 format!(
653 "{} {} {} {}",
654 point(frame.origin()),
655 direction(frame.z().vector()),
656 direction(frame.x().vector()),
657 direction(frame.y().vector())
658 )
659 };
660 match surface {
661 SurfaceGeometry::Plane(p) => {
662 let _ = writeln!(out, "1 {}", seat(&p.plane().frame()));
663 }
664 SurfaceGeometry::Cylinder(c) => {
665 let _ = writeln!(
666 out,
667 "2 {} {}",
668 seat(&c.cylinder().frame()),
669 real(c.cylinder().radius())
670 );
671 }
672 SurfaceGeometry::Cone(c) => {
673 let cone = c.cone();
674 let _ = writeln!(
675 out,
676 "3 {} {} {}",
677 seat(&cone.frame()),
678 real(cone.reference_radius()),
679 real(cone.half_angle())
680 );
681 }
682 SurfaceGeometry::Sphere(s) => {
683 let _ = writeln!(
684 out,
685 "4 {} {}",
686 seat(&s.sphere().frame()),
687 real(s.sphere().radius())
688 );
689 }
690 SurfaceGeometry::Torus(t) => {
691 let torus = t.torus();
692 let _ = writeln!(
693 out,
694 "5 {} {} {}",
695 seat(&torus.frame()),
696 real(torus.major_radius()),
697 real(torus.minor_radius())
698 );
699 }
700 SurfaceGeometry::Extrusion(e) => {
701 let _ = writeln!(out, "6 {}", direction(e.direction().vector()));
702 write_curve(out, e.curve())?;
703 }
704 SurfaceGeometry::Revolution(r) => {
705 let axis = r.axis();
706 let _ = writeln!(
707 out,
708 "7 {} {}",
709 point(axis.location),
710 direction(axis.direction.vector())
711 );
712 write_curve(out, r.curve())?;
713 }
714 SurfaceGeometry::BSpline(b) => {
715 let grid = b.grid();
716 let rational = grid.points().iter().any(|c| (c.weight - 1.0).abs() > 0.0);
717 let u_distinct = b.u_knots().distinct();
718 let v_distinct = b.v_knots().distinct();
719 let _ = writeln!(
720 out,
721 "9 {} {} 0 0 {} {} {} {} {} {}",
722 usize::from(rational),
723 usize::from(rational),
724 b.u_knots().degree(),
725 b.v_knots().degree(),
726 grid.u_count(),
727 grid.v_count(),
728 u_distinct.len(),
729 v_distinct.len()
730 );
731 for i in 0..grid.u_count() {
733 let mut line = String::new();
734 for j in 0..grid.v_count() {
735 let control = grid.points()[i * grid.v_count() + j];
736 let at = control.point();
737 let _ = write!(line, " {}", point(at));
738 if rational {
739 let _ = write!(line, " {}", real(control.weight));
740 }
741 }
742 let _ = writeln!(out, "{line}");
743 }
744 let mut line = String::new();
745 for (value, multiplicity) in u_distinct {
746 let _ = write!(line, " {} {multiplicity}", real(value));
747 }
748 let _ = writeln!(out, "{line}");
749 let mut line = String::new();
750 for (value, multiplicity) in v_distinct {
751 let _ = write!(line, " {} {multiplicity}", real(value));
752 }
753 let _ = writeln!(out, "{line}");
754 }
755 SurfaceGeometry::Trimmed(t) => {
756 let ((u0, u1), (v0, v1)) = t.domain();
757 let _ = writeln!(
758 out,
759 "10 {} {} {} {}",
760 real(u0),
761 real(u1),
762 real(v0),
763 real(v1)
764 );
765 write_surface(out, t.basis())?;
766 }
767 SurfaceGeometry::Offset(o) => {
768 let _ = writeln!(out, "11 {}", real(o.distance()));
769 write_surface(out, o.basis())?;
770 }
771 }
772 Ok(())
773}
774
775fn real(v: f64) -> String {
777 let mut text = format!("{v:?}");
778 if text.ends_with(".0") {
779 text.truncate(text.len() - 2);
780 }
781 text
782}
783
784fn point(p: Point) -> String {
785 format!("{} {} {}", real(p.x), real(p.y), real(p.z))
786}
787
788fn point2(p: Point2) -> String {
789 format!("{} {}", real(p.x), real(p.y))
790}
791
792fn direction(v: Vector) -> String {
793 format!("{} {} {}", real(v.x), real(v.y), real(v.z))
794}
795
796fn direction2(v: Vector2) -> String {
797 format!("{} {}", real(v.x), real(v.y))
798}
799
800pub fn read(text: &str, tol: Tolerances) -> OgeomResult<(Model, Shape)> {
810 let mut lines = text.lines();
811 let Some(first) = lines.next() else {
812 ogeom_bail!(Construction, "the file is empty");
813 };
814 if first.trim() != CONTENT_TYPE {
815 ogeom_bail!(
816 Construction,
817 "this is not the interchange format: it opens with `{}`",
818 first.trim()
819 );
820 }
821 let rest: String = lines.collect::<Vec<_>>().join("\n");
824 let Some(at) = rest.find("Locations") else {
825 ogeom_bail!(Construction, "the file has no placement table");
826 };
827 let mut cursor = Cursor::new(&rest[at..]);
828 let mut model = Model::new();
829 let mut built = Built::default();
830
831 cursor.expect("Locations")?;
832 let count = cursor.count()?;
833 for _ in 0..count {
834 let kind = cursor.count()?;
835 match kind {
836 1 => {
837 let mut rows = [[0.0_f64; 3]; 3];
838 let mut translation = [0.0_f64; 3];
839 for (row, offset) in rows.iter_mut().zip(&mut translation) {
840 for cell in row.iter_mut() {
841 *cell = cursor.number()?;
842 }
843 *offset = cursor.number()?;
844 }
845 built.locations.push(transform_of(rows, translation, tol)?);
846 }
847 2 => {
848 let mut composed = Transform::IDENTITY;
851 loop {
852 let index = cursor.count()?;
853 if index == 0 {
854 break;
855 }
856 let power: i64 = cursor.integer()?;
857 let Some(base) = built.locations.get(index - 1).copied() else {
858 ogeom_bail!(
859 Construction,
860 "a composed placement names placement {index}, which is not above it"
861 );
862 };
863 let step = if power < 0 { base.inverse()? } else { base };
864 for _ in 0..power.abs() {
865 composed = composed * step;
866 }
867 }
868 built.locations.push(composed);
869 }
870 other => ogeom_bail!(
871 Construction,
872 "placement record {other} is not one this reads"
873 ),
874 }
875 }
876
877 cursor.expect("Curve2ds")?;
878 let count = cursor.count()?;
879 for _ in 0..count {
880 let curve = cursor.pcurve(tol)?;
881 built.pcurves.push(model.geometry_mut().add_pcurve(curve));
882 }
883
884 cursor.expect("Curves")?;
885 let count = cursor.count()?;
886 for _ in 0..count {
887 let curve = cursor.curve(tol)?;
888 built.curves.push(model.geometry_mut().add_curve(curve));
889 }
890
891 cursor.expect("Polygon3D")?;
893 let count = cursor.count()?;
894 for _ in 0..count {
895 cursor.skip_polygon3d()?;
896 }
897 cursor.expect("PolygonOnTriangulations")?;
898 let count = cursor.count()?;
899 for _ in 0..count {
900 cursor.skip_polygon_on_triangulation()?;
901 }
902
903 cursor.expect("Surfaces")?;
904 let count = cursor.count()?;
905 for _ in 0..count {
906 let surface = cursor.surface(tol)?;
907 built
908 .surfaces
909 .push(model.geometry_mut().add_surface(surface));
910 }
911
912 cursor.expect("Triangulations")?;
913 let count = cursor.count()?;
914 for _ in 0..count {
915 cursor.skip_triangulation()?;
916 }
917
918 cursor.expect("TShapes")?;
919 let total = cursor.count()?;
920 let mut shapes: Vec<Shape> = Vec::with_capacity(total);
924 for _ in 0..total {
925 let shape = cursor.record(&mut model, &built, &shapes, total, tol)?;
926 shapes.push(shape);
927 }
928 let (orientation, number, location) = cursor.reference()?;
931 let Some(root) = pick(&shapes, number, total) else {
932 ogeom_bail!(
933 Construction,
934 "the file's own shape is numbered {number}, which is not there"
935 );
936 };
937 let root = placed(root, &built, location, &mut model)?.composed(orientation);
938 Ok((model, root))
939}
940
941const OPEN_EXTENT: f64 = 1e6;
948
949#[derive(Default)]
951struct Built {
952 locations: Vec<Transform>,
953 curves: Vec<CurveId>,
954 pcurves: Vec<PCurveId>,
955 surfaces: Vec<SurfaceId>,
956}
957
958fn pick(shapes: &[Shape], number: usize, total: usize) -> Option<Shape> {
960 total
963 .checked_sub(number)
964 .and_then(|at| shapes.get(at))
965 .cloned()
966}
967
968fn transform_of(
970 rows: [[f64; 3]; 3],
971 translation: [f64; 3],
972 tol: Tolerances,
973) -> OgeomResult<Transform> {
974 let m = Matrix3::new(rows);
977 let determinant = m.determinant();
978 if determinant.abs() <= f64::MIN_POSITIVE {
979 ogeom_bail!(
980 Construction,
981 "a placement with a singular matrix places nothing"
982 );
983 }
984 let scale = determinant.cbrt();
985 let mut unit = [[0.0_f64; 3]; 3];
986 for row in 0..3 {
987 for column in 0..3 {
988 unit[row][column] = rows[row][column] / scale;
989 }
990 }
991 Transform::from_parts(
992 Matrix3::new(unit),
993 scale,
994 Vector::new(translation[0], translation[1], translation[2]),
995 tol.angular().max(1e-9),
996 )
997}
998
999fn placed(shape: Shape, built: &Built, number: usize, model: &mut Model) -> OgeomResult<Shape> {
1001 if number == 0 {
1002 return Ok(shape);
1003 }
1004 let Some(transform) = built.locations.get(number - 1) else {
1005 ogeom_bail!(
1006 Construction,
1007 "a shape names placement {number}, which is not there"
1008 );
1009 };
1010 let datum = model.add_datum(*transform);
1011 Ok(shape.moved(&Location::of(datum)))
1012}
1013
1014struct Cursor<'a> {
1016 tokens: Vec<&'a str>,
1017 at: usize,
1018}
1019
1020impl<'a> Cursor<'a> {
1021 fn new(text: &'a str) -> Self {
1022 Self {
1023 tokens: text.split_whitespace().collect(),
1024 at: 0,
1025 }
1026 }
1027
1028 fn word(&mut self) -> OgeomResult<&'a str> {
1029 let Some(token) = self.tokens.get(self.at) else {
1030 ogeom_bail!(Construction, "the file ends in the middle of a record");
1031 };
1032 self.at += 1;
1033 Ok(token)
1034 }
1035
1036 fn peek(&self) -> Option<&'a str> {
1037 self.tokens.get(self.at).copied()
1038 }
1039
1040 fn expect(&mut self, what: &str) -> OgeomResult<()> {
1041 let token = self.word()?;
1042 if token != what {
1043 ogeom_bail!(Construction, "expected the {what} table, found `{token}`");
1044 }
1045 Ok(())
1046 }
1047
1048 fn number(&mut self) -> OgeomResult<f64> {
1049 let token = self.word()?;
1050 let Ok(value) = token.parse::<f64>() else {
1051 ogeom_bail!(Construction, "`{token}` is not a number");
1052 };
1053 Ok(value)
1054 }
1055
1056 fn integer(&mut self) -> OgeomResult<i64> {
1057 let token = self.word()?;
1058 let Ok(value) = token.parse::<i64>() else {
1059 ogeom_bail!(Construction, "`{token}` is not a whole number");
1060 };
1061 Ok(value)
1062 }
1063
1064 fn count(&mut self) -> OgeomResult<usize> {
1065 let value = self.integer()?;
1066 let Ok(count) = usize::try_from(value) else {
1067 ogeom_bail!(Construction, "{value} is not a count");
1068 };
1069 Ok(count)
1070 }
1071
1072 fn flag(&mut self) -> OgeomResult<bool> {
1073 Ok(self.count()? != 0)
1074 }
1075
1076 fn point(&mut self) -> OgeomResult<Point> {
1077 Ok(Point::new(self.number()?, self.number()?, self.number()?))
1078 }
1079
1080 fn point2(&mut self) -> OgeomResult<Point2> {
1081 Ok(Point2::new(self.number()?, self.number()?))
1082 }
1083
1084 fn direction(&mut self, tol: Tolerances) -> OgeomResult<Direction> {
1085 let v = Vector::new(self.number()?, self.number()?, self.number()?);
1086 Direction::new(v, tol)
1087 }
1088
1089 fn direction2(&mut self, tol: Tolerances) -> OgeomResult<Direction2> {
1090 let v = Vector2::new(self.number()?, self.number()?);
1091 Direction2::new(v, tol)
1092 }
1093
1094 fn seat(&mut self, tol: Tolerances) -> OgeomResult<Frame> {
1096 let origin = self.point()?;
1097 let normal = self.direction(tol)?;
1098 let x = self.direction(tol)?;
1099 let _y = self.direction(tol)?;
1100 Frame::new(origin, normal, x, tol)
1101 }
1102
1103 fn seat2(&mut self, tol: Tolerances) -> OgeomResult<Frame2> {
1104 let origin = self.point2()?;
1105 let x = self.direction2(tol)?;
1106 let y = self.direction2(tol)?;
1107 Frame2::from_axes(origin, x, y, tol)
1108 }
1109
1110 fn spline_knots(&mut self, degree: usize, count: usize) -> OgeomResult<KnotVector> {
1112 let mut flat = Vec::new();
1113 for _ in 0..count {
1114 let value = self.number()?;
1115 let multiplicity = self.count()?;
1116 for _ in 0..multiplicity {
1117 flat.push(value);
1118 }
1119 }
1120 KnotVector::new(flat, degree)
1121 }
1122
1123 fn curve(&mut self, tol: Tolerances) -> OgeomResult<Curve> {
1124 let kind = self.count()?;
1125 Ok(match kind {
1126 1 => {
1127 let origin = self.point()?;
1128 let direction = self.direction(tol)?;
1129 LineCurve::over(Axis::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
1132 }
1133 2 => {
1134 let frame = self.seat(tol)?;
1135 CircleCurve::new(Circle::new(frame, self.number()?, tol)?).into()
1136 }
1137 3 => {
1138 let frame = self.seat(tol)?;
1139 let ellipse = Ellipse::new(frame, self.number()?, self.number()?, tol)?;
1140 EllipseCurve::new(ellipse).into()
1141 }
1142 4 => {
1143 let frame = self.seat(tol)?;
1144 let parabola = Parabola::new(frame, self.number()?, tol)?;
1145 ParabolaCurve::new(parabola, OPEN_EXTENT)?.into()
1146 }
1147 5 => {
1148 let frame = self.seat(tol)?;
1149 let hyperbola = Hyperbola::new(frame, self.number()?, self.number()?, tol)?;
1150 HyperbolaCurve::new(hyperbola, OPEN_EXTENT)?.into()
1151 }
1152 7 => {
1153 let rational = self.flag()?;
1154 let _periodic = self.flag()?;
1155 let degree = self.count()?;
1156 let poles = self.count()?;
1157 let knot_count = self.count()?;
1158 let mut control = Vec::with_capacity(poles);
1159 for _ in 0..poles {
1160 let at = self.point()?;
1161 let weight = if rational { self.number()? } else { 1.0 };
1162 control.push(Weighted::new(at, weight, tol)?);
1163 }
1164 let knots = self.spline_knots(degree, knot_count)?;
1165 BSplineCurve::rational(knots, control)?.into()
1166 }
1167 8 => {
1168 let lo = self.number()?;
1169 let hi = self.number()?;
1170 let basis = self.curve(tol)?;
1171 Curve::Trimmed(Box::new(TrimmedCurve::new(basis, lo, hi, tol)?))
1172 }
1173 9 => {
1174 let distance = self.number()?;
1175 let reference = self.direction(tol)?;
1176 let basis = self.curve(tol)?;
1177 Curve::Offset(Box::new(ogeom_geom::OffsetCurve::new(
1178 basis, distance, reference,
1179 )?))
1180 }
1181 other => ogeom_bail!(
1182 Construction,
1183 "curve record {other} is one this does not read; a Bezier \
1184 curve is record 6, and nothing here builds one"
1185 ),
1186 })
1187 }
1188
1189 fn pcurve(&mut self, tol: Tolerances) -> OgeomResult<PlanarCurve> {
1190 let kind = self.count()?;
1191 Ok(match kind {
1192 1 => {
1193 let origin = self.point2()?;
1194 let direction = self.direction2(tol)?;
1195 Line2d::over(Axis2::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
1196 }
1197 2 => {
1198 let frame = self.seat2(tol)?;
1199 Circle2d::new(Circle2::new(frame, self.number()?, tol)?).into()
1200 }
1201 3 => {
1202 let frame = self.seat2(tol)?;
1203 let ellipse = Ellipse2::new(frame, self.number()?, self.number()?, tol)?;
1204 Ellipse2d::new(ellipse).into()
1205 }
1206 7 => {
1207 let rational = self.flag()?;
1208 let _periodic = self.flag()?;
1209 let degree = self.count()?;
1210 let poles = self.count()?;
1211 let knot_count = self.count()?;
1212 let mut control = Vec::with_capacity(poles);
1213 for _ in 0..poles {
1214 let at = self.point2()?;
1215 let weight = if rational { self.number()? } else { 1.0 };
1216 control.push(Weighted::new(at, weight, tol)?);
1217 }
1218 let knots = self.spline_knots(degree, knot_count)?;
1219 ogeom_geom::BSpline2d::rational(knots, control)?.into()
1220 }
1221 8 => {
1222 let lo = self.number()?;
1223 let hi = self.number()?;
1224 let basis = self.pcurve(tol)?;
1225 PlanarCurve::Trimmed(Box::new(ogeom_geom::Trimmed2d::new(basis, lo, hi, tol)?))
1226 }
1227 9 => {
1228 let distance = self.number()?;
1229 let basis = self.pcurve(tol)?;
1230 PlanarCurve::Offset(Box::new(ogeom_geom::Offset2d::new(basis, distance)?))
1231 }
1232 other => ogeom_bail!(
1233 Construction,
1234 "chart curve record {other} is one this does not read"
1235 ),
1236 })
1237 }
1238
1239 fn surface(&mut self, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
1240 let kind = self.count()?;
1241 const REACH: (f64, f64) = (-1e9, 1e9);
1245 Ok(match kind {
1246 1 => {
1247 let frame = self.seat(tol)?;
1248 PlaneSurface::over(Plane::new(frame), REACH, REACH)?.into()
1249 }
1250 2 => {
1251 let frame = self.seat(tol)?;
1252 let cylinder = Cylinder::new(frame, self.number()?, tol)?;
1253 CylinderSurface::new(cylinder, REACH)?.into()
1254 }
1255 3 => {
1256 let frame = self.seat(tol)?;
1257 let cone = Cone::new(frame, self.number()?, self.number()?, tol)?;
1258 ConeSurface::new(cone, REACH)?.into()
1259 }
1260 4 => {
1261 let frame = self.seat(tol)?;
1262 SphereSurface::new(Sphere::new(frame, self.number()?, tol)?).into()
1263 }
1264 5 => {
1265 let frame = self.seat(tol)?;
1266 let torus = Torus::new(frame, self.number()?, self.number()?, tol)?;
1267 TorusSurface::new(torus).into()
1268 }
1269 6 => {
1270 let direction = self.direction(tol)?;
1271 let basis = self.curve(tol)?;
1272 ExtrusionSurface::new(basis, direction, 1e9)?.into()
1273 }
1274 7 => {
1275 let origin = self.point()?;
1276 let direction = self.direction(tol)?;
1277 let basis = self.curve(tol)?;
1278 RevolutionSurface::new(basis, Axis::new(origin, direction), core::f64::consts::TAU)?
1279 .into()
1280 }
1281 9 => {
1282 let u_rational = self.flag()?;
1283 let v_rational = self.flag()?;
1284 let _u_periodic = self.flag()?;
1285 let _v_periodic = self.flag()?;
1286 let u_degree = self.count()?;
1287 let v_degree = self.count()?;
1288 let u_poles = self.count()?;
1289 let v_poles = self.count()?;
1290 let u_knot_count = self.count()?;
1291 let v_knot_count = self.count()?;
1292 let rational = u_rational || v_rational;
1293 let mut points = Vec::with_capacity(u_poles * v_poles);
1294 for _ in 0..u_poles * v_poles {
1295 let at = self.point()?;
1296 let weight = if rational { self.number()? } else { 1.0 };
1297 points.push(Weighted::new(at, weight, tol)?);
1298 }
1299 let u_knots = self.spline_knots(u_degree, u_knot_count)?;
1300 let v_knots = self.spline_knots(v_degree, v_knot_count)?;
1301 let grid = ControlGrid::new(points, u_poles, v_poles)?;
1302 BSplineSurface::rational(u_knots, v_knots, grid)?.into()
1303 }
1304 10 => {
1305 let u = (self.number()?, self.number()?);
1306 let v = (self.number()?, self.number()?);
1307 let basis = self.surface(tol)?;
1308 SurfaceGeometry::Trimmed(Box::new(TrimmedSurface::new(basis, u, v, tol)?))
1309 }
1310 11 => {
1311 let distance = self.number()?;
1312 let basis = self.surface(tol)?;
1313 SurfaceGeometry::Offset(Box::new(ogeom_geom::OffsetSurface::new(basis, distance)?))
1314 }
1315 other => ogeom_bail!(
1316 Construction,
1317 "surface record {other} is one this does not read; a Bezier \
1318 surface is record 8, and nothing here builds one"
1319 ),
1320 })
1321 }
1322
1323 fn reference(&mut self) -> OgeomResult<(Orientation, usize, usize)> {
1326 let token = self.word()?;
1327 let (orientation, digits) = match token.split_at(1) {
1328 ("+", rest) => (Orientation::Forward, rest),
1329 ("-", rest) => (Orientation::Reversed, rest),
1330 ("i", rest) => (Orientation::Internal, rest),
1331 ("e", rest) => (Orientation::External, rest),
1332 _ => ogeom_bail!(Construction, "`{token}` does not name a subshape"),
1333 };
1334 let Ok(number) = digits.parse::<usize>() else {
1335 ogeom_bail!(Construction, "`{token}` does not name a subshape number");
1336 };
1337 Ok((orientation, number, self.count()?))
1338 }
1339
1340 fn skip_polygon3d(&mut self) -> OgeomResult<()> {
1341 let nodes = self.count()?;
1342 let has_parameters = self.flag()?;
1343 let _deflection = self.number()?;
1344 for _ in 0..nodes * 3 {
1345 let _ = self.number()?;
1346 }
1347 if has_parameters {
1348 for _ in 0..nodes {
1349 let _ = self.number()?;
1350 }
1351 }
1352 Ok(())
1353 }
1354
1355 fn skip_polygon_on_triangulation(&mut self) -> OgeomResult<()> {
1356 let nodes = self.count()?;
1357 for _ in 0..nodes {
1358 let _ = self.integer()?;
1359 }
1360 if self.peek() == Some("p") {
1362 let _ = self.word()?;
1363 let _deflection = self.number()?;
1364 let _flag = self.count()?;
1365 let count = self.count()?;
1366 for _ in 0..count {
1367 let _ = self.number()?;
1368 }
1369 }
1370 Ok(())
1371 }
1372
1373 fn skip_triangulation(&mut self) -> OgeomResult<()> {
1374 let nodes = self.count()?;
1375 let triangles = self.count()?;
1376 let has_parameters = self.flag()?;
1377 let _deflection = self.number()?;
1378 for _ in 0..nodes * 3 {
1379 let _ = self.number()?;
1380 }
1381 if has_parameters {
1382 for _ in 0..nodes * 2 {
1383 let _ = self.number()?;
1384 }
1385 }
1386 for _ in 0..triangles * 3 {
1387 let _ = self.integer()?;
1388 }
1389 Ok(())
1390 }
1391
1392 fn record(
1394 &mut self,
1395 model: &mut Model,
1396 built: &Built,
1397 shapes: &[Shape],
1398 total: usize,
1399 tol: Tolerances,
1400 ) -> OgeomResult<Shape> {
1401 let kind = self.word()?;
1402 let data = match kind {
1403 "Ve" => {
1404 let tolerance = self.number()?;
1405 let at = self.point()?;
1406 loop {
1409 let first = self.word()?;
1410 let second = self.word()?;
1411 if first == "0" && second == "0" {
1412 break;
1413 }
1414 match second {
1417 "1" => {
1418 let _ = self.count()?;
1419 }
1420 "2" | "3" => {
1421 let _ = self.count()?;
1422 let _ = self.count()?;
1423 }
1424 other => ogeom_bail!(
1425 Construction,
1426 "a vertex representation of kind {other} is not one this reads"
1427 ),
1428 }
1429 let _ = self.count()?;
1430 }
1431 Record::Vertex(VertexData {
1432 point: at,
1433 tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
1434 })
1435 }
1436 "Ed" => {
1437 let tolerance = self.number()?;
1438 let _same_parameter = self.flag()?;
1439 let _same_range = self.flag()?;
1440 let degenerate = self.flag()?;
1441 let mut data = EdgeData::new();
1442 data.tolerance = Tolerance::new(tolerance.max(tol.confusion()))?;
1443 data.degenerate = degenerate;
1444 loop {
1445 let kind = self.count()?;
1446 match kind {
1447 0 => break,
1448 1 => {
1449 let curve = self.count()?;
1450 let _location = self.count()?;
1451 let lo = self.number()?;
1452 let hi = self.number()?;
1453 let Some(id) = built.curves.get(curve - 1).copied() else {
1454 ogeom_bail!(Construction, "an edge names curve {curve}");
1455 };
1456 data.add(EdgeRepr::Curve3d {
1457 curve: id,
1458 range: (lo, hi),
1459 location: Location::identity(),
1460 });
1461 }
1462 2 => {
1463 let pcurve = self.count()?;
1464 let surface = self.count()?;
1465 let _location = self.count()?;
1466 let lo = self.number()?;
1467 let hi = self.number()?;
1468 let (Some(curve), Some(on)) = (
1469 built.pcurves.get(pcurve - 1).copied(),
1470 built.surfaces.get(surface - 1).copied(),
1471 ) else {
1472 ogeom_bail!(
1473 Construction,
1474 "an edge names chart curve {pcurve} on surface {surface}"
1475 );
1476 };
1477 data.add(EdgeRepr::PCurve {
1478 curve,
1479 range: (lo, hi),
1480 surface: on,
1481 location: Location::identity(),
1482 });
1483 }
1484 3 => {
1485 let forward = self.count()?;
1486 let reversed = self.count()?;
1487 let _continuity = self.word()?;
1488 let surface = self.count()?;
1489 let _location = self.count()?;
1490 let lo = self.number()?;
1491 let hi = self.number()?;
1492 let (Some(f), Some(r), Some(on)) = (
1493 built.pcurves.get(forward - 1).copied(),
1494 built.pcurves.get(reversed - 1).copied(),
1495 built.surfaces.get(surface - 1).copied(),
1496 ) else {
1497 ogeom_bail!(Construction, "an edge names a seam that is not there");
1498 };
1499 data.add(EdgeRepr::Seam {
1500 forward: f,
1501 reversed: r,
1502 range: (lo, hi),
1503 surface: on,
1504 location: Location::identity(),
1505 });
1506 }
1507 4 => {
1508 let _continuity = self.word()?;
1509 for _ in 0..4 {
1510 let _ = self.count()?;
1511 }
1512 }
1513 5 => {
1514 let _ = self.count()?;
1515 let _ = self.count()?;
1516 }
1517 6 => {
1518 for _ in 0..3 {
1519 let _ = self.count()?;
1520 }
1521 }
1522 7 => {
1523 for _ in 0..4 {
1524 let _ = self.count()?;
1525 }
1526 }
1527 other => ogeom_bail!(
1528 Construction,
1529 "an edge representation of kind {other} is not one this reads"
1530 ),
1531 }
1532 }
1533 data.assert_same_parameter(agree_on_parameter(model, &data, tol));
1539 Record::Edge(Box::new(data))
1540 }
1541 "Fa" => {
1542 let _natural = self.flag()?;
1543 let tolerance = self.number()?;
1544 let surface = self.count()?;
1545 let _location = self.count()?;
1546 if self.peek() == Some("2") {
1547 let _ = self.word()?;
1548 let _triangulation = self.count()?;
1549 }
1550 let Some(on) = built.surfaces.get(surface.max(1) - 1).copied() else {
1551 ogeom_bail!(Construction, "a face names surface {surface}");
1552 };
1553 Record::Face(Box::new(FaceData {
1554 surface: on,
1555 location: Location::identity(),
1556 tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
1557 natural_restriction: false,
1558 triangulation: None,
1559 }))
1560 }
1561 "Wi" => Record::Container(ShapeType::Wire),
1562 "Sh" => Record::Container(ShapeType::Shell),
1563 "So" => Record::Container(ShapeType::Solid),
1564 "CS" => Record::Container(ShapeType::CompSolid),
1565 "Co" => Record::Container(ShapeType::Compound),
1566 other => ogeom_bail!(Construction, "`{other}` is not a shape record this reads"),
1567 };
1568
1569 let _flags = self.word()?;
1572 let mut children = Vec::new();
1573 while self.peek() != Some("*") {
1574 let (orientation, number, location) = self.reference()?;
1575 let Some(child) = pick(shapes, number, total) else {
1576 ogeom_bail!(
1577 Construction,
1578 "a record names subshape {number}, which is not above it"
1579 );
1580 };
1581 children.push(placed(child, built, location, model)?.composed(orientation));
1582 }
1583 let _ = self.word()?;
1584
1585 Ok(match data {
1586 Record::Vertex(data) => model.add_vertex(data),
1587 Record::Edge(data) => model.add_edge(*data, &children)?,
1588 Record::Face(data) => model.add_face(*data, &children)?,
1589 Record::Container(ShapeType::Wire) => model.add_wire(&children)?,
1590 Record::Container(ShapeType::Shell) => model.add_shell(&children)?,
1591 Record::Container(ShapeType::Solid) => model.add_solid(&children)?,
1592 Record::Container(ShapeType::CompSolid) => model.add_compsolid(&children)?,
1593 Record::Container(_) => model.add_compound(&children)?,
1594 })
1595 }
1596}
1597
1598enum Record {
1600 Vertex(VertexData),
1601 Edge(Box<EdgeData>),
1602 Face(Box<FaceData>),
1603 Container(ShapeType),
1604}
1605
1606fn agree_on_parameter(model: &Model, data: &EdgeData, tol: Tolerances) -> bool {
1614 use ogeom_geom::Surface as _;
1615 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
1616 return false;
1617 };
1618 let Some(geometry) = model.geometry().curve(*curve) else {
1619 return false;
1620 };
1621 let allowed = data.tolerance.get().max(tol.confusion());
1622 for representation in &data.representations {
1623 let (pcurve, prange, surface) = match representation {
1624 EdgeRepr::PCurve {
1625 curve,
1626 range,
1627 surface,
1628 ..
1629 } => (*curve, *range, *surface),
1630 EdgeRepr::Seam {
1631 forward,
1632 range,
1633 surface,
1634 ..
1635 } => (*forward, *range, *surface),
1636 _ => continue,
1637 };
1638 let (Some(chart), Some(on)) = (
1639 model.geometry().pcurve(pcurve),
1640 model.geometry().surface(surface),
1641 ) else {
1642 return false;
1643 };
1644 for k in 0..=8 {
1645 let f = f64::from(k) / 8.0;
1646 let t = (range.1 - range.0).mul_add(f, range.0);
1647 let pt = (prange.1 - prange.0).mul_add(f, prange.0);
1648 let (Ok(at), Ok(uv)) = (geometry.point_at(t, tol), chart.point_at(pt, tol)) else {
1649 return false;
1650 };
1651 let Ok(lifted) = on.point_at(uv.x, uv.y, tol) else {
1652 return false;
1653 };
1654 if lifted.distance(at) > allowed {
1655 return false;
1656 }
1657 }
1658 }
1659 true
1660}