Map Underlay in the Python host model; record DXF rotation blocker

Scripts place an underlay against an existing UnderlayDefinition; the host
checks that the definition exists and matches the underlay type.
staged_python_underlay_lifecycle_over_real_ipc passes every gate except DXF
persistence: acadrust's reader returns the degree-valued rotation
unconverted and repeated saves compound it. Pinned by canary expectations.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 15:05:11 +02:00
1 parent 96e3b54f08
commit 2e4d77dc09
10 files changed
+170 -12

No files matched your search

+1
View File
@@ -793,6 +793,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
| ("Helix", _) | ("Helix", _)
| ("RasterImage", _) | ("RasterImage", _)
| ("Wipeout", _) | ("Wipeout", _)
| ("Underlay", _)
| ( | (
"MLine", "MLine",
"flags" "flags"
@@ -416,6 +416,22 @@
"clip_mode", "clip_mode",
"clip_type", "clip_type",
"clip_boundary_vertices" "clip_boundary_vertices"
],
"Underlay": [
"layer",
"underlay_type",
"definition_handle",
"insertion_point",
"x_scale",
"y_scale",
"z_scale",
"rotation",
"normal",
"flags",
"contrast",
"fade",
"clip_boundary_vertices",
"clip_inverted"
] ]
}, },
"readable": { "readable": {
@@ -874,6 +874,51 @@ fn validate_wipeout(
Ok(()) Ok(())
} }
/// Underlay placement and display. The file and page live on the referenced
/// `UnderlayDefinition`, which a script names by handle at creation.
#[cfg(feature = "host")]
fn validate_underlay(
old: Option<&crate::host::acadrust::entities::Underlay>,
new: &crate::host::acadrust::entities::Underlay,
) -> Result<(), String> {
if old == Some(new) {
return Ok(());
}
if let Some(old) = old {
if old.definition_handle != new.definition_handle {
return Err("Underlay.definition_handle is fixed at creation".into());
}
if old.underlay_type != new.underlay_type {
return Err("Underlay.underlay_type is fixed at creation".into());
}
}
finite_vector("Underlay.insertion_point", &new.insertion_point)?;
solid_normal("Underlay.normal", &new.normal)?;
for (name, value) in [("x_scale", new.x_scale), ("y_scale", new.y_scale), ("z_scale", new.z_scale)] {
if !value.is_finite() || value == 0.0 {
return Err(format!("Underlay.{name} must be finite and nonzero"));
}
}
if !new.rotation.is_finite() {
return Err("Underlay.rotation must be finite".into());
}
if new.contrast > 100 || new.fade > 100 {
return Err("Underlay contrast and fade must be within 0..=100".into());
}
if new.flags.bits() & !0x0f != 0 {
return Err("Underlay.flags has unknown bits".into());
}
for (index, vertex) in new.clip_boundary_vertices.iter().enumerate() {
if !vertex.x.is_finite() || !vertex.y.is_finite() {
return Err(format!("Underlay.clip_boundary_vertices[{index}] must be finite"));
}
}
if !new.clip_boundary_vertices.is_empty() && new.clip_boundary_vertices.len() < 2 {
return Err("Underlay clip boundary needs at least 2 vertices".into());
}
Ok(())
}
/// Validate newly created geometry for the kinds whose mapped fields have /// Validate newly created geometry for the kinds whose mapped fields have
/// host checks. Called by the Python add path before an entity enters the document. /// host checks. Called by the Python add path before an entity enters the document.
#[cfg(feature = "host")] #[cfg(feature = "host")]
@@ -976,6 +1021,7 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
EntityType::Helix(value) => validate_helix(None, value)?, EntityType::Helix(value) => validate_helix(None, value)?,
EntityType::RasterImage(value) => validate_raster_image(None, value)?, EntityType::RasterImage(value) => validate_raster_image(None, value)?,
EntityType::Wipeout(value) => validate_wipeout(None, value)?, EntityType::Wipeout(value) => validate_wipeout(None, value)?,
EntityType::Underlay(value) => validate_underlay(None, value)?,
EntityType::AttributeDefinition(value) => { EntityType::AttributeDefinition(value) => {
finite_vector( finite_vector(
"AttributeDefinition.insertion_point", "AttributeDefinition.insertion_point",
@@ -1339,6 +1385,7 @@ pub fn validate_entity_mutation(
(EntityType::Helix(old), EntityType::Helix(new)) => validate_helix(Some(old), new)?, (EntityType::Helix(old), EntityType::Helix(new)) => validate_helix(Some(old), new)?,
(EntityType::RasterImage(old), EntityType::RasterImage(new)) => validate_raster_image(Some(old), new)?, (EntityType::RasterImage(old), EntityType::RasterImage(new)) => validate_raster_image(Some(old), new)?,
(EntityType::Wipeout(old), EntityType::Wipeout(new)) => validate_wipeout(Some(old), new)?, (EntityType::Wipeout(old), EntityType::Wipeout(new)) => validate_wipeout(Some(old), new)?,
(EntityType::Underlay(old), EntityType::Underlay(new)) => validate_underlay(Some(old), new)?,
(EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?, (EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?,
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?, (EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
(EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => { (EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => {
@@ -1602,6 +1649,24 @@ pub fn validate_canvas_entity_references(
return Err(format!("Shape text style {:?} has no SHX file", style.name)); return Err(format!("Shape text style {:?} has no SHX file", style.name));
} }
} }
if let EntityType::Underlay(value) = entity {
match document.objects.get(&value.definition_handle) {
Some(crate::host::acadrust::objects::ObjectType::UnderlayDefinition(definition)) => {
if definition.underlay_type != value.underlay_type {
return Err(format!(
"Underlay type {:?} does not match its definition ({:?})",
value.underlay_type, definition.underlay_type
));
}
}
_ => {
return Err(format!(
"Underlay definition {:?} does not exist",
value.definition_handle
));
}
}
}
if let EntityType::RasterImage(value) = entity { if let EntityType::RasterImage(value) = entity {
if let Some(handle) = value.definition_handle.filter(|handle| !handle.is_null()) { if let Some(handle) = value.definition_handle.filter(|handle| !handle.is_null()) {
if !matches!( if !matches!(
@@ -2029,6 +2094,7 @@ mod tests {
| "Helix" | "Helix"
| "RasterImage" | "RasterImage"
| "Wipeout" | "Wipeout"
| "Underlay"
) )
{ {
assert!( assert!(
@@ -2314,6 +2380,19 @@ mod tests {
"Wipeout.clip_mode".to_owned(), "Wipeout.clip_mode".to_owned(),
"Wipeout.clip_type".to_owned(), "Wipeout.clip_type".to_owned(),
"Wipeout.clip_boundary_vertices".to_owned(), "Wipeout.clip_boundary_vertices".to_owned(),
"Underlay.underlay_type".to_owned(),
"Underlay.definition_handle".to_owned(),
"Underlay.insertion_point".to_owned(),
"Underlay.x_scale".to_owned(),
"Underlay.y_scale".to_owned(),
"Underlay.z_scale".to_owned(),
"Underlay.rotation".to_owned(),
"Underlay.normal".to_owned(),
"Underlay.flags".to_owned(),
"Underlay.contrast".to_owned(),
"Underlay.fade".to_owned(),
"Underlay.clip_boundary_vertices".to_owned(),
"Underlay.clip_inverted".to_owned(),
]) ])
); );
} }
+5 -4
View File
@@ -7,13 +7,13 @@ Build a general Python document model over the OCS host API for all **43 canvas
As of 20 September 2026, the working branch is `plugin/host-model-api` at As of 20 September 2026, the working branch is `plugin/host-model-api` at
`a8dd3640` plus the Leader increment, published as draft PR `a8dd3640` plus the Leader increment, published as draft PR
[#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is [#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is
v7. The Python feature has explicit writes for **33 of 43** canvas kinds: 32 v7. The Python feature has explicit writes for **34 of 43** canvas kinds: 33
are creatable and `Insert` remains update-only. This is **not** 33 completed are creatable and `Insert` remains update-only. This is **not** 34 completed
kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine, Dimension, MultiLeader, Table, PolygonMesh, PolyfaceMesh, Mesh and Wipeout pass every completion gate. kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine, Dimension, MultiLeader, Table, PolygonMesh, PolyfaceMesh, Mesh and Wipeout pass every completion gate.
AttributeDefinition passes its host and real IPC lifecycle but remains short of AttributeDefinition passes its host and real IPC lifecycle but remains short of
`Complete` because the previously pinned CAD codec dropped optional ATTDEF `Complete` because the previously pinned CAD codec dropped optional ATTDEF
fields during DXF reads; revalidate that blocker against the current fields during DXF reads; revalidate that blocker against the current
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader and Helix are mapped and integration-tested but blocked at W by acadrust persistence gaps (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. The other 10 canvas kinds have only `layer` `acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader, Helix and Underlay are mapped and integration-tested but blocked at W by acadrust persistence gaps (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. The other 9 canvas kinds have only `layer`
writes through the document model. All 43 have raw typed snapshots. There are writes through the document model. All 43 have raw typed snapshots. There are
three internal records and two opaque fallbacks outside the 43 canvas kinds. three internal records and two opaque fallbacks outside the 43 canvas kinds.
@@ -123,7 +123,7 @@ The last five may require new engine capabilities rather than only Python conver
## Handoff start point ## Handoff start point
Continue with **Underlay** (queue item 12). Leader is done except for its W Continue with **Viewport** (queue item 13). Leader is done except for its W
gate; when the engine writes DXF group 340, reads group 213 and the DWG gate; when the engine writes DXF group 340, reads group 213 and the DWG
R2010+ text-size question is decided, flip the canary assertions in R2010+ text-size question is decided, flip the canary assertions in
`staged_python_leader_lifecycle_over_real_ipc` and mark it `Complete`. `staged_python_leader_lifecycle_over_real_ipc` and mark it `Complete`.
@@ -166,3 +166,4 @@ exact queue above and update statuses with evidence after each increment.
- Mapped `Helix` with a host-rebuilt spline. `run_mesh_lifecycle` gained per-format DXF expectations so an engine gap is pinned by a canary instead of hidden. All gates pass except W for `handedness` in DXF (reader ignores boolean group 290). The queue now continues with `RasterImage`. - Mapped `Helix` with a host-rebuilt spline. `run_mesh_lifecycle` gained per-format DXF expectations so an engine gap is pinned by a canary instead of hidden. All gates pass except W for `handedness` in DXF (reader ignores boolean group 290). The queue now continues with `RasterImage`.
- Mapped `RasterImage`. The host reads the image file, creates and links the `ImageDefinition`, and derives `size` and the default clip boundary. The shared lifecycle driver passes every gate in OCS with a real PNG; `ACAD_IMAGE_DICT`/reactor creation is not implemented, so the kind stays integration-tested. The queue now continues with `Wipeout`. - Mapped `RasterImage`. The host reads the image file, creates and links the `ImageDefinition`, and derives `size` and the default clip boundary. The shared lifecycle driver passes every gate in OCS with a real PNG; `ACAD_IMAGE_DICT`/reactor creation is not implemented, so the kind stays integration-tested. The queue now continues with `Wipeout`.
- Completed `Wipeout` (queue item 11) with rectangular and polygonal clip masks through the shared lifecycle driver; scripted creation matches OCS's native command. The queue now continues with `Underlay`. - Completed `Wipeout` (queue item 11) with rectangular and polygonal clip masks through the shared lifecycle driver; scripted creation matches OCS's native command. The queue now continues with `Underlay`.
- Mapped `Underlay` against a fixture PDF `UnderlayDefinition`. All gates pass except DXF W: the reader does not convert the degree-valued rotation back to radians, and repeated DXF round trips compound the error (pinned by a canary). The queue now continues with `Viewport`.
+2 -2
View File
@@ -1,6 +1,6 @@
# OCS canvas kind coverage ledger # OCS canvas kind coverage ledger
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 20 September 2026: **33 of 43 canvas kinds have explicit geometry writes** (32 creatable, plus update-only `Insert`), **12 kinds are complete**, and 10 remain 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 20 September 2026: **34 of 43 canvas kinds have explicit geometry writes** (33 creatable, plus update-only `Insert`), **12 kinds are complete**, and 9 remain 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`.
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. 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.
@@ -39,7 +39,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
| 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 | **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. |
| 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. **Why not `Complete`:** the host does not create an `ACAD_IMAGE_DICT` entry or `IMAGEDEF_REACTOR` for a scripted image, so other CAD applications may need to repair the linkage; that cannot be verified here. Editing an existing definition (file path, resolution) is out of scope. | | 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. **Why not `Complete`:** the host does not create an `ACAD_IMAGE_DICT` entry or `IMAGEDEF_REACTOR` for a scripted image, so other CAD applications may need to repair the linkage; that cannot be verified here. 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. | | 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 | Layer-only | Requires external definition/file fixture. | | 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. |
| Viewport | Layer-only | Requires layout and view-state fixture. | | Viewport | Layer-only | Requires layout and view-state fixture. |
| ViewBorder | Layer-only | Requires linked view records. | | ViewBorder | Layer-only | Requires linked view records. |
| SectionSymbol | Layer-only | Requires section-view style and point records. | | SectionSymbol | Layer-only | Requires section-view style and point records. |
+2 -2
View File
@@ -35,7 +35,7 @@ attached attribute and sequence records. Those fields report `transaction_geomet
catalog. All 43 canvas kinds also support nonempty `layer` changes through catalog. All 43 canvas kinds also support nonempty `layer` changes through
the same undoable transaction; internal and opaque variants remain read-only the same undoable transaction; internal and opaque variants remain read-only
through the document model. The adapter accepts only `layer` patches for through the document model. The adapter accepts only `layer` patches for
canvas kinds outside its 33 geometry converters. Other writable properties canvas kinds outside its 34 geometry converters. Other writable properties
still report `type_conversion_only`; broader CAD range and cross-property still report `type_conversion_only`; broader CAD range and cross-property
checks remain outstanding. checks remain outstanding.
The host's `entity_snapshot` helper serializes any typed entity as a detached The host's `entity_snapshot` helper serializes any typed entity as a detached
@@ -91,7 +91,7 @@ Input tokens are bound to the drawing tab that requested the pick. Polling a
token from another tab returns no result and does not consume it. token from another tab returns no result and does not consume it.
Python `doc.entities[handle]` returns a descriptor for every entity. For kinds Python `doc.entities[handle]` returns a descriptor for every entity. For kinds
outside the 33-kind generated schema it contains only handle, kind, and layer; outside the 34-kind generated schema it contains only handle, kind, and layer;
`layer` is editable on canvas kinds. `doc.coverage()` enumerates the `layer` is editable on canvas kinds. `doc.coverage()` enumerates the
catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For
covered kinds, the coverage object lists actual readable and editable covered kinds, the coverage object lists actual readable and editable
+1 -1
View File
@@ -94,7 +94,7 @@ The host-owned catalog classifies all 48 entity variants and tracks every
traced field, including fields marked `unmapped`. Query it with traced field, including fields marked `unmapped`. Query it with
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field, `doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
type, access, and current validation status. Geometry edits remain limited to type, access, and current validation status. Geometry edits remain limited to
the 33 mapped kinds. the 34 mapped kinds.
For any entity, `entity.snapshot` returns a detached dictionary of its raw For any entity, `entity.snapshot` returns a detached dictionary of its raw
CAD fields, including `snapshot["common"]` and kind-specific nested values. CAD fields, including `snapshot["common"]` and kind-specific nested values.
It can inspect an unmapped kind; editing the dictionary does It can inspect an unmapped kind; editing the dictionary does
+8 -1
View File
@@ -32,7 +32,8 @@
"Mesh", "Mesh",
"Helix", "Helix",
"RasterImage", "RasterImage",
"Wipeout" "Wipeout",
"Underlay"
], ],
"manual_kinds": ["Dimension"], "manual_kinds": ["Dimension"],
"base_fields": ["handle", "layer", "owner_handle"], "base_fields": ["handle", "layer", "owner_handle"],
@@ -360,6 +361,12 @@
"definition_handle": { "read_only": true }, "definition_handle": { "read_only": true },
"definition_reactor_handle": { "read_only": true } "definition_reactor_handle": { "read_only": true }
} }
},
"Underlay": {
"fields": {
"definition_handle": { "required": true },
"insertion_point": { "required": true }
}
} }
} }
} }
+1 -1
View File
@@ -4756,7 +4756,7 @@ mod tests {
); );
} }
} }
assert_eq!(emitted.len(), 33); assert_eq!(emitted.len(), 34);
} }
#[cfg(feature = "experimental-host-model")] #[cfg(feature = "experimental-host-model")]
+55 -1
View File
@@ -3194,7 +3194,16 @@ mod tests {
}; };
let tmp = std::env::temp_dir().to_string_lossy().into_owned(); let tmp = std::env::temp_dir().to_string_lossy().into_owned();
run_script(&mut host, "create", &case.create.replace("TMPDIR", &tmp)); let mut create_script = case.create.replace("TMPDIR", &tmp);
if create_script.contains("DEFHANDLE") {
// Scripts reference an underlay definition that the drawing owns.
let handle = host.document_mut().allocate_handle();
let mut definition = acadrust::objects::UnderlayDefinition::pdf("plan.pdf", "1");
definition.handle = handle;
host.document_mut().objects.insert(handle, acadrust::objects::ObjectType::UnderlayDefinition(definition));
create_script = create_script.replace("DEFHANDLE", &handle.value().to_string());
}
run_script(&mut host, "create", &create_script);
let handle = host.document().entities().find(|entity| (case.is_kind)(entity)) let handle = host.document().entities().find(|entity| (case.is_kind)(entity))
.map(|entity| entity.common().handle) .map(|entity| entity.common().handle)
.unwrap_or_else(|| panic!("Python did not create the mesh: {}", last_output(&host))); .unwrap_or_else(|| panic!("Python did not create the mesh: {}", last_output(&host)));
@@ -3532,6 +3541,51 @@ mod tests {
}); });
} }
#[test]
fn staged_python_underlay_lifecycle_over_real_ipc() {
run_mesh_lifecycle(&MeshCase {
create: concat!(
"doc.create_entity('Underlay', underlay_type='Pdf', definition_handle=DEFHANDLE,\n",
" insertion_point=P(5.0, 5.0, 0.0), x_scale=2.0, y_scale=2.0)\n"),
edit: concat!(
"u = doc.entities[HANDLE]\n",
"with doc.transaction('Edit underlay'):\n",
" u.insertion_point = (8.0, 9.0, 0.0)\n",
" u.x_scale = 3.0\n",
" u.y_scale = 3.0\n",
" u.rotation = 0.5\n",
" u.contrast = 70\n",
" u.fade = 20\n",
" u.clip_boundary_vertices = [{'x': 0.0, 'y': 0.0}, {'x': 4.0, 'y': 3.0}]\n",
"doc.selection = [u]\n"),
rejects: &[
("'definition_handle':999999", "does not exist"),
("'underlay_type':'Dwf'", "does not match its definition"),
("'x_scale':0.0", "nonzero"),
("'rotation':float('nan')", "finite"),
("'contrast':101", "within 0..=100"),
("'normal':{'x':0.0,'y':0.0,'z':0.0}", "nonzero"),
("'clip_boundary_vertices':[{'x':1.0,'y':1.0}]", "at least 2"),
("'flags':64", "unknown or out-of-range bits"),
],
is_kind: |entity| matches!(entity, EntityType::Underlay(_)),
digest: |entity| match entity {
EntityType::Underlay(u) => format!("{:?} at{:.1},{:.1} s{:.1} r{:.1} c{} f{} clip{}", u.underlay_type,
u.insertion_point.x, u.insertion_point.y, u.x_scale, u.rotation, u.contrast, u.fade,
u.clip_boundary_vertices.len()),
_ => "wrong kind".into(),
},
reedit: |entity| if let EntityType::Underlay(u) = entity { u.x_scale = 4.0; },
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_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",
});
}
#[test] #[test]
fn staged_python_point_pick_and_cancel_over_real_ipc() { fn staged_python_point_pick_and_cancel_over_real_ipc() {
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else { let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {