1use ogeom_core::{OgeomResult, Tolerances, ogeom_bail};
16use ogeom_geom::Curve3d as _;
17use ogeom_geom::Surface as _;
18use ogeom_geom::Transformable as _;
19use ogeom_geom::{BSplineCurve, Curve, SurfaceGeometry};
20use ogeom_math::{Frame, Point, Transform};
21use ogeom_topo::{EdgeRepr, Filter, NodeData, Shape, ShapeType, explore};
22use std::collections::HashMap;
23
24pub fn write_iges(document: &ogeom_doc::Document, tol: Tolerances) -> OgeomResult<String> {
32 let mut writer = Writer {
33 model: document.model(),
34 entities: Vec::new(),
35 vertices: HashMap::new(),
36 edges: HashMap::new(),
37 vertex_coords: Vec::new(),
38 edge_records: Vec::new(),
39 tol,
40 };
41
42 let mut solids = Vec::new();
43 for (_, product) in document.products() {
44 let ogeom_doc::ProductKind::Part { shape } = &product.kind else {
45 continue;
46 };
47 for solid in explore(writer.model, shape, Filter::OfType(ShapeType::Solid))? {
48 solids.push((product.name.clone(), solid));
49 }
50 }
51 if solids.is_empty() {
52 ogeom_bail!(Construction, "the document holds no solid to write as IGES");
53 }
54 for (label, solid) in solids {
55 writer.solid(&solid, &label)?;
56 }
57 Ok(writer.serialize())
58}
59
60struct Pending {
62 kind: i64,
63 form: i64,
64 transform: Option<usize>,
66 independent: bool,
68 label: String,
69 params: String,
70}
71
72struct Writer<'a> {
73 model: &'a ogeom_topo::Model,
74 entities: Vec<Pending>,
75 vertices: HashMap<(ogeom_topo::TShapeId, [u64; 3]), usize>,
79 edges: HashMap<(ogeom_topo::TShapeId, [u64; 3]), usize>,
82 vertex_coords: Vec<Point>,
84 edge_records: Vec<(usize, usize, usize)>,
86 tol: Tolerances,
87}
88
89impl Writer<'_> {
90 fn push(&mut self, p: Pending) -> usize {
91 self.entities.push(p);
92 self.entities.len() - 1
93 }
94
95 #[allow(clippy::cast_possible_wrap, reason = "entity counts are small")]
97 fn de(&self, index: usize) -> i64 {
98 2 * index as i64 + 1
99 }
100
101 fn solid(&mut self, solid: &Shape, label: &str) -> OgeomResult<()> {
102 self.vertices.clear();
106 self.edges.clear();
107 self.vertex_coords.clear();
108 self.edge_records.clear();
109
110 let shells = explore(self.model, solid, Filter::OfType(ShapeType::Shell))?;
111 let Some(shell) = shells.first() else {
112 ogeom_bail!(Construction, "a solid with no shell cannot be written");
113 };
114 let mut face_entities = Vec::new();
115 for face in self.model.ordered_children_of(shell)? {
116 face_entities.push((self.face(&face)?, face.orientation()));
117 }
118
119 let vertex_list = {
121 let mut params = format!("{}", self.vertex_coords.len());
122 for p in &self.vertex_coords {
123 params.push_str(&format!(",{},{},{}", fmt(p.x), fmt(p.y), fmt(p.z)));
124 }
125 self.push(Pending {
126 kind: 502,
127 form: 1,
128 transform: None,
129 independent: false,
130 label: String::new(),
131 params,
132 })
133 };
134 let edge_list = {
135 let records = std::mem::take(&mut self.edge_records);
136 let mut params = format!("{}", records.len());
137 let mut curve_des = Vec::new();
138 for (curve_entity, sv, tv) in &records {
139 curve_des.push(self.de(*curve_entity));
140 params.push_str(&format!(",{},@V,{sv},@V,{tv}", self.de(*curve_entity)));
141 }
142 self.push(Pending {
143 kind: 504,
144 form: 1,
145 transform: None,
146 independent: false,
147 label: String::new(),
148 params,
149 })
150 };
151 let vlist_de = self.de(vertex_list);
154 let elist_de = self.de(edge_list);
155 for e in &mut self.entities {
156 if e.kind == 508 || e.kind == 504 {
157 e.params = e.params.replace("@E", &elist_de.to_string());
158 e.params = e.params.replace("@V", &vlist_de.to_string());
159 }
160 }
161
162 let shell_entity = {
163 let mut params = format!("{}", face_entities.len());
164 for (face, orientation) in &face_entities {
165 let flag = i32::from(*orientation != ogeom_topo::Orientation::Reversed);
166 params.push_str(&format!(",{},{flag}", self.de(*face)));
167 }
168 self.push(Pending {
169 kind: 514,
170 form: 1,
171 transform: None,
172 independent: false,
173 label: String::new(),
174 params,
175 })
176 };
177 let shell_de = self.de(shell_entity);
178 self.push(Pending {
179 kind: 186,
180 form: 0,
181 transform: None,
182 independent: true,
183 label: label.chars().take(8).collect(),
184 params: format!("{shell_de},1,0"),
185 });
186 Ok(())
187 }
188
189 fn face(&mut self, face: &Shape) -> OgeomResult<usize> {
190 let placement = face.transform(self.model.datums())?;
191 let surface = {
192 let Some(node) = self.model.node(face) else {
193 ogeom_bail!(Dangling, "face is not in this model");
194 };
195 let NodeData::Face(data) = node.data() else {
196 ogeom_bail!(Construction, "face node holds no face data");
197 };
198 let Some(surface) = self.model.geometry().surface(data.surface) else {
199 ogeom_bail!(Dangling, "face refers to a surface not in this model");
200 };
201 surface.clone().transformed(&placement, self.tol)?
202 };
203 let surface_entity = self.surface(&surface)?;
204 let wires = self.model.ordered_children_of(face)?;
205 let mut loop_entities = Vec::new();
206 for wire in &wires {
207 if let Some(entity) = self.wire(wire)? {
208 loop_entities.push(entity);
209 }
210 }
211 if loop_entities.is_empty() {
212 ogeom_bail!(
213 Construction,
214 "a face whose every boundary is degenerate cannot be written \
215 as IGES; see docs/PARITY.md, io.iges"
216 );
217 }
218 let mut params = format!("{},{},1", self.de(surface_entity), loop_entities.len());
219 for entity in &loop_entities {
220 params.push_str(&format!(",{}", self.de(*entity)));
221 }
222 Ok(self.push(Pending {
223 kind: 510,
224 form: 1,
225 transform: None,
226 independent: false,
227 label: String::new(),
228 params,
229 }))
230 }
231
232 fn wire(&mut self, wire: &Shape) -> OgeomResult<Option<usize>> {
236 let children = self.model.ordered_children_of(wire)?;
237 let degenerate = |edge: &Shape| {
238 self.model
239 .node(edge)
240 .and_then(|n| n.data().as_edge())
241 .is_some_and(|d| d.degenerate)
242 };
243 let live: Vec<&Shape> = children.iter().filter(|e| !degenerate(e)).collect();
244 if live.is_empty() {
245 return Ok(None);
246 }
247 let mut entries = Vec::new();
248 for edge in live {
249 let index = self.edge(edge)?;
250 let flag = i32::from(edge.orientation() != ogeom_topo::Orientation::Reversed);
251 entries.push(format!("0,@E,{index},{flag},0"));
252 }
253 let params = format!("{},{}", entries.len(), entries.join(","));
254 Ok(Some(self.push(Pending {
255 kind: 508,
256 form: 1,
257 transform: None,
258 independent: false,
259 label: String::new(),
260 params,
261 })))
262 }
263
264 fn edge(&mut self, edge: &Shape) -> OgeomResult<usize> {
267 let placement = edge.transform(self.model.datums())?;
268 let key = (edge.node(), transform_bits(&placement));
269 if let Some(&index) = self.edges.get(&key) {
270 return Ok(index);
271 }
272 let (curve, range) = {
273 let Some(data) = self.model.node(edge).and_then(|n| n.data().as_edge()) else {
274 ogeom_bail!(Construction, "edge node holds no edge data");
275 };
276 let Some(EdgeRepr::Curve3d { curve, range, .. }) = data.curve3d() else {
277 ogeom_bail!(Construction, "an edge with no curve cannot be written");
278 };
279 let Some(geometry) = self.model.geometry().curve(*curve) else {
280 ogeom_bail!(Dangling, "curve is not in this model");
281 };
282 (geometry.clone().transformed(&placement, self.tol)?, *range)
283 };
284 let curve_entity = self.curve(&curve, range)?;
285 let vertices = self.model.children_of(edge)?;
286 let (from, to) = match vertices.len() {
287 0 => ogeom_bail!(Construction, "an edge with no vertices cannot be written"),
288 1 => (vertices[0].clone(), vertices[0].clone()),
289 _ => (vertices[0].clone(), vertices[vertices.len() - 1].clone()),
290 };
291 let sv = {
296 let at = from.transform(self.model.datums())?;
297 self.vertex(&from, &at)?
298 };
299 let tv = {
300 let at = to.transform(self.model.datums())?;
301 self.vertex(&to, &at)?
302 };
303 self.edge_records.push((curve_entity, sv, tv));
304 let index = self.edge_records.len();
305 self.edges.insert(key, index);
306 Ok(index)
307 }
308
309 fn vertex(&mut self, vertex: &Shape, placement: &Transform) -> OgeomResult<usize> {
311 let Some(data) = self.model.node(vertex).and_then(|n| n.data().as_vertex()) else {
312 ogeom_bail!(Construction, "vertex node holds no vertex data");
313 };
314 let at = placement.apply(data.point);
315 let key = (
316 vertex.node(),
317 [at.x.to_bits(), at.y.to_bits(), at.z.to_bits()],
318 );
319 if let Some(&index) = self.vertices.get(&key) {
320 return Ok(index);
321 }
322 self.vertex_coords.push(at);
323 let index = self.vertex_coords.len();
324 self.vertices.insert(key, index);
325 Ok(index)
326 }
327
328 fn curve(&mut self, curve: &Curve, range: (f64, f64)) -> OgeomResult<usize> {
330 match curve {
331 Curve::Line(line) => {
332 let a = line.point_at(range.0, self.tol)?;
333 let b = line.point_at(range.1, self.tol)?;
334 Ok(self.push(Pending {
335 kind: 110,
336 form: 0,
337 transform: None,
338 independent: false,
339 label: String::new(),
340 params: format!(
341 "{},{},{},{},{},{}",
342 fmt(a.x),
343 fmt(a.y),
344 fmt(a.z),
345 fmt(b.x),
346 fmt(b.y),
347 fmt(b.z)
348 ),
349 }))
350 }
351 Curve::Circle(c) => {
352 let circle = c.circle();
353 let r = circle.radius();
354 let transform = self.transform_entity(&circle.frame());
355 let (s, e) = (range.0, range.1);
356 let params = format!(
357 "0.,0.,0.,{},{},{},{}",
358 fmt(r * s.cos()),
359 fmt(r * s.sin()),
360 fmt(r * e.cos()),
361 fmt(r * e.sin()),
362 );
363 Ok(self.push(Pending {
364 kind: 100,
365 form: 0,
366 transform: Some(transform),
367 independent: false,
368 label: String::new(),
369 params,
370 }))
371 }
372 Curve::Ellipse(el) => {
373 let ellipse = el.ellipse();
374 let (a, b) = (ellipse.major_radius(), ellipse.minor_radius());
375 let transform = self.transform_entity(&ellipse.frame());
376 let at = |t: f64| (a * t.cos(), b * t.sin());
377 let (sx, sy) = at(range.0);
378 let (ex, ey) = at(range.1);
379 let params = format!(
381 "{},0.,{},0.,0.,-1.,0.,{},{},{},{}",
382 fmt(1.0 / (a * a)),
383 fmt(1.0 / (b * b)),
384 fmt(sx),
385 fmt(sy),
386 fmt(ex),
387 fmt(ey),
388 );
389 Ok(self.push(Pending {
390 kind: 104,
391 form: 1,
392 transform: Some(transform),
393 independent: false,
394 label: String::new(),
395 params,
396 }))
397 }
398 Curve::Trimmed(t) => self.curve(t.basis(), range),
399 Curve::BSpline(b) => self.nurbs_curve(b, range),
400 other => {
401 let bspline = other.to_bspline_over(range, self.tol)?;
404 self.nurbs_curve(&bspline, ogeom_geom::Curve3d::domain(&bspline))
405 }
406 }
407 }
408
409 fn nurbs_curve(&mut self, curve: &BSplineCurve, range: (f64, f64)) -> OgeomResult<usize> {
410 let knots = curve.knots();
411 let control = curve.control_points();
412 let degree = knots.degree();
413 let k = control.len() - 1;
414 let rational = curve.is_rational();
415 let closed = {
416 let (lo, hi) = ogeom_geom::Curve3d::domain(curve);
417 let a = curve.point_at(lo, self.tol)?;
418 let b = curve.point_at(hi, self.tol)?;
419 i32::from(a.distance(b) < self.tol.confusion())
420 };
421 let mut params = format!("{k},{degree},0,{closed},{},0", i32::from(!rational));
422 for t in knots.knots() {
423 params.push_str(&format!(",{}", fmt(*t)));
424 }
425 for w in control {
426 params.push_str(&format!(",{}", fmt(w.weight)));
427 }
428 for w in control {
429 let p = (*w).point();
430 params.push_str(&format!(",{},{},{}", fmt(p.x), fmt(p.y), fmt(p.z)));
431 }
432 params.push_str(&format!(",{},{}", fmt(range.0), fmt(range.1)));
433 Ok(self.push(Pending {
434 kind: 126,
435 form: 0,
436 transform: None,
437 independent: false,
438 label: String::new(),
439 params,
440 }))
441 }
442
443 fn transform_entity(&mut self, frame: &Frame) -> usize {
445 let (x, y, z) = (frame.x().vector(), frame.y().vector(), frame.z().vector());
446 let o = frame.origin();
447 let params = format!(
448 "{},{},{},{},{},{},{},{},{},{},{},{}",
449 fmt(x.x),
450 fmt(y.x),
451 fmt(z.x),
452 fmt(o.x),
453 fmt(x.y),
454 fmt(y.y),
455 fmt(z.y),
456 fmt(o.y),
457 fmt(x.z),
458 fmt(y.z),
459 fmt(z.z),
460 fmt(o.z),
461 );
462 self.push(Pending {
463 kind: 124,
464 form: 0,
465 transform: None,
466 independent: false,
467 label: String::new(),
468 params,
469 })
470 }
471
472 fn point_entity(&mut self, p: Point) -> usize {
474 self.push(Pending {
475 kind: 116,
476 form: 0,
477 transform: None,
478 independent: false,
479 label: String::new(),
480 params: format!("{},{},{},0", fmt(p.x), fmt(p.y), fmt(p.z)),
481 })
482 }
483
484 fn direction_entity(&mut self, d: ogeom_math::Direction) -> usize {
486 let v = d.vector();
487 self.push(Pending {
488 kind: 123,
489 form: 0,
490 transform: None,
491 independent: false,
492 label: String::new(),
493 params: format!("{},{},{}", fmt(v.x), fmt(v.y), fmt(v.z)),
494 })
495 }
496
497 fn surface(&mut self, surface: &SurfaceGeometry) -> OgeomResult<usize> {
499 use SurfaceGeometry as S;
500 match surface {
501 S::Plane(p) => {
502 let frame = p.plane().frame();
503 let point = self.point_entity(frame.origin());
504 let normal = self.direction_entity(frame.z());
505 let params = format!("{},{}", self.de(point), self.de(normal));
506 Ok(self.push(Pending {
507 kind: 190,
508 form: 0,
509 transform: None,
510 independent: false,
511 label: String::new(),
512 params,
513 }))
514 }
515 S::Cylinder(c) => {
516 let cyl = c.cylinder();
517 let frame = cyl.frame();
518 let point = self.point_entity(frame.origin());
519 let axis = self.direction_entity(frame.z());
520 let params = format!("{},{},{}", self.de(point), self.de(axis), fmt(cyl.radius()));
521 Ok(self.push(Pending {
522 kind: 192,
523 form: 0,
524 transform: None,
525 independent: false,
526 label: String::new(),
527 params,
528 }))
529 }
530 S::Cone(c) => {
531 let cone = c.cone();
532 let frame = cone.frame();
533 let point = self.point_entity(frame.origin());
534 let axis = self.direction_entity(frame.z());
535 let params = format!(
536 "{},{},{},{}",
537 self.de(point),
538 self.de(axis),
539 fmt(cone.reference_radius()),
540 fmt(cone.half_angle().to_degrees()),
541 );
542 Ok(self.push(Pending {
543 kind: 194,
544 form: 0,
545 transform: None,
546 independent: false,
547 label: String::new(),
548 params,
549 }))
550 }
551 S::Sphere(s) => {
552 let sphere = s.sphere();
553 let point = self.point_entity(sphere.centre());
554 let params = format!("{},{}", self.de(point), fmt(sphere.radius()));
555 Ok(self.push(Pending {
556 kind: 196,
557 form: 0,
558 transform: None,
559 independent: false,
560 label: String::new(),
561 params,
562 }))
563 }
564 S::Torus(t) => {
565 let torus = t.torus();
566 let frame = torus.frame();
567 let point = self.point_entity(frame.origin());
568 let axis = self.direction_entity(frame.z());
569 let params = format!(
570 "{},{},{},{}",
571 self.de(point),
572 self.de(axis),
573 fmt(torus.major_radius()),
574 fmt(torus.minor_radius()),
575 );
576 Ok(self.push(Pending {
577 kind: 198,
578 form: 0,
579 transform: None,
580 independent: false,
581 label: String::new(),
582 params,
583 }))
584 }
585 S::BSpline(b) => {
586 let (uk, vk) = (b.u_knots(), b.v_knots());
587 let grid = b.grid();
588 let (nu, nv) = (grid.u_count(), grid.v_count());
589 let (k1, k2) = (nu - 1, nv - 1);
590 let (m1, m2) = (uk.degree(), vk.degree());
591 let mut params = format!(
592 "{k1},{k2},{m1},{m2},0,0,{},0,0",
593 i32::from(!b.is_rational())
594 );
595 for t in uk.knots() {
596 params.push_str(&format!(",{}", fmt(*t)));
597 }
598 for t in vk.knots() {
599 params.push_str(&format!(",{}", fmt(*t)));
600 }
601 for v in 0..nv {
603 for u in 0..nu {
604 let w = grid.get(u, v).map_or(1.0, |w| w.weight);
605 params.push_str(&format!(",{}", fmt(w)));
606 }
607 }
608 for v in 0..nv {
609 for u in 0..nu {
610 let p = grid
611 .get(u, v)
612 .map_or(Point::ORIGIN, ogeom_math::Weighted::point);
613 params.push_str(&format!(",{},{},{}", fmt(p.x), fmt(p.y), fmt(p.z)));
614 }
615 }
616 let ((u0, u1), (v0, v1)) = b.domain();
617 params.push_str(&format!(",{},{},{},{}", fmt(u0), fmt(u1), fmt(v0), fmt(v1)));
618 Ok(self.push(Pending {
619 kind: 128,
620 form: 0,
621 transform: None,
622 independent: false,
623 label: String::new(),
624 params,
625 }))
626 }
627 other => {
628 let bspline = other.to_bspline(self.tol)?;
632 self.surface(&SurfaceGeometry::BSpline(bspline))
633 }
634 }
635 }
636
637 fn serialize(&self) -> String {
639 let mut s = String::new();
640 fn push_record(s: &mut String, body: &str, section: char, seq: usize) {
641 s.push_str(&format!("{body:<72}{section}{seq:>7}\n"));
642 }
643 push_record(&mut s, "ogeom IGES writer", 'S', 1);
644
645 let mut param_lines: Vec<Vec<String>> = Vec::with_capacity(self.entities.len());
648 for e in &self.entities {
649 let full = format!("{},{};", e.kind, e.params);
650 param_lines.push(wrap_params(&full));
651 }
652 let mut param_starts = Vec::with_capacity(self.entities.len());
653 let mut next_param = 1usize;
654 for lines in ¶m_lines {
655 param_starts.push(next_param);
656 next_param += lines.len();
657 }
658
659 let globals = [
660 "1H,".to_string(),
661 "1H;".to_string(),
662 "5Hogeom".to_string(),
663 "9Hmodel.igs".to_string(),
664 "5Hogeom".to_string(),
665 "5Hogeom".to_string(),
666 "32".to_string(),
667 "308".to_string(),
668 "15".to_string(),
669 "308".to_string(),
670 "15".to_string(),
671 "5Hogeom".to_string(),
672 "1.".to_string(),
673 "2".to_string(),
675 "2HMM".to_string(),
676 "1".to_string(),
677 "0.01".to_string(),
678 "15H20260807.000000".to_string(),
679 fmt(1e-7),
680 "0.".to_string(),
681 "5Hogeom".to_string(),
682 "5Hogeom".to_string(),
683 "11".to_string(),
684 "0".to_string(),
685 "15H20260807.000000".to_string(),
686 ];
687 let global_text = globals.join(",") + ";";
688 let global_lines = wrap_params(&global_text);
689 for (i, line) in global_lines.iter().enumerate() {
690 push_record(&mut s, line, 'G', i + 1);
691 }
692
693 let mut d_seq = 1usize;
694 for (i, e) in self.entities.iter().enumerate() {
695 let transform_de = e.transform.map_or(0, |t| self.de(t));
696 let status = if e.independent {
697 "00000000"
698 } else {
699 "00010000"
700 };
701 let line1 = format!(
702 "{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{status}",
703 e.kind, param_starts[i], 0, 0, 0, 0, transform_de, 0
704 );
705 push_record(&mut s, &line1, 'D', d_seq);
706 let line2 = format!(
707 "{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}{:>8}",
708 e.kind,
709 0,
710 0,
711 param_lines[i].len(),
712 e.form,
713 "",
714 "",
715 e.label,
716 0
717 );
718 push_record(&mut s, &line2, 'D', d_seq + 1);
719 d_seq += 2;
720 }
721
722 let mut p_seq = 1usize;
723 for (i, lines) in param_lines.iter().enumerate() {
724 let back = self.de(i);
725 for line in lines {
726 s.push_str(&format!("{line:<64}{back:>8}P{p_seq:>7}\n"));
727 p_seq += 1;
728 }
729 }
730 let tail = format!(
731 "S{:>7}G{:>7}D{:>7}P{:>7}",
732 1,
733 global_lines.len(),
734 d_seq - 1,
735 p_seq - 1
736 );
737 push_record(&mut s, &tail, 'T', 1);
738 s
739 }
740}
741
742fn transform_bits(t: &Transform) -> [u64; 3] {
745 let p = t.apply(Point::new(0.123_456_789, 9.87, -3.21));
746 [p.x.to_bits(), p.y.to_bits(), p.z.to_bits()]
747}
748
749fn fmt(v: f64) -> String {
756 let positional = {
757 let s = format!("{v}");
758 if s.contains('.') { s } else { format!("{s}.") }
759 };
760 let exponent = {
761 let s = format!("{v:E}");
762 match s.split_once('E') {
763 Some((mantissa, power)) if !mantissa.contains('.') => {
764 format!("{mantissa}.E{power}")
765 }
766 _ => s,
767 }
768 };
769 if exponent.len() < positional.len() {
770 exponent
771 } else {
772 positional
773 }
774}
775
776fn wrap_params(text: &str) -> Vec<String> {
786 const COLUMNS: usize = 64;
787 let mut lines = Vec::new();
788 let mut current = String::new();
789 for piece in text.split_inclusive(',') {
790 if current.len() + piece.len() > COLUMNS && piece.len() <= COLUMNS {
791 lines.push(std::mem::take(&mut current));
792 }
793 let mut rest = piece;
794 while current.len() + rest.len() > COLUMNS {
795 let mut room = COLUMNS - current.len();
796 while !rest.is_char_boundary(room) {
797 room -= 1;
798 }
799 current.push_str(&rest[..room]);
800 rest = &rest[room..];
801 lines.push(std::mem::take(&mut current));
802 }
803 current.push_str(rest);
804 }
805 if !current.is_empty() {
806 lines.push(current);
807 }
808 lines
809}
810
811#[cfg(test)]
812#[allow(clippy::unwrap_used, reason = "test code")]
813mod tests {
814 use super::{fmt, wrap_params};
815
816 #[test]
817 fn a_real_takes_its_shorter_spelling_and_reads_back() {
818 for v in [
819 0.0,
820 1.0,
821 -2.5,
822 1e-51,
823 1.48776941870366e-51,
824 2.76e-76,
825 1e20,
826 123_456.789,
827 ] {
828 let s = fmt(v);
829 assert!(s.len() <= 24, "{v} spelt {s}");
830 assert!(s.contains('.'), "{s} reads as a real");
831 let back: f64 = s.replace('D', "E").parse().unwrap();
832 assert_eq!(back, v, "{s}");
833 }
834 assert_eq!(fmt(1.0), "1.");
835 assert_eq!(fmt(1e-51), "1.E-51");
836 }
837
838 #[test]
839 fn a_parameter_longer_than_a_record_fills_the_records_it_crosses() {
840 let name = format!("100H{},", "x".repeat(100));
841 let text = format!("128,{name}1.5,2.;");
842 let lines = wrap_params(&text);
843 assert!(lines.iter().all(|l| l.len() <= 64));
844 let long: Vec<&String> = lines.iter().filter(|l| l.contains('x')).collect();
847 for l in &long[..long.len() - 1] {
848 assert_eq!(l.len(), 64);
849 }
850 assert_eq!(lines.concat(), text);
851 }
852}