forked from 1b-agent/opencadstudio-upstream
Register scripted image definitions in ACAD_IMAGE_DICT and reuse per file
A scripted RasterImage now reuses one ImageDefinition per file and registers it in ACAD_IMAGE_DICT, which owns it, as for an AutoCAD-attached image. The reactor is deliberately not written because its owner convention cannot be verified. An ignored spike records that OCS's own IMAGE command creates bare images that lose their file path on save. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6e2e073e29
commit
7c959c8395
3 files changed
+189
-12
No files matched your search
@@ -185,3 +185,4 @@ exact queue above and update statuses with evidence after each increment.
|
||||
- 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.
|
||||
- Completed `Viewport` by closing its missing canvas oracle and its derived state. The shared driver now switches the scene to the layout that owns a viewport and requires a drawn frame. The host assigns a unique id (at least 2) and derives `custom_scale = height / view_height` on every scripted create or edit, as MVIEW does, so `custom_scale` became read-only. A control showed my earlier suspicion was wrong: the default id 0 still draws a frame in a layout with no sheet viewport, so the id matters for uniqueness, not visibility. 31 kinds are complete.
|
||||
- Improved `RasterImage` linkage. A scripted image now reuses one definition per file and registers it in `ACAD_IMAGE_DICT`, which owns it; this persists through DWG and DXF. The `IMAGEDEF_REACTOR` is deliberately not written: its owner convention is ambiguous between cadcodec's struct docs and its DXF writer and cannot be checked against AutoCAD here, so the kind stays integration-tested. An exploratory spike also showed that OCS's own IMAGE command creates bare images that lose their file path on save, an OCS bug outside this plugin work that is flagged separately.
|
||||
@@ -37,7 +37,7 @@ Completion gate codes: **C** = create through Python document model; **R** = rea
|
||||
| PolyfaceMesh | **Complete** | Writable: `elevation`, `flags`, `normal`, `start_width`, `end_width`, `smooth_surface`, `thickness`, `vertices` (location, flags, bulge, widths, tangent, id) and `faces` (four indices, flags, optional colour). `seqend_handle` is read-only and preserved. Host validation requires at least 3 finite vertices and 1 face, every face index within the vertex count (a negative index hides that edge, 0 marks an unused corner), at least 3 indices per face, and finite widths, thickness and elevation. `staged_python_polyface_mesh_lifecycle_over_real_ipc` passes the full gate: two triangles, a vertex move plus a new vertex and a hidden-edge face in one transaction, canvas wires, six rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. |
|
||||
| Mesh | **Complete** | Writable: `blend_crease`, `subdivision_level`, `vertices`, `faces`, `edges` (start, end, optional crease), `override_option`; `version` is read-only. Host validation requires at least 3 finite vertices, faces of at least 3 in-range indices, edges with distinct in-range endpoints and a finite non-negative crease, and a non-negative subdivision level, so out-of-range topology is rejected before mutation. `staged_python_mesh_lifecycle_over_real_ipc` passes the full gate: a quad with a creased edge, an added vertex and triangle plus subdivision level 1 in one transaction, canvas wires, seven rejection cases plus an atomic rollback, DWG and DXF reopen with re-edit, and three-step undo/redo. |
|
||||
| Helix | **Integration-tested / blocked at W by acadrust (DXF handedness)** | Writable generating parameters: `axis_base_point`, `start_point`, `axis_vector`, `radius` (the top radius; the base radius is the start point's distance from the axis), `turns`, `turn_height`, `handedness`, `constraint`. Read-only: `spline`, `major_version`, `maintenance_version`. The host rebuilds the derived NURBS spline with OCS's own helix builder on every scripted create or parameter edit (`normalize_scripted_helix`), so the spline always matches the parameters. Host validation requires finite positive radius, turns and turn height, a nonzero axis and a start point off the axis. `staged_python_helix_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`) passes C/R/E/D/U/I/V/P: creation with a derived spline ending at z = turns × turn height, a turns and handedness edit that regenerates it, canvas wires, seven rejection cases plus an atomic rollback, DWG reopen and re-edit, DXF reopen and re-edit, and three-step undo/redo. **W blocker (engine, pinned by a canary assertion):** the DXF reader parses boolean group 290 as an `i16` and never applies it, so a clockwise helix reopens counter-clockwise from DXF while DWG keeps it. Not `Complete` until the reader is fixed. |
|
||||
| RasterImage | **Integration-tested** | Writable: `file_path` (creation only; a later change is rejected because the image definition owns it), `insertion_point`, `u_vector`, `v_vector` (world units per pixel), `flags`, `clipping_enabled`, `brightness`, `contrast`, `fade`, `clip_boundary`. Read-only: `size` (pixel size, taken from the file), `class_version`, `definition_handle`, `definition_reactor_handle`. Creating from a script requires a readable image file: the host reads its pixel size, creates the `ImageDefinition` object, links it, and sets `size` and a whole-image clip boundary from the file. Host validation requires finite, non-parallel `u`/`v` vectors, brightness/contrast/fade within 0..=100, known display flags, finite clip vertices, at least 2 clip vertices when clipping is enabled, and an existing definition. `staged_python_raster_image_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, real PNG fixture) passes C/R/E/D/U/I/W/V/P inside OCS: creation with a host-made definition, placement/scale/brightness/contrast/fade and clip edit in one transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG and DXF reopen with the definition link and file path intact, re-edit, and three-step undo/redo. **Why not `Complete`:** the host does not create an `ACAD_IMAGE_DICT` entry or `IMAGEDEF_REACTOR` for a scripted image, so other CAD applications may need to repair the linkage; that cannot be verified here. Editing an existing definition (file path, resolution) is out of scope. |
|
||||
| RasterImage | **Integration-tested** | Writable: `file_path` (creation only; a later change is rejected because the image definition owns it), `insertion_point`, `u_vector`, `v_vector` (world units per pixel), `flags`, `clipping_enabled`, `brightness`, `contrast`, `fade`, `clip_boundary`. Read-only: `size` (pixel size, taken from the file), `class_version`, `definition_handle`, `definition_reactor_handle`. Creating from a script requires a readable image file: the host reads its pixel size, creates the `ImageDefinition` object, links it, and sets `size` and a whole-image clip boundary from the file. Host validation requires finite, non-parallel `u`/`v` vectors, brightness/contrast/fade within 0..=100, known display flags, finite clip vertices, at least 2 clip vertices when clipping is enabled, and an existing definition. `staged_python_raster_image_lifecycle_over_real_ipc` (shared `run_mesh_lifecycle`, real PNG fixture) passes C/R/E/D/U/I/W/V/P inside OCS: creation with a host-made definition, placement/scale/brightness/contrast/fade and clip edit in one transaction, canvas wires, eight rejection cases plus an atomic rollback, DWG and DXF reopen with the definition link and file path intact, re-edit, and three-step undo/redo. **Linkage (updated 2026-09-20):** a scripted image now reuses one `ImageDefinition` per file and registers each definition in `ACAD_IMAGE_DICT` under the file's name (a `_2`, `_3` suffix keeps keys unique), creating that dictionary under the root when the drawing has none, and the dictionary owns the definition, as for an image AutoCAD attached. `audit_python_raster_image_definition_linkage_over_real_ipc` checks two images of one file sharing a definition, a third file getting its own, the entries resolving with the right owner, and all of it surviving DWG and DXF reopen and an undo without a dangling entry. **Why not `Complete`:** the host still writes no `IMAGEDEF_REACTOR`: cadcodec's struct documents the reactor's owner as the definition while its DXF writer follows a different convention, and AutoCAD's behavior cannot be checked here, so guessing risks a structure AutoCAD repairs or rejects. **Related finding:** OCS's own IMAGE command creates a bare `RasterImage` with no definition, and a save through either writer drops its file path (`spike_raster_image_definition_persistence`), while the definition-linked form keeps it. 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. |
|
||||
| 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 | **Complete (OCS round trip)** | Writable: `center`, `width`, `height`, `status`, `view_center`, `view_direction`, `view_target`, `lens_length`, `front_clip_z`, `back_clip_z`, `view_height`, `twist_angle`, `frozen_layers` (layer handles), `render_mode`, `circle_sides`, `ucs_icon_visible`. Read-only: `id`, `custom_scale`, snap and 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 and every frozen-layer handle an existing layer. **Derived by the host (new 2026-09-20), as the MVIEW command does:** a scripted viewport gets an `id` unique in its layout and at least 2 (max existing plus one), and `custom_scale` is `height / view_height`, recomputed whenever either changes, so `custom_scale` is now read-only (setting the scale directly would have let it drift from the geometry). 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. **Canvas oracle (new):** the shared driver now activates the layout that owns the viewport (`Scene::set_current_layout`) and requires the viewport to draw a frame both after creation and after the edit. `staged_python_viewport_lifecycle_over_real_ipc` passes C/R/E/D/U/I/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. `audit_python_viewport_ids_and_scale_over_real_ipc` checks unique ids (2, 3, 4), the derived scale on create and after a zoom and a resize, an id kept across edits, a rejected direct `custom_scale` write, and that every viewport draws a frame. A control showed that the engine default id 0 is **not** invisible in a layout with no sheet viewport (it draws a frame), which corrects an earlier suspicion; the derived id matters for uniqueness. Not covered: clip boundary entities (`clip_boundary_handle` is read-only and preserved), non-rectangular viewports, and AutoCAD acceptance of the written viewports, which cannot be tested here. |
|
||||
|
||||
+187
-11
@@ -220,6 +220,30 @@ impl<'a> HostSession<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `ACAD_IMAGE_DICT` dictionary under the named-objects root, created
|
||||
/// when the drawing has none. `None` when the drawing has no root dictionary.
|
||||
fn ensure_image_dictionary(&mut self) -> Option<Handle> {
|
||||
use acadrust::objects::{Dictionary, ObjectType};
|
||||
let root = self.document().header.named_objects_dict_handle;
|
||||
let Some(ObjectType::Dictionary(root_dictionary)) = self.document().objects.get(&root) else {
|
||||
return None;
|
||||
};
|
||||
if let Some(existing) = root_dictionary.get("ACAD_IMAGE_DICT") {
|
||||
if matches!(self.document().objects.get(&existing), Some(ObjectType::Dictionary(_))) {
|
||||
return Some(existing);
|
||||
}
|
||||
}
|
||||
let handle = self.document_mut().allocate_handle();
|
||||
let mut dictionary = Dictionary::new();
|
||||
dictionary.handle = handle;
|
||||
dictionary.owner = root;
|
||||
self.document_mut().objects.insert(handle, ObjectType::Dictionary(dictionary));
|
||||
if let Some(ObjectType::Dictionary(root_dictionary)) = self.document_mut().objects.get_mut(&root) {
|
||||
root_dictionary.add_entry("ACAD_IMAGE_DICT", handle);
|
||||
}
|
||||
Some(handle)
|
||||
}
|
||||
|
||||
/// A scripted RasterImage names a file; the host creates the image
|
||||
/// definition object it needs (pixel size read from the file) and links it.
|
||||
fn prepare_scripted_raster_image(&mut self, entity: &mut EntityType) -> Result<(), String> {
|
||||
@@ -231,17 +255,51 @@ impl<'a> HostSession<'a> {
|
||||
}
|
||||
let (width, height) = image::image_dimensions(&image.file_path)
|
||||
.map_err(|error| format!("cannot read image {:?}: {error}", image.file_path))?;
|
||||
let handle = self.document_mut().allocate_handle();
|
||||
let mut definition = acadrust::objects::ImageDefinition::with_dimensions(
|
||||
image.file_path.clone(),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
definition.handle = handle;
|
||||
definition.is_loaded = true;
|
||||
self.document_mut()
|
||||
.objects
|
||||
.insert(handle, acadrust::objects::ObjectType::ImageDefinition(definition));
|
||||
// One definition per file, as AutoCAD keeps it: reuse an existing one.
|
||||
let existing = self.document().objects.iter().find_map(|(handle, object)| match object {
|
||||
acadrust::objects::ObjectType::ImageDefinition(definition)
|
||||
if definition.file_name == image.file_path => Some(*handle),
|
||||
_ => None,
|
||||
});
|
||||
let handle = match existing {
|
||||
Some(handle) => handle,
|
||||
None => {
|
||||
let handle = self.document_mut().allocate_handle();
|
||||
let mut definition = acadrust::objects::ImageDefinition::with_dimensions(
|
||||
image.file_path.clone(),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
definition.handle = handle;
|
||||
definition.is_loaded = true;
|
||||
// Register it in ACAD_IMAGE_DICT under the file's name, and let
|
||||
// that dictionary own it, like an image AutoCAD attached.
|
||||
let stem = std::path::Path::new(&image.file_path)
|
||||
.file_stem()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or("IMAGE")
|
||||
.to_owned();
|
||||
if let Some(dictionary) = self.ensure_image_dictionary() {
|
||||
definition.owner = dictionary;
|
||||
if let Some(acadrust::objects::ObjectType::Dictionary(entries)) =
|
||||
self.document_mut().objects.get_mut(&dictionary)
|
||||
{
|
||||
let mut key = stem.clone();
|
||||
let mut suffix = 1;
|
||||
while entries.get(&key).is_some() {
|
||||
suffix += 1;
|
||||
key = format!("{stem}_{suffix}");
|
||||
}
|
||||
entries.add_entry(key, handle);
|
||||
}
|
||||
}
|
||||
self.document_mut()
|
||||
.objects
|
||||
.insert(handle, acadrust::objects::ObjectType::ImageDefinition(definition));
|
||||
handle
|
||||
}
|
||||
};
|
||||
image.definition_handle = Some(handle);
|
||||
// `size` is the image's pixel size, and an untouched clip boundary
|
||||
// covers the whole image, so both follow the file.
|
||||
@@ -5867,6 +5925,124 @@ mod tests {
|
||||
assert!(!host.app.tabs[0].scene.wire_models_for(&[bare]).is_empty());
|
||||
}
|
||||
|
||||
/// Evidence for the RasterImage ledger row: what a save does with a bare
|
||||
/// image (OCS's native IMAGE command) and with a definition-linked one.
|
||||
#[test]
|
||||
#[ignore = "exploratory evidence"]
|
||||
fn spike_raster_image_definition_persistence() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("ocs_spike_image.png").to_string_lossy().into_owned();
|
||||
image::RgbaImage::from_pixel(8, 4, image::Rgba([1, 2, 3, 255])).save(&path).unwrap();
|
||||
for linked in [false, true] {
|
||||
let mut doc = acadrust::CadDocument::new();
|
||||
let mut img = acadrust::entities::RasterImage::with_size(&path, acadrust::types::Vector3::ZERO, 8.0, 4.0, 16.0, 8.0);
|
||||
if linked {
|
||||
let h = doc.allocate_handle();
|
||||
let mut def = acadrust::objects::ImageDefinition::with_dimensions(path.clone(), 8, 4);
|
||||
def.handle = h;
|
||||
def.is_loaded = true;
|
||||
doc.objects.insert(h, acadrust::objects::ObjectType::ImageDefinition(def));
|
||||
img.definition_handle = Some(h);
|
||||
}
|
||||
let handle = doc.add_entity(EntityType::RasterImage(img)).unwrap();
|
||||
for (label, document) in [
|
||||
("DWG", crate::io::load_bytes("i.dwg", acadrust::DwgWriter::write_to_vec(&doc).unwrap()).unwrap()),
|
||||
("DXF", crate::io::load_bytes("i.dxf", acadrust::DxfWriter::new(&doc).write_to_vec().unwrap()).unwrap()),
|
||||
] {
|
||||
let entity = document.get_entity(handle);
|
||||
let defs = document.objects.values().filter(|o| matches!(o, acadrust::objects::ObjectType::ImageDefinition(_))).count();
|
||||
let reactors = document.objects.values().filter(|o| matches!(o, acadrust::objects::ObjectType::ImageDefinitionReactor(_))).count();
|
||||
let dicts = document.objects.values().filter(|o| matches!(o, acadrust::objects::ObjectType::Dictionary(d) if d.entries.iter().any(|(k, _)| k.contains("IMAGE")))).count();
|
||||
match entity {
|
||||
Some(EntityType::RasterImage(i)) => eprintln!("SPIKE linked={linked} {label}: file_path_ok={} definition={:?} reactor={:?} defs={defs} reactors={reactors} image_dicts={dicts}",
|
||||
i.file_path == path, i.definition_handle.map(|h| h.value()), i.definition_reactor_handle.map(|h| h.value())),
|
||||
other => eprintln!("SPIKE linked={linked} {label}: came back as {:?}", other.map(|e| format!("{e:?}").chars().take(24).collect::<String>())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_python_raster_image_definition_linkage_over_real_ipc() {
|
||||
use acadrust::objects::ObjectType;
|
||||
let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else {
|
||||
return;
|
||||
};
|
||||
let dir = std::env::temp_dir();
|
||||
let (a, b) = (dir.join("ocs_link_a.png"), dir.join("ocs_link_b.png"));
|
||||
image::RgbaImage::from_pixel(8, 4, image::Rgba([9, 9, 9, 255])).save(&a).unwrap();
|
||||
image::RgbaImage::from_pixel(6, 6, image::Rgba([90, 9, 9, 255])).save(&b).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 script = dir.join(format!("ocs_link_{}.py", std::process::id()));
|
||||
std::fs::write(&script, format!(concat!(
|
||||
"doc = ocs.active_document\n",
|
||||
"doc.create_entity('RasterImage', file_path={a:?}, insertion_point=(0, 0, 0))\n",
|
||||
"doc.create_entity('RasterImage', file_path={a:?}, insertion_point=(20, 0, 0))\n",
|
||||
"doc.create_entity('RasterImage', file_path={b:?}, insertion_point=(40, 0, 0))\n",
|
||||
), a = a.display().to_string(), b = b.display().to_string())).unwrap();
|
||||
assert!(process.dispatch(&mut host, &format!("PY_RUN {}", script.display()), &mut |_| {}).unwrap());
|
||||
let _ = std::fs::remove_file(&script);
|
||||
|
||||
// The structure: one definition per file, registered and owned by ACAD_IMAGE_DICT.
|
||||
let structure = |document: &CadDocument| {
|
||||
let root = document.header.named_objects_dict_handle;
|
||||
let Some(ObjectType::Dictionary(root_dict)) = document.objects.get(&root) else { panic!("no root dictionary") };
|
||||
let dict_handle = root_dict.get("ACAD_IMAGE_DICT");
|
||||
let entries = dict_handle.and_then(|h| match document.objects.get(&h) {
|
||||
Some(ObjectType::Dictionary(d)) => Some(d.entries.clone()), _ => None }).unwrap_or_default();
|
||||
let defs: Vec<_> = document.objects.iter().filter_map(|(h, o)| match o {
|
||||
ObjectType::ImageDefinition(d) => Some((*h, d.owner, d.file_name.clone())), _ => None }).collect();
|
||||
let mut images: Vec<_> = document.entities().filter_map(|e| match e {
|
||||
EntityType::RasterImage(i) => Some((i.common.handle, i.definition_handle, i.file_path.clone())), _ => None }).collect();
|
||||
images.sort_by_key(|(h, _, _)| h.value());
|
||||
(dict_handle, entries, defs, images)
|
||||
};
|
||||
let check = |label: &str, document: &CadDocument| {
|
||||
let (dict, entries, defs, images) = structure(document);
|
||||
let dict = dict.unwrap_or_else(|| panic!("{label}: no ACAD_IMAGE_DICT"));
|
||||
assert_eq!(defs.len(), 2, "{label}: one definition per file");
|
||||
let mut keys: Vec<_> = entries.iter().map(|(k, _)| k.as_str()).collect();
|
||||
keys.sort();
|
||||
assert_eq!(keys, vec!["ocs_link_a", "ocs_link_b"], "{label}");
|
||||
for (key, handle) in &entries {
|
||||
let (_, owner, file) = defs.iter().find(|(h, _, _)| h == handle).unwrap_or_else(|| panic!("{label}: dangling entry {key}"));
|
||||
assert_eq!(*owner, dict, "{label}: {key} is owned by ACAD_IMAGE_DICT");
|
||||
assert!(file.contains(key.as_str()), "{label}: {key} -> {file}");
|
||||
}
|
||||
assert_eq!(images.len(), 3, "{label}");
|
||||
assert_eq!(images[0].1, images[1].1, "{label}: images of one file share a definition");
|
||||
assert_ne!(images[0].1, images[2].1, "{label}");
|
||||
for (_, definition, path) in &images {
|
||||
assert!(defs.iter().any(|(h, _, _)| Some(*h) == *definition), "{label}: definition resolves");
|
||||
assert!(path.contains("ocs_link_"), "{label}: file path kept: {path}");
|
||||
}
|
||||
};
|
||||
check("live", host.document());
|
||||
let dwg = crate::io::load_bytes("link.dwg", acadrust::DwgWriter::write_to_vec(host.document()).unwrap()).unwrap();
|
||||
let dxf = crate::io::load_bytes("link.dxf", acadrust::DxfWriter::new(host.document()).write_to_vec().unwrap()).unwrap();
|
||||
check("DWG", &dwg);
|
||||
check("DXF", &dxf);
|
||||
|
||||
// U: undoing the last image leaves no dangling dictionary entry.
|
||||
drop(process);
|
||||
drop(host);
|
||||
app.finish_pending_history(0);
|
||||
app.undo_steps(1);
|
||||
let (_, entries, defs, images) = structure(&app.tabs[0].scene.document);
|
||||
assert!(images.len() < 3);
|
||||
for (key, handle) in &entries {
|
||||
assert!(defs.iter().any(|(h, _, _)| h == handle), "undo left a dangling entry {key}");
|
||||
}
|
||||
for file in [&a, &b] {
|
||||
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 {
|
||||
|
||||
Reference in new issue
Block a user