//! ASCII DXF (the public AutoCAD DXF reference): the ENTITIES section's LINE, CIRCLE, ARC, //! LWPOLYLINE, POLYLINE/VERTEX, POINT, ELLIPSE, SPLINE, TEXT and MTEXT, scaled to millimetres by //! `$INSUNITS`. Block references (INSERT, with scale, rotation and arrays) are expanded, //! nested blocks too; paper-space entities are skipped. The writer emits an R12-style file of lines, circles, //! arcs and points (free-form curves as polylines); the flat pattern's drawing, with ELLIPSE and SPLINE, is //! [`crate::write_dxf_drawing`]. use std::collections::HashMap; use solidcraft_geom::Vec2; use crate::sketch2d::{Geom2, MAX_DRAWING_BYTES, MAX_ENTITIES, bulge_arc}; use crate::{IoError, Result}; /// Group code / value pairs. fn pairs(text: &str) -> Vec<(i32, String)> { let mut out = Vec::new(); let mut lines = text.lines(); while let (Some(c), Some(v)) = (lines.next(), lines.next()) { let Ok(code) = c.trim().parse::() else { continue }; out.push((code, v.trim().to_string())); } out } fn units_scale(p: &[(i32, String)]) -> f64 { // $INSUNITS: 1 in, 2 ft, 4 mm, 5 cm, 6 m, 0 unitless (taken as mm). let i = p.iter().position(|(c, v)| *c == 9 && v == "$INSUNITS"); match i.and_then(|i| p.get(i + 1)).and_then(|(_, v)| v.parse::().ok()) { Some(1) => 25.4, Some(2) => 304.8, Some(5) => 10.0, Some(6) => 1000.0, _ => 1.0, } } #[derive(Default)] struct Ent { kind: String, f: Vec<(i32, f64)>, s: Vec<(i32, String)>, } impl Ent { fn get(&self, code: i32) -> Option { self.f.iter().find(|(c, _)| *c == code).map(|(_, v)| *v) } fn all(&self, code: i32) -> Vec { self.f.iter().filter(|(c, _)| *c == code).map(|(_, v)| *v).collect() } fn text(&self, code: i32) -> Option<&str> { self.s.iter().find(|(c, _)| *c == code).map(|(_, v)| v.as_str()) } } /// Read the entities of an ASCII DXF file. pub fn read_dxf(text: &str) -> Result> { if text.len() > MAX_DRAWING_BYTES { return Err(IoError::Invalid("the DXF file is too large".into())); } if text.starts_with("AutoCAD Binary DXF") { return Err(IoError::Invalid("binary DXF is not supported; save as ASCII DXF".into())); } let p = pairs(text); let k = units_scale(&p); let start = p.iter().position(|(c, v)| *c == 2 && v == "ENTITIES").ok_or_else(|| IoError::Invalid("no ENTITIES section".into()))?; let ents = section(&p, start)?; // Blocks: name → (base point, entities). let mut blocks: HashMap)> = HashMap::new(); if let Some(bs) = p.iter().position(|(c, v)| *c == 2 && v == "BLOCKS") { let all = section(&p, bs)?; let mut cur: Option<(String, Vec2, Vec)> = None; for e in all { match e.kind.as_str() { "BLOCK" => { let base = Vec2::new(e.get(10).unwrap_or(0.0) * k, e.get(20).unwrap_or(0.0) * k); cur = Some((e.text(2).unwrap_or("").to_string(), base, Vec::new())); } "ENDBLK" => { if let Some((name, base, list)) = cur.take() { blocks.insert(name, (base, list)); } } _ => { if let Some(c) = cur.as_mut() { c.2.push(e); } } } } } let mut out = Vec::new(); convert(&ents, k, &blocks, 0, &mut out, &mut 0)?; Ok(out) } /// The entities of the section whose name is at `start`. fn section(p: &[(i32, String)], start: usize) -> Result> { let mut ents: Vec = Vec::new(); for (c, v) in p.iter().skip(start + 1) { if *c == 0 { if v == "ENDSEC" || v == "EOF" { break; } ents.push(Ent { kind: v.clone(), ..Default::default() }); if ents.len() > MAX_ENTITIES { return Err(IoError::Invalid("too many entities".into())); } continue; } let Some(e) = ents.last_mut() else { continue }; let numeric = matches!(*c, 10..=59 | 60..=99 | 140..=147 | 210..=239); match v.parse::() { Ok(x) if numeric && x.is_finite() && x.abs() < 1e12 => e.f.push((*c, x)), _ => e.s.push((*c, v.clone())), } } Ok(ents) } /// Deepest block nesting expanded. const MAX_BLOCK_DEPTH: usize = 16; /// Entities visited and array cells placed while expanding blocks. Every bound below this one /// is on the geometry that comes out, and a block that emits none trips none of them: N blocks /// that each insert the same empty block N times fan out to N^MAX_BLOCK_DEPTH visits for no /// output at all. This counts the work instead. const MAX_WORK: usize = 4 * MAX_ENTITIES; /// Entities to geometry (millimetres), expanding block references. fn convert(ents: &[Ent], k: f64, blocks: &HashMap)>, depth: usize, out: &mut Vec, work: &mut usize) -> Result<()> { let pt = |x: f64, y: f64| Vec2::new(x * k, y * k); let mut i = 0; while i < ents.len() { let Some(e) = ents.get(i) else { break }; i += 1; *work = work.saturating_add(1); if out.len() > MAX_ENTITIES || *work > MAX_WORK { return Err(IoError::Invalid("the drawing is too complex".into())); } // Paper space (layouts) is not the drawing. if e.get(67).is_some_and(|v| v != 0.0) { continue; } match e.kind.as_str() { "INSERT" => { let Some((base, list)) = e.text(2).and_then(|n| blocks.get(n)) else { continue }; if depth >= MAX_BLOCK_DEPTH { continue; } let mut inner = Vec::new(); convert(list, k, blocks, depth + 1, &mut inner, work)?; if inner.is_empty() { continue; } let (sx, sy) = (e.get(41).unwrap_or(1.0), e.get(42).unwrap_or(1.0)); let rot = e.get(50).unwrap_or(0.0).to_radians(); let ins = pt(e.get(10).unwrap_or(0.0), e.get(20).unwrap_or(0.0)); let (cols, rows) = (e.get(70).unwrap_or(1.0).clamp(1.0, 10_000.0) as usize, e.get(71).unwrap_or(1.0).clamp(1.0, 10_000.0) as usize); let cells = cols.saturating_mul(rows); *work = work.saturating_add(cells); if cells > MAX_ENTITIES || cells.saturating_mul(inner.len()) > MAX_ENTITIES || *work > MAX_WORK { return Err(IoError::Invalid("the drawing is too complex".into())); } let (dc, dr) = (e.get(44).unwrap_or(0.0) * k, e.get(45).unwrap_or(0.0) * k); for r in 0..rows { for c in 0..cols { // Array offsets run along the rotated block axes. let off = Vec2::from_angle(rot) * (dc * c as f64) + Vec2::from_angle(rot).perp() * (dr * r as f64); let t = Affine::insert(ins + off, *base, sx, sy, rot); for g in &inner { t.apply(g, out); } } } } "LINE" => { if let (Some(x0), Some(y0), Some(x1), Some(y1)) = (e.get(10), e.get(20), e.get(11), e.get(21)) { out.push(Geom2::Line(pt(x0, y0), pt(x1, y1))); } } "POINT" => { if let (Some(x), Some(y)) = (e.get(10), e.get(20)) { out.push(Geom2::Point(pt(x, y))); } } "CIRCLE" => { if let (Some(x), Some(y), Some(r)) = (e.get(10), e.get(20), e.get(40)) { out.push(Geom2::Circle(pt(x, y), r * k)); } } "ARC" => { if let (Some(x), Some(y), Some(r), Some(a0), Some(a1)) = (e.get(10), e.get(20), e.get(40), e.get(50), e.get(51)) { let c = pt(x, y); let at = |deg: f64| c + Vec2::from_angle(deg.to_radians()) * (r * k); out.push(Geom2::Arc { c, a: at(a0), b: at(a1) }); } } "LWPOLYLINE" => { let ys = e.all(20); // Bulges follow their vertex: walk the pairs in order. let mut verts: Vec<(Vec2, f64)> = Vec::new(); let mut xi = 0; for (c, v) in &e.f { match *c { 10 => { if let Some(y) = ys.get(xi) { verts.push((pt(*v, *y), 0.0)); } xi += 1; } 42 => { if let Some(last) = verts.last_mut() { last.1 = *v; } } _ => {} } } let closed = e.get(70).is_some_and(|f| (f as i64) & 1 == 1); polyline(&verts, closed, out); } "POLYLINE" => { let closed = e.get(70).is_some_and(|f| (f as i64) & 1 == 1); let mut verts = Vec::new(); while let Some(v) = ents.get(i) { if v.kind != "VERTEX" { break; } if let (Some(x), Some(y)) = (v.get(10), v.get(20)) { verts.push((pt(x, y), v.get(42).unwrap_or(0.0))); } i += 1; } if ents.get(i).is_some_and(|x| x.kind == "SEQEND") { i += 1; } polyline(&verts, closed, out); } "ELLIPSE" => { if let (Some(x), Some(y), Some(mx), Some(my), Some(ratio)) = (e.get(10), e.get(20), e.get(11), e.get(21), e.get(40)) { let c = pt(x, y); let major = c + Vec2::new(mx, my) * k; let a = Vec2::new(mx, my).len() * k; let (t0, t1) = (e.get(41).unwrap_or(0.0), e.get(42).unwrap_or(std::f64::consts::TAU)); if (t1 - t0 - std::f64::consts::TAU).abs() < 1e-9 || (t1 - t0).abs() < 1e-12 { out.push(Geom2::Ellipse { c, major, minor: a * ratio }); } else { // Partial ellipse: a fit spline through samples. let u = (major - c) / a.max(1e-12); let v = u.perp() * (a * ratio); let mut sweep = t1 - t0; while sweep <= 0.0 { sweep += std::f64::consts::TAU; } let n = 16; let pts = (0..=n) .map(|j| { let t = t0 + sweep * j as f64 / n as f64; c + u * (a * t.cos()) + v * t.sin() }) .collect(); out.push(Geom2::Spline { pts, control: false, degree: 3 }); } } } "SPLINE" => { let degree = e.get(71).unwrap_or(3.0).clamp(1.0, 7.0) as u8; let (fx, fy) = (e.all(11), e.all(21)); let (cx, cy) = (e.all(10), e.all(20)); if fx.len() >= 2 && fx.len() == fy.len() { out.push(Geom2::Spline { pts: fx.iter().zip(&fy).map(|(x, y)| pt(*x, *y)).collect(), control: false, degree: 3 }); } else if cx.len() >= 2 && cx.len() == cy.len() { out.push(Geom2::Spline { pts: cx.iter().zip(&cy).map(|(x, y)| pt(*x, *y)).collect(), control: true, degree }); } } "TEXT" | "MTEXT" => { if let (Some(x), Some(y), Some(h), Some(t)) = (e.get(10), e.get(20), e.get(40), e.text(1)) { // MTEXT paragraph codes: keep the plain text. let t = t.replace("\\P", "\n"); out.push(Geom2::Text { text: t, at: pt(x, y), height: h * k, angle: e.get(50).unwrap_or(0.0).to_radians() }); } } _ => {} } } Ok(()) } /// p ↦ m·p + t. #[derive(Clone, Copy)] struct Affine { m: [f64; 4], t: Vec2, } impl Affine { /// A block placed at `ins`: its base point moves there, scaled then rotated. fn insert(ins: Vec2, base: Vec2, sx: f64, sy: f64, rot: f64) -> Affine { let (s, c) = rot.sin_cos(); let m = [c * sx, -s * sy, s * sx, c * sy]; let mb = Vec2::new(m[0] * base.x + m[1] * base.y, m[2] * base.x + m[3] * base.y); Affine { m, t: ins - mb } } fn p(&self, p: Vec2) -> Vec2 { Vec2::new(self.m[0] * p.x + self.m[1] * p.y, self.m[2] * p.x + self.m[3] * p.y) + self.t } fn v(&self, v: Vec2) -> Vec2 { Vec2::new(self.m[0] * v.x + self.m[1] * v.y, self.m[2] * v.x + self.m[3] * v.y) } fn det(&self) -> f64 { self.m[0] * self.m[3] - self.m[1] * self.m[2] } /// The scale when it is the same in every direction. fn uniform(&self) -> Option { let (a, b) = (self.v(Vec2::new(1.0, 0.0)), self.v(Vec2::new(0.0, 1.0))); ((a.len() - b.len()).abs() <= 1e-9 * a.len().max(b.len()) && a.dot(b).abs() <= 1e-9 * a.len() * b.len()).then_some(a.len()) } fn apply(&self, g: &Geom2, out: &mut Vec) { let flip = self.det() < 0.0; let sampled = |f: &dyn Fn(f64) -> Vec2, closed: bool| { let n = 32; let mut pts: Vec = (0..=n).map(|j| self.p(f(j as f64 / n as f64))).collect(); if closed { pts.pop(); if let Some(first) = pts.first().copied() { pts.push(first); } } Geom2::Spline { pts, control: false, degree: 3 } }; out.push(match g { Geom2::Point(p) => Geom2::Point(self.p(*p)), Geom2::Line(a, b) => Geom2::Line(self.p(*a), self.p(*b)), Geom2::Circle(c, r) => match self.uniform() { Some(s) => Geom2::Circle(self.p(*c), r * s), None => { // The scaled axes are the ellipse's axes (scale comes before rotation). let (u, w) = (self.v(Vec2::new(*r, 0.0)), self.v(Vec2::new(0.0, *r))); let (maj, min) = if u.len() >= w.len() { (u, w) } else { (w, u) }; Geom2::Ellipse { c: self.p(*c), major: self.p(*c) + maj, minor: min.len() } } }, Geom2::Arc { c, a, b } => match self.uniform() { Some(_) if flip => Geom2::Arc { c: self.p(*c), a: self.p(*b), b: self.p(*a) }, Some(_) => Geom2::Arc { c: self.p(*c), a: self.p(*a), b: self.p(*b) }, None => { let (r, a0) = ((*a - *c).len(), (*a - *c).angle()); let mut sweep = (*b - *c).angle() - a0; while sweep <= 0.0 { sweep += std::f64::consts::TAU; } sampled(&|t| *c + Vec2::from_angle(a0 + sweep * t) * r, false) } }, Geom2::Ellipse { c, major, minor } => { let u = *major - *c; let w = u.perp() * (minor / u.len().max(1e-300)); match self.uniform() { Some(s) => Geom2::Ellipse { c: self.p(*c), major: self.p(*major), minor: minor * s }, None => sampled(&|t| *c + u * (t * std::f64::consts::TAU).cos() + w * (t * std::f64::consts::TAU).sin(), true), } } Geom2::Spline { pts, control, degree } => { Geom2::Spline { pts: pts.iter().map(|p| self.p(*p)).collect(), control: *control, degree: *degree } } Geom2::Conic { a, apex, b, rho } => Geom2::Conic { a: self.p(*a), apex: self.p(*apex), b: self.p(*b), rho: *rho }, Geom2::Text { text, at, height, angle } => { let dir = self.v(Vec2::from_angle(*angle)); let up = self.v(Vec2::from_angle(*angle).perp()); Geom2::Text { text: text.clone(), at: self.p(*at), height: height * up.len(), angle: dir.angle() } } }); } } fn polyline(verts: &[(Vec2, f64)], closed: bool, out: &mut Vec) { let n = verts.len(); let segs = if closed { n } else { n.saturating_sub(1) }; for j in 0..segs { let (Some((p0, b)), Some((p1, _))) = (verts.get(j), verts.get((j + 1) % n)) else { continue }; if p0.dist(*p1) < 1e-12 { continue; } if let Some(g) = bulge_arc(*p0, *p1, *b) { out.push(g); } } } /// Write 2D geometry (millimetres) as an R12-style ASCII DXF. pub fn write_dxf(geom: &[Geom2]) -> String { let mut s = String::new(); let mut w = |c: i32, v: &str| { s.push_str(&format!("{c}\n{v}\n")); }; w(0, "SECTION"); w(2, "HEADER"); w(9, "$INSUNITS"); w(70, "4"); w(0, "ENDSEC"); w(0, "SECTION"); w(2, "ENTITIES"); let f = |x: f64| format!("{x:.9}"); for g in geom { match g { Geom2::Point(p) => { w(0, "POINT"); w(8, "0"); w(10, &f(p.x)); w(20, &f(p.y)); } Geom2::Line(a, b) => { w(0, "LINE"); w(8, "0"); w(10, &f(a.x)); w(20, &f(a.y)); w(11, &f(b.x)); w(21, &f(b.y)); } Geom2::Circle(c, r) => { w(0, "CIRCLE"); w(8, "0"); w(10, &f(c.x)); w(20, &f(c.y)); w(40, &f(*r)); } Geom2::Arc { c, a, b } => { w(0, "ARC"); w(8, "0"); w(10, &f(c.x)); w(20, &f(c.y)); w(40, &f(c.dist(*a))); w(50, &f((*a - *c).angle().to_degrees())); w(51, &f((*b - *c).angle().to_degrees())); } Geom2::Text { text, at, height, angle } => { w(0, "TEXT"); w(8, "0"); w(10, &f(at.x)); w(20, &f(at.y)); w(40, &f(*height)); w(50, &f(angle.to_degrees())); w(1, &text.replace('\n', " ")); } // Free-form curves are written by the caller as polylines (lines). Geom2::Ellipse { .. } | Geom2::Spline { .. } | Geom2::Conic { .. } => {} } } w(0, "ENDSEC"); w(0, "EOF"); s } #[cfg(test)] mod tests { use super::*; /// A DXF is group codes and values the user did not write, and blocks recurse. None of these /// may panic (solidcraft#469). #[test] fn hostile_dxf_never_panics() { let good = write_dxf(&[Geom2::Line(Vec2::ZERO, Vec2::new(10.0, 0.0)), Geom2::Circle(Vec2::new(5.0, 5.0), 2.0)]); let mut cases: Vec = vec![ String::new(), "0".into(), "0\n".into(), "garbage".into(), "0\nSECTION\n2\n".into(), "0\nSECTION\n2\nENTITIES\n0\nEOF\n".into(), "0\nSECTION\n2\nENTITIES\n0\nLWPOLYLINE\n90\n4\n10\n0\n20\n0\n0\nENDSEC\n0\nEOF\n".into(), "0\nSECTION\n2\nENTITIES\n0\nLWPOLYLINE\n90\n-1\n0\nENDSEC\n0\nEOF\n".into(), "0\nSECTION\n2\nENTITIES\n0\nLWPOLYLINE\n90\n99999999\n0\nENDSEC\n0\nEOF\n".into(), "0\nSECTION\n2\nENTITIES\n0\nCIRCLE\n10\nnan\n20\ninf\n40\n-1\n0\nENDSEC\n0\nEOF\n".into(), "0\nSECTION\n2\nENTITIES\n0\nCIRCLE\n10\n1e400\n20\n1e400\n40\n1e400\n0\nENDSEC\n0\nEOF\n".into(), format!("0\nSECTION\n2\nBLOCKS\n{}0\nENDSEC\n0\nEOF\n", "0\nBLOCK\n2\nb\n0\nENDBLK\n".repeat(300)), "0\nSECTION\n2\nBLOCKS\n0\nBLOCK\n2\na\n1\nINSERT\n2\na\n0\nENDBLK\n0\nENDSEC\n0\nEOF\n".into(), format!("0\nSECTION\n2\nENTITIES\n{}0\nENDSEC\n0\nEOF\n", "0\nLINE\n10\n0\n20\n0\n11\n1\n21\n1\n".repeat(5000)), ]; for cut in (0..good.len()).step_by(11) { cases.push(good.get(..cut).unwrap_or("").to_string()); } for (from, to) in [("0\n", "1\n"), ("SECTION", "SECTI0N"), ("EOF", "EOFF")] { cases.push(good.replacen(from, to, 3)); } for c in &cases { let r = std::panic::catch_unwind(|| read_dxf(c)); assert!(r.is_ok(), "panicked on {:?}", c.get(..c.len().min(200))); } } #[test] fn round_trip_and_polyline_bulges() { let g = vec![ Geom2::Line(Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0)), Geom2::Circle(Vec2::new(5.0, 5.0), 2.0), Geom2::Arc { c: Vec2::ZERO, a: Vec2::new(3.0, 0.0), b: Vec2::new(0.0, 3.0) }, ]; let back = read_dxf(&write_dxf(&g)).unwrap(); assert_eq!(back.len(), 3); match &back[2] { Geom2::Arc { c, a, b } => { assert!(c.dist(Vec2::ZERO) < 1e-6 && a.dist(Vec2::new(3.0, 0.0)) < 1e-6 && b.dist(Vec2::new(0.0, 3.0)) < 1e-6); } x => panic!("{x:?}"), } // A closed LWPOLYLINE slot: two lines and two half circles (bulge 1), in inches. let dxf = "0\nSECTION\n2\nHEADER\n9\n$INSUNITS\n70\n1\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nLWPOLYLINE\n90\n4\n70\n1\n10\n0\n20\n0\n10\n2\n20\n0\n42\n1\n10\n2\n20\n1\n10\n0\n20\n1\n42\n1\n0\nENDSEC\n0\nEOF\n"; let g = read_dxf(dxf).unwrap(); assert_eq!(g.len(), 4, "{g:?}"); match &g[1] { Geom2::Arc { c, a, .. } => { assert!(c.dist(Vec2::new(50.8, 12.7)) < 1e-9, "{c:?}"); assert!((c.dist(*a) - 12.7).abs() < 1e-9); } x => panic!("{x:?}"), } assert!(read_dxf("garbage").is_err()); // A block of a unit square and a circle, inserted twice: rotated 90° at (10, 0), and as a // 2 × 1 array scaled 2 × 1 at (0, 20) (the circle becomes an ellipse). Nested once more. let dxf = "0\nSECTION\n2\nBLOCKS\n0\nBLOCK\n2\nSQ\n10\n0\n20\n0\n0\nLINE\n10\n0\n20\n0\n11\n1\n21\n0\n0\nCIRCLE\n10\n0\n20\n0\n40\n1\n0\nENDBLK\n0\nBLOCK\n2\nOUTER\n10\n0\n20\n0\n0\nINSERT\n2\nSQ\n10\n5\n20\n5\n0\nENDBLK\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nINSERT\n2\nSQ\n10\n10\n20\n0\n50\n90\n0\nINSERT\n2\nSQ\n10\n0\n20\n20\n41\n2\n42\n1\n70\n2\n44\n10\n0\nINSERT\n2\nOUTER\n10\n100\n20\n0\n0\nINSERT\n2\nNOPE\n0\nLINE\n67\n1\n10\n0\n20\n0\n11\n1\n21\n1\n0\nENDSEC\n0\nEOF\n"; let g = read_dxf(dxf).unwrap(); assert_eq!(g.len(), 8, "{g:?}"); match &g[0] { Geom2::Line(a, b) => assert!(a.dist(Vec2::new(10.0, 0.0)) < 1e-9 && b.dist(Vec2::new(10.0, 1.0)) < 1e-9, "{a:?} {b:?}"), x => panic!("{x:?}"), } match &g[5] { Geom2::Ellipse { c, major, minor } => { assert!(c.dist(Vec2::new(10.0, 20.0)) < 1e-9 && major.dist(Vec2::new(12.0, 20.0)) < 1e-9 && (minor - 1.0).abs() < 1e-9) } x => panic!("{x:?}"), } assert!(matches!(&g[7], Geom2::Circle(c, r) if c.dist(Vec2::new(105.0, 5.0)) < 1e-9 && (*r - 1.0).abs() < 1e-12)); // A block that inserts itself stops at the depth limit. let selfref = "0\nSECTION\n2\nBLOCKS\n0\nBLOCK\n2\nA\n0\nINSERT\n2\nA\n0\nLINE\n10\n0\n20\n0\n11\n1\n21\n0\n0\nENDBLK\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nINSERT\n2\nA\n0\nENDSEC\n0\nEOF\n"; assert_eq!(read_dxf(selfref).unwrap().len(), 16); assert!(read_dxf("AutoCAD Binary DXF\r\n").is_err()); } /// Bounds on the geometry that comes out do not see a block that emits none: six /// self-inserts fan out to 6^16 visits for no output. Both shapes must be refused quickly. #[test] fn empty_blocks_do_not_fan_out() { let t = std::time::Instant::now(); let fanout = format!( "0\nSECTION\n2\nBLOCKS\n0\nBLOCK\n2\nA\n{}0\nENDBLK\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n0\nINSERT\n2\nA\n0\nENDSEC\n0\nEOF\n", "0\nINSERT\n2\nA\n".repeat(6) ); assert!(read_dxf(&fanout).is_err(), "a block of six self-inserts must be refused"); // An empty block placed as a 10,000 by 10,000 array, a thousand times over. let array = format!( "0\nSECTION\n2\nBLOCKS\n0\nBLOCK\n2\nE\n0\nENDBLK\n0\nENDSEC\n0\nSECTION\n2\nENTITIES\n{}0\nENDSEC\n0\nEOF\n", "0\nINSERT\n2\nE\n70\n10000\n71\n10000\n".repeat(1000) ); let g = read_dxf(&array).unwrap_or_default(); assert!(g.is_empty(), "an empty block has no geometry to place"); assert!(t.elapsed() < std::time::Duration::from_secs(5), "took {:?}", t.elapsed()); } }