1use ogeom_core::{FastMap, FastSet};
56use std::borrow::Cow;
57use std::fmt::Write as _;
58
59use ogeom_core::{OgeomResult, Tolerance, Tolerances, ogeom_bail};
60use ogeom_geom::{
61 BSpline2d, BSplineCurve, BSplineSurface, Circle2d, CircleCurve, ConeSurface, Curve,
62 Curve2d as _, Curve3d as _, CylinderSurface, Ellipse2d, EllipseCurve, ExtrusionSurface,
63 Hyperbola2d, HyperbolaCurve, Line2d, LineCurve, Offset2d, OffsetCurve, ParabolaCurve,
64 PlanarCurve, PlaneSurface, Reversible as _, RevolutionSurface, SphereSurface, Surface as _,
65 SurfaceGeometry, TorusSurface, Transformable as _, Trimmed2d, TrimmedCurve, TrimmedSurface,
66};
67use ogeom_math::{
68 Axis, Axis2, Circle, Circle2, Cone, ControlGrid, Cylinder, Direction, Direction2, Ellipse,
69 Ellipse2, Frame, Frame2, Handedness, Hyperbola, Hyperbola2, KnotVector, Matrix3, Parabola,
70 Plane, Point, Point2, Sphere, Torus, Transform, Transform2, TransformKind, Vector, Vector2,
71 Weighted,
72};
73use ogeom_topo::{
74 CurveId, EdgeData, EdgeRepr, FaceData, Location, Model, NodeData, Orientation, PCurveId, Shape,
75 ShapeType, SurfaceId, TShape, TShapeId, VertexData,
76};
77
78const CONTENT_TYPE: &str = "DBRep_DrawableShape";
81const VERSION: &str = "CASCADE Topology V1, (c) Matra-Datavision";
82const VERSION_FAMILY: &str = "CASCADE Topology V";
84
85pub fn write(model: &Model, root: &Shape, tol: Tolerances) -> OgeomResult<String> {
97 let mut tables = Tables::default();
98 tables.gather(model, root, tol)?;
99
100 let mut out = String::new();
101 let _ = writeln!(out, "{CONTENT_TYPE}\n");
102 let _ = writeln!(out, "{VERSION}");
103
104 let _ = writeln!(out, "Locations {}", tables.locations.len());
109 for location in &tables.locations {
110 let transform = location.composed(model.datums())?;
111 let linear = transform.linear();
112 let scale = transform.scale_factor();
113 let translation = transform.translation_vector();
114 let _ = writeln!(out, "1");
116 for row in 0..3 {
117 let mut line = String::new();
118 for column in 0..3 {
119 let _ = write!(line, " {}", real(linear.get(row, column)? * scale));
120 }
121 let _ = write!(
122 line,
123 " {}",
124 real(match row {
125 0 => translation.x,
126 1 => translation.y,
127 _ => translation.z,
128 })
129 );
130 let _ = writeln!(out, "{}", line.trim_start());
131 }
132 }
133
134 let _ = writeln!(out, "Curve2ds {}", tables.pcurves.len());
135 for (id, surface, shift) in &tables.pcurves {
136 let chart = chart(model, *id, *surface, tol)?;
137 let (form, own) = spelled2(&chart, tol)?;
138 if own == *shift {
139 write_pcurve(&mut out, &form)?;
140 } else {
141 let Some(moved) = shifted2(&form, own - shift, tol)? else {
142 ogeom_bail!(Construction, "{UNSHIFTED}");
143 };
144 write_pcurve(&mut out, &moved)?;
145 }
146 }
147
148 let _ = writeln!(out, "Curves {}", tables.curves.len());
149 for (id, shift) in &tables.curves {
150 let Some(geometry) = model.geometry().curve(*id) else {
151 ogeom_bail!(Dangling, "a curve is not in this model");
152 };
153 let (form, own) = spelled(geometry, tol)?;
154 if own == *shift {
155 write_curve(&mut out, &form)?;
156 } else {
157 let Some(moved) = shifted(&form, own - shift, tol)? else {
158 ogeom_bail!(Construction, "{UNSHIFTED}");
159 };
160 write_curve(&mut out, &moved)?;
161 }
162 }
163
164 let _ = writeln!(out, "Polygon3D 0");
167 let _ = writeln!(out, "PolygonOnTriangulations 0");
168
169 let _ = writeln!(out, "Surfaces {}", tables.surfaces.len());
170 for id in &tables.surfaces {
171 let Some(geometry) = model.geometry().surface(*id) else {
172 ogeom_bail!(Dangling, "a surface is not in this model");
173 };
174 write_surface(&mut out, geometry, tol)?;
175 }
176 let _ = writeln!(out, "Triangulations 0");
177
178 let total = tables.order.len();
181 let _ = writeln!(out, "\nTShapes {total}");
182 let number_of = |node: TShapeId| -> OgeomResult<usize> {
183 let Some(position) = tables.index_of.get(&node) else {
184 ogeom_bail!(Construction, "a subshape was never written");
185 };
186 Ok(total - position)
187 };
188
189 for node_id in &tables.order {
190 let shape = Shape::of(*node_id);
191 let Some(node) = model.node(&shape) else {
192 ogeom_bail!(Dangling, "a shape is not in this model");
193 };
194 write_record(&mut out, model, &shape, &tables, tol)?;
195
196 let _ = writeln!(out, "\n0001000");
202 let mut line = String::new();
203 for child in node.children() {
204 let _ = write!(
205 line,
206 "{}{} {} ",
207 match child.orientation() {
208 Orientation::Forward => "+",
209 Orientation::Reversed => "-",
210 Orientation::Internal => "i",
211 Orientation::External => "e",
212 },
213 number_of(child.node())?,
214 tables.location_number(child.location()),
215 );
216 }
217 let _ = writeln!(out, "{line}*");
218 }
219
220 let _ = writeln!(
222 out,
223 "\n{}{} {}",
224 match root.orientation() {
225 Orientation::Forward => "+",
226 Orientation::Reversed => "-",
227 Orientation::Internal => "i",
228 Orientation::External => "e",
229 },
230 number_of(root.node())?,
231 tables.location_number(root.location()),
232 );
233 Ok(out)
234}
235
236const BACKWARDS: &str = "a curve run backwards reached a record unspelled";
239
240const UNSHIFTED: &str = "an edge has a curve the format states only at a shifted \
243 parameter (a conic run backwards over a window not \
244 symmetric about zero), and another of its curves needs \
245 another shift, or has no record that shifts with it";
246
247#[derive(Default)]
253struct Tables {
254 curves: Vec<(CurveId, f64)>,
255 pcurves: Vec<(PCurveId, SurfaceId, f64)>,
256 surfaces: Vec<SurfaceId>,
257 locations: Vec<Location>,
258 order: Vec<TShapeId>,
259 index_of: FastMap<TShapeId, usize>,
260 curve_at: FastMap<(CurveId, u64), usize>,
261 pcurve_at: FastMap<(PCurveId, SurfaceId, u64), usize>,
262 surface_at: FastMap<SurfaceId, usize>,
263 location_at: FastMap<Location, usize>,
264 edge_shift: FastMap<TShapeId, f64>,
267 chart_uses: FastMap<TShapeId, Vec<(usize, Location)>>,
271}
272
273impl Tables {
274 fn gather(&mut self, model: &Model, root: &Shape, tol: Tolerances) -> OgeomResult<()> {
276 self.visit(model, root, tol)?;
277 self.take_location(root.location());
279 Ok(())
280 }
281
282 fn visit(&mut self, model: &Model, shape: &Shape, tol: Tolerances) -> OgeomResult<()> {
283 if self.index_of.contains_key(&shape.node()) {
284 return Ok(());
285 }
286 let Some(node) = model.node(shape) else {
287 ogeom_bail!(Dangling, "shape is not in this model");
288 };
289 for child in node.children() {
292 self.visit(model, child, tol)?;
293 self.take_location(child.location());
294 }
295 match node.data() {
296 NodeData::Edge(data) => {
297 let shift = edge_shift(model, data, tol)?;
298 if shift != 0.0 {
299 self.edge_shift.insert(shape.node(), shift);
300 }
301 for representation in &data.representations {
302 match representation {
303 EdgeRepr::Curve3d {
304 curve, location, ..
305 } => {
306 self.take_curve(*curve, shift);
307 self.take_location(location);
308 }
309 EdgeRepr::PCurve {
310 curve,
311 surface,
312 location,
313 ..
314 } => {
315 self.take_pcurve(*curve, *surface, shift);
316 self.take_surface(*surface);
317 self.take_location(&location.inverted());
318 }
319 EdgeRepr::Seam {
320 forward,
321 reversed,
322 surface,
323 location,
324 ..
325 } => {
326 self.take_pcurve(*forward, *surface, shift);
327 self.take_pcurve(*reversed, *surface, shift);
328 self.take_surface(*surface);
329 self.take_location(&location.inverted());
330 }
331 _ => {}
332 }
333 }
334 }
335 NodeData::Face(data) => {
336 self.take_surface(data.surface);
337 self.take_location(&data.location);
338 for wire in model.children_of(&Shape::of(shape.node()))? {
342 for edge in model.children_of(&wire)? {
343 let Some(edge_data) = model.node(&edge).and_then(|n| n.data().as_edge())
344 else {
345 continue;
346 };
347 let Some(index) = edge_data.representations.iter().position(|r| {
348 Some(r) == edge_data.pcurve_for(data.surface, edge.location())
349 }) else {
350 continue;
351 };
352 let seen = edge.location().inverted().then(&data.location);
353 self.take_location(&seen);
354 let uses = self.chart_uses.entry(edge.node()).or_default();
355 if !uses.contains(&(index, seen.clone())) {
356 uses.push((index, seen));
357 }
358 }
359 }
360 }
361 NodeData::Vertex(_) | NodeData::Container => {}
362 }
363 self.index_of.insert(shape.node(), self.order.len());
364 self.order.push(shape.node());
365 Ok(())
366 }
367
368 fn take_curve(&mut self, id: CurveId, shift: f64) {
369 let key = (id, shift.to_bits());
370 if !self.curve_at.contains_key(&key) {
371 self.curve_at.insert(key, self.curves.len());
372 self.curves.push((id, shift));
373 }
374 }
375
376 fn take_pcurve(&mut self, id: PCurveId, surface: SurfaceId, shift: f64) {
377 let key = (id, surface, shift.to_bits());
378 if !self.pcurve_at.contains_key(&key) {
379 self.pcurve_at.insert(key, self.pcurves.len());
380 self.pcurves.push((id, surface, shift));
381 }
382 }
383
384 fn take_surface(&mut self, id: SurfaceId) {
385 if !self.surface_at.contains_key(&id) {
386 self.surface_at.insert(id, self.surfaces.len());
387 self.surfaces.push(id);
388 }
389 }
390
391 fn take_location(&mut self, location: &Location) {
392 if location.is_identity() || self.location_at.contains_key(location) {
393 return;
394 }
395 self.location_at
396 .insert(location.clone(), self.locations.len());
397 self.locations.push(location.clone());
398 }
399
400 fn location_number(&self, location: &Location) -> usize {
403 if location.is_identity() {
404 return 0;
405 }
406 self.location_at.get(location).map_or(0, |at| at + 1)
407 }
408
409 fn curve_number(&self, id: CurveId, shift: f64) -> usize {
410 self.curve_at
411 .get(&(id, shift.to_bits()))
412 .map_or(0, |at| at + 1)
413 }
414
415 fn pcurve_number(&self, id: PCurveId, surface: SurfaceId, shift: f64) -> usize {
416 self.pcurve_at
417 .get(&(id, surface, shift.to_bits()))
418 .map_or(0, |at| at + 1)
419 }
420
421 fn surface_number(&self, id: SurfaceId) -> usize {
422 self.surface_at.get(&id).map_or(0, |at| at + 1)
423 }
424}
425
426fn write_record(
428 out: &mut String,
429 model: &Model,
430 shape: &Shape,
431 tables: &Tables,
432 tol: Tolerances,
433) -> OgeomResult<()> {
434 let Some(node) = model.node(shape) else {
435 ogeom_bail!(Dangling, "a shape is not in this model");
436 };
437 match node.data() {
438 NodeData::Vertex(data) => {
439 let _ = writeln!(out, "Ve");
440 let _ = writeln!(out, "{}", real(data.tolerance.get()));
441 let _ = writeln!(
442 out,
443 "{} {} {}",
444 real(data.point.x),
445 real(data.point.y),
446 real(data.point.z)
447 );
448 let _ = writeln!(out, "0 0");
451 }
452 NodeData::Edge(data) => {
453 let _ = writeln!(out, "Ed");
454 let _ = writeln!(
455 out,
456 " {} {} {} {}",
457 real(data.tolerance.get()),
458 usize::from(data.same_parameter()),
459 1,
460 usize::from(data.degenerate)
461 );
462 let shift = tables.edge_shift.get(&shape.node()).copied().unwrap_or(0.0);
463 for (index, representation) in data.representations.iter().enumerate() {
464 let uses: Vec<&Location> = tables
468 .chart_uses
469 .get(&shape.node())
470 .map(|uses| {
471 uses.iter()
472 .filter(|(at, _)| *at == index)
473 .map(|(_, seen)| seen)
474 .collect()
475 })
476 .unwrap_or_default();
477 let inverted = representation.location().map(Location::inverted);
478 let seen: Vec<&Location> = if uses.is_empty() {
479 inverted.iter().collect()
480 } else {
481 uses
482 };
483 match representation {
484 EdgeRepr::Curve3d {
485 curve,
486 range,
487 location,
488 } => {
489 let _ = writeln!(
490 out,
491 "1 {} {} {} {}",
492 tables.curve_number(*curve, shift),
493 tables.location_number(location),
494 real(range.0 + shift),
495 real(range.1 + shift)
496 );
497 }
498 EdgeRepr::PCurve {
499 curve,
500 range,
501 surface,
502 ..
503 } => {
504 for seen in seen {
505 let _ = writeln!(
506 out,
507 "2 {} {} {} {} {}",
508 tables.pcurve_number(*curve, *surface, shift),
509 tables.surface_number(*surface),
510 tables.location_number(seen),
511 real(range.0 + shift),
512 real(range.1 + shift)
513 );
514 }
515 }
516 EdgeRepr::Seam {
517 forward,
518 reversed,
519 range,
520 surface,
521 ..
522 } => {
523 for seen in seen {
524 let _ = writeln!(
525 out,
526 "3 {} {} C0 {} {} {} {}",
527 tables.pcurve_number(*forward, *surface, shift),
528 tables.pcurve_number(*reversed, *surface, shift),
529 tables.surface_number(*surface),
530 tables.location_number(seen),
531 real(range.0 + shift),
532 real(range.1 + shift)
533 );
534 }
535 }
536 other => ogeom_bail!(
537 Construction,
538 "an edge carries a representation this format has no \
539 record for: {other:?}"
540 ),
541 }
542 }
543 let _ = writeln!(out, "0");
544 }
545 NodeData::Face(data) => {
546 let _ = writeln!(out, "Fa");
547 let _ = writeln!(
548 out,
549 "0 {} {} {}",
550 real(data.tolerance.get()),
551 tables.surface_number(data.surface),
552 tables.location_number(&data.location)
553 );
554 }
555 NodeData::Container => {
556 let kind = match model.kind_of(shape)? {
557 ShapeType::Wire => "Wi",
558 ShapeType::Shell => "Sh",
559 ShapeType::Solid => "So",
560 ShapeType::CompSolid => "CS",
561 ShapeType::Compound => "Co",
562 other => ogeom_bail!(Construction, "a {other:?} has no record in this format"),
563 };
564 let _ = writeln!(out, "{kind}\n");
565 }
566 }
567 let _ = tol;
568 Ok(())
569}
570
571fn write_curve(out: &mut String, curve: &Curve) -> OgeomResult<()> {
573 let backwards = match curve {
574 Curve::Circle(c) => c.is_reversed(),
575 Curve::Ellipse(e) => e.is_reversed(),
576 Curve::Hyperbola(h) => h.is_reversed(),
577 Curve::Parabola(p) => p.is_reversed(),
578 Curve::Trimmed(t) => t.is_reversed(),
579 _ => false,
580 };
581 if backwards {
582 ogeom_bail!(Construction, "{BACKWARDS}");
583 }
584 match curve {
585 Curve::Line(l) => {
586 let axis = l.axis();
587 let _ = writeln!(
588 out,
589 "1 {} {}",
590 point(axis.location),
591 direction(axis.direction.vector())
592 );
593 }
594 Curve::Circle(c) => {
595 let circle = c.circle();
596 let frame = circle.frame();
597 let _ = writeln!(
598 out,
599 "2 {} {} {} {} {}",
600 point(frame.origin()),
601 direction(frame.z().vector()),
602 direction(frame.x().vector()),
603 direction(frame.y().vector()),
604 real(circle.radius())
605 );
606 }
607 Curve::Ellipse(e) => {
608 let ellipse = e.ellipse();
609 let frame = ellipse.frame();
610 let _ = writeln!(
611 out,
612 "3 {} {} {} {} {} {}",
613 point(frame.origin()),
614 direction(frame.z().vector()),
615 direction(frame.x().vector()),
616 direction(frame.y().vector()),
617 real(ellipse.major_radius()),
618 real(ellipse.minor_radius())
619 );
620 }
621 Curve::Parabola(p) => {
622 let parabola = p.parabola();
623 let frame = parabola.frame();
624 let _ = writeln!(
625 out,
626 "4 {} {} {} {} {}",
627 point(frame.origin()),
628 direction(frame.z().vector()),
629 direction(frame.x().vector()),
630 direction(frame.y().vector()),
631 real(parabola.focal())
632 );
633 }
634 Curve::Hyperbola(h) => {
635 let hyperbola = h.hyperbola();
636 let frame = hyperbola.frame();
637 let _ = writeln!(
638 out,
639 "5 {} {} {} {} {} {}",
640 point(frame.origin()),
641 direction(frame.z().vector()),
642 direction(frame.x().vector()),
643 direction(frame.y().vector()),
644 real(hyperbola.major_radius()),
645 real(hyperbola.minor_radius())
646 );
647 }
648 Curve::BSpline(b) => {
649 let rational = b
650 .control_points()
651 .iter()
652 .any(|c| (c.weight - 1.0).abs() > 0.0);
653 let distinct = b.knots().distinct();
654 let mut line = format!(
655 "7 {} 0 {} {} {}",
656 usize::from(rational),
657 b.knots().degree(),
658 b.control_points().len(),
659 distinct.len()
660 );
661 for control in b.control_points() {
662 let at = control.point();
663 let _ = write!(line, " {}", point(at));
664 if rational {
665 let _ = write!(line, " {}", real(control.weight));
666 }
667 }
668 let _ = writeln!(out, "{line}");
669 let mut knot_line = String::new();
670 for (value, multiplicity) in distinct {
671 let _ = write!(knot_line, " {} {multiplicity}", real(value));
672 }
673 let _ = writeln!(out, "{knot_line}");
674 }
675 Curve::Trimmed(t) => {
676 let (lo, hi) = t.domain();
677 let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
678 write_curve(out, t.basis())?;
679 }
680 Curve::Offset(o) => {
681 let _ = writeln!(
682 out,
683 "9 {} {}",
684 real(o.distance()),
685 direction(o.reference().vector())
686 );
687 write_curve(out, o.basis())?;
688 }
689 Curve::Helix(_) | Curve::OnSurface(_) => ogeom_bail!(
690 Construction,
691 "the format has no record for a helix or a curve carried on a \
692 surface, and writing one as anything else would be writing a \
693 different curve"
694 ),
695 }
696 Ok(())
697}
698
699fn write_pcurve(out: &mut String, curve: &PlanarCurve) -> OgeomResult<()> {
701 let backwards = match curve {
702 PlanarCurve::Circle(c) => c.is_reversed(),
703 PlanarCurve::Ellipse(e) => e.is_reversed(),
704 PlanarCurve::Hyperbola(h) => h.is_reversed(),
705 PlanarCurve::Trimmed(t) => t.is_reversed(),
706 _ => false,
707 };
708 if backwards {
709 ogeom_bail!(Construction, "{BACKWARDS}");
710 }
711 match curve {
712 PlanarCurve::Line(l) => {
713 let axis = l.axis();
714 let _ = writeln!(
715 out,
716 "1 {} {}",
717 point2(axis.location),
718 direction2(axis.direction.vector())
719 );
720 }
721 PlanarCurve::Circle(c) => {
722 let circle = c.circle();
723 let frame = circle.frame();
724 let _ = writeln!(
725 out,
726 "2 {} {} {} {}",
727 point2(frame.origin()),
728 direction2(frame.x().vector()),
729 direction2(frame.y().vector()),
730 real(circle.radius())
731 );
732 }
733 PlanarCurve::Ellipse(e) => {
734 let ellipse = e.ellipse();
735 let frame = ellipse.frame();
736 let _ = writeln!(
737 out,
738 "3 {} {} {} {} {}",
739 point2(frame.origin()),
740 direction2(frame.x().vector()),
741 direction2(frame.y().vector()),
742 real(ellipse.major_radius()),
743 real(ellipse.minor_radius())
744 );
745 }
746 PlanarCurve::Hyperbola(h) => {
747 let hyperbola = h.hyperbola();
748 let frame = hyperbola.frame();
749 let _ = writeln!(
750 out,
751 "5 {} {} {} {} {}",
752 point2(frame.origin()),
753 direction2(frame.x().vector()),
754 direction2(frame.y().vector()),
755 real(hyperbola.major_radius()),
756 real(hyperbola.minor_radius())
757 );
758 }
759 PlanarCurve::BSpline(b) => {
760 let rational = b
761 .control_points()
762 .iter()
763 .any(|c| (c.weight - 1.0).abs() > 0.0);
764 let distinct = b.knots().distinct();
765 let mut line = format!(
766 "7 {} 0 {} {} {}",
767 usize::from(rational),
768 b.knots().degree(),
769 b.control_points().len(),
770 distinct.len()
771 );
772 for control in b.control_points() {
773 let at = control.point();
774 let _ = write!(line, " {}", point2(at));
775 if rational {
776 let _ = write!(line, " {}", real(control.weight));
777 }
778 }
779 let _ = writeln!(out, "{line}");
780 let mut knot_line = String::new();
781 for (value, multiplicity) in distinct {
782 let _ = write!(knot_line, " {} {multiplicity}", real(value));
783 }
784 let _ = writeln!(out, "{knot_line}");
785 }
786 PlanarCurve::Trimmed(t) => {
787 let (lo, hi) = t.domain();
788 let _ = writeln!(out, "8 {} {}", real(lo), real(hi));
789 write_pcurve(out, t.basis())?;
790 }
791 PlanarCurve::Offset(o) => {
792 let _ = writeln!(out, "9 {}", real(o.distance()));
793 write_pcurve(out, o.basis())?;
794 }
795 PlanarCurve::Trig(_) => ogeom_bail!(
796 Construction,
797 "the format has no record for the sinusoidal chart curve an \
798 oblique section leaves on a cylinder; writing it as a spline \
799 would be writing a fit and calling it exact"
800 ),
801 }
802 Ok(())
803}
804
805fn write_surface(out: &mut String, surface: &SurfaceGeometry, tol: Tolerances) -> OgeomResult<()> {
806 let seat = |frame: &Frame| {
809 format!(
810 "{} {} {} {}",
811 point(frame.origin()),
812 direction(frame.z().vector()),
813 direction(frame.x().vector()),
814 direction(frame.y().vector())
815 )
816 };
817 match surface {
818 SurfaceGeometry::Plane(p) => {
819 let _ = writeln!(out, "1 {}", seat(&p.plane().frame()));
820 }
821 SurfaceGeometry::Cylinder(c) => {
822 let _ = writeln!(
823 out,
824 "2 {} {}",
825 seat(&c.cylinder().frame()),
826 real(c.cylinder().radius())
827 );
828 }
829 SurfaceGeometry::Cone(c) => {
830 let cone = c.cone();
831 let _ = writeln!(
832 out,
833 "3 {} {} {}",
834 seat(&cone.frame()),
835 real(cone.reference_radius()),
836 real(cone.half_angle())
837 );
838 }
839 SurfaceGeometry::Sphere(s) => {
840 let _ = writeln!(
841 out,
842 "4 {} {}",
843 seat(&s.sphere().frame()),
844 real(s.sphere().radius())
845 );
846 }
847 SurfaceGeometry::Torus(t) => {
848 let torus = t.torus();
849 let _ = writeln!(
850 out,
851 "5 {} {} {}",
852 seat(&torus.frame()),
853 real(torus.major_radius()),
854 real(torus.minor_radius())
855 );
856 }
857 SurfaceGeometry::Extrusion(e) => {
858 let _ = writeln!(out, "6 {}", direction(e.direction().vector()));
859 write_curve(out, &spelled(e.curve(), tol)?.0)?;
860 }
861 SurfaceGeometry::Revolution(r) => {
862 let axis = r.axis();
863 let _ = writeln!(
864 out,
865 "7 {} {}",
866 point(axis.location),
867 direction(axis.direction.vector())
868 );
869 write_curve(out, &spelled(r.curve(), tol)?.0)?;
870 }
871 SurfaceGeometry::BSpline(b) => {
872 let grid = b.grid();
873 let rational = grid.points().iter().any(|c| (c.weight - 1.0).abs() > 0.0);
874 let u_distinct = b.u_knots().distinct();
875 let v_distinct = b.v_knots().distinct();
876 let _ = writeln!(
877 out,
878 "9 {} {} 0 0 {} {} {} {} {} {}",
879 usize::from(rational),
880 usize::from(rational),
881 b.u_knots().degree(),
882 b.v_knots().degree(),
883 grid.u_count(),
884 grid.v_count(),
885 u_distinct.len(),
886 v_distinct.len()
887 );
888 for i in 0..grid.u_count() {
890 let mut line = String::new();
891 for j in 0..grid.v_count() {
892 let control = grid.points()[i * grid.v_count() + j];
893 let at = control.point();
894 let _ = write!(line, " {}", point(at));
895 if rational {
896 let _ = write!(line, " {}", real(control.weight));
897 }
898 }
899 let _ = writeln!(out, "{line}");
900 }
901 let mut line = String::new();
902 for (value, multiplicity) in u_distinct {
903 let _ = write!(line, " {} {multiplicity}", real(value));
904 }
905 let _ = writeln!(out, "{line}");
906 let mut line = String::new();
907 for (value, multiplicity) in v_distinct {
908 let _ = write!(line, " {} {multiplicity}", real(value));
909 }
910 let _ = writeln!(out, "{line}");
911 }
912 SurfaceGeometry::Trimmed(t) => {
913 let ((u0, u1), (v0, v1)) = t.domain();
914 let moved = chart_shift(t.basis(), tol)?;
915 let ((u0, u1), (v0, v1)) = ((u0 + moved.x, u1 + moved.x), (v0 + moved.y, v1 + moved.y));
916 let _ = writeln!(
917 out,
918 "10 {} {} {} {}",
919 real(u0),
920 real(u1),
921 real(v0),
922 real(v1)
923 );
924 write_surface(out, t.basis(), tol)?;
925 }
926 SurfaceGeometry::Offset(o) => {
927 let _ = writeln!(out, "11 {}", real(o.distance()));
928 write_surface(out, o.basis(), tol)?;
929 }
930 }
931 Ok(())
932}
933
934fn real(v: f64) -> String {
936 let mut text = format!("{v:?}");
937 if text.ends_with(".0") {
938 text.truncate(text.len() - 2);
939 }
940 text
941}
942
943fn point(p: Point) -> String {
944 format!("{} {} {}", real(p.x), real(p.y), real(p.z))
945}
946
947fn point2(p: Point2) -> String {
948 format!("{} {}", real(p.x), real(p.y))
949}
950
951fn direction(v: Vector) -> String {
952 format!("{} {} {}", real(v.x), real(v.y), real(v.z))
953}
954
955fn direction2(v: Vector2) -> String {
956 format!("{} {}", real(v.x), real(v.y))
957}
958
959fn spelled(curve: &Curve, tol: Tolerances) -> OgeomResult<(Cow<'_, Curve>, f64)> {
976 let owned = |c: Curve| Cow::Owned(c);
977 Ok(match curve {
978 Curve::Circle(c) if c.is_reversed() => (owned(c.forward(tol)?.into()), 0.0),
979 Curve::Ellipse(e) if e.is_reversed() => (owned(e.forward(tol)?.into()), 0.0),
980 Curve::Hyperbola(h) if h.is_reversed() => {
981 let (lo, hi) = h.domain();
982 let conic = h.hyperbola();
983 let turned = Hyperbola::new(
984 turned_about_x(conic.frame(), tol)?,
985 conic.major_radius(),
986 conic.minor_radius(),
987 tol,
988 )?;
989 let (form, shift) = mirrored(
990 HyperbolaCurve::over(turned, -hi, -lo)?.into(),
991 (lo, hi),
992 tol,
993 )?;
994 (owned(form), shift)
995 }
996 Curve::Parabola(p) if p.is_reversed() => {
997 let (lo, hi) = p.domain();
998 let conic = p.parabola();
999 let turned = Parabola::new(turned_about_x(conic.frame(), tol)?, conic.focal(), tol)?;
1000 let (form, shift) =
1001 mirrored(ParabolaCurve::over(turned, -hi, -lo)?.into(), (lo, hi), tol)?;
1002 (owned(form), shift)
1003 }
1004 Curve::Trimmed(t) => {
1005 let (s, e) = t.domain();
1006 let (basis, shift) = if t.is_reversed() {
1007 let (lo, hi) = t.basis().domain();
1008 let reversed = t.basis().reversed();
1009 let (basis, shift) = spelled(&reversed, tol)?;
1010 let basis = basis.into_owned();
1011 (Cow::Owned(basis), shift + ((lo + hi) - (s + e)))
1012 } else {
1013 spelled(t.basis(), tol)?
1014 };
1015 if let Cow::Borrowed(_) = basis {
1016 (Cow::Borrowed(curve), 0.0)
1017 } else if shift == 0.0 {
1018 (
1019 owned(TrimmedCurve::new(basis.into_owned(), s, e, tol)?.into()),
1020 0.0,
1021 )
1022 } else if let Some(moved) = shifted(&basis, shift, tol)? {
1023 (owned(TrimmedCurve::new(moved, s, e, tol)?.into()), 0.0)
1024 } else {
1025 let trimmed = TrimmedCurve::new(basis.into_owned(), s + shift, e + shift, tol)?;
1026 (owned(trimmed.into()), shift)
1027 }
1028 }
1029 Curve::Offset(o) => match spelled(o.basis(), tol)? {
1030 (Cow::Borrowed(_), _) => (Cow::Borrowed(curve), 0.0),
1031 (basis, shift) => {
1032 let offset = OffsetCurve::new(basis.into_owned(), o.distance(), o.reference())?;
1033 (owned(Curve::Offset(Box::new(offset))), shift)
1034 }
1035 },
1036 other => (Cow::Borrowed(other), 0.0),
1037 })
1038}
1039
1040fn mirrored(turned: Curve, (lo, hi): (f64, f64), tol: Tolerances) -> OgeomResult<(Curve, f64)> {
1045 let shift = -(lo + hi);
1046 Ok(if shift == 0.0 {
1047 (turned, 0.0)
1048 } else {
1049 (TrimmedCurve::new(turned, -hi, -lo, tol)?.into(), shift)
1050 })
1051}
1052
1053fn turned_about_x(frame: Frame, tol: Tolerances) -> OgeomResult<Frame> {
1057 Frame::from_axes(
1058 frame.origin(),
1059 frame.x(),
1060 frame.y().reversed(),
1061 frame.z().reversed(),
1062 tol,
1063 )
1064}
1065
1066fn shifted(curve: &Curve, by: f64, tol: Tolerances) -> OgeomResult<Option<Curve>> {
1073 if by == 0.0 {
1074 return Ok(Some(curve.clone()));
1075 }
1076 Ok(match curve {
1077 Curve::Line(l) => {
1078 let axis = l.axis();
1079 let (lo, hi) = l.domain();
1080 let moved = Axis::new(axis.point_at(by), axis.direction);
1081 Some(LineCurve::over(moved, lo - by, hi - by)?.into())
1082 }
1083 Curve::Circle(c) if !c.is_reversed() => {
1084 let circle = c.circle();
1085 let frame = turned_by(circle.frame(), by, tol)?;
1086 Some(CircleCurve::new(Circle::new(frame, circle.radius(), tol)?).into())
1087 }
1088 Curve::BSpline(b) => {
1089 let knots = b.knots().knots().iter().map(|k| k - by).collect();
1090 let knots = KnotVector::new(knots, b.knots().degree())?;
1091 Some(BSplineCurve::rational(knots, b.control_points().to_vec())?.into())
1092 }
1093 Curve::Trimmed(t) if !t.is_reversed() => match shifted(t.basis(), by, tol)? {
1094 Some(basis) => {
1095 let (s, e) = t.domain();
1096 Some(TrimmedCurve::new(basis, s - by, e - by, tol)?.into())
1097 }
1098 None => None,
1099 },
1100 Curve::Offset(o) => match shifted(o.basis(), by, tol)? {
1101 Some(basis) => Some(Curve::Offset(Box::new(OffsetCurve::new(
1102 basis,
1103 o.distance(),
1104 o.reference(),
1105 )?))),
1106 None => None,
1107 },
1108 _ => None,
1109 })
1110}
1111
1112fn turned_by(frame: Frame, angle: f64, tol: Tolerances) -> OgeomResult<Frame> {
1115 let (sin, cos) = angle.sin_cos();
1116 let (x, y) = (frame.x().vector(), frame.y().vector());
1117 Frame::from_axes(
1118 frame.origin(),
1119 Direction::new(x * cos + y * sin, tol)?,
1120 Direction::new(y * cos - x * sin, tol)?,
1121 frame.z(),
1122 tol,
1123 )
1124}
1125
1126fn spelled2(curve: &PlanarCurve, tol: Tolerances) -> OgeomResult<(Cow<'_, PlanarCurve>, f64)> {
1132 let owned = |c: PlanarCurve| Cow::Owned(c);
1133 Ok(match curve {
1134 PlanarCurve::Circle(c) if c.is_reversed() => {
1135 let circle = c.circle();
1136 let frame = mirrored_frame(circle.frame(), tol)?;
1137 let turned = Circle2d::new(Circle2::new(frame, circle.radius(), tol)?);
1138 (owned(turned.into()), 0.0)
1139 }
1140 PlanarCurve::Ellipse(e) if e.is_reversed() => {
1141 let ellipse = e.ellipse();
1142 let frame = mirrored_frame(ellipse.frame(), tol)?;
1143 let turned = Ellipse2::new(frame, ellipse.major_radius(), ellipse.minor_radius(), tol)?;
1144 (owned(Ellipse2d::new(turned).into()), 0.0)
1145 }
1146 PlanarCurve::Hyperbola(h) if h.is_reversed() => {
1147 let (lo, hi) = h.domain();
1148 let conic = h.hyperbola();
1149 let turned = Hyperbola2::new(
1150 mirrored_frame(conic.frame(), tol)?,
1151 conic.major_radius(),
1152 conic.minor_radius(),
1153 tol,
1154 )?;
1155 let turned: PlanarCurve = Hyperbola2d::over(turned, -hi, -lo)?.into();
1156 let shift = -(lo + hi);
1157 if shift == 0.0 {
1158 (owned(turned), 0.0)
1159 } else {
1160 (owned(Trimmed2d::new(turned, -hi, -lo, tol)?.into()), shift)
1161 }
1162 }
1163 PlanarCurve::Trimmed(t) => {
1164 let (s, e) = t.domain();
1165 let (basis, shift) = if t.is_reversed() {
1166 let (lo, hi) = t.basis().domain();
1167 let reversed = t.basis().reversed();
1168 let (basis, shift) = spelled2(&reversed, tol)?;
1169 let basis = basis.into_owned();
1170 (Cow::Owned(basis), shift + ((lo + hi) - (s + e)))
1171 } else {
1172 spelled2(t.basis(), tol)?
1173 };
1174 if let Cow::Borrowed(_) = basis {
1175 (Cow::Borrowed(curve), 0.0)
1176 } else if shift == 0.0 {
1177 (
1178 owned(Trimmed2d::new(basis.into_owned(), s, e, tol)?.into()),
1179 0.0,
1180 )
1181 } else if let Some(moved) = shifted2(&basis, shift, tol)? {
1182 (owned(Trimmed2d::new(moved, s, e, tol)?.into()), 0.0)
1183 } else {
1184 let trimmed = Trimmed2d::new(basis.into_owned(), s + shift, e + shift, tol)?;
1185 (owned(trimmed.into()), shift)
1186 }
1187 }
1188 PlanarCurve::Offset(o) => match spelled2(o.basis(), tol)? {
1189 (Cow::Borrowed(_), _) => (Cow::Borrowed(curve), 0.0),
1190 (basis, shift) => {
1191 let offset = Offset2d::new(basis.into_owned(), o.distance())?;
1192 (owned(PlanarCurve::Offset(Box::new(offset))), shift)
1193 }
1194 },
1195 other => (Cow::Borrowed(other), 0.0),
1196 })
1197}
1198
1199fn mirrored_frame(frame: Frame2, tol: Tolerances) -> OgeomResult<Frame2> {
1201 Frame2::from_axes(frame.origin(), frame.x(), frame.y().reversed(), tol)
1202}
1203
1204fn shifted2(curve: &PlanarCurve, by: f64, tol: Tolerances) -> OgeomResult<Option<PlanarCurve>> {
1206 if by == 0.0 {
1207 return Ok(Some(curve.clone()));
1208 }
1209 Ok(match curve {
1210 PlanarCurve::Line(l) => {
1211 let axis = l.axis();
1212 let (lo, hi) = l.domain();
1213 let moved = Axis2::new(axis.point_at(by), axis.direction);
1214 Some(Line2d::over(moved, lo - by, hi - by)?.into())
1215 }
1216 PlanarCurve::Circle(c) if !c.is_reversed() => {
1217 let circle = c.circle();
1218 let frame = circle.frame();
1219 let (sin, cos) = by.sin_cos();
1220 let (x, y) = (frame.x().vector(), frame.y().vector());
1221 let frame = Frame2::from_axes(
1222 frame.origin(),
1223 Direction2::new(x * cos + y * sin, tol)?,
1224 Direction2::new(y * cos - x * sin, tol)?,
1225 tol,
1226 )?;
1227 Some(Circle2d::new(Circle2::new(frame, circle.radius(), tol)?).into())
1228 }
1229 PlanarCurve::BSpline(b) => {
1230 let knots = b.knots().knots().iter().map(|k| k - by).collect();
1231 let knots = KnotVector::new(knots, b.knots().degree())?;
1232 Some(BSpline2d::rational(knots, b.control_points().to_vec())?.into())
1233 }
1234 PlanarCurve::Trimmed(t) if !t.is_reversed() => match shifted2(t.basis(), by, tol)? {
1235 Some(basis) => {
1236 let (s, e) = t.domain();
1237 Some(Trimmed2d::new(basis, s - by, e - by, tol)?.into())
1238 }
1239 None => None,
1240 },
1241 PlanarCurve::Offset(o) => match shifted2(o.basis(), by, tol)? {
1242 Some(basis) => Some(PlanarCurve::Offset(Box::new(Offset2d::new(
1243 basis,
1244 o.distance(),
1245 )?))),
1246 None => None,
1247 },
1248 _ => None,
1249 })
1250}
1251
1252fn chart_shift(surface: &SurfaceGeometry, tol: Tolerances) -> OgeomResult<Vector2> {
1256 Ok(match surface {
1257 SurfaceGeometry::Extrusion(e) => Vector2::new(spelled(e.curve(), tol)?.1, 0.0),
1258 SurfaceGeometry::Revolution(r) => Vector2::new(0.0, spelled(r.curve(), tol)?.1),
1259 SurfaceGeometry::Trimmed(t) => chart_shift(t.basis(), tol)?,
1260 SurfaceGeometry::Offset(o) => chart_shift(o.basis(), tol)?,
1261 _ => Vector2::new(0.0, 0.0),
1262 })
1263}
1264
1265fn chart(
1269 model: &Model,
1270 id: PCurveId,
1271 surface: SurfaceId,
1272 tol: Tolerances,
1273) -> OgeomResult<Cow<'_, PlanarCurve>> {
1274 let (Some(geometry), Some(on)) = (
1275 model.geometry().pcurve(id),
1276 model.geometry().surface(surface),
1277 ) else {
1278 ogeom_bail!(Dangling, "a pcurve or its surface is not in this model");
1279 };
1280 let mut chart = match on {
1281 SurfaceGeometry::Cone(cone) => {
1282 Cow::Owned(geometry.stretched(1.0, 1.0 / cone.cone().half_angle().cos(), tol)?)
1283 }
1284 _ => Cow::Borrowed(geometry),
1285 };
1286 let moved = chart_shift(on, tol)?;
1287 if moved != Vector2::ZERO {
1288 chart = Cow::Owned(chart.transformed(&Transform2::translation(moved), tol)?);
1289 }
1290 Ok(chart)
1291}
1292
1293fn edge_shift(model: &Model, data: &EdgeData, tol: Tolerances) -> OgeomResult<f64> {
1297 for representation in &data.representations {
1298 let own = |id: PCurveId, surface: SurfaceId| -> OgeomResult<f64> {
1299 let chart = chart(model, id, surface, tol)?;
1300 Ok(spelled2(&chart, tol)?.1)
1301 };
1302 let shifts = match representation {
1303 EdgeRepr::Curve3d { curve, .. } => {
1304 let Some(geometry) = model.geometry().curve(*curve) else {
1305 ogeom_bail!(Dangling, "a curve is not in this model");
1306 };
1307 [spelled(geometry, tol)?.1, 0.0]
1308 }
1309 EdgeRepr::PCurve { curve, surface, .. } => [own(*curve, *surface)?, 0.0],
1310 EdgeRepr::Seam {
1311 forward,
1312 reversed,
1313 surface,
1314 ..
1315 } => [own(*forward, *surface)?, own(*reversed, *surface)?],
1316 _ => continue,
1317 };
1318 if let Some(shift) = shifts.into_iter().find(|s| *s != 0.0) {
1319 return Ok(shift);
1320 }
1321 }
1322 Ok(0.0)
1323}
1324
1325pub fn read(text: &str, tol: Tolerances) -> OgeomResult<(Model, Shape)> {
1335 let (model, shape, _) = read_numbered(text, tol)?;
1336 Ok((model, shape))
1337}
1338
1339#[derive(Debug, Clone, Default)]
1349pub struct Numbering {
1350 pub edges: Vec<Shape>,
1352 pub faces: Vec<Shape>,
1354}
1355
1356pub fn read_numbered(text: &str, tol: Tolerances) -> OgeomResult<(Model, Shape, Numbering)> {
1362 let mut lines = text.lines().skip_while(|line| line.trim().is_empty());
1366 let Some(first) = lines.next() else {
1367 ogeom_bail!(Construction, "the file is empty");
1368 };
1369 let first = first.trim();
1370 if first != CONTENT_TYPE && !first.starts_with(VERSION_FAMILY) {
1371 ogeom_bail!(
1372 Construction,
1373 "this is not the interchange format: it opens with `{first}`"
1374 );
1375 }
1376 let rest: String = lines.collect::<Vec<_>>().join("\n");
1379 let Some(at) = rest.find("Locations") else {
1380 ogeom_bail!(Construction, "the file has no placement table");
1381 };
1382 let mut cursor = Cursor::new(&rest[at..]);
1383 let mut model = Model::new();
1384 let mut built = Built::default();
1385
1386 cursor.expect("Locations")?;
1387 let count = cursor.count()?;
1388 for _ in 0..count {
1389 let kind = cursor.count()?;
1390 match kind {
1391 1 => {
1392 let mut rows = [[0.0_f64; 3]; 3];
1393 let mut translation = [0.0_f64; 3];
1394 for (row, offset) in rows.iter_mut().zip(&mut translation) {
1395 for cell in row.iter_mut() {
1396 *cell = cursor.number()?;
1397 }
1398 *offset = cursor.number()?;
1399 }
1400 let transform = transform_of(rows, translation, tol)?;
1401 built
1405 .chains
1406 .push(if transform.kind() == TransformKind::Identity {
1407 Location::identity()
1408 } else {
1409 Location::of(model.add_datum(transform))
1410 });
1411 built.locations.push(transform);
1412 }
1413 2 => {
1414 let mut composed = Transform::IDENTITY;
1420 let mut chain = Location::identity();
1421 loop {
1422 let index = cursor.count()?;
1423 if index == 0 {
1424 break;
1425 }
1426 let power: i64 = cursor.integer()?;
1427 let (Some(base), Some(factor)) = (
1428 built.locations.get(index - 1).copied(),
1429 built.chains.get(index - 1).cloned(),
1430 ) else {
1431 ogeom_bail!(
1432 Construction,
1433 "a composed placement names placement {index}, which is not above it"
1434 );
1435 };
1436 let (step, factor) = if power < 0 {
1437 (base.inverse()?, factor.inverted())
1438 } else {
1439 (base, factor)
1440 };
1441 for _ in 0..power.abs() {
1442 composed = step * composed;
1443 chain = factor.then(&chain);
1444 }
1445 }
1446 built.locations.push(composed);
1447 built.chains.push(chain);
1448 }
1449 other => ogeom_bail!(
1450 Construction,
1451 "placement record {other} is not one this reads"
1452 ),
1453 }
1454 }
1455
1456 cursor.expect("Curve2ds")?;
1457 let count = cursor.count()?;
1458 for _ in 0..count {
1459 let curve = cursor.pcurve(tol)?;
1460 built.pcurves.push(model.geometry_mut().add_pcurve(curve));
1461 }
1462
1463 cursor.expect("Curves")?;
1464 let count = cursor.count()?;
1465 for _ in 0..count {
1466 let curve = cursor.curve(tol)?;
1467 built.curves.push(model.geometry_mut().add_curve(curve));
1468 }
1469
1470 cursor.expect("Polygon3D")?;
1472 let count = cursor.count()?;
1473 for _ in 0..count {
1474 cursor.skip_polygon3d()?;
1475 }
1476 cursor.expect("PolygonOnTriangulations")?;
1477 let count = cursor.count()?;
1478 for _ in 0..count {
1479 cursor.skip_polygon_on_triangulation()?;
1480 }
1481
1482 cursor.expect("Surfaces")?;
1483 let count = cursor.count()?;
1484 for _ in 0..count {
1485 let surface = cursor.surface(tol)?;
1486 built
1487 .surfaces
1488 .push(model.geometry_mut().add_surface(surface));
1489 }
1490
1491 cursor.expect("Triangulations")?;
1492 let count = cursor.count()?;
1493 for _ in 0..count {
1494 cursor.skip_triangulation()?;
1495 }
1496
1497 cursor.expect("TShapes")?;
1498 let total = cursor.count()?;
1499 let mut shapes: Vec<Shape> = Vec::with_capacity(cursor.room(total));
1503 for _ in 0..total {
1504 let shape = cursor.record(&mut model, &mut built, &shapes, total, tol)?;
1505 shapes.push(shape);
1506 }
1507 for edge in &shapes {
1510 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
1511 continue;
1512 };
1513 let agrees = agree_on_parameter(&model, data, tol);
1514 if let Some(NodeData::Edge(data)) = model.node_mut(edge).map(TShape::data_mut) {
1515 data.assert_same_parameter(agrees);
1516 }
1517 }
1518 let (orientation, number, location) = cursor.reference()?;
1521 let Some(root) = pick(&shapes, number, total) else {
1522 ogeom_bail!(
1523 Construction,
1524 "the file's own shape is numbered {number}, which is not there"
1525 );
1526 };
1527 let sense = built.sense(&root, orientation);
1528 let root = placed(root, &built, location)?.composed(sense);
1529 let numbering = numbered(&model, &built, &shapes, (number, location))?;
1530 Ok((model, root, numbering))
1531}
1532
1533const OPEN_EXTENT: f64 = 1e6;
1540
1541#[derive(Default)]
1543struct Built {
1544 locations: Vec<Transform>,
1545 chains: Vec<Location>,
1547 curves: Vec<CurveId>,
1548 pcurves: Vec<PCurveId>,
1549 surfaces: Vec<SurfaceId>,
1550 placed_curves: FastMap<(usize, usize), CurveId>,
1553 placed_surfaces: FastMap<(usize, usize), SurfaceId>,
1554 charts: FastMap<TShapeId, Vec<StatedChart>>,
1558 chart_maps: FastMap<SurfaceId, ChartMap>,
1561 mapped_charts: FastMap<(PCurveId, SurfaceId), PCurveId>,
1562 turned_faces: FastSet<TShapeId>,
1565 written: Vec<Vec<(usize, usize)>>,
1569 settled: FastMap<(TShapeId, Location), TShapeId>,
1572}
1573
1574#[derive(Clone, Copy)]
1576struct ChartMap {
1577 turned: bool,
1579 v_scale: f64,
1582}
1583
1584struct StatedChart {
1586 curves: (PCurveId, Option<PCurveId>),
1588 surface: usize,
1589 location: usize,
1590 range: (f64, f64),
1591}
1592
1593impl Built {
1594 fn sense(&self, shape: &Shape, stated: Orientation) -> Orientation {
1597 if self.turned_faces.contains(&shape.node()) {
1598 stated.compose(Orientation::Reversed)
1599 } else {
1600 stated
1601 }
1602 }
1603
1604 fn transform(&self, number: usize) -> OgeomResult<Transform> {
1606 if number == 0 {
1607 return Ok(Transform::IDENTITY);
1608 }
1609 let Some(transform) = self.locations.get(number - 1) else {
1610 ogeom_bail!(
1611 Construction,
1612 "a record names placement {number}, which is not there"
1613 );
1614 };
1615 Ok(*transform)
1616 }
1617
1618 fn location(&self, number: usize) -> OgeomResult<Location> {
1620 if number == 0 {
1621 return Ok(Location::identity());
1622 }
1623 let Some(chain) = self.chains.get(number - 1) else {
1624 ogeom_bail!(
1625 Construction,
1626 "a record names placement {number}, which is not there"
1627 );
1628 };
1629 Ok(chain.clone())
1630 }
1631
1632 fn curve(
1634 &mut self,
1635 model: &mut Model,
1636 number: usize,
1637 location: usize,
1638 tol: Tolerances,
1639 ) -> OgeomResult<CurveId> {
1640 let Some(id) = number
1641 .checked_sub(1)
1642 .and_then(|at| self.curves.get(at))
1643 .copied()
1644 else {
1645 ogeom_bail!(Construction, "an edge names curve {number}");
1646 };
1647 let transform = self.transform(location)?;
1648 if transform.kind() == TransformKind::Identity {
1649 return Ok(id);
1650 }
1651 if let Some(placed) = self.placed_curves.get(&(number, location)) {
1652 return Ok(*placed);
1653 }
1654 let Some(curve) = model.geometry().curve(id) else {
1655 ogeom_bail!(Dangling, "a curve read from the file is not in the model");
1656 };
1657 let moved = curve.transformed(&transform, tol)?;
1658 let placed = model.geometry_mut().add_curve(moved);
1659 self.placed_curves.insert((number, location), placed);
1660 Ok(placed)
1661 }
1662
1663 fn surface(
1666 &mut self,
1667 model: &mut Model,
1668 number: usize,
1669 location: usize,
1670 tol: Tolerances,
1671 ) -> OgeomResult<SurfaceId> {
1672 let Some(id) = number
1673 .checked_sub(1)
1674 .and_then(|at| self.surfaces.get(at))
1675 .copied()
1676 else {
1677 ogeom_bail!(Construction, "a face names surface {number}");
1678 };
1679 if let Some(placed) = self.placed_surfaces.get(&(number, location)) {
1680 return Ok(*placed);
1681 }
1682 let Some(surface) = model.geometry().surface(id) else {
1683 ogeom_bail!(Dangling, "a surface read from the file is not in the model");
1684 };
1685 let transform = self.transform(location)?;
1686 let moved = transform.kind() != TransformKind::Identity;
1687 let surface = if moved {
1688 surface.transformed(&transform, tol)?
1689 } else {
1690 surface.clone()
1691 };
1692 let (surface, rebuilt, turned) = match right_handed(&surface, tol)? {
1693 Some((upright, turned)) => (upright, true, turned),
1694 None => (surface, false, false),
1695 };
1696 let v_scale = match &surface {
1697 SurfaceGeometry::Cone(c) => c.cone().half_angle().cos(),
1698 _ => 1.0,
1699 };
1700 let placed = if moved || rebuilt {
1701 model.geometry_mut().add_surface(surface)
1702 } else {
1703 id
1704 };
1705 if turned || v_scale != 1.0 {
1706 self.chart_maps.insert(placed, ChartMap { turned, v_scale });
1707 }
1708 self.placed_surfaces.insert((number, location), placed);
1709 Ok(placed)
1710 }
1711
1712 fn turned(&self, on: SurfaceId) -> bool {
1714 self.chart_maps.get(&on).is_some_and(|map| map.turned)
1715 }
1716
1717 fn chart_on(
1720 &mut self,
1721 model: &mut Model,
1722 curve: PCurveId,
1723 on: SurfaceId,
1724 tol: Tolerances,
1725 ) -> OgeomResult<PCurveId> {
1726 let Some(map) = self.chart_maps.get(&on).copied() else {
1727 return Ok(curve);
1728 };
1729 if let Some(mapped) = self.mapped_charts.get(&(curve, on)) {
1730 return Ok(*mapped);
1731 }
1732 let Some(geometry) = model.geometry().pcurve(curve) else {
1733 ogeom_bail!(
1734 Dangling,
1735 "a chart curve read from the file is not in the model"
1736 );
1737 };
1738 let mut mapped = geometry.stretched(1.0, map.v_scale, tol)?;
1739 if map.turned {
1740 mapped = mapped.transformed(&turned_chart(), tol)?;
1741 }
1742 let mapped = model.geometry_mut().add_pcurve(mapped);
1743 self.mapped_charts.insert((curve, on), mapped);
1744 Ok(mapped)
1745 }
1746
1747 fn bind_charts(
1755 &mut self,
1756 model: &mut Model,
1757 (surface, location): (usize, usize),
1758 on: SurfaceId,
1759 wires: &[Shape],
1760 tol: Tolerances,
1761 ) -> OgeomResult<()> {
1762 let face_placement = self.transform(location)?;
1763 for wire in wires {
1764 for edge in model.children_of(wire)? {
1765 let within = edge.location().clone();
1766 let seen = within.composed(model.datums())?.inverse()? * face_placement;
1767 let mut found = None;
1768 for chart in self.charts.get(&edge.node()).map_or(&[][..], Vec::as_slice) {
1769 if chart.surface == surface
1770 && self.transform(chart.location)?.is_equal(&seen, tol)
1771 {
1772 found = Some((chart.curves, chart.range));
1773 break;
1774 }
1775 }
1776 let Some((curves, range)) = found else {
1777 plane_chart(model, &edge, on, tol)?;
1778 continue;
1779 };
1780 let representation = match curves {
1781 (curve, None) => EdgeRepr::PCurve {
1782 curve: self.chart_on(model, curve, on, tol)?,
1783 range,
1784 surface: on,
1785 location: within.clone(),
1786 },
1787 (first, Some(second)) => {
1790 let (first, second) = (
1791 self.chart_on(model, first, on, tol)?,
1792 self.chart_on(model, second, on, tol)?,
1793 );
1794 let (forward, reversed) = if self.turned(on) {
1795 (second, first)
1796 } else {
1797 (first, second)
1798 };
1799 EdgeRepr::Seam {
1800 forward,
1801 reversed,
1802 range,
1803 surface: on,
1804 location: within.clone(),
1805 }
1806 }
1807 };
1808 let Some(NodeData::Edge(data)) = model.node_mut(&edge).map(TShape::data_mut) else {
1809 ogeom_bail!(Construction, "a wire holds something that is not an edge");
1810 };
1811 let held = data
1812 .representations
1813 .iter()
1814 .any(|r| r.surface() == Some(on) && r.location() == Some(&within));
1815 if !held {
1816 data.add(representation);
1817 }
1818 }
1819 }
1820 Ok(())
1821 }
1822}
1823
1824fn pick(shapes: &[Shape], number: usize, total: usize) -> Option<Shape> {
1826 total
1829 .checked_sub(number)
1830 .and_then(|at| shapes.get(at))
1831 .cloned()
1832}
1833
1834fn transform_of(
1836 rows: [[f64; 3]; 3],
1837 translation: [f64; 3],
1838 tol: Tolerances,
1839) -> OgeomResult<Transform> {
1840 let m = Matrix3::new(rows);
1843 let determinant = m.determinant();
1844 if determinant.abs() <= f64::MIN_POSITIVE {
1845 ogeom_bail!(
1846 Construction,
1847 "a placement with a singular matrix places nothing"
1848 );
1849 }
1850 let scale = determinant.cbrt();
1851 let mut unit = [[0.0_f64; 3]; 3];
1852 for row in 0..3 {
1853 for column in 0..3 {
1854 unit[row][column] = rows[row][column] / scale;
1855 }
1856 }
1857 Transform::from_parts(
1858 Matrix3::new(unit),
1859 scale,
1860 Vector::new(translation[0], translation[1], translation[2]),
1861 tol.angular().max(1e-9),
1862 )
1863}
1864
1865fn right_handed(
1874 surface: &SurfaceGeometry,
1875 tol: Tolerances,
1876) -> OgeomResult<Option<(SurfaceGeometry, bool)>> {
1877 use ogeom_geom::Surface as _;
1878 let upright = |frame: Frame| Frame::new(frame.origin(), frame.z(), frame.x(), tol);
1879 let left = |frame: Frame| frame.handedness() == Handedness::Left;
1880 let (_, v) = surface.domain();
1881 Ok(Some(match surface {
1882 SurfaceGeometry::Plane(s) if left(s.plane().frame()) => {
1883 let frame = s.plane().frame();
1884 let (u, v) = s.domain();
1885 let normal = Direction::new(-frame.z().vector(), tol)?;
1886 let frame = Frame::new(frame.origin(), normal, frame.x(), tol)?;
1887 (PlaneSurface::over(Plane::new(frame), u, v)?.into(), false)
1888 }
1889 SurfaceGeometry::Cylinder(s) if left(s.cylinder().frame()) => {
1890 let c = s.cylinder();
1891 let c = Cylinder::new(upright(c.frame())?, c.radius(), tol)?;
1892 (CylinderSurface::new(c, v)?.into(), true)
1893 }
1894 SurfaceGeometry::Cone(s) if left(s.cone().frame()) => {
1895 let c = s.cone();
1896 let c = Cone::new(
1897 upright(c.frame())?,
1898 c.reference_radius(),
1899 c.half_angle(),
1900 tol,
1901 )?;
1902 (ConeSurface::new(c, v)?.into(), true)
1903 }
1904 SurfaceGeometry::Sphere(s) if left(s.sphere().frame()) => {
1905 let b = s.sphere();
1906 (
1907 SphereSurface::new(Sphere::new(upright(b.frame())?, b.radius(), tol)?).into(),
1908 true,
1909 )
1910 }
1911 SurfaceGeometry::Torus(s) if left(s.torus().frame()) => {
1912 let t = s.torus();
1913 let t = Torus::new(upright(t.frame())?, t.major_radius(), t.minor_radius(), tol)?;
1914 (TorusSurface::new(t).into(), true)
1915 }
1916 SurfaceGeometry::Trimmed(t) => match right_handed(t.basis(), tol)? {
1919 Some((basis, false)) => {
1920 let (u, v) = surface.domain();
1921 (
1922 SurfaceGeometry::Trimmed(Box::new(TrimmedSurface::new(basis, u, v, tol)?)),
1923 false,
1924 )
1925 }
1926 _ => return Ok(None),
1927 },
1928 _ => return Ok(None),
1929 }))
1930}
1931
1932fn turned_chart() -> Transform2 {
1935 Transform2::line_mirror(Point2::new(core::f64::consts::PI, 0.0), Direction2::X)
1936}
1937
1938fn plane_chart(model: &mut Model, edge: &Shape, on: SurfaceId, tol: Tolerances) -> OgeomResult<()> {
1946 let Some(surface) = model.geometry().surface(on).cloned() else {
1947 return Ok(());
1948 };
1949 let flat = match &surface {
1950 SurfaceGeometry::Plane(_) => true,
1951 SurfaceGeometry::Trimmed(t) => matches!(t.basis(), SurfaceGeometry::Plane(_)),
1952 _ => false,
1953 };
1954 if !flat {
1955 return Ok(());
1956 }
1957 let Some(data) = model.node(edge).and_then(|n| n.data().as_edge()) else {
1958 return Ok(());
1959 };
1960 let within = edge.location().clone();
1961 if data.degenerate
1962 || data
1963 .pcurve_for(on, &within)
1964 .is_some_and(|r| r.location() == Some(&within))
1965 {
1966 return Ok(());
1967 }
1968 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d().cloned() else {
1969 return Ok(());
1970 };
1971 let Some(geometry) = model.geometry().curve(curve) else {
1972 ogeom_bail!(Dangling, "an edge's curve is not in the model");
1973 };
1974 let geometry = geometry.transformed(&within.composed(model.datums())?, tol)?;
1976 let image = match ogeom_intersect::exact_pcurve_over(&geometry, range, &surface, tol) {
1977 Some(exact) => crate::pcurves::over_range(&exact, range),
1978 None => crate::pcurves::fit_projected_pcurve(&geometry, range, &surface, tol)?.0,
1979 };
1980 let image = model.geometry_mut().add_pcurve(image);
1981 let Some(NodeData::Edge(data)) = model.node_mut(edge).map(TShape::data_mut) else {
1982 return Ok(());
1983 };
1984 data.add(EdgeRepr::PCurve {
1985 curve: image,
1986 range,
1987 surface: on,
1988 location: within,
1989 });
1990 Ok(())
1991}
1992
1993fn numbered(
1995 model: &Model,
1996 built: &Built,
1997 shapes: &[Shape],
1998 root: (usize, usize),
1999) -> OgeomResult<Numbering> {
2000 let total = shapes.len();
2001 let mut numbering = Numbering::default();
2002 let mut met: FastSet<(TShapeId, Location)> = FastSet::default();
2003 let mut stack = vec![(root.0, Location::identity(), built.location(root.1)?)];
2006 while let Some((number, outer, own)) = stack.pop() {
2007 let (Some(index), Some(shape)) = (total.checked_sub(number), pick(shapes, number, total))
2008 else {
2009 ogeom_bail!(
2010 Construction,
2011 "a record names subshape {number}, which is not above it"
2012 );
2013 };
2014 let at = outer.then(&own);
2015 let kind = model.kind_of(&shape)?;
2016 if matches!(kind, ShapeType::Edge | ShapeType::Face) {
2017 if !met.insert((shape.node(), at.clone())) {
2018 continue;
2019 }
2020 if kind == ShapeType::Edge {
2021 numbering.edges.push(shape.moved(&at));
2022 continue;
2023 }
2024 numbering
2026 .faces
2027 .push(match built.settled.get(&(shape.node(), own)) {
2028 Some(rebuilt) => Shape::of(*rebuilt).moved(&outer),
2029 None => shape.moved(&at),
2030 });
2031 }
2032 if kind == ShapeType::Vertex {
2033 continue;
2034 }
2035 let Some(written) = built.written.get(index) else {
2036 continue;
2037 };
2038 for &(child, location) in written.iter().rev() {
2039 stack.push((child, at.clone(), built.location(location)?));
2040 }
2041 }
2042 Ok(numbering)
2043}
2044
2045fn settled(
2055 model: &mut Model,
2056 built: &mut Built,
2057 face: Shape,
2058 tol: Tolerances,
2059) -> OgeomResult<Shape> {
2060 if face.location().is_identity() || model.kind_of(&face)? != ShapeType::Face {
2061 return Ok(face);
2062 }
2063 let Some(data) = model.node(&face).and_then(|n| n.data().as_face()).cloned() else {
2064 return Ok(face);
2065 };
2066 let bare = Shape::of(face.node());
2067 let wires = model.children_of(&bare)?;
2068 let mut inside = Vec::new();
2069 for wire in &wires {
2070 inside.push(model.children_of(&wire.oriented(Orientation::Forward))?);
2071 }
2072 let outer = face.location().clone();
2073 if inside
2074 .iter()
2075 .flatten()
2076 .all(|edge| outer.then(edge.location()).ends_with(edge.location()))
2077 {
2078 return Ok(face);
2079 }
2080 let Some(surface) = model.geometry().surface(data.surface) else {
2081 ogeom_bail!(Dangling, "a face's surface is not in the model");
2082 };
2083 let surface = surface.transformed(&outer.composed(model.datums())?, tol)?;
2084 let surface = model.geometry_mut().add_surface(surface);
2085 let mut placed_wires = Vec::with_capacity(wires.len());
2086 for (wire, edges) in wires.iter().zip(&inside) {
2087 let mut placed_edges = Vec::with_capacity(edges.len());
2088 for edge in edges {
2089 let at = outer.then(edge.location());
2090 let chart = model
2091 .node(edge)
2092 .and_then(|n| n.data().as_edge())
2093 .and_then(|d| d.pcurve_for(data.surface, edge.location()))
2094 .cloned();
2095 let bound = match chart {
2096 Some(EdgeRepr::PCurve { curve, range, .. }) => Some(EdgeRepr::PCurve {
2097 curve,
2098 range,
2099 surface,
2100 location: at.clone(),
2101 }),
2102 Some(EdgeRepr::Seam {
2103 forward,
2104 reversed,
2105 range,
2106 ..
2107 }) => Some(EdgeRepr::Seam {
2108 forward,
2109 reversed,
2110 range,
2111 surface,
2112 location: at.clone(),
2113 }),
2114 _ => None,
2115 };
2116 if let Some(bound) = bound
2117 && let Some(NodeData::Edge(edge_data)) = model.node_mut(edge).map(TShape::data_mut)
2118 {
2119 edge_data.add(bound);
2120 }
2121 placed_edges.push(
2122 Shape::of(edge.node())
2123 .moved(&at)
2124 .oriented(edge.orientation()),
2125 );
2126 }
2127 placed_wires.push(model.add_wire(&placed_edges)?.oriented(wire.orientation()));
2128 }
2129 let rebuilt = model.add_face(
2130 FaceData {
2131 surface,
2132 location: Location::identity(),
2133 ..data
2134 },
2135 &placed_wires,
2136 )?;
2137 built.settled.insert((face.node(), outer), rebuilt.node());
2138 Ok(rebuilt.oriented(face.orientation()))
2139}
2140
2141fn walked(model: &Model, edges: &[Shape]) -> OgeomResult<Option<Vec<Shape>>> {
2148 let mut ends = Vec::with_capacity(edges.len());
2151 for edge in edges {
2152 let mut start = None;
2153 let mut end = None;
2154 for vertex in model.children_of(edge)? {
2155 let at = Some((vertex.node(), vertex.location().clone()));
2156 match vertex.orientation() {
2157 Orientation::Forward => start = at,
2158 Orientation::Reversed => end = at,
2159 Orientation::Internal | Orientation::External => {}
2160 }
2161 }
2162 let (Some(start), Some(end)) = (start, end) else {
2163 return Ok(None);
2164 };
2165 ends.push((start, end));
2166 }
2167 let mut leaving: FastMap<&(TShapeId, Location), Vec<usize>> = FastMap::default();
2173 for (i, (start, _)) in ends.iter().enumerate().rev() {
2174 leaving.entry(start).or_default().push(i);
2175 }
2176 let Some((first, _)) = ends.first() else {
2177 return Ok(None);
2178 };
2179 let mut stack: Vec<(&(TShapeId, Location), Option<usize>)> = vec![(first, None)];
2180 let mut order = Vec::with_capacity(edges.len());
2181 while let Some(&(at, by)) = stack.last() {
2182 if let Some(next) = leaving.get_mut(at).and_then(Vec::pop) {
2183 stack.push((&ends[next].1, Some(next)));
2184 } else {
2185 stack.pop();
2186 order.extend(by);
2187 }
2188 }
2189 order.reverse();
2190 if order.len() != edges.len() {
2191 return Ok(None);
2192 }
2193 if order.iter().enumerate().all(|(at, &i)| at == i) {
2194 return Ok(None);
2195 }
2196 Ok(Some(order.into_iter().map(|i| edges[i].clone()).collect()))
2197}
2198
2199fn placed(shape: Shape, built: &Built, number: usize) -> OgeomResult<Shape> {
2201 let location = built.location(number)?;
2202 if location.is_identity() {
2203 return Ok(shape);
2204 }
2205 Ok(shape.moved(&location))
2206}
2207
2208fn split_continuity(word: &str) -> impl Iterator<Item = &str> {
2214 let split = ["C0", "C1", "C2", "C3", "CN", "G1", "G2"]
2215 .iter()
2216 .find_map(|name| {
2217 let number = word.strip_suffix(name)?;
2218 (!number.is_empty() && number.bytes().all(|b| b.is_ascii_digit()))
2219 .then(|| word.split_at(number.len()))
2220 });
2221 let (first, second) = split.map_or((word, None), |(a, b)| (a, Some(b)));
2222 std::iter::once(first).chain(second)
2223}
2224
2225struct Cursor<'a> {
2227 tokens: Vec<&'a str>,
2228 at: usize,
2229}
2230
2231impl<'a> Cursor<'a> {
2232 fn new(text: &'a str) -> Self {
2233 Self {
2234 tokens: text.split_whitespace().flat_map(split_continuity).collect(),
2235 at: 0,
2236 }
2237 }
2238
2239 fn word(&mut self) -> OgeomResult<&'a str> {
2240 let Some(token) = self.tokens.get(self.at) else {
2241 ogeom_bail!(Construction, "the file ends in the middle of a record");
2242 };
2243 self.at += 1;
2244 Ok(token)
2245 }
2246
2247 fn peek(&self) -> Option<&'a str> {
2248 self.tokens.get(self.at).copied()
2249 }
2250
2251 fn expect(&mut self, what: &str) -> OgeomResult<()> {
2252 let token = self.word()?;
2253 if token != what {
2254 ogeom_bail!(Construction, "expected the {what} table, found `{token}`");
2255 }
2256 Ok(())
2257 }
2258
2259 fn number(&mut self) -> OgeomResult<f64> {
2260 let token = self.word()?;
2261 let Ok(value) = token.parse::<f64>() else {
2262 ogeom_bail!(Construction, "`{token}` is not a number");
2263 };
2264 Ok(value)
2265 }
2266
2267 fn integer(&mut self) -> OgeomResult<i64> {
2268 let token = self.word()?;
2269 let Ok(value) = token.parse::<i64>() else {
2270 ogeom_bail!(Construction, "`{token}` is not a whole number");
2271 };
2272 Ok(value)
2273 }
2274
2275 fn room(&self, n: usize) -> usize {
2279 n.min(self.tokens.len().saturating_sub(self.at))
2280 }
2281
2282 fn count(&mut self) -> OgeomResult<usize> {
2283 let value = self.integer()?;
2284 let Ok(count) = usize::try_from(value) else {
2285 ogeom_bail!(Construction, "{value} is not a count");
2286 };
2287 Ok(count)
2288 }
2289
2290 fn flag(&mut self) -> OgeomResult<bool> {
2291 Ok(self.count()? != 0)
2292 }
2293
2294 fn point(&mut self) -> OgeomResult<Point> {
2295 Ok(Point::new(self.number()?, self.number()?, self.number()?))
2296 }
2297
2298 fn point2(&mut self) -> OgeomResult<Point2> {
2299 Ok(Point2::new(self.number()?, self.number()?))
2300 }
2301
2302 fn direction(&mut self, tol: Tolerances) -> OgeomResult<Direction> {
2303 let v = Vector::new(self.number()?, self.number()?, self.number()?);
2304 Direction::new(v, tol)
2305 }
2306
2307 fn direction2(&mut self, tol: Tolerances) -> OgeomResult<Direction2> {
2308 let v = Vector2::new(self.number()?, self.number()?);
2309 Direction2::new(v, tol)
2310 }
2311
2312 fn seat(&mut self, tol: Tolerances) -> OgeomResult<Frame> {
2319 let origin = self.point()?;
2320 let normal = self.direction(tol)?;
2321 let x = self.direction(tol)?;
2322 let y = self.direction(tol)?;
2323 Frame::from_axes(origin, x, y, normal, tol)
2324 }
2325
2326 fn seat2(&mut self, tol: Tolerances) -> OgeomResult<Frame2> {
2327 let origin = self.point2()?;
2328 let x = self.direction2(tol)?;
2329 let y = self.direction2(tol)?;
2330 Frame2::from_axes(origin, x, y, tol)
2331 }
2332
2333 fn spline_knots(&mut self, degree: usize, count: usize) -> OgeomResult<KnotVector> {
2335 let mut flat = Vec::new();
2336 for _ in 0..count {
2337 let value = self.number()?;
2338 let multiplicity = self.count()?;
2339 if multiplicity > degree.min(64) + 1 {
2342 ogeom_bail!(
2343 Construction,
2344 "a knot of multiplicity {multiplicity} on a degree {degree} spline"
2345 );
2346 }
2347 flat.extend(std::iter::repeat_n(value, multiplicity));
2348 }
2349 KnotVector::new(flat, degree)
2350 }
2351
2352 fn curve(&mut self, tol: Tolerances) -> OgeomResult<Curve> {
2353 let kind = self.count()?;
2354 Ok(match kind {
2355 1 => {
2356 let origin = self.point()?;
2357 let direction = self.direction(tol)?;
2358 LineCurve::over(Axis::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
2361 }
2362 2 => {
2363 let frame = self.seat(tol)?;
2364 CircleCurve::new(Circle::new(frame, self.number()?, tol)?).into()
2365 }
2366 3 => {
2367 let frame = self.seat(tol)?;
2368 let ellipse = Ellipse::new(frame, self.number()?, self.number()?, tol)?;
2369 EllipseCurve::new(ellipse).into()
2370 }
2371 4 => {
2372 let frame = self.seat(tol)?;
2373 let parabola = Parabola::new(frame, self.number()?, tol)?;
2374 ParabolaCurve::new(parabola, OPEN_EXTENT)?.into()
2375 }
2376 5 => {
2377 let frame = self.seat(tol)?;
2378 let hyperbola = Hyperbola::new(frame, self.number()?, self.number()?, tol)?;
2379 HyperbolaCurve::new(hyperbola, OPEN_EXTENT)?.into()
2380 }
2381 7 => {
2382 let rational = self.flag()?;
2383 let _periodic = self.flag()?;
2384 let degree = self.count()?;
2385 let poles = self.count()?;
2386 let knot_count = self.count()?;
2387 let mut control = Vec::with_capacity(self.room(poles));
2388 for _ in 0..poles {
2389 let at = self.point()?;
2390 let weight = if rational { self.number()? } else { 1.0 };
2391 control.push(Weighted::new(at, weight, tol)?);
2392 }
2393 let knots = self.spline_knots(degree, knot_count)?;
2394 BSplineCurve::rational(knots, control)?.into()
2395 }
2396 8 => {
2397 let lo = self.number()?;
2398 let hi = self.number()?;
2399 let basis = self.curve(tol)?;
2400 Curve::Trimmed(Box::new(TrimmedCurve::new(basis, lo, hi, tol)?))
2401 }
2402 9 => {
2403 let distance = self.number()?;
2404 let reference = self.direction(tol)?;
2405 let basis = self.curve(tol)?;
2406 Curve::Offset(Box::new(ogeom_geom::OffsetCurve::new(
2407 basis, distance, reference,
2408 )?))
2409 }
2410 other => ogeom_bail!(
2411 Construction,
2412 "curve record {other} is one this does not read; a Bezier \
2413 curve is record 6, and nothing here builds one"
2414 ),
2415 })
2416 }
2417
2418 fn pcurve(&mut self, tol: Tolerances) -> OgeomResult<PlanarCurve> {
2419 let kind = self.count()?;
2420 Ok(match kind {
2421 1 => {
2422 let origin = self.point2()?;
2423 let direction = self.direction2(tol)?;
2424 Line2d::over(Axis2::new(origin, direction), -OPEN_EXTENT, OPEN_EXTENT)?.into()
2425 }
2426 2 => {
2427 let frame = self.seat2(tol)?;
2428 Circle2d::new(Circle2::new(frame, self.number()?, tol)?).into()
2429 }
2430 3 => {
2431 let frame = self.seat2(tol)?;
2432 let ellipse = Ellipse2::new(frame, self.number()?, self.number()?, tol)?;
2433 Ellipse2d::new(ellipse).into()
2434 }
2435 5 => {
2436 let frame = self.seat2(tol)?;
2437 let hyperbola = Hyperbola2::new(frame, self.number()?, self.number()?, tol)?;
2438 ogeom_geom::Hyperbola2d::over(hyperbola, -OPEN_EXTENT, OPEN_EXTENT)?.into()
2439 }
2440 7 => {
2441 let rational = self.flag()?;
2442 let _periodic = self.flag()?;
2443 let degree = self.count()?;
2444 let poles = self.count()?;
2445 let knot_count = self.count()?;
2446 let mut control = Vec::with_capacity(self.room(poles));
2447 for _ in 0..poles {
2448 let at = self.point2()?;
2449 let weight = if rational { self.number()? } else { 1.0 };
2450 control.push(Weighted::new(at, weight, tol)?);
2451 }
2452 let knots = self.spline_knots(degree, knot_count)?;
2453 ogeom_geom::BSpline2d::rational(knots, control)?.into()
2454 }
2455 8 => {
2456 let lo = self.number()?;
2457 let hi = self.number()?;
2458 let basis = self.pcurve(tol)?;
2459 PlanarCurve::Trimmed(Box::new(ogeom_geom::Trimmed2d::new(basis, lo, hi, tol)?))
2460 }
2461 9 => {
2462 let distance = self.number()?;
2463 let basis = self.pcurve(tol)?;
2464 PlanarCurve::Offset(Box::new(ogeom_geom::Offset2d::new(basis, distance)?))
2465 }
2466 other => ogeom_bail!(
2467 Construction,
2468 "chart curve record {other} is one this does not read"
2469 ),
2470 })
2471 }
2472
2473 fn surface(&mut self, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
2474 let kind = self.count()?;
2475 const REACH: (f64, f64) = (-1e9, 1e9);
2479 Ok(match kind {
2480 1 => {
2481 let frame = self.seat(tol)?;
2482 PlaneSurface::over(Plane::new(frame), REACH, REACH)?.into()
2483 }
2484 2 => {
2485 let frame = self.seat(tol)?;
2486 let cylinder = Cylinder::new(frame, self.number()?, tol)?;
2487 CylinderSurface::new(cylinder, REACH)?.into()
2488 }
2489 3 => {
2490 let frame = self.seat(tol)?;
2491 let cone = Cone::new(frame, self.number()?, self.number()?, tol)?;
2492 ConeSurface::new(cone, REACH)?.into()
2493 }
2494 4 => {
2495 let frame = self.seat(tol)?;
2496 SphereSurface::new(Sphere::new(frame, self.number()?, tol)?).into()
2497 }
2498 5 => {
2499 let frame = self.seat(tol)?;
2500 let torus = Torus::new(frame, self.number()?, self.number()?, tol)?;
2501 TorusSurface::new(torus).into()
2502 }
2503 6 => {
2504 let direction = self.direction(tol)?;
2505 let basis = self.curve(tol)?;
2506 ExtrusionSurface::over(basis, direction, REACH)?.into()
2507 }
2508 7 => {
2509 let origin = self.point()?;
2510 let direction = self.direction(tol)?;
2511 let basis = self.curve(tol)?;
2512 RevolutionSurface::new(basis, Axis::new(origin, direction), core::f64::consts::TAU)?
2513 .into()
2514 }
2515 9 => {
2516 let u_rational = self.flag()?;
2517 let v_rational = self.flag()?;
2518 let _u_periodic = self.flag()?;
2519 let _v_periodic = self.flag()?;
2520 let u_degree = self.count()?;
2521 let v_degree = self.count()?;
2522 let u_poles = self.count()?;
2523 let v_poles = self.count()?;
2524 let u_knot_count = self.count()?;
2525 let v_knot_count = self.count()?;
2526 let rational = u_rational || v_rational;
2527 let Some(cells) = u_poles.checked_mul(v_poles) else {
2528 ogeom_bail!(Construction, "a {u_poles} by {v_poles} pole grid");
2529 };
2530 let mut points = Vec::with_capacity(self.room(cells));
2531 for _ in 0..cells {
2532 let at = self.point()?;
2533 let weight = if rational { self.number()? } else { 1.0 };
2534 points.push(Weighted::new(at, weight, tol)?);
2535 }
2536 let u_knots = self.spline_knots(u_degree, u_knot_count)?;
2537 let v_knots = self.spline_knots(v_degree, v_knot_count)?;
2538 let grid = ControlGrid::new(points, u_poles, v_poles)?;
2539 BSplineSurface::rational(u_knots, v_knots, grid)?.into()
2540 }
2541 10 => {
2542 let u = (self.number()?, self.number()?);
2543 let v = (self.number()?, self.number()?);
2544 let basis = self.surface(tol)?;
2545 SurfaceGeometry::Trimmed(Box::new(TrimmedSurface::new(basis, u, v, tol)?))
2546 }
2547 11 => {
2548 let distance = self.number()?;
2549 let basis = self.surface(tol)?;
2550 SurfaceGeometry::Offset(Box::new(ogeom_geom::OffsetSurface::new(basis, distance)?))
2551 }
2552 other => ogeom_bail!(
2553 Construction,
2554 "surface record {other} is one this does not read; a Bezier \
2555 surface is record 8, and nothing here builds one"
2556 ),
2557 })
2558 }
2559
2560 fn reference(&mut self) -> OgeomResult<(Orientation, usize, usize)> {
2563 let token = self.word()?;
2564 let (orientation, digits) = match token.split_at(1) {
2565 ("+", rest) => (Orientation::Forward, rest),
2566 ("-", rest) => (Orientation::Reversed, rest),
2567 ("i", rest) => (Orientation::Internal, rest),
2568 ("e", rest) => (Orientation::External, rest),
2569 _ => ogeom_bail!(Construction, "`{token}` does not name a subshape"),
2570 };
2571 let Ok(number) = digits.parse::<usize>() else {
2572 ogeom_bail!(Construction, "`{token}` does not name a subshape number");
2573 };
2574 Ok((orientation, number, self.count()?))
2575 }
2576
2577 fn skip_polygon3d(&mut self) -> OgeomResult<()> {
2578 let nodes = self.count()?;
2579 let has_parameters = self.flag()?;
2580 let _deflection = self.number()?;
2581 for _ in 0..nodes * 3 {
2582 let _ = self.number()?;
2583 }
2584 if has_parameters {
2585 for _ in 0..nodes {
2586 let _ = self.number()?;
2587 }
2588 }
2589 Ok(())
2590 }
2591
2592 fn skip_polygon_on_triangulation(&mut self) -> OgeomResult<()> {
2593 let nodes = self.count()?;
2594 for _ in 0..nodes {
2595 let _ = self.integer()?;
2596 }
2597 if self.peek() == Some("p") {
2599 let _ = self.word()?;
2600 let _deflection = self.number()?;
2601 let _flag = self.count()?;
2602 let count = self.count()?;
2603 for _ in 0..count {
2604 let _ = self.number()?;
2605 }
2606 }
2607 Ok(())
2608 }
2609
2610 fn skip_triangulation(&mut self) -> OgeomResult<()> {
2611 let nodes = self.count()?;
2612 let triangles = self.count()?;
2613 let has_parameters = self.flag()?;
2614 let _deflection = self.number()?;
2615 for _ in 0..nodes * 3 {
2616 let _ = self.number()?;
2617 }
2618 if has_parameters {
2619 for _ in 0..nodes * 2 {
2620 let _ = self.number()?;
2621 }
2622 }
2623 for _ in 0..triangles * 3 {
2624 let _ = self.integer()?;
2625 }
2626 Ok(())
2627 }
2628
2629 fn record(
2631 &mut self,
2632 model: &mut Model,
2633 built: &mut Built,
2634 shapes: &[Shape],
2635 total: usize,
2636 tol: Tolerances,
2637 ) -> OgeomResult<Shape> {
2638 let kind = self.word()?;
2639 let data = match kind {
2640 "Ve" => {
2641 let tolerance = self.number()?;
2642 let at = self.point()?;
2643 loop {
2646 let first = self.word()?;
2647 let second = self.word()?;
2648 if first == "0" && second == "0" {
2649 break;
2650 }
2651 match second {
2654 "1" => {
2655 let _ = self.count()?;
2656 }
2657 "2" | "3" => {
2658 let _ = self.count()?;
2659 let _ = self.count()?;
2660 }
2661 other => ogeom_bail!(
2662 Construction,
2663 "a vertex representation of kind {other} is not one this reads"
2664 ),
2665 }
2666 let _ = self.count()?;
2667 }
2668 Record::Vertex(VertexData {
2669 point: at,
2670 tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
2671 })
2672 }
2673 "Ed" => {
2674 let tolerance = self.number()?;
2675 let _same_parameter = self.flag()?;
2676 let _same_range = self.flag()?;
2677 let degenerate = self.flag()?;
2678 let mut data = EdgeData::new();
2679 data.tolerance = Tolerance::new(tolerance.max(tol.confusion()))?;
2680 data.degenerate = degenerate;
2681 let mut charts = Vec::new();
2682 loop {
2683 let kind = self.count()?;
2684 match kind {
2685 0 => break,
2686 1 => {
2687 let curve = self.count()?;
2688 let location = self.count()?;
2689 let lo = self.number()?;
2690 let hi = self.number()?;
2691 let id = built.curve(model, curve, location, tol)?;
2692 data.add(EdgeRepr::Curve3d {
2693 curve: id,
2694 range: (lo, hi),
2695 location: Location::identity(),
2696 });
2697 }
2698 2 | 3 => {
2699 let first = self.count()?;
2700 let second = if kind == 3 {
2701 let reversed = self.count()?;
2702 let _continuity = self.word()?;
2703 Some(reversed)
2704 } else {
2705 None
2706 };
2707 let surface = self.count()?;
2708 let location = self.count()?;
2709 let lo = self.number()?;
2710 let hi = self.number()?;
2711 let chart = |number: usize| {
2712 number
2713 .checked_sub(1)
2714 .and_then(|at| built.pcurves.get(at))
2715 .copied()
2716 };
2717 let (Some(curve), true, true) = (
2718 chart(first),
2719 second.is_none_or(|n| chart(n).is_some()),
2720 (1..=built.surfaces.len()).contains(&surface),
2721 ) else {
2722 ogeom_bail!(
2723 Construction,
2724 "an edge names chart curve {first} on surface {surface}"
2725 );
2726 };
2727 charts.push(StatedChart {
2728 curves: (curve, second.and_then(chart)),
2729 surface,
2730 location,
2731 range: (lo, hi),
2732 });
2733 }
2734 4 => {
2735 let _continuity = self.word()?;
2736 for _ in 0..4 {
2737 let _ = self.count()?;
2738 }
2739 }
2740 5 => {
2741 let _ = self.count()?;
2742 let _ = self.count()?;
2743 }
2744 6 => {
2745 for _ in 0..3 {
2746 let _ = self.count()?;
2747 }
2748 }
2749 7 => {
2750 for _ in 0..4 {
2751 let _ = self.count()?;
2752 }
2753 }
2754 other => ogeom_bail!(
2755 Construction,
2756 "an edge representation of kind {other} is not one this reads"
2757 ),
2758 }
2759 }
2760 data.assert_same_parameter(agree_on_parameter(model, &data, tol));
2767 Record::Edge(Box::new(data), charts)
2768 }
2769 "Fa" => {
2770 let _natural = self.flag()?;
2771 let tolerance = self.number()?;
2772 let surface = self.count()?;
2773 let location = self.count()?;
2774 if self.peek() == Some("2") {
2775 let _ = self.word()?;
2776 let _triangulation = self.count()?;
2777 }
2778 let on = built.surface(model, surface.max(1), location, tol)?;
2779 Record::Face(
2780 Box::new(FaceData {
2781 surface: on,
2782 location: Location::identity(),
2783 tolerance: Tolerance::new(tolerance.max(tol.confusion()))?,
2784 natural_restriction: false,
2785 triangulation: None,
2786 }),
2787 (surface.max(1), location),
2788 )
2789 }
2790 "Wi" => Record::Container(ShapeType::Wire),
2791 "Sh" => Record::Container(ShapeType::Shell),
2792 "So" => Record::Container(ShapeType::Solid),
2793 "CS" => Record::Container(ShapeType::CompSolid),
2794 "Co" => Record::Container(ShapeType::Compound),
2795 other => ogeom_bail!(Construction, "`{other}` is not a shape record this reads"),
2796 };
2797
2798 let _flags = self.word()?;
2801 let mut children = Vec::new();
2802 let mut written = Vec::new();
2803 while self.peek() != Some("*") {
2804 let (orientation, number, location) = self.reference()?;
2805 written.push((number, location));
2806 let Some(child) = pick(shapes, number, total) else {
2807 ogeom_bail!(
2808 Construction,
2809 "a record names subshape {number}, which is not above it"
2810 );
2811 };
2812 let sense = built.sense(&child, orientation);
2813 children.push(placed(child, built, location)?.composed(sense));
2814 }
2815 let _ = self.word()?;
2816 built.written.push(written);
2817
2818 Ok(match data {
2819 Record::Vertex(data) => model.add_vertex(data),
2820 Record::Edge(data, charts) => {
2821 children.sort_by_key(|v| v.orientation() != Orientation::Forward);
2824 let edge = model.add_edge(*data, &children)?;
2825 if !charts.is_empty() {
2826 built.charts.insert(edge.node(), charts);
2827 }
2828 edge
2829 }
2830 Record::Face(data, stated) => {
2831 let mut wires = Vec::with_capacity(children.len());
2836 for wire in children {
2837 if wire.location().is_identity() {
2838 wires.push(wire);
2839 continue;
2840 }
2841 let edges = model.children_of(&wire.oriented(Orientation::Forward))?;
2842 wires.push(model.add_wire(&edges)?.oriented(wire.orientation()));
2843 }
2844 built.bind_charts(model, stated, data.surface, &wires, tol)?;
2845 if built.turned(data.surface) {
2846 let wires: Vec<Shape> = wires.iter().map(Shape::reversed).collect();
2847 let face = model.add_face(*data, &wires)?;
2848 built.turned_faces.insert(face.node());
2849 face
2850 } else {
2851 model.add_face(*data, &wires)?
2852 }
2853 }
2854 Record::Container(ShapeType::Wire) => {
2855 let walk = walked(model, &children)?;
2856 model.add_wire(walk.as_deref().unwrap_or(&children))?
2857 }
2858 Record::Container(ShapeType::Shell) => {
2859 let faces = children
2860 .into_iter()
2861 .map(|face| settled(model, built, face, tol))
2862 .collect::<OgeomResult<Vec<_>>>()?;
2863 model.add_shell(&faces)?
2864 }
2865 Record::Container(ShapeType::Solid) => model.add_solid(&children)?,
2866 Record::Container(ShapeType::CompSolid) => model.add_compsolid(&children)?,
2867 Record::Container(_) => model.add_compound(&children)?,
2868 })
2869 }
2870}
2871
2872enum Record {
2874 Vertex(VertexData),
2875 Edge(Box<EdgeData>, Vec<StatedChart>),
2877 Face(Box<FaceData>, (usize, usize)),
2879 Container(ShapeType),
2880}
2881
2882fn agree_on_parameter(model: &Model, data: &EdgeData, tol: Tolerances) -> bool {
2890 use ogeom_geom::Surface as _;
2891 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
2892 return false;
2893 };
2894 let Some(geometry) = model.geometry().curve(*curve) else {
2895 return false;
2896 };
2897 let allowed = data.tolerance.get().max(tol.confusion());
2898 for representation in &data.representations {
2899 let Ok(moved) = representation
2902 .location()
2903 .map_or(Ok(Transform::IDENTITY), |l| l.composed(model.datums()))
2904 else {
2905 return false;
2906 };
2907 let (pcurve, prange, surface) = match representation {
2908 EdgeRepr::PCurve {
2909 curve,
2910 range,
2911 surface,
2912 ..
2913 } => (*curve, *range, *surface),
2914 EdgeRepr::Seam {
2915 forward,
2916 range,
2917 surface,
2918 ..
2919 } => (*forward, *range, *surface),
2920 _ => continue,
2921 };
2922 let (Some(chart), Some(on)) = (
2923 model.geometry().pcurve(pcurve),
2924 model.geometry().surface(surface),
2925 ) else {
2926 return false;
2927 };
2928 for k in 0..=8 {
2929 let f = f64::from(k) / 8.0;
2930 let t = (range.1 - range.0).mul_add(f, range.0);
2931 let pt = (prange.1 - prange.0).mul_add(f, prange.0);
2932 let (Ok(at), Ok(uv)) = (geometry.point_at(t, tol), chart.point_at(pt, tol)) else {
2933 return false;
2934 };
2935 let Ok(lifted) = on.point_at(uv.x, uv.y, tol) else {
2936 return false;
2937 };
2938 if lifted.distance(moved.apply(at)) > allowed {
2939 return false;
2940 }
2941 }
2942 }
2943 true
2944}