3MF: flatten budgeted triangles but copied the whole vertex array for every placement, so an object with 100,000 vertices and one triangle placed by 500 build items reached 6.8 GB resident, and 10,000 items (still a ~200 KB file) asked for about 140 GB and aborted on the allocation. Copy only the vertices the triangles reach, keeping their relative order so an ordinary mesh is numbered as it was in the file. DXF: every bound in convert was on the geometry that comes out, and a block that emits none tripped none of them. Six self-inserts in one empty block fan out to 6^16 visits, and a 176-byte file never returned. Count entities visited and array cells placed against a work budget, skip the array loop for a block with no geometry, and bound the array size on its own. Fixes #144 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 3bf3b68a1340d9bdef8dcefd6c4fb5a7cae6eec4)
530 lines
23 KiB
Rust
530 lines
23 KiB
Rust
//! 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).
|
||
|
||
use std::collections::HashMap;
|
||
|
||
use solvecraft_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::<i32>() 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::<i32>().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<f64> {
|
||
self.f.iter().find(|(c, _)| *c == code).map(|(_, v)| *v)
|
||
}
|
||
fn all(&self, code: i32) -> Vec<f64> {
|
||
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<Vec<Geom2>> {
|
||
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<String, (Vec2, Vec<Ent>)> = 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<Ent>)> = 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<Vec<Ent>> {
|
||
let mut ents: Vec<Ent> = 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::<f64>() {
|
||
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<String, (Vec2, Vec<Ent>)>, depth: usize, out: &mut Vec<Geom2>, 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<f64> {
|
||
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<Geom2>) {
|
||
let flip = self.det() < 0.0;
|
||
let sampled = |f: &dyn Fn(f64) -> Vec2, closed: bool| {
|
||
let n = 32;
|
||
let mut pts: Vec<Vec2> = (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<Geom2>) {
|
||
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::*;
|
||
|
||
#[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());
|
||
}
|
||
}
|