forked from 1b-agent/opencadstudio-upstream
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:
1 parent
2e4d77dc09
commit
9075e53511
10 files changed
+219
-14
No files matched your search
@@ -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(),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -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. |
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user