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/docs/data/spec-coverage.tsv b/docs/data/spec-coverage.tsv index cfc7cd3..df666d8 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). DXF oracle (examples/dxf_oracle): 48 records R2000-R2018, every decoded value equals AutoCAD's (23360 equal, 0 by the DXF default, 20 with no DXF group); 454 DXF groups not decoded. 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 DXF oracle (examples/dxf_oracle): 6 records R2000-R2018, every decoded value equals AutoCAD's (135 equal, 0 by the DXF default, 0 with no DXF group); 48 DXF groups not decoded. 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 DXF oracle (examples/dxf_oracle): 6 records R2000-R2018, every decoded value equals AutoCAD's (48 equal, 0 by the DXF default, 0 with no DXF group); 12 DXF groups not decoded. 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 364b24b..8271c6d 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). DXF oracle (examples/dxf_oracle): 48 records R2000-R2018, every decoded value equals AutoCAD's (23360 equal, 0 by the DXF default, 20 with no DXF group); 454 DXF groups not decoded. 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 | DXF oracle (examples/dxf_oracle): 6 records R2000-R2018, every decoded value equals AutoCAD's (135 equal, 0 by the DXF default, 0 with no DXF group); 48 DXF groups not decoded. 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 | DXF oracle (examples/dxf_oracle): 6 records R2000-R2018, every decoded value equals AutoCAD's (48 equal, 0 by the DXF default, 0 with no DXF group); 12 DXF groups not decoded. 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 01c5f8b..2e14392 100644 --- a/examples/dxf_oracle/scorers.rs +++ b/examples/dxf_oracle/scorers.rs @@ -786,6 +786,7 @@ pub fn section_of(label: &str) -> &'static str { "MLINESTYLE" => "§20.4.73", "MLINE" => "§20.4.50", "MLEADERSTYLE" => "§20.4.87", + "GEODATA" => "§20.4.78", "DICTIONARYVAR" => "§20.4.74", "LAYOUT" => "§20.4.84", "LWPOLYLINE" => "§20.4.85", @@ -1043,6 +1044,7 @@ pub fn score( D::Leader(l) => score_leader(&mut c, l), 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". @@ -2088,6 +2090,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 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