1use ogeom_math::{Point2, Vector2};
16use std::fmt::Write as _;
17
18#[must_use]
24pub fn write_dxf(visible: &[Vec<Point2>], hidden: &[Vec<Point2>]) -> String {
25 let mut out = String::new();
26 push(
28 &mut out,
29 &[("0", "SECTION"), ("2", "HEADER"), ("0", "ENDSEC")],
30 );
31
32 push(&mut out, &[("0", "SECTION"), ("2", "TABLES")]);
34 push(&mut out, &[("0", "TABLE"), ("2", "LTYPE"), ("70", "2")]);
35 push(
36 &mut out,
37 &[
38 ("0", "LTYPE"),
39 ("2", "CONTINUOUS"),
40 ("70", "0"),
41 ("3", "Solid line"),
42 ("72", "65"),
43 ("73", "0"),
44 ("40", "0.0"),
45 ],
46 );
47 push(
48 &mut out,
49 &[
50 ("0", "LTYPE"),
51 ("2", "DASHED"),
52 ("70", "0"),
53 ("3", "Dashed line"),
54 ("72", "65"),
55 ("73", "2"),
56 ("40", "0.75"),
57 ("49", "0.5"),
58 ("49", "-0.25"),
59 ],
60 );
61 push(&mut out, &[("0", "ENDTAB")]);
62 push(&mut out, &[("0", "TABLE"), ("2", "LAYER"), ("70", "2")]);
63 push(
64 &mut out,
65 &[
66 ("0", "LAYER"),
67 ("2", "VISIBLE"),
68 ("70", "0"),
69 ("62", "7"),
70 ("6", "CONTINUOUS"),
71 ],
72 );
73 push(
74 &mut out,
75 &[
76 ("0", "LAYER"),
77 ("2", "HIDDEN"),
78 ("70", "0"),
79 ("62", "8"),
80 ("6", "DASHED"),
81 ],
82 );
83 push(&mut out, &[("0", "ENDTAB"), ("0", "ENDSEC")]);
84
85 push(&mut out, &[("0", "SECTION"), ("2", "ENTITIES")]);
86 for (layer, curves) in [("VISIBLE", visible), ("HIDDEN", hidden)] {
87 for curve in curves {
88 if curve.len() < 2 {
89 continue;
90 }
91 push(
92 &mut out,
93 &[("0", "POLYLINE"), ("8", layer), ("66", "1"), ("70", "0")],
94 );
95 for p in curve {
96 push(&mut out, &[("0", "VERTEX"), ("8", layer)]);
97 let _ = writeln!(out, "10\n{}\n20\n{}\n30\n0.0", real(p.x), real(p.y));
98 }
99 push(&mut out, &[("0", "SEQEND")]);
100 }
101 }
102 push(&mut out, &[("0", "ENDSEC"), ("0", "EOF")]);
103 out
104}
105
106fn real(v: f64) -> String {
108 let s = format!("{v:?}");
109 if s.contains('.') || s.contains('e') {
110 s
111 } else {
112 format!("{s}.0")
113 }
114}
115
116fn push(out: &mut String, pairs: &[(&str, &str)]) {
118 for (code, value) in pairs {
119 let _ = writeln!(out, "{code}\n{value}");
120 }
121}
122
123#[cfg(test)]
124#[allow(clippy::unwrap_used)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn a_drawing_writes_layers_polylines_and_exact_coordinates() {
130 let visible = vec![vec![
131 Point2::new(0.0, 0.0),
132 Point2::new(10.0, 0.0),
133 Point2::new(10.0, 5.0),
134 ]];
135 let hidden = vec![vec![Point2::new(1.5, 2.25), Point2::new(3.0, 2.25)]];
136 let text = write_dxf(&visible, &hidden);
137
138 assert!(text.starts_with("0\nSECTION"));
139 assert!(text.trim_end().ends_with("EOF"));
140 assert_eq!(text.matches("POLYLINE").count(), 2);
141 assert_eq!(text.matches("VERTEX").count(), 5);
142 assert_eq!(text.matches("SEQEND").count(), 2);
143 assert!(text.contains("VISIBLE"));
145 assert!(text.contains("HIDDEN"));
146 assert!(text.contains("DASHED"));
147 assert!(text.contains("10\n10.0\n20\n5.0"));
149 assert!(text.contains("10\n1.5\n20\n2.25"));
150 }
151
152 #[test]
153 fn degenerate_polylines_are_dropped() {
154 let text = write_dxf(&[vec![Point2::new(1.0, 1.0)]], &[vec![]]);
155 assert_eq!(text.matches("POLYLINE").count(), 0);
156 }
157}
158
159#[derive(Debug, Clone, Default, PartialEq)]
161pub struct DxfDrawing {
162 pub visible: Vec<Vec<Point2>>,
164 pub hidden: Vec<Vec<Point2>>,
166}
167
168pub fn read_dxf(text: &str) -> ogeom_core::OgeomResult<DxfDrawing> {
181 let mut out = DxfDrawing::default();
182 for entity in read_dxf_entities(text)?.entities {
183 let points = match entity.curve {
184 DxfCurve::Line { start, end } => vec![start, end],
185 DxfCurve::Polyline { vertices, closed } => {
186 let mut points: Vec<Point2> = vertices.iter().map(|v| v.0).collect();
187 if closed
188 && points.len() > 2
189 && let (Some(first), Some(last)) = (points.first(), points.last())
190 && first.distance(*last) > 0.0
191 {
192 points.push(*first);
193 }
194 points
195 }
196 _ => continue,
197 };
198 if points.len() < 2 {
199 continue;
200 }
201 if entity.hidden {
202 out.hidden.push(points);
203 } else {
204 out.visible.push(points);
205 }
206 }
207 Ok(out)
208}
209
210#[derive(Debug, Clone, Default, PartialEq)]
212pub struct DxfEntities {
213 pub insunits: Option<i32>,
215 pub unit_mm: Option<f64>,
217 pub entities: Vec<DxfEntity>,
219}
220
221#[derive(Debug, Clone, PartialEq)]
223pub struct DxfEntity {
224 pub layer: String,
226 pub hidden: bool,
229 pub curve: DxfCurve,
231}
232
233#[derive(Debug, Clone, PartialEq)]
235pub enum DxfCurve {
236 Line {
238 start: Point2,
240 end: Point2,
242 },
243 Arc {
245 centre: Point2,
247 radius: f64,
249 start_angle: f64,
251 end_angle: f64,
253 },
254 Circle {
256 centre: Point2,
258 radius: f64,
260 },
261 Ellipse {
264 centre: Point2,
266 major: Vector2,
268 ratio: f64,
270 start_param: f64,
272 end_param: f64,
274 },
275 Spline {
279 degree: usize,
281 knots: Vec<f64>,
283 control_points: Vec<Point2>,
285 weights: Option<Vec<f64>>,
287 closed: bool,
289 },
290 Polyline {
294 vertices: Vec<(Point2, f64)>,
296 closed: bool,
298 },
299}
300
301fn unit_mm(code: i32) -> Option<f64> {
303 Some(match code {
304 1 => 25.4,
305 2 => 304.8,
306 3 => 1_609_344.0,
307 4 => 1.0,
308 5 => 10.0,
309 6 => 1000.0,
310 7 => 1.0e6,
311 8 => 2.54e-5,
312 9 => 0.0254,
313 10 => 914.4,
314 11 => 1.0e-7,
315 12 => 1.0e-6,
316 13 => 1.0e-3,
317 14 => 100.0,
318 15 => 1.0e4,
319 16 => 1.0e5,
320 17 => 1.0e12,
321 18 => 1.495_978_707e14,
322 19 => 9.460_730_472_580_8e18,
323 20 => 3.085_677_581_491_367e19,
324 _ => return None,
325 })
326}
327
328struct Record<'a> {
331 kind: &'a str,
332 pairs: Vec<(i32, &'a str)>,
333}
334
335impl Record<'_> {
336 fn text(&self, code: i32) -> Option<&str> {
337 self.pairs.iter().find(|p| p.0 == code).map(|p| p.1)
338 }
339
340 fn real(&self, code: i32) -> Option<f64> {
341 self.text(code).and_then(|v| v.parse().ok())
342 }
343
344 fn int(&self, code: i32) -> Option<i64> {
345 self.text(code).and_then(|v| v.parse().ok())
346 }
347
348 fn point(&self, x: i32, y: i32) -> Option<Point2> {
349 Some(Point2::new(self.real(x)?, self.real(y)?))
350 }
351
352 fn reals(&self, code: i32) -> Vec<f64> {
353 self.pairs
354 .iter()
355 .filter(|p| p.0 == code)
356 .filter_map(|p| p.1.parse().ok())
357 .collect()
358 }
359
360 fn points(&self, x: i32, y: i32) -> Vec<Point2> {
362 let mut out: Vec<Point2> = Vec::new();
363 for (code, value) in &self.pairs {
364 let Ok(v) = value.parse::<f64>() else {
365 continue;
366 };
367 if *code == x {
368 out.push(Point2::new(v, 0.0));
369 } else if *code == y
370 && let Some(last) = out.last_mut()
371 {
372 last.y = v;
373 }
374 }
375 out
376 }
377}
378
379fn dashed(linetype: &str) -> bool {
381 let name = linetype.to_ascii_uppercase();
382 name.contains("HIDDEN") || name.contains("DASH")
383}
384
385pub fn read_dxf_entities(text: &str) -> ogeom_core::OgeomResult<DxfEntities> {
401 let lines: Vec<&str> = text.lines().map(str::trim).collect();
403 if !lines.len().is_multiple_of(2) && !lines.last().is_some_and(|l| l.is_empty()) {
404 ogeom_core::ogeom_bail!(
405 Construction,
406 "a DXF is group codes and values in pairs; this has an odd number of lines"
407 );
408 }
409 let mut pairs: Vec<(i32, &str)> = Vec::with_capacity(lines.len() / 2);
410 for [code, value] in lines.as_chunks::<2>().0 {
411 let Ok(code) = code.parse::<i32>() else {
412 ogeom_core::ogeom_bail!(
413 Construction,
414 "a DXF group code is an integer; found {code:?}"
415 );
416 };
417 pairs.push((code, *value));
418 }
419
420 let mut sections: Vec<(&str, Vec<Record<'_>>)> = Vec::new();
422 let mut k = 0;
423 while k < pairs.len() {
424 if pairs[k] == (0, "SECTION") && k + 1 < pairs.len() && pairs[k + 1].0 == 2 {
425 let name = pairs[k + 1].1;
426 k += 2;
427 let mut records: Vec<Record<'_>> = Vec::new();
428 let mut current = Record {
431 kind: "",
432 pairs: Vec::new(),
433 };
434 while k < pairs.len() && pairs[k] != (0, "ENDSEC") {
435 if pairs[k].0 == 0 {
436 records.push(core::mem::replace(
437 &mut current,
438 Record {
439 kind: pairs[k].1,
440 pairs: Vec::new(),
441 },
442 ));
443 } else {
444 current.pairs.push(pairs[k]);
445 }
446 k += 1;
447 }
448 records.push(current);
449 sections.push((name, records));
450 }
451 k += 1;
452 }
453
454 let mut out = DxfEntities::default();
455 let mut layer_linetype: std::collections::HashMap<String, String> =
456 std::collections::HashMap::new();
457 for (name, records) in §ions {
458 match *name {
459 "HEADER" => {
460 for record in records {
461 let mut it = record.pairs.iter();
462 while let Some((code, value)) = it.next() {
463 if *code == 9
464 && *value == "$INSUNITS"
465 && let Some((70, units)) = it.next()
466 && let Ok(units) = units.parse::<i32>()
467 {
468 out.insunits = Some(units);
469 out.unit_mm = unit_mm(units);
470 }
471 }
472 }
473 }
474 "TABLES" => {
475 for record in records.iter().filter(|r| r.kind == "LAYER") {
476 if let Some(layer) = record.text(2) {
477 layer_linetype.insert(
478 layer.to_ascii_uppercase(),
479 record.text(6).unwrap_or("").to_string(),
480 );
481 }
482 }
483 }
484 _ => {}
485 }
486 }
487
488 let Some((_, records)) = sections.iter().find(|(name, _)| *name == "ENTITIES") else {
489 return Ok(out);
490 };
491 let mut k = 0;
492 while k < records.len() {
493 let record = &records[k];
494 k += 1;
495 let layer = record.text(8).unwrap_or("0").to_string();
496 let hidden = layer.eq_ignore_ascii_case("HIDDEN")
497 || record.text(6).is_some_and(dashed)
498 || layer_linetype
499 .get(&layer.to_ascii_uppercase())
500 .is_some_and(|l| dashed(l));
501 let from_below = match record.real(230) {
504 None => false,
505 Some(z)
506 if (record.real(210).unwrap_or(0.0).abs()
507 + record.real(220).unwrap_or(0.0).abs())
508 <= 1e-12 =>
509 {
510 z < 0.0
511 }
512 Some(_) => ogeom_core::ogeom_bail!(
513 Construction,
514 "a {} entity is extruded along a tilted axis; only drawings in the XY plane \
515 are read",
516 record.kind
517 ),
518 };
519 let flip = |p: Point2| {
520 if from_below {
521 Point2::new(-p.x, p.y)
522 } else {
523 p
524 }
525 };
526 let curve = match record.kind {
527 "LINE" => {
528 let (Some(a), Some(b)) = (record.point(10, 20), record.point(11, 21)) else {
529 continue;
530 };
531 DxfCurve::Line {
532 start: flip(a),
533 end: flip(b),
534 }
535 }
536 "CIRCLE" => {
537 let (Some(centre), Some(radius)) = (record.point(10, 20), record.real(40)) else {
538 continue;
539 };
540 DxfCurve::Circle {
541 centre: flip(centre),
542 radius,
543 }
544 }
545 "ARC" => {
546 let (Some(centre), Some(radius)) = (record.point(10, 20), record.real(40)) else {
547 continue;
548 };
549 let start = record.real(50).unwrap_or(0.0).to_radians();
550 let end = record.real(51).unwrap_or(360.0).to_radians();
551 let (start_angle, end_angle) = if from_below {
554 (core::f64::consts::PI - end, core::f64::consts::PI - start)
555 } else {
556 (start, end)
557 };
558 DxfCurve::Arc {
559 centre: flip(centre),
560 radius,
561 start_angle,
562 end_angle,
563 }
564 }
565 "ELLIPSE" | "SPLINE" if from_below => ogeom_core::ogeom_bail!(
566 Construction,
567 "a {} seen from below (extrusion 0, 0, -1) is not read yet",
568 record.kind
569 ),
570 "ELLIPSE" => {
571 let (Some(centre), Some(major)) = (record.point(10, 20), record.point(11, 21))
572 else {
573 continue;
574 };
575 DxfCurve::Ellipse {
576 centre,
577 major: Vector2::new(major.x, major.y),
578 ratio: record.real(40).unwrap_or(1.0),
579 start_param: record.real(41).unwrap_or(0.0),
580 end_param: record.real(42).unwrap_or(core::f64::consts::TAU),
581 }
582 }
583 "SPLINE" => {
584 let flags = record.int(70).unwrap_or(0);
585 let control = record.points(10, 20);
586 let weights = record.reals(41);
587 let (control_points, knots) = if control.is_empty() {
588 (record.points(11, 21), Vec::new())
589 } else {
590 (control, record.reals(40))
591 };
592 DxfCurve::Spline {
593 degree: usize::try_from(record.int(71).unwrap_or(3)).unwrap_or(3),
594 knots,
595 weights: (!weights.is_empty() && weights.len() == control_points.len())
596 .then_some(weights),
597 control_points,
598 closed: flags & 1 != 0,
599 }
600 }
601 "LWPOLYLINE" => {
602 let mut vertices: Vec<(Point2, f64)> = Vec::new();
603 for (code, value) in &record.pairs {
604 let Ok(v) = value.parse::<f64>() else {
605 continue;
606 };
607 match code {
608 10 => vertices.push((Point2::new(v, 0.0), 0.0)),
609 20 => {
610 if let Some(last) = vertices.last_mut() {
611 last.0.y = v;
612 }
613 }
614 42 => {
615 if let Some(last) = vertices.last_mut() {
616 last.1 = v;
617 }
618 }
619 _ => {}
620 }
621 }
622 polyline(vertices, record.int(70).unwrap_or(0), from_below)
623 }
624 "POLYLINE" => {
625 let mut vertices: Vec<(Point2, f64)> = Vec::new();
628 while k < records.len() && records[k].kind == "VERTEX" {
629 let v = &records[k];
630 k += 1;
631 if v.int(70).unwrap_or(0) & 16 != 0 {
633 continue;
634 }
635 if let Some(p) = v.point(10, 20) {
636 vertices.push((p, v.real(42).unwrap_or(0.0)));
637 }
638 }
639 if k < records.len() && records[k].kind == "SEQEND" {
640 k += 1;
641 }
642 polyline(vertices, record.int(70).unwrap_or(0), from_below)
643 }
644 _ => continue,
645 };
646 out.entities.push(DxfEntity {
647 layer,
648 hidden,
649 curve,
650 });
651 }
652 Ok(out)
653}
654
655fn polyline(vertices: Vec<(Point2, f64)>, flags: i64, from_below: bool) -> DxfCurve {
658 let vertices = if from_below {
659 vertices
660 .into_iter()
661 .map(|(p, b)| (Point2::new(-p.x, p.y), -b))
662 .collect()
663 } else {
664 vertices
665 };
666 DxfCurve::Polyline {
667 vertices,
668 closed: flags & 1 != 0,
669 }
670}