1mod entities_out;
18
19pub use entities_out::{DxfItem, DxfLayer, DxfOut, DxfShape, DxfUnit, write_dxf_entities};
20
21use ogeom_math::{Point2, Vector2};
22use std::fmt::Write as _;
23
24#[must_use]
30pub fn write_dxf(visible: &[Vec<Point2>], hidden: &[Vec<Point2>]) -> String {
31 let mut out = String::new();
32 push(
34 &mut out,
35 &[("0", "SECTION"), ("2", "HEADER"), ("0", "ENDSEC")],
36 );
37
38 push(&mut out, &[("0", "SECTION"), ("2", "TABLES")]);
40 push(&mut out, &[("0", "TABLE"), ("2", "LTYPE"), ("70", "2")]);
41 push(
42 &mut out,
43 &[
44 ("0", "LTYPE"),
45 ("2", "CONTINUOUS"),
46 ("70", "0"),
47 ("3", "Solid line"),
48 ("72", "65"),
49 ("73", "0"),
50 ("40", "0.0"),
51 ],
52 );
53 push(
54 &mut out,
55 &[
56 ("0", "LTYPE"),
57 ("2", "DASHED"),
58 ("70", "0"),
59 ("3", "Dashed line"),
60 ("72", "65"),
61 ("73", "2"),
62 ("40", "0.75"),
63 ("49", "0.5"),
64 ("49", "-0.25"),
65 ],
66 );
67 push(&mut out, &[("0", "ENDTAB")]);
68 push(&mut out, &[("0", "TABLE"), ("2", "LAYER"), ("70", "2")]);
69 push(
70 &mut out,
71 &[
72 ("0", "LAYER"),
73 ("2", "VISIBLE"),
74 ("70", "0"),
75 ("62", "7"),
76 ("6", "CONTINUOUS"),
77 ],
78 );
79 push(
80 &mut out,
81 &[
82 ("0", "LAYER"),
83 ("2", "HIDDEN"),
84 ("70", "0"),
85 ("62", "8"),
86 ("6", "DASHED"),
87 ],
88 );
89 push(&mut out, &[("0", "ENDTAB"), ("0", "ENDSEC")]);
90
91 push(&mut out, &[("0", "SECTION"), ("2", "ENTITIES")]);
92 for (layer, curves) in [("VISIBLE", visible), ("HIDDEN", hidden)] {
93 for curve in curves {
94 if curve.len() < 2 {
95 continue;
96 }
97 push(
98 &mut out,
99 &[("0", "POLYLINE"), ("8", layer), ("66", "1"), ("70", "0")],
100 );
101 for p in curve {
102 push(&mut out, &[("0", "VERTEX"), ("8", layer)]);
103 let _ = writeln!(out, "10\n{}\n20\n{}\n30\n0.0", real(p.x), real(p.y));
104 }
105 push(&mut out, &[("0", "SEQEND")]);
106 }
107 }
108 push(&mut out, &[("0", "ENDSEC"), ("0", "EOF")]);
109 out
110}
111
112fn real(v: f64) -> String {
114 let s = format!("{v:?}");
115 if s.contains('.') || s.contains('e') {
116 s
117 } else {
118 format!("{s}.0")
119 }
120}
121
122fn push(out: &mut String, pairs: &[(&str, &str)]) {
124 for (code, value) in pairs {
125 let _ = writeln!(out, "{code}\n{value}");
126 }
127}
128
129#[cfg(test)]
130#[allow(clippy::unwrap_used)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn a_drawing_writes_layers_polylines_and_exact_coordinates() {
136 let visible = vec![vec![
137 Point2::new(0.0, 0.0),
138 Point2::new(10.0, 0.0),
139 Point2::new(10.0, 5.0),
140 ]];
141 let hidden = vec![vec![Point2::new(1.5, 2.25), Point2::new(3.0, 2.25)]];
142 let text = write_dxf(&visible, &hidden);
143
144 assert!(text.starts_with("0\nSECTION"));
145 assert!(text.trim_end().ends_with("EOF"));
146 assert_eq!(text.matches("POLYLINE").count(), 2);
147 assert_eq!(text.matches("VERTEX").count(), 5);
148 assert_eq!(text.matches("SEQEND").count(), 2);
149 assert!(text.contains("VISIBLE"));
151 assert!(text.contains("HIDDEN"));
152 assert!(text.contains("DASHED"));
153 assert!(text.contains("10\n10.0\n20\n5.0"));
155 assert!(text.contains("10\n1.5\n20\n2.25"));
156 }
157
158 #[test]
159 fn degenerate_polylines_are_dropped() {
160 let text = write_dxf(&[vec![Point2::new(1.0, 1.0)]], &[vec![]]);
161 assert_eq!(text.matches("POLYLINE").count(), 0);
162 }
163}
164
165#[derive(Debug, Clone, Default, PartialEq)]
167pub struct DxfDrawing {
168 pub visible: Vec<Vec<Point2>>,
170 pub hidden: Vec<Vec<Point2>>,
172}
173
174pub fn read_dxf(text: &str) -> ogeom_core::OgeomResult<DxfDrawing> {
187 let mut out = DxfDrawing::default();
188 for entity in read_dxf_entities(text)?.entities {
189 let points = match entity.curve {
190 DxfCurve::Line { start, end } => vec![start, end],
191 DxfCurve::Polyline { vertices, closed } => {
192 let mut points: Vec<Point2> = vertices.iter().map(|v| v.0).collect();
193 if closed
194 && points.len() > 2
195 && let (Some(first), Some(last)) = (points.first(), points.last())
196 && first.distance(*last) > 0.0
197 {
198 points.push(*first);
199 }
200 points
201 }
202 _ => continue,
203 };
204 if points.len() < 2 {
205 continue;
206 }
207 if entity.hidden {
208 out.hidden.push(points);
209 } else {
210 out.visible.push(points);
211 }
212 }
213 Ok(out)
214}
215
216#[derive(Debug, Clone, Default, PartialEq)]
218pub struct DxfEntities {
219 pub insunits: Option<i32>,
221 pub unit_mm: Option<f64>,
223 pub entities: Vec<DxfEntity>,
225 pub texts: Vec<DxfLabel>,
227}
228
229#[derive(Debug, Clone, PartialEq)]
231pub struct DxfText {
232 pub at: Point2,
234 pub height: f64,
236 pub angle: f64,
238 pub text: String,
240}
241
242#[derive(Debug, Clone, PartialEq)]
244pub struct DxfLabel {
245 pub layer: String,
247 pub text: DxfText,
249}
250
251#[derive(Debug, Clone, PartialEq)]
253pub struct DxfEntity {
254 pub layer: String,
256 pub hidden: bool,
259 pub curve: DxfCurve,
261}
262
263#[derive(Debug, Clone, PartialEq)]
265pub enum DxfCurve {
266 Line {
268 start: Point2,
270 end: Point2,
272 },
273 Arc {
275 centre: Point2,
277 radius: f64,
279 start_angle: f64,
281 end_angle: f64,
283 },
284 Circle {
286 centre: Point2,
288 radius: f64,
290 },
291 Ellipse {
294 centre: Point2,
296 major: Vector2,
298 ratio: f64,
300 start_param: f64,
302 end_param: f64,
304 },
305 Spline {
309 degree: usize,
311 knots: Vec<f64>,
313 control_points: Vec<Point2>,
315 weights: Option<Vec<f64>>,
317 closed: bool,
319 },
320 Polyline {
324 vertices: Vec<(Point2, f64)>,
326 closed: bool,
328 },
329}
330
331fn unit_mm(code: i32) -> Option<f64> {
333 Some(match code {
334 1 => 25.4,
335 2 => 304.8,
336 3 => 1_609_344.0,
337 4 => 1.0,
338 5 => 10.0,
339 6 => 1000.0,
340 7 => 1.0e6,
341 8 => 2.54e-5,
342 9 => 0.0254,
343 10 => 914.4,
344 11 => 1.0e-7,
345 12 => 1.0e-6,
346 13 => 1.0e-3,
347 14 => 100.0,
348 15 => 1.0e4,
349 16 => 1.0e5,
350 17 => 1.0e12,
351 18 => 1.495_978_707e14,
352 19 => 9.460_730_472_580_8e18,
353 20 => 3.085_677_581_491_367e19,
354 _ => return None,
355 })
356}
357
358struct Record<'a> {
361 kind: &'a str,
362 pairs: Vec<(i32, &'a str)>,
363}
364
365impl Record<'_> {
366 fn text(&self, code: i32) -> Option<&str> {
367 self.pairs.iter().find(|p| p.0 == code).map(|p| p.1)
368 }
369
370 fn real(&self, code: i32) -> Option<f64> {
371 self.text(code).and_then(|v| v.parse().ok())
372 }
373
374 fn int(&self, code: i32) -> Option<i64> {
375 self.text(code).and_then(|v| v.parse().ok())
376 }
377
378 fn point(&self, x: i32, y: i32) -> Option<Point2> {
379 Some(Point2::new(self.real(x)?, self.real(y)?))
380 }
381
382 fn reals(&self, code: i32) -> Vec<f64> {
383 self.pairs
384 .iter()
385 .filter(|p| p.0 == code)
386 .filter_map(|p| p.1.parse().ok())
387 .collect()
388 }
389
390 fn points(&self, x: i32, y: i32) -> Vec<Point2> {
392 let mut out: Vec<Point2> = Vec::new();
393 for (code, value) in &self.pairs {
394 let Ok(v) = value.parse::<f64>() else {
395 continue;
396 };
397 if *code == x {
398 out.push(Point2::new(v, 0.0));
399 } else if *code == y
400 && let Some(last) = out.last_mut()
401 {
402 last.y = v;
403 }
404 }
405 out
406 }
407}
408
409fn dashed(linetype: &str) -> bool {
411 let name = linetype.to_ascii_uppercase();
412 name.contains("HIDDEN") || name.contains("DASH")
413}
414
415pub fn read_dxf_entities(text: &str) -> ogeom_core::OgeomResult<DxfEntities> {
433 let lines: Vec<&str> = text.lines().map(str::trim).collect();
435 if !lines.len().is_multiple_of(2) && !lines.last().is_some_and(|l| l.is_empty()) {
436 ogeom_core::ogeom_bail!(
437 Construction,
438 "a DXF is group codes and values in pairs; this has an odd number of lines"
439 );
440 }
441 let mut pairs: Vec<(i32, &str)> = Vec::with_capacity(lines.len() / 2);
442 for [code, value] in lines.as_chunks::<2>().0 {
443 let Ok(code) = code.parse::<i32>() else {
444 ogeom_core::ogeom_bail!(
445 Construction,
446 "a DXF group code is an integer; found {code:?}"
447 );
448 };
449 pairs.push((code, *value));
450 }
451
452 let mut sections: Vec<(&str, Vec<Record<'_>>)> = Vec::new();
454 let mut k = 0;
455 while k < pairs.len() {
456 if pairs[k] == (0, "SECTION") && k + 1 < pairs.len() && pairs[k + 1].0 == 2 {
457 let name = pairs[k + 1].1;
458 k += 2;
459 let mut records: Vec<Record<'_>> = Vec::new();
460 let mut current = Record {
463 kind: "",
464 pairs: Vec::new(),
465 };
466 while k < pairs.len() && pairs[k] != (0, "ENDSEC") {
467 if pairs[k].0 == 0 {
468 records.push(core::mem::replace(
469 &mut current,
470 Record {
471 kind: pairs[k].1,
472 pairs: Vec::new(),
473 },
474 ));
475 } else {
476 current.pairs.push(pairs[k]);
477 }
478 k += 1;
479 }
480 records.push(current);
481 sections.push((name, records));
482 }
483 k += 1;
484 }
485
486 let mut out = DxfEntities::default();
487 let mut layer_linetype: ogeom_core::FastMap<String, String> = ogeom_core::FastMap::default();
488 for (name, records) in §ions {
489 match *name {
490 "HEADER" => {
491 for record in records {
492 let mut it = record.pairs.iter();
493 while let Some((code, value)) = it.next() {
494 if *code == 9
495 && *value == "$INSUNITS"
496 && let Some((70, units)) = it.next()
497 && let Ok(units) = units.parse::<i32>()
498 {
499 out.insunits = Some(units);
500 out.unit_mm = unit_mm(units);
501 }
502 }
503 }
504 }
505 "TABLES" => {
506 for record in records.iter().filter(|r| r.kind == "LAYER") {
507 if let Some(layer) = record.text(2) {
508 layer_linetype.insert(
509 layer.to_ascii_uppercase(),
510 record.text(6).unwrap_or("").to_string(),
511 );
512 }
513 }
514 }
515 _ => {}
516 }
517 }
518
519 let Some((_, records)) = sections.iter().find(|(name, _)| *name == "ENTITIES") else {
520 return Ok(out);
521 };
522 let mut k = 0;
523 while k < records.len() {
524 let record = &records[k];
525 k += 1;
526 let layer = record.text(8).unwrap_or("0").to_string();
527 let hidden = layer.eq_ignore_ascii_case("HIDDEN")
528 || record.text(6).is_some_and(dashed)
529 || layer_linetype
530 .get(&layer.to_ascii_uppercase())
531 .is_some_and(|l| dashed(l));
532 let from_below = match record.real(230) {
535 None => false,
536 Some(z)
537 if (record.real(210).unwrap_or(0.0).abs()
538 + record.real(220).unwrap_or(0.0).abs())
539 <= 1e-12 =>
540 {
541 z < 0.0
542 }
543 Some(_) => ogeom_core::ogeom_bail!(
544 Construction,
545 "a {} entity is extruded along a tilted axis; only drawings in the XY plane \
546 are read",
547 record.kind
548 ),
549 };
550 let flip = |p: Point2| {
551 if from_below {
552 Point2::new(-p.x, p.y)
553 } else {
554 p
555 }
556 };
557 let curve = match record.kind {
558 "LINE" => {
559 let (Some(a), Some(b)) = (record.point(10, 20), record.point(11, 21)) else {
560 continue;
561 };
562 DxfCurve::Line {
563 start: flip(a),
564 end: flip(b),
565 }
566 }
567 "CIRCLE" => {
568 let (Some(centre), Some(radius)) = (record.point(10, 20), record.real(40)) else {
569 continue;
570 };
571 DxfCurve::Circle {
572 centre: flip(centre),
573 radius,
574 }
575 }
576 "ARC" => {
577 let (Some(centre), Some(radius)) = (record.point(10, 20), record.real(40)) else {
578 continue;
579 };
580 let start = record.real(50).unwrap_or(0.0).to_radians();
581 let end = record.real(51).unwrap_or(360.0).to_radians();
582 let (start_angle, end_angle) = if from_below {
585 (core::f64::consts::PI - end, core::f64::consts::PI - start)
586 } else {
587 (start, end)
588 };
589 DxfCurve::Arc {
590 centre: flip(centre),
591 radius,
592 start_angle,
593 end_angle,
594 }
595 }
596 "TEXT" => {
597 let (Some(at), Some(height)) = (record.point(10, 20), record.real(40)) else {
598 continue;
599 };
600 let angle = record.real(50).unwrap_or(0.0).to_radians();
601 out.texts.push(DxfLabel {
602 layer,
603 text: DxfText {
604 at: flip(at),
605 height,
606 angle: if from_below {
608 core::f64::consts::PI - angle
609 } else {
610 angle
611 },
612 text: unescape(record.text(1).unwrap_or("")),
613 },
614 });
615 continue;
616 }
617 "ELLIPSE" | "SPLINE" if from_below => ogeom_core::ogeom_bail!(
618 Construction,
619 "a {} seen from below (extrusion 0, 0, -1) is not read yet",
620 record.kind
621 ),
622 "ELLIPSE" => {
623 let (Some(centre), Some(major)) = (record.point(10, 20), record.point(11, 21))
624 else {
625 continue;
626 };
627 DxfCurve::Ellipse {
628 centre,
629 major: Vector2::new(major.x, major.y),
630 ratio: record.real(40).unwrap_or(1.0),
631 start_param: record.real(41).unwrap_or(0.0),
632 end_param: record.real(42).unwrap_or(core::f64::consts::TAU),
633 }
634 }
635 "SPLINE" => {
636 let flags = record.int(70).unwrap_or(0);
637 let control = record.points(10, 20);
638 let weights = record.reals(41);
639 let (control_points, knots) = if control.is_empty() {
640 (record.points(11, 21), Vec::new())
641 } else {
642 (control, record.reals(40))
643 };
644 DxfCurve::Spline {
645 degree: usize::try_from(record.int(71).unwrap_or(3)).unwrap_or(3),
646 knots,
647 weights: (!weights.is_empty() && weights.len() == control_points.len())
648 .then_some(weights),
649 control_points,
650 closed: flags & 1 != 0,
651 }
652 }
653 "LWPOLYLINE" => {
654 let mut vertices: Vec<(Point2, f64)> = Vec::new();
655 for (code, value) in &record.pairs {
656 let Ok(v) = value.parse::<f64>() else {
657 continue;
658 };
659 match code {
660 10 => vertices.push((Point2::new(v, 0.0), 0.0)),
661 20 => {
662 if let Some(last) = vertices.last_mut() {
663 last.0.y = v;
664 }
665 }
666 42 => {
667 if let Some(last) = vertices.last_mut() {
668 last.1 = v;
669 }
670 }
671 _ => {}
672 }
673 }
674 polyline(vertices, record.int(70).unwrap_or(0), from_below)
675 }
676 "POLYLINE" => {
677 let mut vertices: Vec<(Point2, f64)> = Vec::new();
680 while k < records.len() && records[k].kind == "VERTEX" {
681 let v = &records[k];
682 k += 1;
683 if v.int(70).unwrap_or(0) & 16 != 0 {
685 continue;
686 }
687 if let Some(p) = v.point(10, 20) {
688 vertices.push((p, v.real(42).unwrap_or(0.0)));
689 }
690 }
691 if k < records.len() && records[k].kind == "SEQEND" {
692 k += 1;
693 }
694 polyline(vertices, record.int(70).unwrap_or(0), from_below)
695 }
696 _ => continue,
697 };
698 out.entities.push(DxfEntity {
699 layer,
700 hidden,
701 curve,
702 });
703 }
704 Ok(out)
705}
706
707fn unescape(text: &str) -> String {
710 let mut out = String::with_capacity(text.len());
711 let mut rest = text;
712 while let Some(at) = rest.find("\\U+") {
713 out.push_str(&rest[..at]);
714 let code = rest
715 .get(at + 3..at + 7)
716 .filter(|h| h.bytes().all(|b| b.is_ascii_hexdigit()))
717 .and_then(|h| u32::from_str_radix(h, 16).ok())
718 .and_then(char::from_u32);
719 if let Some(c) = code {
720 out.push(c);
721 rest = &rest[at + 7..];
722 } else {
723 out.push('\\');
724 rest = &rest[at + 1..];
725 }
726 }
727 out.push_str(rest);
728 out
729}
730
731fn polyline(vertices: Vec<(Point2, f64)>, flags: i64, from_below: bool) -> DxfCurve {
734 let vertices = if from_below {
735 vertices
736 .into_iter()
737 .map(|(p, b)| (Point2::new(-p.x, p.y), -b))
738 .collect()
739 } else {
740 vertices
741 };
742 DxfCurve::Polyline {
743 vertices,
744 closed: flags & 1 != 0,
745 }
746}