forked from 1b-agent/opencadstudio-upstream
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.
This commit is contained in:
1 parent
e6c132e0eb
commit
46a5922520
12 files changed
+109
-6
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -204,6 +204,9 @@ impl DocApiBackend for MockBackend {
|
||||
fn block_entities(&self, _block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn add_raster_image(&mut self, _spec: &ocs_doc_api::ops::RasterImageSpec) -> ApiResult<ObjectId> {
|
||||
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));
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<ObjectId>;
|
||||
|
||||
/// 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).
|
||||
|
||||
@@ -247,6 +247,12 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
Operation::CreateRasterImage(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_raster_image(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
};
|
||||
Ok(Receipt {
|
||||
outcome: Some(outcome),
|
||||
|
||||
@@ -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<Entity> {
|
||||
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<Entity> {
|
||||
let receipt = self.session.apply_op(Operation::CreateHatch(crate::ops::HatchSpec {
|
||||
|
||||
@@ -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`)
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,9 @@ impl DocApiBackend for MockBackend {
|
||||
fn block_entities(&self, _block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn add_raster_image(&mut self, _spec: &ocs_doc_api::ops::RasterImageSpec) -> ApiResult<ObjectId> {
|
||||
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));
|
||||
|
||||
@@ -60,6 +60,9 @@ fn operation_variants() -> Vec<String> {
|
||||
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.
|
||||
|
||||
@@ -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<ObjectId> {
|
||||
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]
|
||||
|
||||
Reference in new issue
Block a user