Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5193,6 +5193,43 @@ BL version (2), B enabled, TV image (an HDR path), BD rotation (0.0), B display_
python3 -I tools/corpus/fetch.py --dest ../dwg-corpus
cargo run --release --example probe_render_environment -- ../dwg-corpus
```

## 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`
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
Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,22 @@
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` (<!-- corpus-run:begin -->[run 40716086829](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40716086829) on 2026-10-28<!-- corpus-run:end -->) with
job on `main` (<!-- corpus-run:begin -->[run 40736084515](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40736084515) on 2026-10-28<!-- corpus-run:end -->) 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):

<!-- corpus-table:begin -->
| 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 %** |
<!-- corpus-table:end -->

A success rate counts records, and the sources differ in size by orders of
Expand All @@ -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 %** |
<!-- corpus-sources:end -->

Totals: <!-- corpus-summary:begin -->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-summary:end -->
Totals: <!-- corpus-summary:begin -->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 %.<!-- corpus-summary:end -->

[`tools/corpus/baseline.tsv`](./tools/corpus/baseline.tsv) pins each
drawing's decoded, skipped and errored counts, its class-census shortfalls
Expand Down
4 changes: 2 additions & 2 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, <!-- corpus-run:begin -->[run 40716086829](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40716086829) on 2026-10-28<!-- corpus-run:end -->, on the public drawings
pinned in `tools/corpus/pins.tsv`): <!-- corpus-summary:begin -->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-summary:end -->
`Corpus (public DWG)` job, <!-- corpus-run:begin -->[run 40736084515](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40736084515) on 2026-10-28<!-- corpus-run:end -->, on the public drawings
pinned in `tools/corpus/pins.tsv`): <!-- corpus-summary:begin -->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 %.<!-- corpus-summary:end -->
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).
Expand Down
2 changes: 1 addition & 1 deletion docs/data/spec-coverage.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -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 closes src/entities/viewport.rs #146 Every field of the section, every version branch, closes on all 51 records of the public corpus (R14 to R2018; no R13 file). Values equal the DXF twin on the 36 R2000 to R2018 records that have one (#71, ARCHITECTURE.md §7u). DXF oracle (examples/dxf_oracle): 36 records R2000-R2018, every decoded value equals AutoCAD's (1686 equal, 0 by the DXF default, 36 with no DXF group); 264 DXF groups not decoded. 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 DXF oracle (examples/dxf_oracle): 12 records R2000-R2018, 2 values disagree (#393) (292 equal, 0 by the DXF default, 46 with no DXF group); 120 DXF groups not decoded. The spec's own examples (§20.4.40.1) close on the record's boundary (#107).
Expand Down
6 changes: 3 additions & 3 deletions docs/spec-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ One row for every section of the *Open Design Specification for .dwg files* v5.4

## Summary

- All sections: 0 `complete`, 105 `closes`, 6 `decoder`, 6 `partial`, 21 `missing`.
- The 104 object prescriptions of §20.4: 0 `complete`, 85 `closes`, 6 `decoder`, 2 `partial`, 11 `missing`.
- All sections: 0 `complete`, 106 `closes`, 6 `decoder`, 6 `partial`, 20 `missing`.
- The 104 object prescriptions of §20.4: 0 `complete`, 86 `closes`, 6 `decoder`, 2 `partial`, 10 `missing`.

## Sections

Expand Down Expand Up @@ -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 | `closes` | src/entities/viewport.rs | #146 | Every field of the section, every version branch, closes on all 51 records of the public corpus (R14 to R2018; no R13 file). Values equal the DXF twin on the 36 R2000 to R2018 records that have one (#71, ARCHITECTURE.md §7u). DXF oracle (examples/dxf_oracle): 36 records R2000-R2018, every decoded value equals AutoCAD's (1686 equal, 0 by the DXF default, 36 with no DXF group); 264 DXF groups not decoded. 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 | DXF oracle (examples/dxf_oracle): 12 records R2000-R2018, 2 values disagree (#393) (292 equal, 0 by the DXF default, 46 with no DXF group); 120 DXF groups not decoded. The spec's own examples (§20.4.40.1) close on the record's boundary (#107). |
Expand Down
72 changes: 72 additions & 0 deletions examples/probe_shape.rs
Original file line number Diff line number Diff line change
@@ -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.to_degrees(), 50),
("width factor", ours.width_factor, 41),
("oblique", ours.oblique.to_degrees(), 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,
)
}
17 changes: 8 additions & 9 deletions src/entities/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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.
Expand Down Expand Up @@ -229,6 +230,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,
Expand Down Expand Up @@ -1502,12 +1504,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,
})
}
Expand Down Expand Up @@ -1540,6 +1539,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)?),
Comment thread
DrunkOnJava marked this conversation as resolved.
Resolved
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
Expand Down Expand Up @@ -1951,6 +1951,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,
Expand Down Expand Up @@ -1979,9 +1980,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}");
}
Expand Down
2 changes: 2 additions & 0 deletions src/entities/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
//! | VERTEX_* | [`vertex`] | §19.4.55-59 |
//! | ACAD_TABLE | [`acad_table`] | §20.4.96.2 |
//! | 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 |
Expand Down Expand Up @@ -95,6 +96,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;
Expand Down
Loading
Loading