Map Ole2Frame as update-only with the embedded object preserved

Frame corners and the aspect lock are scriptable; the opaque storage and
envelope are excluded and refused on edit. The audit embeds a real PNG
through OCS's own builder and proves it survives DWG and DXF byte-for-byte.
Python creation stays a follow-up.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 16:43:25 +02:00
1 parent f1813b3ebe
commit e583da83db
10 files changed
+149 -11

No files matched your search

+1
View File
@@ -793,6 +793,7 @@ fn generate_entity_coverage(out_dir: &Path, registry: &TypeRegistry) {
| ("Helix", _)
| ("RasterImage", _)
| ("Wipeout", _)
| ("Ole2Frame", _)
| ("Underlay", _)
| ("Viewport", _)
| ("ViewBorder", _)
@@ -486,6 +486,13 @@
"shadow_map_softness",
"photometric_mode",
"photometric_data"
],
"Ole2Frame": [
"layer",
"upper_left_corner",
"lower_right_corner",
"lock_aspect",
"is_paper_space"
]
},
"readable": {
@@ -590,6 +597,12 @@
],
"Light": [
"class_version"
],
"Ole2Frame": [
"version",
"source_application",
"ole_object_type",
"dwg_mode"
]
},
"aliases": {
@@ -1223,6 +1223,28 @@ fn validate_basic_entity(
}
}
/// Ole2Frame: only the frame placement is scriptable; the embedded object
/// storage is never rewritten.
#[cfg(feature = "host")]
fn validate_ole_frame(
old: Option<&crate::host::acadrust::entities::Ole2Frame>,
new: &crate::host::acadrust::entities::Ole2Frame,
) -> Result<(), String> {
if old == Some(new) {
return Ok(());
}
finite_vector("Ole2Frame.upper_left_corner", &new.upper_left_corner)?;
finite_vector("Ole2Frame.lower_right_corner", &new.lower_right_corner)?;
let (a, b) = (new.upper_left_corner, new.lower_right_corner);
if (a.x - b.x).abs() <= 1.0e-12 || (a.y - b.y).abs() <= 1.0e-12 {
return Err("Ole2Frame corners must span a nonzero width and height".into());
}
if new.lock_aspect > 1 {
return Err("Ole2Frame.lock_aspect must be 0 or 1".into());
}
Ok(())
}
/// 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.
#[cfg(feature = "host")]
@@ -1348,6 +1370,7 @@ pub fn validate_new_canvas_entity(entity: &crate::host::EntityType) -> Result<()
return Err("Insert array counts must be greater than zero".into());
}
}
EntityType::Ole2Frame(value) => validate_ole_frame(None, value)?,
EntityType::AttributeDefinition(value) => {
finite_vector(
"AttributeDefinition.insertion_point",
@@ -1716,6 +1739,12 @@ pub fn validate_entity_mutation(
(EntityType::Viewport(old), EntityType::Viewport(new)) => validate_viewport(Some(old), new)?,
(EntityType::ViewBorder(old), EntityType::ViewBorder(new)) => validate_view_border(Some(old), new)?,
(EntityType::Light(old), EntityType::Light(new)) => validate_light(Some(old), new)?,
(EntityType::Ole2Frame(old), EntityType::Ole2Frame(new)) => {
validate_ole_frame(Some(old), new)?;
if old.storage != new.storage || old.envelope != new.envelope {
return Err("Ole2Frame embedded storage cannot change".into());
}
}
(EntityType::MLine(old), EntityType::MLine(new)) => validate_mline(Some(old), new)?,
(EntityType::Leader(old), EntityType::Leader(new)) => validate_leader(Some(old), new)?,
(EntityType::AttributeDefinition(old), EntityType::AttributeDefinition(new)) => {
@@ -2502,6 +2531,7 @@ mod tests {
| "Viewport"
| "ViewBorder"
| "Light"
| "Ole2Frame"
)
{
assert!(
@@ -2787,6 +2817,10 @@ mod tests {
"Wipeout.clip_mode".to_owned(),
"Wipeout.clip_type".to_owned(),
"Wipeout.clip_boundary_vertices".to_owned(),
"Ole2Frame.upper_left_corner".to_owned(),
"Ole2Frame.lower_right_corner".to_owned(),
"Ole2Frame.lock_aspect".to_owned(),
"Ole2Frame.is_paper_space".to_owned(),
"Underlay.underlay_type".to_owned(),
"Underlay.definition_handle".to_owned(),
"Underlay.insertion_point".to_owned(),
+4 -3
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
`a8dd3640` plus the Leader increment, published as draft PR
[#1391](https://github.com/HakanSeven12/OpenCADStudio/pull/1391). Host API is
v7. The Python feature has explicit writes for **37 of 43** canvas kinds: 36
are creatable (the legacy `Polyline` is update-only). This is **not** 37 completed
v7. The Python feature has explicit writes for **38 of 43** canvas kinds: 36
are creatable (the legacy `Polyline` and `Ole2Frame` are update-only). This is **not** 38 completed
kinds: Tolerance, Shape, AttributeEntity, Hatch, MLine, Dimension, MultiLeader, Table, PolygonMesh, PolyfaceMesh, Mesh, Wipeout, Point, Line, Circle, Arc, Ellipse, LwPolyline, Spline, Text, MText, Ray, XLine, Solid and Face3D pass every completion gate; Insert now passes every gate too; the audited Polyline (update-only by decision), Polyline2D and Polyline3D pass every gate that their format allows.
AttributeDefinition passes its host and real IPC lifecycle but remains short of
`Complete` because the previously pinned CAD codec dropped optional ATTDEF
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, Underlay, ViewBorder and Light 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 6 canvas kinds (SectionSymbol, Region, Body, Solid3D, Surface, Ole2Frame) have only `layer` and are recorded as blocked
`acadrust` revision `7ea4247`. Leader is mapped and integration-tested but blocked at W by three acadrust persistence gaps (see the ledger). Leader, Helix, Underlay, ViewBorder and Light 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 5 canvas kinds (SectionSymbol, Region, Body, Solid3D, Surface) have only `layer` and are recorded as blocked
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.
@@ -175,3 +175,4 @@ exact queue above and update statuses with evidence after each increment.
- Audited `AttributeDefinition` against acadrust `5b682ed` with a full-field comparison through both formats. Everything passes in DWG. DXF keeps only tag, prompt, value, insertion point, height and rotation because the reader handles eight group codes; alignment, width factor, oblique angle, attribute flags, field length, generation flags, lock and text style reopen as defaults. The kind stays integration-tested and the gap is pinned by canary expectations; the fix belongs in cadcodec's `read_attdef`. The shared driver's block fixture now also serves `MAKEBLOCK` scripts.
- Decided the two open audit questions. **Insert is creatable** by naming an existing block (existence, ordinary-block and no-cycle checks; attribute records stay with the AttributeEntity flow), which completes the kind. **Legacy Polyline is update-only** with a hint pointing scripts at Polyline2D and Polyline3D, because both formats convert it on save. The generator gained an `update_only_message` for such kinds, integer coordinates are accepted in Insert scales, and the driver gained `MAKEPOLYLINE` and `MAKEBLOCK` fixtures.
- Wrote up the cadcodec DXF reader gaps as draft issues in [cadcodec-reader-gaps.md](cadcodec-reader-gaps.md) (nothing filed): ATTDEF, boolean groups read through `as_i16()`, UNDERLAY rotation units, LEADER, DRAWINGVIEW/SECTIONLINE dispatch and TABLE merged ranges. Verifying them corrected an earlier attribution: the Spline "DXF drops weights" finding was an OCS bug (the writer emits weights only for a rational spline), now fixed by setting `flags.rational` from the weights, so Spline is complete.
- Mapped `Ole2Frame` as update-only (queue item 21, previously blocked). OCS already builds real embedded frames (`build_embedded_ole`), which supplied the fixture the blocker was waiting for. Frame corners and the aspect lock are scriptable; the opaque storage and envelope are excluded and refused on edit. The audit proves the embedded picture survives DWG and DXF unchanged. Creation from Python remains a follow-up. The remaining blocked kinds are SectionSymbol, Region, Body, Solid3D and Surface.
+2 -2
View File
@@ -1,6 +1,6 @@
# 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: **37 of 43 canvas kinds have explicit geometry writes** (36 creatable, plus update-only legacy `Polyline`), **26 kinds are complete**, and 6 remain layer-only (one of them, SectionSymbol, blocked). `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: **38 of 43 canvas kinds have explicit geometry writes** (36 creatable, plus update-only legacy `Polyline` and `Ole2Frame`), **26 kinds are complete**, and 5 remain layer-only and blocked (SectionSymbol, Region, Body, Solid3D, Surface). `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.
@@ -48,7 +48,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
| Body | **Blocked** | Not mapped. The entity is an opaque ACIS payload (`acis_data` SAT/SAB text or binary) plus derived `wires`, `silhouettes` and, for solids, a history record. A script can neither produce a valid body nor edit one, because every geometric change needs the CAD kernel to rewrite the payload, and the handover forbids deserializing and rewriting opaque ACIS data without a proven lossless path. The only lossless edit today is the layer change the document model already supports. Adding this kind needs a host-side, kernel-backed API (create a primitive, or apply a transform, then regenerate `acis_data` with its wires and silhouettes) with a DWG and DXF round-trip that preserves the payload and history byte-for-byte for untouched bodies. |
| Solid3D | **Blocked** | Not mapped. The entity is an opaque ACIS payload (`acis_data` SAT/SAB text or binary) plus derived `wires`, `silhouettes` and, for solids, a history record. A script can neither produce a valid body nor edit one, because every geometric change needs the CAD kernel to rewrite the payload, and the handover forbids deserializing and rewriting opaque ACIS data without a proven lossless path. The only lossless edit today is the layer change the document model already supports. Adding this kind needs a host-side, kernel-backed API (create a primitive, or apply a transform, then regenerate `acis_data` with its wires and silhouettes) with a DWG and DXF round-trip that preserves the payload and history byte-for-byte for untouched bodies. |
| Surface | **Blocked** | Not mapped. The entity is an opaque ACIS payload (`acis_data` SAT/SAB text or binary) plus derived `wires`, `silhouettes` and, for solids, a history record. A script can neither produce a valid body nor edit one, because every geometric change needs the CAD kernel to rewrite the payload, and the handover forbids deserializing and rewriting opaque ACIS data without a proven lossless path. The only lossless edit today is the layer change the document model already supports. Adding this kind needs a host-side, kernel-backed API (create a primitive, or apply a transform, then regenerate `acis_data` with its wires and silhouettes) with a DWG and DXF round-trip that preserves the payload and history byte-for-byte for untouched bodies. |
| Ole2Frame | **Blocked** | Not mapped. A valid OLE frame carries a `StructuredStoragePayload` (a compound-file image of the embedded object) that a script cannot supply, so creation is impossible, and frame-geometry edits (`upper_left_corner`, `lower_right_corner`, `lock_aspect`) would be an update-only path that has no real embedded-object fixture to prove storage and aspect persistence through DWG and DXF. Adding it needs a fixture builder for a minimal valid OLE storage plus a proven round-trip of the payload; until then the kind keeps raw snapshot access and layer edits. |
| Ole2Frame | **Update-only (all applicable gates pass)** | Writable: `upper_left_corner`, `lower_right_corner`, `lock_aspect`, `is_paper_space`. Read-only: `version`, `source_application`, `ole_object_type`, `dwg_mode`. The embedded object `storage` and the frame `envelope` are opaque, excluded from the Python form, refused by the mutation check (`Ole2Frame embedded storage cannot change`) and preserved untouched. Host validation requires finite corners spanning a nonzero width and height and `lock_aspect` of 0 or 1. Creation from a script is refused with an explanatory message, because a valid frame needs a real embedded object; the host's own `build_embedded_ole` builds one from an image. `audit_python_ole2frame_lifecycle_over_real_ipc` (shared driver; the fixture embeds a real 4x3 PNG through `add_embedded_image`) passes E/D/U/I/W/V/P: a corner and aspect-lock transaction, canvas geometry, rejection of a zero-size frame, a bad aspect lock, non-finite corners, a read-only `version` and an attempt to write `storage`, an atomic rollback, **DWG and DXF reopen with the 119-byte embedded picture byte-for-byte intact (extracted through `extract_presentation`)**, re-edit, and two-step undo/redo. Not `Complete` because Python cannot create a frame. A create path (`file_path` of an image, built with `build_embedded_ole`) would need a manual-kind converter like Dimension and is the natural follow-up. |
After each increment, update the row with test names, fixture paths, exact
passing commands, the OCS commit hash, and any manual GUI result. Host and
+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
the same undoable transaction; internal and opaque variants remain read-only
through the document model. The adapter accepts only `layer` patches for
canvas kinds outside its 37 geometry converters. Other writable properties
canvas kinds outside its 38 geometry converters. Other writable properties
still report `type_conversion_only`; broader CAD range and cross-property
checks remain outstanding.
The host's `entity_snapshot` helper serializes any typed entity as a detached
@@ -92,7 +92,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.
Python `doc.entities[handle]` returns a descriptor for every entity. For kinds
outside the 37-kind generated schema it contains only handle, kind, and layer;
outside the 38-kind generated schema it contains only handle, kind, and layer;
`layer` is editable on canvas kinds. `doc.coverage()` enumerates the
catalog; `doc.coverage("Line")` and `line.coverage` return one entry. For
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
`doc.coverage()` or `doc.coverage("Line")`; each row includes source field,
type, access, and current validation status. Geometry edits remain limited to
the 37 mapped kinds.
the 38 mapped kinds.
For any entity, `entity.snapshot` returns a detached dictionary of its raw
CAD fields, including `snapshot["common"]` and kind-specific nested values.
It can inspect an unmapped kind; editing the dictionary does
+28 -1
View File
@@ -36,7 +36,8 @@
"Underlay",
"Viewport",
"ViewBorder",
"Light"
"Light",
"Ole2Frame"
],
"manual_kinds": [
"Dimension"
@@ -780,6 +781,32 @@
"required": true
}
}
},
"Ole2Frame": {
"update_only": true,
"update_only_message": "Ole2Frame cannot be created from a script: its embedded object storage is opaque; existing frames can be moved, resized and aspect-locked, and the stored object is preserved untouched",
"fields": {
"storage": {
"exclude": true,
"_comment": "Opaque compound-file payload; snapshot-readable and preserved untouched."
},
"envelope": {
"exclude": true,
"_comment": "Opaque frame envelope; preserved untouched."
},
"version": {
"read_only": true
},
"source_application": {
"read_only": true
},
"ole_object_type": {
"read_only": true
},
"dwg_mode": {
"read_only": true
}
}
}
}
}
+1 -1
View File
@@ -4780,7 +4780,7 @@ mod tests {
);
}
}
assert_eq!(emitted.len(), 37);
assert_eq!(emitted.len(), 38);
}
#[cfg(feature = "experimental-host-model")]
+63 -1
View File
@@ -3212,7 +3212,28 @@ mod tests {
.map(|(x, y, z)| acadrust::types::Vector3::new(x, y, z));
host.add_entity(EntityType::Polyline(acadrust::entities::Polyline::from_points(points.to_vec())));
}
let update_only = create_script.contains("MAKEINSERT") || make_polyline;
let make_ole = create_script.contains("MAKEOLE");
if make_ole {
// An Ole2Frame is update-only: the host embeds a real picture.
let mut png = std::io::Cursor::new(Vec::new());
image::RgbaImage::from_pixel(4, 3, image::Rgba([10, 120, 200, 255]))
.write_to(&mut png, image::ImageFormat::Png)
.unwrap();
let picture = crate::io::ole_embed::EmbeddedImage {
bytes: png.into_inner(),
pixel_width: 4,
pixel_height: 3,
name: "audit.png".into(),
};
crate::io::ole_embed::add_embedded_image(
host.document_mut(),
&picture,
acadrust::types::Vector3::new(10.0, 10.0, 0.0),
20.0,
)
.unwrap();
}
let update_only = create_script.contains("MAKEINSERT") || make_polyline || make_ole;
if update_only || create_script.contains("MAKEBLOCK") {
let next = host.document().next_handle();
let (record_handle, block_handle, end_handle) = (Handle::new(next), Handle::new(next + 1), Handle::new(next + 2));
@@ -4387,6 +4408,47 @@ mod tests {
assert!(last(&host).contains("Polyline2D or Polyline3D"), "{}", last(&host));
}
#[test]
fn audit_python_ole2frame_lifecycle_over_real_ipc() {
run_mesh_lifecycle(&MeshCase {
create: "# MAKEOLE\n",
edit: concat!(
"e = doc.entities[HANDLE]\n",
"with doc.transaction('Edit'):\n",
" e.upper_left_corner = (12.0, 40.0, 0.0)\n",
" e.lower_right_corner = (42.0, 10.0, 0.0)\n",
" e.lock_aspect = 1\n",
"doc.selection = [e]\n"),
rejects: &[
("'upper_left_corner':{'x':42.0,'y':40.0,'z':0.0}", "nonzero width and height"),
("'lock_aspect':2", "0 or 1"),
("'lower_right_corner':{'x':float('nan'),'y':0.0,'z':0.0}", "finite"),
("'version':3", "read-only"),
("'storage':{}", "outside the editable schema"),
],
is_kind: |entity| matches!(entity, EntityType::Ole2Frame(_)),
digest: |entity| match entity {
EntityType::Ole2Frame(v) => {
let picture = match acadrust::entities::extract_presentation(&v.encoded_payload()) {
Some(acadrust::entities::OlePresentation::Raster(bytes)) => bytes.len(),
_ => 0,
};
format!("ul{:.1},{:.1} lr{:.1},{:.1} lock{} picture{}", v.upper_left_corner.x, v.upper_left_corner.y,
v.lower_right_corner.x, v.lower_right_corner.y, v.lock_aspect, picture)
}
_ => "wrong kind".into(),
},
reedit: |entity| if let EntityType::Ole2Frame(v) = entity { v.lower_right_corner.x = 50.0; },
expect_created: "ul10.0,25.0 lr30.0,10.0 lock0 picture119",
expect_edited: "ul12.0,40.0 lr42.0,10.0 lock1 picture119",
expect_reedited: "ul12.0,40.0 lr50.0,10.0 lock1 picture119",
expect_edited_dxf: "",
expect_reedited_dxf: "",
expect_edited_dwg: "",
expect_reedited_dwg: "",
});
}
#[test]
fn staged_python_point_pick_and_cancel_over_real_ipc() {
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {