Complete Ole2Frame: embed a picture file from a script

EmbedPicture builds the frame with OCS's own EmbeddedImage and
build_embedded_ole, so the opaque storage is never scripted. PNG, JPEG and
BMP are stored as read and other formats are re-encoded as PNG. The lifecycle
test now creates through Python; a new test covers formats, aspect, origin,
layer and refusals.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-20 18:12:31 +02:00
1 parent 942222c381
commit 5e6a830782
8 files changed
+148 -7

No files matched your search

+5
View File
@@ -109,6 +109,11 @@ pub enum SolidOperation {
/// layer. The kernel refuses some cases (coincident faces, cuts it has no
/// closed form for); a refusal changes nothing. Curved operands can take a
/// second or two, and the host thread is busy meanwhile.
/// Embed a picture file as an OLE frame whose bottom-left corner is at
/// `origin`, `width` drawing units wide, keeping the picture's aspect.
/// PNG, JPEG and BMP are stored as read; other formats are re-encoded as
/// PNG. The frame goes on `layer` (layer `0` when `None`).
EmbedPicture { path: String, origin: [f64; 3], width: f64, layer: Option<String> },
Boolean {
first: Handle,
second: Handle,
@@ -115,6 +115,7 @@ mod tests {
},
SolidOperation::Transform { handle: acadrust::Handle::new(7), matrix: [1.0; 16] },
SolidOperation::RegionFromProfile { source: acadrust::Handle::new(3), layer: None, delete_source: true },
SolidOperation::EmbedPicture { path: "p.png".into(), origin: [0.0; 3], width: 5.0, layer: None },
SolidOperation::Boolean {
first: acadrust::Handle::new(1),
second: acadrust::Handle::new(2),
+4 -3
View File
@@ -7,9 +7,9 @@ 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 **43 of 43** canvas kinds: 40
are creatable (the legacy `Polyline`, `Ole2Frame` and `Body` are update-only). This is **not** 41 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, Face3D, Solid3D, Region and Surface 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.
v7. The Python feature has explicit writes for **43 of 43** canvas kinds: 41
are creatable (the legacy `Polyline` and `Body` are update-only). This is **not** 41 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, Face3D, Solid3D, Region, Surface and Ole2Frame 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
@@ -183,3 +183,4 @@ exact queue above and update statuses with evidence after each increment.
- Timed booleans in a release build (details in [cadkernel-body-path.md](cadkernel-body-path.md)). A curved box-and-cylinder boolean takes about 1 to 2.2 s, overlapping spheres 0.2 s and planar operands 1 to 2 ms, against 23 s in a debug build. The kernel refuses many curved cases quickly with a reason (`Coincident`, `CutRefused`, `NoClosedForm`), including a second cut in an already drilled box. Recommendation: add a synchronous `SolidOperation::Boolean` that surfaces the refusal and leaves the operands untouched, test it with planar operands plus one ignored curved case, and note the 30 s per-script budget. No code changed in this step.
- Implemented `SolidOperation::Boolean` and `doc.solids.union`, `subtract` and `intersect`, as recommended by the timing study. It runs synchronously, surfaces the kernel's refusal (`Coincident`, `CutRefused`, `NoClosedForm`) with a plain-language reason and the words "nothing was changed", consumes the operands unless `keep_operands`, and records one undo step. The lifecycle test uses planar boxes (millisecond cost, exact volumes, debug-safe) and an `#[ignore]` test documents a curved refusal. With this the kernel operations in the cadkernel note are all built. SectionSymbol is the only unmapped kind.
- Mapped `SectionSymbol`, the last unmapped kind, so all 43 canvas kinds now have explicit writes. The blocker recorded earlier was overstated: cadcodec documents `points` as canonical, says the counts equal the point count in every verified export, and provides `sync_display_fields()`, which OCS's own edits already call; OCS also renders a symbol with no style or view link. The host derives the counts and projections when `points` changes and validates that references are existing `SectionViewStyle` and `ViewRep` class objects. Every gate passes except DXF, where cadcodec reopens a top-level `SECTIONLINE` as an unknown entity (issue 5 in the cadcodec write-up, pinned by a canary). Still unverifiable here: the meaning of `raw_flags_90` and AutoCAD's acceptance of a symbol with no parent view.
- Completed `Ole2Frame` by adding creation. `SolidOperation::EmbedPicture` and `doc.embed_picture` build the frame from a picture file with OCS's own embedding code, so a script never touches the opaque storage; PNG, JPEG and BMP are stored byte-for-byte and other formats are re-encoded as PNG. The lifecycle test now creates through Python and the new creation-rules test covers formats, aspect, origin, layer and seven refusals. 30 kinds are complete.
+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: **43 of 43 canvas kinds have explicit geometry writes** (40 creatable, plus update-only legacy `Polyline`, `Ole2Frame` and `Body`), **29 kinds are complete**, and none remains layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
Use with [the completion plan](plugin-host-model-completion-plan.md). This ledger is current as of 20 September 2026: **43 of 43 canvas kinds have explicit geometry writes** (41 creatable, plus update-only legacy `Polyline` and `Body`), **30 kinds are complete**, and none remains layer-only. `Mapped` means at least one non-layer field is exposed for Python writes; it does **not** mean the kind passes the completion gate. `Update-only` means Python creation is unavailable. `Layer-only` means raw inspection and canvas layer edits exist, but geometry is not mapped. Record actual evidence or a blocking issue before changing any status to `Complete`.
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 | **Integration-tested (rigid move only)** | Mapped read-only like Solid3D (same payload container). `audit_python_body_transform_over_real_ipc` moves a Body carrying a kernel box through `doc.solids.translate`, checks the exact extent, DWG reopen with a losslessly lifting payload and the exact volume, and the explanatory refusal of `create_entity('Body')`. Scripts cannot create a Body, and a non-solid (sheet or wire) body that does not lift is refused untouched. |
| Solid3D | **Complete (create + rigid move)** | New in this session: an additive `HostApi::solid_operation` (`SolidOperation::Create` and `Transform`, plus a `SolidPrimitive` enum) carried by `PluginRequest::SolidOperation` / `PluginResponse::SolidResult` through both IPC clients, the server and `HostSession`, and a Python `doc.solids` API: `box`, `wedge`, `cylinder`, `sphere`, `torus`, `pyramid`, `transform`, `translate`, `rotate`, `mirror`. The host builds bodies with cadkernel, and every result goes through `solid_to_sat`, which lifts its own output back and refuses anything lossy. A transform first lifts the existing payload with `kernel_acis_body` and is refused, leaving the payload untouched, when that is lossy. Only rigid moves and mirrors are accepted (scaling and shear are refused). Regenerated `wires` follow the body, `silhouettes` are cleared and `history_handle` is dropped. Python sees `uid`, `point_of_reference` and `history_handle` read-only; `acis_data`, `wires` and `silhouettes` are excluded and preserved; creation through `create_entity` is refused with a message pointing at `doc.solids`. `audit_python_solid3d_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P through the real runner with the kernel as oracle: all six primitives match their analytic volumes (box, wedge and cube exactly, curved solids within 2 percent of the tessellated volume), a translate, a rotate and a mirror keep the volume and move the extent exactly and keep 12 edge wires, 13 refusals (zero and non-finite sizes, a torus with minor above major, too few pyramid sides, an empty layer, an unknown primitive, a wrong number count, a scaling matrix, a non-finite offset, a payload the kernel cannot lift, a missing entity) change nothing and leave no undo step, DWG and DXF reopen with the payload lifting losslessly and the same volume and extent, a reopened payload moves again, and undo restores the original solid exactly. Not covered: solids that carry parametric history, and AutoCAD acceptance of the written payload, which cannot be tested here. **Booleans** (`SolidOperation::Boolean`, `doc.solids.union`, `subtract` and `intersect`) are covered by `audit_python_boolean_lifecycle_over_real_ipc`: two overlapping boxes give exact volumes (union 360, subtract and intersect 120) and extents, the operands are consumed unless `keep_operands` (undo restores both exactly), a result takes the first operand's layer or an explicit one, a second planar operation chains on the result (a through-hole gives 104), eight refusals change nothing and record no undo step (same operand twice, a non-solid, a missing entity, an unliftable payload, an empty layer, an unknown operation, a disjoint intersection), and DWG and DXF reopen losslessly with a reopened result moving again. The kernel refuses many curved cases (measured in release: about 1 to 2 s when it succeeds, and `Coincident`, `CutRefused` or `NoClosedForm` when it does not, including a second cut in a drilled box); its reason reaches the script and nothing changes, shown by an `#[ignore]` curved test that takes about 27 s in a debug build. Booleans block the host thread and count against the script's 30 s budget; only Solid3D operands are accepted. |
| Surface | **Complete (plane surface, extruded open profile, isolines, rigid move)** | New `SolidOperation::SurfaceFromProfile` and `SolidOperation::Extrude`, exposed as `doc.solids.surface(profile)` and `doc.solids.extrude(profile, direction)`. A closed planar profile becomes a `Plane` surface (built as the region body, so it keeps the plane kind); a profile extruded along a nonzero direction becomes a Solid3D when it is closed and a generic Surface when it is open, using OCS's own `extrusion_body`. Every payload is verified lossless by `solid_to_sat`, and rigid moves (now accepted for surfaces too) keep a plane a plane and turn any other kind generic, as OCS does after an edit. Python sees `surface_kind`, `modeler_format_version`, `point_of_reference` and `history_handle` read-only and `u_isolines` and `v_isolines` writable (0 to 200); the payload, wires, silhouettes and the kind-specific `surface_data` are excluded and preserved. The kind field is exposed as `surface_kind` because a bare `kind` key names the entity type in every Python dict; the new `exposed_property_names_are_unique_per_kind` test guards against that class of collision. `audit_python_surface_lifecycle_over_real_ipc` passes C/R/E/D/U/I/W/V/P through the real runner: two plane surfaces (one on its own layer), a surface from an open polyline extrusion, three extruded solids checked against their analytic volumes (a box exactly, curved ones within 2 percent), `delete_source` erasing only the intended profile, canvas geometry, nine refusals that change nothing and record no undo step, an isoline transaction with four rejections (500, negative, the read-only kind, an excluded key), moves with exact extents, DWG and DXF reopen with lifting payloads, matching extents, kept isolines and the plane kind, a reopened surface moving again, and exact undo. Not covered: lofted, revolved, swept and NURBS surfaces (a script cannot create them; an existing one is preserved and turns generic on a move), parametric history, and AutoCAD acceptance. |
| 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. |
| Ole2Frame | **Complete** | 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. **Creation (new 2026-09-20):** `SolidOperation::EmbedPicture` and `doc.embed_picture(path, origin=(0, 0, 0), width=1, layer=None)` build a frame from a picture file with OCS's own `EmbeddedImage::from_file` and `build_embedded_ole`: PNG, JPEG and BMP are stored as read, any other format (a TIFF in the test) is re-encoded as PNG, `origin` is the bottom-left corner, `width` the frame width, and the height follows the picture's aspect. The generic `create_entity('Ole2Frame')` is refused with a message pointing at `doc.embed_picture`. Host validation requires finite corners spanning a nonzero size and `lock_aspect` of 0 or 1 for edits, and for creation a readable picture, a finite origin, a positive width and a non-empty layer. `audit_python_ole2frame_lifecycle_over_real_ipc` (shared driver) passes C/R/E/D/U/I/W/V/P through the real runner: creation from a PNG, a corner and aspect-lock transaction, canvas geometry, five rejection cases (including 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 three-step undo/redo. `audit_python_ole2frame_creation_rules_over_real_ipc` checks that PNG and JPEG are stored byte-for-byte, a TIFF becomes a PNG, the frame keeps the picture's 2:1 aspect with the requested origin and layer, and seven refusals (missing file, a non-image, empty layer, zero width, non-finite origin, a short origin list, and generic creation) change nothing and record no undo step. Not covered: OLE objects that are not pictures, and AutoCAD acceptance of the written frame, which cannot be tested here. |
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
+1 -1
View File
@@ -789,7 +789,7 @@
},
"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",
"update_only_message": "create an Ole2Frame with doc.embed_picture(path, origin, width); existing frames can be moved, resized and aspect-locked, and the stored object is preserved untouched",
"fields": {
"storage": {
"exclude": true,
@@ -220,6 +220,13 @@ class _Document:
handle = entity.handle if isinstance(entity, _Entity) else int(entity)
ocs.remove_entity(handle)
def embed_picture(self, path, origin=(0, 0, 0), width=1, layer=None):
"""Embed a picture file (PNG, JPEG, BMP or another format re-encoded as
PNG) as an OLE frame. `origin` is the bottom-left corner and `width`
the frame width; the height follows the picture's aspect ratio."""
handle = ocs.embed_picture(str(path), [float(v) for v in origin], float(width), layer)
return self.entities[handle]
def coverage(self, kind=None):
if kind is not None:
return ocs.entity_coverage(kind)
+22
View File
@@ -475,6 +475,28 @@ mod ocs {
.map_err(|error| vm.new_runtime_error(format!("ocs.solid_region: {error}")))
}
/// Embed a picture file as an OLE frame.
#[pyfunction]
fn embed_picture(
path: String,
origin: Vec<f64>,
width: f64,
layer: Option<String>,
vm: &VirtualMachine,
) -> PyResult<u64> {
use ocs_plugin_api::host::SolidOperation;
let origin: [f64; 3] = origin.try_into().map_err(|_| {
vm.new_value_error("ocs.embed_picture: the origin needs 3 numbers".to_owned())
})?;
let result = host_ctx::with_host(|host| {
host.solid_operation(SolidOperation::EmbedPicture { path, origin, width, layer })
})
.ok_or_else(|| vm.new_runtime_error("ocs: not running inside a PY_ command".to_owned()))?;
result
.map(|handle| handle.value())
.map_err(|error| vm.new_runtime_error(format!("ocs.embed_picture: {error}")))
}
/// Combine two solids ("union", "subtract" or "intersect").
#[pyfunction]
fn solid_boolean(
+106 -1
View File
@@ -705,6 +705,30 @@ impl<'a> HostSession<'a> {
};
self.commit_profile_result(result, source, delete_source, "Extrude profile")
}
SolidOperation::EmbedPicture { path, origin, width, layer } => {
finite3("picture origin", origin)?;
positive("picture width", width)?;
if layer.as_ref().is_some_and(|name| name.trim().is_empty()) {
return Err("layer name is empty".into());
}
let picture = crate::io::ole_embed::EmbeddedImage::from_file(std::path::Path::new(&path))
.map_err(|error| format!("cannot read the picture {path:?}: {error}"))?;
let (upper_left, lower_right) = crate::io::ole_embed::corners_from_placement(
acadrust::types::Vector3::new(origin[0], origin[1], origin[2]),
width,
picture.aspect(),
);
let mut frame = crate::io::ole_embed::build_embedded_ole(&picture, upper_left, lower_right);
if let Some(layer) = layer {
frame.common.layer = layer;
}
self.push_undo("Embed picture");
let handle = self.add_entity(EntityType::Ole2Frame(frame));
if handle.is_null() {
return Err("the picture frame could not be added".into());
}
Ok(handle)
}
SolidOperation::Boolean { first, second, operation, layer, keep_operands } => {
use ocs_plugin_api::host::SolidBoolean;
if first == second {
@@ -4779,8 +4803,11 @@ mod tests {
#[test]
fn audit_python_ole2frame_lifecycle_over_real_ipc() {
image::RgbaImage::from_pixel(4, 3, image::Rgba([10, 120, 200, 255]))
.save(std::env::temp_dir().join("ocs_ole_audit.png"))
.unwrap();
run_mesh_lifecycle(&MeshCase {
create: "# MAKEOLE\n",
create: "doc.embed_picture('TMPDIR/ocs_ole_audit.png', origin=(10, 10, 0), width=20)\n",
edit: concat!(
"e = doc.entities[HANDLE]\n",
"with doc.transaction('Edit'):\n",
@@ -5672,6 +5699,84 @@ mod tests {
}
}
#[test]
fn audit_python_ole2frame_creation_rules_over_real_ipc() {
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {
return;
};
let dir = std::env::temp_dir();
let png = dir.join("ocs_ole_rules.png");
let jpeg = dir.join("ocs_ole_rules.jpg");
let tiff = dir.join("ocs_ole_rules.tif");
let text = dir.join("ocs_ole_rules.txt");
let picture = image::RgbImage::from_pixel(8, 4, image::Rgb([200, 40, 40]));
picture.save(&png).unwrap();
picture.save(&jpeg).unwrap();
picture.save(&tiff).unwrap();
std::fs::write(&text, "not a picture").unwrap();
let mut app = OpenCADStudio::new_for_test();
app.tabs[0].is_start = false;
let mut host = HostSession::new(&mut app, 0);
let process = ocs_plugin_api::process::PluginProcess::spawn(
std::path::Path::new(&plugin_path), &mut host, crate::plugin::v4_support::notification_handler(),
).unwrap();
let dispatch = |host: &mut HostSession<'_>, command: &str| {
assert!(process.dispatch(host, command, &mut |_| {}).expect("Python dispatch"));
};
let last = |host: &HostSession<'_>| host.app.command_line.history.last().unwrap().text.clone();
let frames = |host: &HostSession<'_>| host.document().entities().filter(|e| matches!(e, EntityType::Ole2Frame(_))).count();
let picture_len = |host: &HostSession<'_>, index: usize| {
let mut all: Vec<_> = host.document().entities().filter_map(|e| match e {
EntityType::Ole2Frame(f) => Some(f.clone()), _ => None }).collect();
all.sort_by_key(|f| f.common.handle.value());
match acadrust::entities::extract_presentation(&all[index].encoded_payload()) {
Some(acadrust::entities::OlePresentation::Raster(bytes)) => bytes,
other => panic!("no embedded raster: {other:?}"),
}
};
let embed = |file: &std::path::Path, extra: &str| format!(
"PY_EVAL ocs.active_document.embed_picture({:?}, origin=(1, 2, 3), width=16{extra}).handle", file.display().to_string());
// C: PNG and JPEG are stored as read, a TIFF is re-encoded as PNG, and
// the frame keeps the picture's 2:1 aspect and the requested layer.
dispatch(&mut host, &embed(&png, ", layer='PICTURES'"));
dispatch(&mut host, &embed(&jpeg, ""));
dispatch(&mut host, &embed(&tiff, ""));
assert_eq!(frames(&host), 3, "{}", last(&host));
assert_eq!(picture_len(&host, 0), std::fs::read(&png).unwrap(), "PNG is stored as read");
assert_eq!(picture_len(&host, 1), std::fs::read(&jpeg).unwrap(), "JPEG is stored as read");
assert_eq!(&picture_len(&host, 2)[..4], b"\x89PNG", "TIFF is re-encoded as PNG");
let mut all: Vec<_> = host.document().entities().filter_map(|e| match e {
EntityType::Ole2Frame(f) => Some(f.clone()), _ => None }).collect();
all.sort_by_key(|f| f.common.handle.value());
assert_eq!((all[0].common.layer.as_str(), all[1].common.layer.as_str()), ("PICTURES", "0"));
let frame = &all[0];
assert!((frame.lower_right_corner.x - frame.upper_left_corner.x - 16.0).abs() < 1e-9, "width 16");
assert!((frame.upper_left_corner.y - frame.lower_right_corner.y - 8.0).abs() < 1e-9, "height 8 keeps the 2:1 aspect");
assert!((frame.lower_right_corner.y - 2.0).abs() < 1e-9 && (frame.upper_left_corner.x - 1.0).abs() < 1e-9, "origin is the bottom-left corner");
// V: refusals change nothing and record no undo step.
let undo = host.app.tabs[0].history.undo_stack.len();
let missing = dir.join("ocs_ole_rules_missing.png");
for (command, message) in [
(embed(&missing, ""), "cannot read the picture"),
(embed(&text, ""), "cannot read the picture"),
(embed(&png, ", layer=''").replace("width=16", "width=16"), "layer name is empty"),
(embed(&png, "").replace("width=16", "width=0"), "must be finite and greater than zero"),
(embed(&png, "").replace("origin=(1, 2, 3)", "origin=(float('nan'), 0, 0)"), "must be finite"),
("ocs.embed_picture('x.png', [1.0], 5.0, None)".to_string(), "origin needs 3 numbers"),
("ocs.active_document.create_entity('Ole2Frame', upper_left_corner=(0, 0, 0), lower_right_corner=(1, 1, 0))".to_string(), "doc.embed_picture"),
] {
dispatch(&mut host, &format!("PY_EVAL {}", command.trim_start_matches("PY_EVAL ")));
assert!(last(&host).contains(message), "{command}: {}", last(&host));
}
assert_eq!(frames(&host), 3);
assert_eq!(host.app.tabs[0].history.undo_stack.len(), undo, "refusals record no undo step");
for file in [&png, &jpeg, &tiff, &text] {
let _ = std::fs::remove_file(file);
}
}
#[test]
fn staged_python_point_pick_and_cancel_over_real_ipc() {
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {