From bb218101c93081d6e6153454372cd9b1efda6602 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Sun, 15 Nov 2026 21:07:18 +0100 Subject: [PATCH] cad: export DXF through craft-dxf's drawing writer (#7847) * cad: export DXF through craft-dxf's drawing writer Delete the local ASCII writer and map the document through craft_dxf::drawing. Public vectorcraft_cad::export, DxfOptions and DxfVersion stay so vectorcraft#3068 can rebase without a writer API change. Pin craft-dxf at the craft-libs#1588 branch tip; update the rev after that PR merges. Golden files lock shapes_doc() bytes for R12 and 2018 against the old writer. Part of storytold/craft-libs#791 and #778. * cad: pin craft-dxf to the #1588 tip with Dylan's writer fixes Point craft-dxf (and craft-codepage from the same rev) at 0b6244f2b9971deaeb6a79d932c818ff94479757, which includes the Windows golden line-ending fix, BYBLOCK and image-dictionary key changes, and the compile fix for the dictionary-key test. Update the pin again to the merge commit after #1588 lands. * cad: pin shared DXF writer to merged craft-libs #1588 --------- Co-authored-by: FredApps --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/cad/Cargo.toml | 2 +- crates/cad/src/lib.rs | 23 +- crates/cad/src/scene.rs | 8 +- crates/cad/src/tests.rs | 20 + crates/cad/src/writer.rs | 674 -------------- crates/cad/tests/data/shapes_r12.dxf | 448 +++++++++ crates/cad/tests/data/shapes_r2018.dxf | 1146 ++++++++++++++++++++++++ 9 files changed, 1641 insertions(+), 686 deletions(-) delete mode 100644 crates/cad/src/writer.rs create mode 100644 crates/cad/tests/data/shapes_r12.dxf create mode 100644 crates/cad/tests/data/shapes_r2018.dxf diff --git a/Cargo.lock b/Cargo.lock index d80b61462e..96158329f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -987,7 +987,7 @@ dependencies = [ [[package]] name = "craft-codepage" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2" +source = "git+https://github.com/storytold/craft-libs?rev=8bd1412eaa7c0b58f3f9c401495d35fe12d6d85e#8bd1412eaa7c0b58f3f9c401495d35fe12d6d85e" [[package]] name = "craft-docpath" @@ -997,7 +997,7 @@ source = "git+https://github.com/storytold/craft-libs?rev=9be9a63782298ab59cd9b7 [[package]] name = "craft-dxf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=6d620e971e820b517b11a99cad040ebac670e1e2#6d620e971e820b517b11a99cad040ebac670e1e2" +source = "git+https://github.com/storytold/craft-libs?rev=8bd1412eaa7c0b58f3f9c401495d35fe12d6d85e#8bd1412eaa7c0b58f3f9c401495d35fe12d6d85e" dependencies = [ "craft-codepage", "encoding_rs", diff --git a/Cargo.toml b/Cargo.toml index e77c950b1a..76104004f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ craft-svg = { git = "https://github.com/storytold/craft-libs", rev = "6d620e971e # EPS container, previews, ASCII85 and DSC comments (craft-libs#778). craft-eps = { git = "https://github.com/storytold/craft-libs", rev = "6d620e971e820b517b11a99cad040ebac670e1e2" } # DXF tags, reader and writer (craft-libs#778, #926, #927). -craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "6d620e971e820b517b11a99cad040ebac670e1e2" } +craft-dxf = { git = "https://github.com/storytold/craft-libs", rev = "8bd1412eaa7c0b58f3f9c401495d35fe12d6d85e" } # WMF, EMF and EMF+ reader (craft-libs#778). craft-metafile = { git = "https://github.com/storytold/craft-libs", rev = "6d620e971e820b517b11a99cad040ebac670e1e2" } craft-xml = { git = "https://github.com/storytold/craft-libs", rev = "e2dc681a3" } diff --git a/crates/cad/Cargo.toml b/crates/cad/Cargo.toml index e529e1d4e2..4084e75d24 100644 --- a/crates/cad/Cargo.toml +++ b/crates/cad/Cargo.toml @@ -3,7 +3,7 @@ name = "vectorcraft-cad" version.workspace = true edition.workspace = true license.workspace = true -description = "VectorCraft CAD interchange: a hand-written ASCII DXF writer (R12 to 2018) and an HP-GL/2 plot file reader and writer" +description = "VectorCraft CAD interchange: DXF export through craft-dxf and an HP-GL/2 plot file reader and writer" [lints] workspace = true diff --git a/crates/cad/src/lib.rs b/crates/cad/src/lib.rs index bb752267aa..de9e7aea25 100644 --- a/crates/cad/src/lib.rs +++ b/crates/cad/src/lib.rs @@ -1,4 +1,4 @@ -//! CAD interchange: a hand-written ASCII DXF writer (versions R12 to 2018) and a reader on +//! CAD interchange: a DXF writer (versions R12 to 2018) on `craft_dxf::drawing` and a reader on //! `craft_dxf::model`, and an HP-GL/2 plot file reader and writer. //! //! [`export`] writes the visible art of a document into one DXF drawing: each layer becomes a DXF @@ -15,7 +15,7 @@ //! [`hpgl::import`] reads an HP-GL plotter file, and [`hpgl::export`] writes one: see [`hpgl`]. //! //! - `aci`: the 256-colour index palette and the nearest index of a colour. -//! - `writer`: group codes, handles and the file's sections, tables and objects. +//! - The ASCII file around those entities is written by `craft_dxf::drawing`. //! - `scene`: the document walk that turns objects into entities. //! - `import`: the reader, entities to art, blocks to symbols. //! - `hpgl`: the HP-GL reader, the pen's moves to paths, and the writer, paths to pen moves. @@ -24,7 +24,6 @@ mod aci; pub mod hpgl; pub mod import; mod scene; -mod writer; use vectorcraft_doc::{Document, Unit}; use vectorcraft_geom::Rect; @@ -136,11 +135,27 @@ impl DxfVersion { self >= Self::R2004 } /// Text is UTF-8 (earlier versions escape what isn't ASCII as `\U+XXXX`). - fn utf8(self) -> bool { + pub fn utf8(self) -> bool { self >= Self::R2007 } } +impl From for craft_dxf::drawing::Format { + fn from(v: DxfVersion) -> Self { + match v { + DxfVersion::R12 => Self::R12, + DxfVersion::R13 => Self::R13, + DxfVersion::R14 => Self::R14, + DxfVersion::R2000 => Self::R2000, + DxfVersion::R2004 => Self::R2004, + DxfVersion::R2007 => Self::R2007, + DxfVersion::R2010 => Self::R2010, + DxfVersion::R2013 => Self::R2013, + DxfVersion::R2018 => Self::R2018, + } + } +} + /// How many colours entities may use: indexed colours of the 256-colour palette, or true colour /// (with the nearest index for apps that read only the index). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/crates/cad/src/scene.rs b/crates/cad/src/scene.rs index ea3c962053..a5e229c2a4 100644 --- a/crates/cad/src/scene.rs +++ b/crates/cad/src/scene.rs @@ -14,8 +14,8 @@ use vectorcraft_effects::stroke; use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Rect, Vec2}; use crate::aci::nearest_aci; -use crate::writer::{Codes, Drawing, EntityStyle, Handle, ImageDef, Layer, LineType, fixed}; use crate::{CAP_HEIGHT, ColorDepth, DxfImage, DxfOptions, DxfOutput, MAX_NEST, Preserve, RasterFormat}; +use craft_dxf::drawing::{Codes, Drawing, EntityStyle, Handle, ImageDef, Layer, LineType, STANDARD_STYLE, fixed}; /// Curves are flattened to within this distance (points) where DXF needs straight segments /// (hatch boundaries, R12 polylines). @@ -543,7 +543,7 @@ impl<'a> Scene<'a> { o, to_dxf: Affine::new([k, 0.0, 0.0, -k, -r.x0 * k, r.y1 * k]), k, - e: Codes::new(o.version), + e: Codes::new(o.version.into()), next: fixed::FIRST_FREE, layers: vec![], layer_names: Names::with(&["0", "Defpoints"]), @@ -551,7 +551,7 @@ impl<'a> Scene<'a> { flags: LayerFlags { off: false, locked: false, plot: true }, linetypes: vec![], styles: vec![], - style_names: Names::with(&[crate::writer::STANDARD_STYLE]), + style_names: Names::with(&[STANDARD_STYLE]), images: vec![], files: vec![], by_key: HashMap::new(), @@ -579,7 +579,7 @@ impl<'a> Scene<'a> { let one = (self.o.scale - 1.0).abs() < 1e-12; let insunits = if one { crate::insunits_code(self.o.unit).unwrap_or(0) } else { 0 }; let drawing = Drawing { - version: self.o.version, + version: self.o.version.into(), layers: self.layers, linetypes: self.linetypes, styles: self.styles, diff --git a/crates/cad/src/tests.rs b/crates/cad/src/tests.rs index 84e547709d..48f6010925 100644 --- a/crates/cad/src/tests.rs +++ b/crates/cad/src/tests.rs @@ -1451,3 +1451,23 @@ fn symbols_fanning_out_into_empty_symbols_stop_at_the_symbol_budget() { assert!(entities(&out).is_empty()); assert!(out.warnings.iter().any(|w| w.contains("symbols placed inside symbols")), "{:?}", out.warnings); } + +#[test] +fn export_bytes_match_shared_writer_goldens() { + use std::path::PathBuf; + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data"); + let cases: [(&str, DxfVersion, Document); 2] = + [("shapes_r12.dxf", DxfVersion::R12, shapes_doc()), ("shapes_r2018.dxf", DxfVersion::R2018, shapes_doc())]; + for (name, version, doc) in cases { + let out = export(&doc, &DxfOptions { version, ..opts(&doc) }).unwrap(); + let got = std::str::from_utf8(&out.bytes).expect("UTF-8 DXF"); + let path = dir.join(name); + if std::env::var_os("WRITE_DXF_GOLDENS").is_some() { + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(&path, got).unwrap(); + continue; + } + let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + assert_eq!(got, expected, "DXF bytes changed versus {}", path.display()); + } +} diff --git a/crates/cad/src/writer.rs b/crates/cad/src/writer.rs deleted file mode 100644 index 6ba7e5a35d..0000000000 --- a/crates/cad/src/writer.rs +++ /dev/null @@ -1,674 +0,0 @@ -//! The ASCII DXF file: group codes, handles, and the sections, tables and objects around the -//! entities. DXF R13 and later give every object a handle and subclass markers; R2000 and later -//! add layouts and plot styles, which CAD apps expect to find. - -use std::fmt::Write as _; - -use crate::DxfVersion; - -/// An object handle (written in hex). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct Handle(pub u32); - -/// The handles of the fixed objects every drawing has. Entities and table entries count up from -/// [`FIRST_FREE`]. -pub(crate) mod fixed { - use super::Handle; - pub const LAYER_TABLE: Handle = Handle(0x1); - pub const LTYPE_TABLE: Handle = Handle(0x2); - pub const APPID_TABLE: Handle = Handle(0x3); - pub const DIMSTYLE_TABLE: Handle = Handle(0x4); - pub const STYLE_TABLE: Handle = Handle(0x5); - pub const UCS_TABLE: Handle = Handle(0x6); - pub const VIEW_TABLE: Handle = Handle(0x7); - pub const VPORT_TABLE: Handle = Handle(0x8); - pub const BLOCK_RECORD_TABLE: Handle = Handle(0x9); - pub const ROOT: Handle = Handle(0xA); - pub const GROUPS: Handle = Handle(0xB); - pub const LAYOUTS: Handle = Handle(0xC); - pub const MODEL_RECORD: Handle = Handle(0xD); - pub const PAPER_RECORD: Handle = Handle(0xE); - pub const MODEL_BLOCK: Handle = Handle(0xF); - pub const MODEL_END: Handle = Handle(0x10); - pub const PAPER_BLOCK: Handle = Handle(0x11); - pub const PAPER_END: Handle = Handle(0x12); - pub const MODEL_LAYOUT: Handle = Handle(0x13); - pub const PAPER_LAYOUT: Handle = Handle(0x14); - pub const ACTIVE_VPORT: Handle = Handle(0x15); - pub const ACAD_APPID: Handle = Handle(0x16); - pub const STANDARD_DIMSTYLE: Handle = Handle(0x17); - pub const IMAGE_DICT: Handle = Handle(0x18); - pub const IMAGE_VARS: Handle = Handle(0x19); - pub const PLOT_STYLES: Handle = Handle(0x1A); - pub const PLOT_NORMAL: Handle = Handle(0x1B); - pub const FIRST_FREE: u32 = 0x30; -} - -/// A number as DXF writes it: up to 6 decimals, no trailing zeros, 0 for anything not finite. -pub(crate) fn num(v: f64) -> String { - if !v.is_finite() { - return "0".into(); - } - let s = format!("{v:.6}"); - let s = s.trim_end_matches('0').trim_end_matches('.'); - if s == "-0" || s.is_empty() { "0".into() } else { s.into() } -} - -/// Text as a DXF string value: one line, `^` escaped, and before DXF 2007 (which reads UTF-8) -/// every character outside ASCII as `\U+XXXX`. -pub(crate) fn text(s: &str, utf8: bool) -> String { - let mut out = String::with_capacity(s.len()); - for c in s.chars() { - match c { - '\r' | '\n' | '\t' => out.push(' '), - c if c.is_control() => {} - '^' => out.push_str("^ "), - c if !utf8 && !c.is_ascii() => match u16::try_from(u32::from(c)) { - Ok(u) => _ = write!(out, "\\U+{u:04X}"), - Err(_) => out.push('?'), - }, - c => out.push(c), - } - } - out -} - -/// Group-code/value pairs in the order they are written. -pub(crate) struct Codes { - pub v: DxfVersion, - out: String, -} - -impl Codes { - pub fn new(v: DxfVersion) -> Self { - Self { v, out: String::new() } - } - - pub fn into_string(self) -> String { - self.out - } - - pub fn append(&mut self, other: &str) { - self.out.push_str(other); - } - - /// A value written as it is (names and keywords the writer itself chose). - pub fn raw(&mut self, code: u16, value: &str) { - _ = writeln!(self.out, "{code:>3}\n{value}"); - } - - /// Text from the document, encoded for the version (see [`text`]). - pub fn str(&mut self, code: u16, value: &str) { - let t = text(value, self.v.utf8()); - self.raw(code, &t); - } - - pub fn int(&mut self, code: u16, value: i64) { - _ = writeln!(self.out, "{code:>3}\n{value}"); - } - - pub fn num(&mut self, code: u16, value: f64) { - _ = writeln!(self.out, "{code:>3}\n{}", num(value)); - } - - pub fn handle(&mut self, code: u16, h: Handle) { - _ = writeln!(self.out, "{code:>3}\n{:X}", h.0); - } - - /// A 2D point at `code` (x), `code + 10` (y). - pub fn xy(&mut self, code: u16, x: f64, y: f64) { - self.num(code, x); - self.num(code + 10, y); - } - - /// A 3D point at `code`, `code + 10`, `code + 20` (z = 0). - pub fn xyz(&mut self, code: u16, x: f64, y: f64) { - self.xy(code, x, y); - self.num(code + 20, 0.0); - } - - /// The extrusion direction of planar entities: the z axis. - pub fn normal(&mut self) { - self.num(210, 0.0); - self.num(220, 0.0); - self.num(230, 1.0); - } - - /// The start of an object: its type, and from R13 its handle, owner and first subclass. - fn object(&mut self, kind: &str, h: Handle, owner: Handle, subclass: &str) { - self.raw(0, kind); - if self.v.handles() { - self.handle(if kind == "DIMSTYLE" { 105 } else { 5 }, h); - self.handle(330, owner); - self.raw(100, subclass); - } - } - - /// A table entry: the shared record subclass, then the table's own (from R13). - fn record(&mut self, kind: &str, h: Handle, table: Handle, subclass: &str) { - self.object(kind, h, table, "AcDbSymbolTableRecord"); - if self.v.handles() { - self.raw(100, subclass); - } - } - - fn section(&mut self, name: &str) { - self.raw(0, "SECTION"); - self.raw(2, name); - } - - fn end_section(&mut self) { - self.raw(0, "ENDSEC"); - } - - fn table(&mut self, name: &str, h: Handle, count: usize) { - self.raw(0, "TABLE"); - self.raw(2, name); - if self.v.handles() { - self.handle(5, h); - self.raw(330, "0"); - self.raw(100, "AcDbSymbolTable"); - } - self.int(70, count as i64); - if name == "DIMSTYLE" && self.v.r2000() { - self.raw(100, "AcDbDimStyleTable"); - } - } - - fn end_table(&mut self) { - self.raw(0, "ENDTAB"); - } -} - -/// How an entity is drawn: its colour index, true colour, lineweight, linetype and transparency. -#[derive(Clone, Debug, Default)] -pub(crate) struct EntityStyle { - pub aci: u8, - /// `0xRRGGBB` (DXF 2004 and later). - pub rgb: Option, - /// Hundredths of a millimetre (DXF 2000 and later). - pub lineweight: Option, - pub linetype: Option, - /// Opacity 0–255 (DXF 2004 and later; opaque is left out). - pub alpha: Option, -} - -impl Codes { - /// The start of an entity of `kind` owned by `owner` (model space, or a polyline for its - /// vertices) on `layer` with its style, ending with `subclass` (none when empty). - pub fn entity(&mut self, kind: &str, h: Handle, owner: Handle, layer: &str, style: &EntityStyle, subclass: &str) { - self.object(kind, h, owner, "AcDbEntity"); - self.str(8, layer); - if let Some(lt) = &style.linetype { - self.str(6, lt); - } - self.int(62, i64::from(style.aci.max(1))); - if self.v.r2000() - && let Some(lw) = style.lineweight - { - self.int(370, i64::from(lw)); - } - if self.v.true_color() { - if let Some(rgb) = style.rgb { - self.int(420, i64::from(rgb)); - } - if let Some(a) = style.alpha { - self.int(440, 0x0200_0000 | i64::from(a)); - } - } - if self.v.handles() && !subclass.is_empty() { - self.raw(100, subclass); - } - } -} - -/// A DXF layer. -#[derive(Clone, Debug)] -pub(crate) struct Layer { - pub name: String, - pub aci: u8, - /// Switched off (a hidden layer). - pub off: bool, - pub locked: bool, - /// Plotted (printable). - pub plot: bool, -} - -/// A dash pattern: dash, gap, dash… lengths in drawing units. -#[derive(Clone, Debug)] -pub(crate) struct LineType { - pub name: String, - pub pattern: Vec, -} - -/// A placed image file and the image entities that show it. -#[derive(Clone, Debug)] -pub(crate) struct ImageDef { - pub name: String, - pub size: (u32, u32), - pub handle: Handle, - /// `(reactor, image entity)` per image entity. - pub reactors: Vec<(Handle, Handle)>, -} - -/// Everything around the entities, collected while they were written. -pub(crate) struct Drawing { - pub version: DxfVersion, - pub layers: Vec, - pub linetypes: Vec, - /// Text styles: (style name, font family). - pub styles: Vec<(String, String)>, - pub images: Vec, - /// The entities' group codes. - pub entities: String, - /// `[x0, y0, x1, y1]` of everything written (drawing units). - pub extents: [f64; 4], - /// The export region (drawing units). - pub limits: [f64; 4], - /// `$INSUNITS`. - pub insunits: u8, - pub metric: bool, - /// The next free handle. - pub next: u32, -} - -/// The text style every drawing has. -pub(crate) const STANDARD_STYLE: &str = "Standard"; - -impl Drawing { - /// The whole file. - pub fn write(self) -> String { - let v = self.version; - let mut c = Codes::new(v); - // Table entries get the handles after the entities'. - let mut next = self.next; - let mut take = |n: usize| { - let start = next; - next = next.saturating_add(u32::try_from(n).unwrap_or(u32::MAX)); - start - }; - let ltype_base = take(3 + self.linetypes.len()); - let layer_base = take(1 + self.layers.len()); - let style_base = take(1 + self.styles.len()); - self.header(&mut c, next); - if v.handles() { - self.classes(&mut c); - } - c.section("TABLES"); - self.vport(&mut c); - self.ltypes(&mut c, ltype_base); - self.layer_table(&mut c, layer_base); - self.style_table(&mut c, style_base); - if v.handles() { - for (name, h) in [("VIEW", fixed::VIEW_TABLE), ("UCS", fixed::UCS_TABLE)] { - c.table(name, h, 0); - c.end_table(); - } - c.table("APPID", fixed::APPID_TABLE, 1); - c.record("APPID", fixed::ACAD_APPID, fixed::APPID_TABLE, "AcDbRegAppTableRecord"); - c.raw(2, "ACAD"); - c.int(70, 0); - c.end_table(); - c.table("DIMSTYLE", fixed::DIMSTYLE_TABLE, 1); - c.record("DIMSTYLE", fixed::STANDARD_DIMSTYLE, fixed::DIMSTYLE_TABLE, "AcDbDimStyleTableRecord"); - c.raw(2, "Standard"); - c.int(70, 0); - c.end_table(); - c.table("BLOCK_RECORD", fixed::BLOCK_RECORD_TABLE, 2); - for (name, h, layout) in - [("*Model_Space", fixed::MODEL_RECORD, fixed::MODEL_LAYOUT), ("*Paper_Space", fixed::PAPER_RECORD, fixed::PAPER_LAYOUT)] - { - c.record("BLOCK_RECORD", h, fixed::BLOCK_RECORD_TABLE, "AcDbBlockTableRecord"); - c.raw(2, name); - if v.r2000() { - c.handle(340, layout); - c.int(70, 0); - c.int(280, 1); - c.int(281, 0); - } - } - c.end_table(); - } - c.end_section(); - if v.handles() { - c.section("BLOCKS"); - for (name, record, begin, end) in [ - ("*Model_Space", fixed::MODEL_RECORD, fixed::MODEL_BLOCK, fixed::MODEL_END), - ("*Paper_Space", fixed::PAPER_RECORD, fixed::PAPER_BLOCK, fixed::PAPER_END), - ] { - c.object("BLOCK", begin, record, "AcDbEntity"); - c.raw(8, "0"); - c.raw(100, "AcDbBlockBegin"); - c.raw(2, name); - c.int(70, 0); - c.xyz(10, 0.0, 0.0); - c.raw(3, name); - c.raw(1, ""); - c.object("ENDBLK", end, record, "AcDbEntity"); - c.raw(8, "0"); - c.raw(100, "AcDbBlockEnd"); - } - c.end_section(); - } - c.section("ENTITIES"); - c.append(&self.entities); - c.end_section(); - if v.handles() { - self.objects(&mut c); - } - c.raw(0, "EOF"); - c.into_string() - } - - fn header(&self, c: &mut Codes, handseed: u32) { - let v = self.version; - let var = |c: &mut Codes, name: &str| c.raw(9, name); - c.section("HEADER"); - var(c, "$ACADVER"); - c.raw(1, v.acadver()); - var(c, "$DWGCODEPAGE"); - c.raw(3, "ANSI_1252"); - var(c, "$INSBASE"); - c.xyz(10, 0.0, 0.0); - let [x0, y0, x1, y1] = self.extents; - var(c, "$EXTMIN"); - c.xyz(10, x0, y0); - var(c, "$EXTMAX"); - c.xyz(10, x1, y1); - let [lx0, ly0, lx1, ly1] = self.limits; - var(c, "$LIMMIN"); - c.xy(10, lx0, ly0); - var(c, "$LIMMAX"); - c.xy(10, lx1, ly1); - if v.r2000() { - var(c, "$LWDISPLAY"); - c.int(290, 1); - var(c, "$INSUNITS"); - c.int(70, i64::from(self.insunits)); - var(c, "$MEASUREMENT"); - c.int(70, i64::from(self.metric)); - } - if v.handles() { - var(c, "$HANDSEED"); - c.handle(5, Handle(handseed)); - } - c.end_section(); - } - - fn classes(&self, c: &mut Codes) { - let mut classes: Vec<(&str, &str, &str, i64, bool)> = vec![]; - if self.version.r2000() { - classes.extend([ - ("ACDBDICTIONARYWDFLT", "AcDbDictionaryWithDefault", "ObjectDBX Classes", 0, false), - ("ACDBPLACEHOLDER", "AcDbPlaceHolder", "ObjectDBX Classes", 0, false), - ("LAYOUT", "AcDbLayout", "ObjectDBX Classes", 0, false), - ]); - } - if !self.images.is_empty() { - classes.extend([ - ("RASTERVARIABLES", "AcDbRasterVariables", "ISM", 0, false), - ("IMAGE", "AcDbRasterImage", "ISM", 2175, true), - ("IMAGEDEF", "AcDbRasterImageDef", "ISM", 0, false), - ("IMAGEDEF_REACTOR", "AcDbRasterImageDefReactor", "ISM", 1, false), - ]); - } - c.section("CLASSES"); - for (name, cpp, app, flags, entity) in classes { - c.raw(0, "CLASS"); - c.raw(1, name); - c.raw(2, cpp); - c.raw(3, app); - c.int(90, flags); - if self.version.true_color() { - c.int(91, 0); - } - c.int(280, 0); - c.int(281, i64::from(entity)); - } - c.end_section(); - } - - /// The active viewport, zoomed to the drawing's extents. - fn vport(&self, c: &mut Codes) { - let [x0, y0, x1, y1] = self.extents; - c.table("VPORT", fixed::VPORT_TABLE, 1); - c.record("VPORT", fixed::ACTIVE_VPORT, fixed::VPORT_TABLE, "AcDbViewportTableRecord"); - c.raw(2, if self.version.handles() { "*Active" } else { "*ACTIVE" }); - c.int(70, 0); - c.xy(10, 0.0, 0.0); - c.xy(11, 1.0, 1.0); - c.xy(12, (x0 + x1) / 2.0, (y0 + y1) / 2.0); - c.xy(13, 0.0, 0.0); - c.xy(14, 1.0, 1.0); - c.xy(15, 10.0, 10.0); - // The view direction looks down the z axis: (0, 0, 1), each code once. - c.xy(16, 0.0, 0.0); - c.num(36, 1.0); - c.xyz(17, 0.0, 0.0); - let (w, h) = ((x1 - x0).abs(), (y1 - y0).abs()); - // The view's height, with a margin; its width follows from the aspect ratio. - c.num(40, (h.max(w / 1.5) * 1.1).max(1.0)); - c.num(41, 1.5); - c.num(42, 50.0); - c.num(43, 0.0); - c.num(44, 0.0); - c.num(50, 0.0); - c.num(51, 0.0); - for (code, value) in [(71, 0), (72, 1000), (73, 1), (74, 3), (75, 0), (76, 0), (77, 0), (78, 0)] { - c.int(code, value); - } - if self.version.r2000() { - c.int(281, 0); - c.int(65, 0); - c.num(146, 0.0); - } - c.end_table(); - } - - fn ltypes(&self, c: &mut Codes, base: u32) { - c.table("LTYPE", fixed::LTYPE_TABLE, 3 + self.linetypes.len()); - let solid = [("ByBlock", ""), ("ByLayer", ""), ("Continuous", "Solid line")]; - let rows = solid.iter().map(|(n, d)| (*n, *d, &[][..])).chain(self.linetypes.iter().map(|l| (l.name.as_str(), "", l.pattern.as_slice()))); - for (i, (name, desc, pattern)) in rows.enumerate() { - c.record("LTYPE", Handle(base.saturating_add(i as u32)), fixed::LTYPE_TABLE, "AcDbLinetypeTableRecord"); - c.str(2, name); - c.int(70, 0); - c.str(3, if pattern.is_empty() { desc } else { "Dashed" }); - c.int(72, 65); - c.int(73, pattern.len() as i64); - c.num(40, pattern.iter().map(|v| v.abs()).sum()); - for v in pattern { - c.num(49, *v); - if self.version.handles() { - c.int(74, 0); - } - } - } - c.end_table(); - } - - fn layer_table(&self, c: &mut Codes, base: u32) { - let zero = Layer { name: "0".into(), aci: 7, off: false, locked: false, plot: true }; - c.table("LAYER", fixed::LAYER_TABLE, 1 + self.layers.len()); - for (i, l) in std::iter::once(&zero).chain(&self.layers).enumerate() { - c.record("LAYER", Handle(base.saturating_add(i as u32)), fixed::LAYER_TABLE, "AcDbLayerTableRecord"); - c.str(2, &l.name); - c.int(70, if l.locked { 4 } else { 0 }); - // A negative colour switches the layer off. - let aci = i64::from(l.aci.max(1)); - c.int(62, if l.off { -aci } else { aci }); - c.raw(6, "Continuous"); - if self.version.r2000() { - c.int(290, i64::from(l.plot)); - c.int(370, -3); - c.handle(390, fixed::PLOT_NORMAL); - } - } - c.end_table(); - } - - fn style_table(&self, c: &mut Codes, base: u32) { - c.table("STYLE", fixed::STYLE_TABLE, 1 + self.styles.len()); - let standard = (STANDARD_STYLE.to_string(), String::new()); - for (i, (name, family)) in std::iter::once(&standard).chain(&self.styles).enumerate() { - c.record("STYLE", Handle(base.saturating_add(i as u32)), fixed::STYLE_TABLE, "AcDbTextStyleTableRecord"); - c.str(2, name); - c.int(70, 0); - c.num(40, 0.0); - c.num(41, 1.0); - c.num(50, 0.0); - c.int(71, 0); - c.num(42, 2.5); - // The font file; from 2000 the family name too, which is how CAD apps find a TrueType font. - if family.is_empty() { - c.raw(3, "txt"); - } else { - c.str(3, &format!("{family}.ttf")); - } - c.raw(4, ""); - if self.version.r2000() && !family.is_empty() { - c.raw(1001, "ACAD"); - c.str(1000, family); - c.int(1071, 0); - } - } - c.end_table(); - } - - fn objects(&self, c: &mut Codes) { - let v = self.version; - let images = !self.images.is_empty(); - c.section("OBJECTS"); - let dict = |c: &mut Codes, h: Handle, owner: Handle, entries: &[(&str, Handle)]| { - c.object("DICTIONARY", h, owner, "AcDbDictionary"); - c.int(281, 1); - for (name, e) in entries { - c.str(3, name); - c.handle(350, *e); - } - }; - let mut root = vec![("ACAD_GROUP", fixed::GROUPS)]; - if images { - root.extend([("ACAD_IMAGE_DICT", fixed::IMAGE_DICT), ("ACAD_IMAGE_VARS", fixed::IMAGE_VARS)]); - } - if v.r2000() { - root.extend([("ACAD_LAYOUT", fixed::LAYOUTS), ("ACAD_PLOTSTYLENAME", fixed::PLOT_STYLES)]); - } - dict(c, fixed::ROOT, Handle(0), &root); - dict(c, fixed::GROUPS, fixed::ROOT, &[]); - if v.r2000() { - dict(c, fixed::LAYOUTS, fixed::ROOT, &[("Layout1", fixed::PAPER_LAYOUT), ("Model", fixed::MODEL_LAYOUT)]); - c.object("ACDBDICTIONARYWDFLT", fixed::PLOT_STYLES, fixed::ROOT, "AcDbDictionary"); - c.int(281, 1); - c.raw(3, "Normal"); - c.handle(350, fixed::PLOT_NORMAL); - c.raw(100, "AcDbDictionaryWithDefault"); - c.handle(340, fixed::PLOT_NORMAL); - // A placeholder has no data and no subclass marker of its own. - c.raw(0, "ACDBPLACEHOLDER"); - c.handle(5, fixed::PLOT_NORMAL); - c.handle(330, fixed::PLOT_STYLES); - self.layout(c, fixed::MODEL_LAYOUT, "Model", 0, fixed::MODEL_RECORD); - self.layout(c, fixed::PAPER_LAYOUT, "Layout1", 1, fixed::PAPER_RECORD); - } - if images { - c.object("RASTERVARIABLES", fixed::IMAGE_VARS, fixed::ROOT, "AcDbRasterVariables"); - c.int(90, 0); - c.int(70, 0); - c.int(71, 1); - c.int(72, 0); - let entries: Vec<(&str, Handle)> = self.images.iter().map(|i| (i.name.as_str(), i.handle)).collect(); - dict(c, fixed::IMAGE_DICT, fixed::ROOT, &entries); - for img in &self.images { - c.raw(0, "IMAGEDEF"); - c.handle(5, img.handle); - c.raw(102, "{ACAD_REACTORS"); - c.handle(330, fixed::IMAGE_DICT); - for (reactor, _) in &img.reactors { - c.handle(330, *reactor); - } - c.raw(102, "}"); - c.handle(330, fixed::IMAGE_DICT); - c.raw(100, "AcDbRasterImageDef"); - c.int(90, 0); - c.str(1, &img.name); - c.xy(10, f64::from(img.size.0), f64::from(img.size.1)); - c.xy(11, 1.0, 1.0); - c.int(280, 1); - c.int(281, 0); - for (reactor, image) in &img.reactors { - c.object("IMAGEDEF_REACTOR", *reactor, *image, "AcDbRasterImageDefReactor"); - c.int(90, 2); - c.handle(330, *image); - } - } - } - c.end_section(); - } - - /// A layout (DXF 2000 and later): its plot settings, then the layout itself. - fn layout(&self, c: &mut Codes, h: Handle, name: &str, tab: i64, record: Handle) { - let [x0, y0, x1, y1] = self.extents; - c.object("LAYOUT", h, fixed::LAYOUTS, "AcDbPlotSettings"); - c.raw(1, ""); - c.raw(2, "none_device"); - c.raw(4, ""); - c.raw(6, ""); - for code in 40..=49 { - c.num(code, 0.0); - } - c.num(140, 0.0); - c.num(141, 0.0); - c.num(142, 1.0); - c.num(143, 1.0); - // Plot flags; 1024 marks the model space layout. - c.int(70, if record == fixed::MODEL_RECORD { 688 | 1024 } else { 688 }); - c.int(72, 1); - c.int(73, 0); - c.int(74, 5); - c.raw(7, ""); - c.int(75, 16); - if self.version.true_color() { - // Shade plot mode (as displayed), resolution level (normal) and custom DPI. - c.int(76, 0); - c.int(77, 2); - c.int(78, 300); - } - c.num(147, 1.0); - // The paper image origin is 148 (x) and 149 (y), not a point at 148 and 158. - c.num(148, 0.0); - c.num(149, 0.0); - c.raw(100, "AcDbLayout"); - c.raw(1, name); - c.int(70, 1); - c.int(71, tab); - let [lx0, ly0, lx1, ly1] = self.limits; - c.xy(10, lx0, ly0); - c.xy(11, lx1, ly1); - c.xyz(12, 0.0, 0.0); - c.xyz(14, x0, y0); - c.xyz(15, x1, y1); - c.num(146, 0.0); - c.xyz(13, 0.0, 0.0); - c.xyz(16, 1.0, 0.0); - c.xyz(17, 0.0, 1.0); - c.int(76, 1); - c.handle(330, record); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn numbers_and_text_are_written_plainly() { - assert_eq!(num(1.5), "1.5"); - assert_eq!(num(-0.0000001), "0"); - assert_eq!(num(2.0), "2"); - assert_eq!(num(f64::NAN), "0"); - assert_eq!(text("a^b\nc", true), "a^ b c"); - assert_eq!(text("Größe 😀", false), "Gr\\U+00F6\\U+00DFe ?"); - assert_eq!(text("Größe", true), "Größe"); - } -} diff --git a/crates/cad/tests/data/shapes_r12.dxf b/crates/cad/tests/data/shapes_r12.dxf new file mode 100644 index 0000000000..96ea7b638e --- /dev/null +++ b/crates/cad/tests/data/shapes_r12.dxf @@ -0,0 +1,448 @@ + 0 +SECTION + 2 +HEADER + 9 +$ACADVER + 1 +AC1009 + 9 +$DWGCODEPAGE + 3 +ANSI_1252 + 9 +$INSBASE + 10 +0 + 20 +0 + 30 +0 + 9 +$EXTMIN + 10 +10 + 20 +20 + 30 +0 + 9 +$EXTMAX + 10 +180 + 20 +90 + 30 +0 + 9 +$LIMMIN + 10 +0 + 20 +0 + 9 +$LIMMAX + 10 +200 + 20 +100 + 0 +ENDSEC + 0 +SECTION + 2 +TABLES + 0 +TABLE + 2 +VPORT + 70 +1 + 0 +VPORT + 2 +*ACTIVE + 70 +0 + 10 +0 + 20 +0 + 11 +1 + 21 +1 + 12 +95 + 22 +55 + 13 +0 + 23 +0 + 14 +1 + 24 +1 + 15 +10 + 25 +10 + 16 +0 + 26 +0 + 36 +1 + 17 +0 + 27 +0 + 37 +0 + 40 +124.666667 + 41 +1.5 + 42 +50 + 43 +0 + 44 +0 + 50 +0 + 51 +0 + 71 +0 + 72 +1000 + 73 +1 + 74 +3 + 75 +0 + 76 +0 + 77 +0 + 78 +0 + 0 +ENDTAB + 0 +TABLE + 2 +LTYPE + 70 +3 + 0 +LTYPE + 2 +ByBlock + 70 +0 + 3 + + 72 +65 + 73 +0 + 40 +0 + 0 +LTYPE + 2 +ByLayer + 70 +0 + 3 + + 72 +65 + 73 +0 + 40 +0 + 0 +LTYPE + 2 +Continuous + 70 +0 + 3 +Solid line + 72 +65 + 73 +0 + 40 +0 + 0 +ENDTAB + 0 +TABLE + 2 +LAYER + 70 +2 + 0 +LAYER + 2 +0 + 70 +0 + 62 +7 + 6 +Continuous + 0 +LAYER + 2 +LAYER_1 + 70 +0 + 62 +171 + 6 +Continuous + 0 +ENDTAB + 0 +TABLE + 2 +STYLE + 70 +1 + 0 +STYLE + 2 +Standard + 70 +0 + 40 +0 + 41 +1 + 50 +0 + 71 +0 + 42 +2.5 + 3 +txt + 4 + + 0 +ENDTAB + 0 +ENDSEC + 0 +SECTION + 2 +ENTITIES + 0 +POLYLINE + 8 +LAYER_1 + 62 +1 + 66 +1 + 10 +0 + 20 +0 + 30 +0 + 70 +1 + 0 +VERTEX + 8 +LAYER_1 + 62 +1 + 10 +10 + 20 +90 + 30 +0 + 0 +VERTEX + 8 +LAYER_1 + 62 +1 + 10 +110 + 20 +90 + 30 +0 + 0 +VERTEX + 8 +LAYER_1 + 62 +1 + 10 +110 + 20 +40 + 30 +0 + 0 +VERTEX + 8 +LAYER_1 + 62 +1 + 10 +10 + 20 +40 + 30 +0 + 0 +SEQEND + 8 +LAYER_1 + 62 +1 + 0 +POLYLINE + 8 +LAYER_1 + 62 +7 + 66 +1 + 10 +0 + 20 +0 + 30 +0 + 70 +1 + 40 +1 + 41 +1 + 0 +VERTEX + 8 +LAYER_1 + 62 +7 + 10 +10 + 20 +90 + 30 +0 + 0 +VERTEX + 8 +LAYER_1 + 62 +7 + 10 +110 + 20 +90 + 30 +0 + 0 +VERTEX + 8 +LAYER_1 + 62 +7 + 10 +110 + 20 +40 + 30 +0 + 0 +VERTEX + 8 +LAYER_1 + 62 +7 + 10 +10 + 20 +40 + 30 +0 + 0 +SEQEND + 8 +LAYER_1 + 62 +7 + 0 +POLYLINE + 8 +LAYER_1 + 62 +5 + 66 +1 + 10 +0 + 20 +0 + 30 +0 + 70 +1 + 40 +2 + 41 +2 + 0 +VERTEX + 8 +LAYER_1 + 62 +5 + 10 +120 + 20 +50 + 30 +0 + 42 +-2.414214 + 0 +VERTEX + 8 +LAYER_1 + 62 +5 + 10 +150 + 20 +20 + 30 +0 + 42 +-0.414214 + 0 +SEQEND + 8 +LAYER_1 + 62 +5 + 0 +ENDSEC + 0 +EOF diff --git a/crates/cad/tests/data/shapes_r2018.dxf b/crates/cad/tests/data/shapes_r2018.dxf new file mode 100644 index 0000000000..c29b89a037 --- /dev/null +++ b/crates/cad/tests/data/shapes_r2018.dxf @@ -0,0 +1,1146 @@ + 0 +SECTION + 2 +HEADER + 9 +$ACADVER + 1 +AC1032 + 9 +$DWGCODEPAGE + 3 +ANSI_1252 + 9 +$INSBASE + 10 +0 + 20 +0 + 30 +0 + 9 +$EXTMIN + 10 +10 + 20 +20 + 30 +0 + 9 +$EXTMAX + 10 +180 + 20 +90 + 30 +0 + 9 +$LIMMIN + 10 +0 + 20 +0 + 9 +$LIMMAX + 10 +200 + 20 +100 + 9 +$LWDISPLAY +290 +1 + 9 +$INSUNITS + 70 +0 + 9 +$MEASUREMENT + 70 +0 + 9 +$HANDSEED + 5 +3A + 0 +ENDSEC + 0 +SECTION + 2 +CLASSES + 0 +CLASS + 1 +ACDBDICTIONARYWDFLT + 2 +AcDbDictionaryWithDefault + 3 +ObjectDBX Classes + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +ACDBPLACEHOLDER + 2 +AcDbPlaceHolder + 3 +ObjectDBX Classes + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +CLASS + 1 +LAYOUT + 2 +AcDbLayout + 3 +ObjectDBX Classes + 90 +0 + 91 +0 +280 +0 +281 +0 + 0 +ENDSEC + 0 +SECTION + 2 +TABLES + 0 +TABLE + 2 +VPORT + 5 +8 +330 +0 +100 +AcDbSymbolTable + 70 +1 + 0 +VPORT + 5 +15 +330 +8 +100 +AcDbSymbolTableRecord +100 +AcDbViewportTableRecord + 2 +*Active + 70 +0 + 10 +0 + 20 +0 + 11 +1 + 21 +1 + 12 +95 + 22 +55 + 13 +0 + 23 +0 + 14 +1 + 24 +1 + 15 +10 + 25 +10 + 16 +0 + 26 +0 + 36 +1 + 17 +0 + 27 +0 + 37 +0 + 40 +124.666667 + 41 +1.5 + 42 +50 + 43 +0 + 44 +0 + 50 +0 + 51 +0 + 71 +0 + 72 +1000 + 73 +1 + 74 +3 + 75 +0 + 76 +0 + 77 +0 + 78 +0 +281 +0 + 65 +0 +146 +0 + 0 +ENDTAB + 0 +TABLE + 2 +LTYPE + 5 +2 +330 +0 +100 +AcDbSymbolTable + 70 +3 + 0 +LTYPE + 5 +34 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +ByBlock + 70 +0 + 3 + + 72 +65 + 73 +0 + 40 +0 + 0 +LTYPE + 5 +35 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +ByLayer + 70 +0 + 3 + + 72 +65 + 73 +0 + 40 +0 + 0 +LTYPE + 5 +36 +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +Continuous + 70 +0 + 3 +Solid line + 72 +65 + 73 +0 + 40 +0 + 0 +ENDTAB + 0 +TABLE + 2 +LAYER + 5 +1 +330 +0 +100 +AcDbSymbolTable + 70 +2 + 0 +LAYER + 5 +37 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +0 + 70 +0 + 62 +7 + 6 +Continuous +290 +1 +370 +-3 +390 +1B + 0 +LAYER + 5 +38 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +Layer 1 + 70 +0 + 62 +171 + 6 +Continuous +290 +1 +370 +-3 +390 +1B + 0 +ENDTAB + 0 +TABLE + 2 +STYLE + 5 +5 +330 +0 +100 +AcDbSymbolTable + 70 +1 + 0 +STYLE + 5 +39 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +Standard + 70 +0 + 40 +0 + 41 +1 + 50 +0 + 71 +0 + 42 +2.5 + 3 +txt + 4 + + 0 +ENDTAB + 0 +TABLE + 2 +VIEW + 5 +7 +330 +0 +100 +AcDbSymbolTable + 70 +0 + 0 +ENDTAB + 0 +TABLE + 2 +UCS + 5 +6 +330 +0 +100 +AcDbSymbolTable + 70 +0 + 0 +ENDTAB + 0 +TABLE + 2 +APPID + 5 +3 +330 +0 +100 +AcDbSymbolTable + 70 +1 + 0 +APPID + 5 +16 +330 +3 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD + 70 +0 + 0 +ENDTAB + 0 +TABLE + 2 +DIMSTYLE + 5 +4 +330 +0 +100 +AcDbSymbolTable + 70 +1 +100 +AcDbDimStyleTable + 0 +DIMSTYLE +105 +17 +330 +4 +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +Standard + 70 +0 + 0 +ENDTAB + 0 +TABLE + 2 +BLOCK_RECORD + 5 +9 +330 +0 +100 +AcDbSymbolTable + 70 +2 + 0 +BLOCK_RECORD + 5 +D +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Model_Space +340 +13 + 70 +0 +280 +1 +281 +0 + 0 +BLOCK_RECORD + 5 +E +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Paper_Space +340 +14 + 70 +0 +280 +1 +281 +0 + 0 +ENDTAB + 0 +ENDSEC + 0 +SECTION + 2 +BLOCKS + 0 +BLOCK + 5 +F +330 +D +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*Model_Space + 70 +0 + 10 +0 + 20 +0 + 30 +0 + 3 +*Model_Space + 1 + + 0 +ENDBLK + 5 +10 +330 +D +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +11 +330 +E +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*Paper_Space + 70 +0 + 10 +0 + 20 +0 + 30 +0 + 3 +*Paper_Space + 1 + + 0 +ENDBLK + 5 +12 +330 +E +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +ENDSEC + 0 +SECTION + 2 +ENTITIES + 0 +LWPOLYLINE + 5 +30 +330 +D +100 +AcDbEntity + 8 +Layer 1 + 62 +1 +420 +16711680 +100 +AcDbPolyline + 90 +4 + 70 +1 + 10 +10 + 20 +90 + 10 +110 + 20 +90 + 10 +110 + 20 +40 + 10 +10 + 20 +40 + 0 +HATCH + 5 +31 +330 +D +100 +AcDbEntity + 8 +Layer 1 + 62 +1 +420 +16711680 +100 +AcDbHatch + 10 +0 + 20 +0 + 30 +0 +210 +0 +220 +0 +230 +1 + 2 +SOLID + 70 +1 + 71 +0 + 91 +1 + 92 +3 + 72 +0 + 73 +1 + 93 +4 + 10 +10 + 20 +90 + 10 +110 + 20 +90 + 10 +110 + 20 +40 + 10 +10 + 20 +40 + 97 +0 + 75 +0 + 76 +1 + 98 +0 + 0 +LWPOLYLINE + 5 +32 +330 +D +100 +AcDbEntity + 8 +Layer 1 + 62 +7 +370 +35 +420 +0 +100 +AcDbPolyline + 90 +4 + 70 +1 + 10 +10 + 20 +90 + 10 +110 + 20 +90 + 10 +110 + 20 +40 + 10 +10 + 20 +40 + 0 +CIRCLE + 5 +33 +330 +D +100 +AcDbEntity + 8 +Layer 1 + 62 +5 +370 +70 +420 +255 +100 +AcDbCircle + 10 +150 + 20 +50 + 30 +0 + 40 +30 +210 +0 +220 +0 +230 +1 + 0 +ENDSEC + 0 +SECTION + 2 +OBJECTS + 0 +DICTIONARY + 5 +A +330 +0 +100 +AcDbDictionary +281 +1 + 3 +ACAD_GROUP +350 +B + 3 +ACAD_LAYOUT +350 +C + 3 +ACAD_PLOTSTYLENAME +350 +1A + 0 +DICTIONARY + 5 +B +330 +A +100 +AcDbDictionary +281 +1 + 0 +DICTIONARY + 5 +C +330 +A +100 +AcDbDictionary +281 +1 + 3 +Layout1 +350 +14 + 3 +Model +350 +13 + 0 +ACDBDICTIONARYWDFLT + 5 +1A +330 +A +100 +AcDbDictionary +281 +1 + 3 +Normal +350 +1B +100 +AcDbDictionaryWithDefault +340 +1B + 0 +ACDBPLACEHOLDER + 5 +1B +330 +1A + 0 +LAYOUT + 5 +13 +330 +C +100 +AcDbPlotSettings + 1 + + 2 +none_device + 4 + + 6 + + 40 +0 + 41 +0 + 42 +0 + 43 +0 + 44 +0 + 45 +0 + 46 +0 + 47 +0 + 48 +0 + 49 +0 +140 +0 +141 +0 +142 +1 +143 +1 + 70 +1712 + 72 +1 + 73 +0 + 74 +5 + 7 + + 75 +16 + 76 +0 + 77 +2 + 78 +300 +147 +1 +148 +0 +149 +0 +100 +AcDbLayout + 1 +Model + 70 +1 + 71 +0 + 10 +0 + 20 +0 + 11 +200 + 21 +100 + 12 +0 + 22 +0 + 32 +0 + 14 +10 + 24 +20 + 34 +0 + 15 +180 + 25 +90 + 35 +0 +146 +0 + 13 +0 + 23 +0 + 33 +0 + 16 +1 + 26 +0 + 36 +0 + 17 +0 + 27 +1 + 37 +0 + 76 +1 +330 +D + 0 +LAYOUT + 5 +14 +330 +C +100 +AcDbPlotSettings + 1 + + 2 +none_device + 4 + + 6 + + 40 +0 + 41 +0 + 42 +0 + 43 +0 + 44 +0 + 45 +0 + 46 +0 + 47 +0 + 48 +0 + 49 +0 +140 +0 +141 +0 +142 +1 +143 +1 + 70 +688 + 72 +1 + 73 +0 + 74 +5 + 7 + + 75 +16 + 76 +0 + 77 +2 + 78 +300 +147 +1 +148 +0 +149 +0 +100 +AcDbLayout + 1 +Layout1 + 70 +1 + 71 +1 + 10 +0 + 20 +0 + 11 +200 + 21 +100 + 12 +0 + 22 +0 + 32 +0 + 14 +10 + 24 +20 + 34 +0 + 15 +180 + 25 +90 + 35 +0 +146 +0 + 13 +0 + 23 +0 + 33 +0 + 16 +1 + 26 +0 + 36 +0 + 17 +0 + 27 +1 + 37 +0 + 76 +1 +330 +E + 0 +ENDSEC + 0 +EOF