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