From f8ae54a351175162041b266ab11f27dcb9a76d73 Mon Sep 17 00:00:00 2001 From: Sebastian <106036+schoeller@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:43:34 +0200 Subject: [PATCH] =?UTF-8?q?ocs=5Fdoc=5Fapi:=20Phase=203-ii=20=E2=80=94=20t?= =?UTF-8?q?yped=20AttributeDefinition=20create=20(Phase=203=20complete)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateAttributeDefinition(AttributeDefinitionSpec) op (append-only). - backend add_attribute_definition (ATTDEF tag/prompt/default/insertion/height/rotation). - facade create_attribute_definition; entity_kind_name AttributeDefinition/AttributeEntity. - spec AttributeDefinition family; api_reference + binding_schema regenerated; mocks + baseline. - Test phase3ii_create_attribute_definition -> 614 host lib + 3 consistency green. --- crates/ocs_doc_api/IMPLEMENTATION.md | 5 ++- crates/ocs_doc_api/examples/common/mod.rs | 6 +++ crates/ocs_doc_api/spec/entities.toml | 15 ++++++++ crates/ocs_doc_api/src/backend.rs | 3 ++ crates/ocs_doc_api/src/executor.rs | 6 +++ crates/ocs_doc_api/src/facade.rs | 14 +++++++ crates/ocs_doc_api/src/gen/api_reference.md | 8 ++++ .../ocs_doc_api/src/gen/binding_schema.json | 7 ++++ crates/ocs_doc_api/src/gen/ops_gen.rs | 4 +- crates/ocs_doc_api/src/ops.rs | 13 +++++++ crates/ocs_doc_api/tests/mock_backend.rs | 6 +++ .../tests/spec_wire_consistency.rs | 4 ++ src/app/doc_api.rs | 38 +++++++++++++++++++ src/app/doc_api_convert.rs | 2 + 14 files changed, 128 insertions(+), 3 deletions(-) diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index a1af7e731..511b269c8 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -87,8 +87,9 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal (`GetAttributes`), and `block_entities(block_name)` → read-only nested traversal of a block definition's entities (`GetBlockEntities`). Covered by `phase3_attributes_and_block_traversal` test. -- **Remaining:** typed `AttributeDefinition` (in-block attribute-definition) - create. +- **Done (attribute definitions):** `create_attribute_definition(tag, prompt, + default, point, height, rotation)` → `Operation::CreateAttributeDefinition`; + reports kind `AttributeDefinition`. Covered by `phase3ii_*` test. Phase 3 is complete. ### Phase 4 — paper-space & viewports - **Done:** `create_viewport(center, width, height, view_target, view_height)` → diff --git a/crates/ocs_doc_api/examples/common/mod.rs b/crates/ocs_doc_api/examples/common/mod.rs index c6fa42fe8..97a8c51e8 100644 --- a/crates/ocs_doc_api/examples/common/mod.rs +++ b/crates/ocs_doc_api/examples/common/mod.rs @@ -200,6 +200,12 @@ impl DocApiBackend for MockBackend { fn dimension_measurement(&self, id: ObjectId) -> ApiResult { self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id)) } + fn add_attribute_definition(&mut self, spec: &ocs_doc_api::ops::AttributeDefinitionSpec) -> ApiResult { + if spec.tag.is_empty() { + return Err(ApiError::validation("CreateAttributeDefinition", "empty tag")); + } + Ok(self.alloc("AttributeDefinition")) + } fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> 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 969a3893e..ba7ad316c 100644 --- a/crates/ocs_doc_api/spec/entities.toml +++ b/crates/ocs_doc_api/spec/entities.toml @@ -380,6 +380,21 @@ op = "SetAttribute" args = [{ name = "tag", ty = "&str" }, { name = "value", ty = "&str" }] returns = "()" +# ============================================================================ +# ATTRIBUTE-DEFINITION FAMILY (phase 3-ii) — in-block attribute definitions +# ============================================================================ +[[family]] +name = "AttributeDefinition" +acadrust_variant = "AttributeDefinition" +handle = "Entity" +collection = "entities" +description = "In-block attribute definition (ATTDEF): tag/prompt/default at an insertion point." + +[[family.constructor]] +name = "create_attribute_definition" +args = [{ name = "tag", ty = "&str" }, { name = "prompt", ty = "&str" }, { name = "default_value", ty = "&str" }, { name = "insertion_point", ty = "[f64; 3]" }, { name = "height", ty = "f64" }, { name = "rotation", ty = "f64" }] +op = "CreateAttributeDefinition" + # ============================================================================ # PAPER-SPACE FAMILY (phase 4) — viewports # ============================================================================ diff --git a/crates/ocs_doc_api/src/backend.rs b/crates/ocs_doc_api/src/backend.rs index 3f1af4237..6432ed2da 100644 --- a/crates/ocs_doc_api/src/backend.rs +++ b/crates/ocs_doc_api/src/backend.rs @@ -72,6 +72,9 @@ pub trait DocApiBackend { /// Add a 3-point angular `DIMENSION`; returns the fresh `ObjectId`. fn add_dimension_angular(&mut self, spec: &crate::ops::DimensionAngularSpec) -> ApiResult; + /// Add an `ATTDEF` (in-block attribute definition); returns the fresh `ObjectId`. + fn add_attribute_definition(&mut self, spec: &crate::ops::AttributeDefinitionSpec) -> ApiResult; + /// A dimension's measured value (distance for linear/radius, degrees for angular). fn dimension_measurement(&self, id: ObjectId) -> ApiResult; diff --git a/crates/ocs_doc_api/src/executor.rs b/crates/ocs_doc_api/src/executor.rs index ee22506d8..c2c3b7f08 100644 --- a/crates/ocs_doc_api/src/executor.rs +++ b/crates/ocs_doc_api/src/executor.rs @@ -287,6 +287,12 @@ pub fn apply_op(b: &mut B, op: Operation) -> ApiResult { + b.push_undo(name); + let id = b.add_attribute_definition(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 d0874dac9..4cb8e40d9 100644 --- a/crates/ocs_doc_api/src/facade.rs +++ b/crates/ocs_doc_api/src/facade.rs @@ -631,6 +631,20 @@ impl EntityCollection { Ok(Entity::new(self.session.clone(), id)) } + /// Create an ATTDEF (in-block attribute definition): tag/prompt/default at a point. + pub fn create_attribute_definition(&self, tag: &str, prompt: &str, default_value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> ApiResult { + let receipt = self.session.apply_op(Operation::CreateAttributeDefinition(crate::ops::AttributeDefinitionSpec { + tag: tag.to_string(), + prompt: prompt.to_string(), + default_value: default_value.to_string(), + insertion_point, + height, + rotation, + }))?; + let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| ApiError::Transport("create_attribute_definition returned no id".into()))?; + Ok(Entity::new(self.session.clone(), id)) + } + /// 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 { let receipt = self.session.apply_op(Operation::CreateInsert(crate::ops::InsertSpec { diff --git a/crates/ocs_doc_api/src/gen/api_reference.md b/crates/ocs_doc_api/src/gen/api_reference.md index 4fad81f5f..57fadbf00 100644 --- a/crates/ocs_doc_api/src/gen/api_reference.md +++ b/crates/ocs_doc_api/src/gen/api_reference.md @@ -154,6 +154,14 @@ Block reference. Place an existing BlockRecord by name; transform moves its inse - **attributes**() -> `Vec<(String, String)>` — query `GetAttributes` - **set_attribute**(tag: &str, value: &str) -> `()` — op `SetAttribute` +## `Entity` (acadrust `AttributeDefinition`, collection `entities`) + +In-block attribute definition (ATTDEF): tag/prompt/default at an insertion point. + +### Constructors (`doc.entities()`) + +- **create_attribute_definition**(tag: &str, prompt: &str, default_value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> `Entity` — `CreateAttributeDefinition` + ## `Entity` (acadrust `Viewport`, collection `entities`) Paper-space viewport: a width×height window at center looking at view_target with view_height zoom. bounds = center ± w/2,h/2; transform moves center. diff --git a/crates/ocs_doc_api/src/gen/binding_schema.json b/crates/ocs_doc_api/src/gen/binding_schema.json index 69a2d72a2..ed300c3d8 100644 --- a/crates/ocs_doc_api/src/gen/binding_schema.json +++ b/crates/ocs_doc_api/src/gen/binding_schema.json @@ -335,6 +335,13 @@ ], "name": "Insert" }, + { + "acadrust_variant": "AttributeDefinition", + "collection": "entities", + "handle": "Entity", + "methods": [], + "name": "AttributeDefinition" + }, { "acadrust_variant": "Viewport", "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 d9dcd5e51..d88a3cf3f 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, DimensionAngularSpec, DimensionRadialSpec, DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec, SolidPrimitive, TextSpec, ViewportSpec}; +use crate::ops::{AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec, 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. @@ -52,4 +52,6 @@ pub enum Operation { CreateDimensionRadius(DimensionRadialSpec), CreateDimensionDiameter(DimensionRadialSpec), CreateDimensionAngular(DimensionAngularSpec), + // attribute definitions + CreateAttributeDefinition(AttributeDefinitionSpec), } diff --git a/crates/ocs_doc_api/src/ops.rs b/crates/ocs_doc_api/src/ops.rs index 69fb5f900..0fe193af2 100644 --- a/crates/ocs_doc_api/src/ops.rs +++ b/crates/ocs_doc_api/src/ops.rs @@ -136,6 +136,18 @@ pub struct DimensionAngularSpec { pub arc_location: [f64; 3], } +/// Construction spec for an `ATTDEF` (attribute definition): the tag, prompt, +/// and default value for an in-block attribute, at an insertion point. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AttributeDefinitionSpec { + pub tag: String, + pub prompt: String, + pub default_value: String, + pub insertion_point: [f64; 3], + pub height: f64, + pub rotation: f64, +} + /// 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. @@ -210,6 +222,7 @@ impl crate::gen::Operation { CreateDimensionRadius(_) => "CreateDimensionRadius", CreateDimensionDiameter(_) => "CreateDimensionDiameter", CreateDimensionAngular(_) => "CreateDimensionAngular", + CreateAttributeDefinition(_) => "CreateAttributeDefinition", } } } diff --git a/crates/ocs_doc_api/tests/mock_backend.rs b/crates/ocs_doc_api/tests/mock_backend.rs index f83592fee..1e8c46688 100644 --- a/crates/ocs_doc_api/tests/mock_backend.rs +++ b/crates/ocs_doc_api/tests/mock_backend.rs @@ -140,6 +140,12 @@ impl DocApiBackend for MockBackend { fn dimension_measurement(&self, id: ObjectId) -> ApiResult { self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id)) } + fn add_attribute_definition(&mut self, spec: &ocs_doc_api::ops::AttributeDefinitionSpec) -> ApiResult { + if spec.tag.is_empty() { + return Err(ApiError::validation("CreateAttributeDefinition", "empty tag")); + } + Ok(self.alloc("AttributeDefinition")) + } fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> 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 0d9418f43..0486d3ada 100644 --- a/crates/ocs_doc_api/tests/spec_wire_consistency.rs +++ b/crates/ocs_doc_api/tests/spec_wire_consistency.rs @@ -67,6 +67,9 @@ fn operation_variants() -> Vec { Operation::CreateDimensionRadius(ocs_doc_api::ops::DimensionRadialSpec { center: [0.0; 3], point: [0.0; 3] }), Operation::CreateDimensionDiameter(ocs_doc_api::ops::DimensionRadialSpec { center: [0.0; 3], point: [0.0; 3] }), Operation::CreateDimensionAngular(ocs_doc_api::ops::DimensionAngularSpec { vertex: [0.0; 3], first_point: [0.0; 3], second_point: [0.0; 3], arc_location: [0.0; 3] }), + Operation::CreateAttributeDefinition(ocs_doc_api::ops::AttributeDefinitionSpec { + tag: String::new(), prompt: String::new(), default_value: String::new(), insertion_point: [0.0; 3], height: 0.0, rotation: 0.0, + }), ]; // Derive the variant names by serializing a probe of each and reading the // bincode variant index -> name map is not available; instead match by @@ -159,6 +162,7 @@ const OPERATION_BASELINE: &[&str] = &[ "CreateDimensionRadius", "CreateDimensionDiameter", "CreateDimensionAngular", + "CreateAttributeDefinition", ]; /// Recorded baseline of the `Query` variant order. diff --git a/src/app/doc_api.rs b/src/app/doc_api.rs index 186b2f6ba..e771bc2eb 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -292,6 +292,20 @@ impl DocApiBackend for HostSession<'_> { }) } + fn add_attribute_definition(&mut self, spec: &ocs_doc_api::ops::AttributeDefinitionSpec) -> ApiResult { + use acadrust::entities::AttributeDefinition; + use acadrust::types::Vector3; + let mut att = AttributeDefinition::default(); + att.tag = spec.tag.clone(); + att.prompt = spec.prompt.clone(); + att.default_value = spec.default_value.clone(); + att.insertion_point = Vector3::new(spec.insertion_point[0], spec.insertion_point[1], spec.insertion_point[2]); + att.height = spec.height; + att.rotation = spec.rotation; + let handle = self.scene_mut().add_entity(EntityType::AttributeDefinition(att)); + Ok(handle_to_obj(handle)) + } + fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> { let handle = obj_to_handle(id); let entity = self.document().get_entity(handle).cloned().ok_or(ApiError::UnknownId(id))?; @@ -1148,6 +1162,30 @@ mod tests { } } + // ── Phase 3-ii: typed AttributeDefinition create ────────────────────────── + + #[test] + fn phase3ii_create_attribute_definition() { + let mut app = OpenCADStudio::new_for_test(); + let mut host = HostSession::new(&mut app, 0); + let attdef = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateAttributeDefinition( + ocs_doc_api::ops::AttributeDefinitionSpec { + tag: "DOOR_NO".into(), prompt: "Door number?".into(), default_value: "D-1".into(), + insertion_point: [5.0, 5.0, 0.0], height: 2.5, rotation: 0.0, + }))).unwrap()); + let handle = obj_to_handle(attdef); + let Some(EntityType::AttributeDefinition(a)) = host.document().get_entity(handle) else { panic!("attdef not found") }; + assert_eq!(a.tag, "DOOR_NO"); + assert_eq!(a.default_value, "D-1"); + assert!((a.height - 2.5).abs() < 1e-9); + // GetEntity reports the AttributeDefinition kind. + let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetEntity { id: attdef }])).unwrap(); + match &receipt.query_results[0] { + QueryResult::Entity(e) => assert_eq!(e.kind, "AttributeDefinition"), + other => panic!("expected entity, got {other:?}"), + } + } + // ── Phase 5 remaining: typed raster image create ───────────────────────── #[test] diff --git a/src/app/doc_api_convert.rs b/src/app/doc_api_convert.rs index bdbcb78a2..17be1df27 100644 --- a/src/app/doc_api_convert.rs +++ b/src/app/doc_api_convert.rs @@ -318,6 +318,8 @@ pub fn entity_kind_name(entity: &EntityType) -> &'static str { EntityType::Viewport(_) => "Viewport", EntityType::Text(_) => "Text", EntityType::MText(_) => "MText", + EntityType::AttributeDefinition(_) => "AttributeDefinition", + EntityType::AttributeEntity(_) => "AttributeEntity", // Media & misc (phase 5, read-mostly): named so GetEntity reports a real kind. EntityType::RasterImage(_) => "RasterImage", EntityType::Table(_) => "Table",