1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
18use ogeom_doc::{Document, ProductId, ProductKind};
19use ogeom_geom::Transformable as _;
20use ogeom_geom::{Curve, SurfaceGeometry};
21use ogeom_math::{Frame, Point, Transform, Vector};
22use ogeom_topo::{EdgeRepr, Filter, Model, NodeData, Shape, ShapeType, explore};
23use std::collections::HashMap;
24use std::fmt::Write as _;
25
26pub fn write_step(document: &Document, tol: Tolerances) -> OgeomResult<String> {
39 let mut writer = Writer {
40 model: document.model(),
41 entities: Vec::new(),
42 points: HashMap::new(),
43 directions: HashMap::new(),
44 vertices: HashMap::new(),
45 edges: HashMap::new(),
46 written_nodes: Vec::new(),
47 tol,
48 };
49
50 let app = writer.entity("APPLICATION_CONTEXT('automotive design')".into());
51 let _proto = writer.entity(format!(
52 "APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2010,#{app})"
53 ));
54 let lu = writer.entity("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))".into());
55 let au = writer.entity("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))".into());
56 let su = writer.entity("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())".into());
57 let unc = writer.entity(format!(
58 "UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.0E-06),#{lu},'distance_accuracy_value','')"
59 ));
60 let gctx = writer.entity(format!(
61 "(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{unc}))GLOBAL_UNIT_ASSIGNED_CONTEXT((#{lu},#{au},#{su}))REPRESENTATION_CONTEXT('Context','3D'))"
62 ));
63 let pctx = writer.entity(format!("PRODUCT_CONTEXT('',#{app},'mechanical')"));
64 let pdctx = writer.entity(format!(
65 "PRODUCT_DEFINITION_CONTEXT('part definition',#{app},'design')"
66 ));
67
68 let mut pd_of: HashMap<ProductId, u64> = HashMap::new();
71 let mut sr_of: HashMap<ProductId, u64> = HashMap::new();
72 let mut anchor_pds: Option<(u64, u64)> = None;
73 for (id, product) in document.products() {
74 let name = escape(&product.name);
75 let p = writer.entity(format!("PRODUCT('{name}','{name}','',(#{pctx}))"));
76 let formation = writer.entity(format!("PRODUCT_DEFINITION_FORMATION('','',#{p})"));
77 let pd = writer.entity(format!(
78 "PRODUCT_DEFINITION('design','',#{formation},#{pdctx})"
79 ));
80 pd_of.insert(id, pd);
81
82 let world = writer.frame(&Frame::WORLD);
83 let sr = match &product.kind {
84 ProductKind::Part { shape } => {
85 let mut items = vec![world];
86 for solid in writer.solids_of(shape)? {
87 items.push(solid);
88 }
89 let list = items
90 .iter()
91 .map(|i| format!("#{i}"))
92 .collect::<Vec<_>>()
93 .join(",");
94 writer.entity(format!(
95 "ADVANCED_BREP_SHAPE_REPRESENTATION('{name}',({list}),#{gctx})"
96 ))
97 }
98 ProductKind::Assembly { .. } => {
99 writer.entity(format!("SHAPE_REPRESENTATION('{name}',(#{world}),#{gctx})"))
100 }
101 };
102 sr_of.insert(id, sr);
103 let pds = writer.entity(format!("PRODUCT_DEFINITION_SHAPE('','',#{pd})"));
104 writer.entity(format!("SHAPE_DEFINITION_REPRESENTATION(#{pds},#{sr})"));
105 if anchor_pds.is_none() && matches!(product.kind, ProductKind::Part { .. }) {
106 anchor_pds = Some((pds, sr));
107 }
108 }
109
110 let mut usage = 0_usize;
114 for (id, product) in document.products() {
115 let ProductKind::Assembly { children } = &product.kind else {
116 continue;
117 };
118 for instance in children {
119 usage += 1;
120 let designator = instance
121 .name
122 .clone()
123 .unwrap_or_else(|| format!("occurrence-{usage}"));
124 let designator = escape(&designator);
125 let (parent_pd, child_pd) = (pd_of[&id], pd_of[&instance.product]);
126 let (parent_sr, child_sr) = (sr_of[&id], sr_of[&instance.product]);
127 let nauo = writer.entity(format!(
128 "NEXT_ASSEMBLY_USAGE_OCCURRENCE('{designator}','{designator}','',#{parent_pd},#{child_pd},$)"
129 ));
130 let at = location_transform(&instance.location, document.model())?;
134 let placed = writer.placement_frame(&at)?;
135 let world = writer.frame(&Frame::WORLD);
136 let idt = writer.entity(format!(
137 "ITEM_DEFINED_TRANSFORMATION('','',#{world},#{placed})"
138 ));
139 let rr = writer.entity(format!(
140 "(REPRESENTATION_RELATIONSHIP('','',#{child_sr},#{parent_sr})REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#{idt})SHAPE_REPRESENTATION_RELATIONSHIP())"
141 ));
142 let pds = writer.entity(format!("PRODUCT_DEFINITION_SHAPE('','',#{nauo})"));
143 writer.entity(format!(
144 "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#{rr},#{pds})"
145 ));
146 }
147 }
148
149 let mut styled = Vec::new();
152 let node_colours: HashMap<_, _> = document.colours().collect();
153 for (node, step_id) in writer.written_nodes.clone() {
154 if let Some(colour) = node_colours.get(&node) {
155 styled.push(writer.styled_item(step_id, *colour));
156 }
157 }
158 for (id, product) in document.products() {
159 let Some(colour) = product.colour else {
160 continue;
161 };
162 let ProductKind::Part { shape } = &product.kind else {
163 continue;
164 };
165 let _ = id;
166 for (node, step_id) in writer.written_nodes.clone() {
167 if node == shape.node() && !node_colours.contains_key(&node) {
168 styled.push(writer.styled_item(step_id, colour));
169 }
170 }
171 }
172 if !styled.is_empty() {
173 let list = styled
174 .iter()
175 .map(|i| format!("#{i}"))
176 .collect::<Vec<_>>()
177 .join(",");
178 writer.entity(format!(
179 "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',({list}),#{gctx})"
180 ));
181 }
182
183 let pmi = document.pmi();
185 if !pmi.is_empty() {
186 let Some((pds, absr)) = anchor_pds else {
187 ogeom_bail!(
188 Construction,
189 "PMI needs at least one part to anchor its aspects to"
190 );
191 };
192 writer.pmi(pmi, document.views(), pds, absr, lu, au, gctx)?;
193 }
194
195 let mut out = String::new();
196 out.push_str("ISO-10303-21;\nHEADER;\n");
197 out.push_str("FILE_DESCRIPTION(('written by ogeom'),'2;1');\n");
198 out.push_str("FILE_NAME('','',('ogeom'),('ogeom'),'ogeom','ogeom','');\n");
199 out.push_str("FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\n");
200 out.push_str("ENDSEC;\nDATA;\n");
201 for (i, entity) in writer.entities.iter().enumerate() {
202 let _ = writeln!(out, "#{}={entity};", i + 1);
203 }
204 out.push_str("ENDSEC;\nEND-ISO-10303-21;\n");
205 Ok(out)
206}
207
208struct Writer<'a> {
210 model: &'a Model,
211 entities: Vec<String>,
212 points: HashMap<[u64; 3], u64>,
213 directions: HashMap<[u64; 3], u64>,
214 vertices: HashMap<(ogeom_topo::TShapeId, [u64; 3]), u64>,
216 edges: HashMap<(ogeom_topo::TShapeId, [u64; 3]), u64>,
218 written_nodes: Vec<(ogeom_topo::TShapeId, u64)>,
221 tol: Tolerances,
222}
223
224impl Writer<'_> {
225 fn entity(&mut self, text: String) -> u64 {
226 self.entities.push(text);
227 self.entities.len() as u64
228 }
229
230 fn point(&mut self, p: Point) -> u64 {
231 let key = [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()];
232 if let Some(&id) = self.points.get(&key) {
233 return id;
234 }
235 let id = self.entity(format!(
236 "CARTESIAN_POINT('',({},{},{}))",
237 real(p.x),
238 real(p.y),
239 real(p.z)
240 ));
241 self.points.insert(key, id);
242 id
243 }
244
245 fn direction(&mut self, v: Vector) -> u64 {
246 let key = [v.x.to_bits(), v.y.to_bits(), v.z.to_bits()];
247 if let Some(&id) = self.directions.get(&key) {
248 return id;
249 }
250 let id = self.entity(format!(
251 "DIRECTION('',({},{},{}))",
252 real(v.x),
253 real(v.y),
254 real(v.z)
255 ));
256 self.directions.insert(key, id);
257 id
258 }
259
260 fn frame(&mut self, frame: &Frame) -> u64 {
261 let origin = self.point(frame.origin());
262 let z = self.direction(frame.z().vector());
263 let x = self.direction(frame.x().vector());
264 self.entity(format!("AXIS2_PLACEMENT_3D('',#{origin},#{z},#{x})"))
265 }
266
267 fn placement_frame(&mut self, at: &Transform) -> OgeomResult<u64> {
269 let origin = at.apply(Point::ORIGIN);
270 let z = at.apply_vector(Vector::new(0.0, 0.0, 1.0));
271 let x = at.apply_vector(Vector::new(1.0, 0.0, 0.0));
272 let frame = Frame::new(
273 origin,
274 ogeom_math::Direction::new(z, self.tol)?,
275 ogeom_math::Direction::new(x, self.tol)?,
276 self.tol,
277 )
278 .map_err(|_| {
279 ogeom_core::ogeom_err!(
280 Construction,
281 "an instance placement is not rigid; STEP cannot state it"
282 )
283 })?;
284 Ok(self.frame(&frame))
285 }
286
287 fn solids_of(&mut self, shape: &Shape) -> OgeomResult<Vec<u64>> {
289 let mut out = Vec::new();
290 for solid in explore(self.model, shape, Filter::OfType(ShapeType::Solid))? {
291 out.push(self.solid(&solid)?);
292 }
293 if out.is_empty() {
294 ogeom_bail!(Construction, "a part's shape holds no solid to write");
295 }
296 Ok(out)
297 }
298
299 fn solid(&mut self, solid: &Shape) -> OgeomResult<u64> {
300 let shells = explore(self.model, solid, Filter::OfType(ShapeType::Shell))?;
301 let Some(shell) = shells.first() else {
302 ogeom_bail!(Construction, "a solid with no shell cannot be written");
303 };
304 let mut faces = Vec::new();
305 for face in self.model.ordered_children_of(shell)? {
306 faces.push(self.face(&face)?);
307 }
308 let list = faces
309 .iter()
310 .map(|i| format!("#{i}"))
311 .collect::<Vec<_>>()
312 .join(",");
313 let shell_id = self.entity(format!("CLOSED_SHELL('',({list}))"));
314 let msb = self.entity(format!("MANIFOLD_SOLID_BREP('',#{shell_id})"));
315 self.written_nodes.push((solid.node(), msb));
316 Ok(msb)
317 }
318
319 fn face(&mut self, face: &Shape) -> OgeomResult<u64> {
320 let placement = face.transform(self.model.datums())?;
321 let surface = {
322 let Some(node) = self.model.node(face) else {
323 ogeom_bail!(Dangling, "face is not in this model");
324 };
325 let NodeData::Face(data) = node.data() else {
326 ogeom_bail!(Construction, "face node holds no face data");
327 };
328 let Some(surface) = self.model.geometry().surface(data.surface) else {
329 ogeom_bail!(Dangling, "face refers to a surface not in this model");
330 };
331 surface.clone().transformed(&placement, self.tol)?
332 };
333 let surface_id = self.surface(&surface)?;
334
335 let mut bounds = Vec::new();
336 for (index, wire) in self.model.ordered_children_of(face)?.iter().enumerate() {
337 let keyword = if index == 0 {
338 "FACE_OUTER_BOUND"
339 } else {
340 "FACE_BOUND"
341 };
342 let loop_id = self.wire(wire)?;
343 bounds.push(self.entity(format!("{keyword}('',#{loop_id},.T.)")));
344 }
345 let list = bounds
346 .iter()
347 .map(|i| format!("#{i}"))
348 .collect::<Vec<_>>()
349 .join(",");
350 let sense = if face.orientation() == ogeom_topo::Orientation::Reversed {
351 ".F."
352 } else {
353 ".T."
354 };
355 let id = self.entity(format!("ADVANCED_FACE('',({list}),#{surface_id},{sense})"));
356 self.written_nodes.push((face.node(), id));
357 Ok(id)
358 }
359
360 fn wire(&mut self, wire: &Shape) -> OgeomResult<u64> {
364 let children = self.model.ordered_children_of(wire)?;
365 let degenerate = |edge: &Shape| {
366 self.model
367 .node(edge)
368 .and_then(|n| n.data().as_edge())
369 .is_some_and(|d| d.degenerate)
370 };
371 if !children.is_empty() && children.iter().all(degenerate) {
372 let edge = &children[0];
373 let Some(vertex) = self.model.children_of(edge)?.first().cloned() else {
374 ogeom_bail!(Construction, "a degenerate edge has no vertex");
375 };
376 let vertex_id = self.vertex(&vertex, &edge.transform(self.model.datums())?)?;
377 return Ok(self.entity(format!("VERTEX_LOOP('',#{vertex_id})")));
378 }
379 let mut oriented = Vec::new();
380 for edge in &children {
381 if degenerate(edge) {
382 continue;
383 }
384 let edge_id = self.edge(edge)?;
385 let sense = if edge.orientation() == ogeom_topo::Orientation::Reversed {
386 ".F."
387 } else {
388 ".T."
389 };
390 oriented.push(self.entity(format!("ORIENTED_EDGE('',*,*,#{edge_id},{sense})")));
391 }
392 let list = oriented
393 .iter()
394 .map(|i| format!("#{i}"))
395 .collect::<Vec<_>>()
396 .join(",");
397 Ok(self.entity(format!("EDGE_LOOP('',({list}))")))
398 }
399
400 fn edge(&mut self, edge: &Shape) -> OgeomResult<u64> {
401 let placement = edge.transform(self.model.datums())?;
402 let key = (edge.node(), transform_bits(&placement));
403 if let Some(&id) = self.edges.get(&key) {
404 return Ok(id);
405 }
406 let (curve, range) = {
407 let Some(node) = self.model.node(edge) else {
408 ogeom_bail!(Dangling, "edge is not in this model");
409 };
410 let Some(data) = node.data().as_edge() else {
411 ogeom_bail!(Construction, "edge node holds no edge data");
412 };
413 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
414 ogeom_bail!(Construction, "an edge with no curve cannot be written");
415 };
416 let Some(geometry) = self.model.geometry().curve(*curve) else {
417 ogeom_bail!(Dangling, "curve is not in this model");
418 };
419 (geometry.clone().transformed(&placement, self.tol)?, *range)
420 };
421 let curve_id = self.curve(&curve, range)?;
422 let vertices = self.model.children_of(edge)?;
423 let (from, to) = match vertices.len() {
424 0 => ogeom_bail!(Construction, "an edge with no vertices cannot be written"),
425 1 => (vertices[0].clone(), vertices[0].clone()),
426 _ => (vertices[0].clone(), vertices[vertices.len() - 1].clone()),
427 };
428 let from_placement = from.transform(self.model.datums())?;
432 let to_placement = to.transform(self.model.datums())?;
433 let from_id = self.vertex(&from, &from_placement)?;
434 let to_id = self.vertex(&to, &to_placement)?;
435 let id = self.entity(format!(
436 "EDGE_CURVE('',#{from_id},#{to_id},#{curve_id},.T.)"
437 ));
438 self.edges.insert(key, id);
439 Ok(id)
440 }
441
442 fn vertex(&mut self, vertex: &Shape, placement: &Transform) -> OgeomResult<u64> {
443 let Some(data) = self.model.node(vertex).and_then(|n| n.data().as_vertex()) else {
444 ogeom_bail!(Construction, "vertex node holds no vertex data");
445 };
446 let at = placement.apply(data.point);
447 let key = (
448 vertex.node(),
449 [at.x.to_bits(), at.y.to_bits(), at.z.to_bits()],
450 );
451 if let Some(&id) = self.vertices.get(&key) {
452 return Ok(id);
453 }
454 let point = self.point(at);
455 let id = self.entity(format!("VERTEX_POINT('',#{point})"));
456 self.vertices.insert(key, id);
457 Ok(id)
458 }
459
460 fn curve(&mut self, curve: &Curve, range: (f64, f64)) -> OgeomResult<u64> {
463 match curve {
464 Curve::Line(line) => {
465 let axis = line.axis();
466 let origin = self.point(axis.location);
467 let d = self.direction(axis.direction.vector());
468 let vector = self.entity(format!("VECTOR('',#{d},1.0)"));
469 Ok(self.entity(format!("LINE('',#{origin},#{vector})")))
470 }
471 Curve::Circle(c) => {
472 let circle = c.circle();
473 let frame = self.frame(&circle.frame());
474 Ok(self.entity(format!("CIRCLE('',#{frame},{})", real(circle.radius()))))
475 }
476 Curve::Ellipse(el) => {
477 let ellipse = el.ellipse();
478 let frame = self.frame(&ellipse.frame());
479 Ok(self.entity(format!(
480 "ELLIPSE('',#{frame},{},{})",
481 real(ellipse.major_radius()),
482 real(ellipse.minor_radius())
483 )))
484 }
485 Curve::BSpline(b) => self.bspline_curve(b),
486 Curve::Hyperbola(h) if !h.is_reversed() => {
489 let hyperbola = h.hyperbola();
490 let frame = self.frame(&hyperbola.frame());
491 Ok(self.entity(format!(
492 "HYPERBOLA('',#{frame},{},{})",
493 real(hyperbola.major_radius()),
494 real(hyperbola.minor_radius())
495 )))
496 }
497 Curve::Parabola(p) if !p.is_reversed() => {
498 let parabola = p.parabola();
499 let frame = self.frame(¶bola.frame());
500 Ok(self.entity(format!("PARABOLA('',#{frame},{})", real(parabola.focal()))))
501 }
502 Curve::Offset(o) => {
503 let basis = self.curve(o.basis(), range)?;
504 let reference = self.direction(o.reference().vector());
505 Ok(self.entity(format!(
506 "OFFSET_CURVE_3D('',#{basis},{},.F.,#{reference})",
507 real(o.distance())
508 )))
509 }
510 other => {
511 let spline = other.to_bspline_over(range, self.tol)?;
516 self.bspline_curve(&spline)
517 }
518 }
519 }
520
521 fn bspline_curve(&mut self, spline: &ogeom_geom::BSplineCurve) -> OgeomResult<u64> {
522 let control: Vec<String> = spline
523 .control_points()
524 .iter()
525 .map(|c| {
526 let p = Point::from_vector(c.scaled.to_vector() / c.weight);
527 format!("#{}", self.point(p))
528 })
529 .collect();
530 let (mults, knots) = compress_knots(spline.knots().knots());
531 let degree = spline.knots().degree();
532 let rational = spline
533 .control_points()
534 .iter()
535 .any(|c| (c.weight - 1.0).abs() > 1e-12);
536 let control = control.join(",");
537 let mults = mults
538 .iter()
539 .map(ToString::to_string)
540 .collect::<Vec<_>>()
541 .join(",");
542 let knots = knots.iter().map(|k| real(*k)).collect::<Vec<_>>().join(",");
543 if rational {
544 let weights = spline
545 .control_points()
546 .iter()
547 .map(|c| real(c.weight))
548 .collect::<Vec<_>>()
549 .join(",");
550 Ok(self.entity(format!(
551 "(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(''))"
552 )))
553 } else {
554 Ok(self.entity(format!(
555 "B_SPLINE_CURVE_WITH_KNOTS('',{degree},({control}),.UNSPECIFIED.,.F.,.F.,({mults}),({knots}),.UNSPECIFIED.)"
556 )))
557 }
558 }
559
560 fn surface(&mut self, surface: &SurfaceGeometry) -> OgeomResult<u64> {
564 match surface {
565 SurfaceGeometry::Plane(s) => {
566 let frame = self.frame(&s.plane().frame());
567 Ok(self.entity(format!("PLANE('',#{frame})")))
568 }
569 SurfaceGeometry::Cylinder(s) => {
570 let cylinder = s.cylinder();
571 let frame = self.frame(&cylinder.frame());
572 Ok(self.entity(format!(
573 "CYLINDRICAL_SURFACE('',#{frame},{})",
574 real(cylinder.radius())
575 )))
576 }
577 SurfaceGeometry::Cone(s) => {
578 let cone = s.cone();
579 let frame = self.frame(&cone.frame());
580 Ok(self.entity(format!(
581 "CONICAL_SURFACE('',#{frame},{},{})",
582 real(cone.radius_at(0.0)),
583 real(cone.half_angle())
584 )))
585 }
586 SurfaceGeometry::Sphere(s) => {
587 let sphere = s.sphere();
588 let frame = self.frame(&sphere.frame());
589 Ok(self.entity(format!(
590 "SPHERICAL_SURFACE('',#{frame},{})",
591 real(sphere.radius())
592 )))
593 }
594 SurfaceGeometry::Torus(s) => {
595 let torus = s.torus();
596 let frame = self.frame(&torus.frame());
597 Ok(self.entity(format!(
598 "TOROIDAL_SURFACE('',#{frame},{},{})",
599 real(torus.major_radius()),
600 real(torus.minor_radius())
601 )))
602 }
603 SurfaceGeometry::BSpline(b) => self.bspline_surface(b),
604 SurfaceGeometry::Trimmed(t) => self.surface(t.basis()),
605 SurfaceGeometry::Revolution(r) => {
608 let curve = r.curve();
609 let range = ogeom_geom::Curve3d::domain(curve);
610 let swept = self.curve(curve, range)?;
611 let axis = r.axis();
612 let location = self.point(axis.location);
613 let direction = self.direction(axis.direction.vector());
614 let placement =
615 self.entity(format!("AXIS1_PLACEMENT('',#{location},#{direction})"));
616 Ok(self.entity(format!("SURFACE_OF_REVOLUTION('',#{swept},#{placement})")))
617 }
618 SurfaceGeometry::Extrusion(e) => {
619 let curve = e.curve();
620 let range = ogeom_geom::Curve3d::domain(curve);
621 let swept = self.curve(curve, range)?;
622 let direction = self.direction(e.direction().vector());
623 let vector = self.entity(format!("VECTOR('',#{direction},1.0)"));
624 Ok(self.entity(format!(
625 "SURFACE_OF_LINEAR_EXTRUSION('',#{swept},#{vector})"
626 )))
627 }
628 SurfaceGeometry::Offset(o) => {
629 let basis = self.surface(o.basis())?;
630 Ok(self.entity(format!(
631 "OFFSET_SURFACE('',#{basis},{},.F.)",
632 real(o.distance())
633 )))
634 }
635 }
636 }
637
638 fn bspline_surface(&mut self, patch: &ogeom_geom::BSplineSurface) -> OgeomResult<u64> {
639 let grid = patch.grid();
640 let (nu, nv) = (grid.u_count(), grid.v_count());
641 let mut rows = Vec::with_capacity(nu);
642 let mut weights_rows = Vec::with_capacity(nu);
643 let mut rational = false;
644 for u in 0..nu {
645 let mut row = Vec::with_capacity(nv);
646 let mut wrow = Vec::with_capacity(nv);
647 for v in 0..nv {
648 let Some(c) = grid.get(u, v) else {
649 ogeom_bail!(Construction, "a control grid cell is missing");
650 };
651 let p = Point::from_vector(c.scaled.to_vector() / c.weight);
652 row.push(format!("#{}", self.point(p)));
653 wrow.push(real(c.weight));
654 rational |= (c.weight - 1.0).abs() > 1e-12;
655 }
656 rows.push(format!("({})", row.join(",")));
657 weights_rows.push(format!("({})", wrow.join(",")));
658 }
659 let grid_text = rows.join(",");
660 let (u_deg, v_deg) = (patch.u_knots().degree(), patch.v_knots().degree());
661 let (um, uk) = compress_knots(patch.u_knots().knots());
662 let (vm, vk) = compress_knots(patch.v_knots().knots());
663 let fmt_m = |m: &[usize]| {
664 m.iter()
665 .map(ToString::to_string)
666 .collect::<Vec<_>>()
667 .join(",")
668 };
669 let fmt_k = |k: &[f64]| k.iter().map(|v| real(*v)).collect::<Vec<_>>().join(",");
670 let (um, uk, vm, vk) = (fmt_m(&um), fmt_k(&uk), fmt_m(&vm), fmt_k(&vk));
671 if rational {
672 let weights = weights_rows.join(",");
673 Ok(self.entity(format!(
674 "(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())"
675 )))
676 } else {
677 Ok(self.entity(format!(
678 "B_SPLINE_SURFACE_WITH_KNOTS('',{u_deg},{v_deg},({grid_text}),.UNSPECIFIED.,.F.,.F.,.F.,({um}),({vm}),({uk}),({vk}),.UNSPECIFIED.)"
679 )))
680 }
681 }
682
683 #[allow(clippy::many_single_char_names)]
685 #[allow(
686 clippy::too_many_arguments,
687 reason = "five STEP context ids that travel together"
688 )]
689 fn pmi(
690 &mut self,
691 pmi: &ogeom_doc::Pmi,
692 views: &[ogeom_doc::View],
693 pds: u64,
694 absr: u64,
695 lu: u64,
696 au: u64,
697 gctx: u64,
698 ) -> OgeomResult<()> {
699 let by_node: HashMap<ogeom_topo::TShapeId, u64> =
700 self.written_nodes.iter().copied().collect();
701 let aspect_for = |w: &mut Self, items: &[ogeom_topo::TShapeId]| -> u64 {
702 let aspect = w.entity(format!("SHAPE_ASPECT('','',#{pds},.T.)"));
703 for item in items {
704 if let Some(&step_id) = by_node.get(item) {
705 w.entity(format!(
706 "GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#{aspect},#{absr},#{step_id})"
707 ));
708 }
709 }
710 aspect
711 };
712
713 let mut annotation_ids: HashMap<ogeom_doc::Annotated, u64> = HashMap::new();
716 let mut datum_ids: HashMap<&str, u64> = HashMap::new();
717 for datum in &pmi.datums {
718 let label = escape(&datum.label);
719 let id = self.entity(format!("DATUM('','',#{pds},.F.,'{label}')"));
720 for item in &datum.items {
721 if let Some(&step_id) = by_node.get(item) {
722 self.entity(format!(
723 "GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#{id},#{absr},#{step_id})"
724 ));
725 }
726 }
727 datum_ids.insert(datum.label.as_str(), id);
728 annotation_ids.insert(ogeom_doc::Annotated::Datum(datum_ids.len() - 1), id);
729 }
730
731 for (at, dimension) in pmi.dimensions.iter().enumerate() {
732 let name = escape(&dimension.name);
733 let angular = dimension.kind == ogeom_doc::MeasureKind::Angle;
734 let dim = if dimension.location {
735 let empty = Vec::new();
738 let first = dimension.features.first().unwrap_or(&empty);
739 let second = dimension.features.get(1).unwrap_or(first);
740 let a = aspect_for(self, first);
741 let b = aspect_for(self, second);
742 if angular {
743 self.entity(format!("ANGULAR_LOCATION('{name}','',#{a},#{b},.EQUAL.)"))
744 } else {
745 self.entity(format!("DIMENSIONAL_LOCATION('{name}','',#{a},#{b})"))
746 }
747 } else {
748 let empty = Vec::new();
749 let items = dimension.features.first().unwrap_or(&empty);
750 let aspect = aspect_for(self, items);
751 if angular {
752 self.entity(format!("ANGULAR_SIZE(#{aspect},'{name}',.EQUAL.)"))
753 } else {
754 self.entity(format!("DIMENSIONAL_SIZE(#{aspect},'{name}')"))
755 }
756 };
757 let measures: Vec<String> = dimension
758 .values
759 .iter()
760 .map(|&v| format!("#{}", self.measure(v, dimension.kind, lu, au)))
761 .collect();
762 let list = measures.join(",");
763 annotation_ids.insert(ogeom_doc::Annotated::Dimension(at), dim);
764 let sdr = self.entity(format!(
765 "SHAPE_DIMENSION_REPRESENTATION('',({list}),#{gctx})"
766 ));
767 self.entity(format!(
768 "DIMENSIONAL_CHARACTERISTIC_REPRESENTATION(#{dim},#{sdr})"
769 ));
770 if dimension.plus.is_some() || dimension.minus.is_some() {
771 let lower = self.measure(dimension.minus.unwrap_or(0.0), dimension.kind, lu, au);
772 let upper = self.measure(dimension.plus.unwrap_or(0.0), dimension.kind, lu, au);
773 let tv = self.entity(format!("TOLERANCE_VALUE(#{lower},#{upper})"));
774 self.entity(format!("PLUS_MINUS_TOLERANCE(#{tv},#{dim})"));
775 }
776 }
777
778 for (at, tolerance) in pmi.tolerances.iter().enumerate() {
779 let aspect = aspect_for(self, &tolerance.items);
780 let name = escape(&tolerance.name);
781 let magnitude =
782 self.measure(tolerance.magnitude, ogeom_doc::MeasureKind::Length, lu, au);
783 let keyword = format!("{}_TOLERANCE", tolerance.kind.to_uppercase());
784 let refs: Vec<String> = tolerance
788 .datums
789 .iter()
790 .filter_map(|d| {
791 if let Some(id) = datum_ids.get(d.as_str()) {
792 return Some(format!("#{id}"));
793 }
794 let parts: Vec<String> = d
795 .split('-')
796 .filter_map(|label| datum_ids.get(label))
797 .map(|i| format!("#{i}"))
798 .collect();
799 if parts.len() < 2 {
800 return None;
801 }
802 let list = parts.join(",");
803 let compartment = self.entity(format!(
804 "DATUM_REFERENCE_COMPARTMENT('','',#{pds},.F.,({list}),())"
805 ));
806 Some(format!("#{compartment}"))
807 })
808 .collect();
809 let refs = refs.join(",");
810 let written = if tolerance.modifiers.is_empty() {
811 if refs.is_empty() {
812 self.entity(format!("{keyword}('{name}','',#{magnitude},#{aspect})"))
813 } else {
814 self.entity(format!(
815 "{keyword}('{name}','',#{magnitude},#{aspect},({refs}))"
816 ))
817 }
818 } else {
819 let words: Vec<String> = tolerance
823 .modifiers
824 .iter()
825 .map(|m| format!(".{}.", m.to_uppercase()))
826 .collect();
827 let words = words.join(",");
828 let mut parts = vec![
829 format!("GEOMETRIC_TOLERANCE('{name}','',#{magnitude},#{aspect})"),
830 format!("GEOMETRIC_TOLERANCE_WITH_MODIFIERS(({words}))"),
831 format!("{keyword}()"),
832 ];
833 if !refs.is_empty() {
834 parts.push(format!(
835 "GEOMETRIC_TOLERANCE_WITH_DATUM_REFERENCE(({refs}))"
836 ));
837 }
838 parts.sort();
839 self.entity(format!("({})", parts.concat()))
840 };
841 annotation_ids.insert(ogeom_doc::Annotated::Tolerance(at), written);
842 }
843
844 for target in &pmi.targets {
849 let identifier = escape(&target.identifier());
850 let description = match target.kind {
851 ogeom_doc::DatumTargetKind::Point => "point",
852 ogeom_doc::DatumTargetKind::Line { .. } => "line",
853 ogeom_doc::DatumTargetKind::Rectangle { .. } => "rectangle",
854 ogeom_doc::DatumTargetKind::Circle { .. } => "circle",
855 };
856 let id = self.entity(format!(
857 "PLACED_DATUM_TARGET_FEATURE('','{description}',#{pds},.F.,'{identifier}')"
858 ));
859 for item in &target.items {
860 if let Some(&step_id) = by_node.get(item) {
861 self.entity(format!(
862 "GEOMETRIC_ITEM_SPECIFIC_USAGE('','',#{id},#{absr},#{step_id})"
863 ));
864 }
865 }
866 if let Some(&datum_id) = datum_ids.get(target.datum.as_str()) {
869 self.entity(format!(
870 "SHAPE_ASPECT_RELATIONSHIP('','',#{datum_id},#{id})"
871 ));
872 }
873 let frame = target
874 .frame
875 .unwrap_or_else(|| Frame::about(target.at, ogeom_math::Direction::Z));
876 let placement = self.frame(&frame);
877 let sizes: Vec<f64> = match target.kind {
878 ogeom_doc::DatumTargetKind::Point => Vec::new(),
879 ogeom_doc::DatumTargetKind::Line { length } => vec![length],
880 ogeom_doc::DatumTargetKind::Rectangle { length, width } => vec![length, width],
881 ogeom_doc::DatumTargetKind::Circle { diameter } => vec![diameter],
882 };
883 let mut items = vec![format!("#{placement}")];
884 for size in sizes {
885 let measure = self.measure(size, ogeom_doc::MeasureKind::Length, lu, au);
886 items.push(format!("#{measure}"));
887 }
888 let list = items.join(",");
889 let rep = self.entity(format!(
890 "SHAPE_REPRESENTATION('{identifier}',({list}),#{gctx})"
891 ));
892 let property = self.entity(format!("PROPERTY_DEFINITION('','',#{id})"));
893 self.entity(format!(
894 "SHAPE_DEFINITION_REPRESENTATION(#{property},#{rep})"
895 ));
896 }
897
898 let mut callout_ids: Vec<u64> = Vec::new();
903 if !pmi.callouts.is_empty() {
904 let mut drawn: Vec<(u64, ogeom_doc::Annotated)> = Vec::new();
905 let mut planes: Vec<String> = Vec::new();
906 for callout in &pmi.callouts {
907 let name = escape(&callout.name);
908 let mut coordinates: Vec<String> = Vec::new();
909 let mut lines: Vec<String> = Vec::new();
910 let mut next = 1_usize;
911 for polyline in &callout.polylines {
912 let mut indices: Vec<String> = Vec::with_capacity(polyline.len());
913 for p in polyline {
914 coordinates.push(format!("({},{},{})", real(p.x), real(p.y), real(p.z)));
915 indices.push(next.to_string());
916 next += 1;
917 }
918 lines.push(format!("({})", indices.join(",")));
919 }
920 let count = coordinates.len();
921 let coordinates = coordinates.join(",");
922 let list = self.entity(format!(
923 "COORDINATES_LIST('{name}',{count},({coordinates}))"
924 ));
925 let lines = lines.join(",");
926 let set = self.entity(format!("TESSELLATED_CURVE_SET('{name}',#{list},({lines}))"));
927 let occurrence = self.entity(format!(
931 "TESSELLATED_ANNOTATION_OCCURRENCE('{name}',(),#{set})"
932 ));
933 let id = self.entity(format!("DRAUGHTING_CALLOUT('{name}',(#{occurrence}))"));
934 callout_ids.push(id);
935 if let Some(frame) = callout.plane {
936 let placement = self.frame(&frame);
937 let plane = self.entity(format!("PLANE('{name}',#{placement})"));
938 planes.push(
939 self.entity(format!("ANNOTATION_PLANE('{name}',(),#{plane},(#{id}))"))
940 .to_string(),
941 );
942 }
943 if let Some(annotates) = callout.annotates {
944 drawn.push((id, annotates));
945 }
946 }
947 let list: Vec<String> = planes.iter().map(|p| format!("#{p}")).collect();
948 let list = list.join(",");
949 let model = self.entity(format!("DRAUGHTING_MODEL('',({list}),#{gctx})"));
950 for (callout, annotates) in drawn {
951 let Some(&annotation) = annotation_ids.get(&annotates) else {
952 continue;
953 };
954 self.entity(format!(
955 "DRAUGHTING_MODEL_ITEM_ASSOCIATION('PMI representation to presentation \
956 link','',#{annotation},#{model},#{callout})"
957 ));
958 }
959 }
960
961 for view in views {
967 let name = escape(&view.name);
968 let placement = self.frame(&view.frame);
969 let camera = self.entity(format!("CAMERA_MODEL_D3('{name}',#{placement},$)"));
970 let mut items = vec![format!("#{camera}")];
971 for &index in &view.callouts {
972 if let Some(id) = callout_ids.get(index) {
973 items.push(format!("#{id}"));
974 }
975 }
976 let items = items.join(",");
977 self.entity(format!("DRAUGHTING_MODEL('{name}',({items}),#{gctx})"));
978 }
979 Ok(())
980 }
981
982 fn measure(&mut self, value: f64, kind: ogeom_doc::MeasureKind, lu: u64, au: u64) -> u64 {
984 let v = real(value);
985 match kind {
986 ogeom_doc::MeasureKind::Length => self.entity(format!(
987 "(LENGTH_MEASURE_WITH_UNIT()MEASURE_REPRESENTATION_ITEM()MEASURE_WITH_UNIT(LENGTH_MEASURE({v}),#{lu})REPRESENTATION_ITEM(''))"
988 )),
989 ogeom_doc::MeasureKind::Angle => self.entity(format!(
990 "(MEASURE_REPRESENTATION_ITEM()MEASURE_WITH_UNIT(PLANE_ANGLE_MEASURE({v}),#{au})PLANE_ANGLE_MEASURE_WITH_UNIT()REPRESENTATION_ITEM(''))"
991 )),
992 }
993 }
994
995 fn styled_item(&mut self, item: u64, colour: ogeom_doc::Colour) -> u64 {
997 let rgb = self.entity(format!(
998 "COLOUR_RGB('',{},{},{})",
999 real(colour.r),
1000 real(colour.g),
1001 real(colour.b)
1002 ));
1003 let fasc = self.entity(format!("FILL_AREA_STYLE_COLOUR('',#{rgb})"));
1004 let fas = self.entity(format!("FILL_AREA_STYLE('',(#{fasc}))"));
1005 let ssfa = self.entity(format!("SURFACE_STYLE_FILL_AREA(#{fas})"));
1006 let sss = self.entity(format!("SURFACE_SIDE_STYLE('',(#{ssfa}))"));
1007 let ssu = self.entity(format!("SURFACE_STYLE_USAGE(.BOTH.,#{sss})"));
1008 let psa = self.entity(format!("PRESENTATION_STYLE_ASSIGNMENT((#{ssu}))"));
1009 self.entity(format!("STYLED_ITEM('',(#{psa}),#{item})"))
1010 }
1011}
1012
1013fn real(v: f64) -> String {
1016 let mut s = format!("{v:?}");
1017 if let Some(e) = s.find(['e', 'E']) {
1018 let (mantissa, exponent) = s.split_at(e);
1019 let mut m = mantissa.to_string();
1020 if !m.contains('.') {
1021 m.push_str(".0");
1022 }
1023 s = format!("{m}E{}", &exponent[1..]);
1024 } else if !s.contains('.') {
1025 s.push_str(".0");
1026 }
1027 s
1028}
1029
1030fn escape(s: &str) -> String {
1032 s.replace('\'', "''")
1033}
1034
1035fn compress_knots(knots: &[f64]) -> (Vec<usize>, Vec<f64>) {
1037 let mut mults = Vec::new();
1038 let mut values = Vec::new();
1039 for &k in knots {
1040 match values.last() {
1041 Some(&last) if k == last => {
1042 if let Some(m) = mults.last_mut() {
1043 *m += 1;
1044 }
1045 }
1046 _ => {
1047 values.push(k);
1048 mults.push(1);
1049 }
1050 }
1051 }
1052 (mults, values)
1053}
1054
1055fn transform_bits(t: &Transform) -> [u64; 3] {
1057 let p = t.apply(Point::new(0.123_456_789, 9.87, -3.21));
1058 [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]
1059}
1060
1061fn location_transform(location: &ogeom_topo::Location, model: &Model) -> OgeomResult<Transform> {
1063 let mut out = Transform::IDENTITY;
1064 for &(datum, power) in location.chain() {
1065 let Some(t) = model.datums().get(datum) else {
1066 ogeom_bail!(Dangling, "an instance placement names a missing datum");
1067 };
1068 let step = if power >= 0 { t } else { t.inverse()? };
1069 for _ in 0..power.unsigned_abs() {
1070 out = out * step;
1071 }
1072 }
1073 Ok(out)
1074}