From 6545fa61e8bc872ff458c415e9ba81d743519388 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:54:25 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(dxf):=20score=20GEODATA=20in=20the=20v?= =?UTF-8?q?alue=20oracle=20(=C2=A720.4.78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GEODATA decoder (§20.4.78) is now compared with AutoCAD's export field by field, under AcDbGeoData, with the groups the decoder's probe maps: the object version (90), design coordinate type (70), design and reference points (10, 11), unit scales and units (40, 91, 41, 92), up direction (210), north direction (12), scale estimation (95, 141), sea level (294, 142), projection radius (143), the coordinate system definition (the 303 chunks joined with the final 301), the GeoRSS and observation tags (302, 305 to 307), the mesh points (93 with 13 and 14) and the mesh faces (96 with 97, 98 and 99). Object version 1 records (the R2009 layout) do not occur in the corpus and are not scored. On the 49 drawing pairs: 1 record (geoloc.dwg, R2018), 77 values equal the export, the 77 the probe reports, and none differ. tools/oracle/baseline.tsv gains the new rows, and the §20.4.78 row of docs/spec-coverage.md says what the oracle measured. The oracle's section map files GEODATA under §20.4.78. Refs #99, #102, #150 --- docs/data/spec-coverage.tsv | 2 +- docs/spec-coverage.md | 2 +- examples/dxf_oracle/scorers.rs | 113 +++++++++++++++++++++++++++++++++ tools/oracle/baseline.tsv | 33 ++++++++++ 4 files changed, 148 insertions(+), 2 deletions(-) diff --git a/docs/data/spec-coverage.tsv b/docs/data/spec-coverage.tsv index cb5e679..803582a 100644 --- a/docs/data/spec-coverage.tsv +++ b/docs/data/spec-coverage.tsv @@ -102,7 +102,7 @@ 20.4.75 HATCH 188 partial src/entities/hatch.rs #147 Gradient block unconditional (ARCHITECTURE §7g); a spline edge with no fit points stores no tangents (§7y, #228). The 8 R14 records are never routed (#229). The spec's own example (§20.4.75.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). 20.4.76 FIELD 192 missing #149 20.4.77 FIELDLIST 193 missing #149 -20.4.78 GEODATA 194 closes src/entities/geodata.rs #150 Routed by class name; geoloc.dwg's record (object version 3) closes and all 77 values equal its DXF twin. The north direction is a 2RD, not 3RD (ARCHITECTURE §7aa). Object version 1 and the R2007-and-earlier civil block are unmeasured. +20.4.78 GEODATA 194 closes src/entities/geodata.rs #150 Routed by class name; geoloc.dwg's record (object version 3) closes and all 77 values equal its DXF twin. The north direction is a 2RD, not 3RD (ARCHITECTURE §7aa). Object version 1 and the R2007-and-earlier civil block are unmeasured. DXF oracle (examples/dxf_oracle): 1 records R2018, every decoded value equals AutoCAD's (77 equal, 0 by the DXF default, 0 with no DXF group); 3 DXF groups not decoded. 20.4.79 IDBUFFER 196 missing #149 The spec's own example (§20.4.79.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). 20.4.80 IMAGE 197 decoder src/entities/image.rs #150 The spec's own example (§20.4.80.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). 20.4.81 IMAGEDEF 199 decoder src/entities/imagedef.rs #150 The spec's own example (§20.4.81.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). diff --git a/docs/spec-coverage.md b/docs/spec-coverage.md index d663881..ece80ac 100644 --- a/docs/spec-coverage.md +++ b/docs/spec-coverage.md @@ -128,7 +128,7 @@ One row for every section of the *Open Design Specification for .dwg files* v5.4 | §20.4.75 | HATCH | 188 | `partial` | src/entities/hatch.rs | #147 | Gradient block unconditional (ARCHITECTURE §7g); a spline edge with no fit points stores no tangents (§7y, #228). The 8 R14 records are never routed (#229). The spec's own example (§20.4.75.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). | | §20.4.76 | FIELD | 192 | `missing` | | #149 | | | §20.4.77 | FIELDLIST | 193 | `missing` | | #149 | | -| §20.4.78 | GEODATA | 194 | `closes` | src/entities/geodata.rs | #150 | Routed by class name; geoloc.dwg's record (object version 3) closes and all 77 values equal its DXF twin. The north direction is a 2RD, not 3RD (ARCHITECTURE §7aa). Object version 1 and the R2007-and-earlier civil block are unmeasured. | +| §20.4.78 | GEODATA | 194 | `closes` | src/entities/geodata.rs | #150 | Routed by class name; geoloc.dwg's record (object version 3) closes and all 77 values equal its DXF twin. The north direction is a 2RD, not 3RD (ARCHITECTURE §7aa). Object version 1 and the R2007-and-earlier civil block are unmeasured. DXF oracle (examples/dxf_oracle): 1 records R2018, every decoded value equals AutoCAD's (77 equal, 0 by the DXF default, 0 with no DXF group); 3 DXF groups not decoded. | | §20.4.79 | IDBUFFER | 196 | `missing` | | #149 | The spec's own example (§20.4.79.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). | | §20.4.80 | IMAGE | 197 | `decoder` | src/entities/image.rs | #150 | The spec's own example (§20.4.80.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). | | §20.4.81 | IMAGEDEF | 199 | `decoder` | src/entities/imagedef.rs | #150 | The spec's own example (§20.4.81.1) is a class-numbered record with no class table printed, so only its walk and CRC are checked (#107). | diff --git a/examples/dxf_oracle/scorers.rs b/examples/dxf_oracle/scorers.rs index 1085bc9..a40c935 100644 --- a/examples/dxf_oracle/scorers.rs +++ b/examples/dxf_oracle/scorers.rs @@ -773,6 +773,7 @@ pub fn section_of(label: &str) -> &'static str { "DIMSTYLE" => "§20.4.68", "MLINESTYLE" => "§20.4.73", "MLEADERSTYLE" => "§20.4.87", + "GEODATA" => "§20.4.78", "DICTIONARYVAR" => "§20.4.74", "LAYOUT" => "§20.4.84", "LWPOLYLINE" => "§20.4.85", @@ -1022,6 +1023,7 @@ pub fn score( D::DimStyle(d) => score_dimstyle(&mut c, d), D::VPort(v) => score_vport(&mut c, v), D::View(v) => score_view(&mut c, v), + D::GeoData(g) => score_geodata(&mut c, g), D::BlockRecord(b) => score_block_record(&mut c, b, rec.handle, dwg), // §20.4.51 and the other controls: BL num_entries; the TABLE's 70 is // the DXF Reference's "maximum number of entries". @@ -1746,6 +1748,117 @@ fn score_tablestyle(c: &mut Ctx<'_>, t: &dwg::objects::acad_table_style::AcadTab } } +// §20.4.78 against the DXF Reference's AcDbGeoData groups, as the decoder's +// probe maps them: the object version 90, the design coordinate type 70, +// the design point 10, the reference point 11, the unit scales and units +// 40, 91, 41 and 92, the up direction 210, the north direction 12 and 22, +// the scale estimation 95 and 141, the sea level 294 and 142, the +// projection radius 143, the coordinate system definition (303 chunks, +// then 301), the GeoRSS tag 302, the observation tags 305 to 307, the mesh +// points (93, then 13/14 source and destination) and the mesh faces (96, +// then 97, 98 and 99). The definition is written in chunks of 303 and a +// last 301, which are joined before comparing. Version 1 records (the +// R2009 layout) do not occur in the corpus and are not scored. +fn score_geodata(c: &mut Ctx<'_>, g: &dwg::entities::geodata::GeoData) { + if g.object_version < 2 { + c.dwg_only("(object version 1 is not scored)", g.object_version); + return; + } + if !c.subclass("AcDbGeoData") { + c.dwg_only("(the export has no AcDbGeoData subclass)", ()); + return; + } + c.int("object_version", 90, i64::from(g.object_version), None); + c.int( + "design_coordinate_type", + 70, + i64::from(g.design_coordinate_type), + None, + ); + c.point3("design_point", 10, g.design_point, None); + c.point3("reference_point", 11, g.reference_point, None); + c.real("horizontal_unit_scale", 40, g.horizontal_unit_scale, None); + c.int("horizontal_units", 91, i64::from(g.horizontal_units), None); + c.real("vertical_unit_scale", 41, g.vertical_unit_scale, None); + c.int("vertical_units", 92, i64::from(g.vertical_units), None); + c.point3("up_direction", 210, g.up_direction, None); + c.point2("north_direction", 12, g.north_direction); + c.int( + "scale_estimation_method", + 95, + i64::from(g.scale_estimation_method), + None, + ); + c.real("user_scale_factor", 141, g.user_scale_factor, None); + c.int( + "sea_level_correction", + 294, + i64::from(g.sea_level_correction), + None, + ); + c.real("sea_level_elevation", 142, g.sea_level_elevation, None); + c.real( + "coordinate_projection_radius", + 143, + g.coordinate_projection_radius, + None, + ); + let mut chunks = String::new(); + let mut code_page = false; + loop { + match c.take_text(303) { + Ok(Some(t)) => chunks.push_str(t), + Ok(None) => break, + Err(CodePage) => { + code_page = true; + break; + } + } + } + let last = if code_page { + Err(CodePage) + } else { + c.take_text(301) + }; + match last { + Ok(Some(last)) => { + chunks.push_str(last); + let chunks = caret_decode(&chunks); + c.derived( + "coordinate_system_definition (303 chunks + 301)", + "303+301", + g.coordinate_system_definition == chunks, + format!("{} chars", g.coordinate_system_definition.chars().count()), + format!("{} chars", chunks.chars().count()), + ); + } + Ok(None) | Err(CodePage) => c.dwg_only( + "coordinate_system_definition", + g.coordinate_system_definition.chars().count(), + ), + } + c.text("geo_rss_tag", 302, &g.geo_rss_tag, None); + c.text("observation_from_tag", 305, &g.observation_from_tag, None); + c.text("observation_to_tag", 306, &g.observation_to_tag, None); + c.text( + "observation_coverage_tag", + 307, + &g.observation_coverage_tag, + None, + ); + c.int("mesh point count", 93, g.mesh_points.len() as i64, None); + for (s, d) in &g.mesh_points { + c.point2("mesh_points.source", 13, *s); + c.point2("mesh_points.destination", 14, *d); + } + c.int("mesh face count", 96, g.mesh_faces.len() as i64, None); + for f in &g.mesh_faces { + c.int("mesh_faces.0", 97, i64::from(f[0]), None); + c.int("mesh_faces.1", 98, i64::from(f[1]), None); + c.int("mesh_faces.2", 99, i64::from(f[2]), None); + } +} + // §20.4.73 with the DXF Reference's MLINESTYLE groups: 2, 70, 3, 62, 51, 52, // 71, then 49 / 62 / 6 per line. The flag bits are remapped per the spec's // table (DWG 1 -> DXF 2, 2 -> 1, 32 -> 64, 64 -> 32, 512 -> 1024, 1024 -> 512). diff --git a/tools/oracle/baseline.tsv b/tools/oracle/baseline.tsv index 0528380..32440c9 100644 --- a/tools/oracle/baseline.tsv +++ b/tools/oracle/baseline.tsv @@ -4479,6 +4479,39 @@ R2018 §20.4.73 MLINESTYLE name 2 2 0 0 0 R2018 §20.4.73 MLINESTYLE start_angle 51 2 0 0 0 R2018 §20.4.74 DICTIONARYVAR schema 280 21 0 0 0 R2018 §20.4.74 DICTIONARYVAR value 1 21 0 0 0 +R2018 §20.4.78 GEODATA coordinate_projection_radius 143 1 0 0 0 +R2018 §20.4.78 GEODATA coordinate_system_definition (303 chunks + 301) 303+301 1 0 0 0 +R2018 §20.4.78 GEODATA design_coordinate_type 70 1 0 0 0 +R2018 §20.4.78 GEODATA design_point.x 10 1 0 0 0 +R2018 §20.4.78 GEODATA design_point.y 20 1 0 0 0 +R2018 §20.4.78 GEODATA design_point.z 30 1 0 0 0 +R2018 §20.4.78 GEODATA geo_rss_tag 302 1 0 0 0 +R2018 §20.4.78 GEODATA horizontal_unit_scale 40 1 0 0 0 +R2018 §20.4.78 GEODATA horizontal_units 91 1 0 0 0 +R2018 §20.4.78 GEODATA mesh face count 96 1 0 0 0 +R2018 §20.4.78 GEODATA mesh point count 93 1 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.destination.x 14 12 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.destination.y 24 12 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.source.x 13 12 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.source.y 23 12 0 0 0 +R2018 §20.4.78 GEODATA north_direction.x 12 1 0 0 0 +R2018 §20.4.78 GEODATA north_direction.y 22 1 0 0 0 +R2018 §20.4.78 GEODATA object_version 90 1 0 0 0 +R2018 §20.4.78 GEODATA observation_coverage_tag 307 1 0 0 0 +R2018 §20.4.78 GEODATA observation_from_tag 305 1 0 0 0 +R2018 §20.4.78 GEODATA observation_to_tag 306 1 0 0 0 +R2018 §20.4.78 GEODATA reference_point.x 11 1 0 0 0 +R2018 §20.4.78 GEODATA reference_point.y 21 1 0 0 0 +R2018 §20.4.78 GEODATA reference_point.z 31 1 0 0 0 +R2018 §20.4.78 GEODATA scale_estimation_method 95 1 0 0 0 +R2018 §20.4.78 GEODATA sea_level_correction 294 1 0 0 0 +R2018 §20.4.78 GEODATA sea_level_elevation 142 1 0 0 0 +R2018 §20.4.78 GEODATA up_direction.x 210 1 0 0 0 +R2018 §20.4.78 GEODATA up_direction.y 220 1 0 0 0 +R2018 §20.4.78 GEODATA up_direction.z 230 1 0 0 0 +R2018 §20.4.78 GEODATA user_scale_factor 141 1 0 0 0 +R2018 §20.4.78 GEODATA vertical_unit_scale 41 1 0 0 0 +R2018 §20.4.78 GEODATA vertical_units 92 1 0 0 0 R2018 §20.4.82 IMAGEDEF_REACTOR class_version 90 1 0 0 0 R2018 §20.4.84 LAYOUT current_style_sheet 7 7 0 0 0 R2018 §20.4.84 LAYOUT drawing_units 143 7 0 0 0 From a7523fb681850c1615a3eaa9cd3b3a2f532b5c59 Mon Sep 17 00:00:00 2001 From: Griffin Long <151978260+DrunkOnJava@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:54:02 -0400 Subject: [PATCH 2/2] chore(corpus): commit the baselines CI run 40719710653 measured --- README.md | 4 ++-- STATUS.md | 2 +- tools/oracle/baseline.tsv | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 35a705b..885fc01 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 40719710653](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40719710653) 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): @@ -99,7 +99,7 @@ provisional until a controlled AutoCAD drawing confirms it (#348). add up, not that the values are right. The DXF value oracle ([`examples/dxf_oracle/`](./examples/dxf_oracle/main.rs)) pairs every decoded record with the same handle in AutoCAD's DXF export and scores each field. On -`main` it scores 173,688 field comparisons on 73 types: 153,787 equal, 11,916 equal to the DXF default, 288 different and 7,697 present only in the DWG +`main` it scores 173,765 field comparisons on 74 types: 153,864 equal, 11,916 equal to the DXF default, 288 different and 7,697 present only in the DWG ([`tools/oracle/baseline.tsv`](./tools/oracle/baseline.tsv) pins the result, and CI fails when it moves). Decoded types with no scorer yet are listed as unscored in the oracle summary the Corpus job uploads. diff --git a/STATUS.md b/STATUS.md index 052b813..1e8f5cd 100644 --- a/STATUS.md +++ b/STATUS.md @@ -15,7 +15,7 @@ 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 + `Corpus (public DWG)` job, [run 40719710653](https://github.com/DrunkOnJava/dwg-rs/actions/runs/40719710653) 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 %. 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 diff --git a/tools/oracle/baseline.tsv b/tools/oracle/baseline.tsv index 48e4f9b..8cbc301 100644 --- a/tools/oracle/baseline.tsv +++ b/tools/oracle/baseline.tsv @@ -7226,6 +7226,39 @@ R2018 §20.4.75 HATCH seed point count 98 8 0 0 0 R2018 §20.4.75 HATCH seed_points.x 10 8 0 0 0 R2018 §20.4.75 HATCH seed_points.y 20 8 0 0 0 R2018 §20.4.75 HATCH solid_fill 70 8 0 0 0 +R2018 §20.4.78 GEODATA coordinate_projection_radius 143 1 0 0 0 +R2018 §20.4.78 GEODATA coordinate_system_definition (303 chunks + 301) 303+301 1 0 0 0 +R2018 §20.4.78 GEODATA design_coordinate_type 70 1 0 0 0 +R2018 §20.4.78 GEODATA design_point.x 10 1 0 0 0 +R2018 §20.4.78 GEODATA design_point.y 20 1 0 0 0 +R2018 §20.4.78 GEODATA design_point.z 30 1 0 0 0 +R2018 §20.4.78 GEODATA geo_rss_tag 302 1 0 0 0 +R2018 §20.4.78 GEODATA horizontal_unit_scale 40 1 0 0 0 +R2018 §20.4.78 GEODATA horizontal_units 91 1 0 0 0 +R2018 §20.4.78 GEODATA mesh face count 96 1 0 0 0 +R2018 §20.4.78 GEODATA mesh point count 93 1 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.destination.x 14 12 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.destination.y 24 12 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.source.x 13 12 0 0 0 +R2018 §20.4.78 GEODATA mesh_points.source.y 23 12 0 0 0 +R2018 §20.4.78 GEODATA north_direction.x 12 1 0 0 0 +R2018 §20.4.78 GEODATA north_direction.y 22 1 0 0 0 +R2018 §20.4.78 GEODATA object_version 90 1 0 0 0 +R2018 §20.4.78 GEODATA observation_coverage_tag 307 1 0 0 0 +R2018 §20.4.78 GEODATA observation_from_tag 305 1 0 0 0 +R2018 §20.4.78 GEODATA observation_to_tag 306 1 0 0 0 +R2018 §20.4.78 GEODATA reference_point.x 11 1 0 0 0 +R2018 §20.4.78 GEODATA reference_point.y 21 1 0 0 0 +R2018 §20.4.78 GEODATA reference_point.z 31 1 0 0 0 +R2018 §20.4.78 GEODATA scale_estimation_method 95 1 0 0 0 +R2018 §20.4.78 GEODATA sea_level_correction 294 1 0 0 0 +R2018 §20.4.78 GEODATA sea_level_elevation 142 1 0 0 0 +R2018 §20.4.78 GEODATA up_direction.x 210 1 0 0 0 +R2018 §20.4.78 GEODATA up_direction.y 220 1 0 0 0 +R2018 §20.4.78 GEODATA up_direction.z 230 1 0 0 0 +R2018 §20.4.78 GEODATA user_scale_factor 141 1 0 0 0 +R2018 §20.4.78 GEODATA vertical_unit_scale 41 1 0 0 0 +R2018 §20.4.78 GEODATA vertical_units 92 1 0 0 0 R2018 §20.4.80 IMAGE brightness 281 1 0 0 0 R2018 §20.4.80 IMAGE clip vertex count (a polygon is closed by repeating its first) 91 1 0 0 0 R2018 §20.4.80 IMAGE clip_boundary[0].x 14 1 0 0 0