forked from 1b-agent/opencadstudio-upstream
chore: pin cadcodec at dd1d7bf and cadkernel at 6eae7c9
Brings in the DXF round-trip fixes merged as cadcodec #48 and #51 and the block-definition fixes for cadcodec issues #49 and #52. cadkernel moved to the same codec revision first, so the graph keeps a single acadrust. The Python plugin, which is outside the workspace, has its own lock moved too: its staging script requires the plugin and the host to share one acadrust source. The canaries that asserted the old wrong results were flipped against the real-IPC audits, all 60 of which pass with the plugin built: Helix, Light, Underlay, ViewBorder and SectionSymbol now round-trip through DXF, as do the text style flags, the dimension style's text style name, the block description, the table's merged ranges and the leader's annotation link and vectors. Those five kinds move to Complete in the coverage ledger. Two narrower gaps stay pinned. The ATTDEF writer emits no group 280, so `lock_position` is still lost on a DXF save; every other field now survives. A true-colour leader `override_color` cannot be held by DXF group 77, which takes an ACI index, and the DWG writer does not store it. Behaviour change for users: a text style's oblique angle is now written to DXF in degrees, as the format defines it. DXF files written by earlier versions carry radians there and reopen with a much smaller slant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2d534bf71
commit
a479dd11d1
8 files changed
+72
-105
No files matched your search
Generated
+3
-3
@@ -87,7 +87,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.5.5"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=5b682ed#5b682ed66ea2c89be8142c8dd83d83774fc3de08"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=dd1d7bf#dd1d7bfe03f4e733d351c0396e2cf82ae9df957d"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
@@ -923,7 +923,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6f046af#6f046afdda94d8cd9cdbd7fc0ab73ee726aac9ab"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6eae7c9#6eae7c97fb29a21a818164af7c009f93e3ffe5b8"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
@@ -934,7 +934,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "cadkernel-constraints"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6f046af#6f046afdda94d8cd9cdbd7fc0ab73ee726aac9ab"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6eae7c9#6eae7c97fb29a21a818164af7c009f93e3ffe5b8"
|
||||
dependencies = [
|
||||
"nalgebra",
|
||||
]
|
||||
|
||||
+3
-3
@@ -33,9 +33,9 @@ rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive", "string"] }
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b682ed", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6f046af", features = ["acis", "offset"] }
|
||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6f046af" }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd1d7bf", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6eae7c9", features = ["acis", "offset"] }
|
||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6eae7c9" }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true }
|
||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b682ed", optional = true, features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd1d7bf", optional = true, features = ["serde"] }
|
||||
|
||||
# Runtime IPC and serialization (host feature only).
|
||||
interprocess = { version = "2", optional = true }
|
||||
@@ -39,7 +39,7 @@ serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
cargo-lock = "11"
|
||||
# acadrust is scanned at build time to generate the embedded type registry.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b682ed", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd1d7bf", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
@@ -8,7 +8,7 @@ publish = false
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b682ed", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd1d7bf", features = ["serde"] }
|
||||
bincode = "1.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
# cadcodec DXF reader/writer gaps found by the OCS Python host audit
|
||||
|
||||
Reports on `HakanSeven12/cadcodec` (the `acadrust` crate). **Status (21 September
|
||||
2026):** fixes for issues 1-6 below, with regression tests, are submitted as
|
||||
[HakanSeven12/cadcodec#48](https://github.com/HakanSeven12/cadcodec/pull/48) (branch
|
||||
`fix/dxf-reader-writer-roundtrip` on `felixriestra/cadcodec`); the five findings under
|
||||
"Observed but not filed" and the style and block ones added later are not filed. Every finding was made against **cadcodec revision
|
||||
`5b682ed`** (OCS pins `acadrust` to it) by saving a document with
|
||||
`DxfWriter` or `DwgWriter`, reloading it, and comparing entity fields.
|
||||
Reports on `HakanSeven12/cadcodec` (the `acadrust` crate). Every finding was made against
|
||||
**cadcodec revision `5b682ed`** by saving a document with `DxfWriter` or `DwgWriter`,
|
||||
reloading it, and comparing entity fields.
|
||||
|
||||
**Status (23 September 2026):** the fixes landed in cadcodec as
|
||||
[#48](https://github.com/HakanSeven12/cadcodec/pull/48) (issues 1-6) and
|
||||
[#51](https://github.com/HakanSeven12/cadcodec/pull/51) (the first three style findings), and
|
||||
the block description with commit `dd1d7bf` (cadcodec issue #49). OCS pins `dd1d7bf`, and the
|
||||
canaries below were flipped against it. What is still open:
|
||||
|
||||
- **ATTDEF `lock_position`** (issue 1): the reader is complete, but the DXF writer emits no
|
||||
group 280 at all, neither the version byte nor the lock flag the reader expects after it.
|
||||
- **LEADER true-colour `override_color`** (issue 4): DXF group 77 holds an ACI index only, so
|
||||
a true colour cannot be written there, and the DWG writer does not store the field.
|
||||
- **`TextStyle::true_type_font`** (style findings): still neither written nor read.
|
||||
|
||||
Each report names an executable canary in OCS (`src/app/plugin_host.rs`). The
|
||||
canaries assert today's wrong result, so they fail loudly once cadcodec is
|
||||
@@ -214,7 +222,8 @@ AutoCAD expects and that cannot be checked here.
|
||||
- **`DIMSTYLE` text style name:** the DXF reader keeps only the text-style handle (group 340) and never
|
||||
resolves `dimtxsty` from it, so the name reopens as `Standard` (the handle is right; DWG resolves both).
|
||||
- **`TextStyle::true_type_font`** is never written or read by either codec, so it lives only in memory.
|
||||
- **`BLOCK_RECORD` description:** the DXF codec does not carry `BlockRecord::description`
|
||||
(found by `audit_python_blocks_over_real_ipc`, pinned by a canary); DWG keeps it. Not filed, for
|
||||
the same reason as `true_type_font`.
|
||||
- **`BLOCK_RECORD` description:** the DXF codec did not carry `BlockRecord::description`
|
||||
(found by `audit_python_blocks_over_real_ipc`). It needs no XDATA: it is BLOCK group 4, which
|
||||
the reader parsed onto the discarded BLOCK marker and the writer never emitted. Fixed in
|
||||
cadcodec `dd1d7bf` (cadcodec issue #49).
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# OCS canvas kind coverage ledger
|
||||
|
||||
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 21 September 2026 (the object tables, block contents and command runner added since are described in [the design note](plugin-host-model.md) and the plan's progress log; they are not entity kinds): **43 of 43 canvas kinds have explicit geometry writes** (41 creatable, plus update-only legacy `Polyline` and `Body`), **31 kinds are complete**, and none remains layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
|
||||
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 23 September 2026 (the object tables, block contents and command runner added since are described in [the design note](plugin-host-model.md) and the plan's progress log; they are not entity kinds): **43 of 43 canvas kinds have explicit geometry writes** (41 creatable, plus update-only legacy `Polyline` and `Body`), **36 kinds are complete**, and none remains layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
|
||||
|
||||
**Rows blocked at W by cadcodec.** Fixes for six of the DXF bugs behind them are submitted as [HakanSeven12/cadcodec#48](https://github.com/HakanSeven12/cadcodec/pull/48) (ATTDEF fields, boolean group codes for Helix and Light, UNDERLAY rotation, LEADER link and vectors, DRAWINGVIEW/SECTIONLINE dispatch, TABLE merged ranges). OCS still pins the unfixed revision `5b682ed`, so those rows stay blocked and their canaries still assert the wrong result; the expectation is that they flip and the rows move to `Complete` once #48 is merged and OCS repins. That has been checked in cadcodec's own round-trip tests, not yet in OCS.
|
||||
**Rows that were blocked at W by cadcodec.** The DXF fixes they waited on landed as [HakanSeven12/cadcodec#48](https://github.com/HakanSeven12/cadcodec/pull/48) and [#51](https://github.com/HakanSeven12/cadcodec/pull/51), and OCS pins cadcodec `dd1d7bf` since 23 September 2026. The canaries flipped in the real-IPC audits and Helix, Underlay, ViewBorder, SectionSymbol and Light moved to `Complete`. Two rows keep a narrower blocker: AttributeDefinition (the DXF writer emits no group 280, so `lock_position` is lost) and Leader (a true-colour `override_color`, which DXF group 77 cannot hold and the DWG writer does not store).
|
||||
|
||||
Completion gate codes: **C** = create through Python document model; **R** = read; **E** = edit defining geometry and another property; **D** = delete; **U** = undo/redo and atomic failure; **I** = real IPC plus GUI/canvas pick or observable behavior; **W** = DWG and DXF save/reopen; **V** = invalid-input checks and untouched-field/reference preservation; **P** = build and load the in-tree bundled plugin with a manifest matching the host toolchain and acadrust source. A kind is `Complete` only when **C/R/E/D/U/I/W/V/P** all pass and its property catalog matches implementation. `Blocked` requires a specific dependency or failing fixture.
|
||||
|
||||
@@ -27,10 +27,10 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| Insert | **Complete** | Decision (2026-09-20): Insert is creatable. A script names an existing block with `block_name` and a placement; the host does not generate attribute records, because attributes stay with the `AttributeEntity` flow that already creates them from the block's ATTDEFs. Writable: `block_name` (fixed after creation; the mutation check refuses a different block), `insert_point`, `x_scale`, `y_scale`, `z_scale`, `rotation`, `normal`, `column_count`, `row_count`, `column_spacing`, `row_spacing`. Creation and rename rules: the block must exist, must not be model or paper space, and must not make a block contain itself directly or through nested inserts; scales must be finite and nonzero, array counts positive, and the name non-empty. An edit that keeps the block name is exempt from the block checks so a legacy insert of a missing block stays editable. `audit_python_insert_lifecycle_over_real_ipc` (shared driver, host-made block) passes C/R/E/D/U/I/W/V/P: creation by block name, a six-property transaction edit, canvas wires, rejection of zero scale, non-finite rotation and point, zero array count, zero normal and an unknown block, an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. `audit_python_insert_creation_rules_over_real_ipc` covers unknown block, model-space block, bad scale, zero count, empty name, direct self-insertion and indirect nesting cycles. Not covered: MINSERT (`dwg_minsert`) and attached-attribute creation, which stay excluded and preserved. |
|
||||
| Tolerance | **Complete** | `staged_python_tolerance_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with the staged API v7 plugin: high-level create/read/transaction edit/delete; insertion, direction, and text edits; live canvas selection; grouped undo/redo; invalid direction and missing style atomic rollback; exact untouched common/style/private-field preservation; and edited DWG plus DXF reopen. Host-owned coverage exposes placement, direction, normal, text, style name, height and gap; style handle is read-only and the undocumented DWG short remains unmapped. `tolerance_geometry_and_style_references_are_validated` and the existing Tolerance tessellation tests cover reference and rendered-frame behavior. |
|
||||
| Shape | **Complete** | `staged_python_shape_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with a generated parser-valid SHX file and a real shape-file `TextStyle`: high-level create/read/transaction edit/delete; insertion, size, rotation and width-factor edits; live canvas selection; grouped undo/redo; invalid size and missing style rollback; exact identity/style/common-field preservation; and DWG plus DXF reopen. Each live and reopened document renders the three-point SHX glyph rather than the placeholder. Host-owned reference binding writes the stable style handle needed by DWG. The DXF fallback now searches all `is_shape_file` styles, preserving named-style glyph resolution after reopen. |
|
||||
| AttributeDefinition | **Integration-tested / blocked at W by acadrust (DXF ATTDEF reader)** | Host commit `5b854bd1` maps block ownership plus every stable ATTDEF field except the nested `embedded_mtext` payload, which is snapshot-readable and preserved untouched. Two staged tests cover it. `staged_python_attribute_definition_lifecycle_over_real_ipc` passes with a real block record, Block/BlockEnd markers and a linked Insert. The 2026-09-20 audit `audit_python_attribute_definition_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, host-made block) compares **every writable field** through both formats: creation with non-default tag, prompt, value, placement, alignment point, height, rotation, width factor, oblique angle, alignments, all attribute flags, field length, generation flags and lock; a nine-property transaction edit; canvas wires; eight rejection cases (whitespace tag, non-positive height and width factor, missing style, owner change, zero normal, non-finite rotation, unknown alignment) plus an atomic rollback; DWG reopen with the **complete** edited state and re-edit; deletion; and three-step undo/redo. **W blocker, revalidated on acadrust `5b682ed`:** the DXF writer emits every ATTDEF group, but the reader handles only groups 1, 2, 3, 10, 40, 50, 280 and 101, so a DXF reopen keeps tag, prompt, value, insertion point, height and rotation and resets the alignment point, both alignments, width factor, oblique angle, all attribute flags (invisible, constant, verify, preset), field length, generation flags, lock and text style to defaults. This is now pinned by canary expectations for both the edit and the re-edit. Not `Complete` until the reader is extended; the fix belongs in cadcodec's `read_attdef`. `embedded_mtext` mutation remains unsupported pending linked-layout validation. |
|
||||
| AttributeDefinition | **Integration-tested / blocked at W by cadcodec (DXF ATTDEF lock)** | Host commit `5b854bd1` maps block ownership plus every stable ATTDEF field except the nested `embedded_mtext` payload, which is snapshot-readable and preserved untouched. Two staged tests cover it. `staged_python_attribute_definition_lifecycle_over_real_ipc` passes with a real block record, Block/BlockEnd markers and a linked Insert. The 2026-09-20 audit `audit_python_attribute_definition_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, host-made block) compares **every writable field** through both formats: creation with non-default tag, prompt, value, placement, alignment point, height, rotation, width factor, oblique angle, alignments, all attribute flags, field length, generation flags and lock; a nine-property transaction edit; canvas wires; eight rejection cases (whitespace tag, non-positive height and width factor, missing style, owner change, zero normal, non-finite rotation, unknown alignment) plus an atomic rollback; DWG reopen with the **complete** edited state and re-edit; deletion; and three-step undo/redo. **W blocker, revalidated on cadcodec `dd1d7bf`:** the reader now handles every ATTDEF group and all fields round-trip through DXF except `lock_position`, because the DXF writer emits no group 280 (neither the version byte nor the lock flag after it). Pinned by canary expectations for the edit and the re-edit. Not `Complete` until `write_attdef` emits group 280. `embedded_mtext` mutation remains unsupported pending linked-layout validation. |
|
||||
| AttributeEntity | **Complete** | OCS commit `cf2a9235` and adapter commit `9c6b5c3` make nested ATTRIB records addressable by handle while retaining canonical storage in `Insert.attributes`. Host validation binds tag to a definition in the Insert's block, validates placement/text geometry and style references, keeps owner and definition handles read-only, and preserves embedded MTEXT. `staged_python_attribute_entity_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P with a real block, ATTDEF and Insert: high-level create/read/transaction edit/delete, parent selection, rendered text, atomic invalid tag/height/owner rejection, edited and deleted DWG/DXF reopen, and three-step undo/redo. `nested_insert_attribute_crud_uses_parent_storage_and_undo` additionally proves the child never becomes an orphan flat entity in the host document. |
|
||||
| Hatch | **Complete** | OCS commit `98b56ef6` and adapter commit `a9b5de8` expose solid and patterned Hatches with elevation/normal, complete pattern definitions, pattern controls, style, association state, boundary paths and seed points. Nested tagged boundary edges use the reusable `{kind, value}` adapter form. Host validation requires closed finite loops, validates line/arc/ellipse/polyline/spline payloads and stored pattern lines, resolves associative boundary handles, and preserves gradient and MPOLYGON payloads as explicitly unsupported. `staged_python_hatch_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P: Python solid creation, patterned boundary edit, live render-model pattern/boundary checks, selection, atomic invalid scale/association/open-loop/unmapped-property rejection, edited and deleted DWG/DXF reopen, post-reopen edits, and three-step undo/redo. |
|
||||
| Leader | **Integration-tested / blocked at W by acadrust** | Fields mapped writable: `vertices` (≥2 finite points, not all coincident), `annotation_handle`, `creation_type`, `path_type`, `dimension_style`, `arrow_enabled`, `hookline_enabled`, `hookline_direction`, `text_height`, `text_width`, `normal`, `horizontal_direction`, `block_offset`, `annotation_offset`. Read-only: `origin`, `dimension_gap`, `arrowhead_type`, `arrow_size`, `byblock_color`. `override_color` is writable through the new generator `Color` form (`{'kind': 'Rgb', 'value': {'r','g','b'}}`, `Index`, `ByLayer`, `ByBlock`, `None`). Unmapped but preserved on edit: the `dwg_unknown_*` fields. Host reference binding validates that `annotation_handle` exists, is not the leader itself, matches `creation_type` (Text/MText, Tolerance, Insert; `NoAnnotation` forbids a handle) and that `dimension_style` exists; nothing is silently defaulted. `staged_python_leader_lifecycle_over_real_ipc` passes C/R/E/D/U/I/V/P against acadrust `7ea4247`: linked Text annotation, path/height/arrow/offset transaction edit with untouched-field preservation, GUI pick grouping (scripted selection stays exact), seven single-edit rejections plus a cross-entity atomic rollback, four-step undo/redo, real render linework, DWG and DXF reopen and re-edit. Erasing a leader also erases its annotation (scene leader grouping); undo restores both. **W blockers (engine, recorded as canary assertions in the test):** (1) the DXF writer never emits group 340, so a DXF save unlinks the annotation; (2) DXF reads drop `annotation_offset` (group 213 is written but not reassembled); (3) DWG R2010+ does not store `text_height`, `text_width` or `hookline_enabled` (acadrust writes them only for R13–R2007 / R13–R14); (4) neither writer stores `override_color`, so it reopens as `ByLayer` in DWG and DXF. Not `Complete` until the engine round-trips (1) and (2); (3) needs a decision to document or make read-only. |
|
||||
| Leader | **Integration-tested / W open items (DWG-derived text fields, true-colour override)** | Fields mapped writable: `vertices` (≥2 finite points, not all coincident), `annotation_handle`, `creation_type`, `path_type`, `dimension_style`, `arrow_enabled`, `hookline_enabled`, `hookline_direction`, `text_height`, `text_width`, `normal`, `horizontal_direction`, `block_offset`, `annotation_offset`. Read-only: `origin`, `dimension_gap`, `arrowhead_type`, `arrow_size`, `byblock_color`. `override_color` is writable through the new generator `Color` form (`{'kind': 'Rgb', 'value': {'r','g','b'}}`, `Index`, `ByLayer`, `ByBlock`, `None`). Unmapped but preserved on edit: the `dwg_unknown_*` fields. Host reference binding validates that `annotation_handle` exists, is not the leader itself, matches `creation_type` (Text/MText, Tolerance, Insert; `NoAnnotation` forbids a handle) and that `dimension_style` exists; nothing is silently defaulted. `staged_python_leader_lifecycle_over_real_ipc` passes C/R/E/D/U/I/V/P against acadrust `7ea4247`: linked Text annotation, path/height/arrow/offset transaction edit with untouched-field preservation, GUI pick grouping (scripted selection stays exact), seven single-edit rejections plus a cross-entity atomic rollback, four-step undo/redo, real render linework, DWG and DXF reopen and re-edit. Erasing a leader also erases its annotation (scene leader grouping); undo restores both. **W:** (1) the DXF annotation link (group 340) and (2) `annotation_offset` (group 213) round-trip since cadcodec `dd1d7bf`. Still open: (3) DWG R2010+ does not store `text_height`, `text_width` or `hookline_enabled` (acadrust writes them only for R13–R2007 / R13–R14), which needs a decision to document or make read-only; (4) a true-colour `override_color` reopens as `ByLayer` in both formats — DXF group 77 holds an ACI index only, and the DWG writer does not store it (canary assertion in the test). |
|
||||
| MLine | **Complete** | Writable: `vertices` (raw vertex dicts; a script may pass only `position`), `flags` (integer, unknown bits rejected), `justification`, `normal`, `scale_factor`, `style_name`. Read-only: `start_point`, `style_handle`, `style_element_count`. Host validation requires ≥2 finite non-coincident vertices, ≥3 for a closed multiline, a finite nonzero scale and normal, an existing non-empty `MLineStyle`, and per-vertex segments that are either empty or match the style element count. Vertex `direction`/`miter`, segment layout and per-element miter offsets (justification and scale included) are host-derived by `normalize_scripted_mline` on every scripted create or edit, reusing the drawing command's `rebuild_mline_geometry`/`sync_mline_element_parameters`; dash and fill cuts survive an edit unless a vertex moves. `staged_python_mline_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P: create against the Standard style, a three-vertex/scale/justification transaction edit with untouched-field preservation, corner miter widening and element offsets checked, real `mline_lines` linework before and after DWG and DXF reopen, nine single-edit rejections plus a cross-entity atomic rollback, re-edit after reopen in both formats, deletion, and three-step undo/redo. Known limit: switching to a style with a different element count is rejected until the script also clears the vertices' `segments`; only the rejection is covered by the integration test. |
|
||||
| Dimension | **Complete** (all 9 subtypes) | `EntityType::Dimension` wraps a nine-variant enum, so the adapter uses a hand-written flat model (`dimension_model.rs.inc`, wired in through the manifest's `manual_kinds`) and the coverage catalog lists its properties through the policy's `synthetic` section. Python sees `subtype` (fixed at creation), the shared base fields (`text`, `style_name`, `normal`, `text_middle_point`, `attachment_point`, `text_rotation`, `horizontal_direction`, `flip_arrow1/2`, `text_user_positioned`) and only the fields of that subtype; a field from another subtype is rejected, not ignored. `actual_measurement` and the base definition point are host-derived (`normalize_scripted_dimension`, the same recompute grip edits use). Unmapped but preserved: `user_text`, `insertion_point`, `block_name`, version and DWG flags, style overrides in XDATA. Per-subtype status, all verified by `staged_python_dimension_lifecycle_over_real_ipc` (C/R/E/D/U/I/W/V/P; measurement and canvas wires checked for each, DWG and DXF reopen recorded per subtype): Linear (10), Aligned (5), Radius (5), Diameter (10), Angular3Pt (90°), Angular2Ln (90°), Ordinate (5), Arc (5·π/2), LargeRadial (6) — every one creates, reads, derives the right measurement, renders wires, and keeps its measurement through DWG and DXF. Edits (Linear second point/text/attachment, Radius definition point) update the measurement, nine rejection cases plus a cross-entity atomic rollback and three create-time rejections leave the document untouched, and post-reopen re-edit passes in both formats. Not covered: text formatting overrides, arrowheads and other DIMSTYLE overrides stored in XDATA, the anonymous `*D` block. The subtype-specific field meanings for the angular subtypes follow the DXF group-code layout rather than the acadrust convenience constructors. |
|
||||
| MultiLeader | **Complete** | Mapped through the generic generator, which gained support for `Color`, `LineWeight`, mixed unit/payload enums, fixed `[f64; N]` arrays, bitflags-2 newtypes and per-type module paths for this kind. Writable: `content_type`, `path_type`, `line_color`, `line_weight`, `enable_landing`, `enable_dogleg`, `dogleg_length`, `arrowhead_size`, `text_color`, `text_frame`, the four text attachments, attachment direction/point, `text_alignment`, `text_angle_type`, `text_direction_negative`, `scale_factor`, `enable_annotation_scale`, `extend_leader_to_text`, block content fields, the reference handles (`style_handle`, `text_style_handle`, `arrowhead_handle`, `line_type_handle`, `block_content_handle`) and `context`, which merges as a patch so a script can change `text_string` without resetting leader roots. Read-only: `dwg_version`, `property_override_flags`, `text_align_in_ipe` and the entity-level `text_height` (neither writer stores it; set `context.text_height`). Excluded but preserved: `arrowhead_overrides`, `block_attributes`. Host validation checks finite/positive scale, height, dogleg and arrow sizes, finite context geometry and transform, non-empty leader lines, content-type/context consistency, and that every referenced handle exists as the right object kind. `staged_python_multileader_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P: MText creation with a three-point leader root, a partial-context edit that replaces the path (four points), text, text location, dogleg and colour while preserving scale/content/common fields, canvas wires, eleven rejection cases plus a duplicate-handle atomic rollback, DWG and DXF reopen (text, path, dogleg, `context.text_height`, colour) with post-reopen re-edit, and three-step undo/redo. Not covered: block-content MultiLeaders (no block fixture yet), tolerance content, and style-defined defaults. |
|
||||
@@ -38,14 +38,14 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| PolygonMesh | **Complete** | Writable: `flags`, `m_vertex_count`, `n_vertex_count`, `m_smooth_density`, `n_smooth_density`, `smooth_type`, `elevation`, `normal`, `vertices` (each `{location, flags}`; a vertex's own common data is not scripted and defaults). Host validation requires M and N of at least 2, exactly M×N vertices, non-negative densities, finite elevation and locations, and a nonzero normal, so inconsistent counts are refused before any mutation. `staged_python_polygon_mesh_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/W/V/P: a 3×3 grid, a vertex move plus density and cubic smoothing in one transaction, canvas wires, seven rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. |
|
||||
| PolyfaceMesh | **Complete** | Writable: `elevation`, `flags`, `normal`, `start_width`, `end_width`, `smooth_surface`, `thickness`, `vertices` (location, flags, bulge, widths, tangent, id) and `faces` (four indices, flags, optional colour). `seqend_handle` is read-only and preserved. Host validation requires at least 3 finite vertices and 1 face, every face index within the vertex count (a negative index hides that edge, 0 marks an unused corner), at least 3 indices per face, and finite widths, thickness and elevation. `staged_python_polyface_mesh_lifecycle_over_real_ipc` passes the full gate: two triangles, a vertex move plus a new vertex and a hidden-edge face in one transaction, canvas wires, six rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. |
|
||||
| Mesh | **Complete** | Writable: `blend_crease`, `subdivision_level`, `vertices`, `faces`, `edges` (start, end, optional crease), `override_option`; `version` is read-only. Host validation requires at least 3 finite vertices, faces of at least 3 in-range indices, edges with distinct in-range endpoints and a finite non-negative crease, and a non-negative subdivision level, so out-of-range topology is rejected before mutation. `staged_python_mesh_lifecycle_over_real_ipc` passes the full gate: a quad with a creased edge, an added vertex and triangle plus subdivision level 1 in one transaction, canvas wires, seven rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. |
|
||||
| Helix | **Integration-tested / blocked at W by acadrust (DXF handedness)** | Writable generating parameters: `axis_base_point`, `start_point`, `axis_vector`, `radius` (the top radius; the base radius is the start point's distance from the axis), `turns`, `turn_height`, `handedness`, `constraint`. Read-only: `spline`, `major_version`, `maintenance_version`. The host rebuilds the derived NURBS spline with OCS's own helix builder on every scripted create or parameter edit (`normalize_scripted_helix`), so the spline always matches the parameters. Host validation requires finite positive radius, turns and turn height, a nonzero axis and a start point off the axis. `staged_python_helix_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/V/P: creation with a derived spline ending at z = turns × turn height, a turns and handedness edit that regenerates it, canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, DXF reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary assertion):** the DXF reader parses boolean group 290 as an `i16` and never applies it, so a clockwise helix reopens counter-clockwise from DXF while DWG keeps it. Not `Complete` until the reader is fixed. |
|
||||
| Helix | **Complete** | W passes since cadcodec `dd1d7bf`: the DXF canary in the real-IPC audit flipped. Writable generating parameters: `axis_base_point`, `start_point`, `axis_vector`, `radius` (the top radius; the base radius is the start point's distance from the axis), `turns`, `turn_height`, `handedness`, `constraint`. Read-only: `spline`, `major_version`, `maintenance_version`. The host rebuilds the derived NURBS spline with OCS's own helix builder on every scripted create or parameter edit (`normalize_scripted_helix`), so the spline always matches the parameters. Host validation requires finite positive radius, turns and turn height, a nonzero axis and a start point off the axis. `staged_python_helix_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/V/P: creation with a derived spline ending at z = turns × turn height, a turns and handedness edit that regenerates it, canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, DXF reopen and re-edit, and three-step undo/redo. **W (formerly blocked):** the DXF reader parsed boolean group 290 as an `i16` and never applied it, so a clockwise helix reopened counter-clockwise; fixed in cadcodec `dd1d7bf`, and the handedness now round-trips through both formats. |
|
||||
| RasterImage | **Integration-tested** | Writable: `file_path` (creation only; a later change is rejected because the image definition owns it), `insertion_point`, `u_vector`, `v_vector` (world units per pixel), `flags`, `clipping_enabled`, `brightness`, `contrast`, `fade`, `clip_boundary`. Read-only: `size` (pixel size, taken from the file), `class_version`, `definition_handle`, `definition_reactor_handle`. Creating from a script requires a readable image file: the host reads its pixel size, creates the `ImageDefinition` object, links it, and sets `size` and a whole-image clip boundary from the file. Host validation requires finite, non-parallel `u`/`v` vectors, brightness/contrast/fade within 0..=100, known display flags, finite clip vertices, at least 2 clip vertices when clipping is enabled, and an existing definition. `staged_python_raster_image_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, real PNG fixture) passes C/R/E/D/U/I/W/V/P inside OCS: creation with a host-made definition, placement/scale/brightness/contrast/fade and clip edit in one transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG and DXF reopen with the definition link and file path intact, re-edit, and three-step undo/redo. **Linkage (updated 2026-09-20):** a scripted image now reuses one `ImageDefinition` per file and registers each definition in `ACAD_IMAGE_DICT` under the file's name (a `_2`, `_3` suffix keeps keys unique), creating that dictionary under the root when the drawing has none, and the dictionary owns the definition, as for an image AutoCAD attached. `audit_python_raster_image_definition_linkage_over_real_ipc` checks two images of one file sharing a definition, a third file getting its own, the entries resolving with the right owner, and all of it surviving DWG and DXF reopen and an undo without a dangling entry. **Why not `Complete`:** the host still writes no `IMAGEDEF_REACTOR`: cadcodec's struct documents the reactor's owner as the definition while its DXF writer follows a different convention, and AutoCAD's behavior cannot be checked here, so guessing risks a structure AutoCAD repairs or rejects. **Related finding:** OCS's own IMAGE command creates a bare `RasterImage` with no definition, and a save through either writer drops its file path (`spike_raster_image_definition_persistence`), while the definition-linked form keeps it. Editing an existing definition (file path, resolution) is out of scope. |
|
||||
| Wipeout | **Complete** | Writable: `insertion_point`, `u_vector`, `v_vector` (the region's world extent), `size`, `flags`, `clipping_enabled`, `brightness`, `contrast`, `fade`, `clip_mode`, `clip_type` and `clip_boundary_vertices` (normalized image coordinates, -0.5..0.5). Read-only: `class_version`, `definition_handle`, `definition_reactor_handle`. Scripted creation matches OCS's native WIPEOUT command, which also leaves the definition link unset. Host validation requires finite non-parallel `u`/`v` vectors, a positive `size`, brightness/contrast/fade within 0..=100, known flags, finite clip vertices, exactly 2 vertices for a rectangular clip and at least 3 for a polygonal one. `staged_python_wipeout_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/W/V/P: rectangular creation, a switch to a three-vertex polygonal inside-clip with a move in one transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. |
|
||||
| Underlay | **Integration-tested / blocked at W by acadrust (DXF rotation units)** | Writable: `underlay_type` and `definition_handle` (both fixed by the definition: creation names an existing `UnderlayDefinition`, and a mismatched type or missing definition is rejected), `insertion_point`, `x_scale`, `y_scale`, `z_scale`, `rotation`, `normal`, `flags`, `contrast`, `fade`, `clip_boundary_vertices`, `clip_inverted`. The file path and page live on the definition object, which scripts cannot create or list yet; they reference one the drawing already owns. Host validation requires finite nonzero scales, a finite rotation, a nonzero normal, contrast/fade within 0..=100, known flags, finite clip vertices (none, or at least 2) and an existing definition of the same type. `staged_python_underlay_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle` with a fixture PDF definition) passes C/R/E/D/U/I/V/P and DWG W: creation, a placement/scale/rotation/contrast/fade/clip transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by canary assertions):** DXF stores the rotation in degrees but the reader returns it unconverted, so 0.5 rad reopens as 28.6 and each further DXF save-reopen compounds it (1641.4 after a second), silently corrupting the rotation. Not `Complete` until the reader converts degrees back to radians. Not covered: creating definitions from a file path, DWF/DGN fixtures. |
|
||||
| Underlay | **Complete** | W passes since cadcodec `dd1d7bf`: the DXF canary in the real-IPC audit flipped. Writable: `underlay_type` and `definition_handle` (both fixed by the definition: creation names an existing `UnderlayDefinition`, and a mismatched type or missing definition is rejected), `insertion_point`, `x_scale`, `y_scale`, `z_scale`, `rotation`, `normal`, `flags`, `contrast`, `fade`, `clip_boundary_vertices`, `clip_inverted`. The file path and page live on the definition object, which scripts cannot create or list yet; they reference one the drawing already owns. Host validation requires finite nonzero scales, a finite rotation, a nonzero normal, contrast/fade within 0..=100, known flags, finite clip vertices (none, or at least 2) and an existing definition of the same type. `staged_python_underlay_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle` with a fixture PDF definition) passes C/R/E/D/U/I/V/P and DWG W: creation, a placement/scale/rotation/contrast/fade/clip transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W (formerly blocked):** DXF stores the rotation in degrees and the reader returned it unconverted, compounding on every save-reopen; fixed in cadcodec `dd1d7bf`, and the rotation now round-trips through both formats. Not covered: creating definitions from a file path, DWF/DGN fixtures. |
|
||||
| Viewport | **Complete (OCS round trip)** | Writable: `center`, `width`, `height`, `status`, `view_center`, `view_direction`, `view_target`, `lens_length`, `front_clip_z`, `back_clip_z`, `view_height`, `twist_angle`, `frozen_layers` (layer handles), `render_mode`, `circle_sides`, `ucs_icon_visible`. Read-only: `id`, `custom_scale`, snap and grid settings, UCS fields, `clip_boundary_handle`, `style_sheet`, shade-plot, background, visual-style and sun handles, lighting, `ambient_color`, `elevation`. Reference rules: the owner must be a paper-space layout block and every frozen-layer handle an existing layer. **Derived by the host (new 2026-09-20), as the MVIEW command does:** a scripted viewport gets an `id` unique in its layout and at least 2 (max existing plus one), and `custom_scale` is `height / view_height`, recomputed whenever either changes, so `custom_scale` is now read-only (setting the scale directly would have let it drift from the geometry). Host validation requires finite centers, positive width, height, view height, lens length and scale, a nonzero view direction, finite clip planes and twist, and at least 3 circle sides. **Canvas oracle (new):** the shared driver now activates the layout that owns the viewport (`Scene::set_current_layout`) and requires the viewport to draw a frame both after creation and after the edit. `staged_python_viewport_lifecycle_over_real_ipc` passes C/R/E/D/U/I/V/W/P: creation in the paper-space block, a move/resize/view-height/twist/frozen-layer transaction, seven rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. `audit_python_viewport_ids_and_scale_over_real_ipc` checks unique ids (2, 3, 4), the derived scale on create and after a zoom and a resize, an id kept across edits, a rejected direct `custom_scale` write, and that every viewport draws a frame. A control showed that the engine default id 0 is **not** invisible in a layout with no sheet viewport (it draws a frame), which corrects an earlier suspicion; the derived id matters for uniqueness. Not covered: clip boundary entities (`clip_boundary_handle` is read-only and preserved), non-rectangular viewports, and AutoCAD acceptance of the written viewports, which cannot be tested here. |
|
||||
| ViewBorder | **Integration-tested / blocked at W by acadrust (DXF kind)** | Writable: `min`, `max`, `center` (2-D pairs), `scale`, `rotation_angle`, `active_viewport` and `scale_handle` (both required at creation). `version` is read-only. Reference rules: `active_viewport` must be an existing Viewport entity and `scale_handle` an existing `Scale` object. Host validation requires finite bounds with `min` below `max` on both axes, a positive scale and a finite rotation. `staged_python_view_border_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, driver-made viewport and scale fixtures) passes C/R/E/D/U/I/V/P and DWG W: creation, a resize/recenter/scale/rotation transaction, canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary):** a DXF `DRAWINGVIEW` reopens as a different, opaque entity kind rather than as a ViewBorder, so only DWG restores the typed record. Not `Complete` until DXF loading restores the typed entity. Not covered: the linked view representation and drawing-view definition records. |
|
||||
| SectionSymbol | **Integration-tested / blocked at W by cadcodec (DXF SECTIONLINE)** | The earlier blocker was overstated, and reading cadcodec and OCS showed why. cadcodec documents `points` as the canonical geometry, says the point-count fields equal the point count in every verified ODA export, and ships `sync_display_fields()` to refresh the `end_a`/`end_b`/`tick_a`/`tick_b`/`label` projections; OCS's own grip and transform code already calls it. OCS also renders a symbol with no style and no view link. So the kind is mapped: writable `points` (each `{point, bulge, label, label_offset, raw_flag_280}`), `symbol_scale`, `style_handle` and `view_rep_handle`; read-only `view_symbol_version`, `raw_view_symbol_70`, `version`, `raw_flags_90`, the two derived counts and the five projections. The host sets `raw_point_count_90` and `raw_point_record_count` to the point count and calls `sync_display_fields()` whenever `points` is created or changed, and leaves a symbol whose points did not change exactly as read. Host validation requires at least two finite points with finite bulges and label offsets, a positive scale, and that each non-null reference is an existing `SectionViewStyle` (style) or `ViewRep` (view representation) class object; the wrong object kind is refused. Creating one without references gives a standalone symbol. `audit_python_section_symbol_lifecycle_over_real_ipc` (shared driver) passes C/R/E/D/U/I/V/P and DWG W: a two-point symbol, a three-point edit with a new label, bulge and scale in one transaction (counts and projections follow), canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, three-step undo/redo. `audit_python_section_symbol_references_over_real_ipc` covers valid references, a standalone symbol and four wrong-object refusals with host-made class objects. **W blocker (cadcodec, pinned by a canary; issue 5 in [cadcodec-reader-gaps.md](cadcodec-reader-gaps.md)):** the DXF reader dispatches `SECTIONLINE` only inside blocks, so a symbol in the drawing's entity list reopens as an `Unknown` entity. Open items that cannot be checked here: `raw_flags_90` has unknown semantics (0 for a new symbol, preserved on read), and whether AutoCAD accepts a symbol with no parent view. Scripts cannot create the style or view-representation objects. |
|
||||
| Light | **Integration-tested / blocked at W by acadrust (DXF `cast_shadows`)** | Writable: `name`, `light_type` (1 distant, 2 point, 3 spot), `position`, `target`, `status`, `light_color`, `plot_glyph`, `intensity`, the attenuation type and limits, `hotspot_angle`, `falloff_angle`, `cast_shadows`, `shadow_type`, `shadow_map_size`, `shadow_map_softness`, `photometric_mode`, `photometric_data`; `class_version` is read-only. Host validation requires a non-empty name, a known light type, finite placement, a spot or distant target that differs from the position, non-negative intensity, angles and limits, an attenuation start not above its end, a spot hotspot not above its falloff, and a non-negative shadow map size. `staged_python_light_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/V/P and DWG W: a spot light with an RGB colour, an intensity/target/position/shadow transaction that preserves the colour and untouched fields, the canvas glyph, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary):** the DXF reader does not restore `cast_shadows`, which reopens false; colour, placement and intensity persist. Not covered: photometric web data edits and sun/skylight objects. |
|
||||
| ViewBorder | **Complete** | W passes since cadcodec `dd1d7bf`: the DXF canary in the real-IPC audit flipped. Writable: `min`, `max`, `center` (2-D pairs), `scale`, `rotation_angle`, `active_viewport` and `scale_handle` (both required at creation). `version` is read-only. Reference rules: `active_viewport` must be an existing Viewport entity and `scale_handle` an existing `Scale` object. Host validation requires finite bounds with `min` below `max` on both axes, a positive scale and a finite rotation. `staged_python_view_border_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, driver-made viewport and scale fixtures) passes C/R/E/D/U/I/V/P and DWG W: creation, a resize/recenter/scale/rotation transaction, canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W (formerly blocked):** a DXF `DRAWINGVIEW` in the entity list reopened as an opaque entity kind; fixed in cadcodec `dd1d7bf`, and DXF now restores the typed ViewBorder. Not covered: the linked view representation and drawing-view definition records. |
|
||||
| SectionSymbol | **Complete** | W passes since cadcodec `dd1d7bf`: the DXF canary in the real-IPC audit flipped. The earlier blocker was overstated, and reading cadcodec and OCS showed why. cadcodec documents `points` as the canonical geometry, says the point-count fields equal the point count in every verified ODA export, and ships `sync_display_fields()` to refresh the `end_a`/`end_b`/`tick_a`/`tick_b`/`label` projections; OCS's own grip and transform code already calls it. OCS also renders a symbol with no style and no view link. So the kind is mapped: writable `points` (each `{point, bulge, label, label_offset, raw_flag_280}`), `symbol_scale`, `style_handle` and `view_rep_handle`; read-only `view_symbol_version`, `raw_view_symbol_70`, `version`, `raw_flags_90`, the two derived counts and the five projections. The host sets `raw_point_count_90` and `raw_point_record_count` to the point count and calls `sync_display_fields()` whenever `points` is created or changed, and leaves a symbol whose points did not change exactly as read. Host validation requires at least two finite points with finite bulges and label offsets, a positive scale, and that each non-null reference is an existing `SectionViewStyle` (style) or `ViewRep` (view representation) class object; the wrong object kind is refused. Creating one without references gives a standalone symbol. `audit_python_section_symbol_lifecycle_over_real_ipc` (shared driver) passes C/R/E/D/U/I/V/P and DWG W: a two-point symbol, a three-point edit with a new label, bulge and scale in one transaction (counts and projections follow), canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, three-step undo/redo. `audit_python_section_symbol_references_over_real_ipc` covers valid references, a standalone symbol and four wrong-object refusals with host-made class objects. **W (formerly blocked; issue 5 in [cadcodec-reader-gaps.md](cadcodec-reader-gaps.md)):** the DXF reader dispatched `SECTIONLINE` only inside blocks, so a symbol in the entity list reopened as `Unknown`; fixed in cadcodec `dd1d7bf`. Open items that cannot be checked here: `raw_flags_90` has unknown semantics (0 for a new symbol, preserved on read), and whether AutoCAD accepts a symbol with no parent view. Scripts cannot create the style or view-representation objects. |
|
||||
| Light | **Complete** | W passes since cadcodec `dd1d7bf`: the DXF canary in the real-IPC audit flipped. Writable: `name`, `light_type` (1 distant, 2 point, 3 spot), `position`, `target`, `status`, `light_color`, `plot_glyph`, `intensity`, the attenuation type and limits, `hotspot_angle`, `falloff_angle`, `cast_shadows`, `shadow_type`, `shadow_map_size`, `shadow_map_softness`, `photometric_mode`, `photometric_data`; `class_version` is read-only. Host validation requires a non-empty name, a known light type, finite placement, a spot or distant target that differs from the position, non-negative intensity, angles and limits, an attenuation start not above its end, a spot hotspot not above its falloff, and a non-negative shadow map size. `staged_python_light_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/V/P and DWG W: a spot light with an RGB colour, an intensity/target/position/shadow transaction that preserves the colour and untouched fields, the canvas glyph, eight rejection cases plus an atomic rollback, DWG reopen and re-edit, and three-step undo/redo. **W (formerly blocked):** the DXF reader did not restore `cast_shadows`; fixed in cadcodec `dd1d7bf`, and every mapped field now round-trips through both formats. Not covered: photometric web data edits and sun/skylight objects. |
|
||||
| Region | **Complete (create from a profile + rigid move)** | New `SolidOperation::RegionFromProfile` and `doc.solids.region(profile, layer=None, delete_source=False)`. The host builds the region exactly as the REGION command does (`presspull_model::profile_geometry` then `cadkernel::brep::planar_region`), verifies it with `solid_to_sat`, puts it on `layer` or on the profile's layer, and erases the profile only when `delete_source` is set. One closed planar profile per call (a circle, ellipse, closed polyline, spline and so on); an open profile, a ray, a region, a missing entity or an empty layer is refused with a message before anything changes. Rigid moves go through the same `doc.solids.transform` channel as Solid3D. `audit_python_region_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P through the real runner: regions from a circle (source kept), a closed rectangle (source deleted) and a second circle on its own layer with exact extents, canvas geometry, six refusals that change nothing and record no undo step, a move with the exact shifted extent, DWG and DXF reopen with the payload lifting and the same extent, a reopened region moving again, and undo restoring the region exactly. Not covered: regions with holes and merging open curves into a boundary (the REGION command's second path), and AutoCAD acceptance of the payload. |
|
||||
| Body | **Integration-tested (rigid move only)** | Mapped read-only like Solid3D (same payload container). `audit_python_body_transform_over_real_ipc` moves a Body carrying a kernel box through `doc.solids.translate`, checks the exact extent, DWG reopen with a losslessly lifting payload and the exact volume, and the explanatory refusal of `create_entity('Body')`. Scripts cannot create a Body, and a non-solid (sheet or wire) body that does not lift is refused untouched. |
|
||||
| Solid3D | **Complete (create + rigid move)** | New in this session: an additive `HostApi::solid_operation` (`SolidOperation::Create` and `Transform`, plus a `SolidPrimitive` enum) carried by `PluginRequest::SolidOperation` / `PluginResponse::SolidResult` through both IPC clients, the server and `HostSession`, and a Python `doc.solids` API: `box`, `wedge`, `cylinder`, `sphere`, `torus`, `pyramid`, `transform`, `translate`, `rotate`, `mirror`. The host builds bodies with cadkernel, and every result goes through `solid_to_sat`, which lifts its own output back and refuses anything lossy. A transform first lifts the existing payload with `kernel_acis_body` and is refused, leaving the payload untouched, when that is lossy. Only rigid moves and mirrors are accepted (scaling and shear are refused). Regenerated `wires` follow the body, `silhouettes` are cleared and `history_handle` is dropped. Python sees `uid`, `point_of_reference` and `history_handle` read-only; `acis_data`, `wires` and `silhouettes` are excluded and preserved; creation through `create_entity` is refused with a message pointing at `doc.solids`. `audit_python_solid3d_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P through the real runner with the kernel as oracle: all six primitives match their analytic volumes (box, wedge and cube exactly, curved solids within 2 percent of the tessellated volume), a translate, a rotate and a mirror keep the volume and move the extent exactly and keep 12 edge wires, 13 refusals (zero and non-finite sizes, a torus with minor above major, too few pyramid sides, an empty layer, an unknown primitive, a wrong number count, a scaling matrix, a non-finite offset, a payload the kernel cannot lift, a missing entity) change nothing and leave no undo step, DWG and DXF reopen with the payload lifting losslessly and the same volume and extent, a reopened payload moves again, and undo restores the original solid exactly. Not covered: solids that carry parametric history, and AutoCAD acceptance of the written payload, which cannot be tested here. **Booleans** (`SolidOperation::Boolean`, `doc.solids.union`, `subtract` and `intersect`) are covered by `audit_python_boolean_lifecycle_over_real_ipc`: two overlapping boxes give exact volumes (union 360, subtract and intersect 120) and extents, the operands are consumed unless `keep_operands` (undo restores both exactly), a result takes the first operand's layer or an explicit one, a second planar operation chains on the result (a through-hole gives 104), eight refusals change nothing and record no undo step (same operand twice, a non-solid, a missing entity, an unliftable payload, an empty layer, an unknown operation, a disjoint intersection), and DWG and DXF reopen losslessly with a reopened result moving again. The kernel refuses many curved cases (measured in release: about 1 to 2 s when it succeeds, and `Coincident`, `CutRefused` or `NoClosedForm` when it does not, including a second cut in a drilled box); its reason reaches the script and nothing changes, shown by an `#[ignore]` curved test that takes about 27 s in a debug build. Booleans block the host thread and count against the script's 30 s budget; only Solid3D operands are accepted. |
|
||||
|
||||
Generated
+1
-1
@@ -5,7 +5,7 @@ version = 4
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.5.5"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=5b682ed#5b682ed66ea2c89be8142c8dd83d83774fc3de08"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=dd1d7bf#dd1d7bfe03f4e733d351c0396e2cf82ae9df957d"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
|
||||
+34
-76
@@ -4358,27 +4358,15 @@ mod tests {
|
||||
panic!("Leader missing after reopen");
|
||||
};
|
||||
assert_eq!(leader.vertices.len(), 4);
|
||||
if is_dxf {
|
||||
// BLOCKER: acadrust's DXF writer never emits group 340, so a
|
||||
// DXF save unlinks the annotation. Flip this to
|
||||
// `assert_eq!(.., text_handle)` once the engine writes it.
|
||||
assert!(leader.annotation_handle.is_null(), "acadrust now writes DXF 340; drop the blocker");
|
||||
} else {
|
||||
assert_eq!(leader.annotation_handle, text_handle);
|
||||
}
|
||||
assert_eq!(leader.annotation_handle, text_handle);
|
||||
assert!(!leader.arrow_enabled);
|
||||
// BLOCKER: neither writer stores `override_color`, so it reopens
|
||||
// as ByLayer in both formats.
|
||||
// The script sets a true colour. DXF's LEADER group 77 holds an ACI
|
||||
// index only, so it cannot carry one, and the DWG writer does not
|
||||
// store `override_color` at all (BLOCKER, cadcodec): it reopens as
|
||||
// ByLayer in both formats.
|
||||
assert_eq!(leader.override_color, acadrust::types::Color::ByLayer,
|
||||
"acadrust now persists Leader.override_color; drop the blocker");
|
||||
if is_dxf {
|
||||
// BLOCKER: the DXF writer emits group 213 but the reader's
|
||||
// coordinate mapping does not reassemble it.
|
||||
assert_eq!(leader.annotation_offset, acadrust::types::Vector3::ZERO,
|
||||
"acadrust now reads DXF 213; drop the blocker");
|
||||
} else {
|
||||
assert_eq!(leader.annotation_offset, acadrust::types::Vector3::new(1.0, 2.0, 0.0));
|
||||
}
|
||||
"acadrust now persists a true-colour Leader.override_color; drop the blocker");
|
||||
assert_eq!(leader.annotation_offset, acadrust::types::Vector3::new(1.0, 2.0, 0.0));
|
||||
assert_eq!(leader.dimension_style, "Standard");
|
||||
if is_dxf {
|
||||
assert!((leader.text_height - 4.0).abs() < 1e-12);
|
||||
@@ -5081,16 +5069,15 @@ mod tests {
|
||||
if value.columns.first().map(|c| c.width) != Some(30.0) { gaps.push(format!("{format}: column width {:?}", value.columns.first().map(|c| c.width))); }
|
||||
let text = value.rows.get(1).and_then(|r| r.cells.get(1)).and_then(|c| c.contents.first()).map(|c| c.value.text.clone());
|
||||
if text.as_deref() != Some("Hello") { gaps.push(format!("{format}: cell text {text:?}")); }
|
||||
// DWG stores the ranges; DXF stores the origin cell's merge
|
||||
// dimensions and its reader does not rebuild `merged_ranges`
|
||||
// (engine gap, recorded in the ledger). Scripted saves populate
|
||||
// both, so each format keeps the merge in its own form.
|
||||
// DWG stores the ranges and DXF the origin cell's merge dimensions;
|
||||
// the DXF reader rebuilds the ranges from those, so both formats
|
||||
// come back with the merge in both forms.
|
||||
let origin = &value.rows[0].cells[0];
|
||||
if format == "DXF" {
|
||||
if (origin.merge_width, origin.merge_height) != (3, 1) { gaps.push(format!("DXF: merge dims {}x{}", origin.merge_width, origin.merge_height)); }
|
||||
if !value.merged_ranges.is_empty() { gaps.push("DXF: reader now rebuilds merged_ranges; drop the blocker".into()); }
|
||||
} else if value.merged_ranges.len() != 1 {
|
||||
gaps.push(format!("DWG: merged_ranges {}", value.merged_ranges.len()));
|
||||
if format == "DXF" && (origin.merge_width, origin.merge_height) != (3, 1) {
|
||||
gaps.push(format!("DXF: merge dims {}x{}", origin.merge_width, origin.merge_height));
|
||||
}
|
||||
if value.merged_ranges.len() != 1 {
|
||||
gaps.push(format!("{format}: merged_ranges {}", value.merged_ranges.len()));
|
||||
}
|
||||
if value.insertion_point != edited.insertion_point { gaps.push(format!("{format}: insertion {:?}", value.insertion_point)); }
|
||||
}
|
||||
@@ -5535,12 +5522,10 @@ mod tests {
|
||||
expect_created: "r5 t3 h2 endz6.0 ccwtrue cptrue",
|
||||
expect_edited: "r5 t5 h2 endz10.0 ccwfalse cptrue",
|
||||
expect_reedited: "r5 t6 h2 endz10.0 ccwfalse cptrue",
|
||||
// BLOCKER: the DXF reader parses boolean group 290 as an i16 and
|
||||
// never applies it, so handedness always reopens counter-clockwise.
|
||||
expect_edited_dwg: "",
|
||||
expect_reedited_dwg: "",
|
||||
expect_edited_dxf: "r5 t5 h2 endz10.0 ccwtrue cptrue",
|
||||
expect_reedited_dxf: "r5 t6 h2 endz10.0 ccwtrue cptrue",
|
||||
expect_edited_dxf: "",
|
||||
expect_reedited_dxf: "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5674,12 +5659,10 @@ mod tests {
|
||||
expect_created: "Pdf at5.0,5.0 s2.0 r0.0 c100 f0 clip0",
|
||||
expect_edited: "Pdf at8.0,9.0 s3.0 r0.5 c70 f20 clip2",
|
||||
expect_reedited: "Pdf at8.0,9.0 s4.0 r0.5 c70 f20 clip2",
|
||||
// BLOCKER: DXF stores the rotation in degrees and the reader returns
|
||||
// it unconverted, so 0.5 rad reopens as 28.6 and a second save-reopen compounds it to 1641.4.
|
||||
expect_edited_dwg: "",
|
||||
expect_reedited_dwg: "",
|
||||
expect_edited_dxf: "Pdf at8.0,9.0 s3.0 r28.6 c70 f20 clip2",
|
||||
expect_reedited_dxf: "Pdf at8.0,9.0 s4.0 r1641.4 c70 f20 clip2",
|
||||
expect_edited_dxf: "",
|
||||
expect_reedited_dxf: "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5757,12 +5740,10 @@ mod tests {
|
||||
expect_created: "max40.0,30.0 c20.0,15.0 s1.0 r0.0",
|
||||
expect_edited: "max60.0,45.0 c30.0,22.5 s2.0 r0.3",
|
||||
expect_reedited: "max60.0,45.0 c30.0,22.5 s3.0 r0.3",
|
||||
// BLOCKER: a DXF DRAWINGVIEW reopens as a different (opaque) entity
|
||||
// kind, not as a ViewBorder; only DWG restores the typed record.
|
||||
expect_edited_dwg: "",
|
||||
expect_reedited_dwg: "",
|
||||
expect_edited_dxf: "wrong kind",
|
||||
expect_reedited_dxf: "wrong kind",
|
||||
expect_edited_dxf: "",
|
||||
expect_reedited_dxf: "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5801,12 +5782,10 @@ mod tests {
|
||||
expect_created: "Key t3 i1.5 pos10,10,10 tgt0,0 shfalse Rgb { r: 255, g: 240, b: 200 }",
|
||||
expect_edited: "Key t3 i3.0 pos12,8,10 tgt5,5 shtrue Rgb { r: 255, g: 240, b: 200 }",
|
||||
expect_reedited: "Key t3 i4.0 pos12,8,10 tgt5,5 shtrue Rgb { r: 255, g: 240, b: 200 }",
|
||||
// BLOCKER: DXF does not restore `cast_shadows` (it reopens false),
|
||||
// like the other boolean groups the reader parses as integers.
|
||||
expect_edited_dwg: "",
|
||||
expect_reedited_dwg: "",
|
||||
expect_edited_dxf: "Key t3 i3.0 pos12,8,10 tgt5,5 shfalse Rgb { r: 255, g: 240, b: 200 }",
|
||||
expect_reedited_dxf: "Key t3 i4.0 pos12,8,10 tgt5,5 shfalse Rgb { r: 255, g: 240, b: 200 }",
|
||||
expect_edited_dxf: "",
|
||||
expect_reedited_dxf: "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6318,13 +6297,12 @@ mod tests {
|
||||
expect_created: "PART_NO|Part number|PN-001 ins1.0,2.0 al3.0,4.0 h2.5 r0.25 wf1.25 ob0.10 Center/Top f1010 fl12 tg2 lock1",
|
||||
expect_edited: "PART_NO|Serial|PN-002 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock1",
|
||||
expect_reedited: "PART_NO|Serial|PN-003 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock1",
|
||||
// BLOCKER: acadrust's DXF ATTDEF reader handles only groups 1, 2, 3,
|
||||
// 10, 40, 50, 280 and 101, so the alignment point and alignments,
|
||||
// width factor, oblique angle, attribute flags, field length,
|
||||
// generation flags, lock and text style (groups 7, 11, 41, 51, 70-74,
|
||||
// 210, 280 lock) reopen as defaults. The writer emits all of them.
|
||||
expect_edited_dxf: "PART_NO|Serial|PN-002 ins4.0,5.0 al0.0,0.0 h3.0 r0.50 wf1.00 ob0.00 Left/Baseline f0000 fl0 tg0 lock0",
|
||||
expect_reedited_dxf: "PART_NO|Serial|PN-003 ins4.0,5.0 al0.0,0.0 h3.0 r0.50 wf1.00 ob0.00 Left/Baseline f0000 fl0 tg0 lock0",
|
||||
// BLOCKER (cadcodec): the DXF ATTDEF writer emits no group 280 at
|
||||
// all — neither the version byte nor the lock-position flag the
|
||||
// reader looks for after it — so `lock_position` reopens false. Every
|
||||
// other field round-trips since cadcodec dd1d7bf.
|
||||
expect_edited_dxf: "PART_NO|Serial|PN-002 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock0",
|
||||
expect_reedited_dxf: "PART_NO|Serial|PN-003 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock0",
|
||||
expect_edited_dwg: "",
|
||||
expect_reedited_dwg: "",
|
||||
});
|
||||
@@ -7232,11 +7210,8 @@ mod tests {
|
||||
expect_created: "n2 ends 0.0,0.0->40.0,0.0 labelA tick2.0/2.0 scale1.0 counts2/2",
|
||||
expect_edited: "n3 ends 0.0,0.0->40.0,0.0 labelB tick3.0/-3.0 scale2.0 counts3/3",
|
||||
expect_reedited: "n3 ends 0.0,0.0->40.0,0.0 labelB tick3.0/-3.0 scale3.0 counts3/3",
|
||||
// BLOCKER (cadcodec, see docs/cadcodec-reader-gaps.md issue 5): the
|
||||
// DXF reader dispatches SECTIONLINE only inside blocks, so a symbol
|
||||
// in the entity list reopens as an unknown entity.
|
||||
expect_edited_dxf: "wrong kind Unknown",
|
||||
expect_reedited_dxf: "wrong kind Unknown",
|
||||
expect_edited_dxf: "",
|
||||
expect_reedited_dxf: "",
|
||||
expect_edited_dwg: "",
|
||||
expect_reedited_dwg: "",
|
||||
});
|
||||
@@ -7688,25 +7663,14 @@ check('d_modmissing', lambda: D.modify('Nope', dimscale=2))
|
||||
assert!((title.oblique_angle - 15f64.to_radians()).abs() < 1e-6, "{label}: oblique {}", title.oblique_angle);
|
||||
assert_eq!(title.font_file.to_lowercase(), "romans", "{label}");
|
||||
assert_eq!(title.big_font_file.to_lowercase(), "bigfont", "{label}");
|
||||
if label == "DXF" {
|
||||
// BLOCKER (cadcodec): the DXF STYLE writer hard-codes group 71 to 0, so the
|
||||
// backward and upside-down generation flags are lost on a DXF save.
|
||||
assert!(!title.flags.backward && !title.flags.upside_down, "{label}: cadcodec now keeps the flags; flip this canary");
|
||||
} else {
|
||||
assert!(title.flags.backward && title.flags.upside_down, "{label}");
|
||||
}
|
||||
assert!(title.flags.backward && title.flags.upside_down, "{label}");
|
||||
let remarks = document.text_styles.get("Remarks").unwrap_or_else(|| panic!("{label}: Remarks missing"));
|
||||
assert_eq!(remarks.font_file.to_lowercase(), "arial.ttf", "{label}: {remarks:?}");
|
||||
let metric = document.dim_styles.get("Metric").unwrap_or_else(|| panic!("{label}: Metric missing"));
|
||||
assert_eq!((metric.dimscale, metric.dimtxt, metric.dimasz), (3.0, 3.5, 2.5), "{label}");
|
||||
assert_eq!(metric.dimtxsty, "Heading", "{label}: dimtxsty followed the rename");
|
||||
if label == "DXF" {
|
||||
// BLOCKER (cadcodec): the DXF DIMSTYLE reader keeps only the text-style handle
|
||||
// (group 340) and never resolves `dimtxsty` from it, so the name reopens as
|
||||
// "Standard". The handle survives and identifies the right style.
|
||||
assert_eq!(metric.dimtxsty, "Standard", "{label}: cadcodec now resolves the name; flip this canary");
|
||||
assert_eq!(metric.dimtxsty_handle, title.handle, "{label}: text style link by handle");
|
||||
} else {
|
||||
assert_eq!(metric.dimtxsty, "Heading", "{label}: dimtxsty followed the rename");
|
||||
}
|
||||
let copy = document.dim_styles.get("Metric2").unwrap_or_else(|| panic!("{label}: Metric2 missing"));
|
||||
assert_eq!((copy.dimscale, copy.dimtxt), (2.0, 4.0), "{label}: copied before Metric changed");
|
||||
@@ -7845,13 +7809,7 @@ check('modify_missing', lambda: B.modify('Nope', description='x'))
|
||||
let record = document.block_records.get("Gadget").unwrap();
|
||||
assert_eq!((record.base_point.x, record.base_point.y), (1.0, 1.0), "{label}: base point");
|
||||
assert!(!record.explodable, "{label}: explodable");
|
||||
if label == "DXF" {
|
||||
// BLOCKER (cadcodec): the DXF BLOCK_RECORD reader/writer does not carry the
|
||||
// description, so it is lost on a DXF save (DWG keeps it).
|
||||
assert_eq!(record.description, "", "{label}: cadcodec now keeps the description; flip this canary");
|
||||
} else {
|
||||
assert_eq!(record.description, "a gadget", "{label}: description");
|
||||
}
|
||||
assert_eq!(record.description, "a gadget", "{label}: description");
|
||||
assert_eq!(members("Widget2").len(), 1, "{label}: Widget2 members");
|
||||
let inserts: Vec<_> = document.entities().filter_map(|e| match e { EntityType::Insert(i) => Some(i.block_name.clone()), _ => None }).collect();
|
||||
assert_eq!(inserts.iter().filter(|n| n.as_str() == "Gadget").count(), 2, "{label}: inserts follow the rename: {inserts:?}");
|
||||
|
||||
Reference in new issue
Block a user