1use super::parse::{Entity, File, Value};
21use ogeom_algo::{make_edge_between, make_solid, make_vertex, sew};
22use ogeom_core::FastMap;
23use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
24use ogeom_geom::Curve3d as _;
25use ogeom_geom::Transformable as _;
26use ogeom_geom::{
27 BSplineCurve, BSplineSurface, CircleCurve, ConeSurface, Curve, CylinderSurface, EllipseCurve,
28 ExtrusionSurface, HyperbolaCurve, LineCurve, OffsetCurve, OffsetSurface, ParabolaCurve,
29 PlaneSurface, RevolutionSurface, SphereSurface, SurfaceGeometry, TorusSurface, TrimmedCurve,
30};
31use ogeom_math::{
32 Circle, Cone, ControlGrid, Cylinder, Direction, Ellipse, Frame, Hyperbola, KnotVector, Matrix3,
33 Parabola, Plane, Point, Sphere, Torus, Transform, Vector, Weighted,
34};
35use ogeom_topo::{Model, Shape};
36use std::collections::BTreeMap;
37
38const SURFACE_EXTENT: f64 = 1e5;
42
43fn is_bounded_plane(entity: &Entity) -> bool {
48 entity.kind == 108 && entity.form == 1 && (entity.status / 100) % 100 == 0
49}
50
51fn ellipse_start_angle(conic: &Entity) -> Option<f64> {
60 let [a, b, c, d, e, f] = [0, 1, 2, 3, 4, 5].map(|i| conic.at(i).real());
61 if b != 0.0 || a == 0.0 || c == 0.0 || (a > 0.0) != (c > 0.0) {
62 return None;
63 }
64 let (cx, cy) = (-d / (2.0 * a), -e / (2.0 * c));
65 let rhs = a * cx * cx + c * cy * cy - f;
66 let (ra2, rb2) = (rhs / a, rhs / c);
67 if ra2 <= 0.0 || rb2 <= 0.0 {
68 return None;
69 }
70 let (sx, sy) = (conic.at(7).real(), conic.at(8).real());
71 Some(
72 ((sy - cy) / rb2.sqrt())
73 .atan2((sx - cx) / ra2.sqrt())
74 .rem_euclid(core::f64::consts::TAU),
75 )
76}
77
78const UNTRIMMED_KINDS: [i64; 6] = [114, 118, 120, 122, 128, 140];
82
83const CSG_KINDS: [i64; 12] = [150, 152, 154, 156, 158, 160, 162, 164, 168, 180, 184, 430];
84
85const WIRE_KINDS: [i64; 8] = [100, 102, 104, 106, 110, 112, 126, 130];
89
90fn is_wire(entity: &Entity) -> bool {
94 WIRE_KINDS.contains(&entity.kind)
95 && (entity.kind != 106 || matches!(entity.form, 11 | 12 | 13 | 63))
96}
97
98fn moved_segments(
100 segments: Vec<(Curve, (f64, f64))>,
101 motion: &Transform,
102 tol: Tolerances,
103) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
104 if *motion == Transform::IDENTITY {
105 return Ok(segments);
106 }
107 segments
108 .into_iter()
109 .map(|(curve, range)| Ok((curve.transformed(motion, tol)?, range)))
110 .collect()
111}
112
113fn carried(child: &Entity) -> bool {
118 (child.status / 10_000) % 100 != 2
119}
120
121const INSTANCE_KINDS: [i64; 4] = [408, 412, 414, 420];
124
125const MOST_PLACED: usize = 1 << 20;
131
132#[derive(Debug, Default)]
134pub struct IgesReport {
135 pub scale_mm: f64,
137 pub skipped: BTreeMap<String, usize>,
141 pub warnings: Vec<String>,
143}
144
145#[derive(Debug)]
147pub struct IgesImport {
148 pub document: ogeom_doc::Document,
150 pub solids: Vec<Shape>,
153 pub sheets: Vec<Shape>,
155 pub wires: Vec<Shape>,
159 pub points: Vec<Shape>,
163 pub report: IgesReport,
165}
166
167#[derive(Clone, Default)]
170struct Content {
171 solids: Vec<Shape>,
172 faces: Vec<Shape>,
174 wires: Vec<Shape>,
175 points: Vec<Shape>,
176}
177
178impl Content {
179 fn moved(&self, location: &ogeom_topo::Location) -> Self {
180 let moved = |shapes: &[Shape]| shapes.iter().map(|x| x.moved(location)).collect();
181 Self {
182 solids: moved(&self.solids),
183 faces: moved(&self.faces),
184 wires: moved(&self.wires),
185 points: moved(&self.points),
186 }
187 }
188
189 fn len(&self) -> usize {
190 self.solids.len() + self.faces.len() + self.wires.len() + self.points.len()
191 }
192
193 fn extend(&mut self, other: Self) {
194 self.solids.extend(other.solids);
195 self.faces.extend(other.faces);
196 self.wires.extend(other.wires);
197 self.points.extend(other.points);
198 }
199}
200
201#[derive(Default)]
203struct Placed {
204 solids: Vec<Shape>,
205 sheets: Vec<Shape>,
207 wires: Vec<Shape>,
208 points: Vec<Shape>,
209 from: Vec<(i64, Shape)>,
211}
212
213fn array_count(de: i64, count: Option<i64>) -> OgeomResult<usize> {
216 match count.and_then(|c| usize::try_from(c).ok()) {
217 Some(c) if (1..=MOST_PLACED).contains(&c) => Ok(c),
218 _ => ogeom_bail!(
219 Construction,
220 "D{de}: an array of {count:?} positions; it needs 1 to {MOST_PLACED}"
221 ),
222 }
223}
224
225fn kept_positions(array: &Entity, at: usize, count: usize) -> Vec<usize> {
230 let listed = array.count(at);
231 if listed == 0 {
232 return (1..=count).collect();
233 }
234 let named: std::collections::BTreeSet<usize> = (0..listed)
235 .filter_map(|i| usize::try_from(array.at(at + 2 + i).int()).ok())
236 .filter(|k| (1..=count).contains(k))
237 .collect();
238 if array.at(at + 1).int() == 1 {
239 (1..=count).filter(|k| !named.contains(k)).collect()
240 } else {
241 named.into_iter().collect()
242 }
243}
244
245type BuiltEdge = (Shape, Curve, (f64, f64));
247
248struct Library {
252 dir: Option<std::path::PathBuf>,
254 files: BTreeMap<String, Result<File, String>>,
255}
256
257static NO_LIBRARY: Library = Library {
259 dir: None,
260 files: BTreeMap::new(),
261};
262
263impl Library {
264 fn gather(dir: &std::path::Path, root: &File) -> Self {
267 let mut library = Self {
268 dir: Some(dir.to_path_buf()),
269 files: BTreeMap::new(),
270 };
271 let mut wanted = referenced_files(root);
272 while let Some(name) = wanted.pop() {
273 if library.files.contains_key(&name) {
274 continue;
275 }
276 let read = library.read(&name);
277 if let Ok(file) = &read {
278 wanted.extend(referenced_files(file));
279 }
280 library.files.insert(name, read);
281 }
282 library
283 }
284
285 fn read(&self, name: &str) -> Result<File, String> {
290 let Some(dir) = &self.dir else {
291 return Err("the deck was read as text, with no directory to look in; \
292 read_iges_file looks beside the file"
293 .to_owned());
294 };
295 let path = dir.join(name);
296 if !path.is_file() {
297 return Err(format!("{} is not there", path.display()));
298 }
299 let text = std::fs::read_to_string(&path)
300 .map_err(|e| format!("{} could not be read: {e}", path.display()))?;
301 let file = super::parse::parse(&text).map_err(|e| format!("{}: {e}", path.display()))?;
302 let own = file
303 .global
304 .get(3)
305 .and_then(Value::text)
306 .map_or("", file_name);
307 if own != name {
308 return Err(format!(
309 "{} names itself \"{own}\" in its global section, not \"{name}\"",
310 path.display()
311 ));
312 }
313 Ok(file)
314 }
315
316 fn file(&self, id: &str) -> Result<&File, String> {
318 let name = file_name(id);
319 match self.files.get(name) {
320 Some(Ok(file)) => Ok(file),
321 Some(Err(why)) => Err(why.clone()),
322 None => Err(self
323 .read(name)
324 .err()
325 .unwrap_or_else(|| format!("\"{name}\" was not gathered with the deck"))),
326 }
327 }
328}
329
330fn file_name(id: &str) -> &str {
335 id.rsplit(['/', '\\', ':', ']']).next().unwrap_or(id).trim()
336}
337
338fn referenced_files(file: &File) -> Vec<String> {
341 file.entities
342 .values()
343 .filter(|e| e.kind == 416 && matches!(e.form, 0 | 1))
344 .filter_map(|e| e.at(0).text())
345 .map(|id| file_name(id).to_owned())
346 .filter(|name| !name.is_empty())
347 .collect()
348}
349
350pub fn read_iges(text: &str, tol: Tolerances) -> OgeomResult<IgesImport> {
365 let file = super::parse::parse(text)?;
366 read_parsed(&file, &NO_LIBRARY, tol)
367}
368
369pub fn read_iges_file(path: &std::path::Path, tol: Tolerances) -> OgeomResult<IgesImport> {
392 let text = match std::fs::read_to_string(path) {
393 Ok(text) => text,
394 Err(e) => ogeom_bail!(Construction, "{} could not be read: {e}", path.display()),
395 };
396 let file = super::parse::parse(&text)?;
397 let dir = path.parent().unwrap_or_else(|| std::path::Path::new(""));
398 let library = Library::gather(dir, &file);
399 read_parsed(&file, &library, tol)
400}
401
402fn read_parsed(file: &File, library: &Library, tol: Tolerances) -> OgeomResult<IgesImport> {
405 let Some(scale_mm) = file.scale_mm() else {
406 ogeom_bail!(
407 Construction,
408 "IGES global section names units this reader cannot convert to millimetres"
409 );
410 };
411 let mut reader = Reader::new(file, Model::new(), scale_mm, library, tol);
412 let on_sheets = on_sheets(file);
413 let construction_des = construction_entities(file, &on_sheets);
414
415 let mut solids: Vec<(i64, Shape)> = Vec::new();
417 let solid_des: Vec<i64> = file
418 .entities
419 .iter()
420 .filter(|(_, e)| e.kind == 186)
421 .map(|(de, _)| *de)
422 .collect();
423 let total = solid_des.len() as u64;
424 for (done, de) in solid_des.into_iter().enumerate() {
425 ogeom_core::progress::checkpoint()?;
426 ogeom_core::progress::stage_at("iges: solid", done as u64 + 1, total);
427 match reader.manifold_solid(de) {
428 Ok(solid) => solids.push((de, solid)),
429 Err(e) => reader
430 .report
431 .warnings
432 .push(format!("D{de}: manifold solid failed to build: {e}")),
433 }
434 }
435
436 let mut faces = Vec::new();
441 let mut built_from: Vec<(i64, Shape)> = solids.clone();
443 let face_des: Vec<i64> = file
444 .entities
445 .iter()
446 .filter(|(de, e)| {
447 matches!(e.kind, 143 | 144)
448 && (e.status / 10_000) % 100 == 0
449 && !reader.visited.contains_key(de)
450 && !construction_des.contains(de)
451 })
452 .map(|(de, _)| *de)
453 .collect();
454 let total = face_des.len() as u64;
455 reader.trim_poles = reader.poles_of_trims(&face_des);
456 for (done, de) in face_des.into_iter().enumerate() {
457 ogeom_core::progress::checkpoint()?;
458 ogeom_core::progress::stage_at("iges: face", done as u64 + 1, total);
459 match reader.face(de) {
460 Ok(face) => {
461 built_from.push((de, face.clone()));
462 faces.push(face);
463 }
464 Err(e) => reader
465 .report
466 .warnings
467 .push(format!("D{de}: trimmed surface failed to build: {e}")),
468 }
469 }
470 let bare_des: Vec<i64> = file
475 .entities
476 .iter()
477 .filter(|(de, e)| {
478 (UNTRIMMED_KINDS.contains(&e.kind) || is_bounded_plane(e))
479 && (e.status / 10_000) % 100 == 0
480 && !reader.visited.contains_key(de)
481 && !construction_des.contains(de)
482 })
483 .map(|(de, _)| *de)
484 .collect();
485 for de in bare_des {
486 match reader.untrimmed_face(de) {
487 Ok(face) => {
488 built_from.push((de, face.clone()));
489 faces.push(face);
490 }
491 Err(e) => reader
492 .report
493 .warnings
494 .push(format!("D{de}: untrimmed surface failed to build: {e}")),
495 }
496 }
497 let mut sheets = Vec::new();
498 if !faces.is_empty() {
499 let sewn = sew(&mut reader.model, &faces, tol)?;
500 for shell in &sewn.shells {
501 ogeom_algo::join_seam_columns(&mut reader.model, shell, tol)?;
504 if ogeom_algo::is_shell_closed(&reader.model, shell)? {
505 let solid = make_solid(&mut reader.model, std::slice::from_ref(shell))?.shape;
506 solids.push((0, solid));
507 } else {
508 sheets.push(shell.clone());
509 }
510 }
511 }
512
513 let csg_des: Vec<i64> = file
516 .entities
517 .iter()
518 .filter(|(de, e)| {
519 CSG_KINDS.contains(&e.kind)
520 && (e.status / 10_000) % 100 == 0
521 && !reader.visited.contains_key(de)
522 })
523 .map(|(de, _)| *de)
524 .collect();
525 for de in csg_des {
526 match reader.csg_solids(de) {
527 Ok(built) => {
528 for solid in built {
529 built_from.push((de, solid.clone()));
530 solids.push((de, solid));
531 }
532 }
533 Err(e) => reader
534 .report
535 .warnings
536 .push(format!("D{de}: constructive solid failed to build: {e}")),
537 }
538 }
539
540 let placed = reader.subfigure_instances(tol)?;
544 solids.extend(placed.solids.into_iter().map(|s| (0, s)));
545 sheets.extend(placed.sheets);
546 built_from.extend(placed.from);
547 let mut wires = placed.wires;
548 let mut points = placed.points;
549
550 let curve_des: Vec<i64> = file
554 .entities
555 .iter()
556 .filter(|(de, e)| {
557 is_wire(e)
558 && (e.status / 10_000) % 100 == 0
559 && (e.status / 100) % 100 == 0
560 && !on_sheets.contains(de)
561 && !reader.visited.contains_key(de)
562 })
563 .map(|(de, _)| *de)
564 .collect();
565 for de in curve_des {
566 match reader.wireframe(de) {
567 Ok(wire) => wires.push(wire),
568 Err(e) => reader
569 .report
570 .warnings
571 .push(format!("D{de}: curve failed to build: {e}")),
572 }
573 }
574
575 let point_des: Vec<i64> = file
578 .entities
579 .iter()
580 .filter(|(de, e)| {
581 e.kind == 116
582 && (e.status / 10_000) % 100 == 0
583 && (e.status / 100) % 100 == 0
584 && !on_sheets.contains(de)
585 && !reader.visited.contains_key(de)
586 })
587 .map(|(de, _)| *de)
588 .collect();
589 for de in point_des {
590 match reader.point(de) {
591 Ok(point) => points.push(point),
592 Err(e) => reader
593 .report
594 .warnings
595 .push(format!("D{de}: point failed to build: {e}")),
596 }
597 }
598
599 let mut construction = Vec::new();
602 for de in construction_des {
603 if reader.visited.contains_key(&de) {
604 continue;
605 }
606 match reader.construction_item(de) {
607 Ok(Some(geometry)) => {
608 let name = reader
609 .long_name(de)
610 .or_else(|| file.entity(de).map(|e| e.label.clone()))
611 .filter(|n| !n.is_empty())
612 .unwrap_or_else(|| format!("construction-{}", construction.len()));
613 construction.push((name, geometry));
614 }
615 Ok(None) => {}
616 Err(e) => reader
617 .report
618 .warnings
619 .push(format!("D{de}: construction item failed to build: {e}")),
620 }
621 }
622
623 if solids.is_empty()
624 && sheets.is_empty()
625 && wires.is_empty()
626 && points.is_empty()
627 && construction.is_empty()
628 {
629 if reader.report.warnings.is_empty() {
632 ogeom_bail!(
633 Construction,
634 "the IGES file contains no manifold solid, no independent \
635 trimmed or untrimmed surface, no independent curve or \
636 point and no construction geometry this reader translates"
637 );
638 }
639 ogeom_bail!(
640 Construction,
641 "every shape in the IGES file failed to build: {}",
642 reader.report.warnings.join("; ")
643 );
644 }
645
646 if reader.vertex_misses.0 > 0 {
647 let (count, worst) = reader.vertex_misses;
648 reader.report.warnings.push(format!(
649 "{count} vertices sat off the curve ends they bound, by up to {worst:.2e}; \
650 their tolerances grew to say so"
651 ));
652 }
653 crate::bodies::rechart_beside_poles(
654 &mut reader.model,
655 solids.iter().map(|(_, s)| s).chain(sheets.iter()),
656 &mut reader.report.warnings,
657 tol,
658 )?;
659 let (callouts, dimensions) = reader.annotations();
660 for (de, entity) in &file.entities {
662 if (entity.kind == 402 && matches!(entity.form, 1 | 7 | 14 | 15))
663 || (entity.kind == 406 && entity.form == 1)
664 {
665 reader.visited.insert(*de, ());
666 }
667 }
668 let names: Vec<Option<String>> = solids.iter().map(|(de, _)| reader.long_name(*de)).collect();
671 for (de, entity) in &file.entities {
673 if !reader.visited.contains_key(de) {
674 let key = if entity.form == 0 {
675 format!("type {}", entity.kind)
676 } else {
677 format!("type {} form {}", entity.kind, entity.form)
678 };
679 *reader.report.skipped.entry(key).or_default() += 1;
680 }
681 }
682 let mut document = reader.document(&solids, &names, &sheets, construction);
683 {
684 let pmi = document.pmi_mut();
685 let base = pmi.dimensions.len();
686 for (i, dimension) in dimensions.into_iter().enumerate() {
687 let (callout, dimension) = dimension;
688 pmi.dimensions.push(dimension);
689 let mut callout = callout;
690 callout.annotates = Some(ogeom_doc::Annotated::Dimension(base + i));
691 pmi.callouts.push(callout);
692 }
693 pmi.callouts.extend(callouts);
694 }
695 layers(file, &mut document, &built_from, &mut reader.report);
696 let solids = solids.into_iter().map(|(_, s)| s).collect();
697 Ok(IgesImport {
698 document,
699 solids,
700 sheets,
701 wires,
702 points,
703 report: reader.report,
704 })
705}
706
707struct Reader<'a> {
708 file: &'a File,
709 library: &'a Library,
711 model: Model,
712 report: IgesReport,
713 visited: BTreeMap<i64, ()>,
715 vertices: FastMap<(i64, i64), Shape>,
718 edges: FastMap<(i64, i64), BuiltEdge>,
720 pieces: FastMap<(i64, i64), Vec<Shape>>,
723 pole_vertices: Vec<Shape>,
725 trim_poles: Vec<Point>,
729 vertex_misses: (usize, f64),
732 images: FastMap<(i64, u64, u64), Vec<(Curve, DerivedImage)>>,
737 face_de: Option<i64>,
739 depth: u32,
744 tol: Tolerances,
745}
746
747const MOST_NESTING: u32 = 64;
750
751impl<'a> Reader<'a> {
752 fn new(
753 file: &'a File,
754 model: Model,
755 scale_mm: f64,
756 library: &'a Library,
757 tol: Tolerances,
758 ) -> Self {
759 Reader {
760 file,
761 library,
762 model,
763 report: IgesReport {
764 scale_mm,
765 ..IgesReport::default()
766 },
767 visited: BTreeMap::new(),
768 vertices: FastMap::default(),
769 edges: FastMap::default(),
770 pieces: FastMap::default(),
771 pole_vertices: Vec::new(),
772 trim_poles: Vec::new(),
773 vertex_misses: (0, 0.0),
774 images: FastMap::default(),
775 face_de: None,
776 depth: 0,
777 tol,
778 }
779 }
780
781 fn nested<T>(&mut self, build: impl FnOnce(&mut Self) -> OgeomResult<T>) -> OgeomResult<T> {
783 if self.depth >= MOST_NESTING {
784 ogeom_bail!(
785 Construction,
786 "entities refer to one another more than {MOST_NESTING} deep; \
787 the file refers in a circle"
788 );
789 }
790 self.depth += 1;
791 let built = build(self);
792 self.depth -= 1;
793 built
794 }
795
796 fn entity(&mut self, de: i64) -> OgeomResult<&'a Entity> {
797 let Some(entity) = self.file.entity(de) else {
798 ogeom_bail!(Construction, "IGES pointer D{de} names no entity");
799 };
800 self.visited.insert(de.abs(), ());
801 Ok(entity)
802 }
803
804 fn placement(&mut self, entity: &Entity) -> OgeomResult<Transform> {
807 self.placement_within(entity, 0)
808 }
809
810 fn placement_within(&mut self, entity: &Entity, depth: u32) -> OgeomResult<Transform> {
814 const MOST_CHAINED: u32 = 32;
815 if entity.transform == 0 {
816 return Ok(Transform::IDENTITY);
817 }
818 if depth >= MOST_CHAINED {
819 ogeom_bail!(
820 Construction,
821 "transforms chain more than {MOST_CHAINED} deep; the file places a \
822 transform by itself"
823 );
824 }
825 let de = entity.transform;
826 let t = self.entity(de)?;
827 if t.kind != 124 {
828 ogeom_bail!(
829 Construction,
830 "D{de}: a transformation pointer names a type {} entity",
831 t.kind
832 );
833 }
834 let v = |i: usize| t.at(i).real();
835 let s = self.report.scale_mm;
836 let linear = Matrix3::new([[v(0), v(1), v(2)], [v(4), v(5), v(6)], [v(8), v(9), v(10)]]);
837 let translation = Vector::new(v(3) * s, v(7) * s, v(11) * s);
838 let m = Transform::from_parts(linear, 1.0, translation, self.tol.angular())?;
839 if t.transform != 0 {
840 let outer = self.placement_within(t, depth + 1)?;
841 return Ok(outer * m);
842 }
843 Ok(m)
844 }
845
846 fn parent_motion(&mut self, parent: &Entity, child: i64) -> OgeomResult<Transform> {
850 if self.file.entity(child).is_none_or(carried) {
851 self.placement(parent)
852 } else {
853 Ok(Transform::IDENTITY)
854 }
855 }
856
857 fn definition_motion(&mut self, parent: &Entity, child: i64) -> OgeomResult<Option<Transform>> {
863 if parent.transform == 0 || self.file.entity(child).is_none_or(carried) {
864 return Ok(None);
865 }
866 let placement = self.placement(parent)?;
867 if placement == Transform::IDENTITY {
868 return Ok(None);
869 }
870 Ok(Some(placement.inverse()?))
871 }
872
873 fn child_curve(&mut self, parent: &Entity, de: i64) -> OgeomResult<(Curve, (f64, f64))> {
875 let (curve, range) = self.curve(de)?;
876 match self.definition_motion(parent, de)? {
877 Some(motion) => Ok((curve.transformed(&motion, self.tol)?, range)),
878 None => Ok((curve, range)),
879 }
880 }
881
882 fn child_surface(&mut self, parent: &Entity, de: i64) -> OgeomResult<SurfaceGeometry> {
884 let surface = self.surface(de)?;
885 match self.definition_motion(parent, de)? {
886 Some(motion) => Ok(surface.transformed(&motion, self.tol)?),
887 None => Ok(surface),
888 }
889 }
890
891 fn point3(&self, e: &Entity, i: usize) -> Point {
892 let s = self.report.scale_mm;
893 Point::new(
894 e.at(i).real() * s,
895 e.at(i + 1).real() * s,
896 e.at(i + 2).real() * s,
897 )
898 }
899
900 fn curve(&mut self, de: i64) -> OgeomResult<(Curve, (f64, f64))> {
902 self.nested(|reader| reader.curve_at(de))
903 }
904
905 fn curve_at(&mut self, de: i64) -> OgeomResult<(Curve, (f64, f64))> {
906 let entity = self.entity(de)?;
907 let scale = self.report.scale_mm;
908 let (curve, range) = match entity.kind {
909 110 => {
910 let a = self.point3(entity, 0);
911 let b = self.point3(entity, 3);
912 let line = LineCurve::segment(a, b, self.tol)?;
913 (Curve::from(line), (0.0, a.distance(b)))
914 }
915 100 => {
916 let zt = entity.at(0).real() * scale;
919 let c = Point::new(entity.at(1).real() * scale, entity.at(2).real() * scale, zt);
920 let s = Point::new(entity.at(3).real() * scale, entity.at(4).real() * scale, zt);
921 let e = Point::new(entity.at(5).real() * scale, entity.at(6).real() * scale, zt);
922 let radius = c.distance(s);
923 let x = Direction::new(s - c, self.tol)?;
924 let frame = Frame::new(c, Direction::Z, x, self.tol)?;
925 let circle = Circle::new(frame, radius, self.tol)?;
926 let to_end = e - c;
927 let ang = to_end
928 .dot(frame.y().vector())
929 .atan2(to_end.dot(frame.x().vector()))
930 .rem_euclid(core::f64::consts::TAU);
931 let sweep = if ang <= self.tol.parametric() {
932 core::f64::consts::TAU
933 } else {
934 ang
935 };
936 (Curve::from(CircleCurve::new(circle)), (0.0, sweep))
937 }
938 104 => self.conic(de, entity)?,
939 112 => self.spline_curve(de, entity)?,
940 126 => self.nurbs_curve(de, entity)?,
941 130 => self.offset_curve(de, entity)?,
942 106 => self.copious_path(de, entity)?,
943 102 => ogeom_bail!(
944 Construction,
945 "D{de}: a composite curve is a sequence, not a curve; the \
946 caller walks its segments"
947 ),
948 kind => ogeom_bail!(
949 Construction,
950 "D{de}: curve entity type {kind}{} is not translated; see \
951 docs/CAPABILITIES.md, io.iges",
952 super::entity_name(kind).map_or_else(String::new, |n| format!(" ({n})"))
953 ),
954 };
955 let placement = self.placement(entity)?;
957 if placement == Transform::IDENTITY {
958 Ok((curve, range))
959 } else {
960 Ok((curve.transformed(&placement, self.tol)?, range))
961 }
962 }
963
964 fn copious_path(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
973 if !matches!(entity.form, 11 | 12 | 13 | 63) {
974 ogeom_bail!(
975 Construction,
976 "D{de}: copious data form {} is a set of points, not a curve",
977 entity.form
978 );
979 }
980 let scale = self.report.scale_mm;
981 let flag = entity.at(0).int();
982 let count = entity.count(1);
983 let (first, stride) = match flag {
984 1 => (3, 2),
985 2 => (2, 3),
986 3 => (2, 6),
987 other => ogeom_bail!(
988 Construction,
989 "D{de}: copious data interpretation flag {other} is not 1, 2 or 3"
990 ),
991 };
992 let height = if flag == 1 { entity.at(2).real() } else { 0.0 };
993 let mut points: Vec<Point> = Vec::with_capacity(count + 1);
994 for i in 0..count {
995 let at = first + i * stride;
996 let z = if flag == 1 {
997 height
998 } else {
999 entity.at(at + 2).real()
1000 };
1001 let p = Point::new(
1002 entity.at(at).real() * scale,
1003 entity.at(at + 1).real() * scale,
1004 z * scale,
1005 );
1006 if points
1007 .last()
1008 .is_none_or(|q| q.distance(p) > self.tol.confusion())
1009 {
1010 points.push(p);
1011 }
1012 }
1013 if entity.form == 63
1014 && let (Some(&head), Some(&tail)) = (points.first(), points.last())
1015 && head.distance(tail) > self.tol.confusion()
1016 {
1017 points.push(head);
1018 }
1019 if points.len() < 2 {
1020 ogeom_bail!(Construction, "D{de}: copious data draws no segment");
1021 }
1022 let n = points.len();
1023 let mut knots = Vec::with_capacity(n + 2);
1024 knots.push(0.0);
1025 #[allow(clippy::cast_precision_loss, reason = "point counts")]
1026 knots.extend((0..n).map(|i| i as f64));
1027 #[allow(clippy::cast_precision_loss, reason = "point counts")]
1028 knots.push((n - 1) as f64);
1029 let curve = BSplineCurve::new(KnotVector::new(knots, 1)?, points, self.tol)?;
1030 #[allow(clippy::cast_precision_loss, reason = "point counts")]
1031 let range = (0.0, (n - 1) as f64);
1032 Ok((Curve::from(curve), range))
1033 }
1034
1035 fn conic(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
1044 let scale = self.report.scale_mm;
1045 let (a, b, c, d, e, f) = (
1046 entity.at(0).real(),
1047 entity.at(1).real(),
1048 entity.at(2).real(),
1049 entity.at(3).real(),
1050 entity.at(4).real(),
1051 entity.at(5).real(),
1052 );
1053 let zt = entity.at(6).real() * scale;
1054 let start = Point::new(entity.at(7).real() * scale, entity.at(8).real() * scale, zt);
1055 let end = Point::new(
1056 entity.at(9).real() * scale,
1057 entity.at(10).real() * scale,
1058 zt,
1059 );
1060 if b.abs() > 1e-12 {
1061 let theta = 0.5 * b.atan2(a - c);
1062 let (sin, cos) = theta.sin_cos();
1063 let turned = [
1065 a * cos * cos + b * cos * sin + c * sin * sin,
1066 a * sin * sin - b * cos * sin + c * cos * cos,
1067 d * cos + e * sin,
1068 -d * sin + e * cos,
1069 f,
1070 ];
1071 let about = ogeom_math::Axis::new(Point::ORIGIN, Direction::Z);
1072 let back = Transform::rotation(about, theta);
1073 let into = Transform::rotation(about, -theta);
1074 let (curve, range) =
1075 self.axis_aligned_conic(de, turned, zt, into.apply(start), into.apply(end))?;
1076 return Ok((curve.transformed(&back, self.tol)?, range));
1077 }
1078 self.axis_aligned_conic(de, [a, c, d, e, f], zt, start, end)
1079 }
1080
1081 fn axis_aligned_conic(
1084 &mut self,
1085 de: i64,
1086 [a, c, d, e, f]: [f64; 5],
1087 zt: f64,
1088 start: Point,
1089 end: Point,
1090 ) -> OgeomResult<(Curve, (f64, f64))> {
1091 let scale = self.report.scale_mm;
1092 if a != 0.0 && c != 0.0 && (a > 0.0) == (c > 0.0) {
1095 let sign = if a > 0.0 { 1.0 } else { -1.0 };
1096 return self.ellipse_arc(de, [a, c, d, e, f].map(|k| k * sign), zt, start, end);
1097 }
1098 if a != 0.0 && c != 0.0 {
1099 return Self::hyperbola_arc(de, [a, c, d, e, f], zt, start, end, scale, self.tol);
1100 }
1101 if (a == 0.0) != (c == 0.0) {
1102 return Self::parabola_arc(de, [a, c, d, e, f], zt, start, end, scale, self.tol);
1103 }
1104 ogeom_bail!(Construction, "D{de}: conic arc coefficients close no conic")
1105 }
1106
1107 fn ellipse_arc(
1110 &mut self,
1111 de: i64,
1112 [a, c, d, e, f]: [f64; 5],
1113 zt: f64,
1114 start: Point,
1115 end: Point,
1116 ) -> OgeomResult<(Curve, (f64, f64))> {
1117 let scale = self.report.scale_mm;
1118 let cx = -d / (2.0 * a);
1119 let cy = -e / (2.0 * c);
1120 let rhs = a * cx * cx + c * cy * cy - f;
1121 let (ra2, rb2) = (rhs / a, rhs / c);
1122 if ra2 <= 0.0 || rb2 <= 0.0 {
1123 ogeom_bail!(
1124 Construction,
1125 "D{de}: conic arc coefficients close no ellipse"
1126 );
1127 }
1128 let centre = Point::new(cx * scale, cy * scale, zt);
1129 let (rx, ry) = (ra2.sqrt() * scale, rb2.sqrt() * scale);
1130 let (frame, major, minor) = if rx >= ry {
1133 (
1134 Frame::new(centre, Direction::Z, Direction::X, self.tol)?,
1135 rx,
1136 ry,
1137 )
1138 } else {
1139 (
1140 Frame::new(centre, Direction::Z, Direction::Y, self.tol)?,
1141 ry,
1142 rx,
1143 )
1144 };
1145 let ellipse = Ellipse::new(frame, major, minor, self.tol)?;
1146 let angle_of = |p: Point| -> f64 {
1147 let local = frame.to_local(p);
1148 (local.y / minor)
1149 .atan2(local.x / major)
1150 .rem_euclid(core::f64::consts::TAU)
1151 };
1152 let t0 = angle_of(start);
1153 let mut t1 = angle_of(end);
1154 if t1 <= t0 + self.tol.parametric() {
1155 t1 += core::f64::consts::TAU;
1156 }
1157 Ok((Curve::from(EllipseCurve::new(ellipse)), (t0, t1)))
1158 }
1159
1160 fn hyperbola_arc(
1166 de: i64,
1167 [a, c, d, e, f]: [f64; 5],
1168 zt: f64,
1169 start: Point,
1170 end: Point,
1171 scale: f64,
1172 tol: Tolerances,
1173 ) -> OgeomResult<(Curve, (f64, f64))> {
1174 let cx = -d / (2.0 * a);
1175 let cy = -e / (2.0 * c);
1176 let rhs = a * cx * cx + c * cy * cy - f;
1177 if rhs == 0.0 {
1178 ogeom_bail!(
1179 Construction,
1180 "D{de}: conic arc coefficients close no hyperbola; they cross"
1181 );
1182 }
1183 let (pa, pc) = (a / rhs, c / rhs);
1186 let (major, minor, along_x) = if pa > 0.0 {
1187 ((1.0 / pa).sqrt(), (-1.0 / pc).sqrt(), true)
1188 } else {
1189 ((1.0 / pc).sqrt(), (-1.0 / pa).sqrt(), false)
1190 };
1191 let centre = Point::new(cx * scale, cy * scale, zt);
1192 let axis = if along_x { Direction::X } else { Direction::Y };
1193 let side = (start - centre).dot(axis.vector());
1195 let x = if side >= 0.0 {
1196 axis
1197 } else {
1198 Direction::new(-axis.vector(), tol)?
1199 };
1200 let build = |z: Direction| -> OgeomResult<(Curve, (f64, f64))> {
1201 let frame = Frame::new(centre, z, x, tol)?;
1202 let (major, minor) = (major * scale, minor * scale);
1203 let hyperbola = Hyperbola::new(frame, major, minor, tol)?;
1204 let t_of = |p: Point| (frame.to_local(p).y / minor).asinh();
1205 let (t0, t1) = (t_of(start), t_of(end));
1206 let extent = t0.abs().max(t1.abs()).max(1e-3) * 1.5;
1207 Ok((
1208 Curve::from(HyperbolaCurve::new(hyperbola, extent)?),
1209 (t0, t1),
1210 ))
1211 };
1212 let (curve, (t0, t1)) = build(Direction::Z)?;
1213 if t1 > t0 {
1214 return Ok((curve, (t0, t1)));
1215 }
1216 let (curve, (t0, t1)) = build(Direction::new(-Direction::Z.vector(), tol)?)?;
1218 Ok((curve, (t0, t1)))
1219 }
1220
1221 fn parabola_arc(
1225 de: i64,
1226 [a, c, d, e, f]: [f64; 5],
1227 zt: f64,
1228 start: Point,
1229 end: Point,
1230 scale: f64,
1231 tol: Tolerances,
1232 ) -> OgeomResult<(Curve, (f64, f64))> {
1233 let (apex_across, apex_along, k, along) = if c == 0.0 {
1236 if e == 0.0 {
1237 ogeom_bail!(
1238 Construction,
1239 "D{de}: conic arc coefficients close no parabola"
1240 );
1241 }
1242 let x0 = -d / (2.0 * a);
1243 let y0 = -(a * x0 * x0 + d * x0 + f) / e;
1244 (x0, y0, -a / e, Direction::Y)
1245 } else {
1246 if d == 0.0 {
1247 ogeom_bail!(
1248 Construction,
1249 "D{de}: conic arc coefficients close no parabola"
1250 );
1251 }
1252 let y0 = -e / (2.0 * c);
1253 let x0 = -(c * y0 * y0 + e * y0 + f) / d;
1254 (y0, x0, -c / d, Direction::X)
1255 };
1256 let apex = if c == 0.0 {
1257 Point::new(apex_across * scale, apex_along * scale, zt)
1258 } else {
1259 Point::new(apex_along * scale, apex_across * scale, zt)
1260 };
1261 let x = if k >= 0.0 {
1263 along
1264 } else {
1265 Direction::new(-along.vector(), tol)?
1266 };
1267 let focal = scale / (4.0 * k.abs());
1269 let build = |z: Direction| -> OgeomResult<(Curve, (f64, f64))> {
1270 let frame = Frame::new(apex, z, x, tol)?;
1271 let parabola = Parabola::new(frame, focal, tol)?;
1272 let t_of = |p: Point| frame.to_local(p).y;
1273 let (t0, t1) = (t_of(start), t_of(end));
1274 let extent = t0.abs().max(t1.abs()).max(1e-3) * 1.5;
1275 Ok((Curve::from(ParabolaCurve::new(parabola, extent)?), (t0, t1)))
1276 };
1277 let (curve, (t0, t1)) = build(Direction::Z)?;
1278 if t1 > t0 {
1279 return Ok((curve, (t0, t1)));
1280 }
1281 let (curve, (t0, t1)) = build(Direction::new(-Direction::Z.vector(), tol)?)?;
1282 Ok((curve, (t0, t1)))
1283 }
1284
1285 fn offset_curve(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
1296 let scale = self.report.scale_mm;
1297 let kind = entity.at(1).int();
1298 let (d1, d2) = (entity.at(5).real(), entity.at(7).real());
1299 if kind != 1 || (d1 - d2).abs() > 1e-12 {
1300 return self.varying_offset(de, entity, kind);
1301 }
1302 let (basis, range) = self.child_curve(entity, entity.at(0).int())?;
1303 let reference = Direction::from_coords(
1304 entity.at(9).real(),
1305 entity.at(10).real(),
1306 entity.at(11).real(),
1307 self.tol,
1308 )?;
1309 let (tt1, tt2) = (entity.at(12).real(), entity.at(13).real());
1310 let range = if tt2 > tt1 { (tt1, tt2) } else { range };
1311 let offset = OffsetCurve::new(basis, -d1 * scale, reference)?;
1312 Ok((Curve::Offset(Box::new(offset)), range))
1313 }
1314
1315 fn varying_offset(
1317 &mut self,
1318 de: i64,
1319 entity: &Entity,
1320 kind: i64,
1321 ) -> OgeomResult<(Curve, (f64, f64))> {
1322 let scale = self.report.scale_mm;
1323 let (basis, range) = self.child_curve(entity, entity.at(0).int())?;
1324 let reference = Direction::from_coords(
1325 entity.at(9).real(),
1326 entity.at(10).real(),
1327 entity.at(11).real(),
1328 self.tol,
1329 )?
1330 .vector();
1331 let (tt1, tt2) = (entity.at(12).real(), entity.at(13).real());
1332 let range = if tt2 > tt1 { (tt1, tt2) } else { range };
1333 const SAMPLES: usize = 400;
1337 let stations = ogeom_algo::traced::stations(&basis, range, SAMPLES);
1338 let distance: Box<dyn Fn(f64) -> OgeomResult<f64>> = match kind {
1339 2 => {
1340 let (d1, td1, d2, td2) = (
1344 entity.at(5).real(),
1345 entity.at(6).real(),
1346 entity.at(7).real(),
1347 entity.at(8).real(),
1348 );
1349 let mut lengths = vec![0.0_f64];
1350 for pair in stations.windows(2) {
1351 let held = lengths[lengths.len() - 1];
1352 lengths.push(
1353 held + ogeom_algo::curve_length(&basis, (pair[0], pair[1]), self.tol)?
1354 / scale,
1355 );
1356 }
1357 let span = td2 - td1;
1358 let (basis, stations, tol) = (basis.clone(), stations.clone(), self.tol);
1359 Box::new(move |t| {
1360 let i = stations.partition_point(|&s| s <= t).saturating_sub(1);
1361 let along = lengths[i]
1362 + ogeom_algo::curve_length(&basis, (stations[i], t), tol)? / scale;
1363 let f = if span.abs() > 0.0 {
1364 (along - td1) / span
1365 } else {
1366 0.0
1367 };
1368 Ok(d1 + (d2 - d1) * f)
1369 })
1370 }
1371 3 => {
1372 let (function, _) = self.curve(entity.at(2).int())?;
1375 let which = entity.at(3).int();
1376 let tol = self.tol;
1377 Box::new(move |t| {
1378 let p = function.point_at(t, tol)?;
1379 let value = match which {
1380 1 => p.x,
1381 2 => p.y,
1382 _ => p.z,
1383 };
1384 Ok(value / scale)
1385 })
1386 }
1387 _ => ogeom_bail!(
1388 Construction,
1389 "D{de}: offset curve type {kind} names no distance law"
1390 ),
1391 };
1392 let tol = self.tol;
1393 let displaced = |t: f64| -> OgeomResult<Point> {
1394 let p = basis.point_at(t, tol)?;
1395 let tangent = basis.d1_at(t, tol)?;
1396 let side = reference.cross(tangent);
1397 let m = side.magnitude();
1398 if m <= tol.angular() {
1399 ogeom_bail!(
1400 Construction,
1401 "D{de}: the offset's reference runs along its base curve"
1402 );
1403 }
1404 Ok(p + side * (distance(t)? * scale / m))
1405 };
1406 let fitted = ogeom_algo::traced::fit_traced(
1407 displaced,
1408 &stations,
1409 3,
1410 self.tol.confusion() * 100.0,
1411 self.tol,
1412 )?;
1413 if !fitted.met {
1414 self.report.warnings.push(format!(
1415 "D{de}: a varying offset curve fitted to {:.2e}",
1416 fitted.error
1417 ));
1418 }
1419 Ok((Curve::from(fitted.curve), range))
1420 }
1421
1422 fn spline_curve(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
1423 let n = entity.count(3);
1424 if n == 0 {
1425 ogeom_bail!(Construction, "D{de}: a spline curve with no segments");
1426 }
1427 let scale = self.report.scale_mm;
1428 let t = |i: usize| entity.at(4 + i).real();
1431 let base = 4 + n + 1;
1432 let mut control: Vec<Point> = Vec::with_capacity(3 * n + 1);
1433 let mut knots: Vec<f64> = vec![t(0); 4];
1434 for seg in 0..n {
1435 let h = t(seg + 1) - t(seg);
1436 if h <= 0.0 {
1437 ogeom_bail!(Construction, "D{de}: spline segment {seg} has no span");
1438 }
1439 let co = |k: usize| entity.at(base + 12 * seg + k).real();
1440 let (ax, bx, cx, dx) = (co(0), co(1), co(2), co(3));
1441 let (ay, by, cy, dy) = (co(4), co(5), co(6), co(7));
1442 let (az, bz, cz, dz) = (co(8), co(9), co(10), co(11));
1443 let at = |s: f64| {
1444 Point::new(
1445 (ax + s * (bx + s * (cx + s * dx))) * scale,
1446 (ay + s * (by + s * (cy + s * dy))) * scale,
1447 (az + s * (bz + s * (cz + s * dz))) * scale,
1448 )
1449 };
1450 let p0 = at(0.0);
1453 let p3 = at(h);
1454 let d0 = Vector::new(bx, by, bz) * (h * scale / 3.0);
1455 let d1 = Vector::new(
1456 bx + 2.0 * cx * h + 3.0 * dx * h * h,
1457 by + 2.0 * cy * h + 3.0 * dy * h * h,
1458 bz + 2.0 * cz * h + 3.0 * dz * h * h,
1459 ) * (h * scale / 3.0);
1460 if seg == 0 {
1461 control.push(p0);
1462 }
1463 control.push(p0 + d0);
1464 control.push(p3 - d1);
1465 control.push(p3);
1466 if seg + 1 < n {
1467 knots.extend([t(seg + 1); 3]);
1468 }
1469 }
1470 knots.extend([t(n); 4]);
1471 let curve = BSplineCurve::new(KnotVector::new(knots, 3)?, control, self.tol)?;
1472 let range = ogeom_geom::Curve3d::domain(&curve);
1473 Ok((Curve::from(curve), range))
1474 }
1475
1476 fn nurbs_curve(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
1478 let k = entity.count(0);
1479 let degree = entity.count(1);
1480 if degree == 0 {
1481 ogeom_bail!(Construction, "D{de}: a B-spline curve of degree zero");
1482 }
1483 let n_ctrl = k + 1;
1484 let n_knots = n_ctrl + degree + 1;
1485 let knots: Vec<f64> = (0..n_knots).map(|i| entity.at(6 + i).real()).collect();
1486 let w_base = 6 + n_knots;
1487 let p_base = w_base + n_ctrl;
1488 let scale = self.report.scale_mm;
1489 let mut control = Vec::with_capacity(n_ctrl);
1490 for i in 0..n_ctrl {
1491 let w = entity.at(w_base + i).real();
1492 let p = Point::new(
1493 entity.at(p_base + 3 * i).real() * scale,
1494 entity.at(p_base + 3 * i + 1).real() * scale,
1495 entity.at(p_base + 3 * i + 2).real() * scale,
1496 );
1497 control.push(Weighted::new(p, w, self.tol)?);
1498 }
1499 let curve = BSplineCurve::rational(KnotVector::new(knots, degree)?, control)?;
1500 let v0 = entity.at(p_base + 3 * n_ctrl).real();
1501 let v1 = entity.at(p_base + 3 * n_ctrl + 1).real();
1502 let domain = ogeom_geom::Curve3d::domain(&curve);
1503 let range = if v1 > v0 { (v0, v1) } else { domain };
1504 Ok((Curve::from(curve), range))
1505 }
1506
1507 fn surface(&mut self, de: i64) -> OgeomResult<SurfaceGeometry> {
1509 self.nested(|reader| reader.surface_at(de))
1510 }
1511
1512 fn surface_at(&mut self, de: i64) -> OgeomResult<SurfaceGeometry> {
1513 let entity = self.entity(de)?;
1514 let scale = self.report.scale_mm;
1515 let surface: SurfaceGeometry = match entity.kind {
1516 108 => {
1517 let normal = Vector::new(
1519 entity.at(0).real(),
1520 entity.at(1).real(),
1521 entity.at(2).real(),
1522 );
1523 let d = entity.at(3).real();
1524 let origin = Point::ORIGIN + normal * (d / normal.dot(normal)) * scale;
1525 let plane = Plane::through(origin, Direction::new(normal, self.tol)?);
1526 PlaneSurface::over(
1527 plane,
1528 (-SURFACE_EXTENT, SURFACE_EXTENT),
1529 (-SURFACE_EXTENT, SURFACE_EXTENT),
1530 )?
1531 .into()
1532 }
1533 190 => {
1534 let point = self.location_entity(entity.at(0).int())?;
1535 let normal = self.direction_entity(entity.at(1).int())?;
1536 let plane = match self.reference_direction(entity, 2)? {
1538 Some(x) => Plane::new(Frame::new(point, normal, x, self.tol)?),
1539 None => Plane::through(point, normal),
1540 };
1541 PlaneSurface::over(
1542 plane,
1543 (-SURFACE_EXTENT, SURFACE_EXTENT),
1544 (-SURFACE_EXTENT, SURFACE_EXTENT),
1545 )?
1546 .into()
1547 }
1548 120 => {
1549 let axis = {
1551 let (line, _) = self.child_curve(entity, entity.at(0).int())?;
1552 let Curve::Line(l) = line else {
1553 ogeom_bail!(
1554 Construction,
1555 "D{de}: a surface of revolution's axis is not a line"
1556 );
1557 };
1558 l.axis()
1559 };
1560 let (curve, range) = self.child_curve(entity, entity.at(1).int())?;
1561 let mut curve = trimmed_to(curve, range, self.tol)?;
1562 let sa = entity.at(2).real();
1563 let ta = entity.at(3).real();
1564 let sweep = if ta > sa {
1565 ta - sa
1566 } else {
1567 core::f64::consts::TAU
1568 };
1569 if sa.abs() > self.tol.parametric() {
1570 curve = curve.transformed(&Transform::rotation(axis, sa), self.tol)?;
1571 }
1572 RevolutionSurface::new(curve, axis, sweep)?.into()
1573 }
1574 122 => {
1575 let (curve, range) = self.child_curve(entity, entity.at(0).int())?;
1578 let start = curve.point_at(range.0, self.tol)?;
1579 let far = self.point3(entity, 1);
1580 let vec = far - start;
1581 let curve = trimmed_to(curve, range, self.tol)?;
1582 ExtrusionSurface::new(curve, Direction::new(vec, self.tol)?, vec.magnitude())?
1583 .into()
1584 }
1585 128 => self.nurbs_surface(de, entity)?,
1586 114 => self.spline_surface(de, entity)?,
1587 118 => self.ruled_surface(de, entity)?,
1588 140 => {
1589 let given = Direction::from_coords(
1593 entity.at(0).real(),
1594 entity.at(1).real(),
1595 entity.at(2).real(),
1596 self.tol,
1597 )?;
1598 let distance = entity.at(3).real() * scale;
1599 let basis = self.child_surface(entity, entity.at(4).int())?;
1600 let ((ua, ub), (va, vb)) = ogeom_geom::Surface::domain(&basis);
1601 let own = ogeom_geom::Surface::normal_at(
1602 &basis,
1603 f64::midpoint(ua, ub),
1604 f64::midpoint(va, vb),
1605 self.tol,
1606 )?;
1607 let signed = if own.vector().dot(given.vector()) < 0.0 {
1608 -distance
1609 } else {
1610 distance
1611 };
1612 SurfaceGeometry::Offset(Box::new(OffsetSurface::new(basis, signed)?))
1613 }
1614 192 => {
1615 let point = self.location_entity(entity.at(0).int())?;
1616 let dir = self.direction_entity(entity.at(1).int())?;
1617 let radius = entity.at(2).real() * scale;
1618 let frame = self.framed(point, dir, entity, 3)?;
1619 CylinderSurface::new(
1620 Cylinder::new(frame, radius, self.tol)?,
1621 (-SURFACE_EXTENT, SURFACE_EXTENT),
1622 )?
1623 .into()
1624 }
1625 194 => {
1626 let point = self.location_entity(entity.at(0).int())?;
1627 let dir = self.direction_entity(entity.at(1).int())?;
1628 let radius = entity.at(2).real() * scale;
1629 let half_angle = entity.at(3).real().to_radians();
1630 let frame = self.framed(point, dir, entity, 4)?;
1631 ConeSurface::new(
1632 Cone::new(frame, radius, half_angle, self.tol)?,
1633 (-SURFACE_EXTENT, SURFACE_EXTENT),
1634 )?
1635 .into()
1636 }
1637 196 => {
1638 let centre = self.location_entity(entity.at(0).int())?;
1639 let radius = entity.at(1).real() * scale;
1640 if entity.at(2).int() != 0 {
1642 let axis = self.direction_entity(entity.at(2).int())?;
1643 let frame = self.framed(centre, axis, entity, 3)?;
1644 SphereSurface::new(Sphere::new(frame, radius, self.tol)?).into()
1645 } else {
1646 SphereSurface::new(Sphere::centred(centre, radius, self.tol)?).into()
1647 }
1648 }
1649 198 => {
1650 let centre = self.location_entity(entity.at(0).int())?;
1651 let dir = self.direction_entity(entity.at(1).int())?;
1652 let major = entity.at(2).real() * scale;
1653 let minor = entity.at(3).real() * scale;
1654 let frame = self.framed(centre, dir, entity, 4)?;
1655 TorusSurface::new(Torus::new(frame, major, minor, self.tol)?).into()
1656 }
1657 kind => ogeom_bail!(
1658 Construction,
1659 "D{de}: surface entity type {kind}{} is not translated; see \
1660 docs/CAPABILITIES.md, io.iges",
1661 super::entity_name(kind).map_or_else(String::new, |n| format!(" ({n})"))
1662 ),
1663 };
1664 let placement = self.placement(entity)?;
1665 if placement == Transform::IDENTITY {
1666 Ok(surface)
1667 } else {
1668 Ok(surface.transformed(&placement, self.tol)?)
1669 }
1670 }
1671
1672 fn ruled_surface(&mut self, de: i64, entity: &Entity) -> OgeomResult<SurfaceGeometry> {
1681 let (first, first_range) = self.child_curve(entity, entity.at(0).int())?;
1682 let (second, second_range) = self.child_curve(entity, entity.at(1).int())?;
1683 let mut a = first.to_bspline_over(first_range, self.tol)?;
1684 let mut b = second.to_bspline_over(second_range, self.tol)?;
1685 if entity.at(2).int() == 1 {
1686 let Curve::BSpline(turned) =
1687 ogeom_geom::Reversible::reversed(&Curve::BSpline(b.clone()))
1688 else {
1689 ogeom_bail!(Construction, "D{de}: a reversed spline is not a spline");
1690 };
1691 b = turned;
1692 }
1693 while a.degree() < b.degree() {
1694 a = a.elevated(self.tol)?;
1695 }
1696 while b.degree() < a.degree() {
1697 b = b.elevated(self.tol)?;
1698 }
1699 let tol = self.tol;
1700 let unify = |target: &mut BSplineCurve, source: &BSplineCurve| -> OgeomResult<()> {
1701 let (lo, hi) = source.knots().domain();
1702 for (value, multiplicity) in source.knots().distinct() {
1703 if value <= lo + 1e-12 || value >= hi - 1e-12 {
1704 continue;
1705 }
1706 let held = target.knots().multiplicity_of(value);
1707 if multiplicity > held {
1708 *target = target.with_knot_inserted(value, multiplicity - held, tol)?;
1709 }
1710 }
1711 Ok(())
1712 };
1713 unify(&mut a, &b)?;
1714 unify(&mut b, &a)?;
1715 let nu = a.control_points().len();
1716 if b.control_points().len() != nu {
1717 ogeom_bail!(
1718 Construction,
1719 "D{de}: the ruled surface's curves refine to different nets"
1720 );
1721 }
1722 let mut points: Vec<Weighted<Point>> = Vec::with_capacity(nu * 2);
1723 for i in 0..nu {
1724 points.push(a.control_points()[i]);
1725 points.push(b.control_points()[i]);
1726 }
1727 let grid = ControlGrid::new(points, nu, 2)?;
1728 let across = KnotVector::new(vec![0.0, 0.0, 1.0, 1.0], 1)?;
1729 Ok(BSplineSurface::rational(a.knots().clone(), across, grid)?.into())
1730 }
1731
1732 fn spline_surface(&mut self, de: i64, entity: &Entity) -> OgeomResult<SurfaceGeometry> {
1743 let m = entity.count(2);
1744 let n = entity.count(3);
1745 if m == 0 || n == 0 {
1746 ogeom_bail!(Construction, "D{de}: a spline surface with no patches");
1747 }
1748 let scale = self.report.scale_mm;
1749 let tu = |i: usize| entity.at(4 + i).real();
1750 let tv = |j: usize| entity.at(4 + m + 1 + j).real();
1751 let base = 4 + (m + 1) + (n + 1);
1752 let (rows, columns) = (3 * m + 1, 3 * n + 1);
1753 let mut net = vec![Point::ORIGIN; rows * columns];
1754 const TO_BERNSTEIN: [[f64; 4]; 4] = [
1757 [1.0, 0.0, 0.0, 0.0],
1758 [1.0, 1.0 / 3.0, 0.0, 0.0],
1759 [1.0, 2.0 / 3.0, 1.0 / 3.0, 0.0],
1760 [1.0, 1.0, 1.0, 1.0],
1761 ];
1762 for i in 0..m {
1763 let h = tu(i + 1) - tu(i);
1764 for j in 0..n {
1765 let k = tv(j + 1) - tv(j);
1766 if h <= 0.0 || k <= 0.0 {
1767 ogeom_bail!(
1768 Construction,
1769 "D{de}: spline surface patch ({i}, {j}) has no span"
1770 );
1771 }
1772 let at = base + (i * (n + 1) + j) * 48;
1773 let mut coords = [[[0.0_f64; 4]; 4]; 3];
1774 for (axis, block) in coords.iter_mut().enumerate() {
1775 let mut b = [[0.0_f64; 4]; 4];
1777 for (mu, row) in b.iter_mut().enumerate() {
1778 for (lv, value) in row.iter_mut().enumerate() {
1779 let raw = entity.at(at + 16 * axis + mu + 4 * lv).real();
1780 #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
1781 let scaled = raw * h.powi(mu as i32) * k.powi(lv as i32);
1782 *value = scaled;
1783 }
1784 }
1785 let mut q = [[0.0_f64; 4]; 4];
1787 for r in 0..4 {
1788 for lv in 0..4 {
1789 q[r][lv] = (0..4).map(|mu| TO_BERNSTEIN[r][mu] * b[mu][lv]).sum();
1790 }
1791 }
1792 for r in 0..4 {
1793 for c in 0..4 {
1794 block[r][c] = (0..4).map(|lv| TO_BERNSTEIN[c][lv] * q[r][lv]).sum();
1795 }
1796 }
1797 }
1798 for r in 0..4 {
1799 for c in 0..4 {
1800 net[(3 * i + r) * columns + 3 * j + c] = Point::new(
1801 coords[0][r][c] * scale,
1802 coords[1][r][c] * scale,
1803 coords[2][r][c] * scale,
1804 );
1805 }
1806 }
1807 }
1808 }
1809 let knots = |count: usize, at: &dyn Fn(usize) -> f64| -> Vec<f64> {
1810 let mut out = vec![at(0); 4];
1811 for i in 1..count {
1812 out.extend([at(i); 3]);
1813 }
1814 out.extend([at(count); 4]);
1815 out
1816 };
1817 let grid = ControlGrid::new(
1818 net.into_iter()
1819 .map(|p| Weighted::new(p, 1.0, self.tol))
1820 .collect::<OgeomResult<Vec<_>>>()?,
1821 rows,
1822 columns,
1823 )?;
1824 Ok(ogeom_geom::BSplineSurface::rational(
1825 KnotVector::new(knots(m, &tu), 3)?,
1826 KnotVector::new(knots(n, &tv), 3)?,
1827 grid,
1828 )?
1829 .into())
1830 }
1831
1832 fn nurbs_surface(&mut self, de: i64, entity: &Entity) -> OgeomResult<SurfaceGeometry> {
1833 let k1 = entity.count(0);
1834 let k2 = entity.count(1);
1835 let m1 = entity.count(2);
1836 let m2 = entity.count(3);
1837 if m1 == 0 || m2 == 0 {
1838 ogeom_bail!(Construction, "D{de}: a B-spline surface of degree zero");
1839 }
1840 let (nu, nv) = (k1 + 1, k2 + 1);
1841 if nu
1842 .checked_mul(nv)
1843 .is_none_or(|cells| cells > entity.params.len())
1844 {
1845 ogeom_bail!(
1846 Construction,
1847 "D{de}: a {nu} by {nv} control grid in {} parameters",
1848 entity.params.len()
1849 );
1850 }
1851 let (nku, nkv) = (nu + m1 + 1, nv + m2 + 1);
1852 let base = 9;
1853 let u_knots: Vec<f64> = (0..nku).map(|i| entity.at(base + i).real()).collect();
1854 let v_knots: Vec<f64> = (0..nkv).map(|i| entity.at(base + nku + i).real()).collect();
1855 let w_base = base + nku + nkv;
1856 let p_base = w_base + nu * nv;
1857 let scale = self.report.scale_mm;
1858 let raw = |u: usize, v: usize| -> OgeomResult<Weighted<Point>> {
1862 let i = v * nu + u;
1863 let w = entity.at(w_base + i).real();
1864 let p = Point::new(
1865 entity.at(p_base + 3 * i).real() * scale,
1866 entity.at(p_base + 3 * i + 1).real() * scale,
1867 entity.at(p_base + 3 * i + 2).real() * scale,
1868 );
1869 Weighted::new(p, w, self.tol)
1870 };
1871 let mut weighted = Vec::with_capacity(nu * nv);
1872 for u in 0..nu {
1873 for v in 0..nv {
1874 weighted.push(raw(u, v)?);
1875 }
1876 }
1877 let grid = ControlGrid::new(weighted, nu, nv)?;
1878 Ok(ogeom_geom::BSplineSurface::rational(
1879 KnotVector::new(u_knots, m1)?,
1880 KnotVector::new(v_knots, m2)?,
1881 grid,
1882 )?
1883 .into())
1884 }
1885
1886 fn location_entity(&mut self, de: i64) -> OgeomResult<Point> {
1888 let e = self.entity(de)?;
1889 if e.kind != 116 {
1890 ogeom_bail!(
1891 Construction,
1892 "D{de}: expected a point entity, found type {}",
1893 e.kind
1894 );
1895 }
1896 Ok(self.point3(e, 0))
1897 }
1898
1899 fn reference_direction(
1903 &mut self,
1904 entity: &Entity,
1905 field: usize,
1906 ) -> OgeomResult<Option<Direction>> {
1907 let de = entity.at(field).int();
1908 if de == 0 || !self.file.entities.contains_key(&de) {
1909 return Ok(None);
1910 }
1911 Ok(Some(self.direction_entity(de)?))
1912 }
1913
1914 fn framed(
1917 &mut self,
1918 origin: Point,
1919 axis: Direction,
1920 entity: &Entity,
1921 field: usize,
1922 ) -> OgeomResult<Frame> {
1923 if let Some(reference) = self.reference_direction(entity, field)? {
1924 let r = reference.vector();
1925 let square = r - axis.vector() * r.dot(axis.vector());
1926 if let Ok(x) = Direction::new(square, self.tol) {
1927 return Frame::new(origin, axis, x, self.tol);
1928 }
1929 }
1930 frame_about(origin, axis, self.tol)
1931 }
1932
1933 fn direction_entity(&mut self, de: i64) -> OgeomResult<Direction> {
1934 let e = self.entity(de)?;
1935 if e.kind != 123 {
1936 ogeom_bail!(
1937 Construction,
1938 "D{de}: expected a direction entity, found type {}",
1939 e.kind
1940 );
1941 }
1942 Direction::from_coords(e.at(0).real(), e.at(1).real(), e.at(2).real(), self.tol)
1943 }
1944
1945 fn boundary_segments(&mut self, de: i64) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
1949 let entity = self.entity(de)?;
1950 match entity.kind {
1951 142 => {
1959 let (surface_de, b, c) =
1960 (entity.at(1).int(), entity.at(2).int(), entity.at(3).int());
1961 if c == 0 {
1962 return self.lifted_segments(de, surface_de, b);
1963 }
1964 for representation in [b, c] {
1966 if representation != 0 {
1967 self.visited.insert(representation.abs(), ());
1968 }
1969 }
1970 if entity.at(4).int() != 1 || b == 0 {
1971 return self.curve_segments(c);
1972 }
1973 let before = self.report.warnings.len();
1974 let why = match self.lifted_segments(de, surface_de, b) {
1975 Ok(lifted) if self.report.warnings.len() == before => return Ok(lifted),
1976 Ok(_) => "lifted short of its target".to_owned(),
1977 Err(e) => format!("did not lift: {e}"),
1978 };
1979 self.report.warnings.truncate(before);
1980 self.report.warnings.push(format!(
1981 "D{de}: the preferred parameter-space trim {why}; the model-space \
1982 curve stands in"
1983 ));
1984 self.curve_segments(c)
1985 }
1986 141 => {
1987 let n = entity.count(3);
1988 let mut out = Vec::new();
1989 let mut i = 4;
1990 for _ in 0..n {
1991 let cptr = entity.at(i).int();
1992 let k = entity.count(i + 2);
1993 i += 3 + k;
1994 out.extend(self.curve_segments(cptr)?);
1997 }
1998 Ok(out)
1999 }
2000 _ => self.curve_segments(de),
2001 }
2002 }
2003
2004 fn lifted_segments(
2011 &mut self,
2012 de: i64,
2013 surface_de: i64,
2014 b: i64,
2015 ) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
2016 let kind = self.entity(surface_de)?.kind;
2017 let surface = self.surface(surface_de)?;
2018 let scale = self.report.scale_mm;
2019 let half_angle = match &surface {
2027 SurfaceGeometry::Cone(c) => c.cone().half_angle(),
2028 _ => 0.0,
2029 };
2030 let swept = match (kind, &surface) {
2036 (122, SurfaceGeometry::Extrusion(e)) => {
2037 let (lo, hi) = ogeom_geom::Curve3d::domain(e.curve());
2038 let extent = ogeom_geom::Surface::domain(&surface).1;
2039 Some(((lo, hi - lo), (extent.0, extent.1 - extent.0), false))
2040 }
2041 (120, SurfaceGeometry::Revolution(r)) => {
2042 let generatrix = self.entity(surface_de)?.at(1).int();
2043 let (lo, hi) = ogeom_geom::Curve3d::domain(r.curve());
2044 let generatrix = self.entity(generatrix)?;
2045 match generatrix.kind {
2046 110 => Some(((lo, hi - lo), (0.0, 1.0), true)),
2047 126 => Some(((0.0, 1.0), (0.0, 1.0), true)),
2048 100 => {
2053 let start = (generatrix.at(4).real() - generatrix.at(2).real())
2054 .atan2(generatrix.at(3).real() - generatrix.at(1).real())
2055 .rem_euclid(core::f64::consts::TAU);
2056 Some(((lo - start, 1.0), (0.0, 1.0), true))
2057 }
2058 104 => ellipse_start_angle(generatrix)
2066 .map(|start| ((lo - start, 1.0), (0.0, 1.0), true)),
2067 _ => None,
2068 }
2069 }
2070 _ => None,
2071 };
2072 let start_angle = if kind == 120 {
2073 self.entity(surface_de)?.at(2).real()
2074 } else {
2075 0.0
2076 };
2077 let map = |p: ogeom_math::Point2| -> Option<ogeom_math::Point2> {
2078 use ogeom_math::Point2 as P;
2079 let (u, v) = (p.x / scale, p.y / scale);
2082 match kind {
2083 128 => Some(P::new(u, v)),
2084 190 => Some(P::new(u * scale, v * scale)),
2085 192 => Some(P::new(u.to_radians(), v * scale)),
2086 194 => Some(P::new(u.to_radians(), v * scale * half_angle.cos())),
2087 196 => Some(P::new(u.to_radians(), v.to_radians())),
2088 198 => Some(P::new((360.0 - v).to_radians(), u.to_radians())),
2089 122 => swept.map(|((u0, du), (v0, dv), _)| P::new(u0 + du * u, v0 + dv * v)),
2090 120 => swept.map(|((u0, du), _, _)| P::new(v - start_angle, u0 + du * u)),
2093 _ => None,
2094 }
2095 };
2096 if map(ogeom_math::Point2::new(0.0, 0.0)).is_none() {
2097 ogeom_bail!(
2098 Construction,
2099 "D{de}: a curve-on-surface carries no model-space curve, and \
2100 its surface's parameters are the file's convention for a \
2101 kind this reader does not translate; see docs/CAPABILITIES.md, io.iges"
2102 );
2103 }
2104 let mut out = Vec::new();
2105 for (curve, range) in self.curve_segments(b)? {
2106 let chart = match &curve {
2113 Curve::BSpline(spline) => {
2114 let mut control = Vec::with_capacity(spline.control_points().len());
2115 for c in spline.control_points() {
2116 let p = c.point();
2117 let Some(q) = map(ogeom_math::Point2::new(p.x, p.y)) else {
2118 ogeom_bail!(
2119 Construction,
2120 "D{de}: a trim's parameters did not translate"
2121 );
2122 };
2123 control.push(Weighted::new(q, c.weight, self.tol)?);
2124 }
2125 ogeom_geom::BSpline2d::rational(spline.knots().clone(), control)?
2126 }
2127 _ => {
2128 const SAMPLES: usize = 200;
2129 let target = 1e-10;
2130 let stations = ogeom_algo::traced::stations(&curve, range, SAMPLES);
2131 let fitted = ogeom_algo::traced::fit_traced_2d(
2132 |t| {
2133 let p = curve.point_at(t, self.tol)?;
2134 map(ogeom_math::Point2::new(p.x, p.y)).ok_or_else(|| {
2135 ogeom_core::ogeom_err!(
2136 Construction,
2137 "D{de}: a trim's parameters did not translate"
2138 )
2139 })
2140 },
2141 &stations,
2142 3,
2143 target,
2144 self.tol,
2145 )?;
2146 if !fitted.met {
2147 self.report.warnings.push(format!(
2148 "D{de}: a parameter-space trim fitted to within {:.2e} of \
2149 the file's, in the surface's parameters",
2150 fitted.error
2151 ));
2152 }
2153 fitted.curve
2154 }
2155 };
2156 let corners = ogeom_algo::traced::corners_2d(&chart, range);
2160 let lifted = Curve::OnSurface(Box::new(ogeom_geom::CurveOnSurface::new(
2161 chart.into(),
2162 surface.clone(),
2163 )));
2164 let target = self.tol.confusion() * 100.0;
2165 let mut cuts = vec![range.0.min(range.1)];
2166 cuts.extend(corners);
2167 cuts.push(range.0.max(range.1));
2168 let mut pieces = Vec::with_capacity(cuts.len() - 1);
2169 let mut error = 0.0_f64;
2170 for pair in cuts.windows(2) {
2171 let piece = lifted.fitted_bspline_over((pair[0], pair[1]), target, self.tol)?;
2172 error = error.max(piece.error);
2173 pieces.push(piece.curve);
2174 }
2175 let fitted = if pieces.len() == 1 {
2176 ogeom_geom::fit::Fitted {
2177 curve: pieces.remove(0),
2178 error,
2179 met: error <= target,
2180 }
2181 } else {
2182 let (curve, moved) = ogeom_algo::traced::joined(&pieces, self.tol)?;
2183 let error = error + moved;
2184 ogeom_geom::fit::Fitted {
2185 curve,
2186 error,
2187 met: error <= target,
2188 }
2189 };
2190 if !fitted.met {
2191 self.report.warnings.push(format!(
2192 "D{de}: a parameter-space trim lifted to within {:.2e}",
2193 fitted.error
2194 ));
2195 }
2196 let domain = ogeom_geom::Curve3d::domain(&fitted.curve);
2197 out.push((Curve::from(fitted.curve), domain));
2198 }
2199 Ok(out)
2200 }
2201
2202 fn curve_segments(&mut self, de: i64) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
2203 self.nested(|reader| reader.curve_segments_at(de))
2204 }
2205
2206 fn curve_segments_at(&mut self, de: i64) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
2207 let entity = self.entity(de)?;
2208 if entity.kind == 102 {
2209 let placement = self.placement(entity)?;
2214 let n = entity.count(0);
2215 let mut out = Vec::new();
2216 for i in 0..n {
2217 let member = entity.at(1 + i).int();
2218 let child = self.entity(member)?;
2219 if matches!(child.kind, 116 | 132) {
2220 continue;
2221 }
2222 let moves = carried(child);
2223 let segments = self.curve_segments(member)?;
2224 if moves && placement != Transform::IDENTITY {
2225 for (curve, range) in segments {
2226 out.push((curve.transformed(&placement, self.tol)?, range));
2227 }
2228 } else {
2229 out.extend(segments);
2230 }
2231 }
2232 return Ok(out);
2233 }
2234 let path = entity.kind == 106;
2235 let (curve, range) = self.curve(de)?;
2236 if path && let Curve::BSpline(polyline) = &curve {
2239 let mut out = Vec::new();
2240 for pair in polyline.control_points().windows(2) {
2241 let piece = BSplineCurve::new(
2242 KnotVector::new(vec![0.0, 0.0, 1.0, 1.0], 1)?,
2243 vec![pair[0].point(), pair[1].point()],
2244 self.tol,
2245 )?;
2246 out.push((Curve::from(piece), (0.0, 1.0)));
2247 }
2248 return Ok(out);
2249 }
2250 Ok(vec![(curve, range)])
2251 }
2252
2253 fn wireframe(&mut self, de: i64) -> OgeomResult<Shape> {
2258 let segments = self.curve_segments(de)?;
2259 if segments.is_empty() {
2260 ogeom_bail!(Construction, "D{de}: a composite curve with no curves");
2261 }
2262 let ends: Vec<(Point, Point)> = segments
2263 .iter()
2264 .map(|(c, r)| Ok((c.point_at(r.0, self.tol)?, c.point_at(r.1, self.tol)?)))
2265 .collect::<OgeomResult<_>>()?;
2266 let weld = self.tol.confusion() * 100.0;
2267 let walk = |start: Point| -> Result<Vec<bool>, usize> {
2270 let mut at = start;
2271 let mut senses = Vec::with_capacity(ends.len());
2272 for (i, &(s, e)) in ends.iter().enumerate() {
2273 let forward = at.distance(s) <= at.distance(e);
2274 if at.distance(if forward { s } else { e }) > weld {
2275 return Err(i);
2276 }
2277 senses.push(forward);
2278 at = if forward { e } else { s };
2279 }
2280 Ok(senses)
2281 };
2282 let (start, senses) = match (walk(ends[0].0), walk(ends[0].1)) {
2283 (Ok(senses), _) => (ends[0].0, senses),
2284 (_, Ok(senses)) => (ends[0].1, senses),
2285 (Err(a), Err(b)) => ogeom_bail!(
2286 Construction,
2287 "D{de}: curve segment {} starts away from where the previous one ended",
2288 a.max(b)
2289 ),
2290 };
2291 let finish = match senses.last() {
2292 Some(true) => ends[ends.len() - 1].1,
2293 _ => ends[ends.len() - 1].0,
2294 };
2295 let closed = finish.distance(start) <= weld;
2296 let head = make_vertex(&mut self.model, start).shape;
2297 let mut at_vertex = head.clone();
2298 let mut edges = Vec::with_capacity(segments.len());
2299 let n = segments.len();
2300 for (i, (((curve, range), (s, e)), forward)) in
2301 segments.into_iter().zip(ends).zip(senses).enumerate()
2302 {
2303 let next_vertex = if i + 1 == n && closed {
2304 head.clone()
2305 } else {
2306 make_vertex(&mut self.model, if forward { e } else { s }).shape
2307 };
2308 let edge = if forward {
2309 make_edge_between(
2310 &mut self.model,
2311 curve,
2312 range,
2313 &at_vertex,
2314 &next_vertex,
2315 self.tol,
2316 )?
2317 .shape
2318 } else {
2319 make_edge_between(
2320 &mut self.model,
2321 curve,
2322 range,
2323 &next_vertex,
2324 &at_vertex,
2325 self.tol,
2326 )?
2327 .shape
2328 .reversed()
2329 };
2330 edges.push(edge);
2331 at_vertex = next_vertex;
2332 }
2333 Ok(ogeom_algo::make_wire(&mut self.model, &edges, self.tol)?.shape)
2334 }
2335
2336 fn point(&mut self, de: i64) -> OgeomResult<Shape> {
2339 let entity = self.entity(de)?;
2340 let at = self.placement(entity)?.apply(self.point3(entity, 0));
2341 Ok(make_vertex(&mut self.model, at).shape)
2342 }
2343
2344 fn face(&mut self, de: i64) -> OgeomResult<Shape> {
2346 let entity = self.entity(de)?;
2347 let (surface_de, boundaries): (i64, Vec<i64>) = match entity.kind {
2348 144 => {
2349 let s = entity.at(0).int();
2350 let outer_given = entity.at(1).int() == 1;
2351 let n_inner = entity.count(2);
2352 let mut bs = Vec::new();
2353 if outer_given && entity.at(3).int() != 0 {
2354 bs.push(entity.at(3).int());
2355 }
2356 for i in 0..n_inner {
2357 bs.push(entity.at(4 + i).int());
2358 }
2359 (s, bs)
2360 }
2361 143 => {
2362 let s = entity.at(1).int();
2363 let n = entity.count(2);
2364 (s, (0..n).map(|i| entity.at(3 + i).int()).collect())
2365 }
2366 kind => ogeom_bail!(Construction, "D{de}: type {kind} is not a trimmed surface"),
2367 };
2368 let surface = self.trimmed_basis(entity, surface_de)?;
2369 if boundaries.is_empty() {
2370 return Ok(ogeom_algo::make_natural_face(&mut self.model, surface)?.shape);
2373 }
2374 let mut wires = Vec::new();
2375 for boundary in boundaries {
2376 let segments = self.boundary_segments(boundary)?;
2377 let motion = self.parent_motion(entity, boundary)?;
2378 let segments = moved_segments(segments, &motion, self.tol)?;
2379 let segments = self.cut_at_trim_poles(segments);
2380 let edges = self.wire_edges(de, segments)?;
2381 self.cover_trim_poles(&edges)?;
2382 wires.push(edges);
2383 }
2384 self.assemble_face(surface, wires)
2385 }
2386
2387 fn bounded_plane(&mut self, de: i64) -> OgeomResult<Shape> {
2391 let entity = self.entity(de)?;
2392 let surface = self.surface(de)?;
2393 let boundary = entity.at(4).int();
2394 let segments = self.curve_segments(boundary)?;
2395 let motion = self.parent_motion(entity, boundary)?;
2396 let segments = moved_segments(segments, &motion, self.tol)?;
2397 let edges = self.wire_edges(de, segments)?;
2398 self.assemble_face(surface, vec![edges])
2399 }
2400
2401 fn trimmed_basis(&mut self, trimmed: &Entity, de: i64) -> OgeomResult<SurfaceGeometry> {
2404 let surface = self.surface(de)?;
2405 let motion = self.parent_motion(trimmed, de)?;
2406 if motion == Transform::IDENTITY {
2407 Ok(surface)
2408 } else {
2409 Ok(surface.transformed(&motion, self.tol)?)
2410 }
2411 }
2412
2413 fn untrimmed_face(&mut self, de: i64) -> OgeomResult<Shape> {
2417 let mut entity = self.entity(de)?;
2418 if is_bounded_plane(entity) {
2419 return self.bounded_plane(de);
2420 }
2421 let mut depth = 0;
2422 while entity.kind == 140 {
2423 depth += 1;
2424 if depth > MOST_NESTING {
2425 ogeom_bail!(
2426 Construction,
2427 "D{de}: offset surfaces refer to one another more than {MOST_NESTING} deep"
2428 );
2429 }
2430 entity = self.entity(entity.at(4).int())?;
2431 if !UNTRIMMED_KINDS.contains(&entity.kind) {
2432 ogeom_bail!(
2433 Construction,
2434 "D{de}: an offset of a type {} surface has no bounds of its own",
2435 entity.kind
2436 );
2437 }
2438 }
2439 let surface = self.surface(de)?;
2440 Ok(ogeom_algo::make_natural_face(&mut self.model, surface)?.shape)
2441 }
2442
2443 fn poles_of_trims(&mut self, face_des: &[i64]) -> Vec<Point> {
2447 let mut out: Vec<Point> = Vec::new();
2448 for &de in face_des {
2449 let (trimmed, surface_de) = match self.entity(de) {
2450 Ok(e) if e.kind == 144 => (e, e.at(0).int()),
2451 Ok(e) if e.kind == 143 => (e, e.at(1).int()),
2452 _ => continue,
2453 };
2454 let Ok(surface) = self.trimmed_basis(trimmed, surface_de) else {
2455 continue;
2456 };
2457 for pole in crate::pcurves::chart_poles(&surface, self.tol) {
2458 if out.iter().all(|p| p.distance(pole) > self.tol.confusion()) {
2459 out.push(pole);
2460 }
2461 }
2462 }
2463 out
2464 }
2465
2466 fn cut_at_trim_poles(&self, segments: Vec<(Curve, (f64, f64))>) -> Vec<(Curve, (f64, f64))> {
2476 if self.trim_poles.is_empty() {
2477 return segments;
2478 }
2479 let mut out = Vec::with_capacity(segments.len());
2480 for (curve, range) in segments {
2481 let crossings =
2482 crate::pcurves::pole_crossings(&curve, range, &self.trim_poles, self.tol);
2483 let mut from = range.0;
2484 for (t, ..) in crossings {
2485 out.push((curve.clone(), (from, t)));
2486 from = t;
2487 }
2488 out.push((curve, (from, range.1)));
2489 }
2490 out
2491 }
2492
2493 fn cover_trim_poles(&mut self, edges: &[Shape]) -> OgeomResult<()> {
2497 let reach = self.tol.confusion() * 100.0;
2498 for edge in edges {
2499 let Some((first, last)) = ogeom_algo::edge_vertices(&self.model, edge)? else {
2500 continue;
2501 };
2502 for vertex in [first, last] {
2503 let Some(at) = self
2504 .model
2505 .node(&vertex)
2506 .and_then(|n| n.data().as_vertex())
2507 .map(|d| d.point)
2508 else {
2509 continue;
2510 };
2511 let off = self
2512 .trim_poles
2513 .iter()
2514 .map(|p| p.distance(at))
2515 .fold(f64::INFINITY, f64::min);
2516 if off > self.tol.confusion() && off <= reach {
2517 self.model.widen(
2518 &vertex,
2519 ogeom_core::Tolerance::new(off + self.tol.confusion())?,
2520 )?;
2521 }
2522 }
2523 }
2524 Ok(())
2525 }
2526
2527 fn wire_edges(
2536 &mut self,
2537 face_de: i64,
2538 segments: Vec<(Curve, (f64, f64))>,
2539 ) -> OgeomResult<Vec<Shape>> {
2540 if segments.is_empty() {
2541 ogeom_bail!(Construction, "D{face_de}: a boundary with no curves");
2542 }
2543 let ends: Vec<(Point, Point)> = segments
2544 .iter()
2545 .map(|(c, r)| Ok((c.point_at(r.0, self.tol)?, c.point_at(r.1, self.tol)?)))
2546 .collect::<OgeomResult<_>>()?;
2547 let n = segments.len();
2548 let weld = self.tol.confusion() * 100.0;
2549
2550 if n == 1 {
2552 let (curve, range) = segments
2553 .into_iter()
2554 .next()
2555 .unwrap_or_else(|| unreachable!());
2556 let (s, e) = ends[0];
2557 if s.distance(e) > weld {
2558 ogeom_bail!(
2559 Construction,
2560 "D{face_de}: a one-curve boundary whose ends sit {:.2e} apart",
2561 s.distance(e)
2562 );
2563 }
2564 let v = make_vertex(&mut self.model, s).shape;
2565 let edge = make_edge_between(&mut self.model, curve, range, &v, &v, self.tol)?.shape;
2566 return Ok(vec![edge]);
2567 }
2568
2569 let walk = |start: Point| -> Result<(Vec<bool>, f64), (usize, f64)> {
2573 let mut at = start;
2574 let mut senses = Vec::with_capacity(n);
2575 for (i, &(s, e)) in ends.iter().enumerate() {
2576 let forward = at.distance(s) <= at.distance(e);
2577 let gap = at.distance(if forward { s } else { e });
2578 if gap > weld {
2579 return Err((i, gap));
2580 }
2581 senses.push(forward);
2582 at = if forward { e } else { s };
2583 }
2584 Ok((senses, at.distance(start)))
2585 };
2586 let walks = [ends[0].0, ends[0].1].map(|start| (start, walk(start)));
2589 let chosen = walks
2590 .iter()
2591 .find(|(_, w)| matches!(w, Ok((_, gap)) if *gap <= weld))
2592 .or_else(|| walks.iter().find(|(_, w)| w.is_ok()));
2593 let (start, senses) = match chosen {
2594 Some((start, Ok((senses, _)))) => (*start, senses.clone()),
2595 _ => {
2596 let reached = |w: &Result<(Vec<bool>, f64), (usize, f64)>| match w {
2598 Err(failure) => *failure,
2599 Ok(_) => (n, 0.0),
2600 };
2601 let (a, b) = (reached(&walks[0].1), reached(&walks[1].1));
2602 let (i, gap) = if b.0 > a.0 { b } else { a };
2603 ogeom_bail!(
2604 Construction,
2605 "D{face_de}: boundary segment {i} starts {gap:.2e} from \
2606 where the previous one ended"
2607 );
2608 }
2609 };
2610
2611 let head = make_vertex(&mut self.model, start).shape;
2612 let mut at_vertex = head.clone();
2613 let mut edges = Vec::with_capacity(n);
2614 for (i, (((curve, range), (s, e)), forward)) in
2615 segments.into_iter().zip(ends).zip(senses).enumerate()
2616 {
2617 let this_end = if forward { e } else { s };
2618 let last = i + 1 == n;
2619 let next_vertex = if last {
2620 head.clone()
2621 } else {
2622 make_vertex(&mut self.model, this_end).shape
2623 };
2624 let edge = if forward {
2628 make_edge_between(
2629 &mut self.model,
2630 curve,
2631 range,
2632 &at_vertex,
2633 &next_vertex,
2634 self.tol,
2635 )?
2636 .shape
2637 } else {
2638 make_edge_between(
2639 &mut self.model,
2640 curve,
2641 range,
2642 &next_vertex,
2643 &at_vertex,
2644 self.tol,
2645 )?
2646 .shape
2647 .reversed()
2648 };
2649 edges.push(edge);
2650 at_vertex = next_vertex;
2651 }
2652 Ok(edges)
2653 }
2654
2655 fn assemble_face(
2660 &mut self,
2661 surface: SurfaceGeometry,
2662 wires: Vec<Vec<Shape>>,
2663 ) -> OgeomResult<Shape> {
2664 let surface_id = self.model.geometry_mut().add_surface(surface.clone());
2665 let mut wire_shapes = Vec::with_capacity(wires.len());
2666 for edges in &wires {
2667 self.chart_wire(edges, &surface, surface_id)?;
2668 wire_shapes.push(ogeom_algo::make_wire(&mut self.model, edges, self.tol)?.shape);
2669 }
2670 ogeom_algo::chain_wire_branches(&mut self.model, surface_id, &wire_shapes, self.tol)?;
2673 for wire in &mut wire_shapes {
2674 let edges = self.model.ordered_children_of(wire)?;
2675 let closed =
2676 ogeom_algo::closed_at_poles(&mut self.model, surface_id, &edges, self.tol)?;
2677 if closed.len() != edges.len() {
2678 *wire = ogeom_algo::make_wire(&mut self.model, &closed, self.tol)?.shape;
2679 }
2680 }
2681 Ok(ogeom_algo::make_face_on(&mut self.model, surface_id, &wire_shapes, self.tol)?.shape)
2682 }
2683
2684 fn chart_wire(
2702 &mut self,
2703 edges: &[Shape],
2704 surface: &SurfaceGeometry,
2705 surface_id: ogeom_topo::SurfaceId,
2706 ) -> OgeomResult<()> {
2707 use ogeom_geom::Curve2d as _;
2708 let mut counts: FastMap<ogeom_topo::TShapeId, usize> = FastMap::default();
2709 for edge in edges {
2710 *counts.entry(edge.node()).or_default() += 1;
2711 }
2712 let mut images: FastMap<ogeom_topo::TShapeId, (ogeom_geom::PlanarCurve, (f64, f64))> =
2714 FastMap::default();
2715 let mut sides: FastMap<
2717 ogeom_topo::TShapeId,
2718 (
2719 Option<ogeom_geom::PlanarCurve>,
2720 Option<ogeom_geom::PlanarCurve>,
2721 ),
2722 > = FastMap::default();
2723 let mut order: Vec<ogeom_topo::TShapeId> = Vec::new();
2724 let mut previous: Option<ogeom_math::Point2> = None;
2725 let mut opening: Option<(ogeom_topo::TShapeId, bool, f64)> = None;
2726 for edge in edges {
2727 let (curve, range) = {
2728 let Some(data) = self.model.node(edge).and_then(|n| n.data().as_edge()) else {
2729 continue;
2730 };
2731 let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d()
2732 else {
2733 continue;
2734 };
2735 let Some(geometry) = self.model.geometry().curve(*curve) else {
2736 continue;
2737 };
2738 (geometry.clone(), *range)
2739 };
2740 if let ogeom_core::collections::hash_map::Entry::Vacant(slot) =
2741 images.entry(edge.node())
2742 {
2743 let Some(image) = self.image_of(edge, &curve, range, surface)? else {
2744 continue;
2745 };
2746 slot.insert((image, range));
2747 order.push(edge.node());
2748 }
2749 let Some((image, range)) = images.get(&edge.node()).cloned() else {
2750 continue;
2751 };
2752 let backwards = edge.orientation() == ogeom_topo::Orientation::Reversed;
2753 let (start, end) = if backwards {
2754 (range.1, range.0)
2755 } else {
2756 (range.0, range.1)
2757 };
2758 if previous.is_none() && counts.get(&edge.node()).copied().unwrap_or(0) == 1 {
2759 opening = Some((edge.node(), backwards, start));
2760 }
2761 let mut image =
2762 crate::pcurves::shifted_to_meet(&image, start, previous, surface, self.tol)?;
2763 let walked = sides.entry(edge.node()).or_default();
2764 let first = if backwards { &walked.0 } else { &walked.1 };
2765 if counts.get(&edge.node()).copied().unwrap_or(0) > 1
2766 && let Some(first) = first
2767 {
2768 image = crate::pcurves::across_from(image, first, range, surface, self.tol)?;
2769 }
2770 previous = Some(image.point_at(end, self.tol)?);
2771 if backwards {
2772 walked.1 = Some(image);
2773 } else {
2774 walked.0 = Some(image);
2775 }
2776 }
2777 if let (Some((node, backwards, start)), Some(end)) = (opening, previous)
2780 && let Some(walked) = sides.get_mut(&node)
2781 {
2782 let side = if backwards {
2783 &mut walked.1
2784 } else {
2785 &mut walked.0
2786 };
2787 if let Some(image) = side.as_ref() {
2788 *side = Some(crate::pcurves::onto_closing_side(
2789 image, start, end, surface, self.tol,
2790 )?);
2791 }
2792 }
2793 for node in order {
2794 let Some((image, range)) = images.get(&node).cloned() else {
2795 continue;
2796 };
2797 let Some(edge) = edges.iter().find(|e| e.node() == node) else {
2798 continue;
2799 };
2800 let walked = sides.remove(&node).unwrap_or((None, None));
2801 let seam = counts.get(&node).copied().unwrap_or(0) > 1;
2802 let columns = match &walked {
2803 (Some(forward), Some(reversed)) => {
2804 let at = forward.point_at(range.0, self.tol)?;
2805 let other = reversed.point_at(range.0, self.tol)?;
2806 (at.distance(other) > self.tol.confusion()).then_some((forward, reversed))
2807 }
2808 _ => None,
2809 };
2810 match (seam, columns) {
2811 (true, Some((forward, reversed))) => {
2812 ogeom_algo::attach_seam(
2813 &mut self.model,
2814 edge,
2815 forward.clone(),
2816 reversed.clone(),
2817 surface_id,
2818 ogeom_topo::Location::identity(),
2819 range,
2820 )?;
2821 }
2822 (true, None) => {
2828 let other = crate::pcurves::seam_other_side(&image, range, surface, self.tol)?;
2829 ogeom_algo::attach_seam(
2830 &mut self.model,
2831 edge,
2832 image,
2833 other,
2834 surface_id,
2835 ogeom_topo::Location::identity(),
2836 range,
2837 )?;
2838 }
2839 (false, _) => {
2840 let (forward, reversed) = walked;
2841 let backwards = edge.orientation() == ogeom_topo::Orientation::Reversed;
2842 let placed = crate::pcurves::onto_inner_column(
2843 &forward.or(reversed).unwrap_or(image),
2844 range,
2845 backwards,
2846 surface,
2847 self.tol,
2848 )?;
2849 ogeom_algo::attach_pcurve(
2850 &mut self.model,
2851 edge,
2852 placed,
2853 surface_id,
2854 ogeom_topo::Location::identity(),
2855 range,
2856 )?;
2857 }
2858 }
2859 }
2860 Ok(())
2861 }
2862
2863 fn image_of(
2868 &mut self,
2869 edge: &Shape,
2870 curve: &Curve,
2871 range: (f64, f64),
2872 surface: &SurfaceGeometry,
2873 ) -> OgeomResult<Option<ogeom_geom::PlanarCurve>> {
2874 let prepared = self.face_de.and_then(|face| {
2875 let key = (face, range.0.to_bits(), range.1.to_bits());
2876 let slot = self.images.get_mut(&key)?;
2877 let at = slot.iter().position(|(c, _)| c == curve)?;
2878 Some(slot.swap_remove(at).1)
2879 });
2880 let (found, effects) =
2881 prepared.unwrap_or_else(|| derive_image(curve, range, surface, self.tol));
2882 for effect in effects {
2883 match effect {
2884 Effect::Warn(warning) => self.report.warnings.push(warning),
2885 Effect::Gap(off) => self.state_gap(edge, off)?,
2886 }
2887 }
2888 Ok(found)
2889 }
2890
2891 fn prepare_images(&mut self, face_des: &[i64]) {
2903 let warned = self.report.warnings.len();
2904 let visited = self.visited.clone();
2905 let mut surfaces: Vec<SurfaceGeometry> = Vec::new();
2906 let mut jobs: Vec<(i64, usize, Curve, (f64, f64))> = Vec::new();
2907 let mut seen: ogeom_core::FastSet<(i64, i64, i64)> = ogeom_core::FastSet::default();
2908 for &face_de in face_des {
2909 let Ok(entity) = self.entity(face_de) else {
2910 continue;
2911 };
2912 if entity.kind != 510 {
2913 continue;
2914 }
2915 let Ok(surface) = self.surface(entity.at(0).int()) else {
2916 continue;
2917 };
2918 surfaces.push(surface);
2919 let at = surfaces.len() - 1;
2920 for i in 0..entity.count(1) {
2921 let Ok(entries) = self.loop_entries(entity.at(3 + i).int()) else {
2922 continue;
2923 };
2924 for (list_de, index, _) in entries.into_iter().flatten() {
2925 if !seen.insert((face_de, list_de, index)) {
2926 continue;
2927 }
2928 for (curve, range) in self.edge_geometry(list_de, index) {
2929 jobs.push((face_de, at, curve, range));
2930 }
2931 }
2932 }
2933 }
2934 self.report.warnings.truncate(warned);
2935 self.visited = visited;
2936 if jobs.len() < 16 {
2938 return;
2939 }
2940 let tol = self.tol;
2941 let derived = ogeom_core::parallel::map_ordered(&jobs, |_, (_, at, curve, range)| {
2942 derive_image(curve, *range, &surfaces[*at], tol)
2943 });
2944 for ((face, _, curve, range), image) in jobs.into_iter().zip(derived) {
2945 self.images
2946 .entry((face, range.0.to_bits(), range.1.to_bits()))
2947 .or_default()
2948 .push((curve, image));
2949 }
2950 }
2951
2952 fn edge_geometry(&mut self, list_de: i64, index: i64) -> Vec<(Curve, (f64, f64))> {
2957 let key = (list_de, index);
2958 let built: Vec<Shape> = match (self.pieces.get(&key), self.edges.get(&key)) {
2959 (Some(pieces), _) => pieces.clone(),
2960 (None, Some((edge, ..))) => vec![edge.clone()],
2961 (None, None) => {
2962 return self
2963 .edge_on_file(list_de, index)
2964 .map(|(curve, range, ..)| vec![(curve, range)])
2965 .unwrap_or_default();
2966 }
2967 };
2968 built
2969 .iter()
2970 .filter_map(|edge| {
2971 let data = self.model.node(edge)?.data().as_edge()?;
2972 let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d()
2973 else {
2974 return None;
2975 };
2976 Some((self.model.geometry().curve(*curve)?.clone(), *range))
2977 })
2978 .collect()
2979 }
2980
2981 fn state_gap(&mut self, edge: &Shape, off: f64) -> OgeomResult<()> {
2987 if off > self.tol.confusion() * 1e7 {
2988 self.report.warnings.push(format!(
2989 "a projected pcurve fit stopped at {off:.2e}; \
2990 the face's mesh may sit that far off along this edge"
2991 ));
2992 } else if off > self.tol.confusion() {
2993 self.model.widen(
2994 edge,
2995 ogeom_core::Tolerance::new(off + self.tol.confusion())?,
2996 )?;
2997 }
2998 Ok(())
2999 }
3000
3001 fn manifold_solid(&mut self, de: i64) -> OgeomResult<Shape> {
3003 let entity = self.entity(de)?;
3004 let agrees = |flag: &crate::iges::parse::Value| {
3009 matches!(flag, crate::iges::parse::Value::Default) || flag.int() != 0
3010 };
3011 let used = |shell: Shape, agrees: bool| if agrees { shell } else { shell.reversed() };
3012 let shell = used(self.shell(entity.at(0).int())?, agrees(entity.at(1)));
3013 let n_voids = entity.count(2);
3014 let mut shells = vec![shell];
3015 for i in 0..n_voids {
3016 let void = self.shell(entity.at(3 + 2 * i).int())?;
3017 shells.push(used(void, agrees(entity.at(4 + 2 * i))));
3018 }
3019 let solid = make_solid(&mut self.model, &shells)?.shape;
3020 ogeom_algo::restore_containment(&mut self.model, &solid)?;
3024 Ok(solid)
3025 }
3026
3027 fn shell(&mut self, de: i64) -> OgeomResult<Shape> {
3028 self.nested(|reader| reader.shell_at(de))
3029 }
3030
3031 fn shell_at(&mut self, de: i64) -> OgeomResult<Shape> {
3032 let entity = self.entity(de)?;
3033 if entity.kind != 514 {
3034 ogeom_bail!(
3035 Construction,
3036 "D{de}: expected a shell, found type {}",
3037 entity.kind
3038 );
3039 }
3040 let n = entity.count(0);
3041 let face_des: Vec<i64> = (0..n).map(|i| entity.at(1 + 2 * i).int()).collect();
3042 self.cut_at_poles(&face_des);
3043 self.prepare_images(&face_des);
3044 let mut faces = Vec::with_capacity(n);
3045 for i in 0..n {
3046 let face_de = entity.at(1 + 2 * i).int();
3047 let same_sense = entity.at(2 + 2 * i).int() != 0;
3048 let face = self.brep_face(face_de)?;
3049 faces.push(if same_sense { face } else { face.reversed() });
3050 }
3051 self.images.clear();
3052 Ok(ogeom_algo::make_shell(&mut self.model, &faces)?.shape)
3053 }
3054
3055 fn brep_face(&mut self, de: i64) -> OgeomResult<Shape> {
3056 let entity = self.entity(de)?;
3057 if entity.kind != 510 {
3058 ogeom_bail!(
3059 Construction,
3060 "D{de}: expected a face, found type {}",
3061 entity.kind
3062 );
3063 }
3064 let surface = self.surface(entity.at(0).int())?;
3065 let n_loops = entity.count(1);
3066 let mut wires = Vec::with_capacity(n_loops);
3068 for i in 0..n_loops {
3069 wires.push(self.loop_edges(entity.at(3 + i).int())?);
3070 }
3071 let outer = self.face_de.replace(de);
3072 let face = self.assemble_face(surface, wires);
3073 self.face_de = outer;
3074 face
3075 }
3076
3077 fn loop_entries(&mut self, de: i64) -> OgeomResult<Vec<Option<(i64, i64, bool)>>> {
3080 let entity = self.entity(de)?;
3081 if entity.kind != 508 {
3082 ogeom_bail!(
3083 Construction,
3084 "D{de}: expected a loop, found type {}",
3085 entity.kind
3086 );
3087 }
3088 let n = entity.count(0);
3089 let mut entries = Vec::with_capacity(n);
3090 let mut i = 1;
3091 for _ in 0..n {
3092 let is_vertex = entity.at(i).int() == 1;
3093 let list_de = entity.at(i + 1).int();
3094 let index = entity.at(i + 2).int();
3095 let orientation = entity.at(i + 3).int();
3096 let k = entity.count(i + 4);
3097 i += 5 + 2 * k;
3098 entries.push((!is_vertex).then_some((list_de, index, orientation != 0)));
3099 }
3100 Ok(entries)
3101 }
3102
3103 fn loop_edges(&mut self, de: i64) -> OgeomResult<Vec<Shape>> {
3104 let mut edges = Vec::new();
3105 for entry in self.loop_entries(de)? {
3106 let Some((list_de, index, along)) = entry else {
3107 self.report.warnings.push(format!(
3110 "D{de}: a loop lists a vertex entry, which this reader skips"
3111 ));
3112 continue;
3113 };
3114 let pieces = match self.pieces.get(&(list_de, index)) {
3115 Some(pieces) => pieces.clone(),
3116 None => vec![self.list_edge(list_de, index)?.0],
3117 };
3118 if along {
3119 edges.extend(pieces);
3120 } else {
3121 edges.extend(pieces.into_iter().rev().map(|p| p.reversed()));
3122 }
3123 }
3124 Ok(edges)
3125 }
3126
3127 fn cut_at_poles(&mut self, face_des: &[i64]) {
3135 let mut crossings: BTreeMap<(i64, i64), Vec<crate::pcurves::Crossing>> = BTreeMap::new();
3136 for &face_de in face_des {
3137 let Ok(entity) = self.entity(face_de) else {
3138 continue;
3139 };
3140 if entity.kind != 510 {
3141 continue;
3142 }
3143 let loops: Vec<i64> = (0..entity.count(1))
3144 .map(|i| entity.at(3 + i).int())
3145 .collect();
3146 let Ok(surface) = self.surface(entity.at(0).int()) else {
3147 continue;
3148 };
3149 let poles = crate::pcurves::chart_poles(&surface, self.tol);
3150 if poles.is_empty() {
3151 continue;
3152 }
3153 for loop_de in loops {
3154 let Ok(entries) = self.loop_entries(loop_de) else {
3155 continue;
3156 };
3157 for (list_de, index, _) in entries.into_iter().flatten() {
3158 let key = (list_de, index);
3159 let Ok((_, curve, range)) = self.list_edge(list_de, index) else {
3160 continue;
3161 };
3162 let found = crate::pcurves::pole_crossings(&curve, range, &poles, self.tol);
3163 let entry = crossings.entry(key).or_default();
3164 for crossing in found {
3165 if entry.iter().all(|(t, ..)| (t - crossing.0).abs() > 1e-9) {
3166 entry.push(crossing);
3167 }
3168 }
3169 }
3170 }
3171 }
3172 for (key, mut at) in crossings {
3173 if at.is_empty() || self.pieces.contains_key(&key) {
3174 continue;
3175 }
3176 at.sort_by(|x, y| x.0.total_cmp(&y.0));
3177 let Some((edge, curve, range)) = self.edges.get(&key).cloned() else {
3178 continue;
3179 };
3180 if let Ok(cut) = crate::pcurves::split_at_poles(
3181 &mut self.model,
3182 &edge,
3183 &curve,
3184 range,
3185 &at,
3186 &mut self.pole_vertices,
3187 self.tol,
3188 ) {
3189 self.pieces
3190 .insert(key, cut.into_iter().map(|(piece, _)| piece).collect());
3191 }
3192 }
3193 }
3194
3195 fn list_edge(&mut self, list_de: i64, index: i64) -> OgeomResult<BuiltEdge> {
3197 let key = (list_de, index);
3198 if let Some(found) = self.edges.get(&key) {
3199 return Ok(found.clone());
3200 }
3201 let (curve, range, start, end) = self.edge_on_file(list_de, index)?;
3202 let vs = self.list_vertex(start.0, start.1)?;
3203 let ve = self.list_vertex(end.0, end.1)?;
3204 let cap = self.tol.confusion() * 1e7;
3211 for (vertex, t) in [(&vs, range.0), (&ve, range.1)] {
3212 let (Ok(end), Some(stated)) = (
3213 curve.point_at(t, self.tol),
3214 self.model
3215 .node(vertex)
3216 .and_then(|n| n.data().as_vertex())
3217 .map(|d| (d.point, d.tolerance)),
3218 ) else {
3219 continue;
3220 };
3221 let gap = end.distance(stated.0);
3222 if gap > stated.1.get() && gap <= cap {
3223 self.model.widen(
3224 vertex,
3225 ogeom_core::Tolerance::new(gap + self.tol.confusion())?,
3226 )?;
3227 self.vertex_misses.0 += 1;
3228 self.vertex_misses.1 = self.vertex_misses.1.max(gap);
3229 }
3230 }
3231 let edge =
3232 make_edge_between(&mut self.model, curve.clone(), range, &vs, &ve, self.tol)?.shape;
3233 let built = (edge, curve, range);
3234 self.edges.insert(key, built.clone());
3235 Ok(built)
3236 }
3237
3238 #[allow(clippy::type_complexity, reason = "one edge entry's parts")]
3242 fn edge_on_file(
3243 &mut self,
3244 list_de: i64,
3245 index: i64,
3246 ) -> OgeomResult<(Curve, (f64, f64), (i64, i64), (i64, i64))> {
3247 let entity = self.entity(list_de)?;
3248 if entity.kind != 504 {
3249 ogeom_bail!(
3250 Construction,
3251 "D{list_de}: expected an edge list, found type {}",
3252 entity.kind
3253 );
3254 }
3255 let i = index
3256 .checked_sub(1)
3257 .and_then(|i| usize::try_from(i).ok())
3258 .ok_or_else(|| {
3259 ogeom_core::ogeom_err!(Construction, "D{list_de}: edge index {index} out of range")
3260 })?;
3261 let base = 1 + 5 * i;
3262 let curve_de = entity.at(base).int();
3263 let start = (entity.at(base + 1).int(), entity.at(base + 2).int());
3264 let end = (entity.at(base + 3).int(), entity.at(base + 4).int());
3265 let (curve, mut range) = self.curve(curve_de)?;
3266 let ps = self.vertex_point(start.0, start.1)?;
3267 let pe = self.vertex_point(end.0, end.1)?;
3268 if let (Some(a), Some(b)) = (parameter_on(&curve, ps), parameter_on(&curve, pe)) {
3272 let period = if curve.is_periodic() {
3273 let (lo, hi) = curve.domain();
3274 hi - lo
3275 } else {
3276 0.0
3277 };
3278 range = if ps.distance(pe) < self.tol.confusion() && period > 0.0 {
3279 (a, a + period)
3280 } else if period > 0.0 && b <= a + self.tol.parametric() {
3281 (a, b + period)
3282 } else {
3283 (a, b)
3284 };
3285 if period == 0.0 {
3289 let (lo, hi) = curve.domain();
3290 range = (range.0.clamp(lo, hi), range.1.clamp(lo, hi));
3291 }
3292 }
3293 Ok((curve, range, start, end))
3294 }
3295
3296 fn vertex_point(&mut self, list_de: i64, index: i64) -> OgeomResult<Point> {
3298 if let Some(found) = self.vertices.get(&(list_de, index)) {
3299 return Ok(self.point_of(found));
3300 }
3301 let entity = self.entity(list_de)?;
3302 if entity.kind != 502 {
3303 ogeom_bail!(
3304 Construction,
3305 "D{list_de}: expected a vertex list, found type {}",
3306 entity.kind
3307 );
3308 }
3309 let i = index
3310 .checked_sub(1)
3311 .and_then(|i| usize::try_from(i).ok())
3312 .ok_or_else(|| {
3313 ogeom_core::ogeom_err!(
3314 Construction,
3315 "D{list_de}: vertex index {index} out of range"
3316 )
3317 })?;
3318 Ok(self.point3(entity, 1 + 3 * i))
3319 }
3320
3321 fn list_vertex(&mut self, list_de: i64, index: i64) -> OgeomResult<Shape> {
3324 let key = (list_de, index);
3325 if let Some(found) = self.vertices.get(&key) {
3326 return Ok(found.clone());
3327 }
3328 let point = self.vertex_point(list_de, index)?;
3329 let vertex = make_vertex(&mut self.model, point).shape;
3330 self.vertices.insert(key, vertex.clone());
3331 Ok(vertex)
3332 }
3333
3334 fn point_of(&self, vertex: &Shape) -> Point {
3335 self.model
3336 .node(vertex)
3337 .and_then(|n| n.data().as_vertex())
3338 .map_or(Point::ORIGIN, |d| d.point)
3339 }
3340
3341 fn csg_solids(&mut self, de: i64) -> OgeomResult<Vec<Shape>> {
3346 let entity = self.entity(de)?;
3347 if entity.kind != 184 {
3348 return Ok(vec![self.csg(de)?]);
3349 }
3350 let n = entity.count(0);
3352 let mut out = Vec::with_capacity(n);
3353 for i in 0..n {
3354 let item = entity.at(1 + i).int();
3355 let matrix = entity.at(1 + n + i).int();
3356 let mut solid = self.csg(item)?;
3357 if matrix != 0 {
3358 let placement = {
3359 let holder = Entity {
3360 kind: 0,
3361 form: 0,
3362 transform: matrix,
3363 colour: 0,
3364 level: 0,
3365 status: 0,
3366 label: String::new(),
3367 params: Vec::new(),
3368 };
3369 self.placement(&holder)?
3370 };
3371 let location = ogeom_topo::Location::of(self.model.add_datum(placement));
3372 solid = solid.moved(&location);
3373 }
3374 out.push(solid);
3375 }
3376 Ok(out)
3377 }
3378
3379 #[allow(clippy::too_many_lines, reason = "one case per primitive")]
3383 fn csg(&mut self, de: i64) -> OgeomResult<Shape> {
3384 let entity = self.entity(de)?;
3385 let s = self.report.scale_mm;
3386 let tol = self.tol;
3387 let r = |i: usize| entity.at(i).real();
3388 let length = |i: usize| entity.at(i).real() * s;
3389 let point = |i: usize| Point::new(r(i) * s, r(i + 1) * s, r(i + 2) * s);
3390 let direction = |i: usize, default: Vector| -> OgeomResult<Direction> {
3392 let v = Vector::new(r(i), r(i + 1), r(i + 2));
3393 Direction::new(if v.magnitude() > 0.0 { v } else { default }, tol)
3394 };
3395 let shape = match entity.kind {
3396 150 => {
3397 let frame = Frame::new(
3398 point(3),
3399 direction(9, Vector::Z)?,
3400 direction(6, Vector::X)?,
3401 tol,
3402 )?;
3403 ogeom_algo::make_box(
3404 &mut self.model,
3405 frame,
3406 (length(0), length(1), length(2)),
3407 tol,
3408 )?
3409 .shape
3410 }
3411 152 => {
3412 let x_file = direction(7, Vector::X)?;
3417 let z_file = direction(10, Vector::Z)?;
3418 let y_file = Direction::new(z_file.vector().cross(x_file.vector()), tol)?;
3419 let frame = Frame::new(point(4), y_file, z_file, tol)?;
3420 ogeom_algo::make_wedge(
3421 &mut self.model,
3422 frame,
3423 (length(2), length(0), length(1)),
3424 (length(2), length(3)),
3425 tol,
3426 )?
3427 .shape
3428 }
3429 154 => {
3430 let frame = frame_about(point(2), direction(5, Vector::Z)?, tol)?;
3431 ogeom_algo::make_cylinder(&mut self.model, frame, length(1), length(0), tol)?.shape
3432 }
3433 156 => {
3434 let frame = frame_about(point(3), direction(6, Vector::Z)?, tol)?;
3435 ogeom_algo::make_cone(&mut self.model, frame, length(1), length(2), length(0), tol)?
3436 .shape
3437 }
3438 158 => {
3439 let frame = frame_about(point(1), Direction::Z, tol)?;
3440 ogeom_algo::make_sphere(&mut self.model, frame, length(0), tol)?.shape
3441 }
3442 160 => {
3443 let frame = frame_about(point(2), direction(5, Vector::Z)?, tol)?;
3444 ogeom_algo::make_torus(&mut self.model, frame, length(0), length(1), tol)?.shape
3445 }
3446 162 => {
3447 let axis = ogeom_math::Axis::new(point(2), direction(5, Vector::Z)?);
3450 let fraction = match r(1) {
3451 f if f > 0.0 => f.min(1.0),
3452 _ => 1.0,
3453 };
3454 let face = self.profile_face(de, entity.at(0).int(), Some(axis))?;
3455 ogeom_algo::make_revolution(
3456 &mut self.model,
3457 &face,
3458 axis,
3459 core::f64::consts::TAU * fraction,
3460 tol,
3461 )?
3462 .shape
3463 }
3464 164 => {
3465 let face = self.profile_face(de, entity.at(0).int(), None)?;
3466 let along = direction(2, Vector::Z)?.vector() * length(1);
3467 ogeom_algo::make_prism(&mut self.model, &face, along, tol)?.shape
3468 }
3469 168 => {
3470 let x = direction(6, Vector::X)?.vector();
3472 let z = direction(9, Vector::Z)?.vector();
3473 let y = z.cross(x);
3474 let (a, b, c) = (length(0), length(1), length(2));
3475 let linear = Matrix3::new([
3476 [x.x * a, y.x * b, z.x * c],
3477 [x.y * a, y.y * b, z.y * c],
3478 [x.z * a, y.z * b, z.z * c],
3479 ]);
3480 let ball = ogeom_algo::make_sphere(&mut self.model, Frame::WORLD, 1.0, tol)?.shape;
3481 let stretch = ogeom_math::GeneralTransform::new(linear, point(3).to_vector());
3482 ogeom_algo::general_transformed_shape(&mut self.model, &ball, &stretch, tol)?.shape
3483 }
3484 180 => {
3485 let n = entity.count(0);
3488 let mut stack: Vec<Shape> = Vec::new();
3489 for i in 0..n {
3490 let item = entity.at(1 + i).int();
3491 if item < 0 {
3492 stack.push(self.csg(-item)?);
3493 continue;
3494 }
3495 let (Some(b), Some(a)) = (stack.pop(), stack.pop()) else {
3496 ogeom_bail!(
3497 Construction,
3498 "D{de}: a boolean tree's operation has too few operands"
3499 );
3500 };
3501 let result = match item {
3502 1 => ogeom_bool::fuse(&mut self.model, &a, &b, tol)?,
3503 2 => ogeom_bool::common(&mut self.model, &a, &b, tol)?,
3504 3 => ogeom_bool::cut(&mut self.model, &a, &b, tol)?,
3505 code => ogeom_bail!(
3506 Construction,
3507 "D{de}: boolean operation code {code} names no operation"
3508 ),
3509 };
3510 stack.push(result.shape);
3511 }
3512 let [solid] = stack.as_slice() else {
3513 ogeom_bail!(
3514 Construction,
3515 "D{de}: a boolean tree leaves {} results",
3516 stack.len()
3517 );
3518 };
3519 solid.clone()
3520 }
3521 430 => self.csg(entity.at(0).int())?,
3522 186 => self.manifold_solid(de)?,
3523 kind => ogeom_bail!(
3524 Construction,
3525 "D{de}: type {kind} is not a constructive solid"
3526 ),
3527 };
3528 let placement = self.placement(entity)?;
3529 if placement == Transform::IDENTITY {
3530 return Ok(shape);
3531 }
3532 let location = ogeom_topo::Location::of(self.model.add_datum(placement));
3533 Ok(shape.moved(&location))
3534 }
3535
3536 fn profile_face(
3539 &mut self,
3540 de: i64,
3541 curve: i64,
3542 axis: Option<ogeom_math::Axis>,
3543 ) -> OgeomResult<Shape> {
3544 let mut segments = self.curve_segments(curve)?;
3545 let start = segments[0].0.point_at(segments[0].1.0, self.tol)?;
3546 let last = &segments[segments.len() - 1];
3547 let end = last.0.point_at(last.1.1, self.tol)?;
3548 if start.distance(end) > self.tol.confusion() * 100.0 {
3549 let Some(axis) = axis else {
3550 ogeom_bail!(Construction, "D{de}: an extruded profile does not close");
3551 };
3552 let foot = |p: Point| {
3553 let o = axis.location;
3554 let d = axis.direction.vector();
3555 o + d * (p - o).dot(d)
3556 };
3557 let (end_foot, start_foot) = (foot(end), foot(start));
3558 for (a, b) in [(end, end_foot), (end_foot, start_foot), (start_foot, start)] {
3559 if a.distance(b) > self.tol.confusion() * 100.0 {
3560 let line: Curve = LineCurve::segment(a, b, self.tol)?.into();
3561 let domain = line.domain();
3562 segments.push((line, domain));
3563 }
3564 }
3565 }
3566 let edges = self.wire_edges(de, segments)?;
3567 let wire = ogeom_algo::make_wire(&mut self.model, &edges, self.tol)?.shape;
3568 let Some(plane) = ogeom_algo::find_plane(&self.model, &wire, self.tol)? else {
3569 ogeom_bail!(Construction, "D{de}: a solid's profile is not planar");
3570 };
3571 let surface: SurfaceGeometry = PlaneSurface::over(
3572 plane,
3573 (-SURFACE_EXTENT, SURFACE_EXTENT),
3574 (-SURFACE_EXTENT, SURFACE_EXTENT),
3575 )?
3576 .into();
3577 Ok(ogeom_algo::make_face_with_pcurves(&mut self.model, surface, &[edges], self.tol)?.shape)
3578 }
3579
3580 fn subfigure_instances(&mut self, tol: Tolerances) -> OgeomResult<Placed> {
3589 let instances: Vec<i64> = self
3590 .file
3591 .entities
3592 .iter()
3593 .filter(|(_, e)| INSTANCE_KINDS.contains(&e.kind) && (e.status / 10_000) % 100 == 0)
3594 .map(|(de, _)| *de)
3595 .collect();
3596 let mut built: FastMap<i64, Content> = FastMap::default();
3597 let mut placed = Placed::default();
3598 for de in instances {
3599 let copies = match self.instance(de, &mut built, 0) {
3600 Ok(found) => found,
3601 Err(e) => {
3602 self.report
3603 .warnings
3604 .push(format!("D{de}: subfigure instance not placed: {e}"));
3605 continue;
3606 }
3607 };
3608 for copy in copies {
3609 placed
3610 .from
3611 .extend(copy.solids.iter().map(|s| (de, s.clone())));
3612 placed
3613 .from
3614 .extend(copy.faces.iter().map(|f| (de, f.clone())));
3615 placed.solids.extend(copy.solids);
3616 placed.wires.extend(copy.wires);
3617 placed.points.extend(copy.points);
3618 if copy.faces.is_empty() {
3619 continue;
3620 }
3621 let sewn = sew(&mut self.model, ©.faces, tol)?;
3622 for shell in &sewn.shells {
3623 ogeom_algo::join_seam_columns(&mut self.model, shell, tol)?;
3624 if ogeom_algo::is_shell_closed(&self.model, shell)? {
3625 placed
3626 .solids
3627 .push(make_solid(&mut self.model, std::slice::from_ref(shell))?.shape);
3628 } else {
3629 placed.sheets.push(shell.clone());
3630 }
3631 }
3632 }
3633 }
3634 Ok(placed)
3635 }
3636
3637 fn instance(
3652 &mut self,
3653 de: i64,
3654 built: &mut FastMap<i64, Content>,
3655 depth: usize,
3656 ) -> OgeomResult<Vec<Content>> {
3657 let entity = self.entity(de)?;
3658 let s = self.report.scale_mm;
3659 let at = |i: usize| entity.at(i).real() * s;
3660 let scale_at = |i: usize| match entity.at(i).real() {
3661 f if f > 0.0 => f,
3662 _ => 1.0,
3663 };
3664 let (base, factor, positions): (i64, f64, Vec<Vector>) = match entity.kind {
3666 408 => {
3667 let definition = entity.at(0).int();
3668 if let Some(e) = self.file.entity(definition)
3669 && !matches!(e.kind, 308 | 416)
3670 {
3671 ogeom_bail!(
3672 Construction,
3673 "D{de}: an instance names a type {} entity, not a subfigure \
3674 definition or an external reference to one",
3675 e.kind
3676 );
3677 }
3678 (
3679 definition,
3680 scale_at(4),
3681 vec![Vector::new(at(1), at(2), at(3))],
3682 )
3683 }
3684 412 => {
3685 let (columns, rows) = (entity.at(5).int(), entity.at(6).int());
3686 let grid = (columns >= 1 && rows >= 1)
3687 .then(|| columns.checked_mul(rows))
3688 .flatten();
3689 let count = array_count(de, grid)?;
3690 let kept = kept_positions(entity, 10, count);
3691 let corner = Vector::new(at(2), at(3), at(4));
3692 let (dx, dy) = (at(7), at(8));
3693 let (sin, cos) = entity.at(9).real().sin_cos();
3694 let rows = usize::try_from(rows).unwrap_or(1);
3695 let positions = kept
3696 .into_iter()
3697 .map(|k| {
3698 let (column, row) = ((k - 1) / rows, (k - 1) % rows);
3699 #[allow(clippy::cast_precision_loss, reason = "array sizes are small")]
3700 let (x, y) = (column as f64 * dx, row as f64 * dy);
3701 corner + Vector::new(cos * x - sin * y, sin * x + cos * y, 0.0)
3702 })
3703 .collect();
3704 (entity.at(0).int(), scale_at(1), positions)
3705 }
3706 420 => {
3707 let definition = entity.at(0).int();
3708 if let Some(e) = self.file.entity(definition)
3709 && !matches!(e.kind, 320 | 416)
3710 {
3711 ogeom_bail!(
3712 Construction,
3713 "D{de}: a network instance names a type {} entity, not a network \
3714 subfigure definition or an external reference to one",
3715 e.kind
3716 );
3717 }
3718 let defined = match self.file.entity(definition) {
3720 Some(e) if e.kind == 416 => self
3721 .referenced(definition, e)
3722 .ok()
3723 .and_then(|(file, target)| file.entity(target?)),
3724 found => found,
3725 };
3726 let flag = match entity.at(7).int() {
3730 0 => defined
3731 .filter(|e| e.kind == 320)
3732 .map_or(0, |e| e.at(3 + e.count(2)).int()),
3733 flag => flag,
3734 };
3735 if flag == 1 {
3736 ogeom_bail!(
3737 Construction,
3738 "D{de}: a logical network instance is a schematic's, not the \
3739 model's geometry"
3740 );
3741 }
3742 let x = scale_at(4);
3746 let or_x = |i: usize| match entity.at(i).real() {
3747 f if f > 0.0 => f,
3748 _ => x,
3749 };
3750 let (y, z) = (or_x(5), or_x(6));
3751 if y != x || z != x {
3752 ogeom_bail!(
3753 Construction,
3754 "D{de}: a network instance scales its axes unlike ({x}, {y}, {z}), \
3755 which distorts what it places"
3756 );
3757 }
3758 (definition, x, vec![Vector::new(at(1), at(2), at(3))])
3759 }
3760 414 => {
3761 let count = array_count(de, Some(entity.at(1).int()))?;
3762 let kept = kept_positions(entity, 8, count);
3763 let centre = Vector::new(at(2), at(3), at(4));
3764 let radius = at(5);
3765 let (start, delta) = (entity.at(6).real(), entity.at(7).real());
3766 let positions = kept
3767 .into_iter()
3768 .map(|k| {
3769 #[allow(clippy::cast_precision_loss, reason = "array sizes are small")]
3770 let (sin, cos) = (start + (k - 1) as f64 * delta).sin_cos();
3771 centre + Vector::new(radius * cos, radius * sin, 0.0)
3772 })
3773 .collect();
3774 (entity.at(0).int(), 1.0, positions)
3775 }
3776 kind => ogeom_bail!(
3777 Construction,
3778 "D{de}: type {kind} is not a subfigure instance"
3779 ),
3780 };
3781 let content = self.member(base, built, depth + 1)?;
3782 if positions.len().saturating_mul(content.len()) > MOST_PLACED {
3783 ogeom_bail!(
3784 Construction,
3785 "D{de}: places {} copies of {} shapes, past {MOST_PLACED}",
3786 positions.len(),
3787 content.len()
3788 );
3789 }
3790 let within = self.placement(entity)? * Transform::scaling(Point::ORIGIN, factor, self.tol)?;
3791 let mut copies = Vec::with_capacity(positions.len());
3792 for position in positions {
3793 let motion = Transform::translation(position) * within;
3794 let location = ogeom_topo::Location::of(self.model.add_datum(motion));
3795 copies.push(content.moved(&location));
3796 }
3797 Ok(copies)
3798 }
3799
3800 fn member(
3806 &mut self,
3807 de: i64,
3808 built: &mut FastMap<i64, Content>,
3809 depth: usize,
3810 ) -> OgeomResult<Content> {
3811 if let Some(found) = built.get(&de) {
3812 return Ok(found.clone());
3813 }
3814 if depth > 32 {
3815 ogeom_bail!(
3816 Construction,
3817 "D{de}: subfigures nest past any sensible depth"
3818 );
3819 }
3820 let Some(entity) = self.file.entity(de) else {
3821 ogeom_bail!(Construction, "IGES pointer D{de} names no entity");
3822 };
3823 let (kind, wire) = (entity.kind, is_wire(entity));
3824 let mut content = Content::default();
3825 match kind {
3826 308 | 320 => content = self.subfigure(de, built, depth)?,
3827 416 => content = self.external(de, depth)?,
3828 402 if matches!(entity.form, 1 | 7 | 14 | 15) => {
3829 content = self.group(de, built, depth)?;
3830 }
3831 408 | 412 | 414 | 420 => {
3832 for copy in self.instance(de, built, depth)? {
3833 content.extend(copy);
3834 }
3835 }
3836 186 => content.solids.push(self.manifold_solid(de)?),
3837 143 | 144 => content.faces.push(self.face(de)?),
3838 k if UNTRIMMED_KINDS.contains(&k) || is_bounded_plane(entity) => {
3839 content.faces.push(self.untrimmed_face(de)?);
3840 }
3841 k if CSG_KINDS.contains(&k) => content.solids.extend(self.csg_solids(de)?),
3842 _ if wire => content.wires.push(self.wireframe(de)?),
3843 116 => content.points.push(self.point(de)?),
3844 _ => {}
3845 }
3846 built.insert(de, content.clone());
3847 Ok(content)
3848 }
3849
3850 fn subfigure(
3856 &mut self,
3857 de: i64,
3858 built: &mut FastMap<i64, Content>,
3859 depth: usize,
3860 ) -> OgeomResult<Content> {
3861 let entity = self.entity(de)?;
3862 let count = entity.count(2);
3863 let members: Vec<i64> = (0..count).map(|i| entity.at(3 + i).int()).collect();
3864 let placement = self.placement(entity)?;
3865 if placement == Transform::IDENTITY {
3866 return self.members(&members, built, depth);
3867 }
3868 let (moved, left): (Vec<i64>, Vec<i64>) = members
3869 .iter()
3870 .partition(|&&m| self.file.entity(m).is_none_or(carried));
3871 let location = ogeom_topo::Location::of(self.model.add_datum(placement));
3872 let mut content = self.members(&moved, built, depth)?.moved(&location);
3873 content.extend(self.members(&left, built, depth)?);
3874 Ok(content)
3875 }
3876
3877 fn group(
3880 &mut self,
3881 de: i64,
3882 built: &mut FastMap<i64, Content>,
3883 depth: usize,
3884 ) -> OgeomResult<Content> {
3885 let entity = self.entity(de)?;
3886 let count = entity.count(0);
3887 let members: Vec<i64> = (0..count).map(|i| entity.at(1 + i).int().abs()).collect();
3888 self.members(&members, built, depth)
3889 }
3890
3891 fn referenced(&self, de: i64, reference: &Entity) -> OgeomResult<(&'a File, Option<i64>)> {
3896 let symbol = match reference.form {
3897 0 => match reference.at(1).text() {
3898 Some(name) => Some(name),
3899 None => ogeom_bail!(
3900 Construction,
3901 "D{de}: an external reference to a definition names no symbol"
3902 ),
3903 },
3904 1 => None,
3905 2 => ogeom_bail!(
3906 Construction,
3907 "D{de}: a logical external reference (416 form 2) relates two entities; \
3908 it defines nothing to place"
3909 ),
3910 3 | 4 => ogeom_bail!(
3911 Construction,
3912 "D{de}: an external reference (416 form {}) to a definition the receiving \
3913 system keeps natively names nothing any file holds",
3914 reference.form
3915 ),
3916 form => ogeom_bail!(
3917 Construction,
3918 "D{de}: external reference form {form} is not defined"
3919 ),
3920 };
3921 let id = reference.at(0).text().unwrap_or("");
3922 if file_name(id).is_empty() {
3923 ogeom_bail!(Construction, "D{de}: an external reference names no file");
3924 }
3925 let library = self.library;
3926 let file = match library.file(id) {
3927 Ok(file) => file,
3928 Err(why) => ogeom_bail!(
3929 Construction,
3930 "D{de}: an external reference names the file \"{id}\", which is not \
3931 to be had: {why}"
3932 ),
3933 };
3934 let Some(symbol) = symbol else {
3935 return Ok((file, None));
3936 };
3937 let target = file
3938 .entities
3939 .values()
3940 .filter(|e| e.kind == 402 && e.form == 12)
3941 .find_map(|index| {
3942 (0..index.count(0)).find_map(|i| {
3943 (index.at(1 + 2 * i).text() == Some(symbol)).then(|| index.at(2 + 2 * i).int())
3944 })
3945 });
3946 match target {
3947 Some(target) if file.entity(target).is_some() => Ok((file, Some(target.abs()))),
3948 _ => ogeom_bail!(
3949 Construction,
3950 "D{de}: \"{}\" lists no definition named \"{symbol}\" in its index",
3951 file_name(id)
3952 ),
3953 }
3954 }
3955
3956 fn external(&mut self, de: i64, depth: usize) -> OgeomResult<Content> {
3961 let reference = self.entity(de)?;
3962 let (file, target) = self.referenced(de, reference)?;
3963 let name = file_name(reference.at(0).text().unwrap_or("")).to_owned();
3964 let Some(scale_mm) = file.scale_mm() else {
3965 ogeom_bail!(
3966 Construction,
3967 "D{de}: \"{name}\" names units this reader cannot convert to millimetres"
3968 );
3969 };
3970 let model = core::mem::take(&mut self.model);
3971 let mut there = Reader::new(file, model, scale_mm, self.library, self.tol);
3972 there.depth = self.depth;
3973 let mut built = FastMap::default();
3974 let content = match target {
3975 Some(target) => there.member(target, &mut built, depth + 1),
3976 None => there.whole_file(&mut built, depth + 1),
3977 };
3978 self.model = core::mem::take(&mut there.model);
3979 self.report.warnings.extend(
3980 there
3981 .report
3982 .warnings
3983 .into_iter()
3984 .map(|w| format!("{name}: {w}")),
3985 );
3986 self.vertex_misses.0 += there.vertex_misses.0;
3987 self.vertex_misses.1 = self.vertex_misses.1.max(there.vertex_misses.1);
3988 content
3989 }
3990
3991 fn whole_file(
3998 &mut self,
3999 built: &mut FastMap<i64, Content>,
4000 depth: usize,
4001 ) -> OgeomResult<Content> {
4002 let file = self.file;
4003 let on_sheets = on_sheets(file);
4004 let stage = |de: i64, e: &Entity| -> Option<u8> {
4005 if (e.status / 10_000) % 100 != 0 {
4006 return None;
4007 }
4008 let loose = (e.status / 100) % 100 == 0 && !on_sheets.contains(&de);
4009 match e.kind {
4010 186 => Some(0),
4011 143 | 144 => Some(1),
4012 k if UNTRIMMED_KINDS.contains(&k) || is_bounded_plane(e) => Some(2),
4013 k if CSG_KINDS.contains(&k) => Some(3),
4014 k if INSTANCE_KINDS.contains(&k) => Some(4),
4015 _ if is_wire(e) && loose => Some(5),
4016 116 if loose => Some(6),
4017 _ => None,
4018 }
4019 };
4020 let mut content = Content::default();
4021 for step in 0..=6 {
4022 let des: Vec<i64> = file
4023 .entities
4024 .iter()
4025 .filter(|(de, e)| stage(**de, e) == Some(step) && !self.visited.contains_key(de))
4026 .map(|(de, _)| *de)
4027 .collect();
4028 content.extend(self.members(&des, built, depth)?);
4029 }
4030 Ok(content)
4031 }
4032
4033 fn members(
4035 &mut self,
4036 members: &[i64],
4037 built: &mut FastMap<i64, Content>,
4038 depth: usize,
4039 ) -> OgeomResult<Content> {
4040 let trims: Vec<i64> = members
4041 .iter()
4042 .copied()
4043 .filter(|&m| {
4044 self.file
4045 .entity(m)
4046 .is_some_and(|e| matches!(e.kind, 143 | 144))
4047 })
4048 .collect();
4049 let poles = self.poles_of_trims(&trims);
4050 let outer_poles = core::mem::replace(&mut self.trim_poles, poles);
4053 let mut content = Content::default();
4054 let mut read_members = || -> OgeomResult<()> {
4055 for &member in members {
4056 let Some(kind) = self.file.entity(member).map(|e| e.kind) else {
4057 continue;
4058 };
4059 match self.member(member, built, depth + 1) {
4063 Ok(found) => content.extend(found),
4064 Err(e) if !matches!(kind, 143 | 144 | 186 | 408) => self
4065 .report
4066 .warnings
4067 .push(format!("D{member}: subfigure member not read: {e}")),
4068 Err(e) => return Err(e),
4069 }
4070 }
4071 Ok(())
4072 };
4073 let read = read_members();
4074 self.trim_poles = outer_poles;
4075 read?;
4076 Ok(content)
4077 }
4078
4079 fn annotations(
4088 &mut self,
4089 ) -> (
4090 Vec<ogeom_doc::Callout>,
4091 Vec<(ogeom_doc::Callout, ogeom_doc::Dimension)>,
4092 ) {
4093 let on_sheets = on_sheets(self.file);
4094 let drafting: Vec<i64> = self
4095 .file
4096 .entities
4097 .iter()
4098 .filter(|(de, e)| {
4099 matches!(
4100 e.kind,
4101 202 | 206 | 208 | 210 | 212 | 214 | 216 | 218 | 220 | 222 | 228
4102 ) && (e.status / 10_000) % 100 == 0
4103 && !on_sheets.contains(de)
4104 })
4105 .map(|(de, _)| *de)
4106 .collect();
4107 let mut callouts = Vec::new();
4108 let mut dimensions = Vec::new();
4109 for de in drafting {
4110 match self.drafting(de) {
4111 Ok((callout, Some(dimension))) => dimensions.push((callout, dimension)),
4112 Ok((callout, None)) => callouts.push(callout),
4113 Err(e) => self
4114 .report
4115 .warnings
4116 .push(format!("D{de}: annotation not drawn: {e}")),
4117 }
4118 }
4119 (callouts, dimensions)
4120 }
4121
4122 fn drafting(
4125 &mut self,
4126 de: i64,
4127 ) -> OgeomResult<(ogeom_doc::Callout, Option<ogeom_doc::Dimension>)> {
4128 let entity = self.entity(de)?;
4129 let pointer = |i: usize| entity.at(i).int();
4130 let mut polylines: Vec<(i64, Vec<Point>)> = Vec::new();
4133 let mut text = String::new();
4134 let mut measure: Option<(&str, ogeom_doc::MeasureKind)> = None;
4135 let draw =
4136 |reader: &mut Self, lines: &mut Vec<(i64, Vec<Point>)>, de: i64| -> OgeomResult<()> {
4137 if de != 0 {
4138 lines.push((de, reader.drafted_line(de)?));
4139 }
4140 Ok(())
4141 };
4142 match entity.kind {
4143 212 => text = self.note_text(de)?,
4144 214 => polylines.push((0, self.drafted_line(de)?)),
4145 216 => {
4146 text = self.note_text(pointer(0))?;
4147 for i in 1..=4 {
4148 draw(self, &mut polylines, pointer(i))?;
4149 }
4150 measure = Some(("linear distance", ogeom_doc::MeasureKind::Length));
4151 }
4152 206 => {
4153 text = self.note_text(pointer(0))?;
4154 for i in 1..=2 {
4155 draw(self, &mut polylines, pointer(i))?;
4156 }
4157 measure = Some(("diameter", ogeom_doc::MeasureKind::Length));
4158 }
4159 222 => {
4160 text = self.note_text(pointer(0))?;
4161 draw(self, &mut polylines, pointer(1))?;
4162 if entity.params.len() > 4 {
4163 draw(self, &mut polylines, pointer(4))?;
4164 }
4165 measure = Some(("radius", ogeom_doc::MeasureKind::Length));
4166 }
4167 202 => {
4168 text = self.note_text(pointer(0))?;
4169 for i in [1, 2, 6, 7] {
4170 draw(self, &mut polylines, pointer(i))?;
4171 }
4172 measure = Some(("angle", ogeom_doc::MeasureKind::Angle));
4173 }
4174 218 | 220 => {
4175 text = self.note_text(pointer(0))?;
4176 draw(self, &mut polylines, pointer(1))?;
4177 measure = Some(("ordinate", ogeom_doc::MeasureKind::Length));
4178 if entity.kind == 220 {
4179 measure = None;
4180 }
4181 }
4182 208 | 210 => {
4183 let at = if entity.kind == 208 { 4 } else { 0 };
4186 text = self.note_text(pointer(at))?;
4187 let n = entity.count(at + 1);
4188 for i in 0..n {
4189 draw(self, &mut polylines, pointer(at + 2 + i))?;
4190 }
4191 }
4192 228 => {
4193 text = self.note_text(pointer(0))?;
4195 let ng = entity.count(1);
4196 for i in 0..ng {
4197 polylines.push((pointer(2 + i), self.sampled_curve(pointer(2 + i))?));
4198 }
4199 let nl = entity.count(2 + ng);
4200 for i in 0..nl {
4201 draw(self, &mut polylines, pointer(3 + ng + i))?;
4202 }
4203 }
4204 kind => ogeom_bail!(Construction, "type {kind} is not drafting"),
4205 }
4206 let polylines = polylines
4209 .into_iter()
4210 .map(|(child, line)| {
4211 let motion = if child == 0 {
4212 Transform::IDENTITY
4213 } else {
4214 self.parent_motion(entity, child)?
4215 };
4216 Ok(line.into_iter().map(|p| motion.apply(p)).collect())
4217 })
4218 .collect::<OgeomResult<Vec<Vec<Point>>>>()?;
4219 let placement = self.placement(entity)?;
4220 let plane = Frame::new(
4221 placement.apply(Point::ORIGIN),
4222 Direction::new(placement.apply_vector(Vector::Z), self.tol)?,
4223 Direction::new(placement.apply_vector(Vector::X), self.tol)?,
4224 self.tol,
4225 )
4226 .ok();
4227 let callout = ogeom_doc::Callout {
4228 name: text.clone(),
4229 plane,
4230 polylines,
4231 annotates: None,
4232 items: Vec::new(),
4233 text: Some(text.clone()),
4234 };
4235 let dimension = measure.and_then(|(name, kind)| {
4236 let value = first_number(&text)?;
4237 let value = match kind {
4238 ogeom_doc::MeasureKind::Angle => value.to_radians(),
4239 ogeom_doc::MeasureKind::Length => value * self.report.scale_mm,
4240 };
4241 Some(ogeom_doc::Dimension {
4242 name: name.to_owned(),
4243 values: vec![value],
4244 kind,
4245 plus: None,
4246 minus: None,
4247 features: Vec::new(),
4248 location: entity.kind == 216 || entity.kind == 218,
4249 })
4250 });
4251 Ok((callout, dimension))
4252 }
4253
4254 fn note_text(&mut self, de: i64) -> OgeomResult<String> {
4256 if de == 0 {
4257 return Ok(String::new());
4258 }
4259 let entity = self.entity(de)?;
4260 if entity.kind != 212 {
4261 ogeom_bail!(Construction, "D{de}: type {} is not a note", entity.kind);
4262 }
4263 let count = entity.count(0);
4264 let mut lines = Vec::with_capacity(count);
4265 for i in 0..count {
4266 if let super::parse::Value::Text(t) = entity.at(1 + 12 * i + 11) {
4267 lines.push(t.clone());
4268 }
4269 }
4270 Ok(lines.join("\n"))
4271 }
4272
4273 fn drafted_line(&mut self, de: i64) -> OgeomResult<Vec<Point>> {
4276 let entity = self.entity(de)?;
4277 let s = self.report.scale_mm;
4278 let mut points = Vec::new();
4279 match entity.kind {
4280 214 => {
4281 let n = entity.count(0);
4282 let z = entity.at(3).real();
4283 points.push(Point::new(
4284 entity.at(4).real() * s,
4285 entity.at(5).real() * s,
4286 z * s,
4287 ));
4288 for i in 0..n {
4289 points.push(Point::new(
4290 entity.at(6 + 2 * i).real() * s,
4291 entity.at(7 + 2 * i).real() * s,
4292 z * s,
4293 ));
4294 }
4295 }
4296 106 => {
4297 let n = entity.count(1);
4298 let z = entity.at(2).real();
4299 for i in 0..n {
4300 points.push(Point::new(
4301 entity.at(3 + 2 * i).real() * s,
4302 entity.at(4 + 2 * i).real() * s,
4303 z * s,
4304 ));
4305 }
4306 }
4307 _ => return self.sampled_curve(de),
4308 }
4309 let placement = self.placement(entity)?;
4310 Ok(points.into_iter().map(|p| placement.apply(p)).collect())
4311 }
4312
4313 fn sampled_curve(&mut self, de: i64) -> OgeomResult<Vec<Point>> {
4315 let (curve, range) = self.curve(de)?;
4316 (0..=32)
4317 .map(|i| {
4318 curve.point_at(
4319 range.0 + (range.1 - range.0) * f64::from(i) / 32.0,
4320 self.tol,
4321 )
4322 })
4323 .collect()
4324 }
4325
4326 fn construction_item(
4330 &mut self,
4331 de: i64,
4332 ) -> OgeomResult<Option<ogeom_doc::ConstructionGeometry>> {
4333 use ogeom_doc::ConstructionGeometry as G;
4334 let entity = self.entity(de)?;
4335 Ok(Some(match entity.kind {
4336 116 => G::Point(self.placement(entity)?.apply(self.point3(entity, 0))),
4337 124 => G::Axis(self.axis(entity)?),
4338 100 => {
4339 let (curve, range) = self.construction_arc(entity)?;
4340 G::Curve { curve, range }
4341 }
4342 144 => {
4343 let surface = self.trimmed_basis(entity, entity.at(0).int())?;
4344 let (u, v) = ogeom_geom::Surface::domain(&surface);
4345 G::Surface { surface, u, v }
4346 }
4347 kind if UNTRIMMED_KINDS.contains(&kind) => {
4348 let surface = self.surface(de)?;
4349 let (u, v) = ogeom_geom::Surface::domain(&surface);
4350 G::Surface { surface, u, v }
4351 }
4352 _ => {
4353 let mut segments = self.curve_segments(de)?;
4354 if segments.len() != 1 {
4355 self.report.warnings.push(format!(
4356 "D{de}: a construction curve of {} pieces is no one curve; \
4357 it is left out",
4358 segments.len()
4359 ));
4360 return Ok(None);
4361 }
4362 let (curve, range) = segments.remove(0);
4363 G::Curve { curve, range }
4364 }
4365 }))
4366 }
4367
4368 fn construction_arc(&mut self, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
4373 let scale = self.report.scale_mm;
4374 let zt = entity.at(0).real() * scale;
4375 let at = |i: usize| {
4376 Point::new(
4377 entity.at(i).real() * scale,
4378 entity.at(i + 1).real() * scale,
4379 zt,
4380 )
4381 };
4382 let (c, s, e) = (at(1), at(3), at(5));
4383 let angle = |p: Point| (p.y - c.y).atan2(p.x - c.x);
4384 let start = angle(s);
4385 let ang = (angle(e) - start).rem_euclid(core::f64::consts::TAU);
4386 let sweep = if ang <= self.tol.parametric() {
4387 core::f64::consts::TAU
4388 } else {
4389 ang
4390 };
4391 let frame = Frame::new(c, Direction::Z, Direction::X, self.tol)?;
4392 let circle = Circle::new(frame, c.distance(s), self.tol)?;
4393 let curve = Curve::from(CircleCurve::new(circle));
4394 let placement = self.placement(entity)?;
4395 Ok((
4396 curve.transformed(&placement, self.tol)?,
4397 (start, start + sweep),
4398 ))
4399 }
4400
4401 fn axis(&mut self, matrix: &Entity) -> OgeomResult<Frame> {
4405 let v = |i: usize| matrix.at(i).real();
4406 let s = self.report.scale_mm;
4407 let placement = self.placement(matrix)?;
4408 let column = |k: usize| -> OgeomResult<Direction> {
4409 Direction::new(
4410 placement.apply_vector(Vector::new(v(k), v(4 + k), v(8 + k))),
4411 self.tol,
4412 )
4413 };
4414 let origin = placement.apply(Point::new(v(3) * s, v(7) * s, v(11) * s));
4415 Frame::from_axes(origin, column(0)?, column(1)?, column(2)?, self.tol)
4416 }
4417
4418 fn long_name(&mut self, de: i64) -> Option<String> {
4423 let entity = self.file.entity(de)?;
4424 let back_at = defined_parameters(entity)?;
4425 let back = usize::try_from(entity.params.get(back_at)?.int()).ok()?;
4426 let count_at = back_at + 1 + back;
4427 let properties = usize::try_from(entity.params.get(count_at)?.int()).ok()?;
4428 for i in 0..properties {
4429 let pointer = entity.params.get(count_at + 1 + i)?.int();
4430 let Some(property) = self.file.entity(pointer) else {
4431 continue;
4432 };
4433 if property.kind == 406 && property.form == 15 {
4434 self.visited.insert(pointer, ());
4435 if let Some(Value::Text(name)) = property.params.get(1) {
4436 let name = name.trim();
4437 if !name.is_empty() {
4438 return Some(name.to_owned());
4439 }
4440 }
4441 }
4442 }
4443 None
4444 }
4445
4446 fn document(
4447 &mut self,
4448 solids: &[(i64, Shape)],
4449 names: &[Option<String>],
4450 sheets: &[Shape],
4451 construction: Vec<(String, ogeom_doc::ConstructionGeometry)>,
4452 ) -> ogeom_doc::Document {
4453 let bare = (solids.is_empty() && sheets.is_empty() && !construction.is_empty())
4457 .then(|| ogeom_algo::make_compound(&mut self.model, &[]).map(|built| built.shape));
4458 let colours: Vec<(Shape, ogeom_doc::Colour)> = solids
4459 .iter()
4460 .filter_map(|(de, s)| self.colour_of(*de).map(|c| (s.clone(), c)))
4461 .collect();
4462 let mut document = ogeom_doc::Document::over(std::mem::take(&mut self.model));
4463 for (i, (de, solid)) in solids.iter().enumerate() {
4464 let label = names
4465 .get(i)
4466 .cloned()
4467 .flatten()
4468 .or_else(|| {
4469 self.file
4470 .entity(*de)
4471 .map(|e| e.label.clone())
4472 .filter(|l| !l.is_empty())
4473 })
4474 .unwrap_or_else(|| format!("solid-{i}"));
4475 document.add_part(label, solid.clone());
4476 }
4477 for (i, sheet) in sheets.iter().enumerate() {
4478 document.add_part(format!("sheet-{i}"), sheet.clone());
4479 }
4480 for (shape, colour) in colours {
4481 document.set_colour(&shape, colour);
4482 }
4483 let owner = match bare {
4484 Some(Ok(nothing)) => Some(document.add_part("construction", nothing)),
4485 Some(Err(e)) => {
4486 self.report
4487 .warnings
4488 .push(format!("construction geometry has no part to go in: {e}"));
4489 None
4490 }
4491 None => document.products().next().map(|(id, _)| id),
4492 };
4493 if let Some(owner) = owner {
4494 for (name, geometry) in construction {
4495 if let Err(e) = document.add_construction(owner, name.clone(), geometry) {
4496 self.report
4497 .warnings
4498 .push(format!("construction item '{name}' was refused: {e}"));
4499 }
4500 }
4501 }
4502 document
4503 }
4504
4505 fn colour_of(&mut self, de: i64) -> Option<ogeom_doc::Colour> {
4508 let entity = self.file.entity(de)?;
4509 let c = entity.colour;
4510 if c < 0 {
4511 let e = self.file.entity(-c)?;
4512 self.visited.insert(-c, ());
4513 if e.kind != 314 {
4514 return None;
4515 }
4516 return Some(ogeom_doc::Colour::rgb(
4517 (e.at(0).real() / 100.0).clamp(0.0, 1.0),
4518 (e.at(1).real() / 100.0).clamp(0.0, 1.0),
4519 (e.at(2).real() / 100.0).clamp(0.0, 1.0),
4520 ));
4521 }
4522 let palette = [
4525 (0.0, 0.0, 0.0),
4526 (1.0, 0.0, 0.0),
4527 (0.0, 1.0, 0.0),
4528 (0.0, 0.0, 1.0),
4529 (1.0, 1.0, 0.0),
4530 (1.0, 0.0, 1.0),
4531 (0.0, 1.0, 1.0),
4532 (1.0, 1.0, 1.0),
4533 ];
4534 let index = usize::try_from(c).ok()?.checked_sub(1)?;
4535 palette
4536 .get(index)
4537 .map(|&(r, g, b)| ogeom_doc::Colour::rgb(r, g, b))
4538 }
4539}
4540
4541fn defined_parameters(entity: &Entity) -> Option<usize> {
4547 Some(match entity.kind {
4548 116 => 4,
4549 110 => 6,
4550 100 => 7,
4551 104 => 11,
4552 124 => 12,
4553 186 => 3 + 2 * entity.count(2),
4554 144 => 4 + entity.count(2),
4555 126 => {
4558 let (k, m) = (entity.count(0), entity.count(1));
4559 6 + (k + m + 2) + 4 * (k + 1) + 2 + 3
4560 }
4561 128 => {
4564 let (k1, k2) = (entity.count(0), entity.count(1));
4565 let (m1, m2) = (entity.count(2), entity.count(3));
4566 9 + (k1 + m1 + 2) + (k2 + m2 + 2) + 4 * (k1 + 1) * (k2 + 1) + 4
4567 }
4568 _ => return None,
4569 })
4570}
4571
4572fn construction_entities(file: &File, on_sheets: &std::collections::BTreeSet<i64>) -> Vec<i64> {
4581 let placements: std::collections::BTreeSet<i64> =
4582 file.entities.values().map(|e| e.transform.abs()).collect();
4583 let bases: std::collections::BTreeSet<i64> = file
4584 .entities
4585 .values()
4586 .filter_map(|e| match e.kind {
4587 143 => Some(e.at(1).int().abs()),
4588 144 => Some(e.at(0).int().abs()),
4589 _ => None,
4590 })
4591 .collect();
4592 let bounds_itself = |de: i64| {
4593 file.entity(de)
4594 .is_some_and(|b| UNTRIMMED_KINDS.contains(&b.kind))
4595 };
4596 file.entities
4597 .iter()
4598 .filter(|(de, e)| {
4599 (e.status / 10_000) % 100 == 0
4600 && (e.status / 100) % 100 == 6
4601 && !on_sheets.contains(de)
4602 && match e.kind {
4603 116 => true,
4604 124 => !placements.contains(de),
4605 144 => e.at(1).int() == 0 && e.count(2) == 0 && bounds_itself(e.at(0).int()),
4606 kind if UNTRIMMED_KINDS.contains(&kind) => !bases.contains(de),
4607 _ => is_wire(e),
4608 }
4609 })
4610 .map(|(de, _)| *de)
4611 .collect()
4612}
4613
4614fn on_sheets(file: &File) -> std::collections::BTreeSet<i64> {
4617 let mut on_sheets = std::collections::BTreeSet::new();
4618 for entity in file.entities.values().filter(|e| e.kind == 404) {
4619 let views = entity.count(0);
4620 let at = 1 + 3 * views;
4621 let count = entity.count(at);
4622 for i in 0..count {
4623 on_sheets.insert(entity.at(at + 1 + i).int().abs());
4624 }
4625 }
4626 on_sheets
4627}
4628
4629fn layers(
4635 file: &File,
4636 document: &mut ogeom_doc::Document,
4637 built_from: &[(i64, Shape)],
4638 report: &mut IgesReport,
4639) {
4640 let mut by_name: FastMap<String, ogeom_doc::LayerId> = FastMap::default();
4641 let mut layer = |document: &mut ogeom_doc::Document, name: String| {
4642 *by_name
4643 .entry(name.clone())
4644 .or_insert_with(|| document.add_layer(name))
4645 };
4646 for (de, shape) in built_from {
4647 let Some(entity) = file.entity(*de) else {
4648 continue;
4649 };
4650 let levels: Vec<i64> = if entity.level > 0 {
4651 vec![entity.level]
4652 } else if entity.level < 0 {
4653 match file.entity(-entity.level) {
4654 Some(p) if p.kind == 406 && p.form == 1 => {
4655 let n = p.count(0);
4656 (0..n).map(|i| p.at(1 + i).int()).collect()
4657 }
4658 _ => {
4659 report
4660 .warnings
4661 .push(format!("D{de}: its level names no levels property"));
4662 Vec::new()
4663 }
4664 }
4665 } else {
4666 Vec::new()
4667 };
4668 for level in levels {
4669 let id = layer(document, format!("level {level}"));
4670 document.place_on_layer(shape, id);
4671 }
4672 }
4673 for (de, group) in &file.entities {
4674 if group.kind != 402 || !matches!(group.form, 1 | 7 | 14 | 15) {
4675 continue;
4676 }
4677 let n = group.count(0);
4678 let members: Vec<i64> = (0..n).map(|i| group.at(1 + i).int().abs()).collect();
4679 let shapes: Vec<&Shape> = built_from
4680 .iter()
4681 .filter(|(from, _)| members.contains(from))
4682 .map(|(_, s)| s)
4683 .collect();
4684 if shapes.is_empty() {
4685 continue;
4686 }
4687 let name = if group.label.is_empty() {
4688 format!("group D{de}")
4689 } else {
4690 group.label.clone()
4691 };
4692 let id = layer(document, name);
4693 for shape in shapes {
4694 document.place_on_layer(shape, id);
4695 }
4696 }
4697}
4698
4699fn first_number(text: &str) -> Option<f64> {
4703 let trimmed = text.trim_start();
4704 let count = trimmed
4705 .find(|c: char| !c.is_ascii_digit())
4706 .filter(|&n| n > 0)
4707 .and_then(|n| {
4708 let rest = trimmed[n..].trim_start();
4709 rest.strip_prefix(['X', 'x']).map(|after| after.len())
4710 });
4711 let text = match count {
4712 Some(len) => &trimmed[trimmed.len() - len..],
4713 None => text,
4714 };
4715 let start = text.find(|c: char| c.is_ascii_digit() || c == '.')?;
4716 let tail = &text[start..];
4717 let end = tail
4718 .find(|c: char| !(c.is_ascii_digit() || c == '.'))
4719 .unwrap_or(tail.len());
4720 tail[..end].parse().ok()
4721}
4722
4723enum Effect {
4728 Warn(String),
4729 Gap(f64),
4730}
4731
4732type DerivedImage = (Option<ogeom_geom::PlanarCurve>, Vec<Effect>);
4735
4736fn derive_image(
4740 curve: &Curve,
4741 range: (f64, f64),
4742 surface: &SurfaceGeometry,
4743 tol: Tolerances,
4744) -> DerivedImage {
4745 use ogeom_geom::PlanarCurve;
4746 let widen = |p: PlanarCurve| -> PlanarCurve {
4747 if let PlanarCurve::Line(l) = &p {
4748 use ogeom_geom::Curve2d as _;
4749 let (lo, hi) = (l.domain().0.min(range.0), l.domain().1.max(range.1));
4750 if let Ok(wider) = ogeom_geom::Line2d::over(l.axis(), lo, hi) {
4751 return wider.into();
4752 }
4753 }
4754 p
4755 };
4756 let mut effects = Vec::new();
4757 let found = match ogeom_intersect::exact_pcurve_over(curve, range, surface, tol).map(widen) {
4758 Some(exact) => exact,
4759 None => match crate::pcurves::fit_projected_pcurve(curve, range, surface, tol) {
4760 Ok((fitted, error, met, worst_off, slop)) => {
4761 if let Some(w) = slop {
4762 effects.push(Effect::Warn(w));
4763 }
4764 if !met {
4765 effects.push(Effect::Warn(format!(
4766 "a projected pcurve fit stopped at {error:.2e}; \
4767 the face's mesh may sit that far off along this edge"
4768 )));
4769 }
4770 effects.push(Effect::Gap(worst_off));
4771 fitted
4772 }
4773 Err(e) => {
4774 effects.push(Effect::Warn(format!(
4775 "no pcurve for an edge on this surface ({e}); the \
4776 face may not triangulate"
4777 )));
4778 return (None, effects);
4779 }
4780 },
4781 };
4782 let off =
4787 ogeom_algo::pcurve_fit::lifted_gap((curve, range), (&found, range), surface, false, tol)
4788 .unwrap_or(0.0);
4789 effects.push(Effect::Gap(off));
4790 (Some(found), effects)
4791}
4792
4793fn trimmed_to(curve: Curve, range: (f64, f64), tol: Tolerances) -> OgeomResult<Curve> {
4794 let (lo, hi) = curve.domain();
4795 if (range.0 - lo).abs() < tol.parametric() && (range.1 - hi).abs() < tol.parametric() {
4796 return Ok(curve);
4797 }
4798 Ok(Curve::from(TrimmedCurve::new(
4799 curve, range.0, range.1, tol,
4800 )?))
4801}
4802
4803use crate::inversion::parameter_on;
4804
4805fn frame_about(origin: Point, axis: Direction, tol: Tolerances) -> OgeomResult<Frame> {
4807 let seed = if axis.vector().dot(Vector::X).abs() < 0.9 {
4808 Vector::X
4809 } else {
4810 Vector::Y
4811 };
4812 let x = Direction::from_cross(axis.vector(), seed, tol)?;
4813 Frame::new(origin, axis, x, tol)
4814}
4815
4816#[cfg(test)]
4817#[allow(clippy::unwrap_used)]
4818mod tests {
4819 use super::super::parse::Value;
4820 use super::*;
4821
4822 const T: Tolerances = Tolerances::millimetres();
4823
4824 fn entity(kind: i64, form: i64, params: Vec<Value>) -> Entity {
4825 Entity {
4826 kind,
4827 form,
4828 transform: 0,
4829 colour: 0,
4830 level: 0,
4831 status: 0,
4832 label: String::new(),
4833 params,
4834 }
4835 }
4836
4837 fn file(entities: Vec<(i64, Entity)>) -> File {
4838 let mut global = vec![Value::Default; 16];
4839 global[13] = Value::Int(2);
4840 File {
4841 global,
4842 entities: entities.into_iter().collect(),
4843 }
4844 }
4845
4846 fn reader(file: &File) -> Reader<'_> {
4847 Reader::new(file, Model::new(), 1.0, &NO_LIBRARY, T)
4848 }
4849
4850 #[test]
4853 fn entities_that_refer_in_a_circle_are_refused() {
4854 let mut turned = entity(124, 0, vec![Value::Real(1.0); 12]);
4855 turned.transform = 1;
4856 let mut line = entity(110, 0, vec![Value::Real(0.0); 6]);
4857 line.transform = 1;
4858 let f = file(vec![(1, turned), (3, line)]);
4859 let mut r = reader(&f);
4860 assert!(r.curve(3).is_err());
4861
4862 let composite = entity(102, 0, vec![Value::Int(1), Value::Int(1)]);
4863 let f = file(vec![(1, composite)]);
4864 let mut r = reader(&f);
4865 assert!(r.curve(1).is_err());
4866 }
4867
4868 fn reals(values: &[f64]) -> Vec<Value> {
4869 values.iter().map(|v| Value::Real(*v)).collect()
4870 }
4871
4872 fn line(from: [f64; 3], to: [f64; 3]) -> Entity {
4873 entity(
4874 110,
4875 0,
4876 reals(&[from[0], from[1], from[2], to[0], to[1], to[2]]),
4877 )
4878 }
4879
4880 #[test]
4885 fn hyperbola_and_parabola_conic_arcs_read_as_their_curves() {
4886 let (cosh1, sinh1) = (1.0_f64.cosh(), 1.0_f64.sinh());
4887 let hyperbola = |forward: bool| {
4889 let (s, e) = if forward {
4890 ([2.0, 0.0], [2.0 * cosh1, 3.0 * sinh1])
4891 } else {
4892 ([2.0 * cosh1, 3.0 * sinh1], [2.0, 0.0])
4893 };
4894 entity(
4895 104,
4896 2,
4897 reals(&[
4898 0.25,
4899 0.0,
4900 -1.0 / 9.0,
4901 0.0,
4902 0.0,
4903 -1.0,
4904 0.0,
4905 s[0],
4906 s[1],
4907 e[0],
4908 e[1],
4909 ]),
4910 )
4911 };
4912 let parabola_y = entity(
4914 104,
4915 3,
4916 reals(&[1.0, 0.0, 0.0, 0.0, -4.0, 0.0, 0.0, 0.0, 0.0, 4.0, 4.0]),
4917 );
4918 let parabola_x = entity(
4919 104,
4920 3,
4921 reals(&[0.0, 0.0, 1.0, -4.0, 0.0, 0.0, 0.0, 4.0, 4.0, 0.0, 0.0]),
4922 );
4923 let deck = file(vec![
4924 (1, hyperbola(true)),
4925 (3, hyperbola(false)),
4926 (5, parabola_y),
4927 (7, parabola_x),
4928 ]);
4929 let mut reader = reader(&deck);
4930 for de in [1, 3] {
4931 let (curve, (t0, t1)) = reader.curve(de).unwrap();
4932 assert!(matches!(curve, Curve::Hyperbola(_)), "D{de} is a hyperbola");
4933 assert!(t1 > t0, "the arc runs forward: {t0} .. {t1}");
4934 let entity = deck.entity(de).unwrap();
4935 let start = Point::new(entity.at(7).real(), entity.at(8).real(), 0.0);
4936 let end = Point::new(entity.at(9).real(), entity.at(10).real(), 0.0);
4937 assert!(curve.point_at(t0, T).unwrap().distance(start) < 1e-9);
4938 assert!(curve.point_at(t1, T).unwrap().distance(end) < 1e-9);
4939 let mid = curve.point_at(f64::midpoint(t0, t1), T).unwrap();
4940 assert!((mid.x * mid.x / 4.0 - mid.y * mid.y / 9.0 - 1.0).abs() < 1e-9);
4941 }
4942 for (de, along_y) in [(5, true), (7, false)] {
4943 let (curve, (t0, t1)) = reader.curve(de).unwrap();
4944 assert!(matches!(curve, Curve::Parabola(_)), "D{de} is a parabola");
4945 assert!(t1 > t0);
4946 let (start, end) = if along_y {
4947 (Point::ORIGIN, Point::new(4.0, 4.0, 0.0))
4948 } else {
4949 (Point::new(4.0, 4.0, 0.0), Point::ORIGIN)
4950 };
4951 assert!(curve.point_at(t0, T).unwrap().distance(start) < 1e-9);
4952 assert!(curve.point_at(t1, T).unwrap().distance(end) < 1e-9);
4953 let mid = curve.point_at(f64::midpoint(t0, t1), T).unwrap();
4954 let residual = if along_y {
4955 mid.x * mid.x - 4.0 * mid.y
4956 } else {
4957 mid.y * mid.y - 4.0 * mid.x
4958 };
4959 assert!(residual.abs() < 1e-9, "on the parabola: {mid:?}");
4960 }
4961 }
4962
4963 #[test]
4966 fn a_ruled_surface_reads_as_the_patch_between_its_curves() {
4967 let deck = file(vec![
4968 (1, line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])),
4969 (3, line([0.0, 5.0, 2.0], [10.0, 5.0, 2.0])),
4970 (
4971 5,
4972 entity(
4973 118,
4974 1,
4975 vec![Value::Int(1), Value::Int(3), Value::Int(0), Value::Int(0)],
4976 ),
4977 ),
4978 (
4979 7,
4980 entity(
4981 118,
4982 1,
4983 vec![Value::Int(1), Value::Int(3), Value::Int(1), Value::Int(0)],
4984 ),
4985 ),
4986 ]);
4987 let mut reader = reader(&deck);
4988 let straight = reader.surface(5).unwrap();
4989 let turned = reader.surface(7).unwrap();
4990 use ogeom_geom::Surface as _;
4991 let middle = straight.point_at(0.5, 0.5, T).unwrap();
4992 assert!(
4993 middle.distance(Point::new(5.0, 2.5, 1.0)) < 1e-9,
4994 "{middle:?}"
4995 );
4996 let far = straight.point_at(0.0, 1.0, T).unwrap();
4997 assert!(far.distance(Point::new(0.0, 5.0, 2.0)) < 1e-9, "{far:?}");
4998 let far = turned.point_at(0.0, 1.0, T).unwrap();
4999 assert!(far.distance(Point::new(10.0, 5.0, 2.0)) < 1e-9, "{far:?}");
5000 }
5001
5002 #[test]
5006 fn offset_entities_displace_the_way_the_file_says() {
5007 let deck = file(vec![
5008 (1, entity(108, 0, reals(&[0.0, 0.0, 1.0, 0.0]))),
5009 (
5010 3,
5011 entity(
5012 140,
5013 0,
5014 vec![
5015 Value::Real(0.0),
5016 Value::Real(0.0),
5017 Value::Real(1.0),
5018 Value::Real(3.0),
5019 Value::Int(1),
5020 ],
5021 ),
5022 ),
5023 (
5024 5,
5025 entity(
5026 140,
5027 0,
5028 vec![
5029 Value::Real(0.0),
5030 Value::Real(0.0),
5031 Value::Real(-1.0),
5032 Value::Real(3.0),
5033 Value::Int(1),
5034 ],
5035 ),
5036 ),
5037 (7, line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])),
5038 (
5039 9,
5040 entity(
5041 130,
5042 0,
5043 vec![
5044 Value::Int(7),
5045 Value::Int(1),
5046 Value::Int(0),
5047 Value::Int(3),
5048 Value::Int(0),
5049 Value::Real(2.0),
5050 Value::Real(0.0),
5051 Value::Real(2.0),
5052 Value::Real(0.0),
5053 Value::Real(0.0),
5054 Value::Real(0.0),
5055 Value::Real(1.0),
5056 Value::Real(0.0),
5057 Value::Real(10.0),
5058 ],
5059 ),
5060 ),
5061 ]);
5062 let mut reader = reader(&deck);
5063 use ogeom_geom::Surface as _;
5064 let up = reader.surface(3).unwrap();
5065 assert!((up.point_at(1.0, 2.0, T).unwrap().z - 3.0).abs() < 1e-9);
5066 let down = reader.surface(5).unwrap();
5067 assert!((down.point_at(1.0, 2.0, T).unwrap().z + 3.0).abs() < 1e-9);
5068 let (offset, (t0, t1)) = reader.curve(9).unwrap();
5069 assert!(matches!(offset, Curve::Offset(_)));
5070 assert!((t0, t1) == (0.0, 10.0));
5071 let at = offset.point_at(5.0, T).unwrap();
5072 assert!(at.distance(Point::new(5.0, 2.0, 0.0)) < 1e-9, "{at:?}");
5073 }
5074
5075 #[test]
5078 fn a_rotated_conic_reads_as_its_turned_curve() {
5079 let theta = 30.0_f64.to_radians();
5080 let (sin, cos) = theta.sin_cos();
5081 let (a2, b2) = (16.0, 4.0);
5082 let coefficients = [
5083 cos * cos / a2 + sin * sin / b2,
5084 2.0 * sin * cos * (1.0 / a2 - 1.0 / b2),
5085 sin * sin / a2 + cos * cos / b2,
5086 0.0,
5087 0.0,
5088 -1.0,
5089 ];
5090 let start = [4.0 * cos, 4.0 * sin];
5091 let end = [-2.0 * sin, 2.0 * cos];
5092 let mut values = coefficients.to_vec();
5093 values.extend([0.0, start[0], start[1], end[0], end[1]]);
5094 let deck = file(vec![(1, entity(104, 1, reals(&values)))]);
5095 let mut reader = reader(&deck);
5096 let (curve, (t0, t1)) = reader.curve(1).unwrap();
5097 assert!(matches!(curve, Curve::Ellipse(_)), "a turned ellipse");
5098 assert!(
5099 curve
5100 .point_at(t0, T)
5101 .unwrap()
5102 .distance(Point::new(start[0], start[1], 0.0))
5103 < 1e-9
5104 );
5105 assert!(
5106 curve
5107 .point_at(t1, T)
5108 .unwrap()
5109 .distance(Point::new(end[0], end[1], 0.0))
5110 < 1e-9
5111 );
5112 for k in 0..=8 {
5113 let p = curve
5114 .point_at(t0 + (t1 - t0) * f64::from(k) / 8.0, T)
5115 .unwrap();
5116 let [a, b, c, _, _, f] = coefficients;
5117 let residual = a * p.x * p.x + b * p.x * p.y + c * p.y * p.y + f;
5118 assert!(residual.abs() < 1e-9, "on the conic: {p:?}");
5119 }
5120 }
5121
5122 #[test]
5126 fn a_linearly_varying_offset_curve_reads_as_its_fitted_displacement() {
5127 let deck = file(vec![
5128 (1, line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])),
5129 (
5130 3,
5131 entity(
5132 130,
5133 0,
5134 vec![
5135 Value::Int(1),
5136 Value::Int(2),
5137 Value::Int(0),
5138 Value::Int(0),
5139 Value::Int(0),
5140 Value::Real(1.0),
5141 Value::Real(0.0),
5142 Value::Real(3.0),
5143 Value::Real(10.0),
5144 Value::Real(0.0),
5145 Value::Real(0.0),
5146 Value::Real(1.0),
5147 Value::Real(0.0),
5148 Value::Real(0.0),
5149 ],
5150 ),
5151 ),
5152 ]);
5153 let mut reader = reader(&deck);
5154 let (curve, (t0, t1)) = reader.curve(3).unwrap();
5155 for k in 0..=10 {
5156 let t = t0 + (t1 - t0) * f64::from(k) / 10.0;
5157 let p = curve.point_at(t, T).unwrap();
5158 let want = Point::new(p.x, 1.0 + 2.0 * p.x / 10.0, 0.0);
5159 assert!(p.distance(want) < 1e-5, "{p:?} against {want:?}");
5160 }
5161 assert!(
5162 curve
5163 .point_at(t0, T)
5164 .unwrap()
5165 .distance(Point::new(0.0, 1.0, 0.0))
5166 < 1e-5
5167 );
5168 assert!(
5169 curve
5170 .point_at(t1, T)
5171 .unwrap()
5172 .distance(Point::new(10.0, 3.0, 0.0))
5173 < 1e-5
5174 );
5175 }
5176
5177 #[test]
5181 fn a_varying_offset_of_a_many_span_base_holds_between_the_samples() {
5182 use ogeom_geom::Curve3d as _;
5183 const SPANS: usize = 400;
5184 let n = SPANS + 3;
5185 let mut base = vec![
5186 Value::Int(i64::try_from(n - 1).unwrap()),
5187 Value::Int(3),
5188 Value::Int(1),
5189 Value::Int(0),
5190 Value::Int(1),
5191 Value::Int(0),
5192 ];
5193 let mut knots = vec![0.0; 4];
5194 #[allow(clippy::cast_precision_loss)]
5195 knots.extend((1..SPANS).map(|i| i as f64 / SPANS as f64));
5196 knots.extend([1.0; 4]);
5197 base.extend(reals(&knots));
5198 base.extend(reals(&vec![1.0; n]));
5199 for i in 0..n {
5200 #[allow(clippy::cast_precision_loss)]
5201 let x = 10.0 * i as f64 / (n - 1) as f64;
5202 base.extend(reals(&[
5203 x,
5204 if i.is_multiple_of(2) { 5e-4 } else { -5e-4 },
5205 0.0,
5206 ]));
5207 }
5208 base.extend(reals(&[0.0, 1.0, 0.0, 0.0, 1.0]));
5209 let deck = file(vec![(1, entity(126, 0, base)), (3, entity(130, 0, vec![]))]);
5210 let mut reader = reader(&deck);
5211 let (basis, range) = reader.curve(1).unwrap();
5212 let length = ogeom_algo::curve_length(&basis, range, T).unwrap();
5213 let offset = entity(
5214 130,
5215 0,
5216 vec![
5217 Value::Int(1),
5218 Value::Int(2),
5219 Value::Int(0),
5220 Value::Int(0),
5221 Value::Int(0),
5222 Value::Real(0.05),
5223 Value::Real(0.0),
5224 Value::Real(0.1),
5225 Value::Real(length),
5226 Value::Real(0.0),
5227 Value::Real(0.0),
5228 Value::Real(1.0),
5229 Value::Real(0.0),
5230 Value::Real(0.0),
5231 ],
5232 );
5233 let deck = file(vec![(1, deck.entities[&1].clone()), (3, offset)]);
5234 let mut reader = super::tests::reader(&deck);
5235 let (curve, (t0, t1)) = reader.curve(3).unwrap();
5236 let mut worst = 0.0_f64;
5237 let mut along = 0.0;
5238 let mut last = t0;
5239 for k in 0..=4000 {
5240 let t = t0 + (t1 - t0) * f64::from(k) / 4000.0;
5241 along += ogeom_algo::curve_length(&basis, (last, t), T).unwrap();
5242 last = t;
5243 let d = 0.05 + 0.05 * along / length;
5244 let side = Vector::Z.cross(basis.d1_at(t, T).unwrap());
5245 let want = basis.point_at(t, T).unwrap() + side * (d / side.magnitude());
5246 worst = worst.max(curve.point_at(t, T).unwrap().distance(want));
5247 }
5248 assert!(
5249 worst < 1e-4,
5250 "the offset strays {worst} between its samples: {:?}",
5251 reader.report.warnings
5252 );
5253 }
5254
5255 #[test]
5260 fn a_parametric_spline_surface_reads_as_its_exact_bspline() {
5261 let patch = |x: &[(usize, f64)], y: &[(usize, f64)], z: &[(usize, f64)]| -> Vec<f64> {
5262 let mut out = vec![0.0; 48];
5263 for (axis, terms) in [x, y, z].iter().enumerate() {
5264 for &(k, v) in *terms {
5265 out[16 * axis + k] = v;
5266 }
5267 }
5268 out
5269 };
5270 let header = |m: i64, n: i64, tu: &[f64], tv: &[f64]| -> Vec<Value> {
5271 let mut out = vec![Value::Int(3), Value::Int(0), Value::Int(m), Value::Int(n)];
5272 out.extend(reals(tu));
5273 out.extend(reals(tv));
5274 out
5275 };
5276 let whole = {
5278 let mut v = header(1, 1, &[0.0, 2.0], &[0.0, 3.0]);
5279 v.extend(reals(&patch(&[(1, 1.0)], &[(4, 1.0)], &[(5, 1.0)])));
5280 v.extend(reals(&[0.0; 48]));
5281 v.extend(reals(&[0.0; 96]));
5282 v
5283 };
5284 let split = {
5287 let mut v = header(2, 1, &[0.0, 1.0, 2.0], &[0.0, 3.0]);
5288 v.extend(reals(&patch(&[(1, 1.0)], &[(4, 1.0)], &[(5, 1.0)])));
5289 v.extend(reals(&[0.0; 48]));
5290 v.extend(reals(&patch(
5291 &[(0, 1.0), (1, 1.0)],
5292 &[(4, 1.0)],
5293 &[(4, 1.0), (5, 1.0)],
5294 )));
5295 v.extend(reals(&[0.0; 48]));
5296 v.extend(reals(&[0.0; 96]));
5297 v
5298 };
5299 let deck = file(vec![(1, entity(114, 0, whole)), (3, entity(114, 0, split))]);
5300 let mut reader = reader(&deck);
5301 for de in [1, 3] {
5302 let surface = reader.surface(de).unwrap();
5303 for (u, v) in [(0.0, 0.0), (0.5, 1.0), (1.0, 2.5), (1.5, 0.7), (2.0, 3.0)] {
5304 let p = ogeom_geom::Surface::point_at(&surface, u, v, T).unwrap();
5305 let want = Point::new(u, v, u * v);
5306 assert!(p.distance(want) < 1e-12, "D{de} at ({u}, {v}): {p:?}");
5307 }
5308 }
5309 }
5310
5311 #[test]
5316 fn a_parameter_space_trim_lifts_through_its_bspline_surface() {
5317 let surface = entity(
5319 128,
5320 0,
5321 vec![
5322 Value::Int(1),
5323 Value::Int(1),
5324 Value::Int(1),
5325 Value::Int(1),
5326 Value::Int(0),
5327 Value::Int(0),
5328 Value::Int(1),
5329 Value::Int(0),
5330 Value::Int(0),
5331 Value::Real(0.0),
5332 Value::Real(0.0),
5333 Value::Real(1.0),
5334 Value::Real(1.0),
5335 Value::Real(0.0),
5336 Value::Real(0.0),
5337 Value::Real(1.0),
5338 Value::Real(1.0),
5339 Value::Real(1.0),
5340 Value::Real(1.0),
5341 Value::Real(1.0),
5342 Value::Real(1.0),
5343 Value::Real(0.0),
5344 Value::Real(0.0),
5345 Value::Real(0.0),
5346 Value::Real(10.0),
5347 Value::Real(0.0),
5348 Value::Real(0.0),
5349 Value::Real(0.0),
5350 Value::Real(10.0),
5351 Value::Real(0.0),
5352 Value::Real(10.0),
5353 Value::Real(10.0),
5354 Value::Real(0.0),
5355 Value::Real(0.0),
5356 Value::Real(1.0),
5357 Value::Real(0.0),
5358 Value::Real(1.0),
5359 ],
5360 );
5361 let on_surface = |model_curve: i64| {
5362 entity(
5363 142,
5364 0,
5365 vec![
5366 Value::Int(0),
5367 Value::Int(1),
5368 Value::Int(3),
5369 Value::Int(model_curve),
5370 Value::Int(0),
5371 ],
5372 )
5373 };
5374 let deck = file(vec![
5375 (1, surface),
5376 (3, line([0.2, 0.2, 0.0], [0.8, 0.8, 0.0])),
5377 (5, on_surface(0)),
5378 (7, line([2.0, 2.0, 0.0], [8.0, 8.0, 0.0])),
5379 (9, on_surface(7)),
5380 ]);
5381 let mut reader = reader(&deck);
5382 for de in [5, 9] {
5383 let segments = reader.boundary_segments(de).unwrap();
5384 let [(curve, (t0, t1))] = segments.as_slice() else {
5385 panic!("one segment");
5386 };
5387 assert!(
5388 curve
5389 .point_at(*t0, T)
5390 .unwrap()
5391 .distance(Point::new(2.0, 2.0, 0.0))
5392 < 1e-6
5393 );
5394 assert!(
5395 curve
5396 .point_at(*t1, T)
5397 .unwrap()
5398 .distance(Point::new(8.0, 8.0, 0.0))
5399 < 1e-6
5400 );
5401 let mid = curve.point_at(f64::midpoint(*t0, *t1), T).unwrap();
5402 assert!(
5403 (mid.x - mid.y).abs() < 1e-6 && mid.z.abs() < 1e-9,
5404 "D{de} {mid:?}"
5405 );
5406 }
5407 }
5408
5409 #[test]
5413 fn a_many_cornered_parameter_space_trim_keeps_its_corners() {
5414 let mut patch = vec![
5416 Value::Int(1),
5417 Value::Int(1),
5418 Value::Int(1),
5419 Value::Int(1),
5420 Value::Int(0),
5421 Value::Int(0),
5422 Value::Int(1),
5423 Value::Int(0),
5424 Value::Int(0),
5425 ];
5426 patch.extend(reals(&[0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0]));
5427 patch.extend(reals(&[1.0; 4]));
5428 patch.extend(reals(&[
5429 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 10.0, 0.0, 10.0, 10.0, 0.0,
5430 ]));
5431 patch.extend(reals(&[0.0, 1.0, 0.0, 1.0]));
5432 const CORNERS: usize = 301;
5434 let corner = |i: usize| -> [f64; 2] {
5435 #[allow(clippy::cast_precision_loss)]
5436 let f = i as f64 / (CORNERS - 1) as f64;
5437 [0.1 + 0.8 * f, if i.is_multiple_of(2) { 0.49 } else { 0.51 }]
5438 };
5439 let mut zigzag = vec![
5440 Value::Int(i64::try_from(CORNERS - 1).unwrap()),
5441 Value::Int(1),
5442 Value::Int(1),
5443 Value::Int(0),
5444 Value::Int(1),
5445 Value::Int(0),
5446 ];
5447 let mut knots = vec![0.0];
5448 #[allow(clippy::cast_precision_loss)]
5449 knots.extend((0..CORNERS).map(|i| i as f64 / (CORNERS - 1) as f64));
5450 knots.push(1.0);
5451 zigzag.extend(reals(&knots));
5452 zigzag.extend(reals(&[1.0; CORNERS]));
5453 for i in 0..CORNERS {
5454 let [u, v] = corner(i);
5455 zigzag.extend(reals(&[u, v, 0.0]));
5456 }
5457 zigzag.extend(reals(&[0.0, 1.0, 0.0, 0.0, 1.0]));
5458 let deck = file(vec![
5459 (1, entity(128, 0, patch)),
5460 (3, entity(126, 0, zigzag)),
5461 (
5462 5,
5463 entity(
5464 142,
5465 0,
5466 vec![
5467 Value::Int(0),
5468 Value::Int(1),
5469 Value::Int(3),
5470 Value::Int(0),
5471 Value::Int(0),
5472 ],
5473 ),
5474 ),
5475 ]);
5476 let mut reader = reader(&deck);
5477 let segments = reader.boundary_segments(5).unwrap();
5478 let [(curve, (t0, t1))] = segments.as_slice() else {
5479 panic!("one segment");
5480 };
5481 let fine: Vec<Point> = (0..=60_000)
5483 .map(|k| {
5484 let t = t0 + (t1 - t0) * f64::from(k) / 60_000.0;
5485 curve.point_at(t, T).unwrap()
5486 })
5487 .collect();
5488 let mut worst = 0.0_f64;
5489 for i in (0..CORNERS).step_by(7) {
5490 let [u, v] = corner(i);
5491 let want = Point::new(10.0 * u, 10.0 * v, 0.0);
5492 let near = fine
5493 .iter()
5494 .map(|p| p.distance(want))
5495 .fold(f64::INFINITY, f64::min);
5496 worst = worst.max(near);
5497 }
5498 assert!(
5499 worst < 1e-6,
5500 "a corner lost by {worst}: {:?}",
5501 reader.report.warnings
5502 );
5503 }
5504
5505 #[test]
5510 fn a_parameter_space_trim_lifts_through_an_analytic_surface() {
5511 let point = |p: [f64; 3]| entity(116, 0, reals(&p));
5512 let direction = |d: [f64; 3]| entity(123, 0, reals(&d));
5513 let on_surface = |surface: i64| {
5514 entity(
5515 142,
5516 0,
5517 vec![
5518 Value::Int(0),
5519 Value::Int(surface),
5520 Value::Int(3),
5521 Value::Int(0),
5522 Value::Int(0),
5523 ],
5524 )
5525 };
5526 type Case = (Entity, [f64; 2], [f64; 2], Point, Point);
5528 let cases: Vec<Case> = vec![
5529 (
5530 entity(
5532 192,
5533 1,
5534 vec![
5535 Value::Int(11),
5536 Value::Int(13),
5537 Value::Real(2.0),
5538 Value::Int(15),
5539 ],
5540 ),
5541 [0.0, 0.0],
5542 [90.0, 5.0],
5543 Point::new(0.0, 2.0, 0.0),
5544 Point::new(-2.0, 0.0, 5.0),
5545 ),
5546 (
5547 entity(
5549 196,
5550 1,
5551 vec![
5552 Value::Int(11),
5553 Value::Real(3.0),
5554 Value::Int(13),
5555 Value::Int(15),
5556 ],
5557 ),
5558 [0.0, 0.0],
5559 [90.0, 45.0],
5560 Point::new(0.0, 3.0, 0.0),
5561 Point::new(
5562 -3.0 * core::f64::consts::FRAC_1_SQRT_2,
5563 0.0,
5564 3.0 * core::f64::consts::FRAC_1_SQRT_2,
5565 ),
5566 ),
5567 (
5568 entity(
5572 198,
5573 1,
5574 vec![
5575 Value::Int(11),
5576 Value::Int(13),
5577 Value::Real(5.0),
5578 Value::Real(1.0),
5579 Value::Int(15),
5580 ],
5581 ),
5582 [0.0, 360.0],
5583 [90.0, 270.0],
5584 Point::new(0.0, 6.0, 0.0),
5585 Point::new(-5.0, 0.0, 1.0),
5586 ),
5587 ];
5588 for (surface, from, to, at_from, at_to) in cases {
5589 let deck = file(vec![
5590 (1, surface),
5591 (3, line([from[0], from[1], 0.0], [to[0], to[1], 0.0])),
5592 (5, on_surface(1)),
5593 (11, point([0.0, 0.0, 0.0])),
5594 (13, direction([0.0, 0.0, 1.0])),
5595 (15, direction([0.0, 1.0, 0.0])),
5596 ]);
5597 let mut reader = reader(&deck);
5598 let segments = reader.boundary_segments(5).unwrap();
5599 let [(curve, (t0, t1))] = segments.as_slice() else {
5600 panic!("one segment");
5601 };
5602 let (a, b) = (
5603 curve.point_at(*t0, T).unwrap(),
5604 curve.point_at(*t1, T).unwrap(),
5605 );
5606 assert!(a.distance(at_from) < 1e-6, "{a:?} against {at_from:?}");
5607 assert!(b.distance(at_to) < 1e-6, "{b:?} against {at_to:?}");
5608 }
5609 }
5610
5611 #[test]
5616 fn copious_data_paths_read_as_their_polylines() {
5617 let path = |flag: i64, count: i64, values: &[f64]| -> Vec<Value> {
5618 let mut out = vec![Value::Int(flag), Value::Int(count)];
5619 out.extend(reals(values));
5620 out
5621 };
5622 let pairs = [2.0, 0.0, 0.0, 3.0, 0.0, 3.0, 4.0];
5623 let cases = [
5624 (11, path(1, 3, &pairs), 7.0, 3),
5625 (63, path(1, 3, &pairs), 12.0, 4),
5626 (
5627 12,
5628 path(2, 3, &[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.0]),
5629 5.0,
5630 2,
5631 ),
5632 ];
5633 for (form, params, length, points) in cases {
5634 let deck = file(vec![(1, entity(106, form, params))]);
5635 let mut r = reader(&deck);
5636 let (curve, range) = r.curve(1).unwrap();
5637 let Curve::BSpline(polyline) = &curve else {
5638 panic!("form {form}: a polyline");
5639 };
5640 assert_eq!(polyline.control_points().len(), points, "form {form}");
5641 let walked = ogeom_algo::curve_length(&curve, range, T).unwrap();
5642 assert!((walked - length).abs() < 1e-12, "form {form}: {walked}");
5643 }
5644 let deck = file(vec![(1, entity(106, 2, path(2, 1, &[0.0, 0.0, 0.0])))]);
5645 let err = reader(&deck).curve(1).unwrap_err();
5646 assert!(err.to_string().contains("a set of points"), "{err}");
5647 }
5648
5649 #[test]
5655 fn a_parameter_space_trim_lifts_through_a_revolved_ellipse() {
5656 let big = 10.0;
5657 let lifted = |rx: f64, ry: f64, t_s: f64, t_e: f64, sign: f64| {
5660 let at = |t: f64| {
5661 if sign > 0.0 {
5662 (rx * t.cos(), big + ry * t.sin())
5663 } else {
5664 (rx * t.cosh(), big + ry * t.sinh())
5665 }
5666 };
5667 let (s, e) = (at(t_s), at(t_e));
5668 let ellipse = entity(
5669 104,
5670 if sign > 0.0 { 1 } else { 2 },
5671 reals(&[
5672 1.0 / (rx * rx),
5673 0.0,
5674 sign / (ry * ry),
5675 0.0,
5676 -2.0 * big * sign / (ry * ry),
5677 big * big * sign / (ry * ry) - 1.0,
5678 0.0,
5679 s.0,
5680 s.1,
5681 e.0,
5682 e.1,
5683 ]),
5684 );
5685 let revolved = entity(
5686 120,
5687 0,
5688 vec![
5689 Value::Int(9),
5690 Value::Int(7),
5691 Value::Real(0.0),
5692 Value::Real(core::f64::consts::FRAC_PI_2),
5693 ],
5694 );
5695 let on_surface = entity(
5696 142,
5697 0,
5698 vec![
5699 Value::Int(0),
5700 Value::Int(1),
5701 Value::Int(3),
5702 Value::Int(0),
5703 Value::Int(0),
5704 ],
5705 );
5706 let (from, to) = ([t_s + 0.2, 0.1], [t_e - 0.3, 1.2]);
5707 let deck = file(vec![
5708 (1, revolved),
5709 (3, line([from[0], from[1], 0.0], [to[0], to[1], 0.0])),
5710 (5, on_surface),
5711 (7, ellipse),
5712 (9, line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])),
5713 ]);
5714 let mut reader = reader(&deck);
5715 let segments = reader.boundary_segments(5)?;
5716 let [(curve, (t0, t1))] = segments.as_slice() else {
5717 panic!("one segment");
5718 };
5719 let ends = (curve.point_at(*t0, T)?, curve.point_at(*t1, T)?);
5720 let expect = |t: f64, phi: f64| {
5722 let (x, y) = at(t);
5723 Point::new(x, y * phi.cos(), y * phi.sin())
5724 };
5725 Ok::<_, ogeom_core::OgeomError>((
5726 ends,
5727 (expect(from[0], from[1]), expect(to[0], to[1])),
5728 ))
5729 };
5730 for (rx, ry, t_s, t_e) in [(3.0, 2.0, 0.5, 2.0), (2.0, 3.0, 4.0, 5.5)] {
5731 let ((a, b), (want_a, want_b)) = lifted(rx, ry, t_s, t_e, 1.0).unwrap();
5732 assert!(
5733 a.distance(want_a) < 1e-9,
5734 "{rx} x {ry}: {a:?} against {want_a:?}"
5735 );
5736 assert!(
5737 b.distance(want_b) < 1e-9,
5738 "{rx} x {ry}: {b:?} against {want_b:?}"
5739 );
5740 }
5741 }
5742
5743 #[test]
5747 fn a_parameter_space_trim_lifts_through_a_swept_surface() {
5748 let on_surface = |surface: i64| {
5749 entity(
5750 142,
5751 0,
5752 vec![
5753 Value::Int(0),
5754 Value::Int(surface),
5755 Value::Int(3),
5756 Value::Int(0),
5757 Value::Int(0),
5758 ],
5759 )
5760 };
5761 let tabulated = entity(
5764 122,
5765 0,
5766 vec![
5767 Value::Int(7),
5768 Value::Real(0.0),
5769 Value::Real(0.0),
5770 Value::Real(4.0),
5771 ],
5772 );
5773 let revolved = entity(
5777 120,
5778 0,
5779 vec![
5780 Value::Int(9),
5781 Value::Int(7),
5782 Value::Real(0.0),
5783 Value::Real(core::f64::consts::FRAC_PI_2),
5784 ],
5785 );
5786 let cases = [
5787 (
5788 tabulated,
5789 line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0]),
5790 [0.5, 0.0],
5791 [0.5, 1.0],
5792 Point::new(5.0, 0.0, 0.0),
5793 Point::new(5.0, 0.0, 4.0),
5794 ),
5795 (
5796 revolved,
5797 line([2.0, 0.0, 0.0], [2.0, 0.0, 6.0]),
5798 [0.0, 0.0],
5799 [1.0, core::f64::consts::FRAC_PI_2],
5800 Point::new(2.0, 0.0, 0.0),
5801 Point::new(0.0, 2.0, 6.0),
5802 ),
5803 ];
5804 for (surface, curve, from, to, at_from, at_to) in cases {
5805 let deck = file(vec![
5806 (1, surface),
5807 (3, line([from[0], from[1], 0.0], [to[0], to[1], 0.0])),
5808 (5, on_surface(1)),
5809 (7, curve),
5810 (9, line([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])),
5811 ]);
5812 let mut reader = reader(&deck);
5813 let segments = reader.boundary_segments(5).unwrap();
5814 let [(lifted, (t0, t1))] = segments.as_slice() else {
5815 panic!("one segment");
5816 };
5817 let (a, b) = (
5818 lifted.point_at(*t0, T).unwrap(),
5819 lifted.point_at(*t1, T).unwrap(),
5820 );
5821 assert!(a.distance(at_from) < 1e-6, "{a:?} against {at_from:?}");
5822 assert!(b.distance(at_to) < 1e-6, "{b:?} against {at_to:?}");
5823 }
5824 }
5825
5826 #[test]
5830 fn a_model_space_dimension_reads_as_its_callout_and_value() {
5831 let note = |text: &str| {
5832 let mut e = entity(
5833 212,
5834 0,
5835 vec![
5836 Value::Int(1),
5837 Value::Int(5),
5838 Value::Real(10.0),
5839 Value::Real(3.0),
5840 Value::Int(1),
5841 Value::Real(0.0),
5842 Value::Real(0.0),
5843 Value::Int(0),
5844 Value::Int(0),
5845 Value::Real(10.0),
5846 Value::Real(12.0),
5847 Value::Real(0.0),
5848 Value::Text(text.to_owned()),
5849 ],
5850 );
5851 e.status = 10_000;
5852 e
5853 };
5854 let leader = |head: [f64; 2], tail: [f64; 2]| {
5855 let mut e = entity(
5856 214,
5857 1,
5858 reals(&[1.0, 1.0, 0.5, 0.0, head[0], head[1], tail[0], tail[1]]),
5859 );
5860 e.status = 10_000;
5861 e
5862 };
5863 let witness = |a: [f64; 2], b: [f64; 2]| {
5864 let mut e = entity(106, 40, reals(&[1.0, 2.0, 0.0, a[0], a[1], b[0], b[1]]));
5865 e.status = 10_000;
5866 e
5867 };
5868 let dimension = entity(
5869 216,
5870 0,
5871 vec![
5872 Value::Int(3),
5873 Value::Int(5),
5874 Value::Int(7),
5875 Value::Int(9),
5876 Value::Int(11),
5877 ],
5878 );
5879 let mut sheet_note = note("on the sheet");
5880 sheet_note.status = 0;
5881 let drawing = entity(404, 0, vec![Value::Int(0), Value::Int(1), Value::Int(13)]);
5882 let deck = file(vec![
5883 (1, dimension),
5884 (3, note("2X 25.40")),
5885 (5, leader([0.0, 11.0], [8.0, 11.0])),
5886 (7, leader([25.4, 11.0], [17.0, 11.0])),
5887 (9, witness([0.0, 0.0], [0.0, 12.0])),
5888 (11, witness([25.4, 0.0], [25.4, 12.0])),
5889 (13, sheet_note),
5890 (15, drawing),
5891 ]);
5892 let mut reader = reader(&deck);
5893 let (callouts, dimensions) = reader.annotations();
5894 assert!(callouts.is_empty(), "the sheet's note stays on the sheet");
5895 let [(callout, dimension)] = dimensions.as_slice() else {
5896 panic!("one dimension, found {}", dimensions.len());
5897 };
5898 assert_eq!(callout.name, "2X 25.40");
5899 assert_eq!(
5900 callout.polylines.len(),
5901 4,
5902 "two leaders and two witness lines"
5903 );
5904 assert!(
5905 (dimension.values[0] - 25.4).abs() < 1e-12,
5906 "{:?}",
5907 dimension.values
5908 );
5909 assert!(dimension.location);
5910 assert!(!reader.visited.contains_key(&13));
5911 }
5912
5913 #[test]
5918 fn a_dimension_s_matrix_carries_its_leaders() {
5919 let leader = |status: i64, head: [f64; 2], tail: [f64; 2]| {
5920 let mut e = entity(
5921 214,
5922 1,
5923 reals(&[1.0, 1.0, 0.5, 0.0, head[0], head[1], tail[0], tail[1]]),
5924 );
5925 e.status = status;
5926 e
5927 };
5928 let mut dimension = entity(
5929 216,
5930 0,
5931 vec![
5932 Value::Int(0),
5933 Value::Int(3),
5934 Value::Int(5),
5935 Value::Int(0),
5936 Value::Int(0),
5937 ],
5938 );
5939 dimension.transform = 7;
5940 let mut lift = entity(
5941 124,
5942 0,
5943 reals(&[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 5.0]),
5944 );
5945 lift.status = 10_000;
5946 let deck = file(vec![
5947 (1, dimension),
5948 (3, leader(10_000, [0.0, 11.0], [8.0, 11.0])),
5949 (5, leader(20_000, [25.4, 11.0], [17.0, 11.0])),
5950 (7, lift),
5951 ]);
5952 let mut reader = reader(&deck);
5953 let (callouts, dimensions) = reader.annotations();
5954 assert!(dimensions.is_empty() && callouts.len() == 1);
5955 let at = |x: f64, y: f64, z: f64| Point::new(x, y, z);
5956 assert_eq!(
5957 callouts[0].polylines,
5958 vec![
5959 vec![at(0.0, 11.0, 5.0), at(8.0, 11.0, 5.0)],
5960 vec![at(25.4, 11.0, 0.0), at(17.0, 11.0, 0.0)],
5961 ]
5962 );
5963 }
5964
5965 #[test]
5969 fn subfigure_instances_place_their_definition() {
5970 let dependent = |mut e: Entity| {
5971 e.status = 10_000;
5972 e
5973 };
5974 let square = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
5975 let mut entities = vec![(1, dependent(entity(108, 0, reals(&[0.0, 0.0, 1.0, 0.0]))))];
5976 let mut sides = Vec::new();
5977 for i in 0..4 {
5978 let (a, b) = (square[i], square[(i + 1) % 4]);
5979 let de = 3 + 2 * i64::try_from(i).unwrap();
5980 entities.push((de, dependent(line([a[0], a[1], 0.0], [b[0], b[1], 0.0]))));
5981 sides.push(Value::Int(de));
5982 }
5983 let mut composite = vec![Value::Int(4)];
5984 composite.extend(sides);
5985 entities.push((11, dependent(entity(102, 0, composite))));
5986 entities.push((
5987 13,
5988 dependent(entity(
5989 144,
5990 0,
5991 vec![Value::Int(1), Value::Int(1), Value::Int(0), Value::Int(11)],
5992 )),
5993 ));
5994 entities.push((
5995 15,
5996 entity(
5997 308,
5998 0,
5999 vec![
6000 Value::Int(0),
6001 Value::Text("tile".to_owned()),
6002 Value::Int(1),
6003 Value::Int(13),
6004 ],
6005 ),
6006 ));
6007 entities.push((
6008 17,
6009 entity(
6010 408,
6011 0,
6012 vec![
6013 Value::Int(15),
6014 Value::Real(100.0),
6015 Value::Real(0.0),
6016 Value::Real(0.0),
6017 Value::Real(1.0),
6018 ],
6019 ),
6020 ));
6021 entities.push((
6022 19,
6023 entity(
6024 408,
6025 0,
6026 vec![
6027 Value::Int(15),
6028 Value::Real(0.0),
6029 Value::Real(50.0),
6030 Value::Real(0.0),
6031 Value::Real(2.0),
6032 ],
6033 ),
6034 ));
6035 let deck = file(entities);
6036 let mut reader = reader(&deck);
6037 let Placed { solids, sheets, .. } = reader.subfigure_instances(T).unwrap();
6038 assert!(solids.is_empty());
6039 assert_eq!(sheets.len(), 2);
6040 let deflection = ogeom_mesh::Deflection::default();
6041 let mut measured: Vec<(f64, Point)> = sheets
6042 .iter()
6043 .map(|sheet| {
6044 let props =
6045 ogeom_algo::surface_properties(&reader.model, sheet, deflection, T).unwrap();
6046 (props.mass, props.centre)
6047 })
6048 .collect();
6049 measured.sort_by(|a, b| a.0.total_cmp(&b.0));
6050 assert!((measured[0].0 - 100.0).abs() < 1e-9, "{measured:?}");
6051 assert!(measured[0].1.distance(Point::new(105.0, 5.0, 0.0)) < 1e-9);
6052 assert!((measured[1].0 - 400.0).abs() < 1e-9, "{measured:?}");
6053 assert!(measured[1].1.distance(Point::new(10.0, 60.0, 0.0)) < 1e-9);
6054 let face_of = |shape: &Shape| {
6056 ogeom_topo::explore(
6057 &reader.model,
6058 shape,
6059 ogeom_topo::Filter::OfType(ogeom_topo::ShapeType::Face),
6060 )
6061 .unwrap()
6062 .remove(0)
6063 };
6064 assert_eq!(face_of(&sheets[0]).node(), face_of(&sheets[1]).node());
6065 }
6066
6067 #[test]
6070 fn levels_and_groups_read_as_layers() {
6071 let square = |z: f64, base: i64| -> Vec<(i64, Entity)> {
6072 let dependent = |mut e: Entity| {
6073 e.status = 10_000;
6074 e
6075 };
6076 let corners = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
6077 let mut out = vec![(base, dependent(entity(108, 0, reals(&[0.0, 0.0, 1.0, z]))))];
6078 let mut sides = vec![Value::Int(4)];
6079 for i in 0..4 {
6080 let (a, b) = (corners[i], corners[(i + 1) % 4]);
6081 let de = base + 2 + 2 * i64::try_from(i).unwrap();
6082 out.push((de, dependent(line([a[0], a[1], z], [b[0], b[1], z]))));
6083 sides.push(Value::Int(de));
6084 }
6085 out.push((base + 10, dependent(entity(102, 0, sides))));
6086 out
6087 };
6088 let mut entities = square(0.0, 1);
6089 entities.extend(square(5.0, 21));
6090 let mut low = entity(
6091 144,
6092 0,
6093 vec![Value::Int(1), Value::Int(1), Value::Int(0), Value::Int(11)],
6094 );
6095 low.level = 3;
6096 let mut high = entity(
6097 144,
6098 0,
6099 vec![Value::Int(21), Value::Int(1), Value::Int(0), Value::Int(31)],
6100 );
6101 high.level = 7;
6102 entities.push((41, low));
6103 entities.push((43, high));
6104 let mut group = entity(402, 7, vec![Value::Int(1), Value::Int(43)]);
6105 group.label = "LID".to_owned();
6106 entities.push((45, group));
6107 let deck = file(entities);
6108 let mut reader = reader(&deck);
6109 let low_face = reader.face(41).unwrap();
6110 let high_face = reader.face(43).unwrap();
6111 let built = vec![(41, low_face.clone()), (43, high_face.clone())];
6112 let mut document = ogeom_doc::Document::over(std::mem::take(&mut reader.model));
6113 let mut report = IgesReport::default();
6114 layers(&deck, &mut document, &built, &mut report);
6115 let names_of = |shape: &Shape| -> Vec<String> {
6116 let mut names: Vec<String> = document
6117 .layers_of(shape)
6118 .iter()
6119 .map(|id| document.layer(*id).unwrap().name.clone())
6120 .collect();
6121 names.sort();
6122 names
6123 };
6124 assert_eq!(names_of(&low_face), vec!["level 3".to_owned()]);
6125 assert_eq!(
6126 names_of(&high_face),
6127 vec!["LID".to_owned(), "level 7".to_owned()]
6128 );
6129 }
6130
6131 #[test]
6136 fn constructive_solids_read_as_their_volumes() {
6137 let pi = core::f64::consts::PI;
6138 let square = |base: i64, corners: [[f64; 3]; 4]| -> Vec<(i64, Entity)> {
6139 let mut out = Vec::new();
6140 let mut sides = vec![Value::Int(4)];
6141 for i in 0..4 {
6142 let de = base + 2 * i64::try_from(i).unwrap();
6143 out.push((de, line(corners[i], corners[(i + 1) % 4])));
6144 sides.push(Value::Int(de));
6145 }
6146 out.push((base + 8, entity(102, 0, sides)));
6147 out
6148 };
6149 let mut entities = vec![
6150 (
6152 1,
6153 entity(
6154 150,
6155 0,
6156 reals(&[
6157 20.0, 20.0, 20.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
6158 ]),
6159 ),
6160 ),
6161 (
6163 3,
6164 entity(
6165 154,
6166 0,
6167 reals(&[40.0, 5.0, 10.0, 10.0, -10.0, 0.0, 0.0, 1.0]),
6168 ),
6169 ),
6170 (
6171 5,
6172 entity(
6173 180,
6174 0,
6175 vec![Value::Int(3), Value::Int(-1), Value::Int(-3), Value::Int(3)],
6176 ),
6177 ),
6178 (
6180 7,
6181 entity(
6182 152,
6183 0,
6184 reals(&[
6185 10.0, 5.0, 4.0, 6.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
6186 ]),
6187 ),
6188 ),
6189 (
6191 9,
6192 entity(
6193 156,
6194 0,
6195 reals(&[10.0, 5.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
6196 ),
6197 ),
6198 (
6200 11,
6201 entity(
6202 168,
6203 0,
6204 reals(&[3.0, 2.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
6205 ),
6206 ),
6207 (
6209 13,
6210 entity(
6211 162,
6212 0,
6213 vec![
6214 Value::Int(29),
6215 Value::Real(0.5),
6216 Value::Real(0.0),
6217 Value::Real(0.0),
6218 Value::Real(0.0),
6219 Value::Real(0.0),
6220 Value::Real(0.0),
6221 Value::Real(1.0),
6222 ],
6223 ),
6224 ),
6225 (
6227 15,
6228 entity(
6229 164,
6230 0,
6231 vec![
6232 Value::Int(49),
6233 Value::Real(5.0),
6234 Value::Real(0.0),
6235 Value::Real(0.0),
6236 Value::Real(1.0),
6237 ],
6238 ),
6239 ),
6240 (
6242 17,
6243 entity(
6244 124,
6245 0,
6246 reals(&[1.0, 0.0, 0.0, 100.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0]),
6247 ),
6248 ),
6249 (
6250 19,
6251 entity(
6252 184,
6253 0,
6254 vec![
6255 Value::Int(2),
6256 Value::Int(1),
6257 Value::Int(1),
6258 Value::Int(0),
6259 Value::Int(17),
6260 ],
6261 ),
6262 ),
6263 ];
6264 entities.extend(square(
6265 21,
6266 [
6267 [2.0, 0.0, 0.0],
6268 [4.0, 0.0, 0.0],
6269 [4.0, 0.0, 3.0],
6270 [2.0, 0.0, 3.0],
6271 ],
6272 ));
6273 entities.extend(square(
6274 41,
6275 [
6276 [0.0, 0.0, 0.0],
6277 [2.0, 0.0, 0.0],
6278 [2.0, 2.0, 0.0],
6279 [0.0, 2.0, 0.0],
6280 ],
6281 ));
6282 let deck = file(entities);
6283 let mut reader = reader(&deck);
6284 let volume = |reader: &Reader<'_>, shape: &Shape| {
6285 ogeom_algo::volume_properties(
6286 &reader.model,
6287 shape,
6288 ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
6289 T,
6290 )
6291 .unwrap()
6292 .mass
6293 };
6294 for (de, want, within) in [
6295 (5, 8000.0 - pi * 25.0 * 20.0, 1e-6),
6296 (7, (10.0 + 6.0) / 2.0 * 5.0 * 4.0, 1e-9),
6297 (9, pi * 10.0 / 3.0 * (25.0 + 10.0 + 4.0), 1e-6),
6298 (11, 4.0 / 3.0 * pi * 6.0, 2e-3),
6301 (13, 0.5 * pi * (16.0 - 4.0) * 3.0, 1e-6),
6302 (15, 20.0, 1e-9),
6303 ] {
6304 let solids = reader.csg_solids(de).unwrap();
6305 let [solid] = solids.as_slice() else {
6306 panic!("D{de} is one solid");
6307 };
6308 let got = volume(&reader, solid);
6309 assert!(
6310 (got - want).abs() <= within * want.max(1.0),
6311 "D{de}: {got} against {want}"
6312 );
6313 }
6314 let ellipsoid = reader.csg_solids(11).unwrap().remove(0);
6317 for face in ogeom_topo::explore(
6318 &reader.model,
6319 &ellipsoid,
6320 ogeom_topo::Filter::OfType(ogeom_topo::ShapeType::Face),
6321 )
6322 .unwrap()
6323 {
6324 let data = reader
6325 .model
6326 .node(&face)
6327 .unwrap()
6328 .data()
6329 .as_face()
6330 .unwrap()
6331 .clone();
6332 let surface = reader
6333 .model
6334 .geometry()
6335 .surface(data.surface)
6336 .unwrap()
6337 .clone();
6338 let ((u0, u1), (v0, v1)) = ogeom_geom::Surface::domain(&surface);
6339 for i in 0..=6 {
6340 for j in 0..=6 {
6341 let (u, v) = (
6342 u0 + (u1 - u0) * f64::from(i) / 6.0,
6343 v0 + (v1 - v0) * f64::from(j) / 6.0,
6344 );
6345 let p = ogeom_geom::Surface::point_at(&surface, u, v, T).unwrap();
6346 let residual = p.x * p.x / 9.0 + p.y * p.y / 4.0 + p.z * p.z - 1.0;
6347 assert!(residual.abs() < 1e-9, "off the ellipsoid: {p:?}");
6348 }
6349 }
6350 }
6351 let placed = reader.csg_solids(19).unwrap();
6352 assert_eq!(placed.len(), 2);
6353 let centres: Vec<Point> = placed
6354 .iter()
6355 .map(|s| {
6356 ogeom_algo::volume_properties(
6357 &reader.model,
6358 s,
6359 ogeom_mesh::Deflection::default(),
6360 T,
6361 )
6362 .unwrap()
6363 .centre
6364 })
6365 .collect();
6366 assert!(
6367 centres
6368 .iter()
6369 .any(|c| c.distance(Point::new(10.0, 10.0, 10.0)) < 1e-9)
6370 );
6371 assert!(
6372 centres
6373 .iter()
6374 .any(|c| c.distance(Point::new(110.0, 10.0, 10.0)) < 1e-9)
6375 );
6376 }
6377}