From 46a59225203c1d8d00483588f945e8ac8c43d10a Mon Sep 17 00:00:00 2001 From: Sebastian <106036+schoeller@users.noreply.github.com> Date: Sun, 6 Sep 2026 10:28:51 +0200 Subject: [PATCH] ocs_doc_api: Phase 5 typed raster image create - CreateRasterImage(RasterImageSpec) op (append-only). - backend add_raster_image (RasterImage with file_path/insertion/u/v/size; the scene auto-registers the ImageDefinition on add_entity). - facade create_raster_image; spec RasterImage constructor; api_reference + binding_schema regenerated; mocks + baseline. - Test phase5_create_raster_image_registers_definition (asserts definition_handle auto-registered + 4-corner bounds) -> 610 host lib + 3 consistency green. - Phase 5 complete. --- crates/ocs_doc_api/IMPLEMENTATION.md | 8 ++-- crates/ocs_doc_api/examples/common/mod.rs | 3 ++ crates/ocs_doc_api/spec/entities.toml | 7 ++- crates/ocs_doc_api/src/backend.rs | 3 ++ crates/ocs_doc_api/src/executor.rs | 6 +++ crates/ocs_doc_api/src/facade.rs | 13 ++++++ crates/ocs_doc_api/src/gen/api_reference.md | 6 ++- crates/ocs_doc_api/src/gen/ops_gen.rs | 4 +- crates/ocs_doc_api/src/ops.rs | 14 ++++++ crates/ocs_doc_api/tests/mock_backend.rs | 3 ++ .../tests/spec_wire_consistency.rs | 4 ++ src/app/doc_api.rs | 44 +++++++++++++++++++ 12 files changed, 109 insertions(+), 6 deletions(-) diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index 653ca79b0..a413b20f5 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -102,9 +102,11 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal `Region`, `Body`, `Surface`, `Face3D`, `Dimension`, `Hatch`) instead of "Other"; `RasterImage` has coarse bounds (insertion + uΒ·width + vΒ·height); generic `delete` works on all. Covered by `phase5_*` test. -- **Remaining:** typed create/property ops for `RasterImage`/`Table`/light; - these largely stay read-only `EntityView` DTOs (typed ops only where the spec - declares an action mapping). +- **Done (typed create):** `create_raster_image(file_path, insertion, u, v, size)` + β†’ `Operation::CreateRasterImage`; the host auto-registers the `ImageDefinition` + (asserted by `phase5_create_raster_image_registers_definition`). Covered by that + test. `Table`/light stay read-only `EntityView` DTOs (typed ops only where the + spec declares an action mapping). ## πŸ”§ Outstanding methods on supported families diff --git a/crates/ocs_doc_api/examples/common/mod.rs b/crates/ocs_doc_api/examples/common/mod.rs index da2614520..b52e9781c 100644 --- a/crates/ocs_doc_api/examples/common/mod.rs +++ b/crates/ocs_doc_api/examples/common/mod.rs @@ -204,6 +204,9 @@ impl DocApiBackend for MockBackend { fn block_entities(&self, _block_name: &str) -> ApiResult> { Ok(Vec::new()) } + fn add_raster_image(&mut self, _spec: &ocs_doc_api::ops::RasterImageSpec) -> ApiResult { + Ok(self.alloc("RasterImage")) + } fn set_viewport_view(&mut self, id: ObjectId, view_target: [f64; 3], view_height: f64) -> ApiResult<()> { if !self.entity_exists(id) { return Err(ApiError::UnknownId(id)); diff --git a/crates/ocs_doc_api/spec/entities.toml b/crates/ocs_doc_api/spec/entities.toml index f28d8df80..72d3f2314 100644 --- a/crates/ocs_doc_api/spec/entities.toml +++ b/crates/ocs_doc_api/spec/entities.toml @@ -398,7 +398,12 @@ name = "RasterImage" acadrust_variant = "RasterImage" handle = "Entity" collection = "entities" -description = "Raster image (read-mostly): GetEntity kind + coarse bounds (insertion + u*width + v*height); generic delete. No typed create in v1." +description = "Raster image: create_raster_image(file_path, insertion, u, v, size) places an image (host auto-registers the ImageDefinition); bounds (4-corner bracket); delete." + +[[family.constructor]] +name = "create_raster_image" +args = [{ name = "file_path", ty = "&str" }, { name = "insertion_point", ty = "[f64; 3]" }, { name = "u_vector", ty = "[f64; 3]" }, { name = "v_vector", ty = "[f64; 3]" }, { name = "size", ty = "[f64; 2]" }] +op = "CreateRasterImage" [[family]] name = "Table" diff --git a/crates/ocs_doc_api/src/backend.rs b/crates/ocs_doc_api/src/backend.rs index edca3215f..ba25033d4 100644 --- a/crates/ocs_doc_api/src/backend.rs +++ b/crates/ocs_doc_api/src/backend.rs @@ -81,6 +81,9 @@ pub trait DocApiBackend { /// A viewport's view (target WCS + zoom height). Viewport-only. fn viewport_view(&self, id: ObjectId) -> ApiResult<([f64; 3], f64)>; + /// Add a `RASTER_IMAGE` (host auto-registers the ImageDefinition). + fn add_raster_image(&mut self, spec: &crate::ops::RasterImageSpec) -> ApiResult; + /// Can `id` be modified in place right now (exists, is the expected family, /// not on a locked layer)? Read-only pre-check used before mutations. /// Default: existence + not-locked (backends narrow the family check). diff --git a/crates/ocs_doc_api/src/executor.rs b/crates/ocs_doc_api/src/executor.rs index 319339b6e..57ad09c2c 100644 --- a/crates/ocs_doc_api/src/executor.rs +++ b/crates/ocs_doc_api/src/executor.rs @@ -247,6 +247,12 @@ pub fn apply_op(b: &mut B, op: Operation) -> ApiResult { + b.push_undo(name); + let id = b.add_raster_image(spec)?; + b.finalize_op(); + OpOutcome::NewId(id) + } }; Ok(Receipt { outcome: Some(outcome), diff --git a/crates/ocs_doc_api/src/facade.rs b/crates/ocs_doc_api/src/facade.rs index 2912d5905..5305469ec 100644 --- a/crates/ocs_doc_api/src/facade.rs +++ b/crates/ocs_doc_api/src/facade.rs @@ -509,6 +509,19 @@ impl CurveCollection { let (s, id) = self.create_curve(Curve2Spec::XLine { origin, direction })?; Ok(XLine::new(s, id)) } + /// A RASTER_IMAGE placed at `insertion_point` (host auto-registers the image definition). + pub fn create_raster_image(&self, file_path: &str, insertion_point: [f64; 3], u_vector: [f64; 3], v_vector: [f64; 3], size: [f64; 2]) -> ApiResult { + let receipt = self.session.apply_op(Operation::CreateRasterImage(crate::ops::RasterImageSpec { + file_path: file_path.to_string(), + insertion_point, + u_vector, + v_vector, + size, + }))?; + let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| ApiError::Transport("create_raster_image returned no id".into()))?; + Ok(Entity::new(self.session.clone(), id)) + } + /// A solid (or pattern) HATCH over a single closed polyline boundary. pub fn create_hatch(&self, boundary: &[[f64; 2]], solid: bool) -> ApiResult { let receipt = self.session.apply_op(Operation::CreateHatch(crate::ops::HatchSpec { diff --git a/crates/ocs_doc_api/src/gen/api_reference.md b/crates/ocs_doc_api/src/gen/api_reference.md index 25e7cad9a..38766a90f 100644 --- a/crates/ocs_doc_api/src/gen/api_reference.md +++ b/crates/ocs_doc_api/src/gen/api_reference.md @@ -165,7 +165,11 @@ Paper-space viewport: a widthΓ—height window at center looking at view_target wi ## `Entity` (acadrust `RasterImage`, collection `entities`) -Raster image (read-mostly): GetEntity kind + coarse bounds (insertion + u*width + v*height); generic delete. No typed create in v1. +Raster image: create_raster_image(file_path, insertion, u, v, size) places an image (host auto-registers the ImageDefinition); bounds (4-corner bracket); delete. + +### Constructors (`doc.entities()`) + +- **create_raster_image**(`file_path: &str, insertion_point: [f64; 3], u_vector: [f64; 3], v_vector: [f64; 3], size: [f64; 2]`) -> `Entity` ## `Entity` (acadrust `Table`, collection `entities`) diff --git a/crates/ocs_doc_api/src/gen/ops_gen.rs b/crates/ocs_doc_api/src/gen/ops_gen.rs index 682db65ab..28ecb9a28 100644 --- a/crates/ocs_doc_api/src/gen/ops_gen.rs +++ b/crates/ocs_doc_api/src/gen/ops_gen.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::id::ObjectId; -use crate::ops::{BoolOp, Curve2Spec, DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, SolidPrimitive, TextSpec, ViewportSpec}; +use crate::ops::{BoolOp, Curve2Spec, DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec, SolidPrimitive, TextSpec, ViewportSpec}; /// A typed write operation (plan Β§5). Each variant is ONE atomic host call = one /// undo step. Append new variants at the END only. @@ -44,4 +44,6 @@ pub enum Operation { SetAttribute { id: ObjectId, tag: String, value: String }, // viewport view SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }, + // media + CreateRasterImage(RasterImageSpec), } diff --git a/crates/ocs_doc_api/src/ops.rs b/crates/ocs_doc_api/src/ops.rs index 3e139ce2e..749ddb753 100644 --- a/crates/ocs_doc_api/src/ops.rs +++ b/crates/ocs_doc_api/src/ops.rs @@ -115,6 +115,19 @@ pub struct DimensionSpec { pub definition_point: [f64; 3], } +/// Construction spec for a `RASTER_IMAGE`: an image file placed at +/// `insertion_point` with per-pixel world-unit vectors `u_vector`/`v_vector` and +/// pixel `size` (width, height). The host auto-registers the ImageDefinition. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct RasterImageSpec { + pub file_path: String, + pub insertion_point: [f64; 3], + pub u_vector: [f64; 3], + pub v_vector: [f64; 3], + /// Pixel size (width, height). + pub size: [f64; 2], +} + /// A rigid similarity (rotation + translation + optional uniform scale/reflection) /// as a plain-data mirror of `cadkernel::brep::transform`'s `Placement` /// (non-serde). Columns are the new basis axes; `origin` is the translation. @@ -171,6 +184,7 @@ impl crate::gen::Operation { CreateDimensionLinear(_) => "CreateDimensionLinear", SetAttribute { .. } => "SetAttribute", SetViewportView { .. } => "SetViewportView", + CreateRasterImage(_) => "CreateRasterImage", } } } diff --git a/crates/ocs_doc_api/tests/mock_backend.rs b/crates/ocs_doc_api/tests/mock_backend.rs index e6f6c36a6..2bd1cd1fe 100644 --- a/crates/ocs_doc_api/tests/mock_backend.rs +++ b/crates/ocs_doc_api/tests/mock_backend.rs @@ -144,6 +144,9 @@ impl DocApiBackend for MockBackend { fn block_entities(&self, _block_name: &str) -> ApiResult> { Ok(Vec::new()) } + fn add_raster_image(&mut self, _spec: &ocs_doc_api::ops::RasterImageSpec) -> ApiResult { + Ok(self.alloc("RasterImage")) + } fn set_viewport_view(&mut self, id: ObjectId, view_target: [f64; 3], view_height: f64) -> ApiResult<()> { if !self.entity_exists(id) { return Err(ApiError::UnknownId(id)); diff --git a/crates/ocs_doc_api/tests/spec_wire_consistency.rs b/crates/ocs_doc_api/tests/spec_wire_consistency.rs index 1309a00bd..b636e3e5d 100644 --- a/crates/ocs_doc_api/tests/spec_wire_consistency.rs +++ b/crates/ocs_doc_api/tests/spec_wire_consistency.rs @@ -60,6 +60,9 @@ fn operation_variants() -> Vec { Operation::CreateDimensionLinear(ocs_doc_api::ops::DimensionSpec { first_point: [0.0; 3], second_point: [0.0; 3], definition_point: [0.0; 3] }), Operation::SetAttribute { id: ObjectId::from_u64(0), tag: String::new(), value: String::new() }, Operation::SetViewportView { id: ObjectId::from_u64(0), view_target: [0.0; 3], view_height: 0.0 }, + Operation::CreateRasterImage(ocs_doc_api::ops::RasterImageSpec { + file_path: String::new(), insertion_point: [0.0; 3], u_vector: [0.0; 3], v_vector: [0.0; 3], size: [0.0; 2], + }), ]; // Derive the variant names by serializing a probe of each and reading the // bincode variant index -> name map is not available; instead match by @@ -147,6 +150,7 @@ const OPERATION_BASELINE: &[&str] = &[ "CreateDimensionLinear", "SetAttribute", "SetViewportView", + "CreateRasterImage", ]; /// Recorded baseline of the `Query` variant order. diff --git a/src/app/doc_api.rs b/src/app/doc_api.rs index 8575cad69..d0d27d0ec 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -333,6 +333,20 @@ impl DocApiBackend for HostSession<'_> { Ok(([vp.view_target.x, vp.view_target.y, vp.view_target.z], vp.view_height)) } + fn add_raster_image(&mut self, spec: &ocs_doc_api::ops::RasterImageSpec) -> ApiResult { + use acadrust::entities::RasterImage; + use acadrust::types::{Vector2, Vector3}; + let mut img = RasterImage::default(); + img.file_path = spec.file_path.clone(); + img.insertion_point = Vector3::new(spec.insertion_point[0], spec.insertion_point[1], spec.insertion_point[2]); + img.u_vector = Vector3::new(spec.u_vector[0], spec.u_vector[1], spec.u_vector[2]); + img.v_vector = Vector3::new(spec.v_vector[0], spec.v_vector[1], spec.v_vector[2]); + img.size = Vector2::new(spec.size[0], spec.size[1]); + // add_entity auto-registers the ImageDefinition (scene/entity.rs). + let handle = self.scene_mut().add_entity(EntityType::RasterImage(img)); + Ok(handle_to_obj(handle)) + } + fn add_vertex(&mut self, id: ObjectId, at: usize, point: [f64; 3]) -> ApiResult<()> { let handle = obj_to_handle(id); let Some(entity) = self.document().get_entity(handle).cloned() else { @@ -1000,6 +1014,36 @@ mod tests { assert!((t.insertion_point.x - 15.0).abs() < 1e-6); } + // ── Phase 5 remaining: typed raster image create ───────────────────────── + + #[test] + fn phase5_create_raster_image_registers_definition() { + let mut app = OpenCADStudio::new_for_test(); + let mut host = HostSession::new(&mut app, 0); + let img = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateRasterImage( + ocs_doc_api::ops::RasterImageSpec { + file_path: "C:/img/photo.png".into(), + insertion_point: [10.0, 20.0, 0.0], + u_vector: [0.5, 0.0, 0.0], + v_vector: [0.0, 0.5, 0.0], + size: [100.0, 50.0], + }))).unwrap()); + let handle = obj_to_handle(img); + let Some(EntityType::RasterImage(i)) = host.document().get_entity(handle) else { panic!("image not found") }; + assert_eq!(i.file_path, "C:/img/photo.png"); + // The host auto-registered an ImageDefinition (definition_handle set). + assert!(i.definition_handle.is_some(), "ImageDefinition was auto-registered"); + // Bounds = insertion + u*100 + v*50 = (60, 45) (4-corner bracket). + let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetBounds { id: img }])).unwrap(); + match &receipt.query_results[0] { + QueryResult::Bounds(b) => { + assert!((b.min[0] - 10.0).abs() < 1e-6 && (b.max[0] - 60.0).abs() < 1e-6, "{b:?}"); + assert!((b.min[1] - 20.0).abs() < 1e-6 && (b.max[1] - 45.0).abs() < 1e-6, "{b:?}"); + } + other => panic!("expected bounds, got {other:?}"), + } + } + // ── Phase 4 remaining: set_view + viewport-view query ──────────────────── #[test]