Skip to main content

ogeom_io/dxf/
entities_out.rs

1//! The R2000 writer: a drawing's entities as written, with its unit and
2//! layers.
3//!
4//! An R2000 (AC1015) file is read by its handles: every table, table entry,
5//! block, entity and object carries one, and each names its owner. The
6//! file holds what a strict reader asks for and nothing besides:
7//!
8//! - HEADER: `$ACADVER`, `$DWGCODEPAGE`, `$INSUNITS`, `$MEASUREMENT` and
9//!   `$HANDSEED`, the next free handle.
10//! - CLASSES, empty: no entity or object here needs a class.
11//! - TABLES: all nine, in the reference's order. VPORT, VIEW and UCS are
12//!   empty; LTYPE holds `ByBlock`, `ByLayer`, `CONTINUOUS` and `DASHED`;
13//!   LAYER holds layer `0` and the drawing's layers; STYLE holds
14//!   `Standard`; APPID holds `ACAD`; DIMSTYLE holds `Standard`;
15//!   BLOCK_RECORD holds `*Model_Space` and `*Paper_Space`.
16//! - BLOCKS: the two spaces' BLOCK and ENDBLK.
17//! - ENTITIES: each owned by `*Model_Space`.
18//! - OBJECTS: the root dictionary, naming the empty `ACAD_GROUP`
19//!   dictionary.
20
21use 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/// The unit a drawing's lengths are in, written as `$INSUNITS`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum DxfUnit {
30    /// No unit declared (`$INSUNITS` 0).
31    Unitless,
32    /// Millimetres (`$INSUNITS` 4).
33    Millimetre,
34    /// Inches (`$INSUNITS` 1).
35    Inch,
36}
37
38impl DxfUnit {
39    /// The `$INSUNITS` code.
40    #[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/// A layer: its name, and whether it draws dashed.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct DxfLayer {
53    /// The name: printable ASCII, none of `< > / \ " : ; ? * | = ``, no
54    /// space at either end, at most 255 characters. `0` names the layer
55    /// every file has.
56    pub name: String,
57    /// Dashed (`DASHED` linetype) rather than continuous.
58    pub dashed: bool,
59}
60
61/// What an item draws.
62#[derive(Debug, Clone, PartialEq)]
63pub enum DxfShape {
64    /// A curve, as [`read_dxf_entities`](super::read_dxf_entities) reads it.
65    Curve(DxfCurve),
66    /// A line of text.
67    Text(DxfText),
68}
69
70/// One entity to write, and the layer it goes on.
71#[derive(Debug, Clone, PartialEq)]
72pub struct DxfItem {
73    /// Index into [`DxfOut::layers`].
74    pub layer: usize,
75    /// What it draws.
76    pub shape: DxfShape,
77}
78
79/// A drawing to write with [`write_dxf_entities`].
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub struct DxfOut<'a> {
82    /// The unit of every length.
83    pub unit: DxfUnit,
84    /// The layers, each declared in the LAYER table whether drawn on or not.
85    pub layers: &'a [DxfLayer],
86    /// The entities, written in order.
87    pub items: &'a [DxfItem],
88}
89
90/// Write a drawing as an R2000 (AC1015) ASCII DXF, each entity as itself.
91///
92/// Lines, arcs, circles, ellipses, `LWPOLYLINE`s with their bulges,
93/// splines and `TEXT`, on named layers, with the unit as `$INSUNITS`.
94/// Coordinates are written in their shortest exact form, so
95/// [`read_dxf_entities`](super::read_dxf_entities) reads back the same
96/// numbers; angles go out in degrees, as the format has them. A spline
97/// with no knots is written by its fit points, as the reader returns one.
98/// Text outside printable ASCII is written as `\U+XXXX` escapes, as are a
99/// backslash, the first `%` of `%%`, and spaces at either end, so that the
100/// string reads back as given in any code page.
101///
102/// # Errors
103///
104/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if a
105/// layer's name is not one DXF allows or two names match ignoring case, an
106/// item names no layer, a number is not finite, a radius, a text height or
107/// a spline weight is not positive, an ellipse's major axis is zero or its
108/// ratio outside (0, 1], a polyline has fewer than two vertices or a bulge
109/// per vertex, a spline's knots do not number its control points plus its
110/// order or decrease, or a text holds a control character or a character
111/// outside the Basic Multilingual Plane.
112pub 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        // Fit points: a curve through them, of the degree given.
293        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
338/// A string as a DXF value that reads back as itself: anything outside
339/// printable ASCII, a backslash, the first `%` of `%%` and a space at
340/// either end become `\U+XXXX`.
341fn 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
357/// The TABLES section, and the block records other records name as owner.
358struct Tables {
359    text: String,
360    model_space: u64,
361    paper_space: u64,
362}
363
364/// Group-coded text with a running handle count; handles start at 1.
365struct 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    /// Open a record with its handle and owner, returning the handle.
407    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    /// A symbol table with its entries, each written by `entry` given the
420    /// table's handle.
421    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    /// Open a table entry: handle, owner, the two subclass markers, name
443    /// and flags.
444    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        // Layer 0 is in every file; a drawing's own `0` replaces it.
490        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        // The group dictionary is the next record, so takes the next handle.
609        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
618/// An entity's own groups, after its common ones.
619fn 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            // Planar, plus rational and closed as given.
682            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
720/// The extrusion of a drawing in the XY plane, seen from above.
721fn normal(out: &mut String) {
722    pair(out, 210, "0.0");
723    pair(out, 220, "0.0");
724    pair(out, 230, "1.0");
725}