ocs_doc_api: Phase 3 attributes + nested block traversal

- SetAttribute op + GetAttributes/GetBlockEntities queries (append-only).
- backend set_attribute (add/update Insert attribute), attributes (tag,value pairs),
  block_entities (read-only traversal via BlockRecord.entity_handles -> id+kind+bounds).
- facade Entity::set_attribute/attributes, Document::block_entities.
- spec Insert methods; api_reference + binding_schema regenerated; mocks + baseline.
- Query enum: drop Copy (GetBlockEntities carries String).
- Test phase3_attributes_and_block_traversal -> 608 host lib + 3 consistency green.
This commit is contained in:
Sebastian committed 2026-09-06 10:02:23 +02:00
1 parent 2dba0158ca
commit 67655add72
15 files changed
+267 -14

No files matched your search

+9
View File
@@ -66,6 +66,15 @@ pub trait DocApiBackend {
/// A dimension's measured value (distance for linear/radius, degrees for angular).
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64>;
/// Set an attribute `value` for `tag` on an insert (adds if absent). Insert-only.
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()>;
/// An insert's attributes as (tag, value) pairs. Insert-only.
fn attributes(&self, id: ObjectId) -> ApiResult<Vec<(String, String)>>;
/// The entities inside a block definition (read-only traversal), as views.
fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>>;
/// 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).
+22 -10
View File
@@ -227,6 +227,16 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
b.finalize_op();
OpOutcome::NewId(id)
}
Operation::SetAttribute { id, tag, value } => {
b.can_modify(*id).map_err(|e| match e {
ApiError::UnknownId(_) => ApiError::validation(name, format!("unknown ObjectId {id:?}")),
other => other,
})?;
b.push_undo(name);
b.set_attribute(*id, tag, value)?;
b.finalize_op();
OpOutcome::Updated(*id)
}
};
Ok(Receipt {
outcome: Some(outcome),
@@ -253,20 +263,22 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
}
let mut results = Vec::with_capacity(queries.len());
for q in &queries {
let r = match *q {
Query::GetEntity { id } => QueryResult::Entity(b.get_entity(id)?),
Query::GetBounds { id } => QueryResult::Bounds(b.bounds(id)?),
Query::GetCentroid { id } => QueryResult::Centroid(b.centroid(id)?),
Query::GetVolume { id } => QueryResult::Volume(b.volume(id)?),
let r = match q {
Query::GetEntity { id } => QueryResult::Entity(b.get_entity(*id)?),
Query::GetBounds { id } => QueryResult::Bounds(b.bounds(*id)?),
Query::GetCentroid { id } => QueryResult::Centroid(b.centroid(*id)?),
Query::GetVolume { id } => QueryResult::Volume(b.volume(*id)?),
Query::GetIntersects { a, b: bid } => {
let ba = b.bounds(a)?;
let bb = b.bounds(bid)?;
let ba = b.bounds(*a)?;
let bb = b.bounds(*bid)?;
QueryResult::Intersects(ba.overlaps(&bb))
}
Query::GetGeometryRevision => QueryResult::Revision(b.revision()),
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(id)?),
Query::GetHatchBoundary { id } => QueryResult::HatchBoundary(b.hatch_boundary(id)?),
Query::GetDimensionMeasurement { id } => QueryResult::DimensionMeasurement(b.dimension_measurement(id)?),
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(*id)?),
Query::GetHatchBoundary { id } => QueryResult::HatchBoundary(b.hatch_boundary(*id)?),
Query::GetDimensionMeasurement { id } => QueryResult::DimensionMeasurement(b.dimension_measurement(*id)?),
Query::GetAttributes { id } => QueryResult::Attributes(b.attributes(*id)?),
Query::GetBlockEntities { block_name } => QueryResult::BlockEntities(b.block_entities(block_name)?),
};
results.push(r);
}
+20
View File
@@ -128,6 +128,14 @@ impl Document {
}
}
/// Read-only traversal of a block definition's entities (ids + kinds + bounds).
pub fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>> {
match self.session.one_query(Query::GetBlockEntities { block_name: block_name.to_string() })? {
QueryResult::BlockEntities(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected block-entities result".into())),
}
}
/// Batch of read-only queries in ONE round-trip (safe: no mutation/undo).
/// The closure records queries on a [`QueryBatch`]; the results are returned
/// in the same order as a [`QueryResults`] view the caller destructures.
@@ -289,6 +297,18 @@ impl Entity {
_ => Err(ApiError::Transport("unexpected entity result".into())),
}
}
/// This insert's attributes as (tag, value) pairs. Insert-only.
pub fn attributes(&self) -> ApiResult<Vec<(String, String)>> {
match self.session.one_query(Query::GetAttributes { id: self.id })? {
QueryResult::Attributes(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected attributes result".into())),
}
}
/// Set an attribute `value` for `tag` on this insert (adds if absent). One undo step.
pub fn set_attribute(&self, tag: &str, value: &str) -> ApiResult<()> {
self.session.apply_op(Operation::SetAttribute { id: self.id, tag: tag.to_string(), value: value.to_string() })?;
Ok(())
}
pub fn as_solid(&self) -> Option<Solid> {
// Typed downcast is validated by the view's kind.
matches!(self.view().ok()?.kind.as_str(), "Solid3D").then(|| Solid::new(self.session.clone(), self.id))
@@ -145,6 +145,11 @@ Block reference. Place an existing BlockRecord by name; transform moves its inse
- **create_insert**(`block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64`) -> `Entity`
### Methods
- **attributes**(``) -> `Vec<(String, String)>` — query `GetAttributes`
- **set_attribute**(`tag: &str, value: &str`) -> `()` — op `SetAttribute`
## `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.
+33 -1
View File
@@ -300,7 +300,39 @@
"acadrust_variant": "Insert",
"collection": "entities",
"handle": "Entity",
"methods": [],
"methods": [
{
"args": [],
"bulk": false,
"fixed": null,
"kind": "query",
"name": "attributes",
"op": null,
"query": "GetAttributes",
"read_only": true,
"returns": "Vec<(String, String)>"
},
{
"args": [
{
"name": "tag",
"ty": "&str"
},
{
"name": "value",
"ty": "&str"
}
],
"bulk": false,
"fixed": null,
"kind": "op",
"name": "set_attribute",
"op": "SetAttribute",
"query": null,
"read_only": false,
"returns": "()"
}
],
"name": "Insert"
},
{
+2
View File
@@ -40,4 +40,6 @@ pub enum Operation {
CreateHatch(HatchSpec),
// dimensions
CreateDimensionLinear(DimensionSpec),
// attributes
SetAttribute { id: ObjectId, tag: String, value: String },
}
+6 -1
View File
@@ -9,7 +9,8 @@ use crate::id::ObjectId;
/// A typed read query (plan §5). Read-only: no mutation, no undo, no revision
/// bump. Safe to batch (`EnvelopeBody::Queries`). Append new variants at the END only.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
/// (Not `Copy`: some variants carry owned data like a block name.)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Query {
GetEntity { id: ObjectId },
@@ -25,4 +26,8 @@ pub enum Query {
GetHatchBoundary { id: ObjectId },
/// The measured value of a Dimension (distance or angle).
GetDimensionMeasurement { id: ObjectId },
/// An insert's attributes as (tag, value) pairs.
GetAttributes { id: ObjectId },
/// The entity ids + kinds inside a block definition (read-only traversal).
GetBlockEntities { block_name: String },
}
+1
View File
@@ -169,6 +169,7 @@ impl crate::gen::Operation {
SetTextContent { .. } => "SetTextContent",
CreateHatch(_) => "CreateHatch",
CreateDimensionLinear(_) => "CreateDimensionLinear",
SetAttribute { .. } => "SetAttribute",
}
}
}
+6
View File
@@ -49,6 +49,10 @@ pub enum QueryResult {
HatchBoundary(Vec<Vec<[f64; 2]>>),
/// A dimension's measured value (distance for linear/radius, degrees for angular).
DimensionMeasurement(f64),
/// An insert's attributes as (tag, value) pairs.
Attributes(Vec<(String, String)>),
/// The entities inside a block definition (read-only traversal).
BlockEntities(Vec<EntityView>),
}
/// Convenience: the query name for diagnostics.
@@ -65,6 +69,8 @@ impl crate::gen::Query {
GetTextContent { .. } => "GetTextContent",
GetHatchBoundary { .. } => "GetHatchBoundary",
GetDimensionMeasurement { .. } => "GetDimensionMeasurement",
GetAttributes { .. } => "GetAttributes",
GetBlockEntities { .. } => "GetBlockEntities",
}
}
}