forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 3 (block references)
- Operation::CreateInsert(InsertSpec) appended to the wire enum (append-only). - backend add_insert validates the BlockRecord exists (unknown block -> Validation, no mutation); host inserts acadrust::entities::Insert with scale+rotation. - Insert transform moves insert_point (transform_entity_geometry) + ensure_transformable includes Insert. - facade EntityCollection::create_insert; spec Insert family; api_reference + binding_schema regenerated; mocks updated. - Test phase3_create_insert_and_transform -> 599 host lib green. - IMPLEMENTATION.md: Phase 3 done (attributes/traversal remaining).
This commit is contained in:
1 parent
d46351cdb3
commit
b03f68737e
13 files changed
+146
-5
No files matched your search
@@ -61,9 +61,11 @@ variants append at enum end.
|
||||
are supported (Phase 0).
|
||||
|
||||
### Phase 3 — containers
|
||||
- **Entities:** insert/block references, attribute definitions/entities.
|
||||
- **Methods:** `create_insert`, placement, attribute get/set.
|
||||
- **Read-only:** nested block content traversal (nested ≠ first-level).
|
||||
- **Done:** `create_insert(block_name, point, scale, rotation)` → `Operation::CreateInsert`
|
||||
(validates the BlockRecord exists; unknown block → `Validation`, no mutation).
|
||||
Insert `transform` moves its `insert_point`. Covered by `phase3_*` test.
|
||||
- **Remaining:** `AttributeDefinition`/`AttributeEntity` typed create + attribute
|
||||
get/set; read-only nested block-content traversal.
|
||||
|
||||
### Phase 4 — paper-space & viewports
|
||||
- **Entities:** viewport entities.
|
||||
|
||||
@@ -99,6 +99,12 @@ impl DocApiBackend for MockBackend {
|
||||
self.curves.insert(id, spec.clone());
|
||||
Ok(id)
|
||||
}
|
||||
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||
if spec.block_name.is_empty() {
|
||||
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
||||
}
|
||||
Ok(self.alloc("Insert"))
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
|
||||
@@ -237,6 +237,21 @@ args = [{ name = "origin", ty = "[f64; 3]" }, { name = "direction", ty = "[f64;
|
||||
op = "CreateCurve"
|
||||
fixed = { curve = "XLine" }
|
||||
|
||||
# ============================================================================
|
||||
# CONTAINER FAMILY (phase 3) — block references
|
||||
# ============================================================================
|
||||
[[family]]
|
||||
name = "Insert"
|
||||
acadrust_variant = "Insert"
|
||||
handle = "Entity"
|
||||
collection = "entities"
|
||||
description = "Block reference. Place an existing BlockRecord by name; transform moves its insert point."
|
||||
|
||||
[[family.constructor]]
|
||||
name = "create_insert"
|
||||
args = [{ name = "block_name", ty = "&str" }, { name = "insert_point", ty = "[f64; 3]" }, { name = "scale", ty = "f64" }, { name = "rotation", ty = "f64" }]
|
||||
op = "CreateInsert"
|
||||
|
||||
# ============================================================================
|
||||
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
||||
# ============================================================================
|
||||
|
||||
@@ -35,6 +35,10 @@ pub trait DocApiBackend {
|
||||
/// Add a 2D curve entity; returns the fresh `ObjectId`.
|
||||
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a block reference (`INSERT`). Validates `block_name` exists; returns
|
||||
/// the fresh `ObjectId`. `Validation` if the block is unknown.
|
||||
fn add_insert(&mut self, spec: &crate::ops::InsertSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Remove any first-level entity by id. Returns `false` if absent; returns
|
||||
/// `Err` if the entity exists but cannot be removed (e.g. locked layer).
|
||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool>;
|
||||
|
||||
@@ -176,6 +176,13 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
b.finalize_op();
|
||||
OpOutcome::Deleted(ids.clone())
|
||||
}
|
||||
Operation::CreateInsert(spec) => {
|
||||
// Validate the block name exists before committing (no rollback needed).
|
||||
b.push_undo(name);
|
||||
let id = b.add_insert(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
};
|
||||
Ok(Receipt {
|
||||
outcome: Some(outcome),
|
||||
|
||||
@@ -464,6 +464,21 @@ pub struct EntityCollection {
|
||||
}
|
||||
|
||||
impl EntityCollection {
|
||||
/// Place a block reference (`INSERT`) for `block_name` (must exist).
|
||||
pub fn create_insert(&self, block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64) -> ApiResult<Entity> {
|
||||
let receipt = self.session.apply_op(Operation::CreateInsert(crate::ops::InsertSpec {
|
||||
block_name: block_name.to_string(),
|
||||
insert_point,
|
||||
scale,
|
||||
rotation,
|
||||
}))?;
|
||||
let id = receipt
|
||||
.outcome
|
||||
.and_then(|o| o.new_id())
|
||||
.ok_or_else(|| ApiError::Transport("create_insert returned no id".into()))?;
|
||||
Ok(Entity::new(self.session.clone(), id))
|
||||
}
|
||||
|
||||
/// Generic lookup by id (any family) → `Entity` (downcast via `as_solid()`, …).
|
||||
pub fn get(&self, id: ObjectId) -> ApiResult<Entity> {
|
||||
// Validate existence eagerly so `get` errors surface as `UnknownId`.
|
||||
|
||||
@@ -87,6 +87,14 @@ B-rep solid. Host resolves bodies via the solid_models cache (lift-on-miss from
|
||||
|
||||
- **create_xline**(`origin: [f64; 3], direction: [f64; 3]`) -> `XLine`
|
||||
|
||||
## `Entity` (acadrust `Insert`, collection `entities`)
|
||||
|
||||
Block reference. Place an existing BlockRecord by name; transform moves its insert point.
|
||||
|
||||
### Constructors (`doc.entities()`)
|
||||
|
||||
- **create_insert**(`block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64`) -> `Entity`
|
||||
|
||||
## Generic methods (every handle)
|
||||
|
||||
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `Entity`) is a
|
||||
|
||||
@@ -187,6 +187,13 @@
|
||||
"handle": "XLine",
|
||||
"methods": [],
|
||||
"name": "XLine"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "Insert",
|
||||
"collection": "entities",
|
||||
"handle": "Entity",
|
||||
"methods": [],
|
||||
"name": "Insert"
|
||||
}
|
||||
],
|
||||
"layouts": {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::id::ObjectId;
|
||||
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, PlacementSpec, SolidPrimitive};
|
||||
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, InsertSpec, PlacementSpec, SolidPrimitive};
|
||||
|
||||
/// A typed write operation (plan §5). Each variant is ONE atomic host call = one
|
||||
/// undo step. Append new variants at the END only.
|
||||
@@ -27,4 +27,6 @@ pub enum Operation {
|
||||
CreateMany(Vec<EntitySpec>),
|
||||
TransformMany { ids: Vec<ObjectId>, placement: PlacementSpec },
|
||||
DeleteMany(Vec<ObjectId>),
|
||||
// containers
|
||||
CreateInsert(InsertSpec),
|
||||
}
|
||||
@@ -56,6 +56,17 @@ pub enum EntitySpec {
|
||||
Solid(SolidPrimitive),
|
||||
}
|
||||
|
||||
/// Construction spec for a block reference (`INSERT`): place the block
|
||||
/// `block_name` (must exist as a BlockRecord) at `insert_point` with uniform
|
||||
/// `scale` and `rotation` (radians, about Z).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct InsertSpec {
|
||||
pub block_name: String,
|
||||
pub insert_point: [f64; 3],
|
||||
pub scale: f64,
|
||||
pub rotation: f64,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -103,6 +114,7 @@ impl crate::gen::Operation {
|
||||
CreateMany(_) => "CreateMany",
|
||||
TransformMany { .. } => "TransformMany",
|
||||
DeleteMany(_) => "DeleteMany",
|
||||
CreateInsert(_) => "CreateInsert",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,12 @@ impl DocApiBackend for MockBackend {
|
||||
};
|
||||
Ok(self.alloc(kind))
|
||||
}
|
||||
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||
if spec.block_name.is_empty() {
|
||||
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
||||
}
|
||||
Ok(self.alloc("Insert"))
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
|
||||
@@ -105,6 +105,26 @@ impl DocApiBackend for HostSession<'_> {
|
||||
Ok(handle_to_obj(handle))
|
||||
}
|
||||
|
||||
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||
// Validate the referenced block exists before committing.
|
||||
if self.document().block_records.get(&spec.block_name).is_none() {
|
||||
return Err(ApiError::validation(
|
||||
"CreateInsert",
|
||||
format!("unknown block {:?}", spec.block_name),
|
||||
));
|
||||
}
|
||||
let mut ins = acadrust::entities::Insert::new(
|
||||
spec.block_name.clone(),
|
||||
acadrust::types::Vector3::new(spec.insert_point[0], spec.insert_point[1], spec.insert_point[2]),
|
||||
);
|
||||
ins.rotation = spec.rotation;
|
||||
ins.set_x_scale(spec.scale);
|
||||
ins.set_y_scale(spec.scale);
|
||||
ins.set_z_scale(spec.scale);
|
||||
let handle = self.scene_mut().add_entity(EntityType::Insert(ins));
|
||||
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 {
|
||||
@@ -162,6 +182,7 @@ impl DocApiBackend for HostSession<'_> {
|
||||
| EntityType::Spline(_)
|
||||
| EntityType::Ray(_)
|
||||
| EntityType::XLine(_)
|
||||
| EntityType::Insert(_)
|
||||
| EntityType::Point(_)
|
||||
| EntityType::LwPolyline(_)
|
||||
);
|
||||
@@ -605,6 +626,37 @@ mod tests {
|
||||
assert!((r.base_point.z - 5.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
// ── Phase 3: containers (block references) ───────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn phase3_create_insert_and_transform() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
// Register a block record so the insert can reference it.
|
||||
host.document_mut().block_records.add(acadrust::tables::BlockRecord::new("MyBlock")).unwrap();
|
||||
|
||||
// Insert the block at (10,20,0) scale 2, rotation 0.
|
||||
let ins = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateInsert(
|
||||
ocs_doc_api::ops::InsertSpec { block_name: "MyBlock".into(), insert_point: [10.0, 20.0, 0.0], scale: 2.0, rotation: 0.0 }))).unwrap());
|
||||
let handle = obj_to_handle(ins);
|
||||
let Some(EntityType::Insert(i)) = host.document().get_entity(handle) else { panic!("insert not found") };
|
||||
assert_eq!(i.block_name, "MyBlock");
|
||||
assert!((i.insert_point.x - 10.0).abs() < 1e-9 && (i.x_scale() - 2.0).abs() < 1e-9);
|
||||
|
||||
// Transform the insert by +5 in X.
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::Transform {
|
||||
id: ins, placement: ocs_doc_api::PlacementSpec::at([5.0, 0.0, 0.0]) })).unwrap();
|
||||
let Some(EntityType::Insert(i)) = host.document().get_entity(handle) else { panic!("insert not found") };
|
||||
assert!((i.insert_point.x - 15.0).abs() < 1e-6);
|
||||
|
||||
// Inserting a non-existent block is a Validation error, no entity created.
|
||||
let before = host.scene().geometry_epoch;
|
||||
let err = dispatch(&mut host, DocApiEnvelope::op(Operation::CreateInsert(
|
||||
ocs_doc_api::ops::InsertSpec { block_name: "NoSuchBlock".into(), insert_point: [0.0; 3], scale: 1.0, rotation: 0.0 }))).unwrap_err();
|
||||
assert!(matches!(err, ApiError::Validation { .. }), "{err:?}");
|
||||
assert_eq!(host.scene().geometry_epoch, before, "no entity on unknown block");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_api_unknown_id_surfaces_structured_error() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
@@ -124,6 +124,11 @@ pub fn transform_entity_geometry(
|
||||
x.base_point = apply(x.base_point);
|
||||
EntityType::XLine(x)
|
||||
}
|
||||
EntityType::Insert(ins) => {
|
||||
let mut ins = ins.clone();
|
||||
ins.insert_point = apply(ins.insert_point);
|
||||
EntityType::Insert(ins)
|
||||
}
|
||||
EntityType::Point(pt) => {
|
||||
let mut pt = pt.clone();
|
||||
pt.location = apply(pt.location);
|
||||
@@ -236,9 +241,9 @@ pub fn entity_kind_name(entity: &EntityType) -> &'static str {
|
||||
EntityType::Spline(_) => "Spline",
|
||||
EntityType::Ray(_) => "Ray",
|
||||
EntityType::XLine(_) => "XLine",
|
||||
EntityType::Insert(_) => "Insert",
|
||||
EntityType::Text(_) => "Text",
|
||||
EntityType::MText(_) => "MText",
|
||||
EntityType::Insert(_) => "Insert",
|
||||
_ => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user