1use super::{DxfCurve, DxfText, real};
22use core::fmt::Display;
23use ogeom_core::{OgeomResult, ogeom_bail};
24use ogeom_math::Point2;
25use std::fmt::Write as _;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DxfUnit {
30 Unitless,
32 Millimetre,
34 Inch,
36}
37
38impl DxfUnit {
39 #[must_use]
41 pub fn insunits(self) -> i32 {
42 match self {
43 Self::Unitless => 0,
44 Self::Millimetre => 4,
45 Self::Inch => 1,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct DxfLayer {
53 pub name: String,
57 pub dashed: bool,
59}
60
61#[derive(Debug, Clone, PartialEq)]
63pub enum DxfShape {
64 Curve(DxfCurve),
66 Text(DxfText),
68}
69
70#[derive(Debug, Clone, PartialEq)]
72pub struct DxfItem {
73 pub layer: usize,
75 pub shape: DxfShape,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq)]
81pub struct DxfOut<'a> {
82 pub unit: DxfUnit,
84 pub layers: &'a [DxfLayer],
86 pub items: &'a [DxfItem],
88}
89
90pub fn write_dxf_entities(drawing: &DxfOut<'_>) -> OgeomResult<String> {
113 check_layers(drawing.layers)?;
114 for item in drawing.items {
115 if item.layer >= drawing.layers.len() {
116 ogeom_bail!(
117 Construction,
118 "an item names layer {}, of {} layers",
119 item.layer,
120 drawing.layers.len()
121 );
122 }
123 check_shape(&item.shape)?;
124 }
125
126 let mut w = Writer::default();
127 let tables = w.tables(drawing.layers);
128 let blocks = w.blocks(&tables);
129 let entities = w.entities(drawing, tables.model_space);
130 let objects = w.objects();
131
132 let mut out = String::new();
133 section(&mut out, "HEADER", &{
134 let mut s = String::new();
135 variable(&mut s, "$ACADVER", &[(1, "AC1015".into())]);
136 variable(&mut s, "$DWGCODEPAGE", &[(3, "ANSI_1252".into())]);
137 variable(
138 &mut s,
139 "$INSUNITS",
140 &[(70, drawing.unit.insunits().to_string())],
141 );
142 let metric = u8::from(drawing.unit != DxfUnit::Inch);
143 variable(&mut s, "$MEASUREMENT", &[(70, metric.to_string())]);
144 variable(&mut s, "$HANDSEED", &[(5, format!("{:X}", w.next))]);
145 s
146 });
147 section(&mut out, "CLASSES", "");
148 section(&mut out, "TABLES", &tables.text);
149 section(&mut out, "BLOCKS", &blocks);
150 section(&mut out, "ENTITIES", &entities);
151 section(&mut out, "OBJECTS", &objects);
152 out.push_str("0\nEOF\n");
153 Ok(out)
154}
155
156fn finite(values: &[f64], what: &str) -> OgeomResult<()> {
157 if values.iter().any(|v| !v.is_finite()) {
158 ogeom_bail!(Construction, "a {what} has a coordinate that is not finite");
159 }
160 Ok(())
161}
162
163fn positive(value: f64, what: &str) -> OgeomResult<()> {
164 if !(value.is_finite() && value > 0.0) {
165 ogeom_bail!(Construction, "a {what} must be positive; found {value}");
166 }
167 Ok(())
168}
169
170fn check_layers(layers: &[DxfLayer]) -> OgeomResult<()> {
171 for (i, layer) in layers.iter().enumerate() {
172 let name = &layer.name;
173 let allowed = |c: char| c.is_ascii_graphic() || c == ' ';
174 if name.is_empty()
175 || name.len() > 255
176 || name.starts_with(' ')
177 || name.ends_with(' ')
178 || !name.chars().all(allowed)
179 || name.contains(['<', '>', '/', '\\', '"', ':', ';', '?', '*', '|', '=', '`'])
180 {
181 ogeom_bail!(Construction, "{name:?} is not a DXF layer name");
182 }
183 if layers[..i]
184 .iter()
185 .any(|other| other.name.eq_ignore_ascii_case(name))
186 {
187 ogeom_bail!(Construction, "layer {name:?} is declared twice");
188 }
189 }
190 Ok(())
191}
192
193fn check_shape(shape: &DxfShape) -> OgeomResult<()> {
194 match shape {
195 DxfShape::Curve(DxfCurve::Line { start, end }) => {
196 finite(&[start.x, start.y, end.x, end.y], "line")
197 }
198 DxfShape::Curve(DxfCurve::Circle { centre, radius }) => {
199 finite(&[centre.x, centre.y], "circle")?;
200 positive(*radius, "circle's radius")
201 }
202 DxfShape::Curve(DxfCurve::Arc {
203 centre,
204 radius,
205 start_angle,
206 end_angle,
207 }) => {
208 finite(&[centre.x, centre.y, *start_angle, *end_angle], "arc")?;
209 positive(*radius, "arc's radius")
210 }
211 DxfShape::Curve(DxfCurve::Ellipse {
212 centre,
213 major,
214 ratio,
215 start_param,
216 end_param,
217 }) => {
218 finite(
219 &[
220 centre.x,
221 centre.y,
222 major.x,
223 major.y,
224 *start_param,
225 *end_param,
226 ],
227 "ellipse",
228 )?;
229 if major.x == 0.0 && major.y == 0.0 {
230 ogeom_bail!(Construction, "an ellipse's major axis is zero");
231 }
232 if !(*ratio > 0.0 && *ratio <= 1.0) {
233 ogeom_bail!(
234 Construction,
235 "an ellipse's minor-to-major ratio is in (0, 1]; found {ratio}"
236 );
237 }
238 Ok(())
239 }
240 DxfShape::Curve(DxfCurve::Polyline { vertices, .. }) => {
241 if vertices.len() < 2 {
242 ogeom_bail!(
243 Construction,
244 "a polyline needs two vertices; found {}",
245 vertices.len()
246 );
247 }
248 for (p, bulge) in vertices {
249 finite(&[p.x, p.y, *bulge], "polyline")?;
250 }
251 Ok(())
252 }
253 DxfShape::Curve(DxfCurve::Spline {
254 degree,
255 knots,
256 control_points,
257 weights,
258 ..
259 }) => check_spline(*degree, knots, control_points, weights.as_deref()),
260 DxfShape::Text(text) => {
261 finite(&[text.at.x, text.at.y, text.angle], "text")?;
262 positive(text.height, "text's height")?;
263 if let Some(c) = text
264 .text
265 .chars()
266 .find(|c| c.is_control() || u32::from(*c) > 0xFFFF)
267 {
268 ogeom_bail!(
269 Construction,
270 "a DXF text cannot hold {c:?}: a control character or one outside the \
271 Basic Multilingual Plane"
272 );
273 }
274 Ok(())
275 }
276 }
277}
278
279fn check_spline(
280 degree: usize,
281 knots: &[f64],
282 points: &[Point2],
283 weights: Option<&[f64]>,
284) -> OgeomResult<()> {
285 if degree == 0 {
286 ogeom_bail!(Construction, "a spline's degree is at least 1");
287 }
288 for p in points {
289 finite(&[p.x, p.y], "spline")?;
290 }
291 if knots.is_empty() {
292 if points.len() < 2 {
294 ogeom_bail!(Construction, "a spline needs two fit points");
295 }
296 if weights.is_some() {
297 ogeom_bail!(Construction, "a spline by fit points has no weights");
298 }
299 return Ok(());
300 }
301 if points.len() <= degree {
302 ogeom_bail!(
303 Construction,
304 "a degree-{degree} spline needs {} control points; found {}",
305 degree + 1,
306 points.len()
307 );
308 }
309 if knots.len() != points.len() + degree + 1 {
310 ogeom_bail!(
311 Construction,
312 "a degree-{degree} spline with {} control points has {} knots; found {}",
313 points.len(),
314 points.len() + degree + 1,
315 knots.len()
316 );
317 }
318 finite(knots, "spline's knot vector")?;
319 if knots.windows(2).any(|w| w[1] < w[0]) {
320 ogeom_bail!(Construction, "a spline's knots decrease");
321 }
322 if let Some(weights) = weights {
323 if weights.len() != points.len() {
324 ogeom_bail!(
325 Construction,
326 "a spline with {} control points has {} weights",
327 points.len(),
328 weights.len()
329 );
330 }
331 for w in weights {
332 positive(*w, "spline's weight")?;
333 }
334 }
335 Ok(())
336}
337
338fn escaped(text: &str) -> String {
342 let chars: Vec<char> = text.chars().collect();
343 let mut out = String::with_capacity(text.len());
344 for (i, &c) in chars.iter().enumerate() {
345 let plain =
346 c.is_ascii_graphic() && c != '\\' && !(c == '%' && chars.get(i + 1) == Some(&'%'))
347 || c == ' ' && i != 0 && i + 1 != chars.len();
348 if plain {
349 out.push(c);
350 } else {
351 let _ = write!(out, "\\U+{:04X}", u32::from(c));
352 }
353 }
354 out
355}
356
357struct Tables {
359 text: String,
360 model_space: u64,
361 paper_space: u64,
362}
363
364struct Writer {
366 next: u64,
367}
368
369impl Default for Writer {
370 fn default() -> Self {
371 Self { next: 1 }
372 }
373}
374
375fn pair(out: &mut String, code: i32, value: impl Display) {
376 let _ = writeln!(out, "{code}\n{value}");
377}
378
379fn point(out: &mut String, code: i32, p: Point2) {
380 pair(out, code, real(p.x));
381 pair(out, code + 10, real(p.y));
382 pair(out, code + 20, "0.0");
383}
384
385fn section(out: &mut String, name: &str, body: &str) {
386 pair(out, 0, "SECTION");
387 pair(out, 2, name);
388 out.push_str(body);
389 pair(out, 0, "ENDSEC");
390}
391
392fn variable(out: &mut String, name: &str, values: &[(i32, String)]) {
393 pair(out, 9, name);
394 for (code, value) in values {
395 pair(out, *code, value);
396 }
397}
398
399impl Writer {
400 fn handle(&mut self) -> u64 {
401 let h = self.next;
402 self.next += 1;
403 h
404 }
405
406 fn record(&mut self, out: &mut String, kind: &str, owner: u64) -> u64 {
408 self.record_coded(out, kind, 5, owner)
409 }
410
411 fn record_coded(&mut self, out: &mut String, kind: &str, code: i32, owner: u64) -> u64 {
412 let h = self.handle();
413 pair(out, 0, kind);
414 pair(out, code, format_args!("{h:X}"));
415 pair(out, 330, format_args!("{owner:X}"));
416 h
417 }
418
419 fn table(
422 &mut self,
423 out: &mut String,
424 name: &str,
425 count: usize,
426 entries: impl FnOnce(&mut Self, &mut String, u64),
427 ) {
428 let h = self.handle();
429 pair(out, 0, "TABLE");
430 pair(out, 2, name);
431 pair(out, 5, format_args!("{h:X}"));
432 pair(out, 330, 0);
433 pair(out, 100, "AcDbSymbolTable");
434 pair(out, 70, count);
435 if name == "DIMSTYLE" {
436 pair(out, 100, "AcDbDimStyleTable");
437 }
438 entries(self, out, h);
439 pair(out, 0, "ENDTAB");
440 }
441
442 fn entry(
445 &mut self,
446 out: &mut String,
447 kind: &str,
448 owner: u64,
449 subclass: &str,
450 name: &str,
451 ) -> u64 {
452 let code = if kind == "DIMSTYLE" { 105 } else { 5 };
453 let h = self.record_coded(out, kind, code, owner);
454 pair(out, 100, "AcDbSymbolTableRecord");
455 pair(out, 100, subclass);
456 pair(out, 2, name);
457 pair(out, 70, 0);
458 h
459 }
460
461 fn tables(&mut self, layers: &[DxfLayer]) -> Tables {
462 let mut out = String::new();
463 self.table(&mut out, "VPORT", 0, |_, _, _| {});
464
465 self.table(&mut out, "LTYPE", 4, |w, out, table| {
466 let linetypes: [(&str, &str, &[f64]); 4] = [
467 ("ByBlock", "", &[]),
468 ("ByLayer", "", &[]),
469 ("CONTINUOUS", "Solid line", &[]),
470 ("DASHED", "Dashed line", &[0.5, -0.25]),
471 ];
472 for (name, description, pattern) in linetypes {
473 w.entry(out, "LTYPE", table, "AcDbLinetypeTableRecord", name);
474 pair(out, 3, description);
475 pair(out, 72, 65);
476 pair(out, 73, pattern.len());
477 pair(
478 out,
479 40,
480 real(pattern.iter().fold(0.0, |sum, d| sum + d.abs())),
481 );
482 for dash in pattern {
483 pair(out, 49, real(*dash));
484 pair(out, 74, 0);
485 }
486 }
487 });
488
489 let mut all: Vec<(&str, bool)> = Vec::with_capacity(layers.len() + 1);
491 if !layers.iter().any(|l| l.name == "0") {
492 all.push(("0", false));
493 }
494 all.extend(layers.iter().map(|l| (l.name.as_str(), l.dashed)));
495 self.table(&mut out, "LAYER", all.len(), |w, out, table| {
496 for (name, dashed) in &all {
497 w.entry(out, "LAYER", table, "AcDbLayerTableRecord", name);
498 pair(out, 62, 7);
499 pair(out, 6, if *dashed { "DASHED" } else { "CONTINUOUS" });
500 }
501 });
502
503 self.table(&mut out, "STYLE", 1, |w, out, table| {
504 w.entry(out, "STYLE", table, "AcDbTextStyleTableRecord", "Standard");
505 pair(out, 40, "0.0");
506 pair(out, 41, "1.0");
507 pair(out, 50, "0.0");
508 pair(out, 71, 0);
509 pair(out, 42, "2.5");
510 pair(out, 3, "txt");
511 pair(out, 4, "");
512 });
513 self.table(&mut out, "VIEW", 0, |_, _, _| {});
514 self.table(&mut out, "UCS", 0, |_, _, _| {});
515 self.table(&mut out, "APPID", 1, |w, out, table| {
516 w.entry(out, "APPID", table, "AcDbRegAppTableRecord", "ACAD");
517 });
518 self.table(&mut out, "DIMSTYLE", 1, |w, out, table| {
519 w.entry(
520 out,
521 "DIMSTYLE",
522 table,
523 "AcDbDimStyleTableRecord",
524 "Standard",
525 );
526 });
527
528 let mut spaces = (0, 0);
529 self.table(&mut out, "BLOCK_RECORD", 2, |w, out, table| {
530 spaces.0 = w.entry(
531 out,
532 "BLOCK_RECORD",
533 table,
534 "AcDbBlockTableRecord",
535 "*Model_Space",
536 );
537 spaces.1 = w.entry(
538 out,
539 "BLOCK_RECORD",
540 table,
541 "AcDbBlockTableRecord",
542 "*Paper_Space",
543 );
544 });
545 Tables {
546 text: out,
547 model_space: spaces.0,
548 paper_space: spaces.1,
549 }
550 }
551
552 fn blocks(&mut self, tables: &Tables) -> String {
553 let mut out = String::new();
554 for (name, owner, paper) in [
555 ("*Model_Space", tables.model_space, false),
556 ("*Paper_Space", tables.paper_space, true),
557 ] {
558 self.record(&mut out, "BLOCK", owner);
559 pair(&mut out, 100, "AcDbEntity");
560 if paper {
561 pair(&mut out, 67, 1);
562 }
563 pair(&mut out, 8, "0");
564 pair(&mut out, 100, "AcDbBlockBegin");
565 pair(&mut out, 2, name);
566 pair(&mut out, 70, 0);
567 point(&mut out, 10, Point2::new(0.0, 0.0));
568 pair(&mut out, 3, name);
569 pair(&mut out, 1, "");
570 self.record(&mut out, "ENDBLK", owner);
571 pair(&mut out, 100, "AcDbEntity");
572 if paper {
573 pair(&mut out, 67, 1);
574 }
575 pair(&mut out, 8, "0");
576 pair(&mut out, 100, "AcDbBlockEnd");
577 }
578 out
579 }
580
581 fn entities(&mut self, drawing: &DxfOut<'_>, owner: u64) -> String {
582 let mut out = String::new();
583 for item in drawing.items {
584 let layer = drawing.layers[item.layer].name.as_str();
585 let kind = match &item.shape {
586 DxfShape::Curve(DxfCurve::Line { .. }) => "LINE",
587 DxfShape::Curve(DxfCurve::Circle { .. }) => "CIRCLE",
588 DxfShape::Curve(DxfCurve::Arc { .. }) => "ARC",
589 DxfShape::Curve(DxfCurve::Ellipse { .. }) => "ELLIPSE",
590 DxfShape::Curve(DxfCurve::Polyline { .. }) => "LWPOLYLINE",
591 DxfShape::Curve(DxfCurve::Spline { .. }) => "SPLINE",
592 DxfShape::Text(_) => "TEXT",
593 };
594 self.record(&mut out, kind, owner);
595 pair(&mut out, 100, "AcDbEntity");
596 pair(&mut out, 8, layer);
597 shape(&mut out, &item.shape);
598 }
599 out
600 }
601
602 fn objects(&mut self) -> String {
603 let mut out = String::new();
604 let root = self.record(&mut out, "DICTIONARY", 0);
605 pair(&mut out, 100, "AcDbDictionary");
606 pair(&mut out, 281, 1);
607 pair(&mut out, 3, "ACAD_GROUP");
608 let group = self.next;
610 pair(&mut out, 350, format_args!("{group:X}"));
611 self.record(&mut out, "DICTIONARY", root);
612 pair(&mut out, 100, "AcDbDictionary");
613 pair(&mut out, 281, 1);
614 out
615 }
616}
617
618fn shape(out: &mut String, shape: &DxfShape) {
620 match shape {
621 DxfShape::Curve(DxfCurve::Line { start, end }) => {
622 pair(out, 100, "AcDbLine");
623 point(out, 10, *start);
624 point(out, 11, *end);
625 }
626 DxfShape::Curve(DxfCurve::Circle { centre, radius }) => {
627 pair(out, 100, "AcDbCircle");
628 point(out, 10, *centre);
629 pair(out, 40, real(*radius));
630 }
631 DxfShape::Curve(DxfCurve::Arc {
632 centre,
633 radius,
634 start_angle,
635 end_angle,
636 }) => {
637 pair(out, 100, "AcDbCircle");
638 point(out, 10, *centre);
639 pair(out, 40, real(*radius));
640 pair(out, 100, "AcDbArc");
641 pair(out, 50, real(start_angle.to_degrees()));
642 pair(out, 51, real(end_angle.to_degrees()));
643 }
644 DxfShape::Curve(DxfCurve::Ellipse {
645 centre,
646 major,
647 ratio,
648 start_param,
649 end_param,
650 }) => {
651 pair(out, 100, "AcDbEllipse");
652 point(out, 10, *centre);
653 point(out, 11, Point2::new(major.x, major.y));
654 normal(out);
655 pair(out, 40, real(*ratio));
656 pair(out, 41, real(*start_param));
657 pair(out, 42, real(*end_param));
658 }
659 DxfShape::Curve(DxfCurve::Polyline { vertices, closed }) => {
660 pair(out, 100, "AcDbPolyline");
661 pair(out, 90, vertices.len());
662 pair(out, 70, u8::from(*closed));
663 pair(out, 43, "0.0");
664 for (p, bulge) in vertices {
665 pair(out, 10, real(p.x));
666 pair(out, 20, real(p.y));
667 if *bulge != 0.0 {
668 pair(out, 42, real(*bulge));
669 }
670 }
671 }
672 DxfShape::Curve(DxfCurve::Spline {
673 degree,
674 knots,
675 control_points,
676 weights,
677 closed,
678 }) => {
679 pair(out, 100, "AcDbSpline");
680 normal(out);
681 let flags = 8 | if weights.is_some() { 4 } else { 0 } | u8::from(*closed);
683 pair(out, 70, flags);
684 pair(out, 71, degree);
685 if knots.is_empty() {
686 pair(out, 72, 0);
687 pair(out, 73, 0);
688 pair(out, 74, control_points.len());
689 pair(out, 44, "0.0");
690 for p in control_points {
691 point(out, 11, *p);
692 }
693 } else {
694 pair(out, 72, knots.len());
695 pair(out, 73, control_points.len());
696 pair(out, 74, 0);
697 for k in knots {
698 pair(out, 40, real(*k));
699 }
700 for w in weights.iter().flatten() {
701 pair(out, 41, real(*w));
702 }
703 for p in control_points {
704 point(out, 10, *p);
705 }
706 }
707 }
708 DxfShape::Text(text) => {
709 pair(out, 100, "AcDbText");
710 point(out, 10, text.at);
711 pair(out, 40, real(text.height));
712 pair(out, 1, escaped(&text.text));
713 pair(out, 50, real(text.angle.to_degrees()));
714 pair(out, 7, "Standard");
715 pair(out, 100, "AcDbText");
716 }
717 }
718}
719
720fn normal(out: &mut String) {
722 pair(out, 210, "0.0");
723 pair(out, 220, "0.0");
724 pair(out, 230, "1.0");
725}