From 508b7c78af765d8875a5784a4175476d0cd97c87 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:20:57 -0400 Subject: [PATCH 1/5] =?UTF-8?q?feat(entities):=20decode=20SHAPE=20from=20?= =?UTF-8?q?=C2=A720.4.37=20(#140)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dispatcher kept SHAPE unhandled because a 160-bit budget seemed to have nothing to spend it on. §20.4.37 spends it: a 3BD insertion point, five BD, a BS shape number and a 3BD extrusion, with no release-dependent branch. All 7 SHAPE records of the corpus close on their data-stream boundary from R14 to R2018, and so does the spec's own R14 example (§20.4.37.1). Against AutoCAD's DXF export (R2000 to R2018) the insertion point and scale of the 6 records with a twin equal it; the groups AutoCAD omits at their defaults are not compared. The label test that asserted SHAPE has no field list now lists it. --- ARCHITECTURE.md | 37 ++ docs/data/spec-coverage.tsv | 2 +- docs/spec-coverage.md | 6 +- examples/probe_shape.rs | 72 ++++ examples/support/twin.rs | 391 +++++++++++++++++++ src/entities/dispatch.rs | 17 +- src/entities/mod.rs | 2 + src/entities/shape.rs | 62 +++ tests/fixtures/spec-examples/r14-objects.tsv | 2 +- tools/corpus/baseline.tsv | 14 +- 10 files changed, 584 insertions(+), 21 deletions(-) create mode 100644 examples/probe_shape.rs create mode 100644 examples/support/twin.rs create mode 100644 src/entities/shape.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8fba91cb..ab6fbf33 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1828,6 +1828,43 @@ cargo run --release --example probe_cross_release -- arc_R14.dwg arc_2004.dwg LA --ignore=shade_plot_mode,shade_plot_resolution_level,shade_plot_custom_dpi,viewport_count,trailing_strings ``` +## 7x. SHAPE (2026-10-26, #140) + +The dispatcher's note on SHAPE said the one record in the corpus "has a +160-bit budget and nothing to spend it on". §20.4.37 spends it: `3BD` +insertion point, five `BD` (scale, rotation, width factor, oblique angle, +thickness), `BS` shape number and `3BD` extrusion, with no release-dependent +branch, then the SHAPEFILE handle in the handle stream. Read as printed the +list closes on all 7 SHAPE records of the corpus, one per ACadSharp +`sample_AC10xx` save, R14 to R2018, on each release's own data-stream +boundary. + +Against AutoCAD's DXF export (R2000 to R2018, matched by the handle in group +5) the insertion point and the scale equal groups 10, 20, 30 and 40 on all 6 +records, 24 values, none differing. AutoCAD omits a group at its default, so the +rotation, width factor, oblique angle, thickness and extrusion are absent +from all 6 and count as omitted, not as compared: 42 values that no twin +confirms, held to the boundary alone. The shape number has no DXF group; +the DXF writes the shape's name, which would need the shape file of the +text style (§20.4.56) to resolve. R14 has no twin. + +```sh +cargo run --release --example probe_shape -- ../dwg-corpus +``` + +| Release | Records | Close | Values compared | Omitted by the DXF | Differ | +|---|---|---|---|---|---| +| R14 | 1 | 1 | no twin | | | +| R2000 | 1 | 1 | 4 | 7 | 0 | +| R2004 | 1 | 1 | 4 | 7 | 0 | +| R2007 | 1 | 1 | 4 | 7 | 0 | +| R2010 | 1 | 1 | 4 | 7 | 0 | +| R2013 | 1 | 1 | 4 | 7 | 0 | +| R2018 | 1 | 1 | 4 | 7 | 0 | + +The spec's own example (§20.4.37.1) is an R14 record; it now runs through +the decoder, and `spec_examples` records its outcome. + ## 8. Write pipeline (current scope) The inverse pipeline is partially shipped. Stage-1 (per-section diff --git a/docs/data/spec-coverage.tsv b/docs/data/spec-coverage.tsv index 9f477225..fc36ff7f 100644 --- a/docs/data/spec-coverage.tsv +++ b/docs/data/spec-coverage.tsv @@ -61,7 +61,7 @@ 20.4.34 POLYLINE (MESH) (30) 130 closes src/entities/polygon_mesh.rs #142 The spec's own example (§20.4.34.1) closes on the record's boundary (#107). 20.4.35 SOLID (31) 131 closes src/entities/solid.rs #145 DXF oracle (examples/dxf_oracle): 84 records R2000-R2018, every decoded value equals AutoCAD's (1008 equal, 336 by the DXF default, 0 with no DXF group); 336 DXF groups not decoded. The spec's own example (§20.4.35.1) closes on the record's boundary (#107). 20.4.36 TRACE (32) 131 closes src/entities/trace.rs #145 The spec's own example (§20.4.36.1) closes on the record's boundary (#107). -20.4.37 SHAPE (33) 132 missing #140 The spec's own example (§20.4.37.1) has no decoder to run (#107). +20.4.37 SHAPE (33) 132 closes src/entities/shape.rs #140 Closes on the one SHAPE of each release R14 to R2018 (the ACadSharp sample set), and on the spec's own example (§20.4.37.1, an R14 record), which closed here for the first time (#107). Against AutoCAD's DXF export the insertion point and scale of the 6 R2000 to R2018 records equal groups 10, 20, 30 and 40: 24 values, 0 differ (examples/probe_shape); the rotation, width factor, oblique angle, thickness and extrusion are at their defaults and absent from the DXF, and the shape number has no DXF group, so those are boundary-only. R14 has no DXF twin. No R13 file is in the corpus. 20.4.38 VIEWPORT ENTITY (34) 133 partial src/entities/viewport.rs #146 45 records error (#71): 6 on each of ACadSharp's R2000 to R2018 saves and 9 on export_sample.dwg; the decoder consumes 266 of a 1,125-bit budget. The spec's own example (§20.4.38.1) closes on the record's boundary (#107). 20.4.39 ELLIPSE (35) 136 closes src/entities/ellipse.rs #143 DXF oracle (examples/dxf_oracle): 11 records R2000-R2018, every decoded value equals AutoCAD's (132 equal, 0 by the DXF default, 0 with no DXF group); 22 DXF groups not decoded. The spec's own example (§20.4.39.1) closes on the record's boundary (#107). 20.4.40 SPLINE (36) 136 closes src/entities/spline.rs #143 The spec's own examples (§20.4.40.1) close on the record's boundary (#107). diff --git a/docs/spec-coverage.md b/docs/spec-coverage.md index f806ce9b..d6b7c516 100644 --- a/docs/spec-coverage.md +++ b/docs/spec-coverage.md @@ -18,8 +18,8 @@ One row for every section of the *Open Design Specification for .dwg files* v5.4 ## Summary -- All sections: 0 `complete`, 79 `closes`, 6 `decoder`, 19 `partial`, 34 `missing`. -- The 104 object prescriptions of §20.4: 0 `complete`, 60 `closes`, 6 `decoder`, 16 `partial`, 22 `missing`. +- All sections: 0 `complete`, 80 `closes`, 6 `decoder`, 19 `partial`, 33 `missing`. +- The 104 object prescriptions of §20.4: 0 `complete`, 61 `closes`, 6 `decoder`, 16 `partial`, 21 `missing`. ## Sections @@ -87,7 +87,7 @@ One row for every section of the *Open Design Specification for .dwg files* v5.4 | §20.4.34 | POLYLINE (MESH) (30) | 130 | `closes` | src/entities/polygon_mesh.rs | #142 | The spec's own example (§20.4.34.1) closes on the record's boundary (#107). | | §20.4.35 | SOLID (31) | 131 | `closes` | src/entities/solid.rs | #145 | DXF oracle (examples/dxf_oracle): 84 records R2000-R2018, every decoded value equals AutoCAD's (1008 equal, 336 by the DXF default, 0 with no DXF group); 336 DXF groups not decoded. The spec's own example (§20.4.35.1) closes on the record's boundary (#107). | | §20.4.36 | TRACE (32) | 131 | `closes` | src/entities/trace.rs | #145 | The spec's own example (§20.4.36.1) closes on the record's boundary (#107). | -| §20.4.37 | SHAPE (33) | 132 | `missing` | | #140 | The spec's own example (§20.4.37.1) has no decoder to run (#107). | +| §20.4.37 | SHAPE (33) | 132 | `closes` | src/entities/shape.rs | #140 | Closes on the one SHAPE of each release R14 to R2018 (the ACadSharp sample set), and on the spec's own example (§20.4.37.1, an R14 record), which closed here for the first time (#107). Against AutoCAD's DXF export the insertion point and scale of the 6 R2000 to R2018 records equal groups 10, 20, 30 and 40: 24 values, 0 differ (examples/probe_shape); the rotation, width factor, oblique angle, thickness and extrusion are at their defaults and absent from the DXF, and the shape number has no DXF group, so those are boundary-only. R14 has no DXF twin. No R13 file is in the corpus. | | §20.4.38 | VIEWPORT ENTITY (34) | 133 | `partial` | src/entities/viewport.rs | #146 | 45 records error (#71): 6 on each of ACadSharp's R2000 to R2018 saves and 9 on export_sample.dwg; the decoder consumes 266 of a 1,125-bit budget. The spec's own example (§20.4.38.1) closes on the record's boundary (#107). | | §20.4.39 | ELLIPSE (35) | 136 | `closes` | src/entities/ellipse.rs | #143 | DXF oracle (examples/dxf_oracle): 11 records R2000-R2018, every decoded value equals AutoCAD's (132 equal, 0 by the DXF default, 0 with no DXF group); 22 DXF groups not decoded. The spec's own example (§20.4.39.1) closes on the record's boundary (#107). | | §20.4.40 | SPLINE (36) | 136 | `closes` | src/entities/spline.rs | #143 | The spec's own examples (§20.4.40.1) close on the record's boundary (#107). | diff --git a/examples/probe_shape.rs b/examples/probe_shape.rs new file mode 100644 index 00000000..3b9ff14e --- /dev/null +++ b/examples/probe_shape.rs @@ -0,0 +1,72 @@ +//! Does the §20.4.37 SHAPE field list close on every corpus record, and do +//! its values equal AutoCAD's DXF export? +//! +//! # What this proves +//! +//! For every SHAPE (type 0x21) of every drawing under the corpus directory +//! this reports whether the crate's decoder closed exactly on the record's +//! data-stream boundary (ARCHITECTURE.md §7i) and, where the corpus holds +//! AutoCAD's DXF export of the drawing, compares each decoded value with the +//! `AcDbShape` entity whose handle (group 5) matches: the insertion point +//! (10, 20, 30), scale (40), rotation (50), width factor (41), oblique angle +//! (51), thickness (39) and extrusion (210, 220, 230). A group the DXF leaves +//! out (AutoCAD omits a default) is counted as omitted, not compared against +//! an assumed default. The shape number has no DXF group, the DXF writing the +//! shape's name (group 2) instead, so it is not compared. R14 has no twin in +//! the corpus, so its record is checked against its boundary alone. +//! +//! ```sh +//! python3 -I tools/corpus/fetch.py --dest ../dwg-corpus +//! cargo run --release --example probe_shape -- ../dwg-corpus +//! ``` +//! +//! Exit status is non-zero when a record errors, a value differs, a twin +//! lacks the record, or no value was compared at all. + +#[path = "support/twin.rs"] +mod twin; + +use dwg::ObjectType; +use dwg::entities::DecodedEntity; +use std::process::ExitCode; +use twin::{Group, Probe, Record, Tally}; + +fn compare(decoded: &DecodedEntity, rec: Option<&Record>, t: &mut Tally) -> bool { + let DecodedEntity::Shape(ours) = decoded else { + return false; + }; + let Some(rec) = rec else { + return true; + }; + let real = |code: u16| rec.first(code).and_then(Group::real); + for (what, v, code) in [ + ("insertion x", ours.insertion_point.x, 10), + ("insertion y", ours.insertion_point.y, 20), + ("insertion z", ours.insertion_point.z, 30), + ("scale", ours.scale, 40), + ("rotation", ours.rotation, 50), + ("width factor", ours.width_factor, 41), + ("oblique", ours.oblique, 51), + ("thickness", ours.thickness, 39), + ("extrusion x", ours.extrusion.x, 210), + ("extrusion y", ours.extrusion.y, 220), + ("extrusion z", ours.extrusion.z, 230), + ] { + t.real(what, v, real(code)); + } + true +} + +fn main() -> ExitCode { + twin::run( + &Probe { + tag: "SHAPE", + class: None, + kind: Some(ObjectType::Shape), + dxf_kind: "SHAPE", + decodes_on: |_| true, + note: "; the shape number has no DXF group (the DXF holds the name)", + }, + compare, + ) +} diff --git a/examples/support/twin.rs b/examples/support/twin.rs new file mode 100644 index 00000000..41ef6093 --- /dev/null +++ b/examples/support/twin.rs @@ -0,0 +1,391 @@ +//! Walking a corpus directory for the object probes: every drawing opened, +//! its object records decoded as `DwgFile::decoded_entities` does and kept +//! next to their handles, and AutoCAD's DXF export of the drawing beside it +//! when the corpus holds one. +//! +//! The DXF side is read as plain group pairs. A record is a `0` group and +//! the groups up to the next `0`; its handle is its first group 5 (or 105); +//! the groups inside a `102 {...}` / `102 }` bracket (reactors, extension +//! dictionary) are dropped, as `dwg::dxf_reader` treats them. +//! +//! Each probe includes this file and uses part of it, so the unused part is +//! not a warning. + +#![allow(dead_code)] + +use dwg::classes::ClassMap; +use dwg::entities::{DecodedEntity, decode_from_raw, decode_from_raw_with_class_map}; +use dwg::object::RawObject; +use dwg::{DwgFile, ObjectType, Version}; +use std::collections::{BTreeMap, HashMap}; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +pub struct Group { + pub code: u16, + pub value: String, +} + +impl Group { + pub fn int(&self) -> Option { + self.value.trim().parse().ok() + } + + pub fn real(&self) -> Option { + self.value.trim().parse().ok() + } +} + +pub struct Record { + pub kind: String, + pub handle: Option, + /// How many groups 330 the `{ACAD_REACTORS` bracket holds. + pub reactors: usize, + pub groups: Vec, +} + +impl Record { + pub fn first(&self, code: u16) -> Option<&Group> { + self.groups.iter().find(|g| g.code == code) + } +} + +pub struct Twin { + records: Vec, + by_handle: HashMap, +} + +impl Twin { + fn parse(bytes: &[u8]) -> Twin { + let text = String::from_utf8_lossy(bytes); + let mut lines = text.lines(); + let mut records: Vec = Vec::new(); + let mut depth = 0usize; + let mut in_reactors = false; + while let (Some(code), Some(value)) = (lines.next(), lines.next()) { + let code: u16 = code.trim().parse().unwrap_or(u16::MAX); + if code == 0 { + depth = 0; + in_reactors = false; + records.push(Record { + kind: value.trim().to_string(), + handle: None, + reactors: 0, + groups: Vec::new(), + }); + continue; + } + let Some(rec) = records.last_mut() else { + continue; + }; + if code == 102 { + if value.trim_start().starts_with('{') { + depth += 1; + in_reactors = value.trim_start().starts_with("{ACAD_REACTORS"); + } else { + depth = depth.saturating_sub(1); + in_reactors = false; + } + continue; + } + if depth > 0 { + if in_reactors && code == 330 { + rec.reactors += 1; + } + continue; + } + if (code == 5 || code == 105) && rec.handle.is_none() { + rec.handle = u64::from_str_radix(value.trim(), 16).ok(); + } + rec.groups.push(Group { + code, + value: value.to_string(), + }); + } + let mut by_handle = HashMap::new(); + for (i, r) in records.iter().enumerate() { + if let Some(h) = r.handle { + by_handle.entry(h).or_insert(i); + } + } + Twin { records, by_handle } + } + + /// The record whose handle is `handle`. + pub fn record(&self, handle: u64) -> Option<&Record> { + self.by_handle.get(&handle).map(|&i| &self.records[i]) + } +} + +pub struct Drawing { + pub name: String, + pub version: Version, + pub twin: Option, + objects: Vec, + classes: Option, +} + +fn collect(dir: &Path, depth: usize, out: &mut Vec) -> std::io::Result<()> { + for entry in std::fs::read_dir(dir)? { + let path = entry?.path(); + if path.is_dir() && depth < 4 { + collect(&path, depth + 1, out)?; + } else if path + .extension() + .is_some_and(|x| x.eq_ignore_ascii_case("dwg")) + { + out.push(path); + } + } + Ok(()) +} + +/// Every `.dwg` under `dir`, sorted, with its DXF twin: `.dxf` or the +/// ACadSharp samples' `_ascii.dxf`. A drawing with no twin (R14: the +/// nextgis saves were exported only to R12 DXF) is returned without one. +pub fn drawings(dir: &Path) -> Result, String> { + let mut paths = Vec::new(); + collect(dir, 0, &mut paths).map_err(|e| format!("{}: {e}", dir.display()))?; + paths.sort(); + let mut out = Vec::new(); + for path in paths { + let name = path + .file_name() + .map_or_else(String::new, |n| n.to_string_lossy().into_owned()); + let file = DwgFile::open(&path).map_err(|e| format!("{name}: open: {e}"))?; + let objects = match file.all_objects() { + Some(Ok(o)) => o, + Some(Err(e)) => return Err(format!("{name}: object walk: {e}")), + None => return Err(format!("{name}: no object stream")), + }; + let stem = path + .file_stem() + .map_or_else(String::new, |s| s.to_string_lossy().into_owned()); + let parent = path.parent().map_or_else(PathBuf::new, Path::to_path_buf); + let twin = [format!("{stem}.dxf"), format!("{stem}_ascii.dxf")] + .iter() + .map(|n| parent.join(n)) + .find(|p| p.exists()) + .map(|p| { + std::fs::read(&p) + .map(|bytes| Twin::parse(&bytes)) + .map_err(|e| format!("{name}: read DXF: {e}")) + }) + .transpose()?; + out.push(Drawing { + name, + version: file.version(), + twin, + objects, + classes: file.class_map().and_then(Result::ok), + }); + } + Ok(out) +} + +impl Drawing { + /// The decode of every record of the custom class `class`, by handle. + pub fn class_records(&self, class: &str) -> Vec<(u64, DecodedEntity)> { + let Some(map) = self.classes.as_ref() else { + return Vec::new(); + }; + self.objects + .iter() + .filter_map(|raw| { + let ObjectType::Custom(code) = raw.kind else { + return None; + }; + let def = map.by_type_code(code)?; + (def.dxf_class_name == class).then(|| { + ( + raw.handle.value, + decode_from_raw_with_class_map(raw, self.version, map, code), + ) + }) + }) + .collect() + } + + /// The decode of every record of the fixed type `kind`, by handle. + pub fn kind_records(&self, kind: ObjectType) -> Vec<(u64, DecodedEntity)> { + self.objects + .iter() + .filter(|raw| raw.kind == kind) + .map(|raw| (raw.handle.value, decode_from_raw(raw, self.version))) + .collect() + } +} + +/// Counts of how a decoder's values stood against the DXF export. +#[derive(Default)] +pub struct Tally { + pub compared: usize, + pub differ: usize, + pub omitted: usize, +} + +impl Tally { + /// Compare an integer; `None` means the DXF omits the group. + pub fn int(&mut self, what: &str, ours: i64, theirs: Option) { + match theirs { + Some(t) if t == ours => self.compared += 1, + Some(t) => { + self.compared += 1; + self.differ += 1; + println!(" {what}: dwg {ours} dxf {t}"); + } + None => self.omitted += 1, + } + } + + /// Compare a real to the 1e-12 relative tolerance of a 16-digit DXF. + pub fn real(&mut self, what: &str, ours: f64, theirs: Option) { + match theirs { + Some(t) if (ours - t).abs() <= 1e-12 * ours.abs().max(t.abs()).max(1.0) => { + self.compared += 1; + } + Some(t) => { + self.compared += 1; + self.differ += 1; + println!(" {what}: dwg {ours} dxf {t}"); + } + None => self.omitted += 1, + } + } + + /// Compare text. + pub fn text(&mut self, what: &str, ours: &str, theirs: Option<&str>) { + match theirs { + Some(t) if t == ours => self.compared += 1, + Some(t) => { + self.compared += 1; + self.differ += 1; + println!(" {what}: dwg {ours:?} dxf {t:?}"); + } + None => self.omitted += 1, + } + } +} + +/// What one probe measures: the records of `class` (a custom class) and of +/// `kind` (a fixed type), the DXF record type they pair with (empty when the +/// DXF exports hold none, so that only the boundary is checked), and the +/// releases that must decode. +pub struct Probe<'a> { + pub tag: &'a str, + pub class: Option<&'a str>, + pub kind: Option, + pub dxf_kind: &'a str, + pub decodes_on: fn(Version) -> bool, + pub note: &'a str, +} + +/// Run `p` over the corpus directory given as the first argument and print +/// each record, a table by release and a total. `compare` gets the decode +/// and the DXF record (`None` on a drawing with no twin), tallies the +/// values and says whether the decode is the variant the probe expects. +/// The exit status is non-zero when a record that must decode errors or is +/// unhandled, a twin lacks the record, a value differs, or none was compared. +pub fn run( + p: &Probe, + compare: impl Fn(&DecodedEntity, Option<&Record>, &mut Tally) -> bool, +) -> ExitCode { + let Some(dir) = std::env::args().nth(1) else { + eprintln!( + "usage: {} ", + std::env::args().next().unwrap_or_default() + ); + return ExitCode::FAILURE; + }; + let drawings = match drawings(Path::new(&dir)) { + Ok(d) => d, + Err(e) => { + eprintln!("{e}"); + return ExitCode::FAILURE; + } + }; + let tag = p.tag; + let mut per_release: BTreeMap = BTreeMap::new(); + let mut total = Tally::default(); + let mut failed = false; + for d in &drawings { + let release = format!("{:?}", d.version); + let mut records = p.class.map_or_else(Vec::new, |c| d.class_records(c)); + records.extend(p.kind.map_or_else(Vec::new, |k| d.kind_records(k))); + for (handle, decoded) in records { + let row = per_release.entry(release.clone()).or_default(); + row[0] += 1; + let name = &d.name; + match &decoded { + DecodedEntity::Error { message, .. } => { + failed = true; + println!("{name} ({release}) {tag} 0x{handle:X} ERROR {message}"); + } + DecodedEntity::Unhandled { .. } if !(p.decodes_on)(d.version) => { + row[2] += 1; + println!("{name} ({release}) {tag} 0x{handle:X} not decoded on this release"); + } + DecodedEntity::Unhandled { .. } => { + failed = true; + println!("{name} ({release}) {tag} 0x{handle:X} UNHANDLED"); + } + _ => { + let mut tally = Tally::default(); + let rec = if p.dxf_kind.is_empty() { + None + } else { + d.twin + .as_ref() + .map(|t| t.record(handle).filter(|r| r.kind == p.dxf_kind)) + }; + if matches!(rec, Some(None)) { + failed = true; + println!( + "{name} ({release}) {tag} 0x{handle:X} closes; the DXF has no {} \ + with this handle", + p.dxf_kind + ); + continue; + } + if !compare(&decoded, rec.flatten(), &mut tally) { + failed = true; + println!("{name} ({release}) {tag} 0x{handle:X} decoded as another type"); + continue; + } + row[1] += 1; + if rec.is_some() { + println!( + "{name} ({release}) {tag} 0x{handle:X} closes; DXF: {} compared, \ + {} differ, {} omitted", + tally.compared, tally.differ, tally.omitted + ); + } else { + println!("{name} ({release}) {tag} 0x{handle:X} closes; no DXF twin"); + } + row[3] += tally.compared; + row[4] += tally.differ; + total.compared += tally.compared; + total.differ += tally.differ; + total.omitted += tally.omitted; + } + } + } + } + println!(); + println!("release records close not decoded compared differ"); + for (release, r) in &per_release { + println!( + "{release:<8} {:>7} {:>5} {:>11} {:>8} {:>6}", + r[0], r[1], r[2], r[3], r[4] + ); + } + println!( + "total: {} values compared, {} differ, {} omitted by the DXF{}", + total.compared, total.differ, total.omitted, p.note + ); + if failed || total.differ > 0 || (total.compared == 0 && !p.dxf_kind.is_empty()) { + ExitCode::FAILURE + } else { + ExitCode::SUCCESS + } +} diff --git a/src/entities/dispatch.rs b/src/entities/dispatch.rs index c4cec2d5..3fc2dba9 100644 --- a/src/entities/dispatch.rs +++ b/src/entities/dispatch.rs @@ -57,6 +57,7 @@ pub enum DecodedEntity { Ray(ray::Ray), XLine(xline::XLine), Solid(solid::Solid), + Shape(crate::entities::shape::Shape), /// 3DSOLID (§20.4.41) — an ACIS solid body. ThreeDSolid(three_d_solid::ThreeDSolid), /// REGION (§20.4.41) — an ACIS planar bounded face. @@ -175,6 +176,7 @@ impl DecodedEntity { Self::Ray(_) => OBJECT_TYPE_RAY, Self::XLine(_) => OBJECT_TYPE_XLINE, Self::Solid(_) => OBJECT_TYPE_SOLID, + Self::Shape(_) => OBJECT_TYPE_SHAPE, Self::ThreeDSolid(_) => OBJECT_TYPE_3DSOLID, Self::Region(_) => OBJECT_TYPE_REGION, Self::Body(_) => OBJECT_TYPE_BODY, @@ -1212,12 +1214,9 @@ fn fixed_entity_label(type_code: u16) -> Option<&'static str> { // 0x14 ORDINATE, 0x15 LINEAR, 0x16 ALIGNED, 0x17 ANG_3PT, // 0x18 ANG_2LN, 0x19 RADIUS, 0x1A DIAMETER. OBJECT_TYPE_DIMENSION_MIN..=OBJECT_TYPE_DIMENSION_MAX => "DIMENSION", - // SHAPE has no field list matched against real bytes yet — the - // one record in the corpus has a 160-bit budget and nothing to - // spend it on, so it stays `Unhandled` rather than being fed a - // guess. IMAGE and MLEADER are custom classes whose codes vary - // per file and resolve through `decode_from_raw_with_class_map`. - OBJECT_TYPE_SHAPE => return None, + OBJECT_TYPE_SHAPE => "SHAPE", + // IMAGE and MLEADER are custom classes whose codes vary per file + // and resolve through `decode_from_raw_with_class_map`. _ => return None, }) } @@ -1250,6 +1249,7 @@ fn decode_fixed_entity_body( OBJECT_TYPE_RAY => DecodedEntity::Ray(ray::decode(c)?), OBJECT_TYPE_XLINE => DecodedEntity::XLine(xline::decode(c)?), OBJECT_TYPE_SOLID => DecodedEntity::Solid(solid::decode(c)?), + OBJECT_TYPE_SHAPE => DecodedEntity::Shape(crate::entities::shape::decode(c)?), OBJECT_TYPE_TRACE => DecodedEntity::Trace(trace::decode(c)?), OBJECT_TYPE_3DFACE => DecodedEntity::ThreeDFace(three_d_face::decode(c, version)?), // REGION / 3DSOLID / BODY — one §20.4.41 field list for all @@ -1658,6 +1658,7 @@ mod tests { OBJECT_TYPE_POLYLINE_PFACE, OBJECT_TYPE_POLYLINE_MESH, OBJECT_TYPE_SOLID, + OBJECT_TYPE_SHAPE, OBJECT_TYPE_TRACE, OBJECT_TYPE_VIEWPORT, OBJECT_TYPE_ELLIPSE, @@ -1687,9 +1688,7 @@ mod tests { "0x{code:04X} decodes but has no label, so it would be reported Unhandled" ); } - // SHAPE has a code but no field list; the reserved range has - // neither. - assert!(fixed_entity_label(OBJECT_TYPE_SHAPE).is_none()); + // The reserved range has neither a code nor a field list. for code in [0x00u16, 0x08, 0x09, 0x1FF, 0xFFFF] { assert!(fixed_entity_label(code).is_none(), "0x{code:04X}"); } diff --git a/src/entities/mod.rs b/src/entities/mod.rs index d961102f..35b5e0d1 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -22,6 +22,7 @@ //! | POLYLINE_3D | [`polyline`] | §19.4.46 | //! | VERTEX_* | [`vertex`] | §19.4.55-59 | //! | SEQEND | [`seqend`] | §19.4.6 | +//! | SHAPE | [`shape`] | §20.4.37 | //! | IMAGE | [`image`] | §19.4.35 | //! | IMAGEDEF | [`imagedef`] | §19.5.26 | //! | PROXY ENTITY | [`proxy_entity_passthrough`] | §19.4.91 | @@ -93,6 +94,7 @@ pub mod ray; pub mod region; pub mod revolved_surface; pub mod seqend; +pub mod shape; pub mod solid; pub mod spline; pub mod sun; diff --git a/src/entities/shape.rs b/src/entities/shape.rs new file mode 100644 index 00000000..d4829651 --- /dev/null +++ b/src/entities/shape.rs @@ -0,0 +1,62 @@ +//! SHAPE entity (§20.4.37, type 0x21) — one glyph of a shape file, placed +//! in the drawing. +//! +//! A SHAPE carries the number of its shape in the shape file of its text +//! style, not the shape's name; the DXF writes the name (group 2), which +//! this crate cannot recover without reading the shape file (§20.4.56), so +//! that group is not compared. The field list is the one §20.4.37 prints, +//! with no release-dependent branch: +//! +//! ```text +//! 3BD insertion point DXF 10, 20, 30 +//! BD scale DXF 40, default 1 +//! BD rotation DXF 50, radians, default 0 +//! BD width factor DXF 41, default 1 +//! BD oblique DXF 51, radians, default 0 +//! BD thickness DXF 39 +//! BS shape number DXF 2 (by name) +//! 3BD extrusion DXF 210, 220, 230 +//! H SHAPEFILE hard pointer, in the handle stream +//! ``` +//! +//! The one SHAPE of each release of the public corpus (ACadSharp's +//! `sample_AC10xx`) closes on its data-stream boundary from R14 to R2018. + +use crate::bitcursor::BitCursor; +use crate::entities::{Point3D, Vec3D, read_bd3}; +use crate::error::Result; + +/// A decoded SHAPE. +#[derive(Debug, Clone, PartialEq)] +pub struct Shape { + /// `3BD` insertion point, DXF 10, 20, 30. + pub insertion_point: Point3D, + /// `BD` scale, DXF 40. + pub scale: f64, + /// `BD` rotation in radians, DXF 50. + pub rotation: f64, + /// `BD` width factor, DXF 41. + pub width_factor: f64, + /// `BD` oblique angle in radians, DXF 51. + pub oblique: f64, + /// `BD` thickness, DXF 39. + pub thickness: f64, + /// `BS` index of the shape in the shape file. + pub shape_number: i16, + /// `3BD` extrusion, DXF 210, 220, 230. + pub extrusion: Vec3D, +} + +/// Decode a SHAPE's field list (§20.4.37) past the common entity data. +pub fn decode(c: &mut BitCursor<'_>) -> Result { + Ok(Shape { + insertion_point: read_bd3(c)?, + scale: c.read_bd()?, + rotation: c.read_bd()?, + width_factor: c.read_bd()?, + oblique: c.read_bd()?, + thickness: c.read_bd()?, + shape_number: c.read_bs()?, + extrusion: read_bd3(c)?, + }) +} diff --git a/tests/fixtures/spec-examples/r14-objects.tsv b/tests/fixtures/spec-examples/r14-objects.tsv index d0e22291..7a1a3582 100644 --- a/tests/fixtures/spec-examples/r14-objects.tsv +++ b/tests/fixtures/spec-examples/r14-objects.tsv @@ -34,7 +34,7 @@ section pages label type len handle crc expect record 20.4.34 130-131 3d surf sol st 1E 26 66 3CE7 closes 1A00478059A3680000055B22320C83D182887C0509620B3A0C818C8C3CE7 20.4.35 131 solid 1F 82 CF 1803 closes 520047C073E2981000013350A8BE182452D8F20752C301401D30FA00FF310A9682A0F2068BFA70478B40FA027F99E65FB500EA01FFF9D1853CD8FA027F99E65FB500EA01FFF9D1853CD8FA053820A60A21EA226A1803 20.4.36 132 trace 20 81 E7 7EC2 closes 5100480079E2981000055B5370DAA0ADEEC14205BAE02ADAA96002057529DE3EFF8942034E20B38F50C002004B2A126570A952049E9BA592BF40A2061D16C2A5618152026F4E85D6E788A205260A21F8206C1A7EC2 -20.4.37 133 shape 21 38 F5 3874 unhandled 260048407D67480000015B14AF3D963959A1480420D5143541088A04CD32F4C0182887A03028F9ED3874 +20.4.37 133 shape 21 38 F5 3874 closes 260048407D67480000015B14AF3D963959A1480420D5143541088A04CD32F4C0182887A03028F9ED3874 20.4.38 134-135 vpent 22 279 0126 6C19 closes 1701488080499DF51110005001E4D5649455702004610000A0000000000000000000000000000000000000000000000000A00000000000000000000000000000000000000000000F03F2800000000000000002800000000000022402878921A65ACA21A4028000000000000124028000000000000494028000000000000000028000000000000000046000046640046010046010046000046000046000046000028000000000000000028000000000000000028000000000000000028000000000000E03F28000000000000E03F28000000000000E03F28000000000000E03F4600000200020102018DA100000D6C3C490D32D6510D20000000000000024810F12434CB594454800000000000001120182887A0508129009286C19 20.4.39 136 ellipse 23 76 0122 ED08 closes 4C0048C08048A1481000055B0C0A03298AE7424801F09FBC531040DA045123D0F1D6AF7B9F9A8915EA36B2DD17F50020000000001E07E5D2A47DB04C5EF9FC0C16A22A7D908CA0182887E083A069ED08 20.4.40 138 spline 24 97 0101 99F5 closes 61004900804066A81000055B204819777BAFB3BEF9B67B554821D1F6EC493D161C80603C07634316F89FC0E44E5364CA30B201F0333C1CA7C20E810185809AFE6F6388020789BE3C1B4F51FC5F5114FA2FCF94200418CB8BBBC69D67F182887E08130599F5 diff --git a/tools/corpus/baseline.tsv b/tools/corpus/baseline.tsv index 6e67f10f..7d5402ea 100644 --- a/tools/corpus/baseline.tsv +++ b/tools/corpus/baseline.tsv @@ -52,10 +52,10 @@ polyline2d_line_2007.dwg nextgis AC1021 181 5 0 0 0 CELLSTYLEMAP 3>1;DICTIONARYV polyline2d_line_2010.dwg nextgis AC1024 177 4 0 0 0 CELLSTYLEMAP 1>0;DICTIONARYVAR 10>5 0 491 0 0 0 polyline2d_line_2013.dwg nextgis AC1027 128 4 0 0 0 - 0 369 0 0 0 polyline2d_line_R14.dwg nextgis AC1014 283 9 0 0 0 - 0 0 0 0 292 -sample_AC1014.dwg acadsharp AC1014 1050 115 77 0 0 - 0 0 0 0 1242 -sample_AC1015.dwg acadsharp AC1015 922 78 47 0 0 - 0 3160 6 0 0 -sample_AC1018.dwg acadsharp AC1018 854 74 86 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;CELLSTYLEMAP 2>1;DICTIONARYVAR 15>14;TABLECONTENT 9>2;TABLEGEOMETRY 9>2 0 3135 6 0 0 -sample_AC1021.dwg acadsharp AC1021 838 72 15 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;TABLECONTENT 8>2;TABLEGEOMETRY 8>2 0 2960 6 0 0 -sample_AC1024.dwg acadsharp AC1024 851 57 15 0 0 TABLECONTENT 7>2;TABLEGEOMETRY 7>2 0 3050 6 0 0 -sample_AC1027.dwg acadsharp AC1027 799 57 9 0 0 TABLECONTENT 6>2;TABLEGEOMETRY 6>2 0 2905 6 0 0 -sample_AC1032.dwg acadsharp AC1032 776 57 9 0 0 TABLECONTENT 5>2;TABLEGEOMETRY 5>2 0 2852 6 0 0 +sample_AC1014.dwg acadsharp AC1014 1051 114 77 0 0 - 0 0 0 0 1242 +sample_AC1015.dwg acadsharp AC1015 923 77 47 0 0 - 0 3160 6 0 0 +sample_AC1018.dwg acadsharp AC1018 855 73 86 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;CELLSTYLEMAP 2>1;DICTIONARYVAR 15>14;TABLECONTENT 9>2;TABLEGEOMETRY 9>2 0 3135 6 0 0 +sample_AC1021.dwg acadsharp AC1021 839 71 15 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;TABLECONTENT 8>2;TABLEGEOMETRY 8>2 0 2960 6 0 0 +sample_AC1024.dwg acadsharp AC1024 852 56 15 0 0 TABLECONTENT 7>2;TABLEGEOMETRY 7>2 0 3050 6 0 0 +sample_AC1027.dwg acadsharp AC1027 800 56 9 0 0 TABLECONTENT 6>2;TABLEGEOMETRY 6>2 0 2905 6 0 0 +sample_AC1032.dwg acadsharp AC1032 777 56 9 0 0 TABLECONTENT 5>2;TABLEGEOMETRY 5>2 0 2852 6 0 0 From f7c0ad0e070b6770411844993336d0b2cdbccdcc Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:36:50 -0400 Subject: [PATCH 2/5] fix(examples): carry the one harness file every probe of this lane shares, so the copies merge in any order --- examples/support/twin.rs | 66 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/examples/support/twin.rs b/examples/support/twin.rs index 41ef6093..eaae66ca 100644 --- a/examples/support/twin.rs +++ b/examples/support/twin.rs @@ -115,6 +115,27 @@ impl Twin { pub fn record(&self, handle: u64) -> Option<&Record> { self.by_handle.get(&handle).map(|&i| &self.records[i]) } + + /// Every record of DXF type `kind`, in file order. + pub fn records_of(&self, kind: &str) -> Vec<&Record> { + self.records.iter().filter(|r| r.kind == kind).collect() + } +} + +/// How a DWG record finds its DXF partner. +pub enum Pairing { + /// The DXF record with the same handle. + Handle, + /// The DXF record with the same handle; failing that, the DXF records of + /// the type that no DWG record shares a handle with, taken in file order + /// against the DWG records that find none. For the objects of a table + /// AutoCAD created while saving, whose handles differ between the DWG and + /// the DXF of the same drawing. + HandleThenOrder, + /// The one record of the DXF type in the twin, for an object AutoCAD + /// writes afresh on every save, so that the two files never share a + /// handle. A twin with none or several has no partner for it. + OnlyOfKind, } pub struct Drawing { @@ -289,6 +310,17 @@ pub struct Probe<'a> { pub fn run( p: &Probe, compare: impl Fn(&DecodedEntity, Option<&Record>, &mut Tally) -> bool, +) -> ExitCode { + run_with_twin(p, Pairing::Handle, |d, r, _, t| compare(d, r, t)) +} + +/// [`run`], with the record's partner found by `pairing` and the whole twin +/// handed to `compare`, for a record whose values also live in a second DXF +/// object. +pub fn run_with_twin( + p: &Probe, + pairing: Pairing, + compare: impl Fn(&DecodedEntity, Option<&Record>, Option<&Twin>, &mut Tally) -> bool, ) -> ExitCode { let Some(dir) = std::env::args().nth(1) else { eprintln!( @@ -312,6 +344,22 @@ pub fn run( let release = format!("{:?}", d.version); let mut records = p.class.map_or_else(Vec::new, |c| d.class_records(c)); records.extend(p.kind.map_or_else(Vec::new, |k| d.kind_records(k))); + let mut by_order: HashMap = HashMap::new(); + if let (Pairing::HandleThenOrder, Some(t), false) = + (&pairing, &d.twin, p.dxf_kind.is_empty()) + { + let mut spare = t + .records_of(p.dxf_kind) + .into_iter() + .filter(|r| !records.iter().any(|(h, _)| r.handle == Some(*h))); + for (h, _) in &records { + if t.record(*h).is_none_or(|r| r.kind != p.dxf_kind) { + if let Some(r) = spare.next() { + by_order.insert(*h, r); + } + } + } + } for (handle, decoded) in records { let row = per_release.entry(release.clone()).or_default(); row[0] += 1; @@ -334,20 +382,28 @@ pub fn run( let rec = if p.dxf_kind.is_empty() { None } else { - d.twin - .as_ref() - .map(|t| t.record(handle).filter(|r| r.kind == p.dxf_kind)) + d.twin.as_ref().map(|t| match pairing { + Pairing::Handle => t.record(handle).filter(|r| r.kind == p.dxf_kind), + Pairing::HandleThenOrder => t + .record(handle) + .filter(|r| r.kind == p.dxf_kind) + .or_else(|| by_order.get(&handle).copied()), + Pairing::OnlyOfKind => { + let all = t.records_of(p.dxf_kind); + (all.len() == 1).then(|| all[0]) + } + }) }; if matches!(rec, Some(None)) { failed = true; println!( "{name} ({release}) {tag} 0x{handle:X} closes; the DXF has no {} \ - with this handle", + to pair it with", p.dxf_kind ); continue; } - if !compare(&decoded, rec.flatten(), &mut tally) { + if !compare(&decoded, rec.flatten(), d.twin.as_ref(), &mut tally) { failed = true; println!("{name} ({release}) {tag} 0x{handle:X} decoded as another type"); continue; From 6ddcdac41077e013885f69195b1a352318dda717 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Mon, 26 Oct 2026 11:21:37 -0400 Subject: [PATCH 3/5] fix(examples): compare SHAPE's rotation and oblique angle in the DXF's degrees (#140) --- examples/probe_shape.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/probe_shape.rs b/examples/probe_shape.rs index 3b9ff14e..1b59f384 100644 --- a/examples/probe_shape.rs +++ b/examples/probe_shape.rs @@ -44,9 +44,9 @@ fn compare(decoded: &DecodedEntity, rec: Option<&Record>, t: &mut Tally) -> bool ("insertion y", ours.insertion_point.y, 20), ("insertion z", ours.insertion_point.z, 30), ("scale", ours.scale, 40), - ("rotation", ours.rotation, 50), + ("rotation", ours.rotation.to_degrees(), 50), ("width factor", ours.width_factor, 41), - ("oblique", ours.oblique, 51), + ("oblique", ours.oblique.to_degrees(), 51), ("thickness", ours.thickness, 39), ("extrusion x", ours.extrusion.x, 210), ("extrusion y", ours.extrusion.y, 220), From f506b3a3c7833f47383ee23e2000c79ca65604d1 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:28:33 -0400 Subject: [PATCH 4/5] docs(architecture): renumber the new section (7au->7bt) after the sections main added --- ARCHITECTURE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5f01216a..aeab411d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -3669,7 +3669,7 @@ end; characters above the BMP, two escapes each as before and never measured; an strings encoder for the entities, which the DWG writer does not write at all (its only authored strings are the class table's). -## 7au. SHAPE (2026-10-26, #140) +## 7bt. SHAPE (2026-10-26, #140) The dispatcher's note on SHAPE said the one record in the corpus "has a 160-bit budget and nothing to spend it on". §20.4.37 spends it: `3BD` From 369a72616a3d2a2c4286ccd49853068b877e07b4 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Wed, 28 Oct 2026 13:31:59 -0400 Subject: [PATCH 5/5] chore(corpus): commit the baselines CI run 40736084515 measured --- README.md | 24 ++++++++++++------------ STATUS.md | 4 ++-- tools/corpus/baseline.tsv | 14 +++++++------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 35a705b7..fe5e9d95 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This is **0.1.0-alpha.1**. Do not use it in production. Do not benchmark it against the ODA SDK. Do not tell your CAD team dwg-rs solves their interop problem today. Entity and object decode coverage, measured in CI by the `Corpus (public DWG)` -job on `main` ([run 40716086829](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40716086829) on 2026-10-28) with +job on `main` ([run 40736084515](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40736084515) on 2026-10-28) with [`examples/coverage_report.rs`](./examples/coverage_report.rs), on the public drawings fetched by pinned sha256 ([`tools/corpus/pins.tsv`](./tools/corpus/pins.tsv); none is committed): @@ -25,14 +25,14 @@ drawings fetched by pinned sha256 | Version | Files tested | Decoded | Skipped | Errored | Success rate | Median drawing | |---------|--------------|---------|---------|---------|--------------|----------------| -| R14 (AC1014) | 12 | 4733 | 23 | 0 | **99.5 %** | 100.0 % | -| R2000 (AC1015) | 9 | 2781 | 4 | 0 | **99.9 %** | 100.0 % | -| R2004 (AC1018) | 12 | 106993 | 1 | 0 | **> 99.9 %** | 100.0 % | -| R2007 (AC1021) | 8 | 2236 | 1 | 0 | **> 99.9 %** | 100.0 % | -| R2010 (AC1024) | 8 | 2187 | 1 | 0 | **> 99.9 %** | 100.0 % | -| R2013 (AC1027) | 8 | 1786 | 1 | 0 | **99.9 %** | 100.0 % | -| R2018 (AC1032) | 4 | 1614 | 1 | 0 | **99.9 %** | 100.0 % | -| **Aggregate** | **61** | **122330** | **32** | **0** | **> 99.9 %** | **100.0 %** | +| R14 (AC1014) | 12 | 4734 | 22 | 0 | **99.5 %** | 100.0 % | +| R2000 (AC1015) | 9 | 2782 | 3 | 0 | **99.9 %** | 100.0 % | +| R2004 (AC1018) | 12 | 106994 | 0 | 0 | **100.0 %** | 100.0 % | +| R2007 (AC1021) | 8 | 2237 | 0 | 0 | **100.0 %** | 100.0 % | +| R2010 (AC1024) | 8 | 2188 | 0 | 0 | **100.0 %** | 100.0 % | +| R2013 (AC1027) | 8 | 1787 | 0 | 0 | **100.0 %** | 100.0 % | +| R2018 (AC1032) | 4 | 1615 | 0 | 0 | **100.0 %** | 100.0 % | +| **Aggregate** | **61** | **122337** | **25** | **0** | **> 99.9 %** | **100.0 %** | A success rate counts records, and the sources differ in size by orders of @@ -46,14 +46,14 @@ a change came from decoding or from the corpus: | Source | Files tested | Decoded | Skipped | Errored | Success rate | Median drawing | |--------|--------------|---------|---------|---------|--------------|----------------| | nextgis | 42 | 8443 | 0 | 0 | **100.0 %** | 100.0 % | -| acadsharp | 10 | 7618 | 13 | 0 | **99.8 %** | 99.9 % | +| acadsharp | 10 | 7625 | 6 | 0 | **99.9 %** | 100.0 % | | bookviewerapp3 | 4 | 1440 | 19 | 0 | **98.7 %** | 99.4 % | | oshw-jetson | 4 | 104577 | 0 | 0 | **100.0 %** | 100.0 % | | planar-algorithm | 1 | 252 | 0 | 0 | **100.0 %** | 100.0 % | -| **All sources** | **61** | **122330** | **32** | **0** | **> 99.9 %** | **100.0 %** | +| **All sources** | **61** | **122337** | **25** | **0** | **> 99.9 %** | **100.0 %** | -Totals: 122,330 decoded / 32 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 99.8 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.5 %, R2000 99.9 %, R2004 > 99.9 %, R2007 > 99.9 %, R2010 > 99.9 %, R2013 99.9 %, R2018 99.9 %. +Totals: 122,337 decoded / 25 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 99.9 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.5 %, R2000 99.9 %, R2004 100.0 %, R2007 100.0 %, R2010 100.0 %, R2013 100.0 %, R2018 100.0 %. [`tools/corpus/baseline.tsv`](./tools/corpus/baseline.tsv) pins each drawing's decoded, skipped and errored counts, its class-census shortfalls diff --git a/STATUS.md b/STATUS.md index f610c12f..69d2ef79 100644 --- a/STATUS.md +++ b/STATUS.md @@ -15,8 +15,8 @@ the current capability matrix. examples (`tools/ci/run_test_targets.sh`), and the Corpus job runs the corpus-reading targets on the 19 pinned drawings; clippy and fmt clean. - **Current real-file decode coverage, measured in CI** (the - `Corpus (public DWG)` job, [run 40716086829](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40716086829) on 2026-10-28, on the public drawings - pinned in `tools/corpus/pins.tsv`): 122,330 decoded / 32 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 99.8 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.5 %, R2000 99.9 %, R2004 > 99.9 %, R2007 > 99.9 %, R2010 > 99.9 %, R2013 99.9 %, R2018 99.9 %. + `Corpus (public DWG)` job, [run 40736084515](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40736084515) on 2026-10-28, on the public drawings + pinned in `tools/corpus/pins.tsv`): 122,337 decoded / 25 skipped / **0 errored** / > 99.9 % of all records on 61 drawings; the median drawing decodes 100.0 %. Per source (drawings): nextgis 100.0 % (42), acadsharp 99.9 % (10), bookviewerapp3 98.7 % (4), oshw-jetson 100.0 % (4), planar-algorithm 100.0 % (1). Per version: R14 99.5 %, R2000 99.9 %, R2004 100.0 %, R2007 100.0 %, R2010 100.0 %, R2013 100.0 %, R2018 100.0 %. The 308 errors of the 2026-10-26 measurement were closed by #226, #227, #228 and #71; `sample_AC1032.dwg` comes from ACadSharp's MIT `samples/`, saved by AutoCAD 2025 (#96, CLEANROOM.md). diff --git a/tools/corpus/baseline.tsv b/tools/corpus/baseline.tsv index 33ebd788..270ef7e3 100644 --- a/tools/corpus/baseline.tsv +++ b/tools/corpus/baseline.tsv @@ -60,11 +60,11 @@ polyline2d_line_2007.dwg nextgis AC1021 186 0 0 0 0 CELLSTYLEMAP 3>1;DICTIONARYV polyline2d_line_2010.dwg nextgis AC1024 181 0 0 0 0 CELLSTYLEMAP 1>0;DICTIONARYVAR 10>5 0 491 0 0 0 polyline2d_line_2013.dwg nextgis AC1027 132 0 0 0 0 - 0 369 0 0 0 polyline2d_line_R14.dwg nextgis AC1014 292 0 0 0 0 - 0 0 0 0 292 -sample_AC1014.dwg acadsharp AC1014 1238 4 0 0 0 - 0 0 0 0 1242 -sample_AC1015.dwg acadsharp AC1015 1043 4 0 0 0 - 0 3160 6 0 0 -sample_AC1018.dwg acadsharp AC1018 1013 1 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;CELLSTYLEMAP 2>1;DICTIONARYVAR 15>14;TABLECONTENT 9>2;TABLEGEOMETRY 9>2 0 3135 6 0 0 -sample_AC1021.dwg acadsharp AC1021 924 1 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;TABLECONTENT 8>2;TABLEGEOMETRY 8>2 0 2960 6 0 0 -sample_AC1024.dwg acadsharp AC1024 922 1 0 0 0 TABLECONTENT 7>2;TABLEGEOMETRY 7>2 0 3050 6 0 0 -sample_AC1027.dwg acadsharp AC1027 864 1 0 0 0 TABLECONTENT 6>2;TABLEGEOMETRY 6>2 0 2905 6 0 0 -sample_AC1032.dwg acadsharp AC1032 841 1 0 0 0 TABLECONTENT 5>2;TABLEGEOMETRY 5>2 0 2852 6 0 0 +sample_AC1014.dwg acadsharp AC1014 1239 3 0 0 0 - 0 0 0 0 1242 +sample_AC1015.dwg acadsharp AC1015 1044 3 0 0 0 - 0 3160 6 0 0 +sample_AC1018.dwg acadsharp AC1018 1014 0 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;CELLSTYLEMAP 2>1;DICTIONARYVAR 15>14;TABLECONTENT 9>2;TABLEGEOMETRY 9>2 0 3135 6 0 0 +sample_AC1021.dwg acadsharp AC1021 925 0 0 0 0 ACDB_TEXTOBJECTCONTEXTDATA_CLASS 2>0;TABLECONTENT 8>2;TABLEGEOMETRY 8>2 0 2960 6 0 0 +sample_AC1024.dwg acadsharp AC1024 923 0 0 0 0 TABLECONTENT 7>2;TABLEGEOMETRY 7>2 0 3050 6 0 0 +sample_AC1027.dwg acadsharp AC1027 865 0 0 0 0 TABLECONTENT 6>2;TABLEGEOMETRY 6>2 0 2905 6 0 0 +sample_AC1032.dwg acadsharp AC1032 842 0 0 0 0 TABLECONTENT 5>2;TABLEGEOMETRY 5>2 0 2852 6 0 0 原始数据.dwg planar-algorithm AC1015 252 0 0 0 0 - 0 636 0 0 0