1use super::parse::{Entity, File};
21use ogeom_algo::{make_edge_between, make_solid, make_vertex, sew};
22use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
23use ogeom_geom::Curve3d as _;
24use ogeom_geom::Transformable as _;
25use ogeom_geom::{
26 BSplineCurve, BSplineSurface, CircleCurve, ConeSurface, Curve, CylinderSurface, EllipseCurve,
27 ExtrusionSurface, HyperbolaCurve, LineCurve, OffsetCurve, OffsetSurface, ParabolaCurve,
28 PlaneSurface, RevolutionSurface, SphereSurface, SurfaceGeometry, TorusSurface, TrimmedCurve,
29};
30use ogeom_math::{
31 Circle, Cone, ControlGrid, Cylinder, Direction, Ellipse, Frame, Hyperbola, KnotVector, Matrix3,
32 Parabola, Plane, Point, Sphere, Torus, Transform, Vector, Weighted,
33};
34use ogeom_topo::{Model, Shape};
35use std::collections::{BTreeMap, HashMap};
36
37const SURFACE_EXTENT: f64 = 1e5;
41
42const CSG_KINDS: [i64; 12] = [150, 152, 154, 156, 158, 160, 162, 164, 168, 180, 184, 430];
46
47#[derive(Debug, Default)]
49pub struct IgesReport {
50 pub scale_mm: f64,
52 pub skipped: BTreeMap<String, usize>,
56 pub warnings: Vec<String>,
58}
59
60#[derive(Debug)]
62pub struct IgesImport {
63 pub document: ogeom_doc::Document,
65 pub solids: Vec<Shape>,
68 pub sheets: Vec<Shape>,
70 pub report: IgesReport,
72}
73
74type BuiltEdge = (Shape, Curve, (f64, f64));
76
77pub fn read_iges(text: &str, tol: Tolerances) -> OgeomResult<IgesImport> {
87 let file = super::parse::parse(text)?;
88 let Some(scale_mm) = file.scale_mm() else {
89 ogeom_bail!(
90 Construction,
91 "IGES global section names units this reader cannot convert to millimetres"
92 );
93 };
94 let mut reader = Reader {
95 file: &file,
96 model: Model::new(),
97 report: IgesReport {
98 scale_mm,
99 ..IgesReport::default()
100 },
101 visited: BTreeMap::new(),
102 vertices: HashMap::new(),
103 edges: HashMap::new(),
104 vertex_misses: (0, 0.0),
105 tol,
106 };
107
108 let mut solids: Vec<(i64, Shape)> = Vec::new();
110 let solid_des: Vec<i64> = file
111 .entities
112 .iter()
113 .filter(|(_, e)| e.kind == 186)
114 .map(|(de, _)| *de)
115 .collect();
116 let total = solid_des.len() as u64;
117 for (done, de) in solid_des.into_iter().enumerate() {
118 ogeom_core::progress::checkpoint()?;
119 ogeom_core::progress::stage_at("iges: solid", done as u64 + 1, total);
120 match reader.manifold_solid(de) {
121 Ok(solid) => solids.push((de, solid)),
122 Err(e) => reader
123 .report
124 .warnings
125 .push(format!("D{de}: manifold solid failed to build: {e}")),
126 }
127 }
128
129 let mut faces = Vec::new();
134 let mut built_from: Vec<(i64, Shape)> = solids.clone();
136 let face_des: Vec<i64> = file
137 .entities
138 .iter()
139 .filter(|(de, e)| {
140 matches!(e.kind, 143 | 144)
141 && (e.status / 10_000) % 100 == 0
142 && !reader.visited.contains_key(de)
143 })
144 .map(|(de, _)| *de)
145 .collect();
146 let total = face_des.len() as u64;
147 for (done, de) in face_des.into_iter().enumerate() {
148 ogeom_core::progress::checkpoint()?;
149 ogeom_core::progress::stage_at("iges: face", done as u64 + 1, total);
150 match reader.face(de) {
151 Ok(face) => {
152 built_from.push((de, face.clone()));
153 faces.push(face);
154 }
155 Err(e) => reader
156 .report
157 .warnings
158 .push(format!("D{de}: trimmed surface failed to build: {e}")),
159 }
160 }
161 let mut sheets = Vec::new();
162 if !faces.is_empty() {
163 let sewn = sew(&mut reader.model, &faces, tol)?;
164 for shell in &sewn.shells {
165 if ogeom_algo::is_shell_closed(&reader.model, shell)? {
166 let solid = make_solid(&mut reader.model, std::slice::from_ref(shell))?.shape;
167 solids.push((0, solid));
168 } else {
169 sheets.push(shell.clone());
170 }
171 }
172 }
173
174 let csg_des: Vec<i64> = file
177 .entities
178 .iter()
179 .filter(|(de, e)| {
180 CSG_KINDS.contains(&e.kind)
181 && (e.status / 10_000) % 100 == 0
182 && !reader.visited.contains_key(de)
183 })
184 .map(|(de, _)| *de)
185 .collect();
186 for de in csg_des {
187 match reader.csg_solids(de) {
188 Ok(built) => {
189 for solid in built {
190 built_from.push((de, solid.clone()));
191 solids.push((de, solid));
192 }
193 }
194 Err(e) => reader
195 .report
196 .warnings
197 .push(format!("D{de}: constructive solid failed to build: {e}")),
198 }
199 }
200
201 let (placed_solids, placed_sheets, placed_from) = reader.subfigure_instances(tol)?;
204 solids.extend(placed_solids.into_iter().map(|s| (0, s)));
205 sheets.extend(placed_sheets);
206 built_from.extend(placed_from);
207
208 if solids.is_empty() && sheets.is_empty() {
209 if reader.report.warnings.is_empty() {
212 ogeom_bail!(
213 Construction,
214 "the IGES file contains no manifold solid and no independent \
215 trimmed surface this reader translates"
216 );
217 }
218 ogeom_bail!(
219 Construction,
220 "every shape in the IGES file failed to build: {}",
221 reader.report.warnings.join("; ")
222 );
223 }
224
225 if reader.vertex_misses.0 > 0 {
226 let (count, worst) = reader.vertex_misses;
227 reader.report.warnings.push(format!(
228 "{count} vertices sat off the curve ends they bound, by up to {worst:.2e}; \
229 their tolerances grew to say so"
230 ));
231 }
232 let (callouts, dimensions) = reader.annotations();
233 for (de, entity) in &file.entities {
235 if (entity.kind == 402 && matches!(entity.form, 1 | 7 | 14 | 15))
236 || (entity.kind == 406 && entity.form == 1)
237 {
238 reader.visited.insert(*de, ());
239 }
240 }
241 for (de, entity) in &file.entities {
243 if !reader.visited.contains_key(de) {
244 let key = if entity.form == 0 {
245 format!("type {}", entity.kind)
246 } else {
247 format!("type {} form {}", entity.kind, entity.form)
248 };
249 *reader.report.skipped.entry(key).or_default() += 1;
250 }
251 }
252 let mut document = reader.document(&solids, &sheets);
253 {
254 let pmi = document.pmi_mut();
255 let base = pmi.dimensions.len();
256 for (i, dimension) in dimensions.into_iter().enumerate() {
257 let (callout, dimension) = dimension;
258 pmi.dimensions.push(dimension);
259 let mut callout = callout;
260 callout.annotates = Some(ogeom_doc::Annotated::Dimension(base + i));
261 pmi.callouts.push(callout);
262 }
263 pmi.callouts.extend(callouts);
264 }
265 layers(&file, &mut document, &built_from, &mut reader.report);
266 let solids = solids.into_iter().map(|(_, s)| s).collect();
267 Ok(IgesImport {
268 document,
269 solids,
270 sheets,
271 report: reader.report,
272 })
273}
274
275struct Reader<'a> {
276 file: &'a File,
277 model: Model,
278 report: IgesReport,
279 visited: BTreeMap<i64, ()>,
281 vertices: HashMap<(i64, i64), Shape>,
284 edges: HashMap<(i64, i64), BuiltEdge>,
286 vertex_misses: (usize, f64),
289 tol: Tolerances,
290}
291
292impl<'a> Reader<'a> {
293 fn entity(&mut self, de: i64) -> OgeomResult<&'a Entity> {
294 let Some(entity) = self.file.entity(de) else {
295 ogeom_bail!(Construction, "IGES pointer D{de} names no entity");
296 };
297 self.visited.insert(de.abs(), ());
298 Ok(entity)
299 }
300
301 fn placement(&mut self, entity: &Entity) -> OgeomResult<Transform> {
304 if entity.transform == 0 {
305 return Ok(Transform::IDENTITY);
306 }
307 let de = entity.transform;
308 let t = self.entity(de)?;
309 if t.kind != 124 {
310 ogeom_bail!(
311 Construction,
312 "D{de}: a transformation pointer names a type {} entity",
313 t.kind
314 );
315 }
316 let v = |i: usize| t.at(i).real();
317 let s = self.report.scale_mm;
318 let linear = Matrix3::new([[v(0), v(1), v(2)], [v(4), v(5), v(6)], [v(8), v(9), v(10)]]);
319 let translation = Vector::new(v(3) * s, v(7) * s, v(11) * s);
320 let m = Transform::from_parts(linear, 1.0, translation, self.tol.angular())?;
321 if t.transform != 0 {
322 let outer = self.placement(t)?;
323 return Ok(outer * m);
324 }
325 Ok(m)
326 }
327
328 fn point3(&self, e: &Entity, i: usize) -> Point {
329 let s = self.report.scale_mm;
330 Point::new(
331 e.at(i).real() * s,
332 e.at(i + 1).real() * s,
333 e.at(i + 2).real() * s,
334 )
335 }
336
337 fn curve(&mut self, de: i64) -> OgeomResult<(Curve, (f64, f64))> {
339 let entity = self.entity(de)?;
340 let scale = self.report.scale_mm;
341 let (curve, range) = match entity.kind {
342 110 => {
343 let a = self.point3(entity, 0);
344 let b = self.point3(entity, 3);
345 let line = LineCurve::segment(a, b, self.tol)?;
346 (Curve::from(line), (0.0, a.distance(b)))
347 }
348 100 => {
349 let zt = entity.at(0).real() * scale;
352 let c = Point::new(entity.at(1).real() * scale, entity.at(2).real() * scale, zt);
353 let s = Point::new(entity.at(3).real() * scale, entity.at(4).real() * scale, zt);
354 let e = Point::new(entity.at(5).real() * scale, entity.at(6).real() * scale, zt);
355 let radius = c.distance(s);
356 let x = Direction::new(s - c, self.tol)?;
357 let frame = Frame::new(c, Direction::Z, x, self.tol)?;
358 let circle = Circle::new(frame, radius, self.tol)?;
359 let to_end = e - c;
360 let ang = to_end
361 .dot(frame.y().vector())
362 .atan2(to_end.dot(frame.x().vector()))
363 .rem_euclid(core::f64::consts::TAU);
364 let sweep = if ang <= self.tol.parametric() {
365 core::f64::consts::TAU
366 } else {
367 ang
368 };
369 (Curve::from(CircleCurve::new(circle)), (0.0, sweep))
370 }
371 104 => self.conic(de, entity)?,
372 112 => self.spline_curve(de, entity)?,
373 126 => self.nurbs_curve(de, entity)?,
374 130 => self.offset_curve(de, entity)?,
375 102 => ogeom_bail!(
376 Construction,
377 "D{de}: a composite curve is a sequence, not a curve; the \
378 caller walks its segments"
379 ),
380 kind => ogeom_bail!(
381 Construction,
382 "D{de}: curve entity type {kind}{} is not translated; see \
383 docs/PARITY.md, io.iges",
384 super::entity_name(kind).map_or_else(String::new, |n| format!(" ({n})"))
385 ),
386 };
387 let placement = self.placement(entity)?;
389 if placement == Transform::IDENTITY {
390 Ok((curve, range))
391 } else {
392 Ok((curve.transformed(&placement, self.tol)?, range))
393 }
394 }
395
396 fn conic(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
405 let scale = self.report.scale_mm;
406 let (a, b, c, d, e, f) = (
407 entity.at(0).real(),
408 entity.at(1).real(),
409 entity.at(2).real(),
410 entity.at(3).real(),
411 entity.at(4).real(),
412 entity.at(5).real(),
413 );
414 let zt = entity.at(6).real() * scale;
415 let start = Point::new(entity.at(7).real() * scale, entity.at(8).real() * scale, zt);
416 let end = Point::new(
417 entity.at(9).real() * scale,
418 entity.at(10).real() * scale,
419 zt,
420 );
421 if b.abs() > 1e-12 {
422 let theta = 0.5 * b.atan2(a - c);
423 let (sin, cos) = theta.sin_cos();
424 let turned = [
426 a * cos * cos + b * cos * sin + c * sin * sin,
427 a * sin * sin - b * cos * sin + c * cos * cos,
428 d * cos + e * sin,
429 -d * sin + e * cos,
430 f,
431 ];
432 let about = ogeom_math::Axis::new(Point::ORIGIN, Direction::Z);
433 let back = Transform::rotation(about, theta);
434 let into = Transform::rotation(about, -theta);
435 let (curve, range) =
436 self.axis_aligned_conic(de, turned, zt, into.apply(start), into.apply(end))?;
437 return Ok((curve.transformed(&back, self.tol)?, range));
438 }
439 self.axis_aligned_conic(de, [a, c, d, e, f], zt, start, end)
440 }
441
442 fn axis_aligned_conic(
445 &mut self,
446 de: i64,
447 [a, c, d, e, f]: [f64; 5],
448 zt: f64,
449 start: Point,
450 end: Point,
451 ) -> OgeomResult<(Curve, (f64, f64))> {
452 let scale = self.report.scale_mm;
453 if a != 0.0 && c != 0.0 && (a > 0.0) == (c > 0.0) {
456 let sign = if a > 0.0 { 1.0 } else { -1.0 };
457 return self.ellipse_arc(de, [a, c, d, e, f].map(|k| k * sign), zt, start, end);
458 }
459 if a != 0.0 && c != 0.0 {
460 return Self::hyperbola_arc(de, [a, c, d, e, f], zt, start, end, scale, self.tol);
461 }
462 if (a == 0.0) != (c == 0.0) {
463 return Self::parabola_arc(de, [a, c, d, e, f], zt, start, end, scale, self.tol);
464 }
465 ogeom_bail!(Construction, "D{de}: conic arc coefficients close no conic")
466 }
467
468 fn ellipse_arc(
471 &mut self,
472 de: i64,
473 [a, c, d, e, f]: [f64; 5],
474 zt: f64,
475 start: Point,
476 end: Point,
477 ) -> OgeomResult<(Curve, (f64, f64))> {
478 let scale = self.report.scale_mm;
479 let cx = -d / (2.0 * a);
480 let cy = -e / (2.0 * c);
481 let rhs = a * cx * cx + c * cy * cy - f;
482 let (ra2, rb2) = (rhs / a, rhs / c);
483 if ra2 <= 0.0 || rb2 <= 0.0 {
484 ogeom_bail!(
485 Construction,
486 "D{de}: conic arc coefficients close no ellipse"
487 );
488 }
489 let centre = Point::new(cx * scale, cy * scale, zt);
490 let (rx, ry) = (ra2.sqrt() * scale, rb2.sqrt() * scale);
491 let (frame, major, minor) = if rx >= ry {
494 (
495 Frame::new(centre, Direction::Z, Direction::X, self.tol)?,
496 rx,
497 ry,
498 )
499 } else {
500 (
501 Frame::new(centre, Direction::Z, Direction::Y, self.tol)?,
502 ry,
503 rx,
504 )
505 };
506 let ellipse = Ellipse::new(frame, major, minor, self.tol)?;
507 let angle_of = |p: Point| -> f64 {
508 let local = frame.to_local(p);
509 (local.y / minor)
510 .atan2(local.x / major)
511 .rem_euclid(core::f64::consts::TAU)
512 };
513 let t0 = angle_of(start);
514 let mut t1 = angle_of(end);
515 if t1 <= t0 + self.tol.parametric() {
516 t1 += core::f64::consts::TAU;
517 }
518 Ok((Curve::from(EllipseCurve::new(ellipse)), (t0, t1)))
519 }
520
521 fn hyperbola_arc(
527 de: i64,
528 [a, c, d, e, f]: [f64; 5],
529 zt: f64,
530 start: Point,
531 end: Point,
532 scale: f64,
533 tol: Tolerances,
534 ) -> OgeomResult<(Curve, (f64, f64))> {
535 let cx = -d / (2.0 * a);
536 let cy = -e / (2.0 * c);
537 let rhs = a * cx * cx + c * cy * cy - f;
538 if rhs == 0.0 {
539 ogeom_bail!(
540 Construction,
541 "D{de}: conic arc coefficients close no hyperbola; they cross"
542 );
543 }
544 let (pa, pc) = (a / rhs, c / rhs);
547 let (major, minor, along_x) = if pa > 0.0 {
548 ((1.0 / pa).sqrt(), (-1.0 / pc).sqrt(), true)
549 } else {
550 ((1.0 / pc).sqrt(), (-1.0 / pa).sqrt(), false)
551 };
552 let centre = Point::new(cx * scale, cy * scale, zt);
553 let axis = if along_x { Direction::X } else { Direction::Y };
554 let side = (start - centre).dot(axis.vector());
556 let x = if side >= 0.0 {
557 axis
558 } else {
559 Direction::new(-axis.vector(), tol)?
560 };
561 let build = |z: Direction| -> OgeomResult<(Curve, (f64, f64))> {
562 let frame = Frame::new(centre, z, x, tol)?;
563 let (major, minor) = (major * scale, minor * scale);
564 let hyperbola = Hyperbola::new(frame, major, minor, tol)?;
565 let t_of = |p: Point| (frame.to_local(p).y / minor).asinh();
566 let (t0, t1) = (t_of(start), t_of(end));
567 let extent = t0.abs().max(t1.abs()).max(1e-3) * 1.5;
568 Ok((
569 Curve::from(HyperbolaCurve::new(hyperbola, extent)?),
570 (t0, t1),
571 ))
572 };
573 let (curve, (t0, t1)) = build(Direction::Z)?;
574 if t1 > t0 {
575 return Ok((curve, (t0, t1)));
576 }
577 let (curve, (t0, t1)) = build(Direction::new(-Direction::Z.vector(), tol)?)?;
579 Ok((curve, (t0, t1)))
580 }
581
582 fn parabola_arc(
586 de: i64,
587 [a, c, d, e, f]: [f64; 5],
588 zt: f64,
589 start: Point,
590 end: Point,
591 scale: f64,
592 tol: Tolerances,
593 ) -> OgeomResult<(Curve, (f64, f64))> {
594 let (apex_across, apex_along, k, along) = if c == 0.0 {
597 if e == 0.0 {
598 ogeom_bail!(
599 Construction,
600 "D{de}: conic arc coefficients close no parabola"
601 );
602 }
603 let x0 = -d / (2.0 * a);
604 let y0 = -(a * x0 * x0 + d * x0 + f) / e;
605 (x0, y0, -a / e, Direction::Y)
606 } else {
607 if d == 0.0 {
608 ogeom_bail!(
609 Construction,
610 "D{de}: conic arc coefficients close no parabola"
611 );
612 }
613 let y0 = -e / (2.0 * c);
614 let x0 = -(c * y0 * y0 + e * y0 + f) / d;
615 (y0, x0, -c / d, Direction::X)
616 };
617 let apex = if c == 0.0 {
618 Point::new(apex_across * scale, apex_along * scale, zt)
619 } else {
620 Point::new(apex_along * scale, apex_across * scale, zt)
621 };
622 let x = if k >= 0.0 {
624 along
625 } else {
626 Direction::new(-along.vector(), tol)?
627 };
628 let focal = scale / (4.0 * k.abs());
630 let build = |z: Direction| -> OgeomResult<(Curve, (f64, f64))> {
631 let frame = Frame::new(apex, z, x, tol)?;
632 let parabola = Parabola::new(frame, focal, tol)?;
633 let t_of = |p: Point| frame.to_local(p).y;
634 let (t0, t1) = (t_of(start), t_of(end));
635 let extent = t0.abs().max(t1.abs()).max(1e-3) * 1.5;
636 Ok((Curve::from(ParabolaCurve::new(parabola, extent)?), (t0, t1)))
637 };
638 let (curve, (t0, t1)) = build(Direction::Z)?;
639 if t1 > t0 {
640 return Ok((curve, (t0, t1)));
641 }
642 let (curve, (t0, t1)) = build(Direction::new(-Direction::Z.vector(), tol)?)?;
643 Ok((curve, (t0, t1)))
644 }
645
646 fn offset_curve(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
656 let scale = self.report.scale_mm;
657 let kind = entity.at(1).int();
658 let (d1, d2) = (entity.at(5).real(), entity.at(7).real());
659 if kind != 1 || (d1 - d2).abs() > 1e-12 {
660 return self.varying_offset(de, entity, kind);
661 }
662 let (basis, range) = self.curve(entity.at(0).int())?;
663 let reference = Direction::from_coords(
664 entity.at(9).real(),
665 entity.at(10).real(),
666 entity.at(11).real(),
667 self.tol,
668 )?;
669 let (tt1, tt2) = (entity.at(12).real(), entity.at(13).real());
670 let range = if tt2 > tt1 { (tt1, tt2) } else { range };
671 let offset = OffsetCurve::new(basis, -d1 * scale, reference)?;
672 Ok((Curve::Offset(Box::new(offset)), range))
673 }
674
675 fn varying_offset(
677 &mut self,
678 de: i64,
679 entity: &Entity,
680 kind: i64,
681 ) -> OgeomResult<(Curve, (f64, f64))> {
682 let scale = self.report.scale_mm;
683 let (basis, range) = self.curve(entity.at(0).int())?;
684 let reference = Direction::from_coords(
685 entity.at(9).real(),
686 entity.at(10).real(),
687 entity.at(11).real(),
688 self.tol,
689 )?
690 .vector();
691 let (tt1, tt2) = (entity.at(12).real(), entity.at(13).real());
692 let range = if tt2 > tt1 { (tt1, tt2) } else { range };
693 const SAMPLES: usize = 400;
694 let parameters: Vec<f64> = (0..=SAMPLES)
695 .map(|i| {
696 #[allow(clippy::cast_precision_loss)]
697 let f = i as f64 / SAMPLES as f64;
698 range.0 + (range.1 - range.0) * f
699 })
700 .collect();
701 let distance: Box<dyn Fn(usize, f64) -> OgeomResult<f64>> = match kind {
702 2 => {
703 let (d1, td1, d2, td2) = (
705 entity.at(5).real(),
706 entity.at(6).real(),
707 entity.at(7).real(),
708 entity.at(8).real(),
709 );
710 let mut lengths = vec![0.0_f64];
711 let mut last = basis.point_at(parameters[0], self.tol)?;
712 for &t in ¶meters[1..] {
713 let p = basis.point_at(t, self.tol)?;
714 let held = lengths[lengths.len() - 1];
715 lengths.push(held + last.distance(p) / scale);
716 last = p;
717 }
718 let span = td2 - td1;
719 Box::new(move |i, _| {
720 let f = if span.abs() > 0.0 {
721 (lengths[i] - td1) / span
722 } else {
723 0.0
724 };
725 Ok(d1 + (d2 - d1) * f)
726 })
727 }
728 3 => {
729 let (function, _) = self.curve(entity.at(2).int())?;
732 let which = entity.at(3).int();
733 let tol = self.tol;
734 Box::new(move |_, t| {
735 let p = function.point_at(t, tol)?;
736 let value = match which {
737 1 => p.x,
738 2 => p.y,
739 _ => p.z,
740 };
741 Ok(value / scale)
742 })
743 }
744 _ => ogeom_bail!(
745 Construction,
746 "D{de}: offset curve type {kind} names no distance law"
747 ),
748 };
749 let mut points = Vec::with_capacity(parameters.len());
750 for (i, &t) in parameters.iter().enumerate() {
751 let p = basis.point_at(t, self.tol)?;
752 let tangent = basis.d1_at(t, self.tol)?;
753 let side = reference.cross(tangent);
754 let m = side.magnitude();
755 if m <= self.tol.angular() {
756 ogeom_bail!(
757 Construction,
758 "D{de}: the offset's reference runs along its base curve"
759 );
760 }
761 points.push(p + side * (distance(i, t)? * scale / m));
762 }
763 let fitted = ogeom_geom::fit::fit_points_at(
764 ¶meters,
765 &points,
766 3,
767 self.tol.confusion() * 100.0,
768 self.tol,
769 )?;
770 if !fitted.met {
771 self.report.warnings.push(format!(
772 "D{de}: a varying offset curve fitted to {:.2e}",
773 fitted.error
774 ));
775 }
776 Ok((Curve::from(fitted.curve), range))
777 }
778
779 fn spline_curve(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
780 let n = usize::try_from(entity.at(3).int()).unwrap_or(0);
781 if n == 0 {
782 ogeom_bail!(Construction, "D{de}: a spline curve with no segments");
783 }
784 let scale = self.report.scale_mm;
785 let t = |i: usize| entity.at(4 + i).real();
788 let base = 4 + n + 1;
789 let mut control: Vec<Point> = Vec::with_capacity(3 * n + 1);
790 let mut knots: Vec<f64> = vec![t(0); 4];
791 for seg in 0..n {
792 let h = t(seg + 1) - t(seg);
793 if h <= 0.0 {
794 ogeom_bail!(Construction, "D{de}: spline segment {seg} has no span");
795 }
796 let co = |k: usize| entity.at(base + 12 * seg + k).real();
797 let (ax, bx, cx, dx) = (co(0), co(1), co(2), co(3));
798 let (ay, by, cy, dy) = (co(4), co(5), co(6), co(7));
799 let (az, bz, cz, dz) = (co(8), co(9), co(10), co(11));
800 let at = |s: f64| {
801 Point::new(
802 (ax + s * (bx + s * (cx + s * dx))) * scale,
803 (ay + s * (by + s * (cy + s * dy))) * scale,
804 (az + s * (bz + s * (cz + s * dz))) * scale,
805 )
806 };
807 let p0 = at(0.0);
810 let p3 = at(h);
811 let d0 = Vector::new(bx, by, bz) * (h * scale / 3.0);
812 let d1 = Vector::new(
813 bx + 2.0 * cx * h + 3.0 * dx * h * h,
814 by + 2.0 * cy * h + 3.0 * dy * h * h,
815 bz + 2.0 * cz * h + 3.0 * dz * h * h,
816 ) * (h * scale / 3.0);
817 if seg == 0 {
818 control.push(p0);
819 }
820 control.push(p0 + d0);
821 control.push(p3 - d1);
822 control.push(p3);
823 if seg + 1 < n {
824 knots.extend([t(seg + 1); 3]);
825 }
826 }
827 knots.extend([t(n); 4]);
828 let curve = BSplineCurve::new(KnotVector::new(knots, 3)?, control, self.tol)?;
829 let range = ogeom_geom::Curve3d::domain(&curve);
830 Ok((Curve::from(curve), range))
831 }
832
833 fn nurbs_curve(&mut self, de: i64, entity: &Entity) -> OgeomResult<(Curve, (f64, f64))> {
835 let k = usize::try_from(entity.at(0).int()).unwrap_or(0);
836 let degree = usize::try_from(entity.at(1).int()).unwrap_or(0);
837 if degree == 0 {
838 ogeom_bail!(Construction, "D{de}: a B-spline curve of degree zero");
839 }
840 let n_ctrl = k + 1;
841 let n_knots = n_ctrl + degree + 1;
842 let knots: Vec<f64> = (0..n_knots).map(|i| entity.at(6 + i).real()).collect();
843 let w_base = 6 + n_knots;
844 let p_base = w_base + n_ctrl;
845 let scale = self.report.scale_mm;
846 let mut control = Vec::with_capacity(n_ctrl);
847 for i in 0..n_ctrl {
848 let w = entity.at(w_base + i).real();
849 let p = Point::new(
850 entity.at(p_base + 3 * i).real() * scale,
851 entity.at(p_base + 3 * i + 1).real() * scale,
852 entity.at(p_base + 3 * i + 2).real() * scale,
853 );
854 control.push(Weighted::new(p, w, self.tol)?);
855 }
856 let curve = BSplineCurve::rational(KnotVector::new(knots, degree)?, control)?;
857 let v0 = entity.at(p_base + 3 * n_ctrl).real();
858 let v1 = entity.at(p_base + 3 * n_ctrl + 1).real();
859 let domain = ogeom_geom::Curve3d::domain(&curve);
860 let range = if v1 > v0 { (v0, v1) } else { domain };
861 Ok((Curve::from(curve), range))
862 }
863
864 fn surface(&mut self, de: i64) -> OgeomResult<SurfaceGeometry> {
866 let entity = self.entity(de)?;
867 let scale = self.report.scale_mm;
868 let surface: SurfaceGeometry = match entity.kind {
869 108 => {
870 let normal = Vector::new(
872 entity.at(0).real(),
873 entity.at(1).real(),
874 entity.at(2).real(),
875 );
876 let d = entity.at(3).real();
877 let origin = Point::ORIGIN + normal * (d / normal.dot(normal)) * scale;
878 let plane = Plane::through(origin, Direction::new(normal, self.tol)?);
879 PlaneSurface::over(
880 plane,
881 (-SURFACE_EXTENT, SURFACE_EXTENT),
882 (-SURFACE_EXTENT, SURFACE_EXTENT),
883 )?
884 .into()
885 }
886 190 => {
887 let point = self.location_entity(entity.at(0).int())?;
888 let normal = self.direction_entity(entity.at(1).int())?;
889 let plane = match self.reference_direction(entity, 2)? {
891 Some(x) => Plane::new(Frame::new(point, normal, x, self.tol)?),
892 None => Plane::through(point, normal),
893 };
894 PlaneSurface::over(
895 plane,
896 (-SURFACE_EXTENT, SURFACE_EXTENT),
897 (-SURFACE_EXTENT, SURFACE_EXTENT),
898 )?
899 .into()
900 }
901 120 => {
902 let axis = {
904 let (line, _) = self.curve(entity.at(0).int())?;
905 let Curve::Line(l) = line else {
906 ogeom_bail!(
907 Construction,
908 "D{de}: a surface of revolution's axis is not a line"
909 );
910 };
911 l.axis()
912 };
913 let (curve, range) = self.curve(entity.at(1).int())?;
914 let mut curve = trimmed_to(curve, range, self.tol)?;
915 let sa = entity.at(2).real();
916 let ta = entity.at(3).real();
917 let sweep = if ta > sa {
918 ta - sa
919 } else {
920 core::f64::consts::TAU
921 };
922 if sa.abs() > self.tol.parametric() {
923 curve = curve.transformed(&Transform::rotation(axis, sa), self.tol)?;
924 }
925 RevolutionSurface::new(curve, axis, sweep)?.into()
926 }
927 122 => {
928 let (curve, range) = self.curve(entity.at(0).int())?;
931 let start = curve.point_at(range.0, self.tol)?;
932 let far = self.point3(entity, 1);
933 let vec = far - start;
934 let curve = trimmed_to(curve, range, self.tol)?;
935 ExtrusionSurface::new(curve, Direction::new(vec, self.tol)?, vec.magnitude())?
936 .into()
937 }
938 128 => self.nurbs_surface(de, entity)?,
939 114 => self.spline_surface(de, entity)?,
940 118 => self.ruled_surface(de, entity)?,
941 140 => {
942 let given = Direction::from_coords(
946 entity.at(0).real(),
947 entity.at(1).real(),
948 entity.at(2).real(),
949 self.tol,
950 )?;
951 let distance = entity.at(3).real() * scale;
952 let basis = self.surface(entity.at(4).int())?;
953 let ((ua, ub), (va, vb)) = ogeom_geom::Surface::domain(&basis);
954 let own = ogeom_geom::Surface::normal_at(
955 &basis,
956 f64::midpoint(ua, ub),
957 f64::midpoint(va, vb),
958 self.tol,
959 )?;
960 let signed = if own.vector().dot(given.vector()) < 0.0 {
961 -distance
962 } else {
963 distance
964 };
965 SurfaceGeometry::Offset(Box::new(OffsetSurface::new(basis, signed)?))
966 }
967 192 => {
968 let point = self.location_entity(entity.at(0).int())?;
969 let dir = self.direction_entity(entity.at(1).int())?;
970 let radius = entity.at(2).real() * scale;
971 let frame = self.framed(point, dir, entity, 3)?;
972 CylinderSurface::new(
973 Cylinder::new(frame, radius, self.tol)?,
974 (-SURFACE_EXTENT, SURFACE_EXTENT),
975 )?
976 .into()
977 }
978 194 => {
979 let point = self.location_entity(entity.at(0).int())?;
980 let dir = self.direction_entity(entity.at(1).int())?;
981 let radius = entity.at(2).real() * scale;
982 let half_angle = entity.at(3).real().to_radians();
983 let frame = self.framed(point, dir, entity, 4)?;
984 ConeSurface::new(
985 Cone::new(frame, radius, half_angle, self.tol)?,
986 (-SURFACE_EXTENT, SURFACE_EXTENT),
987 )?
988 .into()
989 }
990 196 => {
991 let centre = self.location_entity(entity.at(0).int())?;
992 let radius = entity.at(1).real() * scale;
993 if entity.at(2).int() != 0 {
995 let axis = self.direction_entity(entity.at(2).int())?;
996 let frame = self.framed(centre, axis, entity, 3)?;
997 SphereSurface::new(Sphere::new(frame, radius, self.tol)?).into()
998 } else {
999 SphereSurface::new(Sphere::centred(centre, radius, self.tol)?).into()
1000 }
1001 }
1002 198 => {
1003 let centre = self.location_entity(entity.at(0).int())?;
1004 let dir = self.direction_entity(entity.at(1).int())?;
1005 let major = entity.at(2).real() * scale;
1006 let minor = entity.at(3).real() * scale;
1007 let frame = self.framed(centre, dir, entity, 4)?;
1008 TorusSurface::new(Torus::new(frame, major, minor, self.tol)?).into()
1009 }
1010 kind => ogeom_bail!(
1011 Construction,
1012 "D{de}: surface entity type {kind}{} is not translated; see \
1013 docs/PARITY.md, io.iges",
1014 super::entity_name(kind).map_or_else(String::new, |n| format!(" ({n})"))
1015 ),
1016 };
1017 let placement = self.placement(entity)?;
1018 if placement == Transform::IDENTITY {
1019 Ok(surface)
1020 } else {
1021 Ok(surface.transformed(&placement, self.tol)?)
1022 }
1023 }
1024
1025 fn ruled_surface(&mut self, de: i64, entity: &Entity) -> OgeomResult<SurfaceGeometry> {
1034 let (first, first_range) = self.curve(entity.at(0).int())?;
1035 let (second, second_range) = self.curve(entity.at(1).int())?;
1036 let mut a = first.to_bspline_over(first_range, self.tol)?;
1037 let mut b = second.to_bspline_over(second_range, self.tol)?;
1038 if entity.at(2).int() == 1 {
1039 let Curve::BSpline(turned) =
1040 ogeom_geom::Reversible::reversed(&Curve::BSpline(b.clone()))
1041 else {
1042 ogeom_bail!(Construction, "D{de}: a reversed spline is not a spline");
1043 };
1044 b = turned;
1045 }
1046 while a.degree() < b.degree() {
1047 a = a.elevated(self.tol)?;
1048 }
1049 while b.degree() < a.degree() {
1050 b = b.elevated(self.tol)?;
1051 }
1052 let tol = self.tol;
1053 let unify = |target: &mut BSplineCurve, source: &BSplineCurve| -> OgeomResult<()> {
1054 let (lo, hi) = source.knots().domain();
1055 for (value, multiplicity) in source.knots().distinct() {
1056 if value <= lo + 1e-12 || value >= hi - 1e-12 {
1057 continue;
1058 }
1059 let held = target.knots().multiplicity_of(value);
1060 if multiplicity > held {
1061 *target = target.with_knot_inserted(value, multiplicity - held, tol)?;
1062 }
1063 }
1064 Ok(())
1065 };
1066 unify(&mut a, &b)?;
1067 unify(&mut b, &a)?;
1068 let nu = a.control_points().len();
1069 if b.control_points().len() != nu {
1070 ogeom_bail!(
1071 Construction,
1072 "D{de}: the ruled surface's curves refine to different nets"
1073 );
1074 }
1075 let mut points: Vec<Weighted<Point>> = Vec::with_capacity(nu * 2);
1076 for i in 0..nu {
1077 points.push(a.control_points()[i]);
1078 points.push(b.control_points()[i]);
1079 }
1080 let grid = ControlGrid::new(points, nu, 2)?;
1081 let across = KnotVector::new(vec![0.0, 0.0, 1.0, 1.0], 1)?;
1082 Ok(BSplineSurface::rational(a.knots().clone(), across, grid)?.into())
1083 }
1084
1085 fn spline_surface(&mut self, de: i64, entity: &Entity) -> OgeomResult<SurfaceGeometry> {
1096 let m = usize::try_from(entity.at(2).int()).unwrap_or(0);
1097 let n = usize::try_from(entity.at(3).int()).unwrap_or(0);
1098 if m == 0 || n == 0 {
1099 ogeom_bail!(Construction, "D{de}: a spline surface with no patches");
1100 }
1101 let scale = self.report.scale_mm;
1102 let tu = |i: usize| entity.at(4 + i).real();
1103 let tv = |j: usize| entity.at(4 + m + 1 + j).real();
1104 let base = 4 + (m + 1) + (n + 1);
1105 let (rows, columns) = (3 * m + 1, 3 * n + 1);
1106 let mut net = vec![Point::ORIGIN; rows * columns];
1107 const TO_BERNSTEIN: [[f64; 4]; 4] = [
1110 [1.0, 0.0, 0.0, 0.0],
1111 [1.0, 1.0 / 3.0, 0.0, 0.0],
1112 [1.0, 2.0 / 3.0, 1.0 / 3.0, 0.0],
1113 [1.0, 1.0, 1.0, 1.0],
1114 ];
1115 for i in 0..m {
1116 let h = tu(i + 1) - tu(i);
1117 for j in 0..n {
1118 let k = tv(j + 1) - tv(j);
1119 if h <= 0.0 || k <= 0.0 {
1120 ogeom_bail!(
1121 Construction,
1122 "D{de}: spline surface patch ({i}, {j}) has no span"
1123 );
1124 }
1125 let at = base + (i * (n + 1) + j) * 48;
1126 let mut coords = [[[0.0_f64; 4]; 4]; 3];
1127 for (axis, block) in coords.iter_mut().enumerate() {
1128 let mut b = [[0.0_f64; 4]; 4];
1130 for (mu, row) in b.iter_mut().enumerate() {
1131 for (lv, value) in row.iter_mut().enumerate() {
1132 let raw = entity.at(at + 16 * axis + mu + 4 * lv).real();
1133 #[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
1134 let scaled = raw * h.powi(mu as i32) * k.powi(lv as i32);
1135 *value = scaled;
1136 }
1137 }
1138 let mut q = [[0.0_f64; 4]; 4];
1140 for r in 0..4 {
1141 for lv in 0..4 {
1142 q[r][lv] = (0..4).map(|mu| TO_BERNSTEIN[r][mu] * b[mu][lv]).sum();
1143 }
1144 }
1145 for r in 0..4 {
1146 for c in 0..4 {
1147 block[r][c] = (0..4).map(|lv| TO_BERNSTEIN[c][lv] * q[r][lv]).sum();
1148 }
1149 }
1150 }
1151 for r in 0..4 {
1152 for c in 0..4 {
1153 net[(3 * i + r) * columns + 3 * j + c] = Point::new(
1154 coords[0][r][c] * scale,
1155 coords[1][r][c] * scale,
1156 coords[2][r][c] * scale,
1157 );
1158 }
1159 }
1160 }
1161 }
1162 let knots = |count: usize, at: &dyn Fn(usize) -> f64| -> Vec<f64> {
1163 let mut out = vec![at(0); 4];
1164 for i in 1..count {
1165 out.extend([at(i); 3]);
1166 }
1167 out.extend([at(count); 4]);
1168 out
1169 };
1170 let grid = ControlGrid::new(
1171 net.into_iter()
1172 .map(|p| Weighted::new(p, 1.0, self.tol))
1173 .collect::<OgeomResult<Vec<_>>>()?,
1174 rows,
1175 columns,
1176 )?;
1177 Ok(ogeom_geom::BSplineSurface::rational(
1178 KnotVector::new(knots(m, &tu), 3)?,
1179 KnotVector::new(knots(n, &tv), 3)?,
1180 grid,
1181 )?
1182 .into())
1183 }
1184
1185 fn nurbs_surface(&mut self, de: i64, entity: &Entity) -> OgeomResult<SurfaceGeometry> {
1186 let k1 = usize::try_from(entity.at(0).int()).unwrap_or(0);
1187 let k2 = usize::try_from(entity.at(1).int()).unwrap_or(0);
1188 let m1 = usize::try_from(entity.at(2).int()).unwrap_or(0);
1189 let m2 = usize::try_from(entity.at(3).int()).unwrap_or(0);
1190 if m1 == 0 || m2 == 0 {
1191 ogeom_bail!(Construction, "D{de}: a B-spline surface of degree zero");
1192 }
1193 let (nu, nv) = (k1 + 1, k2 + 1);
1194 let (nku, nkv) = (nu + m1 + 1, nv + m2 + 1);
1195 let base = 9;
1196 let u_knots: Vec<f64> = (0..nku).map(|i| entity.at(base + i).real()).collect();
1197 let v_knots: Vec<f64> = (0..nkv).map(|i| entity.at(base + nku + i).real()).collect();
1198 let w_base = base + nku + nkv;
1199 let p_base = w_base + nu * nv;
1200 let scale = self.report.scale_mm;
1201 let raw = |u: usize, v: usize| -> OgeomResult<Weighted<Point>> {
1205 let i = v * nu + u;
1206 let w = entity.at(w_base + i).real();
1207 let p = Point::new(
1208 entity.at(p_base + 3 * i).real() * scale,
1209 entity.at(p_base + 3 * i + 1).real() * scale,
1210 entity.at(p_base + 3 * i + 2).real() * scale,
1211 );
1212 Weighted::new(p, w, self.tol)
1213 };
1214 let mut weighted = Vec::with_capacity(nu * nv);
1215 for u in 0..nu {
1216 for v in 0..nv {
1217 weighted.push(raw(u, v)?);
1218 }
1219 }
1220 let grid = ControlGrid::new(weighted, nu, nv)?;
1221 Ok(ogeom_geom::BSplineSurface::rational(
1222 KnotVector::new(u_knots, m1)?,
1223 KnotVector::new(v_knots, m2)?,
1224 grid,
1225 )?
1226 .into())
1227 }
1228
1229 fn location_entity(&mut self, de: i64) -> OgeomResult<Point> {
1231 let e = self.entity(de)?;
1232 if e.kind != 116 {
1233 ogeom_bail!(
1234 Construction,
1235 "D{de}: expected a point entity, found type {}",
1236 e.kind
1237 );
1238 }
1239 Ok(self.point3(e, 0))
1240 }
1241
1242 fn reference_direction(
1246 &mut self,
1247 entity: &Entity,
1248 field: usize,
1249 ) -> OgeomResult<Option<Direction>> {
1250 let de = entity.at(field).int();
1251 if de == 0 || !self.file.entities.contains_key(&de) {
1252 return Ok(None);
1253 }
1254 Ok(Some(self.direction_entity(de)?))
1255 }
1256
1257 fn framed(
1260 &mut self,
1261 origin: Point,
1262 axis: Direction,
1263 entity: &Entity,
1264 field: usize,
1265 ) -> OgeomResult<Frame> {
1266 if let Some(reference) = self.reference_direction(entity, field)? {
1267 let r = reference.vector();
1268 let square = r - axis.vector() * r.dot(axis.vector());
1269 if let Ok(x) = Direction::new(square, self.tol) {
1270 return Frame::new(origin, axis, x, self.tol);
1271 }
1272 }
1273 frame_about(origin, axis, self.tol)
1274 }
1275
1276 fn direction_entity(&mut self, de: i64) -> OgeomResult<Direction> {
1277 let e = self.entity(de)?;
1278 if e.kind != 123 {
1279 ogeom_bail!(
1280 Construction,
1281 "D{de}: expected a direction entity, found type {}",
1282 e.kind
1283 );
1284 }
1285 Direction::from_coords(e.at(0).real(), e.at(1).real(), e.at(2).real(), self.tol)
1286 }
1287
1288 fn boundary_segments(&mut self, de: i64) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
1292 let entity = self.entity(de)?;
1293 match entity.kind {
1294 142 => {
1299 let c = entity.at(3).int();
1300 if c != 0 {
1301 return self.curve_segments(c);
1302 }
1303 let (surface_de, b) = (entity.at(1).int(), entity.at(2).int());
1304 self.lifted_segments(de, surface_de, b)
1305 }
1306 141 => {
1307 let n = usize::try_from(entity.at(3).int()).unwrap_or(0);
1308 let mut out = Vec::new();
1309 let mut i = 4;
1310 for _ in 0..n {
1311 let cptr = entity.at(i).int();
1312 let k = usize::try_from(entity.at(i + 2).int()).unwrap_or(0);
1313 i += 3 + k;
1314 out.extend(self.curve_segments(cptr)?);
1317 }
1318 Ok(out)
1319 }
1320 _ => self.curve_segments(de),
1321 }
1322 }
1323
1324 fn lifted_segments(
1331 &mut self,
1332 de: i64,
1333 surface_de: i64,
1334 b: i64,
1335 ) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
1336 let kind = self.entity(surface_de)?.kind;
1337 let surface = self.surface(surface_de)?;
1338 let scale = self.report.scale_mm;
1339 let half_angle = match &surface {
1347 SurfaceGeometry::Cone(c) => c.cone().half_angle(),
1348 _ => 0.0,
1349 };
1350 let swept = match (kind, &surface) {
1355 (122, SurfaceGeometry::Extrusion(e)) => {
1356 let (lo, hi) = ogeom_geom::Curve3d::domain(e.curve());
1357 let extent = ogeom_geom::Surface::domain(&surface).1;
1358 Some(((lo, hi - lo), (extent.0, extent.1 - extent.0), false))
1359 }
1360 (120, SurfaceGeometry::Revolution(r)) => {
1361 let generatrix = self.entity(surface_de)?.at(1).int();
1362 let (lo, hi) = ogeom_geom::Curve3d::domain(r.curve());
1363 match self.entity(generatrix)?.kind {
1364 110 => Some(((lo, hi - lo), (0.0, 1.0), true)),
1365 126 => Some(((0.0, 1.0), (0.0, 1.0), true)),
1366 _ => None,
1367 }
1368 }
1369 _ => None,
1370 };
1371 let start_angle = if kind == 120 {
1372 self.entity(surface_de)?.at(2).real()
1373 } else {
1374 0.0
1375 };
1376 let map = |p: ogeom_math::Point2| -> Option<ogeom_math::Point2> {
1377 use ogeom_math::Point2 as P;
1378 let (u, v) = (p.x / scale, p.y / scale);
1381 match kind {
1382 128 => Some(P::new(u, v)),
1383 190 => Some(P::new(u * scale, v * scale)),
1384 192 => Some(P::new(u.to_radians(), v * scale)),
1385 194 => Some(P::new(u.to_radians(), v * scale * half_angle.cos())),
1386 196 => Some(P::new(u.to_radians(), v.to_radians())),
1387 198 => Some(P::new((360.0 - v).to_radians(), u.to_radians())),
1388 122 => swept.map(|((u0, du), (v0, dv), _)| P::new(u0 + du * u, v0 + dv * v)),
1389 120 => swept.map(|((u0, du), _, _)| P::new(v - start_angle, u0 + du * u)),
1392 _ => None,
1393 }
1394 };
1395 if map(ogeom_math::Point2::new(0.0, 0.0)).is_none() {
1396 ogeom_bail!(
1397 Construction,
1398 "D{de}: a curve-on-surface carries no model-space curve, and \
1399 its surface's parameters are the file's convention for a \
1400 kind this reader does not translate; see docs/PARITY.md, io.iges"
1401 );
1402 }
1403 let mut out = Vec::new();
1404 for (curve, range) in self.curve_segments(b)? {
1405 const SAMPLES: usize = 200;
1408 let mut parameters = Vec::with_capacity(SAMPLES + 1);
1409 let mut points = Vec::with_capacity(SAMPLES + 1);
1410 for i in 0..=SAMPLES {
1411 #[allow(clippy::cast_precision_loss)]
1412 let t = range.0 + (range.1 - range.0) * (i as f64) / SAMPLES as f64;
1413 let p = curve.point_at(t, self.tol)?;
1414 parameters.push(t);
1415 let Some(q) = map(ogeom_math::Point2::new(p.x, p.y)) else {
1416 ogeom_bail!(Construction, "D{de}: a trim's parameters did not translate");
1417 };
1418 points.push(q);
1419 }
1420 let chart =
1421 ogeom_geom::fit::fit_points_2d_at(¶meters, &points, 3, 1e-10, self.tol)?;
1422 let lifted = Curve::OnSurface(Box::new(ogeom_geom::CurveOnSurface::new(
1423 chart.curve.into(),
1424 surface.clone(),
1425 )));
1426 let fitted =
1427 lifted.fitted_bspline_over(range, self.tol.confusion() * 100.0, self.tol)?;
1428 if !fitted.met {
1429 self.report.warnings.push(format!(
1430 "D{de}: a parameter-space trim lifted to within {:.2e}",
1431 fitted.error
1432 ));
1433 }
1434 let domain = ogeom_geom::Curve3d::domain(&fitted.curve);
1435 out.push((Curve::from(fitted.curve), domain));
1436 }
1437 Ok(out)
1438 }
1439
1440 fn curve_segments(&mut self, de: i64) -> OgeomResult<Vec<(Curve, (f64, f64))>> {
1441 let entity = self.entity(de)?;
1442 if entity.kind == 102 {
1443 let n = usize::try_from(entity.at(0).int()).unwrap_or(0);
1444 let mut out = Vec::new();
1445 for i in 0..n {
1446 out.extend(self.curve_segments(entity.at(1 + i).int())?);
1447 }
1448 return Ok(out);
1449 }
1450 Ok(vec![self.curve(de)?])
1451 }
1452
1453 fn face(&mut self, de: i64) -> OgeomResult<Shape> {
1455 let entity = self.entity(de)?;
1456 let (surface_de, boundaries): (i64, Vec<i64>) = match entity.kind {
1457 144 => {
1458 let s = entity.at(0).int();
1459 let outer_given = entity.at(1).int() == 1;
1460 let n_inner = usize::try_from(entity.at(2).int()).unwrap_or(0);
1461 let mut bs = Vec::new();
1462 if outer_given && entity.at(3).int() != 0 {
1463 bs.push(entity.at(3).int());
1464 }
1465 for i in 0..n_inner {
1466 bs.push(entity.at(4 + i).int());
1467 }
1468 (s, bs)
1469 }
1470 143 => {
1471 let s = entity.at(1).int();
1472 let n = usize::try_from(entity.at(2).int()).unwrap_or(0);
1473 (s, (0..n).map(|i| entity.at(3 + i).int()).collect())
1474 }
1475 kind => ogeom_bail!(Construction, "D{de}: type {kind} is not a trimmed surface"),
1476 };
1477 let surface = self.surface(surface_de)?;
1478 if boundaries.is_empty() {
1479 return Ok(ogeom_algo::make_natural_face(&mut self.model, surface)?.shape);
1482 }
1483 let mut wires = Vec::new();
1484 for boundary in boundaries {
1485 let segments = self.boundary_segments(boundary)?;
1486 wires.push(self.wire_edges(de, segments)?);
1487 }
1488 self.assemble_face(surface, wires)
1489 }
1490
1491 fn wire_edges(
1499 &mut self,
1500 face_de: i64,
1501 segments: Vec<(Curve, (f64, f64))>,
1502 ) -> OgeomResult<Vec<Shape>> {
1503 if segments.is_empty() {
1504 ogeom_bail!(Construction, "D{face_de}: a boundary with no curves");
1505 }
1506 let ends: Vec<(Point, Point)> = segments
1507 .iter()
1508 .map(|(c, r)| Ok((c.point_at(r.0, self.tol)?, c.point_at(r.1, self.tol)?)))
1509 .collect::<OgeomResult<_>>()?;
1510 let n = segments.len();
1511 let weld = self.tol.confusion() * 100.0;
1512
1513 if n == 1 {
1515 let (curve, range) = segments
1516 .into_iter()
1517 .next()
1518 .unwrap_or_else(|| unreachable!());
1519 let (s, e) = ends[0];
1520 if s.distance(e) > weld {
1521 ogeom_bail!(
1522 Construction,
1523 "D{face_de}: a one-curve boundary whose ends sit {:.2e} apart",
1524 s.distance(e)
1525 );
1526 }
1527 let v = make_vertex(&mut self.model, s).shape;
1528 let edge = make_edge_between(&mut self.model, curve, range, &v, &v, self.tol)?.shape;
1529 return Ok(vec![edge]);
1530 }
1531
1532 let head = make_vertex(&mut self.model, ends[0].0).shape;
1533 let mut at = ends[0].0;
1534 let mut at_vertex = head.clone();
1535 let mut edges = Vec::with_capacity(n);
1536 for (i, ((curve, range), (s, e))) in segments.into_iter().zip(ends).enumerate() {
1537 let forward = at.distance(s) <= at.distance(e);
1538 let (this_end, this_point) = if forward { (e, e) } else { (s, s) };
1539 let gap = at.distance(if forward { s } else { e });
1540 if gap > weld {
1541 ogeom_bail!(
1542 Construction,
1543 "D{face_de}: boundary segment {i} starts {gap:.2e} from \
1544 where the previous one ended"
1545 );
1546 }
1547 let last = i + 1 == n;
1548 let next_vertex = if last {
1549 head.clone()
1550 } else {
1551 make_vertex(&mut self.model, this_end).shape
1552 };
1553 let edge = if forward {
1557 make_edge_between(
1558 &mut self.model,
1559 curve,
1560 range,
1561 &at_vertex,
1562 &next_vertex,
1563 self.tol,
1564 )?
1565 .shape
1566 } else {
1567 make_edge_between(
1568 &mut self.model,
1569 curve,
1570 range,
1571 &next_vertex,
1572 &at_vertex,
1573 self.tol,
1574 )?
1575 .shape
1576 .reversed()
1577 };
1578 edges.push(edge);
1579 at = this_point;
1580 at_vertex = next_vertex;
1581 }
1582 Ok(edges)
1583 }
1584
1585 fn assemble_face(
1590 &mut self,
1591 surface: SurfaceGeometry,
1592 wires: Vec<Vec<Shape>>,
1593 ) -> OgeomResult<Shape> {
1594 if let [edges] = wires.as_slice()
1600 && let [a, b] = edges.as_slice()
1601 && a.node() == b.node()
1602 {
1603 use ogeom_geom::Surface as _;
1604 if surface.is_periodic_u() || surface.is_periodic_v() {
1605 return Ok(ogeom_algo::make_natural_face(&mut self.model, surface)?.shape);
1606 }
1607 }
1608 let surface_id = self.model.geometry_mut().add_surface(surface.clone());
1609 let mut wire_shapes = Vec::with_capacity(wires.len());
1610 for edges in &wires {
1611 wire_shapes.push(ogeom_algo::make_wire(&mut self.model, edges, self.tol)?.shape);
1612 }
1613 let face =
1614 ogeom_algo::make_face_on(&mut self.model, surface_id, &wire_shapes, self.tol)?.shape;
1615
1616 for edges in &wires {
1617 self.chart_wire(edges, &surface, surface_id)?;
1618 }
1619 Ok(face)
1620 }
1621
1622 fn chart_wire(
1640 &mut self,
1641 edges: &[Shape],
1642 surface: &SurfaceGeometry,
1643 surface_id: ogeom_topo::SurfaceId,
1644 ) -> OgeomResult<()> {
1645 use ogeom_geom::Curve2d as _;
1646 let mut counts: HashMap<ogeom_topo::TShapeId, usize> = HashMap::new();
1647 for edge in edges {
1648 *counts.entry(edge.node()).or_default() += 1;
1649 }
1650 let mut images: HashMap<ogeom_topo::TShapeId, (ogeom_geom::PlanarCurve, (f64, f64))> =
1652 HashMap::new();
1653 let mut sides: HashMap<
1655 ogeom_topo::TShapeId,
1656 (
1657 Option<ogeom_geom::PlanarCurve>,
1658 Option<ogeom_geom::PlanarCurve>,
1659 ),
1660 > = HashMap::new();
1661 let mut order: Vec<ogeom_topo::TShapeId> = Vec::new();
1662 let mut previous: Option<ogeom_math::Point2> = None;
1663 for edge in edges {
1664 let (curve, range) = {
1665 let Some(data) = self.model.node(edge).and_then(|n| n.data().as_edge()) else {
1666 continue;
1667 };
1668 let Some(ogeom_topo::EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d()
1669 else {
1670 continue;
1671 };
1672 let Some(geometry) = self.model.geometry().curve(*curve) else {
1673 continue;
1674 };
1675 (geometry.clone(), *range)
1676 };
1677 if let std::collections::hash_map::Entry::Vacant(slot) = images.entry(edge.node()) {
1678 let Some(image) = self.image_of(edge, &curve, range, surface)? else {
1679 continue;
1680 };
1681 slot.insert((image, range));
1682 order.push(edge.node());
1683 }
1684 let Some((image, range)) = images.get(&edge.node()).cloned() else {
1685 continue;
1686 };
1687 let backwards = edge.orientation() == ogeom_topo::Orientation::Reversed;
1688 let (start, end) = if backwards {
1689 (range.1, range.0)
1690 } else {
1691 (range.0, range.1)
1692 };
1693 let image =
1694 crate::pcurves::shifted_to_meet(&image, start, previous, surface, self.tol)?;
1695 previous = Some(image.point_at(end, self.tol)?);
1696 let walked = sides.entry(edge.node()).or_default();
1697 if backwards {
1698 walked.1 = Some(image);
1699 } else {
1700 walked.0 = Some(image);
1701 }
1702 }
1703 for node in order {
1704 let Some((image, range)) = images.get(&node).cloned() else {
1705 continue;
1706 };
1707 let Some(edge) = edges.iter().find(|e| e.node() == node) else {
1708 continue;
1709 };
1710 let walked = sides.remove(&node).unwrap_or((None, None));
1711 let seam = counts.get(&node).copied().unwrap_or(0) > 1;
1712 let columns = match &walked {
1713 (Some(forward), Some(reversed)) => {
1714 let at = forward.point_at(range.0, self.tol)?;
1715 let other = reversed.point_at(range.0, self.tol)?;
1716 (at.distance(other) > self.tol.confusion()).then_some((forward, reversed))
1717 }
1718 _ => None,
1719 };
1720 match (seam, columns) {
1721 (true, Some((forward, reversed))) => {
1722 ogeom_algo::attach_seam(
1723 &mut self.model,
1724 edge,
1725 forward.clone(),
1726 reversed.clone(),
1727 surface_id,
1728 ogeom_topo::Location::identity(),
1729 range,
1730 )?;
1731 }
1732 (true, None) => {
1739 let other = crate::pcurves::seam_other_side(&image, range, surface, self.tol)?;
1740 ogeom_algo::attach_seam(
1741 &mut self.model,
1742 edge,
1743 image,
1744 other,
1745 surface_id,
1746 ogeom_topo::Location::identity(),
1747 range,
1748 )?;
1749 }
1750 (false, _) => {
1751 let (forward, reversed) = walked;
1752 ogeom_algo::attach_pcurve(
1753 &mut self.model,
1754 edge,
1755 forward.or(reversed).unwrap_or(image),
1756 surface_id,
1757 ogeom_topo::Location::identity(),
1758 range,
1759 )?;
1760 }
1761 }
1762 }
1763 Ok(())
1764 }
1765
1766 fn image_of(
1771 &mut self,
1772 edge: &Shape,
1773 curve: &Curve,
1774 range: (f64, f64),
1775 surface: &SurfaceGeometry,
1776 ) -> OgeomResult<Option<ogeom_geom::PlanarCurve>> {
1777 use ogeom_geom::PlanarCurve;
1778 let widen = |p: PlanarCurve| -> PlanarCurve {
1779 if let PlanarCurve::Line(l) = &p {
1780 use ogeom_geom::Curve2d as _;
1781 let (lo, hi) = (l.domain().0.min(range.0), l.domain().1.max(range.1));
1782 if let Ok(wider) = ogeom_geom::Line2d::over(l.axis(), lo, hi) {
1783 return wider.into();
1784 }
1785 }
1786 p
1787 };
1788 let found = match ogeom_intersect::exact_pcurve_over(curve, range, surface, self.tol)
1789 .map(widen)
1790 {
1791 Some(exact) => exact,
1792 None => match crate::pcurves::fit_projected_pcurve(curve, range, surface, self.tol) {
1793 Ok((fitted, error, met, worst_off, slop)) => {
1794 if let Some(w) = slop {
1795 self.report.warnings.push(w);
1796 }
1797 if !met {
1798 self.report.warnings.push(format!(
1799 "a projected pcurve fit stopped at {error:.2e}; \
1800 the face's mesh may sit that far off along this edge"
1801 ));
1802 }
1803 if worst_off > self.tol.confusion()
1804 && let Some(node) = self.model.node_mut(edge)
1805 && let ogeom_topo::NodeData::Edge(data) = node.data_mut()
1806 {
1807 data.tolerance = data.tolerance.widen_to(worst_off + self.tol.confusion());
1808 }
1809 fitted
1810 }
1811 Err(e) => {
1812 self.report.warnings.push(format!(
1813 "no pcurve for an edge on this surface ({e}); the \
1814 face may not triangulate"
1815 ));
1816 return Ok(None);
1817 }
1818 },
1819 };
1820 Ok(Some(found))
1821 }
1822
1823 fn manifold_solid(&mut self, de: i64) -> OgeomResult<Shape> {
1825 let entity = self.entity(de)?;
1826 let shell = self.shell(entity.at(0).int())?;
1827 let n_voids = usize::try_from(entity.at(2).int()).unwrap_or(0);
1828 let mut shells = vec![shell];
1829 for i in 0..n_voids {
1830 shells.push(self.shell(entity.at(3 + 2 * i).int())?);
1831 }
1832 let solid = make_solid(&mut self.model, &shells)?.shape;
1833 ogeom_algo::restore_containment(&mut self.model, &solid)?;
1837 Ok(solid)
1838 }
1839
1840 fn shell(&mut self, de: i64) -> OgeomResult<Shape> {
1841 let entity = self.entity(de)?;
1842 if entity.kind != 514 {
1843 ogeom_bail!(
1844 Construction,
1845 "D{de}: expected a shell, found type {}",
1846 entity.kind
1847 );
1848 }
1849 let n = usize::try_from(entity.at(0).int()).unwrap_or(0);
1850 let mut faces = Vec::with_capacity(n);
1851 for i in 0..n {
1852 let face_de = entity.at(1 + 2 * i).int();
1853 let same_sense = entity.at(2 + 2 * i).int() != 0;
1854 let face = self.brep_face(face_de)?;
1855 faces.push(if same_sense { face } else { face.reversed() });
1856 }
1857 Ok(ogeom_algo::make_shell(&mut self.model, &faces)?.shape)
1858 }
1859
1860 fn brep_face(&mut self, de: i64) -> OgeomResult<Shape> {
1861 let entity = self.entity(de)?;
1862 if entity.kind != 510 {
1863 ogeom_bail!(
1864 Construction,
1865 "D{de}: expected a face, found type {}",
1866 entity.kind
1867 );
1868 }
1869 let surface = self.surface(entity.at(0).int())?;
1870 let n_loops = usize::try_from(entity.at(1).int()).unwrap_or(0);
1871 let mut wires = Vec::with_capacity(n_loops);
1873 for i in 0..n_loops {
1874 wires.push(self.loop_edges(entity.at(3 + i).int())?);
1875 }
1876 self.assemble_face(surface, wires)
1877 }
1878
1879 fn loop_edges(&mut self, de: i64) -> OgeomResult<Vec<Shape>> {
1880 let entity = self.entity(de)?;
1881 if entity.kind != 508 {
1882 ogeom_bail!(
1883 Construction,
1884 "D{de}: expected a loop, found type {}",
1885 entity.kind
1886 );
1887 }
1888 let n = usize::try_from(entity.at(0).int()).unwrap_or(0);
1889 let mut edges = Vec::with_capacity(n);
1890 let mut i = 1;
1891 for _ in 0..n {
1892 let is_vertex = entity.at(i).int() == 1;
1893 let list_de = entity.at(i + 1).int();
1894 let index = entity.at(i + 2).int();
1895 let orientation = entity.at(i + 3).int();
1896 let k = usize::try_from(entity.at(i + 4).int()).unwrap_or(0);
1897 i += 5 + 2 * k;
1898 if is_vertex {
1899 self.report.warnings.push(format!(
1902 "D{de}: a loop lists a vertex entry, which this reader skips"
1903 ));
1904 continue;
1905 }
1906 let (edge, _, _) = self.list_edge(list_de, index)?;
1907 edges.push(if orientation != 0 {
1908 edge
1909 } else {
1910 edge.reversed()
1911 });
1912 }
1913 Ok(edges)
1914 }
1915
1916 fn list_edge(&mut self, list_de: i64, index: i64) -> OgeomResult<BuiltEdge> {
1918 let key = (list_de, index);
1919 if let Some(found) = self.edges.get(&key) {
1920 return Ok(found.clone());
1921 }
1922 let entity = self.entity(list_de)?;
1923 if entity.kind != 504 {
1924 ogeom_bail!(
1925 Construction,
1926 "D{list_de}: expected an edge list, found type {}",
1927 entity.kind
1928 );
1929 }
1930 let i = usize::try_from(index - 1).map_err(|_| {
1931 ogeom_core::ogeom_err!(Construction, "D{list_de}: edge index {index} out of range")
1932 })?;
1933 let base = 1 + 5 * i;
1934 let curve_de = entity.at(base).int();
1935 let (sv_list, sv_index) = (entity.at(base + 1).int(), entity.at(base + 2).int());
1936 let (tv_list, tv_index) = (entity.at(base + 3).int(), entity.at(base + 4).int());
1937 let (curve, mut range) = self.curve(curve_de)?;
1938 let vs = self.list_vertex(sv_list, sv_index)?;
1939 let ve = self.list_vertex(tv_list, tv_index)?;
1940 let (ps, pe) = (self.point_of(&vs), self.point_of(&ve));
1941 if let (Some(a), Some(b)) = (parameter_on(&curve, ps), parameter_on(&curve, pe)) {
1945 let period = if curve.is_periodic() {
1946 let (lo, hi) = curve.domain();
1947 hi - lo
1948 } else {
1949 0.0
1950 };
1951 range = if ps.distance(pe) < self.tol.confusion() && period > 0.0 {
1952 (a, a + period)
1953 } else if period > 0.0 && b <= a + self.tol.parametric() {
1954 (a, b + period)
1955 } else {
1956 (a, b)
1957 };
1958 if period == 0.0 {
1962 let (lo, hi) = curve.domain();
1963 range = (range.0.clamp(lo, hi), range.1.clamp(lo, hi));
1964 }
1965 }
1966 let cap = self.tol.confusion() * 1e7;
1973 for (vertex, t) in [(&vs, range.0), (&ve, range.1)] {
1974 let (Ok(end), Some(stated)) = (
1975 curve.point_at(t, self.tol),
1976 self.model
1977 .node(vertex)
1978 .and_then(|n| n.data().as_vertex())
1979 .map(|d| (d.point, d.tolerance)),
1980 ) else {
1981 continue;
1982 };
1983 let gap = end.distance(stated.0);
1984 if gap > stated.1.get() && gap <= cap {
1985 self.model.widen(
1986 vertex,
1987 ogeom_core::Tolerance::new(gap + self.tol.confusion())?,
1988 )?;
1989 self.vertex_misses.0 += 1;
1990 self.vertex_misses.1 = self.vertex_misses.1.max(gap);
1991 }
1992 }
1993 let edge =
1994 make_edge_between(&mut self.model, curve.clone(), range, &vs, &ve, self.tol)?.shape;
1995 let built = (edge, curve, range);
1996 self.edges.insert(key, built.clone());
1997 Ok(built)
1998 }
1999
2000 fn list_vertex(&mut self, list_de: i64, index: i64) -> OgeomResult<Shape> {
2003 let key = (list_de, index);
2004 if let Some(found) = self.vertices.get(&key) {
2005 return Ok(found.clone());
2006 }
2007 let entity = self.entity(list_de)?;
2008 if entity.kind != 502 {
2009 ogeom_bail!(
2010 Construction,
2011 "D{list_de}: expected a vertex list, found type {}",
2012 entity.kind
2013 );
2014 }
2015 let i = usize::try_from(index - 1).map_err(|_| {
2016 ogeom_core::ogeom_err!(
2017 Construction,
2018 "D{list_de}: vertex index {index} out of range"
2019 )
2020 })?;
2021 let point = self.point3(entity, 1 + 3 * i);
2022 let vertex = make_vertex(&mut self.model, point).shape;
2023 self.vertices.insert(key, vertex.clone());
2024 Ok(vertex)
2025 }
2026
2027 fn point_of(&self, vertex: &Shape) -> Point {
2028 self.model
2029 .node(vertex)
2030 .and_then(|n| n.data().as_vertex())
2031 .map_or(Point::ORIGIN, |d| d.point)
2032 }
2033
2034 fn csg_solids(&mut self, de: i64) -> OgeomResult<Vec<Shape>> {
2039 let entity = self.entity(de)?;
2040 if entity.kind != 184 {
2041 return Ok(vec![self.csg(de)?]);
2042 }
2043 let n = usize::try_from(entity.at(0).int()).unwrap_or(0);
2045 let mut out = Vec::with_capacity(n);
2046 for i in 0..n {
2047 let item = entity.at(1 + i).int();
2048 let matrix = entity.at(1 + n + i).int();
2049 let mut solid = self.csg(item)?;
2050 if matrix != 0 {
2051 let placement = {
2052 let holder = Entity {
2053 kind: 0,
2054 form: 0,
2055 transform: matrix,
2056 colour: 0,
2057 level: 0,
2058 status: 0,
2059 label: String::new(),
2060 params: Vec::new(),
2061 };
2062 self.placement(&holder)?
2063 };
2064 let location = ogeom_topo::Location::of(self.model.add_datum(placement));
2065 solid = solid.moved(&location);
2066 }
2067 out.push(solid);
2068 }
2069 Ok(out)
2070 }
2071
2072 #[allow(clippy::too_many_lines, reason = "one case per primitive")]
2076 fn csg(&mut self, de: i64) -> OgeomResult<Shape> {
2077 let entity = self.entity(de)?;
2078 let s = self.report.scale_mm;
2079 let tol = self.tol;
2080 let r = |i: usize| entity.at(i).real();
2081 let length = |i: usize| entity.at(i).real() * s;
2082 let point = |i: usize| Point::new(r(i) * s, r(i + 1) * s, r(i + 2) * s);
2083 let direction = |i: usize, default: Vector| -> OgeomResult<Direction> {
2085 let v = Vector::new(r(i), r(i + 1), r(i + 2));
2086 Direction::new(if v.magnitude() > 0.0 { v } else { default }, tol)
2087 };
2088 let shape = match entity.kind {
2089 150 => {
2090 let frame = Frame::new(
2091 point(3),
2092 direction(9, Vector::Z)?,
2093 direction(6, Vector::X)?,
2094 tol,
2095 )?;
2096 ogeom_algo::make_box(
2097 &mut self.model,
2098 frame,
2099 (length(0), length(1), length(2)),
2100 tol,
2101 )?
2102 .shape
2103 }
2104 152 => {
2105 let x_file = direction(7, Vector::X)?;
2110 let z_file = direction(10, Vector::Z)?;
2111 let y_file = Direction::new(z_file.vector().cross(x_file.vector()), tol)?;
2112 let frame = Frame::new(point(4), y_file, z_file, tol)?;
2113 ogeom_algo::make_wedge(
2114 &mut self.model,
2115 frame,
2116 (length(2), length(0), length(1)),
2117 (length(2), length(3)),
2118 tol,
2119 )?
2120 .shape
2121 }
2122 154 => {
2123 let frame = frame_about(point(2), direction(5, Vector::Z)?, tol)?;
2124 ogeom_algo::make_cylinder(&mut self.model, frame, length(1), length(0), tol)?.shape
2125 }
2126 156 => {
2127 let frame = frame_about(point(3), direction(6, Vector::Z)?, tol)?;
2128 ogeom_algo::make_cone(&mut self.model, frame, length(1), length(2), length(0), tol)?
2129 .shape
2130 }
2131 158 => {
2132 let frame = frame_about(point(1), Direction::Z, tol)?;
2133 ogeom_algo::make_sphere(&mut self.model, frame, length(0), tol)?.shape
2134 }
2135 160 => {
2136 let frame = frame_about(point(2), direction(5, Vector::Z)?, tol)?;
2137 ogeom_algo::make_torus(&mut self.model, frame, length(0), length(1), tol)?.shape
2138 }
2139 162 => {
2140 let axis = ogeom_math::Axis::new(point(2), direction(5, Vector::Z)?);
2143 let fraction = match r(1) {
2144 f if f > 0.0 => f.min(1.0),
2145 _ => 1.0,
2146 };
2147 let face = self.profile_face(de, entity.at(0).int(), Some(axis))?;
2148 ogeom_algo::make_revolution(
2149 &mut self.model,
2150 &face,
2151 axis,
2152 core::f64::consts::TAU * fraction,
2153 tol,
2154 )?
2155 .shape
2156 }
2157 164 => {
2158 let face = self.profile_face(de, entity.at(0).int(), None)?;
2159 let along = direction(2, Vector::Z)?.vector() * length(1);
2160 ogeom_algo::make_prism(&mut self.model, &face, along, tol)?.shape
2161 }
2162 168 => {
2163 let x = direction(6, Vector::X)?.vector();
2165 let z = direction(9, Vector::Z)?.vector();
2166 let y = z.cross(x);
2167 let (a, b, c) = (length(0), length(1), length(2));
2168 let linear = Matrix3::new([
2169 [x.x * a, y.x * b, z.x * c],
2170 [x.y * a, y.y * b, z.y * c],
2171 [x.z * a, y.z * b, z.z * c],
2172 ]);
2173 let ball = ogeom_algo::make_sphere(&mut self.model, Frame::WORLD, 1.0, tol)?.shape;
2174 let stretch = ogeom_math::GeneralTransform::new(linear, point(3).to_vector());
2175 ogeom_algo::general_transformed_shape(&mut self.model, &ball, &stretch, tol)?.shape
2176 }
2177 180 => {
2178 let n = usize::try_from(entity.at(0).int()).unwrap_or(0);
2181 let mut stack: Vec<Shape> = Vec::new();
2182 for i in 0..n {
2183 let item = entity.at(1 + i).int();
2184 if item < 0 {
2185 stack.push(self.csg(-item)?);
2186 continue;
2187 }
2188 let (Some(b), Some(a)) = (stack.pop(), stack.pop()) else {
2189 ogeom_bail!(
2190 Construction,
2191 "D{de}: a boolean tree's operation has too few operands"
2192 );
2193 };
2194 let result = match item {
2195 1 => ogeom_bool::fuse(&mut self.model, &a, &b, tol)?,
2196 2 => ogeom_bool::common(&mut self.model, &a, &b, tol)?,
2197 3 => ogeom_bool::cut(&mut self.model, &a, &b, tol)?,
2198 code => ogeom_bail!(
2199 Construction,
2200 "D{de}: boolean operation code {code} names no operation"
2201 ),
2202 };
2203 stack.push(result.shape);
2204 }
2205 let [solid] = stack.as_slice() else {
2206 ogeom_bail!(
2207 Construction,
2208 "D{de}: a boolean tree leaves {} results",
2209 stack.len()
2210 );
2211 };
2212 solid.clone()
2213 }
2214 430 => self.csg(entity.at(0).int())?,
2215 186 => self.manifold_solid(de)?,
2216 kind => ogeom_bail!(
2217 Construction,
2218 "D{de}: type {kind} is not a constructive solid"
2219 ),
2220 };
2221 let placement = self.placement(entity)?;
2222 if placement == Transform::IDENTITY {
2223 return Ok(shape);
2224 }
2225 let location = ogeom_topo::Location::of(self.model.add_datum(placement));
2226 Ok(shape.moved(&location))
2227 }
2228
2229 fn profile_face(
2232 &mut self,
2233 de: i64,
2234 curve: i64,
2235 axis: Option<ogeom_math::Axis>,
2236 ) -> OgeomResult<Shape> {
2237 let mut segments = self.curve_segments(curve)?;
2238 let start = segments[0].0.point_at(segments[0].1.0, self.tol)?;
2239 let last = &segments[segments.len() - 1];
2240 let end = last.0.point_at(last.1.1, self.tol)?;
2241 if start.distance(end) > self.tol.confusion() * 100.0 {
2242 let Some(axis) = axis else {
2243 ogeom_bail!(Construction, "D{de}: an extruded profile does not close");
2244 };
2245 let foot = |p: Point| {
2246 let o = axis.location;
2247 let d = axis.direction.vector();
2248 o + d * (p - o).dot(d)
2249 };
2250 let (end_foot, start_foot) = (foot(end), foot(start));
2251 for (a, b) in [(end, end_foot), (end_foot, start_foot), (start_foot, start)] {
2252 if a.distance(b) > self.tol.confusion() * 100.0 {
2253 let line: Curve = LineCurve::segment(a, b, self.tol)?.into();
2254 let domain = line.domain();
2255 segments.push((line, domain));
2256 }
2257 }
2258 }
2259 let edges = self.wire_edges(de, segments)?;
2260 let wire = ogeom_algo::make_wire(&mut self.model, &edges, self.tol)?.shape;
2261 let Some(plane) = ogeom_algo::find_plane(&self.model, &wire, self.tol)? else {
2262 ogeom_bail!(Construction, "D{de}: a solid's profile is not planar");
2263 };
2264 let surface: SurfaceGeometry = PlaneSurface::over(
2265 plane,
2266 (-SURFACE_EXTENT, SURFACE_EXTENT),
2267 (-SURFACE_EXTENT, SURFACE_EXTENT),
2268 )?
2269 .into();
2270 Ok(ogeom_algo::make_face_with_pcurves(&mut self.model, surface, &[edges], self.tol)?.shape)
2271 }
2272
2273 #[allow(
2279 clippy::type_complexity,
2280 reason = "the solids, the sheets, and what built what"
2281 )]
2282 fn subfigure_instances(
2283 &mut self,
2284 tol: Tolerances,
2285 ) -> OgeomResult<(Vec<Shape>, Vec<Shape>, Vec<(i64, Shape)>)> {
2286 let instances: Vec<i64> = self
2287 .file
2288 .entities
2289 .iter()
2290 .filter(|(_, e)| e.kind == 408 && (e.status / 10_000) % 100 == 0)
2291 .map(|(de, _)| *de)
2292 .collect();
2293 let mut built: HashMap<i64, (Vec<Shape>, Vec<Shape>)> = HashMap::new();
2294 let (mut solids, mut sheets, mut from) = (Vec::new(), Vec::new(), Vec::new());
2295 for de in instances {
2296 let (placed_solids, faces) = match self.instance(de, &mut built, 0) {
2297 Ok(found) => found,
2298 Err(e) => {
2299 self.report
2300 .warnings
2301 .push(format!("D{de}: subfigure instance not placed: {e}"));
2302 continue;
2303 }
2304 };
2305 from.extend(placed_solids.iter().map(|s| (de, s.clone())));
2306 from.extend(faces.iter().map(|f| (de, f.clone())));
2307 solids.extend(placed_solids);
2308 if faces.is_empty() {
2309 continue;
2310 }
2311 let sewn = sew(&mut self.model, &faces, tol)?;
2312 for shell in &sewn.shells {
2313 if ogeom_algo::is_shell_closed(&self.model, shell)? {
2314 solids.push(make_solid(&mut self.model, std::slice::from_ref(shell))?.shape);
2315 } else {
2316 sheets.push(shell.clone());
2317 }
2318 }
2319 }
2320 Ok((solids, sheets, from))
2321 }
2322
2323 fn instance(
2326 &mut self,
2327 de: i64,
2328 built: &mut HashMap<i64, (Vec<Shape>, Vec<Shape>)>,
2329 depth: usize,
2330 ) -> OgeomResult<(Vec<Shape>, Vec<Shape>)> {
2331 if depth > 32 {
2332 ogeom_bail!(
2333 Construction,
2334 "D{de}: subfigures nest past any sensible depth"
2335 );
2336 }
2337 let entity = self.entity(de)?;
2338 let definition = entity.at(0).int();
2339 let s = self.report.scale_mm;
2340 let offset = Vector::new(
2341 entity.at(1).real() * s,
2342 entity.at(2).real() * s,
2343 entity.at(3).real() * s,
2344 );
2345 let factor = match entity.at(4).real() {
2346 f if f > 0.0 => f,
2347 _ => 1.0,
2348 };
2349 let (solids, faces) = self.subfigure(definition, built, depth)?;
2350 let motion = self.placement(entity)?
2351 * Transform::translation(offset)
2352 * Transform::scaling(Point::ORIGIN, factor, self.tol)?;
2353 let location = ogeom_topo::Location::of(self.model.add_datum(motion));
2354 Ok((
2355 solids.iter().map(|x| x.moved(&location)).collect(),
2356 faces.iter().map(|x| x.moved(&location)).collect(),
2357 ))
2358 }
2359
2360 fn subfigure(
2363 &mut self,
2364 de: i64,
2365 built: &mut HashMap<i64, (Vec<Shape>, Vec<Shape>)>,
2366 depth: usize,
2367 ) -> OgeomResult<(Vec<Shape>, Vec<Shape>)> {
2368 if let Some(found) = built.get(&de) {
2369 return Ok(found.clone());
2370 }
2371 let entity = self.entity(de)?;
2372 if entity.kind != 308 {
2373 ogeom_bail!(
2374 Construction,
2375 "D{de}: an instance names a type {} entity, not a subfigure \
2376 definition",
2377 entity.kind
2378 );
2379 }
2380 let count = usize::try_from(entity.at(2).int()).unwrap_or(0);
2381 let (mut solids, mut faces) = (Vec::new(), Vec::new());
2382 for i in 0..count {
2383 let member = entity.at(3 + i).int();
2384 let Some(kind) = self.file.entity(member).map(|m| m.kind) else {
2385 continue;
2386 };
2387 match kind {
2388 186 => solids.push(self.manifold_solid(member)?),
2389 143 | 144 => faces.push(self.face(member)?),
2390 408 => {
2391 let (s, f) = self.instance(member, built, depth + 1)?;
2392 solids.extend(s);
2393 faces.extend(f);
2394 }
2395 _ => {}
2396 }
2397 }
2398 built.insert(de, (solids.clone(), faces.clone()));
2399 Ok((solids, faces))
2400 }
2401
2402 fn annotations(
2411 &mut self,
2412 ) -> (
2413 Vec<ogeom_doc::Callout>,
2414 Vec<(ogeom_doc::Callout, ogeom_doc::Dimension)>,
2415 ) {
2416 let mut on_sheets: std::collections::BTreeSet<i64> = std::collections::BTreeSet::new();
2417 for entity in self.file.entities.values().filter(|e| e.kind == 404) {
2418 let views = usize::try_from(entity.at(0).int()).unwrap_or(0);
2419 let at = 1 + 3 * views;
2420 let count = usize::try_from(entity.at(at).int()).unwrap_or(0);
2421 for i in 0..count {
2422 on_sheets.insert(entity.at(at + 1 + i).int().abs());
2423 }
2424 }
2425 let drafting: Vec<i64> = self
2426 .file
2427 .entities
2428 .iter()
2429 .filter(|(de, e)| {
2430 matches!(
2431 e.kind,
2432 202 | 206 | 208 | 210 | 212 | 214 | 216 | 218 | 220 | 222 | 228
2433 ) && (e.status / 10_000) % 100 == 0
2434 && !on_sheets.contains(de)
2435 })
2436 .map(|(de, _)| *de)
2437 .collect();
2438 let mut callouts = Vec::new();
2439 let mut dimensions = Vec::new();
2440 for de in drafting {
2441 match self.drafting(de) {
2442 Ok((callout, Some(dimension))) => dimensions.push((callout, dimension)),
2443 Ok((callout, None)) => callouts.push(callout),
2444 Err(e) => self
2445 .report
2446 .warnings
2447 .push(format!("D{de}: annotation not drawn: {e}")),
2448 }
2449 }
2450 (callouts, dimensions)
2451 }
2452
2453 fn drafting(
2456 &mut self,
2457 de: i64,
2458 ) -> OgeomResult<(ogeom_doc::Callout, Option<ogeom_doc::Dimension>)> {
2459 let entity = self.entity(de)?;
2460 let pointer = |i: usize| entity.at(i).int();
2461 let mut polylines: Vec<Vec<Point>> = Vec::new();
2462 let mut text = String::new();
2463 let mut measure: Option<(&str, ogeom_doc::MeasureKind)> = None;
2464 let draw = |reader: &mut Self, lines: &mut Vec<Vec<Point>>, de: i64| -> OgeomResult<()> {
2465 if de != 0 {
2466 lines.push(reader.drafted_line(de)?);
2467 }
2468 Ok(())
2469 };
2470 match entity.kind {
2471 212 => text = self.note_text(de)?,
2472 214 => polylines.push(self.drafted_line(de)?),
2473 216 => {
2474 text = self.note_text(pointer(0))?;
2475 for i in 1..=4 {
2476 draw(self, &mut polylines, pointer(i))?;
2477 }
2478 measure = Some(("linear distance", ogeom_doc::MeasureKind::Length));
2479 }
2480 206 => {
2481 text = self.note_text(pointer(0))?;
2482 for i in 1..=2 {
2483 draw(self, &mut polylines, pointer(i))?;
2484 }
2485 measure = Some(("diameter", ogeom_doc::MeasureKind::Length));
2486 }
2487 222 => {
2488 text = self.note_text(pointer(0))?;
2489 draw(self, &mut polylines, pointer(1))?;
2490 if entity.params.len() > 4 {
2491 draw(self, &mut polylines, pointer(4))?;
2492 }
2493 measure = Some(("radius", ogeom_doc::MeasureKind::Length));
2494 }
2495 202 => {
2496 text = self.note_text(pointer(0))?;
2497 for i in [1, 2, 6, 7] {
2498 draw(self, &mut polylines, pointer(i))?;
2499 }
2500 measure = Some(("angle", ogeom_doc::MeasureKind::Angle));
2501 }
2502 218 | 220 => {
2503 text = self.note_text(pointer(0))?;
2504 draw(self, &mut polylines, pointer(1))?;
2505 measure = Some(("ordinate", ogeom_doc::MeasureKind::Length));
2506 if entity.kind == 220 {
2507 measure = None;
2508 }
2509 }
2510 208 | 210 => {
2511 let at = if entity.kind == 208 { 4 } else { 0 };
2514 text = self.note_text(pointer(at))?;
2515 let n = usize::try_from(pointer(at + 1)).unwrap_or(0);
2516 for i in 0..n {
2517 draw(self, &mut polylines, pointer(at + 2 + i))?;
2518 }
2519 }
2520 228 => {
2521 text = self.note_text(pointer(0))?;
2523 let ng = usize::try_from(pointer(1)).unwrap_or(0);
2524 for i in 0..ng {
2525 polylines.push(self.sampled_curve(pointer(2 + i))?);
2526 }
2527 let nl = usize::try_from(pointer(2 + ng)).unwrap_or(0);
2528 for i in 0..nl {
2529 draw(self, &mut polylines, pointer(3 + ng + i))?;
2530 }
2531 }
2532 kind => ogeom_bail!(Construction, "type {kind} is not drafting"),
2533 }
2534 let placement = self.placement(entity)?;
2535 let plane = Frame::new(
2536 placement.apply(Point::ORIGIN),
2537 Direction::new(placement.apply_vector(Vector::Z), self.tol)?,
2538 Direction::new(placement.apply_vector(Vector::X), self.tol)?,
2539 self.tol,
2540 )
2541 .ok();
2542 let callout = ogeom_doc::Callout {
2543 name: text.clone(),
2544 plane,
2545 polylines,
2546 annotates: None,
2547 };
2548 let dimension = measure.and_then(|(name, kind)| {
2549 let value = first_number(&text)?;
2550 let value = match kind {
2551 ogeom_doc::MeasureKind::Angle => value.to_radians(),
2552 ogeom_doc::MeasureKind::Length => value * self.report.scale_mm,
2553 };
2554 Some(ogeom_doc::Dimension {
2555 name: name.to_owned(),
2556 values: vec![value],
2557 kind,
2558 plus: None,
2559 minus: None,
2560 features: Vec::new(),
2561 location: entity.kind == 216 || entity.kind == 218,
2562 })
2563 });
2564 Ok((callout, dimension))
2565 }
2566
2567 fn note_text(&mut self, de: i64) -> OgeomResult<String> {
2569 if de == 0 {
2570 return Ok(String::new());
2571 }
2572 let entity = self.entity(de)?;
2573 if entity.kind != 212 {
2574 ogeom_bail!(Construction, "D{de}: type {} is not a note", entity.kind);
2575 }
2576 let count = usize::try_from(entity.at(0).int()).unwrap_or(0);
2577 let mut lines = Vec::with_capacity(count);
2578 for i in 0..count {
2579 if let super::parse::Value::Text(t) = entity.at(1 + 12 * i + 11) {
2580 lines.push(t.clone());
2581 }
2582 }
2583 Ok(lines.join("\n"))
2584 }
2585
2586 fn drafted_line(&mut self, de: i64) -> OgeomResult<Vec<Point>> {
2589 let entity = self.entity(de)?;
2590 let s = self.report.scale_mm;
2591 let mut points = Vec::new();
2592 match entity.kind {
2593 214 => {
2594 let n = usize::try_from(entity.at(0).int()).unwrap_or(0);
2595 let z = entity.at(3).real();
2596 points.push(Point::new(
2597 entity.at(4).real() * s,
2598 entity.at(5).real() * s,
2599 z * s,
2600 ));
2601 for i in 0..n {
2602 points.push(Point::new(
2603 entity.at(6 + 2 * i).real() * s,
2604 entity.at(7 + 2 * i).real() * s,
2605 z * s,
2606 ));
2607 }
2608 }
2609 106 => {
2610 let n = usize::try_from(entity.at(1).int()).unwrap_or(0);
2611 let z = entity.at(2).real();
2612 for i in 0..n {
2613 points.push(Point::new(
2614 entity.at(3 + 2 * i).real() * s,
2615 entity.at(4 + 2 * i).real() * s,
2616 z * s,
2617 ));
2618 }
2619 }
2620 _ => return self.sampled_curve(de),
2621 }
2622 let placement = self.placement(entity)?;
2623 Ok(points.into_iter().map(|p| placement.apply(p)).collect())
2624 }
2625
2626 fn sampled_curve(&mut self, de: i64) -> OgeomResult<Vec<Point>> {
2628 let (curve, range) = self.curve(de)?;
2629 (0..=32)
2630 .map(|i| {
2631 curve.point_at(
2632 range.0 + (range.1 - range.0) * f64::from(i) / 32.0,
2633 self.tol,
2634 )
2635 })
2636 .collect()
2637 }
2638
2639 fn document(&mut self, solids: &[(i64, Shape)], sheets: &[Shape]) -> ogeom_doc::Document {
2640 let colours: Vec<(Shape, ogeom_doc::Colour)> = solids
2641 .iter()
2642 .filter_map(|(de, s)| self.colour_of(*de).map(|c| (s.clone(), c)))
2643 .collect();
2644 let mut document = ogeom_doc::Document::over(std::mem::take(&mut self.model));
2645 for (i, (de, solid)) in solids.iter().enumerate() {
2646 let label = self
2647 .file
2648 .entity(*de)
2649 .map(|e| e.label.clone())
2650 .filter(|l| !l.is_empty())
2651 .unwrap_or_else(|| format!("solid-{i}"));
2652 document.add_part(label, solid.clone());
2653 }
2654 for (i, sheet) in sheets.iter().enumerate() {
2655 document.add_part(format!("sheet-{i}"), sheet.clone());
2656 }
2657 for (shape, colour) in colours {
2658 document.set_colour(&shape, colour);
2659 }
2660 document
2661 }
2662
2663 fn colour_of(&mut self, de: i64) -> Option<ogeom_doc::Colour> {
2666 let entity = self.file.entity(de)?;
2667 let c = entity.colour;
2668 if c < 0 {
2669 let e = self.file.entity(-c)?;
2670 self.visited.insert(-c, ());
2671 if e.kind != 314 {
2672 return None;
2673 }
2674 return Some(ogeom_doc::Colour::rgb(
2675 (e.at(0).real() / 100.0).clamp(0.0, 1.0),
2676 (e.at(1).real() / 100.0).clamp(0.0, 1.0),
2677 (e.at(2).real() / 100.0).clamp(0.0, 1.0),
2678 ));
2679 }
2680 let palette = [
2683 (0.0, 0.0, 0.0),
2684 (1.0, 0.0, 0.0),
2685 (0.0, 1.0, 0.0),
2686 (0.0, 0.0, 1.0),
2687 (1.0, 1.0, 0.0),
2688 (1.0, 0.0, 1.0),
2689 (0.0, 1.0, 1.0),
2690 (1.0, 1.0, 1.0),
2691 ];
2692 let index = usize::try_from(c).ok()?.checked_sub(1)?;
2693 palette
2694 .get(index)
2695 .map(|&(r, g, b)| ogeom_doc::Colour::rgb(r, g, b))
2696 }
2697}
2698
2699fn layers(
2707 file: &File,
2708 document: &mut ogeom_doc::Document,
2709 built_from: &[(i64, Shape)],
2710 report: &mut IgesReport,
2711) {
2712 let mut by_name: HashMap<String, ogeom_doc::LayerId> = HashMap::new();
2713 let mut layer = |document: &mut ogeom_doc::Document, name: String| {
2714 *by_name
2715 .entry(name.clone())
2716 .or_insert_with(|| document.add_layer(name))
2717 };
2718 for (de, shape) in built_from {
2719 let Some(entity) = file.entity(*de) else {
2720 continue;
2721 };
2722 let levels: Vec<i64> = if entity.level > 0 {
2723 vec![entity.level]
2724 } else if entity.level < 0 {
2725 match file.entity(-entity.level) {
2726 Some(p) if p.kind == 406 && p.form == 1 => {
2727 let n = usize::try_from(p.at(0).int()).unwrap_or(0);
2728 (0..n).map(|i| p.at(1 + i).int()).collect()
2729 }
2730 _ => {
2731 report
2732 .warnings
2733 .push(format!("D{de}: its level names no levels property"));
2734 Vec::new()
2735 }
2736 }
2737 } else {
2738 Vec::new()
2739 };
2740 for level in levels {
2741 let id = layer(document, format!("level {level}"));
2742 document.place_on_layer(shape, id);
2743 }
2744 }
2745 for (de, group) in &file.entities {
2746 if group.kind != 402 || !matches!(group.form, 1 | 7 | 14 | 15) {
2747 continue;
2748 }
2749 let n = usize::try_from(group.at(0).int()).unwrap_or(0);
2750 let members: Vec<i64> = (0..n).map(|i| group.at(1 + i).int().abs()).collect();
2751 let shapes: Vec<&Shape> = built_from
2752 .iter()
2753 .filter(|(from, _)| members.contains(from))
2754 .map(|(_, s)| s)
2755 .collect();
2756 if shapes.is_empty() {
2757 continue;
2758 }
2759 let name = if group.label.is_empty() {
2760 format!("group D{de}")
2761 } else {
2762 group.label.clone()
2763 };
2764 let id = layer(document, name);
2765 for shape in shapes {
2766 document.place_on_layer(shape, id);
2767 }
2768 }
2769}
2770
2771fn first_number(text: &str) -> Option<f64> {
2775 let trimmed = text.trim_start();
2776 let count = trimmed
2777 .find(|c: char| !c.is_ascii_digit())
2778 .filter(|&n| n > 0)
2779 .and_then(|n| {
2780 let rest = trimmed[n..].trim_start();
2781 rest.strip_prefix(['X', 'x']).map(|after| after.len())
2782 });
2783 let text = match count {
2784 Some(len) => &trimmed[trimmed.len() - len..],
2785 None => text,
2786 };
2787 let start = text.find(|c: char| c.is_ascii_digit() || c == '.')?;
2788 let tail = &text[start..];
2789 let end = tail
2790 .find(|c: char| !(c.is_ascii_digit() || c == '.'))
2791 .unwrap_or(tail.len());
2792 tail[..end].parse().ok()
2793}
2794
2795fn trimmed_to(curve: Curve, range: (f64, f64), tol: Tolerances) -> OgeomResult<Curve> {
2796 let (lo, hi) = curve.domain();
2797 if (range.0 - lo).abs() < tol.parametric() && (range.1 - hi).abs() < tol.parametric() {
2798 return Ok(curve);
2799 }
2800 Ok(Curve::from(TrimmedCurve::new(
2801 curve, range.0, range.1, tol,
2802 )?))
2803}
2804
2805use crate::inversion::parameter_on;
2806
2807fn frame_about(origin: Point, axis: Direction, tol: Tolerances) -> OgeomResult<Frame> {
2809 let seed = if axis.vector().dot(Vector::X).abs() < 0.9 {
2810 Vector::X
2811 } else {
2812 Vector::Y
2813 };
2814 let x = Direction::from_cross(axis.vector(), seed, tol)?;
2815 Frame::new(origin, axis, x, tol)
2816}
2817
2818#[cfg(test)]
2819#[allow(clippy::unwrap_used)]
2820mod tests {
2821 use super::super::parse::Value;
2822 use super::*;
2823
2824 const T: Tolerances = Tolerances::millimetres();
2825
2826 fn entity(kind: i64, form: i64, params: Vec<Value>) -> Entity {
2827 Entity {
2828 kind,
2829 form,
2830 transform: 0,
2831 colour: 0,
2832 level: 0,
2833 status: 0,
2834 label: String::new(),
2835 params,
2836 }
2837 }
2838
2839 fn file(entities: Vec<(i64, Entity)>) -> File {
2840 let mut global = vec![Value::Default; 16];
2841 global[13] = Value::Int(2);
2842 File {
2843 global,
2844 entities: entities.into_iter().collect(),
2845 }
2846 }
2847
2848 fn reader(file: &File) -> Reader<'_> {
2849 Reader {
2850 file,
2851 model: Model::new(),
2852 report: IgesReport {
2853 scale_mm: 1.0,
2854 ..IgesReport::default()
2855 },
2856 visited: BTreeMap::new(),
2857 vertices: HashMap::new(),
2858 edges: HashMap::new(),
2859 vertex_misses: (0, 0.0),
2860 tol: T,
2861 }
2862 }
2863
2864 fn reals(values: &[f64]) -> Vec<Value> {
2865 values.iter().map(|v| Value::Real(*v)).collect()
2866 }
2867
2868 fn line(from: [f64; 3], to: [f64; 3]) -> Entity {
2869 entity(
2870 110,
2871 0,
2872 reals(&[from[0], from[1], from[2], to[0], to[1], to[2]]),
2873 )
2874 }
2875
2876 #[test]
2881 fn hyperbola_and_parabola_conic_arcs_read_as_their_curves() {
2882 let (cosh1, sinh1) = (1.0_f64.cosh(), 1.0_f64.sinh());
2883 let hyperbola = |forward: bool| {
2885 let (s, e) = if forward {
2886 ([2.0, 0.0], [2.0 * cosh1, 3.0 * sinh1])
2887 } else {
2888 ([2.0 * cosh1, 3.0 * sinh1], [2.0, 0.0])
2889 };
2890 entity(
2891 104,
2892 2,
2893 reals(&[
2894 0.25,
2895 0.0,
2896 -1.0 / 9.0,
2897 0.0,
2898 0.0,
2899 -1.0,
2900 0.0,
2901 s[0],
2902 s[1],
2903 e[0],
2904 e[1],
2905 ]),
2906 )
2907 };
2908 let parabola_y = entity(
2910 104,
2911 3,
2912 reals(&[1.0, 0.0, 0.0, 0.0, -4.0, 0.0, 0.0, 0.0, 0.0, 4.0, 4.0]),
2913 );
2914 let parabola_x = entity(
2915 104,
2916 3,
2917 reals(&[0.0, 0.0, 1.0, -4.0, 0.0, 0.0, 0.0, 4.0, 4.0, 0.0, 0.0]),
2918 );
2919 let deck = file(vec![
2920 (1, hyperbola(true)),
2921 (3, hyperbola(false)),
2922 (5, parabola_y),
2923 (7, parabola_x),
2924 ]);
2925 let mut reader = reader(&deck);
2926 for de in [1, 3] {
2927 let (curve, (t0, t1)) = reader.curve(de).unwrap();
2928 assert!(matches!(curve, Curve::Hyperbola(_)), "D{de} is a hyperbola");
2929 assert!(t1 > t0, "the arc runs forward: {t0} .. {t1}");
2930 let entity = deck.entity(de).unwrap();
2931 let start = Point::new(entity.at(7).real(), entity.at(8).real(), 0.0);
2932 let end = Point::new(entity.at(9).real(), entity.at(10).real(), 0.0);
2933 assert!(curve.point_at(t0, T).unwrap().distance(start) < 1e-9);
2934 assert!(curve.point_at(t1, T).unwrap().distance(end) < 1e-9);
2935 let mid = curve.point_at(f64::midpoint(t0, t1), T).unwrap();
2936 assert!((mid.x * mid.x / 4.0 - mid.y * mid.y / 9.0 - 1.0).abs() < 1e-9);
2937 }
2938 for (de, along_y) in [(5, true), (7, false)] {
2939 let (curve, (t0, t1)) = reader.curve(de).unwrap();
2940 assert!(matches!(curve, Curve::Parabola(_)), "D{de} is a parabola");
2941 assert!(t1 > t0);
2942 let (start, end) = if along_y {
2943 (Point::ORIGIN, Point::new(4.0, 4.0, 0.0))
2944 } else {
2945 (Point::new(4.0, 4.0, 0.0), Point::ORIGIN)
2946 };
2947 assert!(curve.point_at(t0, T).unwrap().distance(start) < 1e-9);
2948 assert!(curve.point_at(t1, T).unwrap().distance(end) < 1e-9);
2949 let mid = curve.point_at(f64::midpoint(t0, t1), T).unwrap();
2950 let residual = if along_y {
2951 mid.x * mid.x - 4.0 * mid.y
2952 } else {
2953 mid.y * mid.y - 4.0 * mid.x
2954 };
2955 assert!(residual.abs() < 1e-9, "on the parabola: {mid:?}");
2956 }
2957 }
2958
2959 #[test]
2962 fn a_ruled_surface_reads_as_the_patch_between_its_curves() {
2963 let deck = file(vec![
2964 (1, line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])),
2965 (3, line([0.0, 5.0, 2.0], [10.0, 5.0, 2.0])),
2966 (
2967 5,
2968 entity(
2969 118,
2970 1,
2971 vec![Value::Int(1), Value::Int(3), Value::Int(0), Value::Int(0)],
2972 ),
2973 ),
2974 (
2975 7,
2976 entity(
2977 118,
2978 1,
2979 vec![Value::Int(1), Value::Int(3), Value::Int(1), Value::Int(0)],
2980 ),
2981 ),
2982 ]);
2983 let mut reader = reader(&deck);
2984 let straight = reader.surface(5).unwrap();
2985 let turned = reader.surface(7).unwrap();
2986 use ogeom_geom::Surface as _;
2987 let middle = straight.point_at(0.5, 0.5, T).unwrap();
2988 assert!(
2989 middle.distance(Point::new(5.0, 2.5, 1.0)) < 1e-9,
2990 "{middle:?}"
2991 );
2992 let far = straight.point_at(0.0, 1.0, T).unwrap();
2993 assert!(far.distance(Point::new(0.0, 5.0, 2.0)) < 1e-9, "{far:?}");
2994 let far = turned.point_at(0.0, 1.0, T).unwrap();
2995 assert!(far.distance(Point::new(10.0, 5.0, 2.0)) < 1e-9, "{far:?}");
2996 }
2997
2998 #[test]
3002 fn offset_entities_displace_the_way_the_file_says() {
3003 let deck = file(vec![
3004 (1, entity(108, 0, reals(&[0.0, 0.0, 1.0, 0.0]))),
3005 (
3006 3,
3007 entity(
3008 140,
3009 0,
3010 vec![
3011 Value::Real(0.0),
3012 Value::Real(0.0),
3013 Value::Real(1.0),
3014 Value::Real(3.0),
3015 Value::Int(1),
3016 ],
3017 ),
3018 ),
3019 (
3020 5,
3021 entity(
3022 140,
3023 0,
3024 vec![
3025 Value::Real(0.0),
3026 Value::Real(0.0),
3027 Value::Real(-1.0),
3028 Value::Real(3.0),
3029 Value::Int(1),
3030 ],
3031 ),
3032 ),
3033 (7, line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])),
3034 (
3035 9,
3036 entity(
3037 130,
3038 0,
3039 vec![
3040 Value::Int(7),
3041 Value::Int(1),
3042 Value::Int(0),
3043 Value::Int(3),
3044 Value::Int(0),
3045 Value::Real(2.0),
3046 Value::Real(0.0),
3047 Value::Real(2.0),
3048 Value::Real(0.0),
3049 Value::Real(0.0),
3050 Value::Real(0.0),
3051 Value::Real(1.0),
3052 Value::Real(0.0),
3053 Value::Real(10.0),
3054 ],
3055 ),
3056 ),
3057 ]);
3058 let mut reader = reader(&deck);
3059 use ogeom_geom::Surface as _;
3060 let up = reader.surface(3).unwrap();
3061 assert!((up.point_at(1.0, 2.0, T).unwrap().z - 3.0).abs() < 1e-9);
3062 let down = reader.surface(5).unwrap();
3063 assert!((down.point_at(1.0, 2.0, T).unwrap().z + 3.0).abs() < 1e-9);
3064 let (offset, (t0, t1)) = reader.curve(9).unwrap();
3065 assert!(matches!(offset, Curve::Offset(_)));
3066 assert!((t0, t1) == (0.0, 10.0));
3067 let at = offset.point_at(5.0, T).unwrap();
3068 assert!(at.distance(Point::new(5.0, 2.0, 0.0)) < 1e-9, "{at:?}");
3069 }
3070
3071 #[test]
3074 fn a_rotated_conic_reads_as_its_turned_curve() {
3075 let theta = 30.0_f64.to_radians();
3076 let (sin, cos) = theta.sin_cos();
3077 let (a2, b2) = (16.0, 4.0);
3078 let coefficients = [
3079 cos * cos / a2 + sin * sin / b2,
3080 2.0 * sin * cos * (1.0 / a2 - 1.0 / b2),
3081 sin * sin / a2 + cos * cos / b2,
3082 0.0,
3083 0.0,
3084 -1.0,
3085 ];
3086 let start = [4.0 * cos, 4.0 * sin];
3087 let end = [-2.0 * sin, 2.0 * cos];
3088 let mut values = coefficients.to_vec();
3089 values.extend([0.0, start[0], start[1], end[0], end[1]]);
3090 let deck = file(vec![(1, entity(104, 1, reals(&values)))]);
3091 let mut reader = reader(&deck);
3092 let (curve, (t0, t1)) = reader.curve(1).unwrap();
3093 assert!(matches!(curve, Curve::Ellipse(_)), "a turned ellipse");
3094 assert!(
3095 curve
3096 .point_at(t0, T)
3097 .unwrap()
3098 .distance(Point::new(start[0], start[1], 0.0))
3099 < 1e-9
3100 );
3101 assert!(
3102 curve
3103 .point_at(t1, T)
3104 .unwrap()
3105 .distance(Point::new(end[0], end[1], 0.0))
3106 < 1e-9
3107 );
3108 for k in 0..=8 {
3109 let p = curve
3110 .point_at(t0 + (t1 - t0) * f64::from(k) / 8.0, T)
3111 .unwrap();
3112 let [a, b, c, _, _, f] = coefficients;
3113 let residual = a * p.x * p.x + b * p.x * p.y + c * p.y * p.y + f;
3114 assert!(residual.abs() < 1e-9, "on the conic: {p:?}");
3115 }
3116 }
3117
3118 #[test]
3122 fn a_linearly_varying_offset_curve_reads_as_its_fitted_displacement() {
3123 let deck = file(vec![
3124 (1, line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])),
3125 (
3126 3,
3127 entity(
3128 130,
3129 0,
3130 vec![
3131 Value::Int(1),
3132 Value::Int(2),
3133 Value::Int(0),
3134 Value::Int(0),
3135 Value::Int(0),
3136 Value::Real(1.0),
3137 Value::Real(0.0),
3138 Value::Real(3.0),
3139 Value::Real(10.0),
3140 Value::Real(0.0),
3141 Value::Real(0.0),
3142 Value::Real(1.0),
3143 Value::Real(0.0),
3144 Value::Real(0.0),
3145 ],
3146 ),
3147 ),
3148 ]);
3149 let mut reader = reader(&deck);
3150 let (curve, (t0, t1)) = reader.curve(3).unwrap();
3151 for k in 0..=10 {
3152 let t = t0 + (t1 - t0) * f64::from(k) / 10.0;
3153 let p = curve.point_at(t, T).unwrap();
3154 let want = Point::new(p.x, 1.0 + 2.0 * p.x / 10.0, 0.0);
3155 assert!(p.distance(want) < 1e-5, "{p:?} against {want:?}");
3156 }
3157 assert!(
3158 curve
3159 .point_at(t0, T)
3160 .unwrap()
3161 .distance(Point::new(0.0, 1.0, 0.0))
3162 < 1e-5
3163 );
3164 assert!(
3165 curve
3166 .point_at(t1, T)
3167 .unwrap()
3168 .distance(Point::new(10.0, 3.0, 0.0))
3169 < 1e-5
3170 );
3171 }
3172
3173 #[test]
3178 fn a_parametric_spline_surface_reads_as_its_exact_bspline() {
3179 let patch = |x: &[(usize, f64)], y: &[(usize, f64)], z: &[(usize, f64)]| -> Vec<f64> {
3180 let mut out = vec![0.0; 48];
3181 for (axis, terms) in [x, y, z].iter().enumerate() {
3182 for &(k, v) in *terms {
3183 out[16 * axis + k] = v;
3184 }
3185 }
3186 out
3187 };
3188 let header = |m: i64, n: i64, tu: &[f64], tv: &[f64]| -> Vec<Value> {
3189 let mut out = vec![Value::Int(3), Value::Int(0), Value::Int(m), Value::Int(n)];
3190 out.extend(reals(tu));
3191 out.extend(reals(tv));
3192 out
3193 };
3194 let whole = {
3196 let mut v = header(1, 1, &[0.0, 2.0], &[0.0, 3.0]);
3197 v.extend(reals(&patch(&[(1, 1.0)], &[(4, 1.0)], &[(5, 1.0)])));
3198 v.extend(reals(&[0.0; 48]));
3199 v.extend(reals(&[0.0; 96]));
3200 v
3201 };
3202 let split = {
3205 let mut v = header(2, 1, &[0.0, 1.0, 2.0], &[0.0, 3.0]);
3206 v.extend(reals(&patch(&[(1, 1.0)], &[(4, 1.0)], &[(5, 1.0)])));
3207 v.extend(reals(&[0.0; 48]));
3208 v.extend(reals(&patch(
3209 &[(0, 1.0), (1, 1.0)],
3210 &[(4, 1.0)],
3211 &[(4, 1.0), (5, 1.0)],
3212 )));
3213 v.extend(reals(&[0.0; 48]));
3214 v.extend(reals(&[0.0; 96]));
3215 v
3216 };
3217 let deck = file(vec![(1, entity(114, 0, whole)), (3, entity(114, 0, split))]);
3218 let mut reader = reader(&deck);
3219 for de in [1, 3] {
3220 let surface = reader.surface(de).unwrap();
3221 for (u, v) in [(0.0, 0.0), (0.5, 1.0), (1.0, 2.5), (1.5, 0.7), (2.0, 3.0)] {
3222 let p = ogeom_geom::Surface::point_at(&surface, u, v, T).unwrap();
3223 let want = Point::new(u, v, u * v);
3224 assert!(p.distance(want) < 1e-12, "D{de} at ({u}, {v}): {p:?}");
3225 }
3226 }
3227 }
3228
3229 #[test]
3234 fn a_parameter_space_trim_lifts_through_its_bspline_surface() {
3235 let surface = entity(
3237 128,
3238 0,
3239 vec![
3240 Value::Int(1),
3241 Value::Int(1),
3242 Value::Int(1),
3243 Value::Int(1),
3244 Value::Int(0),
3245 Value::Int(0),
3246 Value::Int(1),
3247 Value::Int(0),
3248 Value::Int(0),
3249 Value::Real(0.0),
3250 Value::Real(0.0),
3251 Value::Real(1.0),
3252 Value::Real(1.0),
3253 Value::Real(0.0),
3254 Value::Real(0.0),
3255 Value::Real(1.0),
3256 Value::Real(1.0),
3257 Value::Real(1.0),
3258 Value::Real(1.0),
3259 Value::Real(1.0),
3260 Value::Real(1.0),
3261 Value::Real(0.0),
3262 Value::Real(0.0),
3263 Value::Real(0.0),
3264 Value::Real(10.0),
3265 Value::Real(0.0),
3266 Value::Real(0.0),
3267 Value::Real(0.0),
3268 Value::Real(10.0),
3269 Value::Real(0.0),
3270 Value::Real(10.0),
3271 Value::Real(10.0),
3272 Value::Real(0.0),
3273 Value::Real(0.0),
3274 Value::Real(1.0),
3275 Value::Real(0.0),
3276 Value::Real(1.0),
3277 ],
3278 );
3279 let on_surface = |model_curve: i64| {
3280 entity(
3281 142,
3282 0,
3283 vec![
3284 Value::Int(0),
3285 Value::Int(1),
3286 Value::Int(3),
3287 Value::Int(model_curve),
3288 Value::Int(0),
3289 ],
3290 )
3291 };
3292 let deck = file(vec![
3293 (1, surface),
3294 (3, line([0.2, 0.2, 0.0], [0.8, 0.8, 0.0])),
3295 (5, on_surface(0)),
3296 (7, line([2.0, 2.0, 0.0], [8.0, 8.0, 0.0])),
3297 (9, on_surface(7)),
3298 ]);
3299 let mut reader = reader(&deck);
3300 for de in [5, 9] {
3301 let segments = reader.boundary_segments(de).unwrap();
3302 let [(curve, (t0, t1))] = segments.as_slice() else {
3303 panic!("one segment");
3304 };
3305 assert!(
3306 curve
3307 .point_at(*t0, T)
3308 .unwrap()
3309 .distance(Point::new(2.0, 2.0, 0.0))
3310 < 1e-6
3311 );
3312 assert!(
3313 curve
3314 .point_at(*t1, T)
3315 .unwrap()
3316 .distance(Point::new(8.0, 8.0, 0.0))
3317 < 1e-6
3318 );
3319 let mid = curve.point_at(f64::midpoint(*t0, *t1), T).unwrap();
3320 assert!(
3321 (mid.x - mid.y).abs() < 1e-6 && mid.z.abs() < 1e-9,
3322 "D{de} {mid:?}"
3323 );
3324 }
3325 }
3326
3327 #[test]
3332 fn a_parameter_space_trim_lifts_through_an_analytic_surface() {
3333 let point = |p: [f64; 3]| entity(116, 0, reals(&p));
3334 let direction = |d: [f64; 3]| entity(123, 0, reals(&d));
3335 let on_surface = |surface: i64| {
3336 entity(
3337 142,
3338 0,
3339 vec![
3340 Value::Int(0),
3341 Value::Int(surface),
3342 Value::Int(3),
3343 Value::Int(0),
3344 Value::Int(0),
3345 ],
3346 )
3347 };
3348 type Case = (Entity, [f64; 2], [f64; 2], Point, Point);
3350 let cases: Vec<Case> = vec![
3351 (
3352 entity(
3354 192,
3355 1,
3356 vec![
3357 Value::Int(11),
3358 Value::Int(13),
3359 Value::Real(2.0),
3360 Value::Int(15),
3361 ],
3362 ),
3363 [0.0, 0.0],
3364 [90.0, 5.0],
3365 Point::new(0.0, 2.0, 0.0),
3366 Point::new(-2.0, 0.0, 5.0),
3367 ),
3368 (
3369 entity(
3371 196,
3372 1,
3373 vec![
3374 Value::Int(11),
3375 Value::Real(3.0),
3376 Value::Int(13),
3377 Value::Int(15),
3378 ],
3379 ),
3380 [0.0, 0.0],
3381 [90.0, 45.0],
3382 Point::new(0.0, 3.0, 0.0),
3383 Point::new(
3384 -3.0 * core::f64::consts::FRAC_1_SQRT_2,
3385 0.0,
3386 3.0 * core::f64::consts::FRAC_1_SQRT_2,
3387 ),
3388 ),
3389 (
3390 entity(
3394 198,
3395 1,
3396 vec![
3397 Value::Int(11),
3398 Value::Int(13),
3399 Value::Real(5.0),
3400 Value::Real(1.0),
3401 Value::Int(15),
3402 ],
3403 ),
3404 [0.0, 360.0],
3405 [90.0, 270.0],
3406 Point::new(0.0, 6.0, 0.0),
3407 Point::new(-5.0, 0.0, 1.0),
3408 ),
3409 ];
3410 for (surface, from, to, at_from, at_to) in cases {
3411 let deck = file(vec![
3412 (1, surface),
3413 (3, line([from[0], from[1], 0.0], [to[0], to[1], 0.0])),
3414 (5, on_surface(1)),
3415 (11, point([0.0, 0.0, 0.0])),
3416 (13, direction([0.0, 0.0, 1.0])),
3417 (15, direction([0.0, 1.0, 0.0])),
3418 ]);
3419 let mut reader = reader(&deck);
3420 let segments = reader.boundary_segments(5).unwrap();
3421 let [(curve, (t0, t1))] = segments.as_slice() else {
3422 panic!("one segment");
3423 };
3424 let (a, b) = (
3425 curve.point_at(*t0, T).unwrap(),
3426 curve.point_at(*t1, T).unwrap(),
3427 );
3428 assert!(a.distance(at_from) < 1e-6, "{a:?} against {at_from:?}");
3429 assert!(b.distance(at_to) < 1e-6, "{b:?} against {at_to:?}");
3430 }
3431 }
3432
3433 #[test]
3437 fn a_parameter_space_trim_lifts_through_a_swept_surface() {
3438 let on_surface = |surface: i64| {
3439 entity(
3440 142,
3441 0,
3442 vec![
3443 Value::Int(0),
3444 Value::Int(surface),
3445 Value::Int(3),
3446 Value::Int(0),
3447 Value::Int(0),
3448 ],
3449 )
3450 };
3451 let tabulated = entity(
3454 122,
3455 0,
3456 vec![
3457 Value::Int(7),
3458 Value::Real(0.0),
3459 Value::Real(0.0),
3460 Value::Real(4.0),
3461 ],
3462 );
3463 let revolved = entity(
3467 120,
3468 0,
3469 vec![
3470 Value::Int(9),
3471 Value::Int(7),
3472 Value::Real(0.0),
3473 Value::Real(core::f64::consts::FRAC_PI_2),
3474 ],
3475 );
3476 let cases = [
3477 (
3478 tabulated,
3479 line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0]),
3480 [0.5, 0.0],
3481 [0.5, 1.0],
3482 Point::new(5.0, 0.0, 0.0),
3483 Point::new(5.0, 0.0, 4.0),
3484 ),
3485 (
3486 revolved,
3487 line([2.0, 0.0, 0.0], [2.0, 0.0, 6.0]),
3488 [0.0, 0.0],
3489 [1.0, core::f64::consts::FRAC_PI_2],
3490 Point::new(2.0, 0.0, 0.0),
3491 Point::new(0.0, 2.0, 6.0),
3492 ),
3493 ];
3494 for (surface, curve, from, to, at_from, at_to) in cases {
3495 let deck = file(vec![
3496 (1, surface),
3497 (3, line([from[0], from[1], 0.0], [to[0], to[1], 0.0])),
3498 (5, on_surface(1)),
3499 (7, curve),
3500 (9, line([0.0, 0.0, 0.0], [0.0, 0.0, 1.0])),
3501 ]);
3502 let mut reader = reader(&deck);
3503 let segments = reader.boundary_segments(5).unwrap();
3504 let [(lifted, (t0, t1))] = segments.as_slice() else {
3505 panic!("one segment");
3506 };
3507 let (a, b) = (
3508 lifted.point_at(*t0, T).unwrap(),
3509 lifted.point_at(*t1, T).unwrap(),
3510 );
3511 assert!(a.distance(at_from) < 1e-6, "{a:?} against {at_from:?}");
3512 assert!(b.distance(at_to) < 1e-6, "{b:?} against {at_to:?}");
3513 }
3514 }
3515
3516 #[test]
3520 fn a_model_space_dimension_reads_as_its_callout_and_value() {
3521 let note = |text: &str| {
3522 let mut e = entity(
3523 212,
3524 0,
3525 vec![
3526 Value::Int(1),
3527 Value::Int(5),
3528 Value::Real(10.0),
3529 Value::Real(3.0),
3530 Value::Int(1),
3531 Value::Real(0.0),
3532 Value::Real(0.0),
3533 Value::Int(0),
3534 Value::Int(0),
3535 Value::Real(10.0),
3536 Value::Real(12.0),
3537 Value::Real(0.0),
3538 Value::Text(text.to_owned()),
3539 ],
3540 );
3541 e.status = 10_000;
3542 e
3543 };
3544 let leader = |head: [f64; 2], tail: [f64; 2]| {
3545 let mut e = entity(
3546 214,
3547 1,
3548 reals(&[1.0, 1.0, 0.5, 0.0, head[0], head[1], tail[0], tail[1]]),
3549 );
3550 e.status = 10_000;
3551 e
3552 };
3553 let witness = |a: [f64; 2], b: [f64; 2]| {
3554 let mut e = entity(106, 40, reals(&[1.0, 2.0, 0.0, a[0], a[1], b[0], b[1]]));
3555 e.status = 10_000;
3556 e
3557 };
3558 let dimension = entity(
3559 216,
3560 0,
3561 vec![
3562 Value::Int(3),
3563 Value::Int(5),
3564 Value::Int(7),
3565 Value::Int(9),
3566 Value::Int(11),
3567 ],
3568 );
3569 let mut sheet_note = note("on the sheet");
3570 sheet_note.status = 0;
3571 let drawing = entity(404, 0, vec![Value::Int(0), Value::Int(1), Value::Int(13)]);
3572 let deck = file(vec![
3573 (1, dimension),
3574 (3, note("2X 25.40")),
3575 (5, leader([0.0, 11.0], [8.0, 11.0])),
3576 (7, leader([25.4, 11.0], [17.0, 11.0])),
3577 (9, witness([0.0, 0.0], [0.0, 12.0])),
3578 (11, witness([25.4, 0.0], [25.4, 12.0])),
3579 (13, sheet_note),
3580 (15, drawing),
3581 ]);
3582 let mut reader = reader(&deck);
3583 let (callouts, dimensions) = reader.annotations();
3584 assert!(callouts.is_empty(), "the sheet's note stays on the sheet");
3585 let [(callout, dimension)] = dimensions.as_slice() else {
3586 panic!("one dimension, found {}", dimensions.len());
3587 };
3588 assert_eq!(callout.name, "2X 25.40");
3589 assert_eq!(
3590 callout.polylines.len(),
3591 4,
3592 "two leaders and two witness lines"
3593 );
3594 assert!(
3595 (dimension.values[0] - 25.4).abs() < 1e-12,
3596 "{:?}",
3597 dimension.values
3598 );
3599 assert!(dimension.location);
3600 assert!(!reader.visited.contains_key(&13));
3601 }
3602
3603 #[test]
3607 fn subfigure_instances_place_their_definition() {
3608 let dependent = |mut e: Entity| {
3609 e.status = 10_000;
3610 e
3611 };
3612 let square = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
3613 let mut entities = vec![(1, dependent(entity(108, 0, reals(&[0.0, 0.0, 1.0, 0.0]))))];
3614 let mut sides = Vec::new();
3615 for i in 0..4 {
3616 let (a, b) = (square[i], square[(i + 1) % 4]);
3617 let de = 3 + 2 * i64::try_from(i).unwrap();
3618 entities.push((de, dependent(line([a[0], a[1], 0.0], [b[0], b[1], 0.0]))));
3619 sides.push(Value::Int(de));
3620 }
3621 let mut composite = vec![Value::Int(4)];
3622 composite.extend(sides);
3623 entities.push((11, dependent(entity(102, 0, composite))));
3624 entities.push((
3625 13,
3626 dependent(entity(
3627 144,
3628 0,
3629 vec![Value::Int(1), Value::Int(1), Value::Int(0), Value::Int(11)],
3630 )),
3631 ));
3632 entities.push((
3633 15,
3634 entity(
3635 308,
3636 0,
3637 vec![
3638 Value::Int(0),
3639 Value::Text("tile".to_owned()),
3640 Value::Int(1),
3641 Value::Int(13),
3642 ],
3643 ),
3644 ));
3645 entities.push((
3646 17,
3647 entity(
3648 408,
3649 0,
3650 vec![
3651 Value::Int(15),
3652 Value::Real(100.0),
3653 Value::Real(0.0),
3654 Value::Real(0.0),
3655 Value::Real(1.0),
3656 ],
3657 ),
3658 ));
3659 entities.push((
3660 19,
3661 entity(
3662 408,
3663 0,
3664 vec![
3665 Value::Int(15),
3666 Value::Real(0.0),
3667 Value::Real(50.0),
3668 Value::Real(0.0),
3669 Value::Real(2.0),
3670 ],
3671 ),
3672 ));
3673 let deck = file(entities);
3674 let mut reader = reader(&deck);
3675 let (solids, sheets, _) = reader.subfigure_instances(T).unwrap();
3676 assert!(solids.is_empty());
3677 assert_eq!(sheets.len(), 2);
3678 let deflection = ogeom_mesh::Deflection::default();
3679 let mut measured: Vec<(f64, Point)> = sheets
3680 .iter()
3681 .map(|sheet| {
3682 let props =
3683 ogeom_algo::surface_properties(&reader.model, sheet, deflection, T).unwrap();
3684 (props.mass, props.centre)
3685 })
3686 .collect();
3687 measured.sort_by(|a, b| a.0.total_cmp(&b.0));
3688 assert!((measured[0].0 - 100.0).abs() < 1e-9, "{measured:?}");
3689 assert!(measured[0].1.distance(Point::new(105.0, 5.0, 0.0)) < 1e-9);
3690 assert!((measured[1].0 - 400.0).abs() < 1e-9, "{measured:?}");
3691 assert!(measured[1].1.distance(Point::new(10.0, 60.0, 0.0)) < 1e-9);
3692 let face_of = |shape: &Shape| {
3694 ogeom_topo::explore(
3695 &reader.model,
3696 shape,
3697 ogeom_topo::Filter::OfType(ogeom_topo::ShapeType::Face),
3698 )
3699 .unwrap()
3700 .remove(0)
3701 };
3702 assert_eq!(face_of(&sheets[0]).node(), face_of(&sheets[1]).node());
3703 }
3704
3705 #[test]
3708 fn levels_and_groups_read_as_layers() {
3709 let square = |z: f64, base: i64| -> Vec<(i64, Entity)> {
3710 let dependent = |mut e: Entity| {
3711 e.status = 10_000;
3712 e
3713 };
3714 let corners = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
3715 let mut out = vec![(base, dependent(entity(108, 0, reals(&[0.0, 0.0, 1.0, z]))))];
3716 let mut sides = vec![Value::Int(4)];
3717 for i in 0..4 {
3718 let (a, b) = (corners[i], corners[(i + 1) % 4]);
3719 let de = base + 2 + 2 * i64::try_from(i).unwrap();
3720 out.push((de, dependent(line([a[0], a[1], z], [b[0], b[1], z]))));
3721 sides.push(Value::Int(de));
3722 }
3723 out.push((base + 10, dependent(entity(102, 0, sides))));
3724 out
3725 };
3726 let mut entities = square(0.0, 1);
3727 entities.extend(square(5.0, 21));
3728 let mut low = entity(
3729 144,
3730 0,
3731 vec![Value::Int(1), Value::Int(1), Value::Int(0), Value::Int(11)],
3732 );
3733 low.level = 3;
3734 let mut high = entity(
3735 144,
3736 0,
3737 vec![Value::Int(21), Value::Int(1), Value::Int(0), Value::Int(31)],
3738 );
3739 high.level = 7;
3740 entities.push((41, low));
3741 entities.push((43, high));
3742 let mut group = entity(402, 7, vec![Value::Int(1), Value::Int(43)]);
3743 group.label = "LID".to_owned();
3744 entities.push((45, group));
3745 let deck = file(entities);
3746 let mut reader = reader(&deck);
3747 let low_face = reader.face(41).unwrap();
3748 let high_face = reader.face(43).unwrap();
3749 let built = vec![(41, low_face.clone()), (43, high_face.clone())];
3750 let mut document = ogeom_doc::Document::over(std::mem::take(&mut reader.model));
3751 let mut report = IgesReport::default();
3752 layers(&deck, &mut document, &built, &mut report);
3753 let names_of = |shape: &Shape| -> Vec<String> {
3754 let mut names: Vec<String> = document
3755 .layers_of(shape)
3756 .iter()
3757 .map(|id| document.layer(*id).unwrap().name.clone())
3758 .collect();
3759 names.sort();
3760 names
3761 };
3762 assert_eq!(names_of(&low_face), vec!["level 3".to_owned()]);
3763 assert_eq!(
3764 names_of(&high_face),
3765 vec!["LID".to_owned(), "level 7".to_owned()]
3766 );
3767 }
3768
3769 #[test]
3774 fn constructive_solids_read_as_their_volumes() {
3775 let pi = core::f64::consts::PI;
3776 let square = |base: i64, corners: [[f64; 3]; 4]| -> Vec<(i64, Entity)> {
3777 let mut out = Vec::new();
3778 let mut sides = vec![Value::Int(4)];
3779 for i in 0..4 {
3780 let de = base + 2 * i64::try_from(i).unwrap();
3781 out.push((de, line(corners[i], corners[(i + 1) % 4])));
3782 sides.push(Value::Int(de));
3783 }
3784 out.push((base + 8, entity(102, 0, sides)));
3785 out
3786 };
3787 let mut entities = vec![
3788 (
3790 1,
3791 entity(
3792 150,
3793 0,
3794 reals(&[
3795 20.0, 20.0, 20.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
3796 ]),
3797 ),
3798 ),
3799 (
3801 3,
3802 entity(
3803 154,
3804 0,
3805 reals(&[40.0, 5.0, 10.0, 10.0, -10.0, 0.0, 0.0, 1.0]),
3806 ),
3807 ),
3808 (
3809 5,
3810 entity(
3811 180,
3812 0,
3813 vec![Value::Int(3), Value::Int(-1), Value::Int(-3), Value::Int(3)],
3814 ),
3815 ),
3816 (
3818 7,
3819 entity(
3820 152,
3821 0,
3822 reals(&[
3823 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,
3824 ]),
3825 ),
3826 ),
3827 (
3829 9,
3830 entity(
3831 156,
3832 0,
3833 reals(&[10.0, 5.0, 2.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
3834 ),
3835 ),
3836 (
3838 11,
3839 entity(
3840 168,
3841 0,
3842 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]),
3843 ),
3844 ),
3845 (
3847 13,
3848 entity(
3849 162,
3850 0,
3851 vec![
3852 Value::Int(29),
3853 Value::Real(0.5),
3854 Value::Real(0.0),
3855 Value::Real(0.0),
3856 Value::Real(0.0),
3857 Value::Real(0.0),
3858 Value::Real(0.0),
3859 Value::Real(1.0),
3860 ],
3861 ),
3862 ),
3863 (
3865 15,
3866 entity(
3867 164,
3868 0,
3869 vec![
3870 Value::Int(49),
3871 Value::Real(5.0),
3872 Value::Real(0.0),
3873 Value::Real(0.0),
3874 Value::Real(1.0),
3875 ],
3876 ),
3877 ),
3878 (
3880 17,
3881 entity(
3882 124,
3883 0,
3884 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]),
3885 ),
3886 ),
3887 (
3888 19,
3889 entity(
3890 184,
3891 0,
3892 vec![
3893 Value::Int(2),
3894 Value::Int(1),
3895 Value::Int(1),
3896 Value::Int(0),
3897 Value::Int(17),
3898 ],
3899 ),
3900 ),
3901 ];
3902 entities.extend(square(
3903 21,
3904 [
3905 [2.0, 0.0, 0.0],
3906 [4.0, 0.0, 0.0],
3907 [4.0, 0.0, 3.0],
3908 [2.0, 0.0, 3.0],
3909 ],
3910 ));
3911 entities.extend(square(
3912 41,
3913 [
3914 [0.0, 0.0, 0.0],
3915 [2.0, 0.0, 0.0],
3916 [2.0, 2.0, 0.0],
3917 [0.0, 2.0, 0.0],
3918 ],
3919 ));
3920 let deck = file(entities);
3921 let mut reader = reader(&deck);
3922 let volume = |reader: &Reader<'_>, shape: &Shape| {
3923 ogeom_algo::volume_properties(
3924 &reader.model,
3925 shape,
3926 ogeom_mesh::Deflection::with_chord(1e-3).unwrap(),
3927 T,
3928 )
3929 .unwrap()
3930 .mass
3931 };
3932 for (de, want, within) in [
3933 (5, 8000.0 - pi * 25.0 * 20.0, 1e-6),
3934 (7, (10.0 + 6.0) / 2.0 * 5.0 * 4.0, 1e-9),
3935 (9, pi * 10.0 / 3.0 * (25.0 + 10.0 + 4.0), 1e-6),
3936 (11, 4.0 / 3.0 * pi * 6.0, 2e-3),
3939 (13, 0.5 * pi * (16.0 - 4.0) * 3.0, 1e-6),
3940 (15, 20.0, 1e-9),
3941 ] {
3942 let solids = reader.csg_solids(de).unwrap();
3943 let [solid] = solids.as_slice() else {
3944 panic!("D{de} is one solid");
3945 };
3946 let got = volume(&reader, solid);
3947 assert!(
3948 (got - want).abs() <= within * want.max(1.0),
3949 "D{de}: {got} against {want}"
3950 );
3951 }
3952 let ellipsoid = reader.csg_solids(11).unwrap().remove(0);
3955 for face in ogeom_topo::explore(
3956 &reader.model,
3957 &ellipsoid,
3958 ogeom_topo::Filter::OfType(ogeom_topo::ShapeType::Face),
3959 )
3960 .unwrap()
3961 {
3962 let data = reader
3963 .model
3964 .node(&face)
3965 .unwrap()
3966 .data()
3967 .as_face()
3968 .unwrap()
3969 .clone();
3970 let surface = reader
3971 .model
3972 .geometry()
3973 .surface(data.surface)
3974 .unwrap()
3975 .clone();
3976 let ((u0, u1), (v0, v1)) = ogeom_geom::Surface::domain(&surface);
3977 for i in 0..=6 {
3978 for j in 0..=6 {
3979 let (u, v) = (
3980 u0 + (u1 - u0) * f64::from(i) / 6.0,
3981 v0 + (v1 - v0) * f64::from(j) / 6.0,
3982 );
3983 let p = ogeom_geom::Surface::point_at(&surface, u, v, T).unwrap();
3984 let residual = p.x * p.x / 9.0 + p.y * p.y / 4.0 + p.z * p.z - 1.0;
3985 assert!(residual.abs() < 1e-9, "off the ellipsoid: {p:?}");
3986 }
3987 }
3988 }
3989 let placed = reader.csg_solids(19).unwrap();
3990 assert_eq!(placed.len(), 2);
3991 let centres: Vec<Point> = placed
3992 .iter()
3993 .map(|s| {
3994 ogeom_algo::volume_properties(
3995 &reader.model,
3996 s,
3997 ogeom_mesh::Deflection::default(),
3998 T,
3999 )
4000 .unwrap()
4001 .centre
4002 })
4003 .collect();
4004 assert!(
4005 centres
4006 .iter()
4007 .any(|c| c.distance(Point::new(10.0, 10.0, 10.0)) < 1e-9)
4008 );
4009 assert!(
4010 centres
4011 .iter()
4012 .any(|c| c.distance(Point::new(110.0, 10.0, 10.0)) < 1e-9)
4013 );
4014 }
4015}