Map Viewport in the Python host model

Curated view fields are writable; the owner must be a paper-space block and
frozen layers must exist. staged_python_viewport_lifecycle_over_real_ipc
passes every gate except a paper-space canvas oracle, so the kind is
recorded as integration-tested.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 15:09:23 +02:00
1 parent 2e4d77dc09
commit 9075e53511
10 files changed
+219 -14

No files matched your search

+1
View File
@@ -794,6 +794,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
| ("RasterImage", _) | ("RasterImage", _)
| ("Wipeout", _) | ("Wipeout", _)
| ("Underlay", _) | ("Underlay", _)
| ("Viewport", _)
| ( | (
"MLine", "MLine",
"flags" "flags"
@@ -432,6 +432,26 @@
"fade", "fade",
"clip_boundary_vertices", "clip_boundary_vertices",
"clip_inverted" "clip_inverted"
],
"Viewport": [
"layer",
"center",
"width",
"height",
"status",
"view_center",
"view_direction",
"view_target",
"lens_length",
"front_clip_z",
"back_clip_z",
"view_height",
"twist_angle",
"custom_scale",
"frozen_layers",
"render_mode",
"circle_sides",
"ucs_icon_visible"
] ]
}, },
"readable": { "readable": {
@@ -503,6 +523,36 @@
"class_version", "class_version",
"definition_handle", "definition_handle",
"definition_reactor_handle" "definition_reactor_handle"
],
"Viewport": [
"id",
"snap_base",
"snap_spacing",
"grid_spacing",
"snap_angle",
"clip_boundary_handle",
"style_sheet",
"ucs_at_origin",
"ucs_per_viewport",
"ucs_origin",
"ucs_x_axis",
"ucs_y_axis",
"ucs_handle",
"base_ucs_handle",
"ucs_ortho_type",
"elevation",
"shade_plot_mode",
"grid_flags",
"grid_major",
"background_handle",
"shade_plot_handle",
"visual_style_handle",
"sun_handle",
"default_lighting",
"default_lighting_type",
"brightness",
"contrast",
"ambient_color"
] ]
}, },
"aliases": { "aliases": {
@@ -919,6 +919,36 @@ fn validate_underlay(
Ok(()) Ok(())
} }
/// Viewport view state. Owner and layer-handle references are checked with the
/// other references; this validates the numbers.
#[cfg(feature = "host")]
fn validate_viewport(
old: Option<&crate::host::acadrust::entities::Viewport>,
new: &crate::host::acadrust::entities::Viewport,
) -> Result<(), String> {
if old == Some(new) {
return Ok(());
}
finite_vector("Viewport.center", &new.center)?;
finite_vector("Viewport.view_center", &new.view_center)?;
finite_vector("Viewport.view_target", &new.view_target)?;
solid_normal("Viewport.view_direction", &new.view_direction)?;
for (name, value) in [("width", new.width), ("height", new.height), ("view_height", new.view_height), ("lens_length", new.lens_length), ("custom_scale", new.custom_scale)] {
if !value.is_finite() || value <= 0.0 {
return Err(format!("Viewport.{name} must be finite and greater than zero"));
}
}
for (name, value) in [("front_clip_z", new.front_clip_z), ("back_clip_z", new.back_clip_z), ("twist_angle", new.twist_angle)] {
if !value.is_finite() {
return Err(format!("Viewport.{name} must be finite"));
}
}
if new.circle_sides < 3 {
return Err("Viewport.circle_sides must be at least 3".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")]
@@ -1022,6 +1052,7 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
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::Underlay(value) => validate_underlay(None, value)?,
EntityType::Viewport(value) => validate_viewport(None, value)?,
EntityType::AttributeDefinition(value) => { EntityType::AttributeDefinition(value) => {
finite_vector( finite_vector(
"AttributeDefinition.insertion_point", "AttributeDefinition.insertion_point",
@@ -1386,6 +1417,7 @@ pub fn validate_entity_mutation(
(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::Underlay(old), EntityType::Underlay(new)) => validate_underlay(Some(old), new)?,
(EntityType::Viewport(old), EntityType::Viewport(new)) => validate_viewport(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)) => {
@@ -1649,6 +1681,22 @@ 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::Viewport(value) = entity {
let owner = value.common.owner_handle;
let in_paper_space = !owner.is_null()
&& document
.block_records
.iter()
.any(|record| record.handle == owner && record.is_paper_space());
if !in_paper_space {
return Err("Viewport must be owned by a paper-space layout block".into());
}
for handle in &value.frozen_layers {
if !document.layers.iter().any(|layer| layer.handle == *handle) {
return Err(format!("Viewport frozen layer {handle:?} does not exist"));
}
}
}
if let EntityType::Underlay(value) = entity { if let EntityType::Underlay(value) = entity {
match document.objects.get(&value.definition_handle) { match document.objects.get(&value.definition_handle) {
Some(crate::host::acadrust::objects::ObjectType::UnderlayDefinition(definition)) => { Some(crate::host::acadrust::objects::ObjectType::UnderlayDefinition(definition)) => {
@@ -2095,6 +2143,7 @@ mod tests {
| "RasterImage" | "RasterImage"
| "Wipeout" | "Wipeout"
| "Underlay" | "Underlay"
| "Viewport"
) )
{ {
assert!( assert!(
@@ -2393,6 +2442,23 @@ mod tests {
"Underlay.fade".to_owned(), "Underlay.fade".to_owned(),
"Underlay.clip_boundary_vertices".to_owned(), "Underlay.clip_boundary_vertices".to_owned(),
"Underlay.clip_inverted".to_owned(), "Underlay.clip_inverted".to_owned(),
"Viewport.center".to_owned(),
"Viewport.width".to_owned(),
"Viewport.height".to_owned(),
"Viewport.status".to_owned(),
"Viewport.view_center".to_owned(),
"Viewport.view_direction".to_owned(),
"Viewport.view_target".to_owned(),
"Viewport.lens_length".to_owned(),
"Viewport.front_clip_z".to_owned(),
"Viewport.back_clip_z".to_owned(),
"Viewport.view_height".to_owned(),
"Viewport.twist_angle".to_owned(),
"Viewport.custom_scale".to_owned(),
"Viewport.frozen_layers".to_owned(),
"Viewport.render_mode".to_owned(),
"Viewport.circle_sides".to_owned(),
"Viewport.ucs_icon_visible".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 **34 of 43** canvas kinds: 33 v7. The Python feature has explicit writes for **35 of 43** canvas kinds: 34
are creatable and `Insert` remains update-only. This is **not** 34 completed are creatable and `Insert` remains update-only. This is **not** 35 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, 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` `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, and RasterImage and Viewport are held short of `Complete` by unverifiable or missing oracles (see the ledger). RasterImage is mapped and integration-tested, held short of `Complete` by unverifiable image-dictionary linkage. The other 8 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 **Viewport** (queue item 13). Leader is done except for its W Continue with **ViewBorder** (queue item 14). 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`.
@@ -167,3 +167,4 @@ exact queue above and update statuses with evidence after each increment.
- 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`. - 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`.
- Mapped `Viewport` with a paper-space owner rule and frozen-layer reference validation. Every gate except a paper-space canvas oracle passes through the shared driver; the kind stays integration-tested. The queue now continues with `ViewBorder`.
+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: **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`. Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 20 September 2026: **35 of 43 canvas kinds have explicit geometry writes** (34 creatable, plus update-only `Insert`), **12 kinds are complete**, and 8 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.
@@ -40,7 +40,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
| 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 | **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 | **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 | **Integration-tested** | Writable: `center`, `width`, `height`, `status`, `view_center`, `view_direction`, `view_target`, `lens_length`, `front_clip_z`, `back_clip_z`, `view_height`, `twist_angle`, `custom_scale`, `frozen_layers` (layer handles), `render_mode`, `circle_sides`, `ucs_icon_visible`. Read-only: `id`, snap/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 (a model-space viewport is rejected) and every frozen-layer handle must be an existing layer. 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. `staged_python_viewport_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, paper-space owner supplied by the driver) passes C/R/E/D/U/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. **Why not `Complete`:** the I gate lacks a canvas oracle because a paper-space viewport only draws on its layout tab, which the driver does not activate; viewport `id` uniqueness within a layout, clip boundary entities and view-to-scale synchronization are not handled by the host. |
| 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. |
| Light | Layer-only | Requires position/target and photometric-state fixture. | | Light | Layer-only | Requires position/target and photometric-state fixture. |
+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 34 geometry converters. Other writable properties canvas kinds outside its 35 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 34-kind generated schema it contains only handle, kind, and layer; outside the 35-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 34 mapped kinds. the 35 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
+37 -1
View File
@@ -33,7 +33,8 @@
"Helix", "Helix",
"RasterImage", "RasterImage",
"Wipeout", "Wipeout",
"Underlay" "Underlay",
"Viewport"
], ],
"manual_kinds": ["Dimension"], "manual_kinds": ["Dimension"],
"base_fields": ["handle", "layer", "owner_handle"], "base_fields": ["handle", "layer", "owner_handle"],
@@ -367,6 +368,41 @@
"definition_handle": { "required": true }, "definition_handle": { "required": true },
"insertion_point": { "required": true } "insertion_point": { "required": true }
} }
},
"Viewport": {
"fields": {
"id": { "read_only": true },
"snap_base": { "read_only": true },
"snap_spacing": { "read_only": true },
"grid_spacing": { "read_only": true },
"snap_angle": { "read_only": true },
"clip_boundary_handle": { "read_only": true },
"style_sheet": { "read_only": true },
"ucs_at_origin": { "read_only": true },
"ucs_per_viewport": { "read_only": true },
"ucs_origin": { "read_only": true },
"ucs_x_axis": { "read_only": true },
"ucs_y_axis": { "read_only": true },
"ucs_handle": { "read_only": true },
"base_ucs_handle": { "read_only": true },
"ucs_ortho_type": { "read_only": true },
"elevation": { "read_only": true },
"shade_plot_mode": { "read_only": true },
"grid_flags": { "read_only": true },
"grid_major": { "read_only": true },
"background_handle": { "read_only": true },
"shade_plot_handle": { "read_only": true },
"visual_style_handle": { "read_only": true },
"sun_handle": { "read_only": true },
"default_lighting": { "read_only": true },
"default_lighting_type": { "read_only": true },
"brightness": { "read_only": true },
"contrast": { "read_only": true },
"ambient_color": { "read_only": true },
"center": { "required": true },
"width": { "required": true },
"height": { "required": true }
}
} }
} }
} }
+1 -1
View File
@@ -4756,7 +4756,7 @@ mod tests {
); );
} }
} }
assert_eq!(emitted.len(), 34); assert_eq!(emitted.len(), 35);
} }
#[cfg(feature = "experimental-host-model")] #[cfg(feature = "experimental-host-model")]
+54 -3
View File
@@ -3195,6 +3195,11 @@ mod tests {
let tmp = std::env::temp_dir().to_string_lossy().into_owned(); let tmp = std::env::temp_dir().to_string_lossy().into_owned();
let mut create_script = case.create.replace("TMPDIR", &tmp); let mut create_script = case.create.replace("TMPDIR", &tmp);
let paper = host.document().block_records.iter().find(|r| r.is_paper_space()).map(|r| r.handle.value());
let layer0 = host.document().layers.iter().find(|l| l.name == "0").map(|l| l.handle.value());
create_script = create_script
.replace("PAPERSPACE", &paper.unwrap_or_default().to_string())
.replace("LAYER0", &layer0.unwrap_or_default().to_string());
if create_script.contains("DEFHANDLE") { if create_script.contains("DEFHANDLE") {
// Scripts reference an underlay definition that the drawing owns. // Scripts reference an underlay definition that the drawing owns.
let handle = host.document_mut().allocate_handle(); let handle = host.document_mut().allocate_handle();
@@ -3209,14 +3214,21 @@ mod tests {
.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)));
let created = host.document().get_entity(handle).unwrap().clone(); let created = host.document().get_entity(handle).unwrap().clone();
assert_eq!((case.digest)(&created), case.expect_created); assert_eq!((case.digest)(&created), case.expect_created);
assert!(!host.app.tabs[0].scene.wire_models_for(&[handle]).is_empty(), "no canvas geometry"); // Viewports draw only on their paper-space layout tab, which this
// driver does not activate, so they have no model-tab wire oracle.
let has_wires = !matches!(created, EntityType::Viewport(_));
if has_wires {
assert!(!host.app.tabs[0].scene.wire_models_for(&[handle]).is_empty(), "no canvas geometry");
}
run_script(&mut host, "edit", &case.edit.replace("HANDLE", &handle.value().to_string()).replace("TMPDIR", &tmp)); run_script(&mut host, "edit", &case.edit.replace("HANDLE", &handle.value().to_string()).replace("TMPDIR", &tmp).replace("LAYER0", &layer0.unwrap_or_default().to_string()));
let expected = host.document().get_entity(handle).unwrap().clone(); let expected = host.document().get_entity(handle).unwrap().clone();
assert_eq!((case.digest)(&expected), case.expect_edited, "edit failed: {}", last_output(&host)); assert_eq!((case.digest)(&expected), case.expect_edited, "edit failed: {}", last_output(&host));
assert_eq!(expected.common(), created.common()); assert_eq!(expected.common(), created.common());
assert_eq!(host.selection(), vec![handle]); assert_eq!(host.selection(), vec![handle]);
assert!(!host.app.tabs[0].scene.wire_models_for(&[handle]).is_empty()); if has_wires {
assert!(!host.app.tabs[0].scene.wire_models_for(&[handle]).is_empty());
}
for (patch, message) in case.rejects { for (patch, message) in case.rejects {
dispatch(&mut host, &format!( dispatch(&mut host, &format!(
@@ -3586,6 +3598,45 @@ mod tests {
}); });
} }
#[test]
fn staged_python_viewport_lifecycle_over_real_ipc() {
run_mesh_lifecycle(&MeshCase {
create: concat!(
"doc.create_entity('Viewport', owner_handle=PAPERSPACE, center=P(100.0, 100.0, 0.0),\n",
" width=80.0, height=60.0, view_center=P(0.0, 0.0, 0.0), view_height=50.0)\n"),
edit: concat!(
"vp = doc.entities[HANDLE]\n",
"with doc.transaction('Edit viewport'):\n",
" vp.center = (120.0, 110.0, 0.0)\n",
" vp.width = 90.0\n",
" vp.view_height = 75.0\n",
" vp.twist_angle = 0.25\n",
" vp.frozen_layers = [LAYER0]\n",
"doc.selection = [vp]\n"),
rejects: &[
("'width':0.0", "greater than zero"),
("'view_height':float('nan')", "greater than zero"),
("'view_direction':{'x':0.0,'y':0.0,'z':0.0}", "nonzero"),
("'frozen_layers':[999999]", "does not exist"),
("'circle_sides':2", "at least 3"),
("'id':7", "read-only"),
("'clip_boundary_handle':5", "read-only"),
],
is_kind: |entity| matches!(entity, EntityType::Viewport(_)),
digest: |entity| match entity {
EntityType::Viewport(v) => format!("at{:.1},{:.1} {:.1}x{:.1} vh{:.1} tw{:.2} frozen{}", v.center.x, v.center.y,
v.width, v.height, v.view_height, v.twist_angle, v.frozen_layers.len()),
_ => "wrong kind".into(),
},
reedit: |entity| if let EntityType::Viewport(v) = entity { v.width = 95.0; },
expect_created: "at100.0,100.0 80.0x60.0 vh50.0 tw0.00 frozen0",
expect_edited: "at120.0,110.0 90.0x60.0 vh75.0 tw0.25 frozen1",
expect_reedited: "at120.0,110.0 95.0x60.0 vh75.0 tw0.25 frozen1",
expect_edited_dxf: "",
expect_reedited_dxf: "",
});
}
#[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 {