1use std::fmt::Write as _;
38
39use ogeom_core::{
40 EntityId, Key, OgeomResult, OpId, Provenance, Role, SourceId, Tolerance, Tolerances, ogeom_bail,
41};
42use ogeom_geom::{
43 BSpline2d, BSplineCurve, BSplineSurface, Circle2d, CircleCurve, ConeSurface, Curve, Curve2d,
44 Curve3d, CylinderSurface, Ellipse2d, EllipseCurve, ExtrusionSurface, HelixCurve,
45 HyperbolaCurve, Line2d, LineCurve, ParabolaCurve, PlanarCurve, PlaneSurface, RevolutionSurface,
46 SphereSurface, Surface, SurfaceGeometry, TorusSurface, Trimmed2d, TrimmedCurve, TrimmedSurface,
47};
48use ogeom_math::{
49 Axis, Axis2, Circle, Circle2, Cone, ControlGrid, Cylinder, Direction, Direction2, Ellipse,
50 Ellipse2, Frame, Frame2, Hyperbola, Hyperbola2, KnotVector, Parabola, Plane, Point, Point2,
51 Sphere, Torus, Transform, Vector, Weighted,
52};
53use ogeom_topo::{
54 DatumId, EdgeData, EdgeRepr, FaceData, GeometryStore, Location, Model, ModelParts, NodeData,
55 Orientation, Shape, ShapeType, TShape, TShapeId, Triangulation, VertexData,
56};
57
58pub use ogeom_topo::Absorbed;
59
60pub const VERSION: u32 = 3;
69
70const GAPLESS_VERSION: u32 = 2;
72
73const MAGIC: &str = "ogeom";
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct WriteOptions {
79 pub triangulations: bool,
88}
89
90impl Default for WriteOptions {
91 fn default() -> Self {
92 Self {
93 triangulations: true,
94 }
95 }
96}
97
98pub fn compacted(model: &Model, roots: &[Shape]) -> OgeomResult<(Model, Vec<Shape>)> {
116 read(&write(model, roots, WriteOptions::default())?)
117}
118
119pub fn write(model: &Model, roots: &[Shape], options: WriteOptions) -> OgeomResult<String> {
137 write_renumbered(model, roots, options).map(|(text, _)| text)
138}
139
140fn write_renumbered(
142 model: &Model,
143 roots: &[Shape],
144 options: WriteOptions,
145) -> OgeomResult<(String, Renumbered)> {
146 for root in roots {
147 if model.node(root).is_none() {
148 ogeom_bail!(Construction, "a root shape is not in this model");
149 }
150 }
151 let closure = if roots.is_empty() {
174 if gapless(model) {
175 let entries: Vec<(EntityId, &Provenance)> = model.provenance().iter().collect();
176 return Ok((
177 write_model(model, roots, options, &entries)?,
178 Renumbered(None),
179 ));
180 }
181 Closure::everything(model)
182 } else {
183 closure_of(model, roots)
184 };
185 let entries = closure.entries(model);
186 if !roots.is_empty() && closure.covers(model) && gapless(model) {
187 return Ok((
188 write_model(model, roots, options, &entries)?,
189 Renumbered(None),
190 ));
191 }
192 let (parts, roots, node_map) = subset_parts(model, roots, &closure)?;
193 let records = Records {
194 scale: parts.tolerances.scale(),
195 datums: parts
196 .datums
197 .iter()
198 .enumerate()
199 .map(|(i, datum)| (Key::from_parts(dense(i), 0), *datum)),
200 geometry: &parts.geometry,
201 entries: &entries,
202 nodes: parts
203 .nodes
204 .iter()
205 .enumerate()
206 .map(|(i, node)| (Key::from_parts(dense(i), 0), node)),
207 identities: parts.identity.iter().copied(),
208 current_op: parts.current_op,
209 };
210 Ok((
211 emit_records(records, &roots, options)?,
212 Renumbered(Some(node_map)),
213 ))
214}
215
216struct Renumbered(Option<ogeom_core::FastMap<u32, u32>>);
220
221impl Renumbered {
222 fn node(&self, id: TShapeId) -> OgeomResult<TShapeId> {
224 match &self.0 {
225 None => Ok(id),
226 Some(map) => map
227 .get(&id.index())
228 .map(|&new| Key::from_parts(new, 0))
229 .ok_or_else(|| {
230 ogeom_core::ogeom_err!(
231 Dangling,
232 "a document record names a node its model no longer holds"
233 )
234 }),
235 }
236 }
237
238 fn shape(&self, shape: &Shape) -> OgeomResult<Shape> {
240 Ok(Shape::new(
241 self.node(shape.node())?,
242 shape.location().clone(),
243 shape.orientation(),
244 ))
245 }
246}
247
248fn gapless(model: &Model) -> bool {
251 fn run<T>(mut keys: impl Iterator<Item = Key<T>>) -> bool {
252 keys.by_ref()
253 .enumerate()
254 .all(|(i, key)| key.index() as usize == i && key.generation() == 0)
255 }
256 let geometry = model.geometry();
257 run(model.nodes().map(|(id, _)| id))
258 && run(geometry.curves().map(|(id, _)| id))
259 && run(geometry.pcurves().map(|(id, _)| id))
260 && run(geometry.surfaces().map(|(id, _)| id))
261 && run(geometry.triangulations().map(|(id, _)| id))
262}
263
264fn dense(n: usize) -> u32 {
267 u32::try_from(n).unwrap_or(u32::MAX)
268}
269
270struct Closure {
272 nodes: ogeom_core::FastSet<TShapeId>,
273 datums: ogeom_core::FastSet<ogeom_topo::DatumId>,
274 curves: ogeom_core::FastSet<ogeom_topo::CurveId>,
275 pcurves: ogeom_core::FastSet<ogeom_topo::PCurveId>,
276 surfaces: ogeom_core::FastSet<ogeom_topo::SurfaceId>,
277 meshes: ogeom_core::FastSet<ogeom_topo::TriangulationId>,
278 entities: ogeom_core::FastSet<EntityId>,
283}
284
285impl Closure {
286 fn everything(model: &Model) -> Self {
288 let geometry = model.geometry();
289 Self {
290 nodes: model.nodes().map(|(id, _)| id).collect(),
291 datums: model.datums().iter().map(|(id, _)| id).collect(),
292 curves: geometry.curves().map(|(id, _)| id).collect(),
293 pcurves: geometry.pcurves().map(|(id, _)| id).collect(),
294 surfaces: geometry.surfaces().map(|(id, _)| id).collect(),
295 meshes: geometry.triangulations().map(|(id, _)| id).collect(),
296 entities: model.provenance().iter().map(|(id, _)| id).collect(),
297 }
298 }
299
300 fn covers(&self, model: &Model) -> bool {
302 let (curves, pcurves, surfaces) = model.geometry().counts();
303 self.curves.len() == curves
304 && self.pcurves.len() == pcurves
305 && self.surfaces.len() == surfaces
306 && self.nodes.len() == model.nodes().count()
307 }
308
309 fn entries<'m>(&self, model: &'m Model) -> Vec<(EntityId, &'m Provenance)> {
311 let mut ids: Vec<EntityId> = self.entities.iter().copied().collect();
312 ids.sort_unstable();
313 ids.into_iter()
314 .filter_map(|id| Some((id, model.provenance().get(id)?)))
315 .collect()
316 }
317}
318
319fn closure_of(model: &Model, roots: &[Shape]) -> Closure {
321 let mut c = Closure {
322 nodes: ogeom_core::FastSet::default(),
323 datums: ogeom_core::FastSet::default(),
324 curves: ogeom_core::FastSet::default(),
325 pcurves: ogeom_core::FastSet::default(),
326 surfaces: ogeom_core::FastSet::default(),
327 meshes: ogeom_core::FastSet::default(),
328 entities: ogeom_core::FastSet::default(),
329 };
330 let note_location = |datums: &mut ogeom_core::FastSet<ogeom_topo::DatumId>, l: &Location| {
331 for (datum, _) in l.chain() {
332 datums.insert(*datum);
333 }
334 };
335 let mut queue: Vec<TShapeId> = Vec::new();
336 for root in roots {
337 note_location(&mut c.datums, root.location());
338 if c.nodes.insert(root.node()) {
339 queue.push(root.node());
340 }
341 }
342 let mut ancestry: Vec<EntityId> = Vec::new();
343 while let Some(id) = queue.pop() {
344 let Some(node) = model.node_by_id(id) else {
345 continue;
346 };
347 if let Some(entity) = model.identity_of(&Shape::of(id))
348 && c.entities.insert(entity)
349 {
350 ancestry.push(entity);
351 }
352 for child in node.children() {
353 note_location(&mut c.datums, child.location());
354 if c.nodes.insert(child.node()) {
355 queue.push(child.node());
356 }
357 }
358 match node.data() {
359 NodeData::Edge(e) => {
360 for repr in &e.representations {
361 match repr {
362 EdgeRepr::Curve3d {
363 curve, location, ..
364 } => {
365 c.curves.insert(*curve);
366 note_location(&mut c.datums, location);
367 }
368 EdgeRepr::PCurve {
369 curve,
370 surface,
371 location,
372 ..
373 } => {
374 c.pcurves.insert(*curve);
375 c.surfaces.insert(*surface);
376 note_location(&mut c.datums, location);
377 }
378 EdgeRepr::Seam {
379 forward,
380 reversed,
381 surface,
382 location,
383 ..
384 } => {
385 c.pcurves.insert(*forward);
386 c.pcurves.insert(*reversed);
387 c.surfaces.insert(*surface);
388 note_location(&mut c.datums, location);
389 }
390 EdgeRepr::Polyline { location, .. } => {
391 note_location(&mut c.datums, location);
392 }
393 EdgeRepr::PolygonOnTriangulation { triangulation, .. } => {
394 c.meshes.insert(*triangulation);
395 }
396 _ => {}
400 }
401 }
402 }
403 NodeData::Face(f) => {
404 c.surfaces.insert(f.surface);
405 note_location(&mut c.datums, &f.location);
406 if let Some(mesh) = f.triangulation {
407 c.meshes.insert(mesh);
408 }
409 }
410 NodeData::Vertex(_) | NodeData::Container => {}
411 }
412 }
413 while let Some(entity) = ancestry.pop() {
414 if let Some(entry) = model.provenance().get(entity) {
415 for &from in entry.inputs() {
416 if c.entities.insert(from) {
417 ancestry.push(from);
418 }
419 }
420 }
421 }
422 c
423}
424
425fn renumbered<T>(
428 set: &ogeom_core::FastSet<Key<T>>,
429) -> (Vec<Key<T>>, ogeom_core::FastMap<u32, u32>) {
430 let mut ordered: Vec<Key<T>> = set.iter().copied().collect();
431 ordered.sort_unstable_by_key(|k| k.index());
432 let map = ordered
433 .iter()
434 .enumerate()
435 .map(|(i, k)| (k.index(), dense(i)))
436 .collect();
437 (ordered, map)
438}
439
440fn subset_parts(
445 model: &Model,
446 roots: &[Shape],
447 closure: &Closure,
448) -> OgeomResult<(ModelParts, Vec<Shape>, ogeom_core::FastMap<u32, u32>)> {
449 let missing = || ogeom_core::ogeom_err!(Dangling, "the closure misses a referenced handle");
450 let (nodes_in_order, node_map) = renumbered(&closure.nodes);
453 let (datum_ids, datum_map) = renumbered(&closure.datums);
454 let datums = datum_ids
455 .iter()
456 .map(|&id| model.datums().get(id).ok_or_else(missing))
457 .collect::<OgeomResult<Vec<_>>>()?;
458 let geometry_in = model.geometry();
459 let mut geometry = GeometryStore::new();
460 let (curve_ids, curve_map) = renumbered(&closure.curves);
461 for id in curve_ids {
462 geometry.add_curve(geometry_in.curve(id).ok_or_else(missing)?.clone());
463 }
464 let (pcurve_ids, pcurve_map) = renumbered(&closure.pcurves);
465 for id in pcurve_ids {
466 geometry.add_pcurve(geometry_in.pcurve(id).ok_or_else(missing)?.clone());
467 }
468 let (surface_ids, surface_map) = renumbered(&closure.surfaces);
469 for id in surface_ids {
470 geometry.add_surface(geometry_in.surface(id).ok_or_else(missing)?.clone());
471 }
472 let (mesh_ids, mesh_map) = renumbered(&closure.meshes);
473 for id in mesh_ids {
474 geometry.add_triangulation(geometry_in.triangulation(id).ok_or_else(missing)?.clone());
475 }
476
477 let relocate = |l: &Location| -> OgeomResult<Location> {
478 let mut out = Location::identity();
479 for (datum, power) in l.chain() {
480 let new = *datum_map.get(&datum.index()).ok_or_else(missing)?;
481 out = out.then(&Location::powered(Key::from_parts(new, 0), *power));
482 }
483 Ok(out)
484 };
485 let reshape = |s: &Shape| -> OgeomResult<Shape> {
486 let new = *node_map.get(&s.node().index()).ok_or_else(missing)?;
487 Ok(Shape::new(
488 Key::from_parts(new, 0),
489 relocate(s.location())?,
490 s.orientation(),
491 ))
492 };
493
494 let mut nodes: Vec<TShape> = Vec::new();
495 for id in &nodes_in_order {
496 let node = model.node_by_id(*id).ok_or_else(missing)?;
497 let data = match node.data() {
498 NodeData::Vertex(v) => {
499 NodeData::Vertex(VertexData::with_tolerance(v.point, v.tolerance.get())?)
500 }
501 NodeData::Edge(e) => {
502 let mut edge = EdgeData::new();
503 edge.tolerance = e.tolerance;
504 edge.degenerate = e.degenerate;
505 for repr in &e.representations {
506 edge.add(match repr {
507 EdgeRepr::Curve3d {
508 curve,
509 location,
510 range,
511 } => EdgeRepr::Curve3d {
512 curve: Key::from_parts(
513 *curve_map.get(&curve.index()).ok_or_else(missing)?,
514 0,
515 ),
516 location: relocate(location)?,
517 range: *range,
518 },
519 EdgeRepr::PCurve {
520 curve,
521 surface,
522 location,
523 range,
524 } => EdgeRepr::PCurve {
525 curve: Key::from_parts(
526 *pcurve_map.get(&curve.index()).ok_or_else(missing)?,
527 0,
528 ),
529 surface: Key::from_parts(
530 *surface_map.get(&surface.index()).ok_or_else(missing)?,
531 0,
532 ),
533 location: relocate(location)?,
534 range: *range,
535 },
536 EdgeRepr::Seam {
537 forward,
538 reversed,
539 surface,
540 location,
541 range,
542 } => EdgeRepr::Seam {
543 forward: Key::from_parts(
544 *pcurve_map.get(&forward.index()).ok_or_else(missing)?,
545 0,
546 ),
547 reversed: Key::from_parts(
548 *pcurve_map.get(&reversed.index()).ok_or_else(missing)?,
549 0,
550 ),
551 surface: Key::from_parts(
552 *surface_map.get(&surface.index()).ok_or_else(missing)?,
553 0,
554 ),
555 location: relocate(location)?,
556 range: *range,
557 },
558 EdgeRepr::Polyline {
559 points,
560 parameters,
561 location,
562 deflection,
563 } => EdgeRepr::Polyline {
564 points: points.clone(),
565 parameters: parameters.clone(),
566 location: relocate(location)?,
567 deflection: *deflection,
568 },
569 EdgeRepr::PolygonOnTriangulation {
570 triangulation,
571 indices,
572 location,
573 } => EdgeRepr::PolygonOnTriangulation {
574 triangulation: Key::from_parts(
575 *mesh_map.get(&triangulation.index()).ok_or_else(missing)?,
576 0,
577 ),
578 indices: indices.clone(),
579 location: relocate(location)?,
580 },
581 other => ogeom_bail!(
582 Construction,
583 "an edge representation this writer does not know: {other:?}"
584 ),
585 });
586 }
587 edge.assert_same_parameter(e.same_parameter());
588 NodeData::Edge(Box::new(edge))
589 }
590 NodeData::Face(f) => {
591 let surface =
592 Key::from_parts(*surface_map.get(&f.surface.index()).ok_or_else(missing)?, 0);
593 let at = relocate(&f.location)?;
594 let mut face = if f.natural_restriction {
595 FaceData::natural(surface, at)
596 } else {
597 FaceData::new(surface, at)
598 };
599 face.tolerance = f.tolerance;
600 face.triangulation = match f.triangulation {
601 Some(mesh) => Some(Key::from_parts(
602 *mesh_map.get(&mesh.index()).ok_or_else(missing)?,
603 0,
604 )),
605 None => None,
606 };
607 NodeData::Face(Box::new(face))
608 }
609 NodeData::Container => NodeData::Container,
610 };
611 let children = node
612 .children()
613 .iter()
614 .map(&reshape)
615 .collect::<OgeomResult<Vec<_>>>()?;
616 nodes.push(TShape::new(node.kind(), data, children));
617 }
618
619 let mut identity = Vec::new();
620 for (new, id) in nodes_in_order.iter().enumerate() {
621 if let Some(entity) = model.identity_of(&Shape::of(*id)) {
622 identity.push((Key::from_parts(dense(new), 0), entity));
623 }
624 }
625
626 let parts = ModelParts {
627 nodes,
628 datums,
629 geometry,
630 provenance: Vec::new(),
631 identity,
632 current_op: model.current_operation(),
633 tolerances: model.tolerances(),
634 };
635 let respelled = roots
636 .iter()
637 .map(&reshape)
638 .collect::<OgeomResult<Vec<_>>>()?;
639 Ok((parts, respelled, node_map))
640}
641
642fn write_model(
644 model: &Model,
645 roots: &[Shape],
646 options: WriteOptions,
647 entries: &[(EntityId, &Provenance)],
648) -> OgeomResult<String> {
649 let records = Records {
650 scale: model.tolerances().scale(),
651 datums: model.datums().iter(),
652 geometry: model.geometry(),
653 entries,
654 nodes: model.nodes(),
655 identities: model.identities(),
656 current_op: model.current_operation(),
657 };
658 emit_records(records, roots, options)
659}
660
661struct Records<'a, D, N, I> {
663 scale: f64,
664 datums: D,
665 geometry: &'a GeometryStore,
666 entries: &'a [(EntityId, &'a Provenance)],
668 nodes: N,
669 identities: I,
670 current_op: OpId,
671}
672
673fn emit_records<'a, D, N, I>(
675 records: Records<'a, D, N, I>,
676 roots: &[Shape],
677 options: WriteOptions,
678) -> OgeomResult<String>
679where
680 D: Iterator<Item = (ogeom_topo::DatumId, ogeom_topo::Datum)>,
681 N: Iterator<Item = (TShapeId, &'a TShape)>,
682 I: Iterator<Item = (TShapeId, EntityId)>,
683{
684 let gapped = records
687 .entries
688 .iter()
689 .enumerate()
690 .any(|(i, (id, _))| id.get() != i as u64 + 1);
691 let mut out = String::new();
692 out.push_str(&format!(
693 "{MAGIC} {}\n",
694 if gapped { VERSION } else { GAPLESS_VERSION }
695 ));
696
697 let mut t = Tokens::default();
702 w(&mut t, "units");
703 n(&mut t, records.scale);
704 emit(&mut out, &t);
705
706 for (id, datum) in records.datums {
707 let mut t = Tokens::default();
708 w(&mut t, "datum");
709 key(&mut t, id);
710 transform(&mut t, &datum)?;
711 emit(&mut out, &t);
712 }
713
714 let geometry = records.geometry;
715 for (id, c) in geometry.curves() {
716 let mut t = Tokens::default();
717 w(&mut t, "curve");
718 key(&mut t, id);
719 curve(&mut t, c)?;
720 emit(&mut out, &t);
721 }
722 for (id, c) in geometry.pcurves() {
723 let mut t = Tokens::default();
724 w(&mut t, "pcurve");
725 key(&mut t, id);
726 pcurve(&mut t, c)?;
727 emit(&mut out, &t);
728 }
729 for (id, s) in geometry.surfaces() {
730 let mut t = Tokens::default();
731 w(&mut t, "surface");
732 key(&mut t, id);
733 surface(&mut t, s)?;
734 emit(&mut out, &t);
735 }
736 if options.triangulations {
737 for (id, mesh) in geometry.triangulations() {
738 write_mesh(&mut out, id, mesh);
739 }
740 } else if geometry.triangulation_count() > 0 {
741 out.push_str(&format!(
745 "# {} triangulation(s) omitted by request\n",
746 geometry.triangulation_count()
747 ));
748 }
749
750 for &(id, entry) in records.entries {
751 let mut t = Tokens::default();
752 w(&mut t, "entity");
753 u(&mut t, id.get());
754 provenance(&mut t, entry);
755 emit(&mut out, &t);
756 }
757
758 for (id, node) in records.nodes {
759 write_node(&mut out, id, node, options)?;
760 }
761 for (node, entity) in records.identities {
762 let mut t = Tokens::default();
763 w(&mut t, "identity");
764 key(&mut t, node);
765 u(&mut t, entity.get());
766 emit(&mut out, &t);
767 }
768
769 let mut t = Tokens::default();
770 w(&mut t, "operation");
771 u(&mut t, u64::from(records.current_op.0));
772 emit(&mut out, &t);
773
774 for root in roots {
775 let mut t = Tokens::default();
776 w(&mut t, "root");
777 shape(&mut t, root);
778 emit(&mut out, &t);
779 }
780 Ok(out)
781}
782
783pub fn read(text: &str) -> OgeomResult<(Model, Vec<Shape>)> {
792 let (model, roots, leftover, _) = read_core(text)?;
793 if let Some(keyword) = leftover {
794 ogeom_bail!(Construction, "unknown record `{keyword}`");
795 }
796 Ok((model, roots))
797}
798
799pub fn read_into(model: &mut Model, text: &str) -> OgeomResult<Absorbed> {
815 let (parts, roots, leftover, _, gaps) = read_parts(text)?;
816 if let Some(keyword) = leftover {
817 ogeom_bail!(Construction, "unknown record `{keyword}`");
818 }
819 let absorbed = model.absorb(parts, &roots)?;
820 model.forget_provenance(
821 gaps.iter()
822 .filter_map(|id| absorbed.entities.get(id).copied()),
823 );
824 Ok(absorbed)
825}
826
827#[allow(clippy::type_complexity)]
832fn read_core(text: &str) -> OgeomResult<(Model, Vec<Shape>, Option<String>, Cursor<'_>)> {
833 let (parts, roots, leftover, cursor, gaps) = read_parts(text)?;
834 let mut model = Model::from_parts(parts)?;
835 model.forget_provenance(gaps);
836 let roots = roots
841 .iter()
842 .map(|root| model.bind(root))
843 .collect::<OgeomResult<Vec<_>>>()?;
844 Ok((model, roots, leftover, cursor))
845}
846
847#[allow(clippy::type_complexity)]
856fn read_parts(
857 text: &str,
858) -> OgeomResult<(
859 ModelParts,
860 Vec<Shape>,
861 Option<String>,
862 Cursor<'_>,
863 Vec<EntityId>,
864)> {
865 let mut cursor = Cursor::new(text);
866 let mut leftover = None;
867
868 if cursor.word()? != MAGIC {
869 ogeom_bail!(Construction, "not an ogeom document");
870 }
871 let version = cursor.count()?;
872 if version == 0 || version > VERSION as usize {
873 ogeom_bail!(
874 Construction,
875 "document is version {version}; this reads versions 1 through \
876 {VERSION}"
877 );
878 }
879
880 let tol = read_units(&mut cursor)?;
883 let mut parts = ModelParts {
884 tolerances: tol,
885 ..ModelParts::default()
886 };
887 let mut geometry = GeometryStore::new();
888 let mut roots = Vec::new();
889 let mut gaps: Vec<EntityId> = Vec::new();
890
891 while !cursor.done() {
892 let tag = cursor.word()?.to_string();
893 match tag.as_str() {
894 "datum" => {
895 let key = cursor.key()?;
896 expect_datum(key, parts.datums.len())?;
897 parts.datums.push(cursor.transform(tol)?);
898 }
899 "curve" => {
900 let (index, _) = cursor.key()?;
901 expect_index(index, geometry.counts().0)?;
902 geometry.add_curve(cursor.curve(tol)?);
903 }
904 "pcurve" => {
905 let (index, _) = cursor.key()?;
906 expect_index(index, geometry.counts().1)?;
907 geometry.add_pcurve(cursor.pcurve(tol)?);
908 }
909 "surface" => {
910 let (index, _) = cursor.key()?;
911 expect_index(index, geometry.counts().2)?;
912 geometry.add_surface(cursor.surface(tol)?);
913 }
914 "mesh" => {
915 let (index, _) = cursor.key()?;
916 expect_index(index, geometry.triangulation_count())?;
917 geometry.add_triangulation(cursor.mesh()?);
918 }
919 "entity" => {
920 let id = cursor.count()?;
921 let next = parts.provenance.len() + 1;
922 if id < next || (version < 3 && id != next) {
923 ogeom_bail!(
924 Construction,
925 "entities must be written in the order their identities \
926 were issued; expected {next}, got {id}"
927 );
928 }
929 for skipped in next..id {
930 let Some(gap) = EntityId::from_raw(skipped as u64) else {
931 ogeom_bail!(Construction, "identity 0 was never issued");
932 };
933 gaps.push(gap);
934 parts.provenance.push(Provenance::Primitive {
935 op: OpId(0),
936 role: Role::SOLE,
937 });
938 }
939 let entry = cursor.provenance()?;
940 if let Some(from) = entry
941 .inputs()
942 .iter()
943 .find(|from| gaps.binary_search(from).is_ok())
944 {
945 ogeom_bail!(
946 Construction,
947 "entity {id} is derived from entity {}, which the file left out",
948 from.get()
949 );
950 }
951 parts.provenance.push(entry);
952 }
953 "node" => {
954 let (index, _) = cursor.key()?;
955 expect_index(index, parts.nodes.len())?;
956 parts.nodes.push(cursor.node()?);
957 }
958 "identity" => {
959 let node = cursor.shape_key()?;
960 let raw = cursor.count()?;
961 let Some(entity) = EntityId::from_raw(raw as u64) else {
962 ogeom_bail!(Construction, "identity 0 was never issued");
963 };
964 parts.identity.push((node, entity));
965 }
966 "operation" => parts.current_op = OpId(cursor.small()?),
967 "root" => roots.push(cursor.shape()?),
968 other => {
969 leftover = Some(other.to_string());
970 break;
971 }
972 }
973 }
974
975 if let Some((_, entity)) = parts
976 .identity
977 .iter()
978 .find(|(_, entity)| gaps.binary_search(entity).is_ok())
979 {
980 ogeom_bail!(
981 Construction,
982 "a node carries entity {}, which the file left out",
983 entity.get()
984 );
985 }
986
987 parts.geometry = geometry;
988 Ok((parts, roots, leftover, cursor, gaps))
989}
990
991fn read_units(cursor: &mut Cursor<'_>) -> OgeomResult<Tolerances> {
993 if cursor.word()? != "units" {
994 ogeom_bail!(
995 Construction,
996 "a document must say what units it is in before anything measured \
997 in them"
998 );
999 }
1000 Tolerances::with_scale(cursor.number()?)
1001}
1002
1003fn expect_index(index: u32, next: usize) -> OgeomResult<()> {
1008 if index as usize != next {
1009 ogeom_bail!(
1010 Construction,
1011 "records must run in arena order; expected index {next}, got {index}"
1012 );
1013 }
1014 Ok(())
1015}
1016
1017fn expect_datum(key: (u32, u32), next: usize) -> OgeomResult<()> {
1019 expect_index(key.0, next)?;
1020 if key.1 != 0 {
1021 ogeom_bail!(
1022 Construction,
1023 "a datum with generation {} cannot be rebuilt: a fresh arena hands \
1024 out generation 0, so the handle would not match what the file says",
1025 key.1
1026 );
1027 }
1028 Ok(())
1029}
1030
1031pub fn write_document(
1046 document: &ogeom_doc::Document,
1047 options: WriteOptions,
1048) -> OgeomResult<String> {
1049 let (mut out, renumbered) = write_renumbered(document.model(), &[], options)?;
1050
1051 for (_, product) in document.products() {
1052 let mut t = Tokens::default();
1053 w(&mut t, "product");
1054 text(&mut t, &product.name);
1055 match product.colour {
1056 Some(c) => {
1057 flag(&mut t, true);
1058 for channel in [c.r, c.g, c.b, c.a] {
1059 n(&mut t, channel);
1060 }
1061 }
1062 None => flag(&mut t, false),
1063 }
1064 match &product.kind {
1065 ogeom_doc::ProductKind::Part { shape: part } => {
1066 w(&mut t, "part");
1067 shape(&mut t, &renumbered.shape(part)?);
1068 }
1069 ogeom_doc::ProductKind::Assembly { children } => {
1070 w(&mut t, "assembly");
1071 u(&mut t, children.len() as u64);
1072 for instance in children {
1073 u(&mut t, u64::from(instance.product.index()));
1074 location(&mut t, &instance.location);
1075 match &instance.name {
1076 Some(name) => text(&mut t, name),
1077 None => w(&mut t, "-"),
1078 }
1079 }
1080 }
1081 }
1082 emit(&mut out, &t);
1083 }
1084
1085 let mut colours: Vec<_> = document.colours().collect();
1086 colours.sort_by_key(|(node, _)| (node.index(), node.generation()));
1087 for (node, colour) in colours {
1088 let mut t = Tokens::default();
1089 w(&mut t, "doc-colour");
1090 key(&mut t, renumbered.node(node)?);
1091 for channel in [colour.r, colour.g, colour.b, colour.a] {
1092 n(&mut t, channel);
1093 }
1094 emit(&mut out, &t);
1095 }
1096 let mut names: Vec<_> = document.names().collect();
1097 names.sort_by_key(|(node, _)| (node.index(), node.generation()));
1098 for (node, name) in names {
1099 let mut t = Tokens::default();
1100 w(&mut t, "doc-name");
1101 key(&mut t, renumbered.node(node)?);
1102 text(&mut t, name);
1103 emit(&mut out, &t);
1104 }
1105
1106 let pmi = document.pmi();
1107 for dimension in &pmi.dimensions {
1108 let mut t = Tokens::default();
1109 w(&mut t, "pmi-dim");
1110 text(&mut t, &dimension.name);
1111 w(
1112 &mut t,
1113 match dimension.kind {
1114 ogeom_doc::MeasureKind::Length => "L",
1115 ogeom_doc::MeasureKind::Angle => "A",
1116 },
1117 );
1118 flag(&mut t, dimension.location);
1119 optional(&mut t, dimension.plus);
1120 optional(&mut t, dimension.minus);
1121 u(&mut t, dimension.values.len() as u64);
1122 for v in &dimension.values {
1123 n(&mut t, *v);
1124 }
1125 u(&mut t, dimension.features.len() as u64);
1126 for feature in &dimension.features {
1127 u(&mut t, feature.len() as u64);
1128 for node in feature {
1129 key(&mut t, renumbered.node(*node)?);
1130 }
1131 }
1132 emit(&mut out, &t);
1133 }
1134 for tolerance in &pmi.tolerances {
1135 let mut t = Tokens::default();
1136 w(&mut t, "pmi-tol");
1137 text(&mut t, &tolerance.kind);
1138 text(&mut t, &tolerance.name);
1139 n(&mut t, tolerance.magnitude);
1140 u(&mut t, tolerance.modifiers.len() as u64);
1141 for word in &tolerance.modifiers {
1142 text(&mut t, word);
1143 }
1144 u(&mut t, tolerance.datums.len() as u64);
1145 for reference in &tolerance.datums {
1146 text(&mut t, &reference.label);
1147 }
1148 u(&mut t, tolerance.items.len() as u64);
1149 for node in &tolerance.items {
1150 key(&mut t, renumbered.node(*node)?);
1151 }
1152 emit(&mut out, &t);
1153 if tolerance.datum_system || tolerance.datums.iter().any(|d| !d.modifiers.is_empty()) {
1156 let mut t = Tokens::default();
1157 w(&mut t, "pmi-tol-frame");
1158 flag(&mut t, tolerance.datum_system);
1159 for reference in &tolerance.datums {
1160 u(&mut t, reference.modifiers.len() as u64);
1161 for modifier in &reference.modifiers {
1162 match modifier {
1163 ogeom_doc::DatumModifier::Simple(word) => {
1164 w(&mut t, "S");
1165 text(&mut t, word);
1166 }
1167 ogeom_doc::DatumModifier::WithValue { kind, value } => {
1168 w(&mut t, "V");
1169 w(&mut t, kind.word());
1170 n(&mut t, *value);
1171 }
1172 }
1173 }
1174 }
1175 emit(&mut out, &t);
1176 }
1177 }
1178 for datum in &pmi.datums {
1179 let mut t = Tokens::default();
1180 w(&mut t, "pmi-datum");
1181 text(&mut t, &datum.label);
1182 u(&mut t, datum.items.len() as u64);
1183 for node in &datum.items {
1184 key(&mut t, renumbered.node(*node)?);
1185 }
1186 emit(&mut out, &t);
1187 }
1188
1189 for callout in &document.pmi().callouts {
1190 let mut t = Tokens::default();
1191 w(&mut t, "pmi-callout");
1192 text(&mut t, &callout.name);
1193 match &callout.plane {
1194 Some(f) => {
1195 flag(&mut t, true);
1196 frame(&mut t, f);
1197 }
1198 None => flag(&mut t, false),
1199 }
1200 u(&mut t, callout.polylines.len() as u64);
1201 for line in &callout.polylines {
1202 u(&mut t, line.len() as u64);
1203 for p in line {
1204 point(&mut t, *p);
1205 }
1206 }
1207 match callout.annotates {
1208 Some(ogeom_doc::Annotated::Dimension(i)) => {
1209 w(&mut t, "D");
1210 u(&mut t, i as u64);
1211 }
1212 Some(ogeom_doc::Annotated::Tolerance(i)) => {
1213 w(&mut t, "T");
1214 u(&mut t, i as u64);
1215 }
1216 Some(ogeom_doc::Annotated::Datum(i)) => {
1217 w(&mut t, "M");
1218 u(&mut t, i as u64);
1219 }
1220 None => w(&mut t, "-"),
1221 }
1222 emit(&mut out, &t);
1223 if let Some(words) = &callout.text {
1226 let mut t = Tokens::default();
1227 w(&mut t, "pmi-callout-text");
1228 text(&mut t, words);
1229 emit(&mut out, &t);
1230 }
1231 if !callout.items.is_empty() {
1232 let mut t = Tokens::default();
1233 w(&mut t, "pmi-callout-items");
1234 u(&mut t, callout.items.len() as u64);
1235 for node in &callout.items {
1236 key(&mut t, renumbered.node(*node)?);
1237 }
1238 emit(&mut out, &t);
1239 }
1240 }
1241 for view in document.views() {
1242 let mut t = Tokens::default();
1243 w(&mut t, "doc-view");
1244 text(&mut t, &view.name);
1245 frame(&mut t, &view.frame);
1246 match &view.clipping {
1247 Some(plane) => {
1248 flag(&mut t, true);
1249 frame(&mut t, &plane.frame());
1250 }
1251 None => flag(&mut t, false),
1252 }
1253 u(&mut t, view.callouts.len() as u64);
1254 for index in &view.callouts {
1255 u(&mut t, *index as u64);
1256 }
1257 emit(&mut out, &t);
1258 }
1259 for note in document.notes() {
1260 let mut t = Tokens::default();
1261 w(&mut t, "doc-note");
1262 text(&mut t, ¬e.author);
1263 text(&mut t, ¬e.text);
1264 match note.product {
1265 Some(id) => {
1266 flag(&mut t, true);
1267 u(&mut t, document.product_index(id) as u64);
1268 }
1269 None => flag(&mut t, false),
1270 }
1271 emit(&mut out, &t);
1272 }
1273 for item in document.construction() {
1274 let mut t = Tokens::default();
1275 w(&mut t, "doc-construction");
1276 u(&mut t, document.product_index(item.product) as u64);
1277 text(&mut t, &item.name);
1278 match &item.geometry {
1279 ogeom_doc::ConstructionGeometry::Point(p) => {
1280 w(&mut t, "point");
1281 point(&mut t, *p);
1282 }
1283 ogeom_doc::ConstructionGeometry::Axis(f) => {
1284 w(&mut t, "axis");
1285 frame(&mut t, f);
1286 }
1287 ogeom_doc::ConstructionGeometry::Curve { curve: c, range: r } => {
1288 w(&mut t, "curve");
1289 range(&mut t, *r);
1290 curve(&mut t, c)?;
1291 }
1292 ogeom_doc::ConstructionGeometry::Surface {
1293 surface: s,
1294 u: a,
1295 v: b,
1296 } => {
1297 w(&mut t, "surface");
1298 range(&mut t, *a);
1299 range(&mut t, *b);
1300 surface(&mut t, s)?;
1301 }
1302 }
1303 emit(&mut out, &t);
1304 }
1305
1306 let mut with_properties: Vec<_> = document.properties().collect();
1310 with_properties.sort_by_key(|(node, _)| (node.index(), node.generation()));
1311 for (node, properties) in with_properties {
1312 for property in properties {
1313 let mut t = Tokens::default();
1314 w(&mut t, "doc-prop");
1315 key(&mut t, renumbered.node(node)?);
1316 text(&mut t, &property.name);
1317 match &property.value {
1318 ogeom_doc::PropertyValue::Text(value) => {
1319 w(&mut t, "T");
1320 text(&mut t, value);
1321 }
1322 ogeom_doc::PropertyValue::Number(value) => {
1323 w(&mut t, "N");
1324 n(&mut t, *value);
1325 }
1326 ogeom_doc::PropertyValue::Flag(value) => {
1327 w(&mut t, "F");
1328 flag(&mut t, *value);
1329 }
1330 }
1331 emit(&mut out, &t);
1332 }
1333 }
1334 for material in document.materials() {
1335 let mut t = Tokens::default();
1336 w(&mut t, "doc-material");
1337 text(&mut t, &material.name);
1338 optional(&mut t, material.density);
1339 match material.colour {
1340 Some(c) => {
1341 flag(&mut t, true);
1342 for channel in [c.r, c.g, c.b, c.a] {
1343 n(&mut t, channel);
1344 }
1345 }
1346 None => flag(&mut t, false),
1347 }
1348 emit(&mut out, &t);
1349 }
1350 let mut assigned: Vec<_> = document.material_assignments().collect();
1351 assigned.sort_by_key(|(node, _)| (node.index(), node.generation()));
1352 for (node, material) in assigned {
1353 let mut t = Tokens::default();
1354 w(&mut t, "doc-material-of");
1355 key(&mut t, renumbered.node(node)?);
1356 u(&mut t, material.index() as u64);
1357 emit(&mut out, &t);
1358 }
1359 for layer in document.layers() {
1360 let mut t = Tokens::default();
1361 w(&mut t, "doc-layer");
1362 text(&mut t, &layer.name);
1363 flag(&mut t, layer.visible);
1364 emit(&mut out, &t);
1365 }
1366 let mut memberships: Vec<_> = document.layer_memberships().collect();
1367 memberships.sort_by_key(|(node, _)| (node.index(), node.generation()));
1368 for (node, layers) in memberships {
1369 let mut t = Tokens::default();
1370 w(&mut t, "doc-on-layer");
1371 key(&mut t, renumbered.node(node)?);
1372 u(&mut t, layers.len() as u64);
1373 for layer in layers {
1374 u(&mut t, layer.index() as u64);
1375 }
1376 emit(&mut out, &t);
1377 }
1378 let mut checks: Vec<_> = document.validations().collect();
1379 checks.sort_by_key(|(node, _)| (node.index(), node.generation()));
1380 for (node, values) in checks {
1381 let mut t = Tokens::default();
1382 w(&mut t, "doc-check");
1383 key(&mut t, renumbered.node(node)?);
1384 n(&mut t, values.volume);
1385 n(&mut t, values.area);
1386 for v in [values.centroid.x, values.centroid.y, values.centroid.z] {
1387 n(&mut t, v);
1388 }
1389 emit(&mut out, &t);
1390 }
1391 Ok(out)
1392}
1393
1394pub fn read_document(text: &str) -> OgeomResult<ogeom_doc::Document> {
1400 let (model, _roots, mut pending, mut cursor) = read_core(text)?;
1401 let mut document = ogeom_doc::Document::over(model);
1402 let mut ids: Vec<ogeom_doc::ProductId> = Vec::new();
1407 let mut instances: Vec<(usize, usize, Location, Option<String>)> = Vec::new();
1408 let mut order = 0_usize;
1409
1410 while let Some(tag) = pending.take().or_else(|| {
1411 if cursor.done() {
1412 None
1413 } else {
1414 cursor.word().ok().map(ToString::to_string)
1415 }
1416 }) {
1417 match tag.as_str() {
1418 "product" => {
1419 let name = read_text(&mut cursor)?;
1420 let colour = if cursor.flag()? {
1421 Some(ogeom_doc::Colour {
1422 r: cursor.number()?,
1423 g: cursor.number()?,
1424 b: cursor.number()?,
1425 a: cursor.number()?,
1426 })
1427 } else {
1428 None
1429 };
1430 match cursor.word()? {
1431 "part" => {
1432 let part = document.model().bind(&cursor.shape()?)?;
1433 let id = document.add_part(&name, part);
1434 ids.push(id);
1435 }
1436 "assembly" => {
1437 let id = document.add_assembly(&name);
1438 ids.push(id);
1439 let count = cursor.count()?;
1440 for _ in 0..count {
1441 let child = cursor.count()?;
1442 let at = cursor.location()?;
1443 let instance_name = match cursor.peek() {
1444 Some("-") => {
1445 cursor.word()?;
1446 None
1447 }
1448 _ => Some(read_text(&mut cursor)?),
1449 };
1450 instances.push((order, child, at, instance_name));
1451 }
1452 }
1453 other => {
1454 ogeom_bail!(
1455 Construction,
1456 "a product is `part` or `assembly`, not `{other}`"
1457 )
1458 }
1459 }
1460 if let Some(c) = colour {
1461 let id = ids[ids.len() - 1];
1462 document.set_product_colour(id, c)?;
1463 }
1464 order += 1;
1465 }
1466 "doc-colour" => {
1467 let node: TShapeId = cursor.handle()?;
1468 let node = bind_node(&document, node)?;
1469 let colour = ogeom_doc::Colour {
1470 r: cursor.number()?,
1471 g: cursor.number()?,
1472 b: cursor.number()?,
1473 a: cursor.number()?,
1474 };
1475 document.set_colour(&Shape::of(node), colour);
1476 }
1477 "doc-name" => {
1478 let node: TShapeId = cursor.handle()?;
1479 let node = bind_node(&document, node)?;
1480 let name = read_text(&mut cursor)?;
1481 document.set_name(&Shape::of(node), name);
1482 }
1483 "doc-prop" => {
1484 let node: TShapeId = cursor.handle()?;
1485 let node = bind_node(&document, node)?;
1486 let name = read_text(&mut cursor)?;
1487 let value = match cursor.word()? {
1488 "T" => ogeom_doc::PropertyValue::Text(read_text(&mut cursor)?),
1489 "F" => ogeom_doc::PropertyValue::Flag(cursor.flag()?),
1490 _ => ogeom_doc::PropertyValue::Number(cursor.number()?),
1491 };
1492 document.set_property(&Shape::of(node), ogeom_doc::Property { name, value });
1493 }
1494 "doc-material" => {
1495 let name = read_text(&mut cursor)?;
1496 let density = read_optional(&mut cursor)?;
1497 let colour = if cursor.flag()? {
1498 Some(ogeom_doc::Colour {
1499 r: cursor.number()?,
1500 g: cursor.number()?,
1501 b: cursor.number()?,
1502 a: cursor.number()?,
1503 })
1504 } else {
1505 None
1506 };
1507 document.add_material(ogeom_doc::Material {
1508 name,
1509 density,
1510 colour,
1511 });
1512 }
1513 "doc-material-of" => {
1514 let node: TShapeId = cursor.handle()?;
1515 let node = bind_node(&document, node)?;
1516 let index = cursor.count()?;
1517 let Some(id) = document.material_id(index) else {
1518 ogeom_bail!(Construction, "material {index} is not in this document");
1519 };
1520 document.assign_material(&Shape::of(node), id);
1521 }
1522 "doc-layer" => {
1523 let name = read_text(&mut cursor)?;
1524 let visible = cursor.flag()?;
1525 let id = document.add_layer(name);
1526 document.set_layer_visible(id, visible);
1527 }
1528 "doc-on-layer" => {
1529 let node: TShapeId = cursor.handle()?;
1530 let node = bind_node(&document, node)?;
1531 for _ in 0..cursor.count()? {
1532 let index = cursor.count()?;
1533 let Some(id) = document.layer_id(index) else {
1534 ogeom_bail!(Construction, "layer {index} is not in this document");
1535 };
1536 document.place_on_layer(&Shape::of(node), id);
1537 }
1538 }
1539 "doc-check" => {
1540 let node: TShapeId = cursor.handle()?;
1541 let node = bind_node(&document, node)?;
1542 let volume = cursor.number()?;
1543 let area = cursor.number()?;
1544 let centroid =
1545 ogeom_math::Point::new(cursor.number()?, cursor.number()?, cursor.number()?);
1546 document.set_validation(
1547 &Shape::of(node),
1548 ogeom_doc::ValidationProperties {
1549 volume,
1550 area,
1551 centroid,
1552 },
1553 );
1554 }
1555 "pmi-dim" => {
1556 let name = read_text(&mut cursor)?;
1557 let kind = match cursor.word()? {
1558 "A" => ogeom_doc::MeasureKind::Angle,
1559 _ => ogeom_doc::MeasureKind::Length,
1560 };
1561 let location = cursor.flag()?;
1562 let plus = read_optional(&mut cursor)?;
1563 let minus = read_optional(&mut cursor)?;
1564 let mut values = Vec::new();
1565 for _ in 0..cursor.count()? {
1566 values.push(cursor.number()?);
1567 }
1568 let mut features = Vec::new();
1569 for _ in 0..cursor.count()? {
1570 let mut feature = Vec::new();
1571 for _ in 0..cursor.count()? {
1572 let node: TShapeId = cursor.handle()?;
1573 feature.push(bind_node(&document, node)?);
1574 }
1575 features.push(feature);
1576 }
1577 document.pmi_mut().dimensions.push(ogeom_doc::Dimension {
1578 name,
1579 values,
1580 kind,
1581 plus,
1582 minus,
1583 features,
1584 location,
1585 });
1586 }
1587 "pmi-tol" => {
1588 let kind = read_text(&mut cursor)?;
1589 let name = read_text(&mut cursor)?;
1590 let magnitude = cursor.number()?;
1591 let mut modifiers = Vec::new();
1592 for _ in 0..cursor.count()? {
1593 modifiers.push(read_text(&mut cursor)?);
1594 }
1595 let mut datums = Vec::new();
1596 for _ in 0..cursor.count()? {
1597 datums.push(ogeom_doc::DatumReference::new(read_text(&mut cursor)?));
1598 }
1599 let mut items = Vec::new();
1600 for _ in 0..cursor.count()? {
1601 let node: TShapeId = cursor.handle()?;
1602 items.push(bind_node(&document, node)?);
1603 }
1604 document
1605 .pmi_mut()
1606 .tolerances
1607 .push(ogeom_doc::GeometricTolerance {
1608 kind,
1609 name,
1610 magnitude,
1611 modifiers,
1612 datums,
1613 datum_system: false,
1614 items,
1615 });
1616 }
1617 "pmi-tol-frame" => {
1618 let system = cursor.flag()?;
1619 let Some(tolerance) = document.pmi_mut().tolerances.last_mut() else {
1620 ogeom_bail!(Construction, "a datum frame follows no tolerance");
1621 };
1622 tolerance.datum_system = system;
1623 for reference in &mut tolerance.datums {
1624 for _ in 0..cursor.count()? {
1625 let modifier = match cursor.word()? {
1626 "S" => ogeom_doc::DatumModifier::Simple(read_text(&mut cursor)?),
1627 "V" => {
1628 let word = cursor.word()?;
1629 let Some(kind) = ogeom_doc::DatumModifierKind::from_word(word)
1630 else {
1631 ogeom_bail!(Construction, "unknown datum modifier `{word}`");
1632 };
1633 ogeom_doc::DatumModifier::WithValue {
1634 kind,
1635 value: cursor.number()?,
1636 }
1637 }
1638 other => {
1639 ogeom_bail!(Construction, "unknown datum modifier tag `{other}`")
1640 }
1641 };
1642 reference.modifiers.push(modifier);
1643 }
1644 }
1645 }
1646 "pmi-datum" => {
1647 let label = read_text(&mut cursor)?;
1648 let mut items = Vec::new();
1649 for _ in 0..cursor.count()? {
1650 let node: TShapeId = cursor.handle()?;
1651 items.push(bind_node(&document, node)?);
1652 }
1653 document
1654 .pmi_mut()
1655 .datums
1656 .push(ogeom_doc::Datum { label, items });
1657 }
1658 "pmi-callout" => {
1659 let name = read_text(&mut cursor)?;
1660 let plane = if cursor.flag()? {
1661 Some(cursor.frame(Tolerances::millimetres())?)
1662 } else {
1663 None
1664 };
1665 let lines = cursor.count()?;
1666 let mut polylines = Vec::with_capacity(cursor.room(lines));
1667 for _ in 0..lines {
1668 let count = cursor.count()?;
1669 let mut line = Vec::with_capacity(cursor.room(count));
1670 for _ in 0..count {
1671 line.push(cursor.point()?);
1672 }
1673 polylines.push(line);
1674 }
1675 let annotates = match cursor.word()? {
1676 "D" => Some(ogeom_doc::Annotated::Dimension(cursor.count()?)),
1677 "T" => Some(ogeom_doc::Annotated::Tolerance(cursor.count()?)),
1678 "M" => Some(ogeom_doc::Annotated::Datum(cursor.count()?)),
1679 _ => None,
1680 };
1681 document.pmi_mut().callouts.push(ogeom_doc::Callout {
1682 name,
1683 plane,
1684 polylines,
1685 annotates,
1686 items: Vec::new(),
1687 text: None,
1688 });
1689 }
1690 "pmi-callout-text" => {
1691 let words = read_text(&mut cursor)?;
1692 let Some(callout) = document.pmi_mut().callouts.last_mut() else {
1693 ogeom_bail!(Construction, "a callout's text follows no callout");
1694 };
1695 callout.text = Some(words);
1696 }
1697 "pmi-callout-items" => {
1698 let mut items = Vec::new();
1699 for _ in 0..cursor.count()? {
1700 let node: TShapeId = cursor.handle()?;
1701 items.push(bind_node(&document, node)?);
1702 }
1703 let Some(callout) = document.pmi_mut().callouts.last_mut() else {
1704 ogeom_bail!(Construction, "a callout's items follow no callout");
1705 };
1706 callout.items = items;
1707 }
1708 "doc-view" => {
1709 let name = read_text(&mut cursor)?;
1710 let frame = cursor.frame(Tolerances::millimetres())?;
1711 let clipping = if cursor.flag()? {
1712 Some(ogeom_math::Plane::new(
1713 cursor.frame(Tolerances::millimetres())?,
1714 ))
1715 } else {
1716 None
1717 };
1718 let count = cursor.count()?;
1719 let mut callouts = Vec::with_capacity(cursor.room(count));
1720 for _ in 0..count {
1721 callouts.push(cursor.count()?);
1722 }
1723 document.add_view(ogeom_doc::View {
1724 name,
1725 frame,
1726 clipping,
1727 callouts,
1728 });
1729 }
1730 "doc-note" => {
1731 let author = read_text(&mut cursor)?;
1732 let text_body = read_text(&mut cursor)?;
1733 let product = if cursor.flag()? {
1734 let index = cursor.count()?;
1735 ids.get(index).copied()
1736 } else {
1737 None
1738 };
1739 document.add_note(ogeom_doc::Note {
1740 author,
1741 text: text_body,
1742 product,
1743 });
1744 }
1745 "doc-construction" => {
1746 let index = cursor.count()?;
1747 let name = read_text(&mut cursor)?;
1748 let tol = Tolerances::millimetres();
1749 let geometry = match cursor.word()? {
1750 "point" => ogeom_doc::ConstructionGeometry::Point(cursor.point()?),
1751 "axis" => ogeom_doc::ConstructionGeometry::Axis(cursor.frame(tol)?),
1752 "curve" => {
1753 let range = cursor.range()?;
1754 ogeom_doc::ConstructionGeometry::Curve {
1755 range,
1756 curve: cursor.curve(tol)?,
1757 }
1758 }
1759 "surface" => {
1760 let u = cursor.range()?;
1761 let v = cursor.range()?;
1762 ogeom_doc::ConstructionGeometry::Surface {
1763 u,
1764 v,
1765 surface: cursor.surface(tol)?,
1766 }
1767 }
1768 other => ogeom_bail!(
1769 Construction,
1770 "a construction item is a point, axis, curve or surface, not `{other}`"
1771 ),
1772 };
1773 let Some(&product) = ids.get(index) else {
1774 ogeom_bail!(
1775 Dangling,
1776 "a construction item names a product not in the file"
1777 );
1778 };
1779 document.add_construction(product, name, geometry)?;
1780 }
1781 other => ogeom_bail!(Construction, "unknown record `{other}`"),
1782 }
1783 }
1784
1785 for (parent, child, at, name) in instances {
1786 let (Some(&parent), Some(&child)) = (ids.get(parent), ids.get(child)) else {
1787 ogeom_bail!(Dangling, "an instance names a product not in the file");
1788 };
1789 let at = document.model().bind_location(&at)?;
1790 document.add_instance_at(parent, child, at, name)?;
1791 }
1792 Ok(document)
1793}
1794
1795fn bind_node(document: &ogeom_doc::Document, node: TShapeId) -> OgeomResult<TShapeId> {
1797 Ok(document.model().bind(&Shape::of(node))?.node())
1798}
1799
1800fn text(t: &mut Tokens, s: &str) {
1803 let mut token = String::from("'");
1804 for byte in s.bytes() {
1805 if byte.is_ascii_graphic() && byte != b'%' {
1806 token.push(byte as char);
1807 } else {
1808 token.push_str(&format!("%{byte:02X}"));
1809 }
1810 }
1811 t.push(token);
1812}
1813
1814fn read_text(cursor: &mut Cursor<'_>) -> OgeomResult<String> {
1816 let token = cursor.word()?;
1817 let Some(body) = token.strip_prefix('\'') else {
1818 ogeom_bail!(Construction, "expected a text token, got `{token}`");
1819 };
1820 let mut out = Vec::new();
1821 let bytes = body.as_bytes();
1822 let mut i = 0;
1823 while i < bytes.len() {
1824 if bytes[i] == b'%' && i + 2 < bytes.len() + 1 && i + 2 < bytes.len() + 1 {
1825 let hex = body.get(i + 1..i + 3).unwrap_or("");
1826 let Ok(byte) = u8::from_str_radix(hex, 16) else {
1827 ogeom_bail!(
1828 Construction,
1829 "a text token escapes `%{hex}`, which is not hex"
1830 );
1831 };
1832 out.push(byte);
1833 i += 3;
1834 } else {
1835 out.push(bytes[i]);
1836 i += 1;
1837 }
1838 }
1839 String::from_utf8(out)
1840 .map_err(|_| ogeom_core::ogeom_err!(Construction, "a text token is not UTF-8"))
1841}
1842
1843fn optional(t: &mut Tokens, v: Option<f64>) {
1845 match v {
1846 Some(value) => {
1847 flag(t, true);
1848 n(t, value);
1849 }
1850 None => flag(t, false),
1851 }
1852}
1853
1854fn read_optional(cursor: &mut Cursor<'_>) -> OgeomResult<Option<f64>> {
1856 Ok(if cursor.flag()? {
1857 Some(cursor.number()?)
1858 } else {
1859 None
1860 })
1861}
1862
1863fn w(t: &mut Tokens, s: &str) {
1865 t.next().push_str(s);
1866}
1867
1868fn n(t: &mut Tokens, v: f64) {
1870 let _ = write!(t.next(), "{v:?}");
1871}
1872
1873fn u(t: &mut Tokens, v: u64) {
1875 let _ = write!(t.next(), "{v}");
1876}
1877
1878fn key<T>(t: &mut Tokens, id: Key<T>) {
1880 let _ = write!(t.next(), "{}:{}", id.index(), id.generation());
1881}
1882
1883fn flag(t: &mut Tokens, v: bool) {
1885 t.next().push(if v { '1' } else { '0' });
1886}
1887
1888#[derive(Default)]
1893struct Tokens(String);
1894
1895impl Tokens {
1896 fn next(&mut self) -> &mut String {
1898 if !self.0.is_empty() {
1899 self.0.push(' ');
1900 }
1901 &mut self.0
1902 }
1903
1904 fn push(&mut self, token: String) {
1905 self.next().push_str(&token);
1906 }
1907}
1908
1909fn emit(out: &mut String, t: &Tokens) {
1911 out.push_str(&t.0);
1912 out.push('\n');
1913}
1914
1915fn point(t: &mut Tokens, p: Point) {
1916 n(t, p.x);
1917 n(t, p.y);
1918 n(t, p.z);
1919}
1920
1921fn point2(t: &mut Tokens, p: Point2) {
1922 n(t, p.x);
1923 n(t, p.y);
1924}
1925
1926fn vector(t: &mut Tokens, v: Vector) {
1927 n(t, v.x);
1928 n(t, v.y);
1929 n(t, v.z);
1930}
1931
1932fn direction(t: &mut Tokens, d: Direction) {
1933 vector(t, d.vector());
1934}
1935
1936fn direction2(t: &mut Tokens, d: Direction2) {
1937 n(t, d.vector().x);
1938 n(t, d.vector().y);
1939}
1940
1941fn frame(t: &mut Tokens, f: &Frame) {
1947 point(t, f.origin());
1948 direction(t, f.x());
1949 direction(t, f.y());
1950 direction(t, f.z());
1951}
1952
1953fn frame2(t: &mut Tokens, f: &Frame2) {
1954 point2(t, f.origin());
1955 direction2(t, f.x());
1956 direction2(t, f.y());
1957}
1958
1959fn axis(t: &mut Tokens, a: Axis) {
1960 point(t, a.location);
1961 direction(t, a.direction);
1962}
1963
1964fn axis2(t: &mut Tokens, a: Axis2) {
1965 point2(t, a.location);
1966 direction2(t, a.direction);
1967}
1968
1969fn range(t: &mut Tokens, r: (f64, f64)) {
1970 n(t, r.0);
1971 n(t, r.1);
1972}
1973
1974fn transform(t: &mut Tokens, x: &Transform) -> OgeomResult<()> {
1976 let m = x.linear();
1977 for row in 0..3 {
1978 for column in 0..3 {
1979 n(t, m.get(row, column)?);
1980 }
1981 }
1982 n(t, x.scale_factor());
1983 vector(t, x.translation_vector());
1984 Ok(())
1985}
1986
1987fn location(t: &mut Tokens, l: &Location) {
1989 if l.is_identity() {
1990 w(t, "-");
1991 return;
1992 }
1993 let chain: Vec<String> = l
1994 .chain()
1995 .iter()
1996 .map(|(datum, power)| format!("{}:{}^{power}", datum.index(), datum.generation()))
1997 .collect();
1998 w(t, &chain.join(","));
1999}
2000
2001fn shape(t: &mut Tokens, s: &Shape) {
2003 let orientation = match s.orientation() {
2004 Orientation::Forward => "F",
2005 Orientation::Reversed => "R",
2006 Orientation::Internal => "I",
2007 Orientation::External => "E",
2008 };
2009 let mut token = format!(
2010 "{}:{}/{orientation}",
2011 s.node().index(),
2012 s.node().generation()
2013 );
2014 if !s.location().is_identity() {
2015 let chain: Vec<String> = s
2016 .location()
2017 .chain()
2018 .iter()
2019 .map(|(datum, power)| format!("{}:{}^{power}", datum.index(), datum.generation()))
2020 .collect();
2021 token.push('/');
2022 token.push_str(&chain.join(","));
2023 }
2024 w(t, &token);
2025}
2026
2027fn knots(t: &mut Tokens, k: &KnotVector) {
2028 u(t, k.degree() as u64);
2029 u(t, k.knots().len() as u64);
2030 for value in k.knots() {
2031 n(t, *value);
2032 }
2033}
2034
2035fn weighted(t: &mut Tokens, c: Weighted<Point>) {
2036 point(t, c.scaled);
2039 n(t, c.weight);
2040}
2041
2042fn weighted2(t: &mut Tokens, c: Weighted<Point2>) {
2043 point2(t, c.scaled);
2044 n(t, c.weight);
2045}
2046
2047fn curve(t: &mut Tokens, c: &Curve) -> OgeomResult<()> {
2048 match c {
2049 Curve::Line(l) => {
2050 w(t, "line");
2051 axis(t, l.axis());
2052 range(t, l.domain());
2053 }
2054 Curve::Circle(c) => {
2055 w(t, "circle");
2056 frame(t, &c.circle().frame());
2057 n(t, c.circle().radius());
2058 flag(t, c.is_reversed());
2059 }
2060 Curve::Ellipse(e) => {
2061 w(t, "ellipse");
2062 frame(t, &e.ellipse().frame());
2063 n(t, e.ellipse().major_radius());
2064 n(t, e.ellipse().minor_radius());
2065 flag(t, e.is_reversed());
2066 }
2067 Curve::Hyperbola(h) => {
2068 w(t, "hyperbola");
2069 frame(t, &h.hyperbola().frame());
2070 n(t, h.hyperbola().major_radius());
2071 n(t, h.hyperbola().minor_radius());
2072 range(t, h.domain());
2073 flag(t, h.is_reversed());
2074 }
2075 Curve::Parabola(p) => {
2076 w(t, "parabola");
2077 frame(t, &p.parabola().frame());
2078 n(t, p.parabola().focal());
2079 range(t, p.domain());
2080 flag(t, p.is_reversed());
2081 }
2082 Curve::Helix(h) => {
2083 if h.taper() == 0.0 {
2084 w(t, "helix");
2085 frame(t, h.frame());
2086 n(t, h.radius());
2087 n(t, h.pitch());
2088 range(t, Curve3d::domain(h));
2089 flag(t, h.is_reversed());
2090 } else {
2091 w(t, "conical");
2092 frame(t, h.frame());
2093 n(t, h.radius());
2094 n(t, h.pitch());
2095 n(t, h.taper());
2096 range(t, Curve3d::domain(h));
2097 flag(t, h.is_reversed());
2098 }
2099 }
2100 Curve::BSpline(b) => {
2101 w(
2102 t,
2103 if b.is_periodic() {
2104 "bspline_periodic"
2105 } else {
2106 "bspline"
2107 },
2108 );
2109 knots(t, b.knots());
2110 u(t, b.control_points().len() as u64);
2111 for c in b.control_points() {
2112 weighted(t, *c);
2113 }
2114 }
2115 Curve::Trimmed(x) => {
2116 w(t, "trimmed");
2117 range(t, x.domain());
2118 flag(t, x.is_reversed());
2119 curve(t, x.basis())?;
2120 }
2121 Curve::Offset(o) => {
2122 w(t, "offset");
2123 n(t, o.distance());
2124 direction(t, o.reference());
2125 curve(t, o.basis())?;
2126 }
2127 Curve::OnSurface(c) => {
2128 w(t, "onsurface");
2129 pcurve(t, c.pcurve())?;
2130 surface(t, c.surface())?;
2131 }
2132 }
2133 Ok(())
2134}
2135
2136fn pcurve(t: &mut Tokens, c: &PlanarCurve) -> OgeomResult<()> {
2137 match c {
2138 PlanarCurve::Line(l) => {
2139 w(t, "line2");
2140 axis2(t, l.axis());
2141 range(t, l.domain());
2142 }
2143 PlanarCurve::Circle(c) => {
2144 w(t, "circle2");
2145 frame2(t, &c.circle().frame());
2146 n(t, c.circle().radius());
2147 flag(t, c.is_reversed());
2148 }
2149 PlanarCurve::Ellipse(e) => {
2150 w(t, "ellipse2");
2151 frame2(t, &e.ellipse().frame());
2152 n(t, e.ellipse().major_radius());
2153 n(t, e.ellipse().minor_radius());
2154 flag(t, e.is_reversed());
2155 }
2156 PlanarCurve::Hyperbola(h) => {
2157 w(t, "hyperbola2");
2158 frame2(t, &h.hyperbola().frame());
2159 n(t, h.hyperbola().major_radius());
2160 n(t, h.hyperbola().minor_radius());
2161 range(t, Curve2d::domain(h));
2162 flag(t, h.is_reversed());
2163 }
2164 PlanarCurve::BSpline(b) => {
2165 w(t, "bspline2");
2166 knots(t, b.knots());
2167 u(t, b.control_points().len() as u64);
2168 for c in b.control_points() {
2169 weighted2(t, *c);
2170 }
2171 }
2172 PlanarCurve::Trimmed(x) => {
2173 w(t, "trimmed2");
2174 range(t, x.domain());
2175 flag(t, x.is_reversed());
2176 pcurve(t, x.basis())?;
2177 }
2178 PlanarCurve::Offset(o) => {
2179 w(t, "offset2");
2180 n(t, o.distance());
2181 pcurve(t, o.basis())?;
2182 }
2183 PlanarCurve::Trig(x) => {
2184 w(t, "trig2");
2185 point2(t, x.constant());
2186 n(t, x.linear().x);
2187 n(t, x.linear().y);
2188 n(t, x.cosine().x);
2189 n(t, x.cosine().y);
2190 n(t, x.sine().x);
2191 n(t, x.sine().y);
2192 range(t, Curve2d::domain(x));
2193 flag(t, x.is_reversed());
2194 }
2195 }
2196 Ok(())
2197}
2198
2199fn surface(t: &mut Tokens, s: &SurfaceGeometry) -> OgeomResult<()> {
2200 let (u_domain, v_domain) = s.domain();
2201 match s {
2202 SurfaceGeometry::Plane(p) => {
2203 w(t, "plane");
2204 frame(t, &p.plane().frame());
2205 range(t, u_domain);
2206 range(t, v_domain);
2207 }
2208 SurfaceGeometry::Cylinder(c) => {
2209 w(t, "cylinder");
2210 frame(t, &c.cylinder().frame());
2211 n(t, c.cylinder().radius());
2212 range(t, v_domain);
2213 }
2214 SurfaceGeometry::Cone(c) => {
2215 w(t, "cone");
2216 frame(t, &c.cone().frame());
2217 n(t, c.cone().reference_radius());
2218 n(t, c.cone().half_angle());
2219 range(t, v_domain);
2220 }
2221 SurfaceGeometry::Sphere(s) => {
2222 w(t, "sphere");
2223 frame(t, &s.sphere().frame());
2224 n(t, s.sphere().radius());
2225 }
2226 SurfaceGeometry::Torus(x) => {
2227 w(t, "torus");
2228 frame(t, &x.torus().frame());
2229 n(t, x.torus().major_radius());
2230 n(t, x.torus().minor_radius());
2231 }
2232 SurfaceGeometry::BSpline(b) => {
2233 w(t, "bsurface");
2234 knots(t, b.u_knots());
2235 knots(t, b.v_knots());
2236 u(t, b.grid().u_count() as u64);
2237 u(t, b.grid().v_count() as u64);
2238 for c in b.grid().points() {
2239 weighted(t, *c);
2240 }
2241 }
2242 SurfaceGeometry::Revolution(r) => {
2243 w(t, "revolution");
2244 axis(t, r.axis());
2245 range(t, u_domain);
2246 curve(t, r.curve())?;
2247 }
2248 SurfaceGeometry::Extrusion(e) => {
2249 w(t, "extrusion");
2250 direction(t, e.direction());
2251 range(t, v_domain);
2252 curve(t, e.curve())?;
2253 }
2254 SurfaceGeometry::Trimmed(x) => {
2255 w(t, "tsurface");
2256 range(t, u_domain);
2257 range(t, v_domain);
2258 surface(t, x.basis())?;
2259 }
2260 SurfaceGeometry::Offset(o) => {
2261 w(t, "osurface");
2262 n(t, o.distance());
2263 surface(t, o.basis())?;
2264 }
2265 }
2266 Ok(())
2267}
2268
2269fn provenance(t: &mut Tokens, p: &Provenance) {
2270 match p {
2271 Provenance::Primitive { op, role } => {
2272 w(t, "primitive");
2273 u(t, u64::from(op.0));
2274 u(t, u64::from(role.0));
2275 }
2276 Provenance::Derived { op, from, role } => {
2277 w(t, "derived");
2278 u(t, u64::from(op.0));
2279 u(t, u64::from(role.0));
2280 u(t, from.len() as u64);
2281 for source in from {
2282 u(t, source.get());
2283 }
2284 }
2285 Provenance::Imported { source, external } => {
2286 w(t, "imported");
2287 u(t, u64::from(source.0));
2288 u(t, *external);
2289 }
2290 }
2291}
2292
2293fn write_mesh(out: &mut String, id: ogeom_topo::TriangulationId, mesh: &Triangulation) {
2299 let mut t = Tokens::default();
2300 w(&mut t, "mesh");
2301 key(&mut t, id);
2302 u(&mut t, mesh.positions.len() as u64);
2303 u(&mut t, mesh.triangles.len() as u64);
2304 flag(&mut t, mesh.deflection_met);
2305 emit(out, &t);
2306
2307 for i in 0..mesh.positions.len() {
2308 let mut t = Tokens::default();
2309 w(&mut t, "v");
2310 point(&mut t, mesh.positions[i]);
2311 vector(&mut t, mesh.normals.get(i).copied().unwrap_or(Vector::ZERO));
2315 let (u_at, v_at) = mesh.parameters.get(i).copied().unwrap_or((0.0, 0.0));
2316 n(&mut t, u_at);
2317 n(&mut t, v_at);
2318 emit(out, &t);
2319 }
2320 for triangle in &mesh.triangles {
2321 let mut t = Tokens::default();
2322 w(&mut t, "f");
2323 for index in triangle {
2324 u(&mut t, u64::from(*index));
2325 }
2326 emit(out, &t);
2327 }
2328}
2329
2330fn write_node(
2331 out: &mut String,
2332 id: TShapeId,
2333 node: &TShape,
2334 options: WriteOptions,
2335) -> OgeomResult<()> {
2336 let mut t = Tokens::default();
2337 w(&mut t, "node");
2338 key(&mut t, id);
2339 w(
2340 &mut t,
2341 match node.kind() {
2342 ShapeType::Vertex => "vertex",
2343 ShapeType::Edge => "edge",
2344 ShapeType::Wire => "wire",
2345 ShapeType::Face => "face",
2346 ShapeType::Shell => "shell",
2347 ShapeType::Solid => "solid",
2348 ShapeType::CompSolid => "compsolid",
2349 ShapeType::Compound => "compound",
2350 },
2351 );
2352
2353 let mut representations = Vec::new();
2354 match node.data() {
2355 NodeData::Vertex(v) => {
2356 n(&mut t, v.tolerance.get());
2357 point(&mut t, v.point);
2358 }
2359 NodeData::Edge(e) => {
2360 n(&mut t, e.tolerance.get());
2361 flag(&mut t, e.same_parameter());
2362 flag(&mut t, e.degenerate);
2363 let kept: Vec<&EdgeRepr> = e
2367 .representations
2368 .iter()
2369 .filter(|repr| {
2370 options.triangulations
2371 || !matches!(repr, EdgeRepr::PolygonOnTriangulation { .. })
2372 })
2373 .collect();
2374 u(&mut t, kept.len() as u64);
2375 for repr in kept {
2376 let mut line = Tokens::default();
2377 w(&mut line, "r");
2378 write_repr(&mut line, repr)?;
2379 representations.push(line);
2380 }
2381 }
2382 NodeData::Face(f) => {
2383 n(&mut t, f.tolerance.get());
2384 key(&mut t, f.surface);
2385 location(&mut t, &f.location);
2386 flag(&mut t, f.natural_restriction);
2387 match f.triangulation.filter(|_| options.triangulations) {
2388 Some(mesh) => key(&mut t, mesh),
2389 None => w(&mut t, "-"),
2390 }
2391 }
2392 NodeData::Container => {}
2393 }
2394
2395 u(&mut t, node.children().len() as u64);
2396 for child in node.children() {
2397 shape(&mut t, child);
2398 }
2399 emit(out, &t);
2400 for line in &representations {
2401 emit(out, line);
2402 }
2403 Ok(())
2404}
2405
2406fn write_repr(t: &mut Tokens, repr: &EdgeRepr) -> OgeomResult<()> {
2407 match repr {
2408 EdgeRepr::Curve3d {
2409 curve,
2410 location: at,
2411 range: r,
2412 } => {
2413 w(t, "curve3d");
2414 key(t, *curve);
2415 location(t, at);
2416 range(t, *r);
2417 }
2418 EdgeRepr::PCurve {
2419 curve,
2420 surface,
2421 location: at,
2422 range: r,
2423 } => {
2424 w(t, "pcurve");
2425 key(t, *curve);
2426 key(t, *surface);
2427 location(t, at);
2428 range(t, *r);
2429 }
2430 EdgeRepr::Seam {
2431 forward,
2432 reversed,
2433 surface,
2434 location: at,
2435 range: r,
2436 } => {
2437 w(t, "seam");
2438 key(t, *forward);
2439 key(t, *reversed);
2440 key(t, *surface);
2441 location(t, at);
2442 range(t, *r);
2443 }
2444 EdgeRepr::Polyline {
2445 points,
2446 parameters,
2447 location: at,
2448 deflection,
2449 } => {
2450 w(t, "polyline");
2451 location(t, at);
2452 n(t, *deflection);
2453 u(t, points.len() as u64);
2454 for p in points {
2455 point(t, *p);
2456 }
2457 u(t, parameters.len() as u64);
2458 for at in parameters {
2459 n(t, *at);
2460 }
2461 }
2462 EdgeRepr::PolygonOnTriangulation {
2463 triangulation,
2464 indices,
2465 location: at,
2466 } => {
2467 w(t, "polygon-on");
2468 key(t, *triangulation);
2469 location(t, at);
2470 u(t, indices.len() as u64);
2471 for index in indices {
2472 u(t, u64::from(*index));
2473 }
2474 }
2475 other => ogeom_bail!(
2480 Construction,
2481 "this version writes no edge representation of that kind: {other:?}"
2482 ),
2483 }
2484 Ok(())
2485}
2486
2487struct Cursor<'a> {
2495 items: Vec<&'a str>,
2496 at: usize,
2497}
2498
2499impl<'a> Cursor<'a> {
2500 fn new(text: &'a str) -> Self {
2501 let items = text
2502 .lines()
2503 .map(|line| line.split('#').next().unwrap_or(""))
2504 .flat_map(str::split_whitespace)
2505 .collect();
2506 Self { items, at: 0 }
2507 }
2508
2509 fn peek(&self) -> Option<&'a str> {
2510 self.items.get(self.at).copied()
2511 }
2512
2513 fn done(&self) -> bool {
2514 self.at >= self.items.len()
2515 }
2516
2517 fn word(&mut self) -> OgeomResult<&'a str> {
2518 let Some(item) = self.items.get(self.at) else {
2519 ogeom_bail!(Construction, "the document ends part-way through a record");
2520 };
2521 self.at += 1;
2522 Ok(item)
2523 }
2524
2525 fn number(&mut self) -> OgeomResult<f64> {
2526 let word = self.word()?;
2527 word.parse::<f64>()
2528 .map_err(|_| ogeom_core::ogeom_err!(Construction, "`{word}` is not a number"))
2529 }
2530
2531 fn room(&self, n: usize) -> usize {
2535 n.min(self.items.len().saturating_sub(self.at))
2536 }
2537
2538 fn count(&mut self) -> OgeomResult<usize> {
2539 let word = self.word()?;
2540 word.parse::<usize>()
2541 .map_err(|_| ogeom_core::ogeom_err!(Construction, "`{word}` is not a count"))
2542 }
2543
2544 fn small(&mut self) -> OgeomResult<u32> {
2545 let word = self.word()?;
2546 word.parse::<u32>()
2547 .map_err(|_| ogeom_core::ogeom_err!(Construction, "`{word}` is not an identifier"))
2548 }
2549
2550 fn flag(&mut self) -> OgeomResult<bool> {
2551 match self.word()? {
2552 "0" => Ok(false),
2553 "1" => Ok(true),
2554 other => ogeom_bail!(Construction, "`{other}` is not a flag"),
2555 }
2556 }
2557
2558 fn key(&mut self) -> OgeomResult<(u32, u32)> {
2560 parse_key(self.word()?)
2561 }
2562
2563 fn handle<T>(&mut self) -> OgeomResult<Key<T>> {
2564 let (index, generation) = self.key()?;
2565 Ok(Key::from_parts(index, generation))
2566 }
2567
2568 fn shape_key(&mut self) -> OgeomResult<TShapeId> {
2569 self.handle()
2570 }
2571
2572 fn point(&mut self) -> OgeomResult<Point> {
2573 Ok(Point::new(self.number()?, self.number()?, self.number()?))
2574 }
2575
2576 fn point2(&mut self) -> OgeomResult<Point2> {
2577 Ok(Point2::new(self.number()?, self.number()?))
2578 }
2579
2580 fn vector(&mut self) -> OgeomResult<Vector> {
2581 Ok(Vector::new(self.number()?, self.number()?, self.number()?))
2582 }
2583
2584 fn direction(&mut self, tol: Tolerances) -> OgeomResult<Direction> {
2589 Direction::unit(self.vector()?, tol)
2590 }
2591
2592 fn direction2(&mut self, tol: Tolerances) -> OgeomResult<Direction2> {
2593 Direction2::unit(
2594 ogeom_math::Vector2::new(self.number()?, self.number()?),
2595 tol,
2596 )
2597 }
2598
2599 fn frame(&mut self, tol: Tolerances) -> OgeomResult<Frame> {
2600 let origin = self.point()?;
2601 let x = self.direction(tol)?;
2602 let y = self.direction(tol)?;
2603 let z = self.direction(tol)?;
2604 Frame::from_axes(origin, x, y, z, tol)
2605 }
2606
2607 fn frame2(&mut self, tol: Tolerances) -> OgeomResult<Frame2> {
2608 let origin = self.point2()?;
2609 let x = self.direction2(tol)?;
2610 let y = self.direction2(tol)?;
2611 Frame2::from_axes(origin, x, y, tol)
2612 }
2613
2614 fn axis(&mut self, tol: Tolerances) -> OgeomResult<Axis> {
2615 Ok(Axis::new(self.point()?, self.direction(tol)?))
2616 }
2617
2618 fn axis2(&mut self, tol: Tolerances) -> OgeomResult<Axis2> {
2619 Ok(Axis2::new(self.point2()?, self.direction2(tol)?))
2620 }
2621
2622 fn range(&mut self) -> OgeomResult<(f64, f64)> {
2623 Ok((self.number()?, self.number()?))
2624 }
2625
2626 fn transform(&mut self, tol: Tolerances) -> OgeomResult<Transform> {
2627 let mut rows = [[0.0_f64; 3]; 3];
2628 for row in &mut rows {
2629 for cell in row.iter_mut() {
2630 *cell = self.number()?;
2631 }
2632 }
2633 let scale = self.number()?;
2634 let translation = self.vector()?;
2635 Transform::from_parts(
2641 ogeom_math::Matrix3::new(rows),
2642 scale,
2643 translation,
2644 tol.angular().max(1e-9),
2645 )
2646 }
2647
2648 fn location(&mut self) -> OgeomResult<Location> {
2649 let word = self.word()?;
2650 parse_location(word)
2651 }
2652
2653 fn shape(&mut self) -> OgeomResult<Shape> {
2654 let word = self.word()?;
2655 let mut parts = word.split('/');
2656 let (Some(node), Some(orientation)) = (parts.next(), parts.next()) else {
2657 ogeom_bail!(Construction, "`{word}` is not a shape");
2658 };
2659 let (index, generation) = parse_key(node)?;
2660 let orientation = match orientation {
2661 "F" => Orientation::Forward,
2662 "R" => Orientation::Reversed,
2663 "I" => Orientation::Internal,
2664 "E" => Orientation::External,
2665 other => ogeom_bail!(Construction, "`{other}` is not an orientation"),
2666 };
2667 let location = match parts.next() {
2668 Some(chain) => parse_location(chain)?,
2669 None => Location::identity(),
2670 };
2671 if parts.next().is_some() {
2672 ogeom_bail!(Construction, "`{word}` has more parts than a shape has");
2673 }
2674 Ok(Shape::new(
2675 Key::from_parts(index, generation),
2676 location,
2677 orientation,
2678 ))
2679 }
2680
2681 fn knots(&mut self) -> OgeomResult<KnotVector> {
2682 let degree = self.count()?;
2683 let n = self.count()?;
2684 let mut values = Vec::with_capacity(self.room(n));
2685 for _ in 0..n {
2686 values.push(self.number()?);
2687 }
2688 KnotVector::new(values, degree)
2689 }
2690
2691 fn weighted(&mut self) -> OgeomResult<Weighted<Point>> {
2692 Ok(Weighted {
2693 scaled: self.point()?,
2694 weight: self.number()?,
2695 })
2696 }
2697
2698 fn weighted2(&mut self) -> OgeomResult<Weighted<Point2>> {
2699 Ok(Weighted {
2700 scaled: self.point2()?,
2701 weight: self.number()?,
2702 })
2703 }
2704
2705 fn curve(&mut self, tol: Tolerances) -> OgeomResult<Curve> {
2706 Ok(match self.word()? {
2707 "line" => {
2708 let axis = self.axis(tol)?;
2709 let (lo, hi) = self.range()?;
2710 LineCurve::over(axis, lo, hi)?.into()
2711 }
2712 "circle" => {
2713 let circle = Circle::new(self.frame(tol)?, self.number()?, tol)?;
2714 reverse_if(CircleCurve::new(circle).into(), self.flag()?)
2715 }
2716 "ellipse" => {
2717 let frame = self.frame(tol)?;
2718 let ellipse = Ellipse::new(frame, self.number()?, self.number()?, tol)?;
2719 reverse_if(EllipseCurve::new(ellipse).into(), self.flag()?)
2720 }
2721 "hyperbola" => {
2722 let frame = self.frame(tol)?;
2723 let h = Hyperbola::new(frame, self.number()?, self.number()?, tol)?;
2724 let (lo, hi) = self.range()?;
2725 let curve = HyperbolaCurve::over(h, lo, hi)?;
2726 reverse_if(curve.into(), self.flag()?)
2727 }
2728 "parabola" => {
2729 let frame = self.frame(tol)?;
2730 let p = Parabola::new(frame, self.number()?, tol)?;
2731 let (lo, hi) = self.range()?;
2732 let curve = ParabolaCurve::over(p, lo, hi)?;
2733 reverse_if(curve.into(), self.flag()?)
2734 }
2735 "helix" => {
2736 let frame = self.frame(tol)?;
2737 let radius = self.number()?;
2738 let pitch = self.number()?;
2739 let (lo, hi) = self.range()?;
2740 let curve = HelixCurve::over(frame, radius, pitch, lo, hi)?;
2741 reverse_if(curve.into(), self.flag()?)
2742 }
2743 "conical" => {
2744 let frame = self.frame(tol)?;
2745 let radius = self.number()?;
2746 let pitch = self.number()?;
2747 let taper = self.number()?;
2748 let (lo, hi) = self.range()?;
2749 let curve = HelixCurve::conical(frame, radius, pitch, taper, lo, hi)?;
2750 reverse_if(curve.into(), self.flag()?)
2751 }
2752 "bspline" => {
2753 let knots = self.knots()?;
2754 let n = self.count()?;
2755 let mut control = Vec::with_capacity(self.room(n));
2756 for _ in 0..n {
2757 control.push(self.weighted()?);
2758 }
2759 BSplineCurve::rational(knots, control)?.into()
2760 }
2761 "bspline_periodic" => {
2762 let knots = self.knots()?;
2763 let n = self.count()?;
2764 let mut control = Vec::with_capacity(self.room(n));
2765 for _ in 0..n {
2766 control.push(self.weighted()?);
2767 }
2768 BSplineCurve::periodic_from_parts(knots, control, tol)?.into()
2769 }
2770 "trimmed" => {
2771 let (lo, hi) = self.range()?;
2772 let reversed = self.flag()?;
2773 let basis = self.curve(tol)?;
2774 let curve = TrimmedCurve::new(basis, lo, hi, tol)?;
2775 reverse_if(Curve::Trimmed(Box::new(curve)), reversed)
2776 }
2777 "offset" => {
2778 let distance = self.number()?;
2779 let reference = self.direction(tol)?;
2780 let basis = self.curve(tol)?;
2781 Curve::Offset(Box::new(ogeom_geom::OffsetCurve::new(
2782 basis, distance, reference,
2783 )?))
2784 }
2785 "onsurface" => {
2786 let pcurve = self.pcurve(tol)?;
2787 let surface = self.surface(tol)?;
2788 Curve::OnSurface(Box::new(ogeom_geom::CurveOnSurface::new(pcurve, surface)))
2789 }
2790 other => ogeom_bail!(Construction, "`{other}` is not a curve this reads"),
2791 })
2792 }
2793
2794 fn pcurve(&mut self, tol: Tolerances) -> OgeomResult<PlanarCurve> {
2795 Ok(match self.word()? {
2796 "line2" => {
2797 let axis = self.axis2(tol)?;
2798 let (lo, hi) = self.range()?;
2799 Line2d::over(axis, lo, hi)?.into()
2800 }
2801 "circle2" => {
2802 let circle = Circle2::new(self.frame2(tol)?, self.number()?, tol)?;
2803 reverse_if(Circle2d::new(circle).into(), self.flag()?)
2804 }
2805 "ellipse2" => {
2806 let frame = self.frame2(tol)?;
2807 let ellipse = Ellipse2::new(frame, self.number()?, self.number()?, tol)?;
2808 reverse_if(Ellipse2d::new(ellipse).into(), self.flag()?)
2809 }
2810 "hyperbola2" => {
2811 let frame = self.frame2(tol)?;
2812 let hyperbola = Hyperbola2::new(frame, self.number()?, self.number()?, tol)?;
2813 let (lo, hi) = self.range()?;
2814 let curve = ogeom_geom::Hyperbola2d::over(hyperbola, lo, hi)?;
2815 reverse_if(curve.into(), self.flag()?)
2816 }
2817 "bspline2" => {
2818 let knots = self.knots()?;
2819 let n = self.count()?;
2820 let mut control = Vec::with_capacity(self.room(n));
2821 for _ in 0..n {
2822 control.push(self.weighted2()?);
2823 }
2824 BSpline2d::rational(knots, control)?.into()
2825 }
2826 "trimmed2" => {
2827 let (lo, hi) = self.range()?;
2828 let reversed = self.flag()?;
2829 let basis = self.pcurve(tol)?;
2830 let curve = Trimmed2d::new(basis, lo, hi, tol)?;
2831 reverse_if(PlanarCurve::Trimmed(Box::new(curve)), reversed)
2832 }
2833 "offset2" => {
2834 let distance = self.number()?;
2835 let basis = self.pcurve(tol)?;
2836 PlanarCurve::Offset(Box::new(ogeom_geom::Offset2d::new(basis, distance)?))
2837 }
2838 "trig2" => {
2839 let c = self.point2()?;
2840 let d = ogeom_math::Vector2::new(self.number()?, self.number()?);
2841 let a = ogeom_math::Vector2::new(self.number()?, self.number()?);
2842 let b = ogeom_math::Vector2::new(self.number()?, self.number()?);
2843 let (lo, hi) = self.range()?;
2844 let curve = ogeom_geom::Trig2d::new(c, d, a, b, (lo, hi))?;
2845 reverse_if(PlanarCurve::Trig(curve), self.flag()?)
2846 }
2847 other => ogeom_bail!(Construction, "`{other}` is not a planar curve this reads"),
2848 })
2849 }
2850
2851 fn surface(&mut self, tol: Tolerances) -> OgeomResult<SurfaceGeometry> {
2852 Ok(match self.word()? {
2853 "plane" => {
2854 let plane = Plane::new(self.frame(tol)?);
2855 PlaneSurface::over(plane, self.range()?, self.range()?)?.into()
2856 }
2857 "cylinder" => {
2858 let cylinder = Cylinder::new(self.frame(tol)?, self.number()?, tol)?;
2859 CylinderSurface::new(cylinder, self.range()?)?.into()
2860 }
2861 "cone" => {
2862 let frame = self.frame(tol)?;
2863 let cone = Cone::new(frame, self.number()?, self.number()?, tol)?;
2864 ConeSurface::new(cone, self.range()?)?.into()
2865 }
2866 "sphere" => {
2867 let sphere = Sphere::new(self.frame(tol)?, self.number()?, tol)?;
2868 SphereSurface::new(sphere).into()
2869 }
2870 "torus" => {
2871 let frame = self.frame(tol)?;
2872 let torus = Torus::new(frame, self.number()?, self.number()?, tol)?;
2873 TorusSurface::new(torus).into()
2874 }
2875 "bsurface" => {
2876 let u_knots = self.knots()?;
2877 let v_knots = self.knots()?;
2878 let u_count = self.count()?;
2879 let v_count = self.count()?;
2880 let Some(cells) = u_count.checked_mul(v_count) else {
2881 ogeom_bail!(Construction, "a {u_count} by {v_count} control grid");
2882 };
2883 let mut points = Vec::with_capacity(self.room(cells));
2884 for _ in 0..cells {
2885 points.push(self.weighted()?);
2886 }
2887 let grid = ControlGrid::new(points, u_count, v_count)?;
2888 BSplineSurface::rational(u_knots, v_knots, grid)?.into()
2889 }
2890 "revolution" => {
2891 let axis = self.axis(tol)?;
2892 let (start, end) = self.range()?;
2893 if start != 0.0 {
2894 ogeom_bail!(
2895 Construction,
2896 "a revolution's angle starts at zero; this one starts at \
2897 {start}"
2898 );
2899 }
2900 let basis = self.curve(tol)?;
2901 RevolutionSurface::new(basis, axis, end)?.into()
2902 }
2903 "extrusion" => {
2904 let direction = self.direction(tol)?;
2905 let extent = self.range()?;
2906 let basis = self.curve(tol)?;
2907 ExtrusionSurface::over(basis, direction, extent)?.into()
2908 }
2909 "tsurface" => {
2910 let u_range = self.range()?;
2911 let v_range = self.range()?;
2912 let basis = self.surface(tol)?;
2913 SurfaceGeometry::Trimmed(Box::new(TrimmedSurface::new(
2914 basis, u_range, v_range, tol,
2915 )?))
2916 }
2917 "osurface" => {
2918 let distance = self.number()?;
2919 let basis = self.surface(tol)?;
2920 SurfaceGeometry::Offset(Box::new(ogeom_geom::OffsetSurface::new(basis, distance)?))
2921 }
2922 other => ogeom_bail!(Construction, "`{other}` is not a surface this reads"),
2923 })
2924 }
2925
2926 fn mesh(&mut self) -> OgeomResult<Triangulation> {
2927 let vertices = self.count()?;
2928 let triangles = self.count()?;
2929 let mut mesh = Triangulation::new();
2930 mesh.deflection_met = self.flag()?;
2931 for _ in 0..vertices {
2932 if self.word()? != "v" {
2933 ogeom_bail!(Construction, "expected a mesh vertex");
2934 }
2935 mesh.positions.push(self.point()?);
2936 mesh.normals.push(self.vector()?);
2937 mesh.parameters.push((self.number()?, self.number()?));
2938 }
2939 for _ in 0..triangles {
2940 if self.word()? != "f" {
2941 ogeom_bail!(Construction, "expected a mesh triangle");
2942 }
2943 let mut corners = [0_u32; 3];
2944 for corner in &mut corners {
2945 *corner = self.small()?;
2946 if *corner as usize >= vertices {
2947 ogeom_bail!(
2948 Dangling,
2949 "a triangle names vertex {corner}, and the mesh has \
2950 {vertices}"
2951 );
2952 }
2953 }
2954 mesh.triangles.push(corners);
2955 }
2956 Ok(mesh)
2957 }
2958
2959 fn provenance(&mut self) -> OgeomResult<Provenance> {
2960 Ok(match self.word()? {
2961 "primitive" => Provenance::Primitive {
2962 op: OpId(self.small()?),
2963 role: Role(self.small()?),
2964 },
2965 "derived" => {
2966 let op = OpId(self.small()?);
2967 let role = Role(self.small()?);
2968 let n = self.count()?;
2969 let mut from = Vec::with_capacity(self.room(n));
2970 for _ in 0..n {
2971 let raw = self.count()?;
2972 let Some(id) = EntityId::from_raw(raw as u64) else {
2973 ogeom_bail!(Construction, "identity 0 was never issued");
2974 };
2975 from.push(id);
2976 }
2977 Provenance::Derived {
2978 op,
2979 from: from.into_iter().collect(),
2980 role,
2981 }
2982 }
2983 "imported" => Provenance::Imported {
2984 source: SourceId(self.small()?),
2985 external: self.count()? as u64,
2986 },
2987 other => ogeom_bail!(Construction, "`{other}` is not a provenance"),
2988 })
2989 }
2990
2991 fn node(&mut self) -> OgeomResult<TShape> {
2992 let kind = match self.word()? {
2993 "vertex" => ShapeType::Vertex,
2994 "edge" => ShapeType::Edge,
2995 "wire" => ShapeType::Wire,
2996 "face" => ShapeType::Face,
2997 "shell" => ShapeType::Shell,
2998 "solid" => ShapeType::Solid,
2999 "compsolid" => ShapeType::CompSolid,
3000 "compound" => ShapeType::Compound,
3001 other => ogeom_bail!(Construction, "`{other}` is not a kind of shape"),
3002 };
3003
3004 let mut pending = None;
3008 let mut data = match kind {
3009 ShapeType::Vertex => {
3010 let tolerance = self.number()?;
3011 NodeData::Vertex(VertexData::with_tolerance(self.point()?, tolerance)?)
3012 }
3013 ShapeType::Edge => {
3014 let mut edge = EdgeData::new();
3015 edge.tolerance = Tolerance::new(self.number()?)?;
3016 let agrees = self.flag()?;
3017 edge.degenerate = self.flag()?;
3018 pending = Some((agrees, self.count()?));
3019 NodeData::Edge(Box::new(edge))
3020 }
3021 ShapeType::Face => {
3022 let tolerance = Tolerance::new(self.number()?)?;
3023 let surface = self.handle()?;
3024 let at = self.location()?;
3025 let natural = self.flag()?;
3026 let mut face = if natural {
3027 FaceData::natural(surface, at)
3028 } else {
3029 FaceData::new(surface, at)
3030 };
3031 face.tolerance = tolerance;
3032 face.triangulation = match self.word()? {
3033 "-" => None,
3034 word => Some(Key::from_parts_of(parse_key(word)?)),
3035 };
3036 NodeData::Face(Box::new(face))
3037 }
3038 _ => NodeData::Container,
3039 };
3040
3041 let n = self.count()?;
3042 let mut children = Vec::with_capacity(self.room(n));
3043 for _ in 0..n {
3044 children.push(self.shape()?);
3045 }
3046
3047 if let (Some((agrees, count)), NodeData::Edge(edge)) = (pending, &mut data) {
3048 for _ in 0..count {
3049 if self.word()? != "r" {
3050 ogeom_bail!(Construction, "expected an edge representation");
3051 }
3052 let repr = self.repr()?;
3053 edge.add(repr);
3054 }
3055 edge.assert_same_parameter(agrees);
3059 }
3060 Ok(TShape::new(kind, data, children))
3061 }
3062
3063 fn repr(&mut self) -> OgeomResult<EdgeRepr> {
3064 Ok(match self.word()? {
3065 "curve3d" => EdgeRepr::Curve3d {
3066 curve: self.handle()?,
3067 location: self.location()?,
3068 range: self.range()?,
3069 },
3070 "pcurve" => EdgeRepr::PCurve {
3071 curve: self.handle()?,
3072 surface: self.handle()?,
3073 location: self.location()?,
3074 range: self.range()?,
3075 },
3076 "seam" => EdgeRepr::Seam {
3077 forward: self.handle()?,
3078 reversed: self.handle()?,
3079 surface: self.handle()?,
3080 location: self.location()?,
3081 range: self.range()?,
3082 },
3083 "polyline" => {
3084 let location = self.location()?;
3085 let deflection = self.number()?;
3086 let n = self.count()?;
3087 let mut points = Vec::with_capacity(self.room(n));
3088 for _ in 0..n {
3089 points.push(self.point()?);
3090 }
3091 let n = self.count()?;
3092 let mut parameters = Vec::with_capacity(self.room(n));
3093 for _ in 0..n {
3094 parameters.push(self.number()?);
3095 }
3096 EdgeRepr::Polyline {
3097 points,
3098 parameters,
3099 location,
3100 deflection,
3101 }
3102 }
3103 "polygon-on" => {
3104 let triangulation = self.handle()?;
3105 let location = self.location()?;
3106 let n = self.count()?;
3107 let mut indices = Vec::with_capacity(self.room(n));
3108 for _ in 0..n {
3109 let raw = self.count()?;
3110 indices.push(u32::try_from(raw).map_err(|_| {
3111 ogeom_core::ogeom_err!(Construction, "a mesh index does not fit u32")
3112 })?);
3113 }
3114 EdgeRepr::PolygonOnTriangulation {
3115 triangulation,
3116 indices,
3117 location,
3118 }
3119 }
3120 other => ogeom_bail!(
3121 Construction,
3122 "`{other}` is not an edge representation this reads"
3123 ),
3124 })
3125 }
3126}
3127
3128fn reverse_if<T: ogeom_geom::Reversible>(curve: T, reversed: bool) -> T {
3130 if reversed { curve.reversed() } else { curve }
3131}
3132
3133fn parse_key(word: &str) -> OgeomResult<(u32, u32)> {
3134 let Some((index, generation)) = word.split_once(':') else {
3135 ogeom_bail!(Construction, "`{word}` is not a handle");
3136 };
3137 let (Ok(index), Ok(generation)) = (index.parse::<u32>(), generation.parse::<u32>()) else {
3138 ogeom_bail!(Construction, "`{word}` is not a handle");
3139 };
3140 Ok((index, generation))
3141}
3142
3143fn parse_location(word: &str) -> OgeomResult<Location> {
3144 if word == "-" {
3145 return Ok(Location::identity());
3146 }
3147 let mut location = Location::identity();
3148 for step in word.split(',') {
3149 let Some((handle, power)) = step.rsplit_once('^') else {
3150 ogeom_bail!(Construction, "`{step}` is not a placement step");
3151 };
3152 let (index, generation) = parse_key(handle)?;
3153 let Ok(power) = power.parse::<i32>() else {
3154 ogeom_bail!(Construction, "`{power}` is not a power");
3155 };
3156 let datum: DatumId = Key::from_parts(index, generation);
3157 location = location.then(&Location::powered(datum, power));
3158 }
3159 Ok(location)
3160}
3161
3162trait FromParsedKey: Sized {
3164 fn from_parts_of(parts: (u32, u32)) -> Self;
3165}
3166
3167impl<T> FromParsedKey for Key<T> {
3168 fn from_parts_of((index, generation): (u32, u32)) -> Self {
3169 Self::from_parts(index, generation)
3170 }
3171}
3172
3173#[cfg(test)]
3174#[allow(clippy::unwrap_used, clippy::expect_used)]
3175mod tests {
3176 use super::*;
3177 use approx::assert_relative_eq;
3178 use ogeom_algo::{
3179 check, check_tessellation, make_box, make_cone, make_cylinder, make_prism, make_revolution,
3180 make_sphere, make_torus, make_wedge,
3181 };
3182 use ogeom_math::{Frame, Vector};
3183 use ogeom_mesh::{Deflection, triangulate};
3184 use ogeom_topo::explore_unique;
3185
3186 const T: Tolerances = Tolerances::millimetres();
3187
3188 fn fine() -> Deflection {
3189 Deflection {
3190 chord: 0.02,
3191 ..Deflection::default()
3192 }
3193 }
3194
3195 #[test]
3198 fn a_document_round_trips_with_everything_it_says() {
3199 use ogeom_doc::{Colour, Dimension, GeometricTolerance, MeasureKind};
3200 let mut document = ogeom_doc::Document::new();
3201 let plate = make_box(document.model_mut(), Frame::WORLD, (40.0, 40.0, 5.0), T)
3202 .unwrap()
3203 .shape;
3204 let bolt = make_box(document.model_mut(), Frame::WORLD, (8.0, 8.0, 20.0), T)
3205 .unwrap()
3206 .shape;
3207 let plate_id = document.add_part("plate", plate.clone());
3208 let bolt_id = document.add_part("bolt", bolt.clone());
3209 let assembly = document.add_assembly("bolted plate");
3210 document
3211 .add_instance(assembly, plate_id, Transform::IDENTITY, None)
3212 .unwrap();
3213 document
3214 .add_instance(
3215 assembly,
3216 bolt_id,
3217 Transform::translation(ogeom_math::Vector::new(10.0, 10.0, 5.0)),
3218 Some("bolt one".into()),
3219 )
3220 .unwrap();
3221 document
3222 .set_product_colour(plate_id, Colour::rgb(0.1, 0.8, 0.2))
3223 .unwrap();
3224 let face = explore_unique(document.model(), &bolt, ShapeType::Face).unwrap()[0].clone();
3225 document.set_colour(&face, Colour::rgb(0.9, 0.1, 0.1));
3226 document.set_name(&face, "the mounting face");
3227 document.pmi_mut().dimensions.push(Dimension {
3228 name: "length".into(),
3229 values: vec![20.0],
3230 kind: MeasureKind::Length,
3231 plus: Some(0.1),
3232 minus: Some(-0.1),
3233 features: vec![vec![face.node()]],
3234 location: false,
3235 });
3236 document.pmi_mut().tolerances.push(GeometricTolerance {
3237 kind: "flatness".into(),
3238 name: "Flatness.1".into(),
3239 magnitude: 0.05,
3240 modifiers: vec!["maximum_material_requirement".into()],
3241 datums: vec!["A".into(), "A-B".into()],
3242 datum_system: false,
3243 items: vec![face.node()],
3244 });
3245 document.pmi_mut().datums.push(ogeom_doc::Datum {
3246 label: "A".into(),
3247 items: vec![face.node()],
3248 });
3249
3250 document.set_property(
3255 &face,
3256 ogeom_doc::Property {
3257 name: "finish".into(),
3258 value: ogeom_doc::PropertyValue::Text("brushed".into()),
3259 },
3260 );
3261 document.set_property(
3262 &face,
3263 ogeom_doc::Property {
3264 name: "cost".into(),
3265 value: ogeom_doc::PropertyValue::Number(12.5),
3266 },
3267 );
3268 document.set_property(
3269 &bolt,
3270 ogeom_doc::Property {
3271 name: "critical".into(),
3272 value: ogeom_doc::PropertyValue::Flag(true),
3273 },
3274 );
3275 let steel = document.add_material(ogeom_doc::Material {
3276 name: "AISI 304".into(),
3277 density: Some(7900.0),
3278 colour: Some(Colour::rgb(0.7, 0.7, 0.75)),
3279 });
3280 document.assign_material(&bolt, steel);
3281 let outline = document.add_layer("outline");
3282 let hidden = document.add_layer("construction");
3283 document.set_layer_visible(hidden, false);
3284 document.place_on_layer(&face, outline);
3285 document.place_on_layer(&face, hidden);
3286 document.set_validation(
3287 &plate,
3288 ogeom_doc::ValidationProperties {
3289 volume: 8000.0,
3290 area: 2400.0,
3291 centroid: ogeom_math::Point::new(10.0, 10.0, 2.5),
3292 },
3293 );
3294
3295 let text = write_document(&document, WriteOptions::default()).unwrap();
3296 let back = read_document(&text).unwrap();
3297 let again = write_document(&back, WriteOptions::default()).unwrap();
3298 assert_eq!(text, again, "the second write reproduces the first");
3299
3300 let read_face = back
3303 .names()
3304 .find(|(_, n)| *n == "the mounting face")
3305 .map(|(node, _)| ogeom_topo::Shape::of(node))
3306 .unwrap();
3307 let properties = back.properties_of(&read_face);
3308 assert_eq!(properties.len(), 2);
3309 assert!(
3310 properties.iter().any(|p| p.name == "finish"
3311 && p.value == ogeom_doc::PropertyValue::Text("brushed".into()))
3312 );
3313 assert!(
3314 properties
3315 .iter()
3316 .any(|p| p.name == "cost" && p.value == ogeom_doc::PropertyValue::Number(12.5))
3317 );
3318 assert_eq!(back.materials().len(), 1);
3319 assert_eq!(back.materials()[0].name, "AISI 304");
3320 assert_eq!(back.materials()[0].density, Some(7900.0));
3321 assert_eq!(back.layers().len(), 2);
3322 assert!(back.layers()[0].visible);
3323 assert!(!back.layers()[1].visible);
3324 assert_eq!(back.layers_of(&read_face).len(), 2);
3325 let validation = back
3326 .validations()
3327 .next()
3328 .map(|(_, v)| v)
3329 .expect("the plate's check values survive");
3330 assert!(validation.agrees_with(
3331 &ogeom_doc::ValidationProperties {
3332 volume: 8000.0,
3333 area: 2400.0,
3334 centroid: ogeom_math::Point::new(10.0, 10.0, 2.5),
3335 },
3336 1e-9,
3337 ));
3338
3339 let names: Vec<&str> = back.products().map(|(_, p)| p.name.as_str()).collect();
3341 assert_eq!(names, ["plate", "bolt", "bolted plate"]);
3342 let root = back.roots()[0];
3343 let mut occurrences = back.occurrences_of(root).unwrap();
3344 occurrences.sort_by(|a, b| a.path.cmp(&b.path));
3345 assert_eq!(occurrences.len(), 2);
3346 assert_eq!(occurrences[0].path, "bolted plate/bolt one");
3347 let world = occurrences[0]
3348 .shape
3349 .transform(back.model().datums())
3350 .unwrap()
3351 .apply(Point::new(0.0, 0.0, 0.0));
3352 assert!(world.is_equal(Point::new(10.0, 10.0, 5.0), T));
3353
3354 let bolt_face = explore_unique(back.model(), &occurrences[0].shape, ShapeType::Face)
3356 .unwrap()[0]
3357 .clone();
3358 assert_eq!(back.colour_of(&bolt_face), Some(Colour::rgb(0.9, 0.1, 0.1)));
3359 assert_eq!(back.name_of(&bolt_face), Some("the mounting face"));
3360 assert_eq!(back.pmi().dimensions.len(), 1);
3361 assert_eq!(back.pmi().dimensions[0].values, [20.0]);
3362 assert_eq!(back.pmi().tolerances[0].kind, "flatness");
3363 assert_eq!(
3364 back.pmi().tolerances[0].modifiers,
3365 ["maximum_material_requirement"]
3366 );
3367 assert_eq!(back.pmi().tolerances[0].datums, ["A", "A-B"]);
3368 assert_eq!(back.pmi().datums[0].label, "A");
3369 assert_eq!(back.pmi().datums[0].items.len(), 1);
3370 }
3371
3372 #[test]
3373 fn asymmetric_conic_domains_round_trip() {
3374 let mut model = Model::new();
3377 let hyperbola = ogeom_geom::HyperbolaCurve::over(
3378 Hyperbola::new(Frame::WORLD, 2.0, 1.0, T).unwrap(),
3379 -0.5,
3380 1.75,
3381 )
3382 .unwrap();
3383 let parabola = ogeom_geom::ParabolaCurve::over(
3384 Parabola::new(Frame::WORLD, 1.5, T).unwrap(),
3385 0.25,
3386 3.0,
3387 )
3388 .unwrap();
3389 for curve in [Curve::Hyperbola(hyperbola), Curve::Parabola(parabola)] {
3390 let domain = Curve3d::domain(&curve);
3391 ogeom_algo::make_edge(&mut model, curve, domain, T).unwrap();
3392 }
3393 let text = write(&model, &[], WriteOptions::default()).unwrap();
3394 let (back, _) = read(&text).unwrap();
3395 let again = write(&back, &[], WriteOptions::default()).unwrap();
3396 assert_eq!(text, again, "the second write reproduces the first");
3397 let domains: Vec<(f64, f64)> = back
3398 .geometry()
3399 .curves()
3400 .map(|(_, c)| Curve3d::domain(c))
3401 .collect();
3402 assert!(domains.contains(&(-0.5, 1.75)));
3403 assert!(domains.contains(&(0.25, 3.0)));
3404 }
3405
3406 #[test]
3407 fn derived_geometry_round_trips_byte_stable() {
3408 use ogeom_geom::{
3409 CurveOnSurface, CylinderSurface, OffsetCurve, OffsetSurface, PlanarCurve,
3410 };
3411 use ogeom_math::Cylinder;
3412
3413 let mut model = Model::new();
3414 let circle: Curve =
3417 ogeom_geom::CircleCurve::new(ogeom_math::Circle::new(Frame::WORLD, 2.0, T).unwrap())
3418 .into();
3419 let offset = Curve::Offset(Box::new(
3420 OffsetCurve::new(circle, 1.0, ogeom_math::Direction::Z).unwrap(),
3421 ));
3422 let domain = Curve3d::domain(&offset);
3423 ogeom_algo::make_edge(&mut model, offset, domain, T).unwrap();
3424
3425 let cylinder = SurfaceGeometry::Cylinder(
3427 CylinderSurface::new(Cylinder::new(Frame::WORLD, 3.0, T).unwrap(), (0.0, 8.0)).unwrap(),
3428 );
3429 let chart_line = PlanarCurve::Line(
3430 ogeom_geom::Line2d::segment(
3431 ogeom_math::Point2::new(0.0, 0.0),
3432 ogeom_math::Point2::new(3.0, 5.0),
3433 T,
3434 )
3435 .unwrap(),
3436 );
3437 let lifted = Curve::OnSurface(Box::new(CurveOnSurface::new(chart_line, cylinder.clone())));
3438 let domain = Curve3d::domain(&lifted);
3439 ogeom_algo::make_edge(&mut model, lifted, domain, T).unwrap();
3440
3441 model
3443 .geometry_mut()
3444 .add_surface(SurfaceGeometry::Offset(Box::new(
3445 OffsetSurface::new(cylinder, -0.5).unwrap(),
3446 )));
3447
3448 let text = write(&model, &[], WriteOptions::default()).unwrap();
3449 let (back, _) = read(&text).unwrap();
3450 let again = write(&back, &[], WriteOptions::default()).unwrap();
3451 assert_eq!(text, again, "the second write reproduces the first");
3452 assert!(
3453 back.geometry()
3454 .curves()
3455 .any(|(_, c)| matches!(c, Curve::Offset(_)))
3456 && back
3457 .geometry()
3458 .curves()
3459 .any(|(_, c)| matches!(c, Curve::OnSurface(_)))
3460 );
3461 }
3462
3463 #[test]
3464 fn a_version_one_document_still_reads() {
3465 let mut model = Model::new();
3468 ogeom_algo::make_box(&mut model, Frame::WORLD, (2.0, 2.0, 2.0), T).unwrap();
3469 let text = write(&model, &[], WriteOptions::default()).unwrap();
3470 let downgraded = text.replacen("ogeom 2", "ogeom 1", 1);
3471 assert_ne!(text, downgraded, "the header really carried version 2");
3472 let (back, _) = read(&downgraded).unwrap();
3473 assert_eq!(
3474 back.geometry().surfaces().count(),
3475 model.geometry().surfaces().count()
3476 );
3477 }
3478
3479 #[test]
3480 fn a_conical_helix_and_a_periodic_bspline_round_trip_byte_stable() {
3481 let mut model = Model::new();
3482 let helix = ogeom_geom::HelixCurve::conical(
3483 Frame::new(
3484 ogeom_math::Point::new(1.0, 2.0, -0.5),
3485 ogeom_math::Direction::from_coords(0.0, 0.6, 0.8, T).unwrap(),
3486 ogeom_math::Direction::X,
3487 T,
3488 )
3489 .unwrap(),
3490 4.0,
3491 3.0,
3492 1.25,
3493 0.5,
3494 9.0,
3495 )
3496 .unwrap();
3497 let domain = Curve3d::domain(&helix);
3498 ogeom_algo::make_edge(&mut model, Curve::Helix(helix), domain, T).unwrap();
3499
3500 let ring: Vec<ogeom_math::Point> = (0..8)
3501 .map(|i| {
3502 let a = core::f64::consts::TAU * f64::from(i) / 8.0;
3503 ogeom_math::Point::new(a.cos() * 4.0, a.sin() * 4.0, 0.0)
3504 })
3505 .collect();
3506 let loop_curve = ogeom_geom::BSplineCurve::periodic(&ring, 3, T).unwrap();
3507 let domain = Curve3d::domain(&loop_curve);
3508 ogeom_algo::make_edge(&mut model, Curve::BSpline(loop_curve), domain, T).unwrap();
3509
3510 let text = write(&model, &[], WriteOptions::default()).unwrap();
3511 let (back, _) = read(&text).unwrap();
3512 let again = write(&back, &[], WriteOptions::default()).unwrap();
3513 assert_eq!(text, again, "the second write reproduces the first");
3514 assert!(
3515 back.geometry()
3516 .curves()
3517 .any(|(_, c)| matches!(c, Curve::Helix(h) if (h.taper() - 1.25).abs() < 1e-12))
3518 );
3519 assert!(
3520 back.geometry()
3521 .curves()
3522 .any(|(_, c)| matches!(c, Curve::BSpline(b) if b.is_periodic()))
3523 );
3524 }
3525
3526 #[test]
3530 fn an_extrusion_over_a_window_below_zero_round_trips() {
3531 let mut model = Model::new();
3532 let line =
3533 Curve::Line(LineCurve::over(Axis::new(Point::ORIGIN, Direction::X), 0.0, 5.0).unwrap());
3534 let extrusion = ExtrusionSurface::over(line, Direction::Z, (-1e9, 1e9)).unwrap();
3535 model.geometry_mut().add_surface(extrusion.into());
3536 let text = write(&model, &[], WriteOptions::default()).unwrap();
3537 let (back, _) = read(&text).unwrap();
3538 assert_eq!(write(&back, &[], WriteOptions::default()).unwrap(), text);
3539 let window = back
3540 .geometry()
3541 .surfaces()
3542 .find_map(|(_, s)| match s {
3543 SurfaceGeometry::Extrusion(e) => Some(e.domain().1),
3544 _ => None,
3545 })
3546 .expect("the extrusion survives");
3547 assert_eq!(window, (-1e9, 1e9));
3548 }
3549
3550 #[test]
3552 fn an_extrusion_over_an_empty_window_is_refused() {
3553 let mut model = Model::new();
3554 let line =
3555 Curve::Line(LineCurve::over(Axis::new(Point::ORIGIN, Direction::X), 0.0, 5.0).unwrap());
3556 let extrusion = ExtrusionSurface::new(line, Direction::Z, 3.0).unwrap();
3557 model.geometry_mut().add_surface(extrusion.into());
3558 let text = write(&model, &[], WriteOptions::default()).unwrap();
3559 assert!(read(&text).is_ok());
3560 let window = "1.0 0.0 3.0 line";
3561 assert!(text.contains(window), "{text}");
3562 for bad in ["1.0 3.0 3.0 line", "1.0 3.0 0.0 line", "1.0 0.0 inf line"] {
3563 assert!(
3564 read(&text.replacen(window, bad, 1)).is_err(),
3565 "`{bad}` read"
3566 );
3567 }
3568 }
3569
3570 #[test]
3571 fn a_helix_edge_round_trips_byte_stable() {
3572 let mut model = Model::new();
3573 let helix = ogeom_geom::HelixCurve::over(
3574 Frame::new(
3575 ogeom_math::Point::new(1.0, 2.0, -0.5),
3576 ogeom_math::Direction::from_coords(0.0, 0.6, 0.8, T).unwrap(),
3577 ogeom_math::Direction::X,
3578 T,
3579 )
3580 .unwrap(),
3581 4.0,
3582 -1.5,
3583 0.5,
3584 9.0,
3585 )
3586 .unwrap();
3587 let domain = Curve3d::domain(&helix);
3588 ogeom_algo::make_edge(&mut model, Curve::Helix(helix), domain, T).unwrap();
3589 let text = write(&model, &[], WriteOptions::default()).unwrap();
3590 let (back, _) = read(&text).unwrap();
3591 let again = write(&back, &[], WriteOptions::default()).unwrap();
3592 assert_eq!(text, again, "the second write reproduces the first");
3593 let restored = back
3594 .geometry()
3595 .curves()
3596 .find_map(|(_, c)| match c {
3597 Curve::Helix(h) => Some(*h),
3598 _ => None,
3599 })
3600 .expect("the helix survives");
3601 assert_eq!(restored.radius(), 4.0);
3602 assert_eq!(restored.pitch(), -1.5);
3603 assert_eq!(Curve3d::domain(&restored), (0.5, 9.0));
3604 }
3605
3606 fn everything() -> (Model, Vec<Shape>) {
3607 let mut model = Model::new();
3608 let mut roots = vec![
3609 make_box(&mut model, Frame::WORLD, (2.0, 3.0, 4.0), T)
3610 .unwrap()
3611 .shape,
3612 make_cylinder(&mut model, Frame::WORLD, 2.0, 5.0, T)
3613 .unwrap()
3614 .shape,
3615 make_sphere(&mut model, Frame::WORLD, 3.0, T).unwrap().shape,
3616 make_cone(&mut model, Frame::WORLD, 3.0, 1.0, 4.0, T)
3617 .unwrap()
3618 .shape,
3619 make_cone(&mut model, Frame::WORLD, 3.0, 0.0, 4.0, T)
3620 .unwrap()
3621 .shape,
3622 make_torus(&mut model, Frame::WORLD, 5.0, 2.0, T)
3623 .unwrap()
3624 .shape,
3625 make_wedge(&mut model, Frame::WORLD, (4.0, 4.0, 6.0), (2.0, 2.0), T)
3626 .unwrap()
3627 .shape,
3628 ];
3629
3630 let face = explore_unique(&model, &roots[0], ShapeType::Face).unwrap()[0].clone();
3632 roots.push(
3633 make_prism(&mut model, &face, Vector::new(0.0, 0.0, 2.0), T)
3634 .unwrap()
3635 .shape,
3636 );
3637 let side = explore_unique(&model, &roots[0], ShapeType::Face)
3642 .unwrap()
3643 .into_iter()
3644 .find(|f| {
3645 model
3646 .provenance_of(f)
3647 .and_then(ogeom_core::Provenance::role)
3648 == Some(ogeom_algo::primitive::roles::FACE_MIN_Y)
3649 })
3650 .expect("the box has a -y face");
3651 let axis = ogeom_math::Axis::new(
3652 ogeom_math::Point::new(-5.0, 0.0, 0.0),
3653 ogeom_math::Direction::Z,
3654 );
3655 roots.push(
3656 make_revolution(&mut model, &side, axis, 1.0, T)
3657 .unwrap()
3658 .shape,
3659 );
3660 (model, roots)
3661 }
3662
3663 #[test]
3664 fn a_file_that_is_not_one_is_refused() {
3665 assert!(read("").is_err());
3666 assert!(read("something else 1").is_err());
3667 assert!(read(&format!("{MAGIC} 99")).is_err());
3668 assert!(read(&format!("{MAGIC} 1\nnonsense\n")).is_err());
3669 }
3670
3671 #[test]
3672 fn writing_what_was_read_gives_the_same_bytes() {
3673 let (model, roots) = everything();
3677 let first = write(&model, &roots, WriteOptions::default()).unwrap();
3678 let (restored, restored_roots) = read(&first).unwrap();
3679 let second = write(&restored, &restored_roots, WriteOptions::default()).unwrap();
3680
3681 if first != second {
3682 let mismatch = first
3683 .lines()
3684 .zip(second.lines())
3685 .enumerate()
3686 .find(|(_, (a, b))| a != b);
3687 panic!("the round trip changed the document: {mismatch:?}");
3688 }
3689 assert_eq!(restored_roots.len(), roots.len());
3690 }
3691
3692 #[test]
3693 fn the_restored_model_is_the_same_model() {
3694 let (model, roots) = everything();
3698 let text = write(&model, &roots, WriteOptions::default()).unwrap();
3699 let (restored, restored_roots) = read(&text).unwrap();
3700
3701 assert_eq!(restored.node_count(), model.node_count());
3702 assert_eq!(restored.geometry().counts(), model.geometry().counts());
3703 assert_eq!(restored.current_operation(), model.current_operation());
3704
3705 for (before, after) in roots.iter().zip(&restored_roots) {
3706 assert_eq!(before.node().index(), after.node().index());
3711 assert_eq!(before.node().generation(), after.node().generation());
3712 assert_ne!(
3713 before.node().scope(),
3714 after.node().scope(),
3715 "a restored document should not answer to the original's handles"
3716 );
3717 assert!(model.node(after).is_none(), "and not the other way round");
3718 assert!(
3724 restored.bind(before).is_err(),
3725 "a foreign handle should not be re-homed"
3726 );
3727
3728 for kind in [
3729 ShapeType::Face,
3730 ShapeType::Edge,
3731 ShapeType::Vertex,
3732 ShapeType::Shell,
3733 ] {
3734 assert_eq!(
3735 explore_unique(&restored, after, kind).unwrap().len(),
3736 explore_unique(&model, before, kind).unwrap().len(),
3737 "{kind:?} count changed"
3738 );
3739 }
3740
3741 assert!(
3742 check(&restored, after, T).unwrap().is_valid(),
3743 "restored shape is invalid: {}",
3744 check(&restored, after, T).unwrap()
3745 );
3746 assert!(
3747 check_tessellation(&restored, after, fine(), T)
3748 .unwrap()
3749 .is_valid(),
3750 "restored shape's mesh came apart"
3751 );
3752
3753 let before_mesh = triangulate(&model, before, fine(), T).unwrap();
3754 let after_mesh = triangulate(&restored, after, fine(), T).unwrap();
3755 assert_eq!(after_mesh.triangle_count(), before_mesh.triangle_count());
3756 assert_relative_eq!(
3757 after_mesh.volume(),
3758 before_mesh.volume(),
3759 max_relative = 0.0
3760 );
3761 }
3762 }
3763
3764 #[test]
3765 fn provenance_and_identity_survive() {
3766 let (model, roots) = everything();
3771 let text = write(&model, &roots, WriteOptions::default()).unwrap();
3772 let (restored, restored_roots) = read(&text).unwrap();
3773
3774 for (before, after) in roots.iter().zip(&restored_roots) {
3775 let faces = explore_unique(&model, before, ShapeType::Face).unwrap();
3776 let restored_faces = explore_unique(&restored, after, ShapeType::Face).unwrap();
3777 assert_eq!(faces.len(), restored_faces.len());
3778 for (a, b) in faces.iter().zip(&restored_faces) {
3779 assert_eq!(
3780 restored.identity_of(b),
3781 model.identity_of(a),
3782 "an identity was renumbered"
3783 );
3784 if let Some(id) = model.identity_of(a) {
3788 let found = restored.shape_of(id).expect("the entity is still there");
3789 assert!(found.is_partner(b), "identity found the wrong node");
3790 }
3791 assert_eq!(
3792 restored.provenance_of(b),
3793 model.provenance_of(a),
3794 "a provenance record changed"
3795 );
3796 assert_eq!(restored.roots_of(b), model.roots_of(a));
3797 }
3798 }
3799 assert_eq!(
3800 restored.provenance().len(),
3801 model.provenance().len(),
3802 "the table changed length"
3803 );
3804 }
3805
3806 #[test]
3807 fn everything_reads_into_a_live_model_and_still_answers_the_same() {
3808 let (model, roots) = everything();
3811 let text = write(&model, &roots, WriteOptions::default()).unwrap();
3812
3813 let mut target = Model::new();
3814 let resident = make_box(&mut target, Frame::WORLD, (7.0, 7.0, 7.0), T)
3815 .unwrap()
3816 .shape;
3817 let absorbed = read_into(&mut target, &text).unwrap();
3818 assert_eq!(absorbed.shapes.len(), roots.len());
3819
3820 for (before, after) in roots.iter().zip(&absorbed.shapes) {
3821 for kind in [
3822 ShapeType::Face,
3823 ShapeType::Edge,
3824 ShapeType::Vertex,
3825 ShapeType::Shell,
3826 ] {
3827 assert_eq!(
3828 explore_unique(&target, after, kind).unwrap().len(),
3829 explore_unique(&model, before, kind).unwrap().len(),
3830 "{kind:?} count changed"
3831 );
3832 }
3833 assert!(
3834 check(&target, after, T).unwrap().is_valid(),
3835 "absorbed shape is invalid: {}",
3836 check(&target, after, T).unwrap()
3837 );
3838 let before_mesh = triangulate(&model, before, fine(), T).unwrap();
3839 let after_mesh = triangulate(&target, after, fine(), T).unwrap();
3840 assert_eq!(after_mesh.triangle_count(), before_mesh.triangle_count());
3841 assert_relative_eq!(
3842 after_mesh.volume(),
3843 before_mesh.volume(),
3844 max_relative = 0.0
3845 );
3846 }
3847
3848 assert!(check(&target, &resident, T).unwrap().is_valid());
3850 assert_relative_eq!(
3851 triangulate(&target, &resident, fine(), T).unwrap().volume(),
3852 343.0,
3853 max_relative = 1e-9
3854 );
3855 }
3856
3857 #[test]
3858 fn identities_read_into_a_live_model_survive_under_an_offset() {
3859 let (model, roots) = everything();
3864 let text = write(&model, &roots, WriteOptions::default()).unwrap();
3865
3866 let mut target = Model::new();
3867 make_box(&mut target, Frame::WORLD, (7.0, 7.0, 7.0), T).unwrap();
3868 let issued_before = target.provenance().len() as u64;
3869 assert!(issued_before > 0);
3870 let absorbed = read_into(&mut target, &text).unwrap();
3871
3872 for (before, after) in roots.iter().zip(&absorbed.shapes) {
3873 let faces = explore_unique(&model, before, ShapeType::Face).unwrap();
3874 let absorbed_faces = explore_unique(&target, after, ShapeType::Face).unwrap();
3875 assert_eq!(faces.len(), absorbed_faces.len());
3876 for (a, b) in faces.iter().zip(&absorbed_faces) {
3877 let old = model.identity_of(a).expect("faces carry identities");
3878 let new = absorbed.entities[&old];
3879 assert_eq!(new.get(), old.get() + issued_before, "not a plain shift");
3880 assert_eq!(target.identity_of(b), Some(new));
3881 let found = target.shape_of(new).expect("the entity is findable");
3882 assert!(found.is_partner(b), "identity found the wrong node");
3883 assert_eq!(
3884 target.provenance_of(b).and_then(Provenance::role),
3885 model.provenance_of(a).and_then(Provenance::role),
3886 "a role changed in the shift"
3887 );
3888 }
3889 }
3890 assert_eq!(
3891 target.provenance().len() as u64,
3892 issued_before + model.provenance().len() as u64
3893 );
3894 }
3895
3896 #[test]
3897 fn a_document_in_other_units_does_not_read_into_a_millimetre_model() {
3898 let mut metric = Model::with_tolerances(Tolerances::metres());
3899 let solid = make_box(
3900 &mut metric,
3901 Frame::WORLD,
3902 (1.0, 1.0, 1.0),
3903 Tolerances::metres(),
3904 )
3905 .unwrap()
3906 .shape;
3907 let text = write(
3908 &metric,
3909 std::slice::from_ref(&solid),
3910 WriteOptions::default(),
3911 )
3912 .unwrap();
3913
3914 let mut target = Model::new();
3915 make_box(&mut target, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
3916 let nodes_before = target.node_count();
3917 assert!(
3918 read_into(&mut target, &text).is_err(),
3919 "a metre document should not land in a millimetre model"
3920 );
3921 assert_eq!(
3922 target.node_count(),
3923 nodes_before,
3924 "a refused read should leave the model alone"
3925 );
3926 }
3927
3928 #[test]
3929 fn reading_the_same_document_into_a_model_twice_gives_independent_copies() {
3930 let mut model = Model::new();
3931 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
3932 .unwrap()
3933 .shape;
3934 let text = write(
3935 &model,
3936 std::slice::from_ref(&solid),
3937 WriteOptions::default(),
3938 )
3939 .unwrap();
3940
3941 let mut target = Model::new();
3942 let first = read_into(&mut target, &text).unwrap();
3943 let second = read_into(&mut target, &text).unwrap();
3944
3945 let a = &first.shapes[0];
3946 let b = &second.shapes[0];
3947 assert_ne!(
3948 a.node().index(),
3949 b.node().index(),
3950 "the copies share a node"
3951 );
3952 let face_a = explore_unique(&target, a, ShapeType::Face).unwrap()[0].clone();
3955 let face_b = explore_unique(&target, b, ShapeType::Face).unwrap()[0].clone();
3956 assert_ne!(
3957 target.identity_of(&face_a),
3958 target.identity_of(&face_b),
3959 "the copies share an identity"
3960 );
3961 assert_relative_eq!(
3962 triangulate(&target, a, fine(), T).unwrap().volume(),
3963 triangulate(&target, b, fine(), T).unwrap().volume(),
3964 max_relative = 0.0
3965 );
3966 }
3967
3968 #[test]
3969 fn read_into_reads_the_same_version_read_does() {
3970 let future = format!("{MAGIC} 99\nunits 1.0\n");
3973 assert!(read(&future).is_err());
3974 let mut target = Model::new();
3975 assert!(read_into(&mut target, &future).is_err());
3976 }
3977
3978 #[test]
3979 fn a_document_with_meshes_reads_into_a_live_model() {
3980 let mut model = Model::new();
3981 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
3982 .unwrap()
3983 .shape;
3984 ogeom_mesh::tessellate(&mut model, &solid, fine(), T).unwrap();
3985 assert!(model.geometry().triangulation_count() > 0);
3986 let text = write(
3987 &model,
3988 std::slice::from_ref(&solid),
3989 WriteOptions::default(),
3990 )
3991 .unwrap();
3992
3993 let mut target = Model::new();
3994 make_box(&mut target, Frame::WORLD, (7.0, 7.0, 7.0), T).unwrap();
3995 let absorbed = read_into(&mut target, &text).unwrap();
3996 assert_eq!(
3997 target.geometry().triangulation_count(),
3998 model.geometry().triangulation_count()
3999 );
4000 for face in explore_unique(&target, &absorbed.shapes[0], ShapeType::Face).unwrap() {
4002 let node = target.node(&face).unwrap();
4003 let mesh = node
4004 .data()
4005 .as_face()
4006 .unwrap()
4007 .triangulation
4008 .expect("the cache travelled");
4009 assert!(target.geometry().triangulation(mesh).is_some());
4010 }
4011 }
4012
4013 #[test]
4014 fn tolerances_survive_entity_by_entity() {
4015 let mut model = Model::new();
4019 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
4020 .unwrap()
4021 .shape;
4022 let edge = explore_unique(&model, &solid, ShapeType::Edge).unwrap()[0].clone();
4023 model.widen(&edge, Tolerance::new(1e-3).unwrap()).unwrap();
4024
4025 let text = write(
4026 &model,
4027 std::slice::from_ref(&solid),
4028 WriteOptions::default(),
4029 )
4030 .unwrap();
4031 let (restored, roots) = read(&text).unwrap();
4032 for kind in [ShapeType::Vertex, ShapeType::Edge, ShapeType::Face] {
4033 let before = explore_unique(&model, &solid, kind).unwrap();
4034 let after = explore_unique(&restored, &roots[0], kind).unwrap();
4035 for (a, b) in before.iter().zip(&after) {
4036 assert_eq!(
4037 restored.tolerance_of(b).unwrap().map(Tolerance::get),
4038 model.tolerance_of(a).unwrap().map(Tolerance::get),
4039 "a {kind:?} tolerance changed"
4040 );
4041 }
4042 }
4043 assert!(
4044 restored
4045 .tolerance_of(&roots[0])
4046 .into_iter()
4047 .flatten()
4048 .count()
4049 <= 1
4050 );
4051 }
4052
4053 #[test]
4054 fn a_cached_mesh_survives_and_omitting_it_says_so() {
4055 let mut model = Model::new();
4056 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), T)
4057 .unwrap()
4058 .shape;
4059 ogeom_mesh::tessellate(&mut model, &solid, fine(), T).unwrap();
4060 assert!(model.geometry().triangulation_count() > 0);
4061
4062 let text = write(
4063 &model,
4064 std::slice::from_ref(&solid),
4065 WriteOptions::default(),
4066 )
4067 .unwrap();
4068 let (restored, _) = read(&text).unwrap();
4069 assert_eq!(
4070 restored.geometry().triangulation_count(),
4071 model.geometry().triangulation_count()
4072 );
4073 for (before, after) in model
4074 .geometry()
4075 .triangulations()
4076 .zip(restored.geometry().triangulations())
4077 {
4078 assert_eq!(before.1, after.1, "a cached mesh changed");
4079 }
4080
4081 let without = write(
4084 &model,
4085 std::slice::from_ref(&solid),
4086 WriteOptions {
4087 triangulations: false,
4088 },
4089 )
4090 .unwrap();
4091 assert!(without.contains("triangulation(s) omitted by request"));
4092 assert!(
4093 without.len() < text.len(),
4094 "omitting the mesh saved nothing"
4095 );
4096 let (bare, bare_roots) = read(&without).unwrap();
4097 assert_eq!(bare.geometry().triangulation_count(), 0);
4098 assert!(check(&bare, &bare_roots[0], T).unwrap().is_valid());
4100 }
4101
4102 #[test]
4103 fn a_document_naming_a_handle_that_is_not_there_is_refused() {
4104 let mut model = Model::new();
4107 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
4108 .unwrap()
4109 .shape;
4110 let text = write(
4111 &model,
4112 std::slice::from_ref(&solid),
4113 WriteOptions::default(),
4114 )
4115 .unwrap();
4116
4117 let broken = text.replace("curve3d 0:0", "curve3d 999:0");
4118 assert_ne!(broken, text, "the substitution found nothing to do");
4119 assert!(read(&broken).is_err(), "a dangling curve was accepted");
4120
4121 let broken = format!("{text}identity 0:0 99999\n");
4123 assert!(read(&broken).is_err(), "a dangling identity was accepted");
4124
4125 let broken = format!("{text}identity 9999:0 1\n");
4128 assert!(
4129 read(&broken).is_err(),
4130 "a dangling node binding was accepted"
4131 );
4132 }
4133
4134 #[test]
4135 fn records_out_of_arena_order_are_refused() {
4136 let mut model = Model::new();
4140 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
4141 .unwrap()
4142 .shape;
4143 let text = write(
4144 &model,
4145 std::slice::from_ref(&solid),
4146 WriteOptions::default(),
4147 )
4148 .unwrap();
4149 let shuffled: String = {
4150 let mut lines: Vec<&str> = text.lines().collect();
4151 let first = lines
4152 .iter()
4153 .position(|l| l.starts_with("curve 0:0"))
4154 .expect("a curve");
4155 lines.swap(first, first + 1);
4156 lines.join("\n")
4157 };
4158 assert!(
4159 read(&shuffled).is_err(),
4160 "an out-of-order curve was accepted"
4161 );
4162 }
4163
4164 #[test]
4165 fn comments_and_blank_lines_are_ignored() {
4166 let mut model = Model::new();
4167 let solid = make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T)
4168 .unwrap()
4169 .shape;
4170 let text = write(
4171 &model,
4172 std::slice::from_ref(&solid),
4173 WriteOptions::default(),
4174 )
4175 .unwrap();
4176 let annotated = format!("# a note\n\n{text}\n\n# and another\n");
4177 let (restored, roots) = read(&annotated).unwrap();
4178 assert_eq!(restored.node_count(), model.node_count());
4179 assert_eq!(roots.len(), 1);
4180 }
4181
4182 #[test]
4183 fn a_root_that_is_not_in_the_model_is_refused() {
4184 let mut model = Model::new();
4185 make_box(&mut model, Frame::WORLD, (1.0, 1.0, 1.0), T).unwrap();
4186 let beyond = Shape::of(Key::from_parts(9999, 0));
4187 assert!(
4188 write(
4189 &model,
4190 std::slice::from_ref(&beyond),
4191 WriteOptions::default()
4192 )
4193 .is_err()
4194 );
4195
4196 let mut elsewhere = Model::new();
4201 let foreign = ogeom_algo::make_sphere(&mut elsewhere, Frame::WORLD, 1.0, T)
4202 .unwrap()
4203 .shape;
4204 assert!(
4205 write(
4206 &model,
4207 std::slice::from_ref(&foreign),
4208 WriteOptions::default()
4209 )
4210 .is_err()
4211 );
4212 }
4213
4214 #[test]
4215 fn every_float_comes_back_bit_for_bit() {
4216 let mut model = Model::new();
4220 let awkward = [
4221 0.1_f64,
4222 -0.0,
4223 1e-300,
4224 1.0 / 3.0,
4225 f64::MAX / 4.0,
4226 f64::MIN_POSITIVE,
4227 std::f64::consts::PI,
4228 ];
4229 let mut points = Vec::new();
4230 for value in awkward {
4231 points.push(model.add_point(ogeom_math::Point::new(value, -value, value * 2.0)));
4232 }
4233 let text = write(&model, &points, WriteOptions::default()).unwrap();
4234 let (restored, roots) = read(&text).unwrap();
4235 for (before, after) in points.iter().zip(&roots) {
4236 let a = model
4237 .node(before)
4238 .unwrap()
4239 .data()
4240 .as_vertex()
4241 .unwrap()
4242 .point;
4243 let b = restored
4244 .node(after)
4245 .unwrap()
4246 .data()
4247 .as_vertex()
4248 .unwrap()
4249 .point;
4250 assert_eq!(a.x.to_bits(), b.x.to_bits());
4251 assert_eq!(a.y.to_bits(), b.y.to_bits());
4252 assert_eq!(a.z.to_bits(), b.z.to_bits());
4253 }
4254 }
4255}
4256
4257#[cfg(test)]
4258#[allow(clippy::unwrap_used, clippy::expect_used)]
4259mod unit_tests {
4260 use super::*;
4261 use ogeom_algo::make_box;
4262 use ogeom_math::Frame;
4263
4264 #[test]
4265 fn a_documents_unit_scale_survives_the_round_trip() {
4266 for tolerances in [
4270 Tolerances::millimetres(),
4271 Tolerances::metres(),
4272 Tolerances::inches(),
4273 ] {
4274 let mut model = Model::with_tolerances(tolerances);
4275 let solid = make_box(&mut model, Frame::WORLD, (1.0, 2.0, 3.0), tolerances)
4276 .unwrap()
4277 .shape;
4278 let text = write(
4279 &model,
4280 std::slice::from_ref(&solid),
4281 WriteOptions::default(),
4282 )
4283 .unwrap();
4284
4285 let (restored, restored_roots) = read(&text).unwrap();
4286 assert_eq!(
4287 restored.tolerances().scale(),
4288 tolerances.scale(),
4289 "the scale changed"
4290 );
4291 assert_eq!(
4292 restored.tolerances().confusion(),
4293 tolerances.confusion(),
4294 "and so did what counts as the same point"
4295 );
4296 let again = write(&restored, &restored_roots, WriteOptions::default()).unwrap();
4300 assert_eq!(text, again);
4301 }
4302 }
4303
4304 #[test]
4305 fn a_document_that_does_not_say_its_units_is_refused() {
4306 let mut model = Model::new();
4307 let solid = make_box(
4308 &mut model,
4309 Frame::WORLD,
4310 (1.0, 1.0, 1.0),
4311 Tolerances::millimetres(),
4312 )
4313 .unwrap()
4314 .shape;
4315 let text = write(
4316 &model,
4317 std::slice::from_ref(&solid),
4318 WriteOptions::default(),
4319 )
4320 .unwrap();
4321
4322 let without: String = text
4323 .lines()
4324 .filter(|line| !line.starts_with("units "))
4325 .collect::<Vec<_>>()
4326 .join("\n");
4327 let err = read(&without).unwrap_err();
4328 assert!(
4329 err.to_string().contains("units"),
4330 "unexpected message: {err}"
4331 );
4332
4333 let broken = text.replace("units ", "units -");
4335 assert!(read(&broken).is_err());
4336 }
4337}