forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 3-ii — typed AttributeDefinition create (Phase 3 complete)
- 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.
This commit is contained in:
1 parent
8d8c120880
commit
f8ae54a351
14 files changed
+128
-3
No files matched your search
@@ -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)` →
|
||||
|
||||
@@ -200,6 +200,12 @@ impl DocApiBackend for MockBackend {
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||
self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn add_attribute_definition(&mut self, spec: &ocs_doc_api::ops::AttributeDefinitionSpec) -> ApiResult<ObjectId> {
|
||||
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));
|
||||
|
||||
@@ -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
|
||||
# ============================================================================
|
||||
|
||||
@@ -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<ObjectId>;
|
||||
|
||||
/// Add an `ATTDEF` (in-block attribute definition); returns the fresh `ObjectId`.
|
||||
fn add_attribute_definition(&mut self, spec: &crate::ops::AttributeDefinitionSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// A dimension's measured value (distance for linear/radius, degrees for angular).
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64>;
|
||||
|
||||
|
||||
@@ -287,6 +287,12 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateAttributeDefinition(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_attribute_definition(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
};
|
||||
Ok(Receipt {
|
||||
outcome: Some(outcome),
|
||||
|
||||
@@ -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<Entity> {
|
||||
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<Entity> {
|
||||
let receipt = self.session.apply_op(Operation::CreateInsert(crate::ops::InsertSpec {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -335,6 +335,13 @@
|
||||
],
|
||||
"name": "Insert"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "AttributeDefinition",
|
||||
"collection": "entities",
|
||||
"handle": "Entity",
|
||||
"methods": [],
|
||||
"name": "AttributeDefinition"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "Viewport",
|
||||
"collection": "entities",
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,12 @@ impl DocApiBackend for MockBackend {
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||
self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn add_attribute_definition(&mut self, spec: &ocs_doc_api::ops::AttributeDefinitionSpec) -> ApiResult<ObjectId> {
|
||||
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));
|
||||
|
||||
@@ -67,6 +67,9 @@ fn operation_variants() -> Vec<String> {
|
||||
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.
|
||||
|
||||
@@ -292,6 +292,20 @@ impl DocApiBackend for HostSession<'_> {
|
||||
})
|
||||
}
|
||||
|
||||
fn add_attribute_definition(&mut self, spec: &ocs_doc_api::ops::AttributeDefinitionSpec) -> ApiResult<ObjectId> {
|
||||
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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user