1use ogeom_core::FastMap;
28use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
29use ogeom_doc::{Document, ProductId, ProductKind};
30use ogeom_geom::Curve2d as _;
31use ogeom_geom::Curve3d as _;
32use ogeom_geom::Surface as _;
33use ogeom_geom::Transformable as _;
34use ogeom_geom::{Curve, PlanarCurve, SurfaceGeometry};
35use ogeom_math::{Frame, Handedness, Point, Point2, Transform, Vector};
36use ogeom_topo::{EdgeRepr, Filter, Model, NodeData, Shape, ShapeType, SurfaceId, explore};
37use std::collections::BTreeMap;
38use std::fmt::Write as _;
39
40pub fn write_step(document: &Document, tol: Tolerances) -> OgeomResult<String> {
63 NON_FINITE.with(|seen| seen.set(false));
64 let mut writer = Writer {
65 model: document.model(),
66 entities: Vec::new(),
67 points: FastMap::default(),
68 directions: FastMap::default(),
69 vertices: FastMap::default(),
70 edges: FastMap::default(),
71 written_nodes: Vec::new(),
72 written_edges: Vec::new(),
73 curve_font: None,
74 surface_curves: BTreeMap::new(),
75 parametric_context: None,
76 tol,
77 };
78
79 let app = writer.entity("APPLICATION_CONTEXT('automotive design')".into());
80 let _proto = writer.entity(format!(
81 "APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2010,#{app})"
82 ));
83 let lu = writer.entity("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))".into());
84 let au = writer.entity("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))".into());
85 let su = writer.entity("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())".into());
86 let unc = writer.entity(format!(
87 "UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.0E-06),#{lu},'distance_accuracy_value','')"
88 ));
89 let gctx = writer.entity(format!(
90 "(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{unc}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#{lu},#{au},#{su}))REPRESENTATION_CONTEXT('Context','3D'))"
91 ));
92 let pctx = writer.entity(format!("PRODUCT_CONTEXT('',#{app},'mechanical')"));
93 let pdctx = writer.entity(format!(
94 "PRODUCT_DEFINITION_CONTEXT('part definition',#{app},'design')"
95 ));
96
97 let mut pd_of: FastMap<ProductId, u64> = FastMap::default();
100 let mut sr_of: FastMap<ProductId, u64> = FastMap::default();
101 let mut anchor_pds: Option<(u64, u64)> = None;
102 let mut owner: FastMap<ogeom_topo::TShapeId, (u64, u64)> = FastMap::default();
103 for (id, product) in document.products() {
104 let name = escape(&product.name);
105 let p = writer.entity(format!("PRODUCT('{name}','{name}','',(#{pctx}))"));
106 let formation = writer.entity(format!("PRODUCT_DEFINITION_FORMATION('','',#{p})"));
107 let pd = writer.entity(format!(
108 "PRODUCT_DEFINITION('design','',#{formation},#{pdctx})"
109 ));
110 pd_of.insert(id, pd);
111
112 let world = writer.frame(&Frame::WORLD);
113 let construction: Vec<&ogeom_doc::Construction> = document.construction_of(id).collect();
114 let written_before = (writer.written_nodes.len(), writer.written_edges.len());
115 let sr = match &product.kind {
116 ProductKind::Part { shape } => {
117 let bodies = if construction.is_empty() {
119 crate::bodies::bodies_of(writer.model, shape, "STEP", false)?
120 } else {
121 crate::bodies::bodies_within(writer.model, shape, "STEP", false)?
122 };
123 let mut solids = vec![world];
124 for solid in &bodies.solids {
125 solids.push(writer.solid(solid)?);
126 }
127 let mut sheets = vec![world];
128 for shell in &bodies.shells {
129 sheets.push(writer.surface_model(shell, false)?);
130 }
131 for face in &bodies.faces {
132 sheets.push(writer.surface_model(face, true)?);
133 }
134 let brep = (solids.len() > 1).then(|| {
138 let list = reference_list(&solids);
139 writer.entity(format!(
140 "ADVANCED_BREP_SHAPE_REPRESENTATION('{name}',({list}),#{gctx})"
141 ))
142 });
143 let surface = (sheets.len() > 1).then(|| {
144 let list = reference_list(&sheets);
145 writer.entity(format!(
146 "MANIFOLD_SURFACE_SHAPE_REPRESENTATION('{name}',({list}),#{gctx})"
147 ))
148 });
149 let geometric = writer.construction(&construction, &name, gctx)?;
150 let sr = match (brep, surface) {
151 (Some(brep), Some(surface)) => {
152 writer.entity(format!(
153 "SHAPE_REPRESENTATION_RELATIONSHIP('','',#{brep},#{surface})"
154 ));
155 brep
156 }
157 (Some(only), None) | (None, Some(only)) => only,
158 (None, None) => match geometric {
159 Some(only) => only,
160 None => ogeom_bail!(
161 Construction,
162 "a part's shape holds no solid, shell or face to write as STEP"
163 ),
164 },
165 };
166 if let Some(geometric) = geometric.filter(|&g| g != sr) {
167 writer.entity(format!(
168 "SHAPE_REPRESENTATION_RELATIONSHIP('','',#{sr},#{geometric})"
169 ));
170 }
171 sr
172 }
173 ProductKind::Assembly { .. } => {
174 let sr =
175 writer.entity(format!("SHAPE_REPRESENTATION('{name}',(#{world}),#{gctx})"));
176 if let Some(geometric) = writer.construction(&construction, &name, gctx)? {
177 writer.entity(format!(
178 "SHAPE_REPRESENTATION_RELATIONSHIP('','',#{sr},#{geometric})"
179 ));
180 }
181 sr
182 }
183 };
184 sr_of.insert(id, sr);
185 let pds = writer.entity(format!("PRODUCT_DEFINITION_SHAPE('','',#{pd})"));
186 writer.entity(format!("SHAPE_DEFINITION_REPRESENTATION(#{pds},#{sr})"));
187 if matches!(product.kind, ProductKind::Part { .. }) {
188 if anchor_pds.is_none() {
189 anchor_pds = Some((pds, sr));
190 }
191 let (nodes, edges) = written_before;
194 for &(node, _) in writer.written_nodes[nodes..]
195 .iter()
196 .chain(&writer.written_edges[edges..])
197 {
198 owner.entry(node).or_insert((pds, sr));
199 }
200 }
201 }
202
203 writer.surface_curves()?;
204
205 let mut usage = 0_usize;
209 for (id, product) in document.products() {
210 let ProductKind::Assembly { children } = &product.kind else {
211 continue;
212 };
213 for instance in children {
214 usage += 1;
215 let designator = instance
216 .name
217 .clone()
218 .unwrap_or_else(|| format!("occurrence-{usage}"));
219 let designator = escape(&designator);
220 let (parent_pd, child_pd) = (pd_of[&id], pd_of[&instance.product]);
221 let (parent_sr, child_sr) = (sr_of[&id], sr_of[&instance.product]);
222 let nauo = writer.entity(format!(
223 "NEXT_ASSEMBLY_USAGE_OCCURRENCE('{designator}','{designator}','',#{parent_pd},#{child_pd},$)"
224 ));
225 let at = location_transform(&instance.location, document.model())?;
229 let placed = writer.placement_frame(&at)?;
230 let world = writer.frame(&Frame::WORLD);
231 let idt = writer.entity(format!(
232 "ITEM_DEFINED_TRANSFORMATION('','',#{world},#{placed})"
233 ));
234 let rr = writer.entity(format!(
235 "(REPRESENTATION_RELATIONSHIP('','',#{child_sr},#{parent_sr})REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#{idt})SHAPE_REPRESENTATION_RELATIONSHIP())"
236 ));
237 let pds = writer.entity(format!("PRODUCT_DEFINITION_SHAPE('','',#{nauo})"));
238 writer.entity(format!(
239 "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#{rr},#{pds})"
240 ));
241 }
242 }
243
244 let mut styled = Vec::new();
248 let node_colours: FastMap<_, _> = document.colours().collect();
249 let written = std::mem::take(&mut writer.written_nodes);
252 let mut by_node: FastMap<ogeom_topo::TShapeId, Vec<u64>> = FastMap::default();
253 for &(node, step_id) in &written {
254 by_node.entry(node).or_default().push(step_id);
255 }
256 for &(node, step_id) in &written {
257 if let Some(colour) = node_colours.get(&node) {
258 styled.push(writer.styled_item(step_id, *colour));
259 }
260 }
261 for (_, product) in document.products() {
262 let Some(colour) = product.colour else {
263 continue;
264 };
265 let ProductKind::Part { shape } = &product.kind else {
266 continue;
267 };
268 if node_colours.contains_key(&shape.node()) {
269 continue;
270 }
271 for &step_id in by_node.get(&shape.node()).map_or(&[][..], Vec::as_slice) {
272 styled.push(writer.styled_item(step_id, colour));
273 }
274 }
275 writer.written_nodes = written;
276 let edges = std::mem::take(&mut writer.written_edges);
277 for &(node, step_id) in &edges {
278 if let Some(colour) = node_colours.get(&node) {
279 styled.push(writer.curve_styled_item(step_id, *colour));
280 }
281 }
282 writer.written_edges = edges;
283 if !styled.is_empty() {
284 let list = styled
285 .iter()
286 .map(|i| format!("#{i}"))
287 .collect::<Vec<_>>()
288 .join(",");
289 writer.entity(format!(
290 "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',({list}),#{gctx})"
291 ));
292 }
293
294 let pmi = document.pmi();
296 if !pmi.is_empty() {
297 let Some((pds, absr)) = anchor_pds else {
298 ogeom_bail!(
299 Construction,
300 "PMI needs at least one part to anchor its aspects to"
301 );
302 };
303 writer.pmi(pmi, document.views(), (pds, absr), &owner, lu, au, gctx)?;
304 }
305
306 let mut out = String::new();
307 out.push_str("ISO-10303-21;\nHEADER;\n");
308 out.push_str("FILE_DESCRIPTION(('written by ogeom'),'2;1');\n");
309 out.push_str("FILE_NAME('','',('ogeom'),('ogeom'),'ogeom','ogeom','');\n");
310 out.push_str("FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\n");
311 out.push_str("ENDSEC;\nDATA;\n");
312 for (i, entity) in writer.entities.iter().enumerate() {
313 let _ = writeln!(out, "#{}={entity};", i + 1);
314 }
315 out.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
316 if NON_FINITE.with(std::cell::Cell::get) {
317 ogeom_bail!(
318 Construction,
319 "the model holds a number that is not finite, which a STEP file cannot state"
320 );
321 }
322 Ok(out)
323}
324
325std::thread_local! {
326 static NON_FINITE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
329}
330
331struct Writer<'a> {
333 model: &'a Model,
334 entities: Vec<String>,
335 points: FastMap<[u64; 3], u64>,
336 directions: FastMap<[u64; 3], u64>,
337 vertices: FastMap<(ogeom_topo::TShapeId, [u64; 3]), u64>,
339 edges: FastMap<(ogeom_topo::TShapeId, [u64; 3]), u64>,
341 written_nodes: Vec<(ogeom_topo::TShapeId, u64)>,
344 written_edges: Vec<(ogeom_topo::TShapeId, u64)>,
347 curve_font: Option<u64>,
349 surface_curves: BTreeMap<u64, EdgeCurve>,
352 parametric_context: Option<u64>,
354 tol: Tolerances,
355}
356
357struct EdgeCurve {
359 from: u64,
360 to: u64,
361 curve: u64,
362 range: (f64, f64),
364 exact: bool,
367 pcurves: Vec<(u64, Vec<PlanarCurve>, Pace)>,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq)]
376struct Pace {
377 scale: f64,
378 shift: f64,
379}
380
381impl Pace {
382 const SAME: Self = Self {
383 scale: 1.0,
384 shift: 0.0,
385 };
386
387 fn between(curve: (f64, f64), pcurve: (f64, f64), slack: f64) -> Option<Self> {
390 if (curve.0 - pcurve.0).abs() <= slack && (curve.1 - pcurve.1).abs() <= slack {
391 return Some(Self::SAME);
392 }
393 let scale = (pcurve.1 - pcurve.0) / (curve.1 - curve.0);
394 let shift = scale.mul_add(-curve.0, pcurve.0);
395 (scale.is_finite() && shift.is_finite() && scale > 0.0).then_some(Self { scale, shift })
396 }
397}
398
399impl Writer<'_> {
400 fn entity(&mut self, text: String) -> u64 {
401 self.entities.push(text);
402 self.entities.len() as u64
403 }
404
405 fn point(&mut self, p: Point) -> u64 {
406 let key = [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
407 if let Some(&id) = self.points.get(&key) {
408 return id;
409 }
410 let id = self.entity(format!(
411 "CARTESIAN_POINT('',({},{},{}))",
412 real(p.x),
413 real(p.y),
414 real(p.z)
415 ));
416 self.points.insert(key, id);
417 id
418 }
419
420 fn direction(&mut self, v: Vector) -> u64 {
421 let key = [v.x.to_bits(), v.y.to_bits(), v.z.to_bits()];
422 if let Some(&id) = self.directions.get(&key) {
423 return id;
424 }
425 let id = self.entity(format!(
426 "DIRECTION('',({},{},{}))",
427 real(v.x),
428 real(v.y),
429 real(v.z)
430 ));
431 self.directions.insert(key, id);
432 id
433 }
434
435 fn frame(&mut self, frame: &Frame) -> u64 {
436 let origin = self.point(frame.origin());
437 let z = self.direction(frame.z().vector());
438 let x = self.direction(frame.x().vector());
439 self.entity(format!("AXIS2_PLACEMENT_3D('',#{origin},#{z},#{x})"))
440 }
441
442 fn conic_frame(&mut self, frame: &Frame) -> u64 {
447 let origin = self.point(frame.origin());
448 let z = self.direction(frame.x().vector().cross(frame.y().vector()));
449 let x = self.direction(frame.x().vector());
450 self.entity(format!("AXIS2_PLACEMENT_3D('',#{origin},#{z},#{x})"))
451 }
452
453 fn placement_frame(&mut self, at: &Transform) -> OgeomResult<u64> {
455 let origin = at.apply(Point::ORIGIN);
456 let z = at.apply_vector(Vector::new(0.0, 0.0, 1.0));
457 let x = at.apply_vector(Vector::new(1.0, 0.0, 0.0));
458 let frame = Frame::new(
459 origin,
460 ogeom_math::Direction::new(z, self.tol)?,
461 ogeom_math::Direction::new(x, self.tol)?,
462 self.tol,
463 )
464 .map_err(|_| {
465 ogeom_core::ogeom_err!(
466 Construction,
467 "an instance placement is not rigid; STEP cannot state it"
468 )
469 })?;
470 Ok(self.frame(&frame))
471 }
472
473 fn surface_model(&mut self, sheet: &Shape, lone: bool) -> OgeomResult<u64> {
477 let faces = if lone {
478 vec![sheet.clone()]
479 } else {
480 self.model.ordered_children_of(sheet)?
481 };
482 if faces.is_empty() {
483 ogeom_bail!(Construction, "a shell with no face cannot be written");
484 }
485 let keyword = if !lone && ogeom_algo::is_shell_closed(self.model, sheet)? {
486 "CLOSED_SHELL"
487 } else {
488 "OPEN_SHELL"
489 };
490 let mut ids = Vec::with_capacity(faces.len());
491 for face in &faces {
492 ids.push(self.face(face)?);
493 }
494 let list = reference_list(&ids);
495 let shell = self.entity(format!("{keyword}('',({list}))"));
496 let model = self.entity(format!("SHELL_BASED_SURFACE_MODEL('',(#{shell}))"));
497 if !lone {
498 self.written_nodes.push((sheet.node(), model));
499 }
500 Ok(model)
501 }
502
503 fn solid(&mut self, solid: &Shape) -> OgeomResult<u64> {
509 let shells = explore(self.model, solid, Filter::OfType(ShapeType::Shell))?;
510 let Some((outer, voids)) = shells.split_first() else {
511 ogeom_bail!(Construction, "a solid with no shell cannot be written");
512 };
513 let shell_id = self.closed_shell(outer, false)?;
514 let msb = if voids.is_empty() {
515 self.entity(format!("MANIFOLD_SOLID_BREP('',#{shell_id})"))
516 } else {
517 let mut uses = Vec::with_capacity(voids.len());
518 for void in voids {
519 let own = self.closed_shell(void, true)?;
520 uses.push(self.entity(format!("ORIENTED_CLOSED_SHELL('',*,#{own},.F.)")));
521 }
522 let list = uses
523 .iter()
524 .map(|i| format!("#{i}"))
525 .collect::<Vec<_>>()
526 .join(",");
527 self.entity(format!("BREP_WITH_VOIDS('',#{shell_id},({list}))"))
528 };
529 self.written_nodes.push((solid.node(), msb));
530 Ok(msb)
531 }
532
533 fn closed_shell(&mut self, shell: &Shape, turned: bool) -> OgeomResult<u64> {
535 let mut faces = Vec::new();
536 for face in self.model.ordered_children_of(shell)? {
537 let face = if turned { face.reversed() } else { face };
538 faces.push(self.face(&face)?);
539 }
540 let list = faces
541 .iter()
542 .map(|i| format!("#{i}"))
543 .collect::<Vec<_>>()
544 .join(",");
545 Ok(self.entity(format!("CLOSED_SHELL('',({list}))")))
546 }
547
548 fn face(&mut self, face: &Shape) -> OgeomResult<u64> {
549 let placement = face.transform(self.model.datums())?;
550 let (surface, held) = {
551 let Some(node) = self.model.node(face) else {
552 ogeom_bail!(Dangling, "face is not in this model");
553 };
554 let NodeData::Face(data) = node.data() else {
555 ogeom_bail!(Construction, "face node holds no face data");
556 };
557 let Some(surface) = self.model.geometry().surface(data.surface) else {
558 ogeom_bail!(Dangling, "face refers to a surface not in this model");
559 };
560 (
561 surface.clone().transformed(&placement, self.tol)?,
562 data.surface,
563 )
564 };
565 let surface_id = self.surface(&surface)?;
566 let chart = (rigid(&placement) && surface_states_parameters(&surface))
569 .then_some((held, surface_id));
570
571 let along = if placement.preserves_handedness() {
577 ".T."
578 } else {
579 ".F."
580 };
581 let wires = self.model.children_of(face)?;
582 let mut uses: FastMap<ogeom_topo::TShapeId, usize> = FastMap::default();
585 for wire in &wires {
586 for edge in self.model.ordered_children_of(wire)? {
587 *uses.entry(edge.node()).or_default() += 1;
588 }
589 }
590 let mut bounds = Vec::new();
591 for (index, wire) in wires.iter().enumerate() {
592 let keyword = if index == 0 {
593 "FACE_OUTER_BOUND"
594 } else {
595 "FACE_BOUND"
596 };
597 let loop_id = self.wire(wire, chart, &uses)?;
598 bounds.push(self.entity(format!("{keyword}('',#{loop_id},{along})")));
599 }
600 let list = bounds
601 .iter()
602 .map(|i| format!("#{i}"))
603 .collect::<Vec<_>>()
604 .join(",");
605 let forward = face.orientation() != ogeom_topo::Orientation::Reversed;
609 let turned =
610 crate::bodies::turned_in_file(&placement, crate::bodies::restates(&surface, true));
611 let sense = if forward == turned { ".F." } else { ".T." };
612 let id = self.entity(format!("ADVANCED_FACE('',({list}),#{surface_id},{sense})"));
613 self.written_nodes.push((face.node(), id));
614 Ok(id)
615 }
616
617 fn wire(
622 &mut self,
623 wire: &Shape,
624 chart: Option<(SurfaceId, u64)>,
625 uses: &FastMap<ogeom_topo::TShapeId, usize>,
626 ) -> OgeomResult<u64> {
627 let children = self.model.ordered_children_of(wire)?;
628 let degenerate = |edge: &Shape| {
629 self.model
630 .node(edge)
631 .and_then(|n| n.data().as_edge())
632 .is_some_and(|d| d.degenerate)
633 };
634 if !children.is_empty() && children.iter().all(degenerate) {
635 let edge = &children[0];
636 let Some(vertex) = self.model.children_of(edge)?.first().cloned() else {
637 ogeom_bail!(Construction, "a degenerate edge has no vertex");
638 };
639 let vertex_id = self.vertex(&vertex, &edge.transform(self.model.datums())?)?;
640 return Ok(self.entity(format!("VERTEX_LOOP('',#{vertex_id})")));
641 }
642 let mut oriented = Vec::new();
643 for (k, edge) in children.iter().enumerate() {
644 if degenerate(edge) {
645 continue;
646 }
647 let edge_id = self.edge(edge)?;
648 if let Some(chart) = chart {
649 let side = if uses.get(&edge.node()).copied().unwrap_or(0) == 1 {
650 let n = children.len();
651 let neighbours = (&children[(k + n - 1) % n], &children[(k + 1) % n]);
652 self.seam_side_walked(edge, neighbours, chart.0)
653 } else {
654 None
655 };
656 self.note_pcurves(edge, edge_id, chart, side);
657 }
658 let sense = if edge.orientation() == ogeom_topo::Orientation::Reversed {
659 ".F."
660 } else {
661 ".T."
662 };
663 oriented.push(self.entity(format!("ORIENTED_EDGE('',*,*,#{edge_id},{sense})")));
664 }
665 let list = oriented
666 .iter()
667 .map(|i| format!("#{i}"))
668 .collect::<Vec<_>>()
669 .join(",");
670 Ok(self.entity(format!("EDGE_LOOP('',({list}))")))
671 }
672
673 fn edge(&mut self, edge: &Shape) -> OgeomResult<u64> {
674 let placement = edge.transform(self.model.datums())?;
675 let key = (edge.node(), transform_bits(&placement));
676 if let Some(&id) = self.edges.get(&key) {
677 return Ok(id);
678 }
679 let (curve, range) = {
680 let Some(node) = self.model.node(edge) else {
681 ogeom_bail!(Dangling, "edge is not in this model");
682 };
683 let Some(data) = node.data().as_edge() else {
684 ogeom_bail!(Construction, "edge node holds no edge data");
685 };
686 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
687 ogeom_bail!(Construction, "an edge with no curve cannot be written");
688 };
689 let Some(geometry) = self.model.geometry().curve(*curve) else {
690 ogeom_bail!(Dangling, "curve is not in this model");
691 };
692 (geometry.clone().transformed(&placement, self.tol)?, *range)
693 };
694 let exact = curve_states_parameter(&curve);
695 let curve_id = self.curve(&curve, range)?;
696 let vertices = self.model.children_of(edge)?;
697 let (from, to) = match vertices.len() {
698 0 => ogeom_bail!(Construction, "an edge with no vertices cannot be written"),
699 1 => (vertices[0].clone(), vertices[0].clone()),
700 _ => (vertices[0].clone(), vertices[vertices.len() - 1].clone()),
701 };
702 let from_placement = from.transform(self.model.datums())?;
706 let to_placement = to.transform(self.model.datums())?;
707 let from_id = self.vertex(&from, &from_placement)?;
708 let to_id = self.vertex(&to, &to_placement)?;
709 let id = self.entity(format!(
710 "EDGE_CURVE('',#{from_id},#{to_id},#{curve_id},.T.)"
711 ));
712 self.edges.insert(key, id);
713 self.written_edges.push((edge.node(), id));
714 self.surface_curves.insert(
715 id,
716 EdgeCurve {
717 from: from_id,
718 to: to_id,
719 curve: curve_id,
720 range,
721 exact,
722 pcurves: Vec::new(),
723 },
724 );
725 Ok(id)
726 }
727
728 fn seam_side_walked(
734 &self,
735 edge: &Shape,
736 (before, after): (&Shape, &Shape),
737 held: SurfaceId,
738 ) -> Option<bool> {
739 let walked = |shape: &Shape, id: ogeom_topo::PCurveId, range: (f64, f64)| {
740 let pcurve = self.model.geometry().pcurve(id)?;
741 let (a, b) = (
742 pcurve.point_at(range.0, self.tol).ok()?,
743 pcurve.point_at(range.1, self.tol).ok()?,
744 );
745 Some(
746 if shape.orientation() == ogeom_topo::Orientation::Reversed {
747 (b, a)
748 } else {
749 (a, b)
750 },
751 )
752 };
753 let single = |shape: &Shape| -> Option<(Point2, Point2)> {
754 let data = self.model.node(shape)?.data().as_edge()?;
755 match data.pcurve_for(held, shape.location()) {
756 Some(EdgeRepr::PCurve { curve, range, .. }) => walked(shape, *curve, *range),
757 _ => None,
758 }
759 };
760 let data = self.model.node(edge)?.data().as_edge()?;
761 let Some(EdgeRepr::Seam {
762 forward,
763 reversed,
764 range,
765 ..
766 }) = data.pcurve_for(held, edge.location())
767 else {
768 return None;
769 };
770 let (prior, next) = (single(before), single(after));
771 if prior.is_none() && next.is_none() {
772 return None;
773 }
774 let miss = |id: ogeom_topo::PCurveId| -> Option<f64> {
775 let (start, end) = walked(edge, id, *range)?;
776 Some(
777 prior.map_or(0.0, |(_, e)| e.distance(start))
778 + next.map_or(0.0, |(s, _)| end.distance(s)),
779 )
780 };
781 let (on_forward, on_reversed) = (miss(*forward)?, miss(*reversed)?);
782 (on_forward != on_reversed).then_some(on_forward < on_reversed)
783 }
784
785 fn note_pcurves(
792 &mut self,
793 edge: &Shape,
794 edge_id: u64,
795 (held, surface_id): (SurfaceId, u64),
796 side: Option<bool>,
797 ) {
798 let Some(record) = self.surface_curves.get_mut(&edge_id) else {
799 return;
800 };
801 if !record.exact || record.pcurves.iter().any(|(s, ..)| *s == surface_id) {
802 return;
803 }
804 let Some(data) = self.model.node(edge).and_then(|n| n.data().as_edge()) else {
805 return;
806 };
807 let (ids, range) = match data.pcurve_for(held, edge.location()) {
808 Some(EdgeRepr::PCurve { curve, range, .. }) => (vec![*curve], *range),
809 Some(EdgeRepr::Seam {
810 forward,
811 reversed,
812 range,
813 ..
814 }) => match side {
815 Some(true) => (vec![*forward], *range),
816 Some(false) => (vec![*reversed], *range),
817 None => (vec![*forward, *reversed], *range),
818 },
819 _ => return,
820 };
821 let Some(pace) = Pace::between(record.range, range, self.tol.parametric()) else {
822 return;
823 };
824 let mut curves = Vec::with_capacity(ids.len());
825 for id in ids {
826 match self.model.geometry().pcurve(id) {
827 Some(pcurve) if planar_stated(pcurve, pace) => curves.push(pcurve.clone()),
828 _ => return,
829 }
830 }
831 record.pcurves.push((surface_id, curves, pace));
832 }
833
834 fn surface_curves(&mut self) -> OgeomResult<()> {
839 let records = std::mem::take(&mut self.surface_curves);
840 for (edge_id, record) in &records {
841 let count: usize = record.pcurves.iter().map(|(_, c, _)| c.len()).sum();
842 let keyword = match (record.pcurves.len(), count) {
843 (1, 2) => "SEAM_CURVE",
844 (1, 1) | (2, 2) => "SURFACE_CURVE",
845 _ => continue,
846 };
847 let mut uses = Vec::with_capacity(count);
848 for (surface_id, curves, pace) in &record.pcurves {
849 for curve in curves {
850 let planar = self.planar_curve(curve, *pace)?;
851 let context = self.parametric_context();
852 let definition = self.entity(format!(
853 "DEFINITIONAL_REPRESENTATION('',(#{planar}),#{context})"
854 ));
855 uses.push(self.entity(format!("PCURVE('',#{surface_id},#{definition})")));
856 }
857 }
858 let list = reference_list(&uses);
859 let curve = self.entity(format!(
860 "{keyword}('',#{},({list}),.PCURVE_S1.)",
861 record.curve
862 ));
863 let (from, to) = (record.from, record.to);
864 let Some(text) = usize::try_from(*edge_id)
865 .ok()
866 .and_then(|i| i.checked_sub(1))
867 .and_then(|i| self.entities.get_mut(i))
868 else {
869 ogeom_bail!(Construction, "an edge entity was lost before its pcurves");
870 };
871 *text = format!("EDGE_CURVE('',#{from},#{to},#{curve},.T.)");
872 }
873 self.surface_curves = records;
874 Ok(())
875 }
876
877 fn parametric_context(&mut self) -> u64 {
880 if let Some(id) = self.parametric_context {
881 return id;
882 }
883 let id = self.entity(
884 "(GEOMETRIC_REPRESENTATION_CONTEXT(2)PARAMETRIC_REPRESENTATION_CONTEXT()REPRESENTATION_CONTEXT('2D SPACE',''))".into(),
885 );
886 self.parametric_context = Some(id);
887 id
888 }
889
890 fn point2(&mut self, p: Point2) -> u64 {
891 self.entity(format!("CARTESIAN_POINT('',({},{}))", real(p.x), real(p.y)))
892 }
893
894 fn direction2(&mut self, v: ogeom_math::Vector2) -> u64 {
895 self.entity(format!("DIRECTION('',({},{}))", real(v.x), real(v.y)))
896 }
897
898 fn placement2(&mut self, frame: &ogeom_math::Frame2) -> u64 {
899 let origin = self.point2(frame.origin());
900 let x = self.direction2(frame.x().vector());
901 self.entity(format!("AXIS2_PLACEMENT_2D('',#{origin},#{x})"))
902 }
903
904 fn planar_curve(&mut self, curve: &PlanarCurve, pace: Pace) -> OgeomResult<u64> {
908 match curve {
909 PlanarCurve::Line(line) => {
910 let axis = line.axis();
911 let direction = axis.direction.vector();
912 let origin = self.point2(axis.location + direction * pace.shift);
913 let d = self.direction2(direction);
914 let vector = self.entity(format!("VECTOR('',#{d},{})", real(pace.scale)));
915 Ok(self.entity(format!("LINE('',#{origin},#{vector})")))
916 }
917 PlanarCurve::Circle(c) => {
918 let circle = c.circle();
919 let frame = self.placement2(&circle.frame());
920 Ok(self.entity(format!("CIRCLE('',#{frame},{})", real(circle.radius()))))
921 }
922 PlanarCurve::Ellipse(e) => {
923 let ellipse = e.ellipse();
924 let frame = self.placement2(&ellipse.frame());
925 Ok(self.entity(format!(
926 "ELLIPSE('',#{frame},{},{})",
927 real(ellipse.major_radius()),
928 real(ellipse.minor_radius())
929 )))
930 }
931 PlanarCurve::BSpline(b) => {
932 let control: Vec<String> = b
933 .control_points()
934 .iter()
935 .map(|c| {
936 let p = Point2::from_vector(c.scaled.to_vector() / c.weight);
937 format!("#{}", self.point2(p))
938 })
939 .collect();
940 let (mults, knots) = compress_knots(b.knots().knots());
941 let knots: Vec<f64> = if pace == Pace::SAME {
942 knots
943 } else {
944 knots
945 .iter()
946 .map(|k| (k - pace.shift) / pace.scale)
947 .collect()
948 };
949 let degree = b.knots().degree();
950 let control = control.join(",");
951 let mults = mults
952 .iter()
953 .map(ToString::to_string)
954 .collect::<Vec<_>>()
955 .join(",");
956 let knots = knots.iter().map(|k| real(*k)).collect::<Vec<_>>().join(",");
957 if b.is_rational() {
958 let weights = b
959 .control_points()
960 .iter()
961 .map(|c| real(c.weight))
962 .collect::<Vec<_>>()
963 .join(",");
964 Ok(self.entity(format!(
965 "(BOUNDED_CURVE()B_SPLINE_CURVE({degree},({control}),.UNSPECIFIED.,.F.,.F.)B_SPLINE_CURVE_WITH_KNOTS(({mults}),({knots}),.UNSPECIFIED.)CURVE()GEOMETRIC_REPRESENTATION_ITEM()RATIONAL_B_SPLINE_CURVE(({weights}))REPRESENTATION_ITEM(''))"
966 )))
967 } else {
968 Ok(self.entity(format!(
969 "B_SPLINE_CURVE_WITH_KNOTS('',{degree},({control}),.UNSPECIFIED.,.F.,.F.,({mults}),({knots}),.UNSPECIFIED.)"
970 )))
971 }
972 }
973 PlanarCurve::Trimmed(t) => self.planar_curve(t.basis(), pace),
974 PlanarCurve::Hyperbola(_) | PlanarCurve::Offset(_) | PlanarCurve::Trig(_) => {
975 ogeom_bail!(
976 Construction,
977 "a pcurve with no exact spelling in the file was admitted"
978 )
979 }
980 }
981 }
982
983 fn vertex(&mut self, vertex: &Shape, placement: &Transform) -> OgeomResult<u64> {
984 let Some(data) = self.model.node(vertex).and_then(|n| n.data().as_vertex()) else {
985 ogeom_bail!(Construction, "vertex node holds no vertex data");
986 };
987 let at = placement.apply(data.point);
988 let key = (
989 vertex.node(),
990 [at.x.to_bits(), at.y.to_bits(), at.z.to_bits()],
991 );
992 if let Some(&id) = self.vertices.get(&key) {
993 return Ok(id);
994 }
995 let point = self.point(at);
996 let id = self.entity(format!("VERTEX_POINT('',#{point})"));
997 self.vertices.insert(key, id);
998 Ok(id)
999 }
1000
1001 fn curve(&mut self, curve: &Curve, range: (f64, f64)) -> OgeomResult<u64> {
1004 match curve {
1005 Curve::Line(line) => {
1006 let axis = line.axis();
1007 let origin = self.point(axis.location);
1008 let d = self.direction(axis.direction.vector());
1009 let vector = self.entity(format!("VECTOR('',#{d},1.0)"));
1010 Ok(self.entity(format!("LINE('',#{origin},#{vector})")))
1011 }
1012 Curve::Circle(c) => {
1015 let circle = c.forward(self.tol)?.circle();
1016 let frame = self.conic_frame(&circle.frame());
1017 Ok(self.entity(format!("CIRCLE('',#{frame},{})", real(circle.radius()))))
1018 }
1019 Curve::Ellipse(el) => {
1020 let ellipse = el.forward(self.tol)?.ellipse();
1021 let frame = self.conic_frame(&ellipse.frame());
1022 Ok(self.entity(format!(
1023 "ELLIPSE('',#{frame},{},{})",
1024 real(ellipse.major_radius()),
1025 real(ellipse.minor_radius())
1026 )))
1027 }
1028 Curve::BSpline(b) => self.bspline_curve(b),
1029 Curve::Hyperbola(h) if !h.is_reversed() => {
1032 let hyperbola = h.hyperbola();
1033 let frame = self.conic_frame(&hyperbola.frame());
1034 Ok(self.entity(format!(
1035 "HYPERBOLA('',#{frame},{},{})",
1036 real(hyperbola.major_radius()),
1037 real(hyperbola.minor_radius())
1038 )))
1039 }
1040 Curve::Parabola(p) if !p.is_reversed() => {
1041 let parabola = p.parabola();
1042 let frame = self.conic_frame(¶bola.frame());
1043 Ok(self.entity(format!("PARABOLA('',#{frame},{})", real(parabola.focal()))))
1044 }
1045 Curve::Offset(o) => {
1046 let basis = self.curve(o.basis(), range)?;
1047 let reference = self.direction(o.reference().vector());
1048 Ok(self.entity(format!(
1049 "OFFSET_CURVE_3D('',#{basis},{},.F.,#{reference})",
1050 real(o.distance())
1051 )))
1052 }
1053 other => {
1054 let spline = other.to_bspline_over(range, self.tol)?;
1059 self.bspline_curve(&spline)
1060 }
1061 }
1062 }
1063
1064 fn bspline_curve(&mut self, spline: &ogeom_geom::BSplineCurve) -> OgeomResult<u64> {
1065 let control: Vec<String> = spline
1066 .control_points()
1067 .iter()
1068 .map(|c| {
1069 let p = Point::from_vector(c.scaled.to_vector() / c.weight);
1070 format!("#{}", self.point(p))
1071 })
1072 .collect();
1073 let (mults, knots) = compress_knots(spline.knots().knots());
1074 let degree = spline.knots().degree();
1075 let rational = spline
1076 .control_points()
1077 .iter()
1078 .any(|c| (c.weight - 1.0).abs() > 1e-12);
1079 let control = control.join(",");
1080 let mults = mults
1081 .iter()
1082 .map(ToString::to_string)
1083 .collect::<Vec<_>>()
1084 .join(",");
1085 let knots = knots.iter().map(|k| real(*k)).collect::<Vec<_>>().join(",");
1086 if rational {
1087 let weights = spline
1088 .control_points()
1089 .iter()
1090 .map(|c| real(c.weight))
1091 .collect::<Vec<_>>()
1092 .join(",");
1093 Ok(self.entity(format!(
1094 "(BOUNDED_CURVE()B_SPLINE_CURVE({degree},({control}),.UNSPECIFIED.,.F.,.F.)B_SPLINE_CURVE_WITH_KNOTS(({mults}),({knots}),.UNSPECIFIED.)CURVE()GEOMETRIC_REPRESENTATION_ITEM()RATIONAL_B_SPLINE_CURVE(({weights}))REPRESENTATION_ITEM(''))"
1095 )))
1096 } else {
1097 Ok(self.entity(format!(
1098 "B_SPLINE_CURVE_WITH_KNOTS('',{degree},({control}),.UNSPECIFIED.,.F.,.F.,({mults}),({knots}),.UNSPECIFIED.)"
1099 )))
1100 }
1101 }
1102
1103 fn surface(&mut self, surface: &SurfaceGeometry) -> OgeomResult<u64> {
1107 match surface {
1108 SurfaceGeometry::Plane(s) => {
1109 let frame = self.frame(&s.plane().frame());
1110 Ok(self.entity(format!("PLANE('',#{frame})")))
1111 }
1112 SurfaceGeometry::Cylinder(s) => {
1113 let cylinder = s.cylinder();
1114 let frame = self.frame(&cylinder.frame());
1115 Ok(self.entity(format!(
1116 "CYLINDRICAL_SURFACE('',#{frame},{})",
1117 real(cylinder.radius())
1118 )))
1119 }
1120 SurfaceGeometry::Cone(s) => {
1121 let cone = s.cone();
1122 let frame = self.frame(&cone.frame());
1123 Ok(self.entity(format!(
1124 "CONICAL_SURFACE('',#{frame},{},{})",
1125 real(cone.radius_at(0.0)),
1126 real(cone.half_angle())
1127 )))
1128 }
1129 SurfaceGeometry::Sphere(s) => {
1130 let sphere = s.sphere();
1131 let frame = self.frame(&sphere.frame());
1132 Ok(self.entity(format!(
1133 "SPHERICAL_SURFACE('',#{frame},{})",
1134 real(sphere.radius())
1135 )))
1136 }
1137 SurfaceGeometry::Torus(s) => {
1138 let torus = s.torus();
1139 let frame = self.frame(&torus.frame());
1140 Ok(self.entity(format!(
1141 "TOROIDAL_SURFACE('',#{frame},{},{})",
1142 real(torus.major_radius()),
1143 real(torus.minor_radius())
1144 )))
1145 }
1146 SurfaceGeometry::BSpline(b) => self.bspline_surface(b),
1147 SurfaceGeometry::Trimmed(t) => self.surface(t.basis()),
1148 SurfaceGeometry::Revolution(r) => {
1151 let curve = r.curve();
1152 let range = ogeom_geom::Curve3d::domain(curve);
1153 let swept = self.curve(curve, range)?;
1154 let axis = r.axis();
1155 let location = self.point(axis.location);
1156 let direction = self.direction(axis.direction.vector());
1157 let placement =
1158 self.entity(format!("AXIS1_PLACEMENT('',#{location},#{direction})"));
1159 Ok(self.entity(format!("SURFACE_OF_REVOLUTION('',#{swept},#{placement})")))
1160 }
1161 SurfaceGeometry::Extrusion(e) => {
1162 let curve = e.curve();
1163 let range = ogeom_geom::Curve3d::domain(curve);
1164 let swept = self.curve(curve, range)?;
1165 let direction = self.direction(e.direction().vector());
1166 let vector = self.entity(format!("VECTOR('',#{direction},1.0)"));
1167 Ok(self.entity(format!(
1168 "SURFACE_OF_LINEAR_EXTRUSION('',#{swept},#{vector})"
1169 )))
1170 }
1171 SurfaceGeometry::Offset(o) => {
1174 let basis = self.surface(o.basis())?;
1175 let distance = if crate::bodies::restates(o.basis(), true) {
1176 -o.distance()
1177 } else {
1178 o.distance()
1179 };
1180 Ok(self.entity(format!(
1181 "OFFSET_SURFACE('',#{basis},{},.F.)",
1182 real(distance)
1183 )))
1184 }
1185 }
1186 }
1187
1188 fn bspline_surface(&mut self, patch: &ogeom_geom::BSplineSurface) -> OgeomResult<u64> {
1189 let grid = patch.grid();
1190 let (nu, nv) = (grid.u_count(), grid.v_count());
1191 let mut rows = Vec::with_capacity(nu);
1192 let mut weights_rows = Vec::with_capacity(nu);
1193 let mut rational = false;
1194 for u in 0..nu {
1195 let mut row = Vec::with_capacity(nv);
1196 let mut wrow = Vec::with_capacity(nv);
1197 for v in 0..nv {
1198 let Some(c) = grid.get(u, v) else {
1199 ogeom_bail!(Construction, "a control grid cell is missing");
1200 };
1201 let p = Point::from_vector(c.scaled.to_vector() / c.weight);
1202 row.push(format!("#{}", self.point(p)));
1203 wrow.push(real(c.weight));
1204 rational |= (c.weight - 1.0).abs() > 1e-12;
1205 }
1206 rows.push(format!("({})", row.join(",")));
1207 weights_rows.push(format!("({})", wrow.join(",")));
1208 }
1209 let grid_text = rows.join(",");
1210 let (u_deg, v_deg) = (patch.u_knots().degree(), patch.v_knots().degree());
1211 let (um, uk) = compress_knots(patch.u_knots().knots());
1212 let (vm, vk) = compress_knots(patch.v_knots().knots());
1213 let fmt_m = |m: &[usize]| {
1214 m.iter()
1215 .map(ToString::to_string)
1216 .collect::<Vec<_>>()
1217 .join(",")
1218 };
1219 let fmt_k = |k: &[f64]| k.iter().map(|v| real(*v)).collect::<Vec<_>>().join(",");
1220 let (um, uk, vm, vk) = (fmt_m(&um), fmt_k(&uk), fmt_m(&vm), fmt_k(&vk));
1221 if rational {
1222 let weights = weights_rows.join(",");
1223 Ok(self.entity(format!(
1224 "(BOUNDED_SURFACE()B_SPLINE_SURFACE({u_deg},{v_deg},({grid_text}),.UNSPECIFIED.,.F.,.F.,.F.)B_SPLINE_SURFACE_WITH_KNOTS(({um}),({vm}),({uk}),({vk}),.UNSPECIFIED.)GEOMETRIC_REPRESENTATION_ITEM()RATIONAL_B_SPLINE_SURFACE(({weights}))REPRESENTATION_ITEM('')SURFACE())"
1225 )))
1226 } else {
1227 Ok(self.entity(format!(
1228 "B_SPLINE_SURFACE_WITH_KNOTS('',{u_deg},{v_deg},({grid_text}),.UNSPECIFIED.,.F.,.F.,.F.,({um}),({vm}),({uk}),({vk}),.UNSPECIFIED.)"
1229 )))
1230 }
1231 }
1232
1233 fn construction(
1240 &mut self,
1241 items: &[&ogeom_doc::Construction],
1242 name: &str,
1243 gctx: u64,
1244 ) -> OgeomResult<Option<u64>> {
1245 use ogeom_doc::ConstructionGeometry as G;
1246 if items.is_empty() {
1247 return Ok(None);
1248 }
1249 let mut elements = Vec::new();
1250 let mut axes = Vec::new();
1251 let mut surfaces = false;
1252 for item in items {
1253 let label = escape(&item.name);
1254 match &item.geometry {
1255 G::Point(p) => elements.push(self.entity(format!(
1256 "CARTESIAN_POINT('{label}',({},{},{}))",
1257 real(p.x),
1258 real(p.y),
1259 real(p.z)
1260 ))),
1261 G::Axis(frame) => {
1262 if frame.handedness() == Handedness::Left {
1263 ogeom_bail!(
1264 Construction,
1265 "construction axis '{}' is left-handed; an axis placement \
1266 states a right-handed frame",
1267 item.name
1268 );
1269 }
1270 let origin = self.point(frame.origin());
1271 let z = self.direction(frame.z().vector());
1272 let x = self.direction(frame.x().vector());
1273 axes.push(
1274 self.entity(format!("AXIS2_PLACEMENT_3D('{label}',#{origin},#{z},#{x})")),
1275 );
1276 }
1277 G::Curve { curve, range } => {
1278 elements.push(self.construction_curve(curve, *range, &label)?);
1279 }
1280 G::Surface { surface, u, v } => {
1281 surfaces = true;
1282 elements.push(self.construction_surface(surface, *u, *v, &label)?);
1283 }
1284 }
1285 }
1286 let (set, representation) = if surfaces {
1287 (
1288 "GEOMETRIC_SET",
1289 "GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION",
1290 )
1291 } else {
1292 (
1293 "GEOMETRIC_CURVE_SET",
1294 "GEOMETRICALLY_BOUNDED_WIREFRAME_SHAPE_REPRESENTATION",
1295 )
1296 };
1297 let mut members = Vec::with_capacity(axes.len() + 1);
1298 if !elements.is_empty() {
1299 let list = reference_list(&elements);
1300 members.push(self.entity(format!("{set}('',({list}))")));
1301 }
1302 members.extend(axes);
1303 let list = reference_list(&members);
1304 Ok(Some(self.entity(format!(
1305 "{representation}('{name}',({list}),#{gctx})"
1306 ))))
1307 }
1308
1309 fn construction_curve(
1313 &mut self,
1314 curve: &Curve,
1315 (t0, t1): (f64, f64),
1316 label: &str,
1317 ) -> OgeomResult<u64> {
1318 let basis = self.curve(curve, (t0, t1))?;
1319 let (p0, p1) = (curve.point_at(t0, self.tol)?, curve.point_at(t1, self.tol)?);
1320 let (p0, p1) = (self.point(p0), self.point(p1));
1321 let text = if curve_states_parameter(curve) {
1322 format!(
1323 "TRIMMED_CURVE('{label}',#{basis},(#{p0},PARAMETER_VALUE({})),(#{p1},PARAMETER_VALUE({})),.T.,.PARAMETER.)",
1324 real(t0),
1325 real(t1)
1326 )
1327 } else {
1328 format!("TRIMMED_CURVE('{label}',#{basis},(#{p0}),(#{p1}),.T.,.CARTESIAN.)")
1329 };
1330 Ok(self.entity(text))
1331 }
1332
1333 fn construction_surface(
1338 &mut self,
1339 surface: &SurfaceGeometry,
1340 u: (f64, f64),
1341 v: (f64, f64),
1342 label: &str,
1343 ) -> OgeomResult<u64> {
1344 let (basis, u, v) = if surface_states_parameters(surface) {
1345 (self.surface(surface)?, u, v)
1346 } else {
1347 let window = ogeom_geom::TrimmedSurface::new(surface.clone(), u, v, self.tol)?;
1348 let patch = SurfaceGeometry::Trimmed(Box::new(window)).to_bspline(self.tol)?;
1349 let (u, v) = patch.domain();
1350 (self.bspline_surface(&patch)?, u, v)
1351 };
1352 Ok(self.entity(format!(
1353 "RECTANGULAR_TRIMMED_SURFACE('{label}',#{basis},{},{},{},{},.T.,.T.)",
1354 real(u.0),
1355 real(u.1),
1356 real(v.0),
1357 real(v.1)
1358 )))
1359 }
1360
1361 #[allow(clippy::many_single_char_names)]
1363 #[allow(
1364 clippy::too_many_arguments,
1365 reason = "five STEP context ids that travel together"
1366 )]
1367 fn pmi(
1368 &mut self,
1369 pmi: &ogeom_doc::Pmi,
1370 views: &[ogeom_doc::View],
1371 anchor: (u64, u64),
1372 owner: &FastMap<ogeom_topo::TShapeId, (u64, u64)>,
1373 lu: u64,
1374 au: u64,
1375 gctx: u64,
1376 ) -> OgeomResult<()> {
1377 let by_node: FastMap<ogeom_topo::TShapeId, u64> = self
1378 .written_nodes
1379 .iter()
1380 .chain(&self.written_edges)
1381 .copied()
1382 .collect();
1383 let part_of = |items: &[ogeom_topo::TShapeId]| -> (u64, u64) {
1386 items
1387 .iter()
1388 .find_map(|item| owner.get(item))
1389 .copied()
1390 .unwrap_or(anchor)
1391 };
1392 let aspect_for = |w: &mut Self, items: &[ogeom_topo::TShapeId]| -> u64 {
1393 let (pds, absr) = part_of(items);
1394 let aspect = w.entity(format!("SHAPE_ASPECT('','',#{pds},.T.)"));
1395 for item in items {
1396 if let Some(&step_id) = by_node.get(item) {
1397 w.entity(format!(
1398 "GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#{aspect},#{absr},#{step_id})"
1399 ));
1400 }
1401 }
1402 aspect
1403 };
1404
1405 let mut annotation_ids: FastMap<ogeom_doc::Annotated, u64> = FastMap::default();
1408 let mut datum_ids: FastMap<&str, u64> = FastMap::default();
1409 for datum in &pmi.datums {
1410 let label = escape(&datum.label);
1411 let (pds, absr) = part_of(&datum.items);
1412 let id = self.entity(format!("DATUM('','',#{pds},.F.,'{label}')"));
1413 for item in &datum.items {
1414 if let Some(&step_id) = by_node.get(item) {
1415 self.entity(format!(
1416 "GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#{id},#{absr},#{step_id})"
1417 ));
1418 }
1419 }
1420 datum_ids.insert(datum.label.as_str(), id);
1421 annotation_ids.insert(ogeom_doc::Annotated::Datum(datum_ids.len() - 1), id);
1422 }
1423
1424 for (at, dimension) in pmi.dimensions.iter().enumerate() {
1425 let name = escape(&dimension.name);
1426 let angular = dimension.kind == ogeom_doc::MeasureKind::Angle;
1427 let dim = if dimension.location {
1428 let empty = Vec::new();
1431 let first = dimension.features.first().unwrap_or(&empty);
1432 let second = dimension.features.get(1).unwrap_or(first);
1433 let a = aspect_for(self, first);
1434 let b = aspect_for(self, second);
1435 if angular {
1436 self.entity(format!("ANGULAR_LOCATION('{name}','',#{a},#{b},.EQUAL.)"))
1437 } else {
1438 self.entity(format!("DIMENSIONAL_LOCATION('{name}','',#{a},#{b})"))
1439 }
1440 } else {
1441 let empty = Vec::new();
1442 let items = dimension.features.first().unwrap_or(&empty);
1443 let aspect = aspect_for(self, items);
1444 if angular {
1445 self.entity(format!("ANGULAR_SIZE(#{aspect},'{name}',.EQUAL.)"))
1446 } else {
1447 self.entity(format!("DIMENSIONAL_SIZE(#{aspect},'{name}')"))
1448 }
1449 };
1450 let measures: Vec<String> = dimension
1451 .values
1452 .iter()
1453 .map(|&v| format!("#{}", self.measure(v, dimension.kind, lu, au)))
1454 .collect();
1455 let list = measures.join(",");
1456 annotation_ids.insert(ogeom_doc::Annotated::Dimension(at), dim);
1457 let sdr = self.entity(format!(
1458 "SHAPE_DIMENSION_REPRESENTATION('',({list}),#{gctx})"
1459 ));
1460 self.entity(format!(
1461 "DIMENSIONAL_CHARACTERISTIC_REPRESENTATION(#{dim},#{sdr})"
1462 ));
1463 if dimension.plus.is_some() || dimension.minus.is_some() {
1464 let lower = self.measure(dimension.minus.unwrap_or(0.0), dimension.kind, lu, au);
1465 let upper = self.measure(dimension.plus.unwrap_or(0.0), dimension.kind, lu, au);
1466 let tv = self.entity(format!("TOLERANCE_VALUE(#{lower},#{upper})"));
1467 self.entity(format!("PLUS_MINUS_TOLERANCE(#{tv},#{dim})"));
1468 }
1469 }
1470
1471 for (at, tolerance) in pmi.tolerances.iter().enumerate() {
1472 let aspect = aspect_for(self, &tolerance.items);
1473 let name = escape(&tolerance.name);
1474 let magnitude =
1475 self.measure(tolerance.magnitude, ogeom_doc::MeasureKind::Length, lu, au);
1476 let keyword = format!("{}_TOLERANCE", tolerance.kind.to_uppercase());
1477 let system = !tolerance.datums.is_empty()
1478 && (tolerance.datum_system
1479 || tolerance.datums.iter().any(|d| !d.modifiers.is_empty()));
1480 let refs: Vec<String> = if system {
1481 let (pds, _) = part_of(&tolerance.items);
1482 let id = self.datum_system(&tolerance.datums, &datum_ids, pds, lu, au)?;
1483 vec![format!("#{id}")]
1484 } else {
1485 tolerance
1489 .datums
1490 .iter()
1491 .filter_map(|d| {
1492 if let Some(id) = datum_ids.get(d.label.as_str()) {
1493 return Some(format!("#{id}"));
1494 }
1495 let parts: Vec<String> = d
1496 .label
1497 .split('-')
1498 .filter_map(|label| datum_ids.get(label))
1499 .map(|i| format!("#{i}"))
1500 .collect();
1501 if parts.len() < 2 {
1502 return None;
1503 }
1504 let list = parts.join(",");
1505 let (pds, _) = part_of(&tolerance.items);
1506 let compartment = self.entity(format!(
1507 "DATUM_REFERENCE_COMPARTMENT('','',#{pds},.F.,({list}),())"
1508 ));
1509 Some(format!("#{compartment}"))
1510 })
1511 .collect()
1512 };
1513 let refs = refs.join(",");
1514 let simple = tolerance.modifiers.is_empty()
1519 && (!system || REFERENCES_DATUMS.contains(&keyword.as_str()));
1520 let written = if simple {
1521 if refs.is_empty() {
1522 self.entity(format!("{keyword}('{name}','',#{magnitude},#{aspect})"))
1523 } else {
1524 self.entity(format!(
1525 "{keyword}('{name}','',#{magnitude},#{aspect},({refs}))"
1526 ))
1527 }
1528 } else {
1529 let mut parts = vec![
1533 format!("GEOMETRIC_TOLERANCE('{name}','',#{magnitude},#{aspect})"),
1534 format!("{keyword}()"),
1535 ];
1536 if !tolerance.modifiers.is_empty() {
1537 let words: Vec<String> = tolerance
1538 .modifiers
1539 .iter()
1540 .map(|m| format!(".{}.", m.to_uppercase()))
1541 .collect();
1542 let words = words.join(",");
1543 parts.push(format!("GEOMETRIC_TOLERANCE_WITH_MODIFIERS(({words}))"));
1544 }
1545 if !refs.is_empty() {
1546 parts.push(format!(
1547 "GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE(({refs}))"
1548 ));
1549 }
1550 parts.sort();
1551 self.entity(format!("({})", parts.concat()))
1552 };
1553 annotation_ids.insert(ogeom_doc::Annotated::Tolerance(at), written);
1554 }
1555
1556 for target in &pmi.targets {
1561 let identifier = escape(&target.identifier());
1562 let description = match target.kind {
1563 ogeom_doc::DatumTargetKind::Point => "point",
1564 ogeom_doc::DatumTargetKind::Line { .. } => "line",
1565 ogeom_doc::DatumTargetKind::Rectangle { .. } => "rectangle",
1566 ogeom_doc::DatumTargetKind::Circle { .. } => "circle",
1567 };
1568 let (pds, absr) = part_of(&target.items);
1569 let id = self.entity(format!(
1570 "PLACED_DATUM_TARGET_FEATURE('','{description}',#{pds},.F.,'{identifier}')"
1571 ));
1572 for item in &target.items {
1573 if let Some(&step_id) = by_node.get(item) {
1574 self.entity(format!(
1575 "GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#{id},#{absr},#{step_id})"
1576 ));
1577 }
1578 }
1579 if let Some(&datum_id) = datum_ids.get(target.datum.as_str()) {
1582 self.entity(format!(
1583 "SHAPE_ASPECT_RELATIONSHIP('','',#{datum_id},#{id})"
1584 ));
1585 }
1586 let frame = target
1587 .frame
1588 .unwrap_or_else(|| Frame::about(target.at, ogeom_math::Direction::Z));
1589 let placement = self.frame(&frame);
1590 let sizes: Vec<f64> = match target.kind {
1591 ogeom_doc::DatumTargetKind::Point => Vec::new(),
1592 ogeom_doc::DatumTargetKind::Line { length } => vec![length],
1593 ogeom_doc::DatumTargetKind::Rectangle { length, width } => vec![length, width],
1594 ogeom_doc::DatumTargetKind::Circle { diameter } => vec![diameter],
1595 };
1596 let mut items = vec![format!("#{placement}")];
1597 for size in sizes {
1598 let measure = self.measure(size, ogeom_doc::MeasureKind::Length, lu, au);
1599 items.push(format!("#{measure}"));
1600 }
1601 let list = items.join(",");
1602 let rep = self.entity(format!(
1603 "SHAPE_REPRESENTATION('{identifier}',({list}),#{gctx})"
1604 ));
1605 let property = self.entity(format!("PROPERTY_DEFINITION('','',#{id})"));
1606 self.entity(format!(
1607 "SHAPE_DEFINITION_REPRESENTATION(#{property},#{rep})"
1608 ));
1609 }
1610
1611 let mut callout_ids: Vec<u64> = Vec::new();
1616 if !pmi.callouts.is_empty() {
1617 let mut drawn: Vec<(u64, ogeom_doc::Annotated)> = Vec::new();
1618 let mut noted: Vec<(u64, u64)> = Vec::new();
1619 let mut planes: Vec<String> = Vec::new();
1620 for callout in &pmi.callouts {
1621 let name = escape(&callout.name);
1622 let mut coordinates: Vec<String> = Vec::new();
1623 let mut lines: Vec<String> = Vec::new();
1624 let mut next = 1_usize;
1625 for polyline in &callout.polylines {
1626 let mut indices: Vec<String> = Vec::with_capacity(polyline.len());
1627 for p in polyline {
1628 coordinates.push(format!("({},{},{})", real(p.x), real(p.y), real(p.z)));
1629 indices.push(next.to_string());
1630 next += 1;
1631 }
1632 lines.push(format!("({})", indices.join(",")));
1633 }
1634 let count = coordinates.len();
1635 let coordinates = coordinates.join(",");
1636 let list = self.entity(format!(
1637 "COORDINATES_LIST('{name}',{count},({coordinates}))"
1638 ));
1639 let lines = lines.join(",");
1640 let set = self.entity(format!("TESSELLATED_CURVE_SET('{name}',#{list},({lines}))"));
1641 let occurrence = self.entity(format!(
1645 "TESSELLATED_ANNOTATION_OCCURRENCE('{name}',(),#{set})"
1646 ));
1647 let mut held = vec![format!("#{occurrence}")];
1648 if let Some(words) = &callout.text {
1651 let words = escape(words);
1652 let placement = self.frame(&callout.plane.unwrap_or(Frame::WORLD));
1653 let font =
1654 self.entity("DRAUGHTING_PRE_DEFINED_TEXT_FONT('ISO 3098')".to_string());
1655 let literal = self.entity(format!(
1656 "TEXT_LITERAL('{name}','{words}',#{placement},'baseline left',.RIGHT.,#{font})"
1657 ));
1658 let text = self.entity(format!(
1659 "ANNOTATION_TEXT_OCCURRENCE('{name}',(),#{literal})"
1660 ));
1661 held.push(format!("#{text}"));
1662 }
1663 let held = held.join(",");
1664 let id = self.entity(format!("DRAUGHTING_CALLOUT('{name}',({held}))"));
1665 callout_ids.push(id);
1666 if let Some(frame) = callout.plane {
1667 let placement = self.frame(&frame);
1668 let plane = self.entity(format!("PLANE('{name}',#{placement})"));
1669 planes.push(
1670 self.entity(format!("ANNOTATION_PLANE('{name}',(),#{plane},(#{id}))"))
1671 .to_string(),
1672 );
1673 }
1674 if let Some(annotates) = callout.annotates {
1675 drawn.push((id, annotates));
1676 } else if !callout.items.is_empty() {
1677 noted.push((id, aspect_for(self, &callout.items)));
1680 }
1681 }
1682 let list: Vec<String> = planes.iter().map(|p| format!("#{p}")).collect();
1683 let list = list.join(",");
1684 let model = self.entity(format!("DRAUGHTING_MODEL('',({list}),#{gctx})"));
1685 for (callout, aspect) in noted {
1686 self.entity(format!(
1687 "DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation \
1688 link','',#{aspect},#{model},#{callout})"
1689 ));
1690 }
1691 for (callout, annotates) in drawn {
1692 let Some(&annotation) = annotation_ids.get(&annotates) else {
1693 continue;
1694 };
1695 self.entity(format!(
1696 "DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation \
1697 link','',#{annotation},#{model},#{callout})"
1698 ));
1699 }
1700 }
1701
1702 for view in views {
1708 let name = escape(&view.name);
1709 let placement = self.frame(&view.frame);
1710 let camera = self.entity(format!("CAMERA_MODEL_D3('{name}',#{placement},$)"));
1711 let mut items = vec![format!("#{camera}")];
1712 for &index in &view.callouts {
1713 if let Some(id) = callout_ids.get(index) {
1714 items.push(format!("#{id}"));
1715 }
1716 }
1717 let items = items.join(",");
1718 self.entity(format!("DRAUGHTING_MODEL('{name}',({items}),#{gctx})"));
1719 }
1720 Ok(())
1721 }
1722
1723 fn datum_system(
1727 &mut self,
1728 references: &[ogeom_doc::DatumReference],
1729 datum_ids: &FastMap<&str, u64>,
1730 pds: u64,
1731 lu: u64,
1732 au: u64,
1733 ) -> OgeomResult<u64> {
1734 if references.len() > 3 {
1735 ogeom_bail!(
1736 Construction,
1737 "a datum system holds at most three references, this one {}",
1738 references.len()
1739 );
1740 }
1741 let mut compartments: Vec<String> = Vec::with_capacity(references.len());
1742 for reference in references {
1743 let base = if let Some(&id) = datum_ids.get(reference.label.as_str()) {
1744 format!("#{id}")
1745 } else {
1746 let mut elements: Vec<String> = Vec::new();
1747 for label in reference.label.split('-') {
1748 let Some(&id) = datum_ids.get(label) else {
1749 ogeom_bail!(
1750 Construction,
1751 "the datum system references `{}`, which no datum is",
1752 reference.label
1753 );
1754 };
1755 let element =
1756 self.entity(format!("DATUM_REFERENCE_ELEMENT('','',#{pds},.F.,#{id},$)"));
1757 elements.push(format!("#{element}"));
1758 }
1759 if elements.len() < 2 {
1760 ogeom_bail!(
1761 Construction,
1762 "the datum system references `{}`, which no datum is",
1763 reference.label
1764 );
1765 }
1766 format!("({})", elements.join(","))
1767 };
1768 let mut modifiers: Vec<String> = Vec::with_capacity(reference.modifiers.len());
1769 for modifier in &reference.modifiers {
1770 modifiers.push(match modifier {
1771 ogeom_doc::DatumModifier::Simple(word) => {
1772 format!("SIMPLE_DATUM_REFERENCE_MODIFIER(.{}.)", word.to_uppercase())
1773 }
1774 ogeom_doc::DatumModifier::WithValue { kind, value } => {
1775 let measure = self.measure(*value, ogeom_doc::MeasureKind::Length, lu, au);
1776 let id = self.entity(format!(
1777 "DATUM_REFERENCE_MODIFIER_WITH_VALUE(.{}.,#{measure})",
1778 kind.word().to_uppercase()
1779 ));
1780 format!("#{id}")
1781 }
1782 });
1783 }
1784 let modifiers = if modifiers.is_empty() {
1785 "$".to_owned()
1786 } else {
1787 format!("({})", modifiers.join(","))
1788 };
1789 let compartment = self.entity(format!(
1790 "DATUM_REFERENCE_COMPARTMENT('','',#{pds},.F.,{base},{modifiers})"
1791 ));
1792 compartments.push(format!("#{compartment}"));
1793 }
1794 let list = compartments.join(",");
1795 Ok(self.entity(format!("DATUM_SYSTEM('','',#{pds},.F.,({list}))")))
1796 }
1797
1798 fn measure(&mut self, value: f64, kind: ogeom_doc::MeasureKind, lu: u64, au: u64) -> u64 {
1800 let v = real(value);
1801 match kind {
1802 ogeom_doc::MeasureKind::Length => self.entity(format!(
1803 "(LENGTH_MEASURE_WITH_UNIT()MEASURE_REPRESENTATION_ITEM()MEASURE_WITH_UNIT(LENGTH_MEASURE({v}),#{lu})REPRESENTATION_ITEM(''))"
1804 )),
1805 ogeom_doc::MeasureKind::Angle => self.entity(format!(
1806 "(MEASURE_REPRESENTATION_ITEM()MEASURE_WITH_UNIT(PLANE_ANGLE_MEASURE({v}),#{au})PLANE_ANGLE_MEASURE_WITH_UNIT()REPRESENTATION_ITEM(''))"
1807 )),
1808 }
1809 }
1810
1811 fn styled_item(&mut self, item: u64, colour: ogeom_doc::Colour) -> u64 {
1813 let rgb = self.entity(format!(
1814 "COLOUR_RGB('',{},{},{})",
1815 real(colour.r),
1816 real(colour.g),
1817 real(colour.b)
1818 ));
1819 let fasc = self.entity(format!("FILL_AREA_STYLE_COLOUR('',#{rgb})"));
1820 let fas = self.entity(format!("FILL_AREA_STYLE('',(#{fasc}))"));
1821 let ssfa = self.entity(format!("SURFACE_STYLE_FILL_AREA(#{fas})"));
1822 let sss = self.entity(format!("SURFACE_SIDE_STYLE('',(#{ssfa}))"));
1823 let ssu = self.entity(format!("SURFACE_STYLE_USAGE(.BOTH.,#{sss})"));
1824 let psa = self.entity(format!("PRESENTATION_STYLE_ASSIGNMENT((#{ssu}))"));
1825 self.entity(format!("STYLED_ITEM('',(#{psa}),#{item})"))
1826 }
1827
1828 fn curve_styled_item(&mut self, item: u64, colour: ogeom_doc::Colour) -> u64 {
1831 let font = match self.curve_font {
1832 Some(font) => font,
1833 None => {
1834 let font = self.entity("DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous')".into());
1835 self.curve_font = Some(font);
1836 font
1837 }
1838 };
1839 let rgb = self.entity(format!(
1840 "COLOUR_RGB('',{},{},{})",
1841 real(colour.r),
1842 real(colour.g),
1843 real(colour.b)
1844 ));
1845 let style = self.entity(format!(
1846 "CURVE_STYLE('',#{font},POSITIVE_LENGTH_MEASURE(0.1),#{rgb})"
1847 ));
1848 let psa = self.entity(format!("PRESENTATION_STYLE_ASSIGNMENT((#{style}))"));
1849 self.entity(format!("STYLED_ITEM('',(#{psa}),#{item})"))
1850 }
1851}
1852
1853const REFERENCES_DATUMS: [&str; 8] = [
1857 "ANGULARITY_TOLERANCE",
1858 "CIRCULAR_RUNOUT_TOLERANCE",
1859 "COAXIALITY_TOLERANCE",
1860 "CONCENTRICITY_TOLERANCE",
1861 "PARALLELISM_TOLERANCE",
1862 "PERPENDICULARITY_TOLERANCE",
1863 "SYMMETRY_TOLERANCE",
1864 "TOTAL_RUNOUT_TOLERANCE",
1865];
1866
1867fn rigid(placement: &Transform) -> bool {
1870 (placement.scale_factor() - 1.0).abs() <= 1e-12 && placement.preserves_handedness()
1871}
1872
1873fn curve_states_parameter(curve: &Curve) -> bool {
1879 match curve {
1880 Curve::Line(_) | Curve::BSpline(_) => true,
1881 Curve::Circle(c) => {
1882 !c.is_reversed() && c.circle().frame().handedness() == Handedness::Right
1883 }
1884 Curve::Ellipse(e) => {
1885 !e.is_reversed() && e.ellipse().frame().handedness() == Handedness::Right
1886 }
1887 Curve::Hyperbola(h) => {
1888 !h.is_reversed() && h.hyperbola().frame().handedness() == Handedness::Right
1889 }
1890 Curve::Offset(o) => curve_states_parameter(o.basis()),
1891 _ => false,
1892 }
1893}
1894
1895fn surface_states_parameters(surface: &SurfaceGeometry) -> bool {
1899 let right = |frame: Frame| frame.handedness() == Handedness::Right;
1900 match surface {
1901 SurfaceGeometry::Plane(s) => right(s.plane().frame()),
1902 SurfaceGeometry::Cylinder(s) => right(s.cylinder().frame()),
1903 SurfaceGeometry::Cone(s) => right(s.cone().frame()),
1904 SurfaceGeometry::Sphere(s) => right(s.sphere().frame()),
1905 SurfaceGeometry::Torus(s) => right(s.torus().frame()),
1906 SurfaceGeometry::BSpline(_) => true,
1907 SurfaceGeometry::Revolution(r) => curve_states_parameter(r.curve()),
1908 SurfaceGeometry::Extrusion(e) => curve_states_parameter(e.curve()),
1909 SurfaceGeometry::Trimmed(t) => surface_states_parameters(t.basis()),
1910 SurfaceGeometry::Offset(o) => surface_states_parameters(o.basis()),
1911 }
1912}
1913
1914fn planar_stated(curve: &PlanarCurve, pace: Pace) -> bool {
1920 match curve {
1921 PlanarCurve::Line(_) | PlanarCurve::BSpline(_) => true,
1922 PlanarCurve::Circle(c) => {
1923 pace == Pace::SAME
1924 && !c.is_reversed()
1925 && c.circle().frame().handedness() == Handedness::Right
1926 }
1927 PlanarCurve::Ellipse(e) => {
1928 pace == Pace::SAME
1929 && !e.is_reversed()
1930 && e.ellipse().frame().handedness() == Handedness::Right
1931 }
1932 PlanarCurve::Trimmed(t) => !t.is_reversed() && planar_stated(t.basis(), pace),
1933 PlanarCurve::Hyperbola(_) | PlanarCurve::Offset(_) | PlanarCurve::Trig(_) => false,
1934 }
1935}
1936
1937fn real(v: f64) -> String {
1940 if !v.is_finite() {
1941 NON_FINITE.with(|seen| seen.set(true));
1942 return "0.0".into();
1943 }
1944 let mut s = format!("{v:?}");
1945 if let Some(e) = s.find(['e', 'E']) {
1946 let (mantissa, exponent) = s.split_at(e);
1947 let mut m = mantissa.to_string();
1948 if !m.contains('.') {
1949 m.push_str(".0");
1950 }
1951 s = format!("{m}E{}", &exponent[1..]);
1952 } else if !s.contains('.') {
1953 s.push_str(".0");
1954 }
1955 s
1956}
1957
1958fn reference_list(ids: &[u64]) -> String {
1960 ids.iter()
1961 .map(|i| format!("#{i}"))
1962 .collect::<Vec<_>>()
1963 .join(",")
1964}
1965
1966fn escape(s: &str) -> String {
1968 let mut out = String::with_capacity(s.len());
1972 let mut wide: Vec<u16> = Vec::new();
1973 let flush = |wide: &mut Vec<u16>, out: &mut String| {
1974 if wide.is_empty() {
1975 return;
1976 }
1977 out.push_str("\\X2\\");
1978 for unit in wide.drain(..) {
1979 out.push_str(&format!("{unit:04X}"));
1980 }
1981 out.push_str("\\X0\\");
1982 };
1983 for c in s.chars() {
1984 if c.is_ascii() {
1985 flush(&mut wide, &mut out);
1986 match c {
1987 '\'' => out.push_str("''"),
1988 '\\' => out.push_str("\\\\"),
1989 _ => out.push(c),
1990 }
1991 } else {
1992 let mut units = [0_u16; 2];
1993 wide.extend_from_slice(c.encode_utf16(&mut units));
1994 }
1995 }
1996 flush(&mut wide, &mut out);
1997 out
1998}
1999
2000fn compress_knots(knots: &[f64]) -> (Vec<usize>, Vec<f64>) {
2002 let mut mults = Vec::new();
2003 let mut values = Vec::new();
2004 for &k in knots {
2005 match values.last() {
2006 Some(&last) if k == last => {
2007 if let Some(m) = mults.last_mut() {
2008 *m += 1;
2009 }
2010 }
2011 _ => {
2012 values.push(k);
2013 mults.push(1);
2014 }
2015 }
2016 }
2017 (mults, values)
2018}
2019
2020fn transform_bits(t: &Transform) -> [u64; 3] {
2022 let p = t.apply(Point::new(0.123_456_789, 9.87, -3.21));
2023 [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]
2024}
2025
2026fn location_transform(location: &ogeom_topo::Location, model: &Model) -> OgeomResult<Transform> {
2028 let mut out = Transform::IDENTITY;
2029 for &(datum, power) in location.chain() {
2030 let Some(t) = model.datums().get(datum) else {
2031 ogeom_bail!(Dangling, "an instance placement names a missing datum");
2032 };
2033 let step = if power >= 0 { t } else { t.inverse()? };
2034 for _ in 0..power.unsigned_abs() {
2035 out = out * step;
2036 }
2037 }
2038 Ok(out)
2039}
2040
2041#[cfg(test)]
2042mod escape_tests {
2043 #[test]
2045 fn strings_round_trip_through_part_21_escapes() {
2046 for name in [
2047 "Welle \u{f8}10",
2048 "\u{65e5}\u{672c}",
2049 "it's",
2050 "back\\slash",
2051 "emoji \u{1f600} end",
2052 ] {
2053 let written = super::escape(name);
2054 assert!(written.is_ascii(), "{written}");
2055 let read = super::super::parse::decode_escapes(&written.replace("''", "'"));
2056 assert_eq!(read, name, "{written}");
2057 }
2058 assert_eq!(super::super::parse::decode_escapes("\\X\\F8"), "\u{f8}");
2060 assert_eq!(super::super::parse::decode_escapes("a\\S\\xb"), "a\u{f8}b");
2061 assert_eq!(super::super::parse::decode_escapes("\\PA\\ok"), "ok");
2062 }
2063}