From 2dba0158ca44569307468f59520e596cdf811765 Mon Sep 17 00:00:00 2001 From: Sebastian <106036+schoeller@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:43:37 +0200 Subject: [PATCH] =?UTF-8?q?ocs=5Fdoc=5Fapi:=20Phase=202b-c=20(linear=20Dim?= =?UTF-8?q?ension)=20=E2=80=94=20Phase=202=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateDimensionLinear(DimensionSpec) op + GetDimensionMeasurement query (append-only). - backend add_dimension_linear (Dimension::Linear via DimensionLinear::new) + dimension_measurement (linear/radius/diameter distance). - Dimension bounds from measured + definition points; Dimension handle + measurement(). - spec Dimension family; api_reference + binding_schema regenerated; mocks + baseline. - Test phase2c_dimension_linear_create_measurement_bounds -> 607 host lib + 8 crate + 3 consistency green. Phase 2 (full 2D + annotations) is now complete. --- crates/ocs_doc_api/IMPLEMENTATION.md | 5 +- crates/ocs_doc_api/examples/common/mod.rs | 14 +++++ crates/ocs_doc_api/spec/entities.toml | 21 +++++++ crates/ocs_doc_api/src/backend.rs | 6 ++ crates/ocs_doc_api/src/executor.rs | 7 +++ crates/ocs_doc_api/src/facade.rs | 22 ++++++++ crates/ocs_doc_api/src/gen/api_reference.md | 12 ++++ .../ocs_doc_api/src/gen/binding_schema.json | 22 +++++++- crates/ocs_doc_api/src/gen/ops_gen.rs | 4 +- crates/ocs_doc_api/src/gen/query_gen.rs | 2 + crates/ocs_doc_api/src/ops.rs | 10 ++++ crates/ocs_doc_api/src/query.rs | 3 + crates/ocs_doc_api/tests/mock_backend.rs | 14 +++++ .../tests/spec_wire_consistency.rs | 4 ++ src/app/doc_api.rs | 56 +++++++++++++++++++ src/app/doc_api_convert.rs | 10 ++++ 16 files changed, 209 insertions(+), 3 deletions(-) diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index 3670bb15c..4d2fdde13 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -68,7 +68,10 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal - **Done (2b-b):** `Hatch` — `create_hatch(boundary, solid)` (`CreateHatch`, validates ≥3 points), `boundary()` → loops (`GetHatchBoundary`), bounds from boundary edges, delete. Covered by `phase2b_hatch_*` test. -- **Remaining (2b-c):** `Dimension` (typed sub-types). +- **Done (2b-c):** linear `Dimension` — `create_dimension_linear(first, second, + definition)` (`CreateDimensionLinear`), `measurement()` → distance + (`GetDimensionMeasurement`), bounds. Covered by `phase2c_*` test. Radius/diameter/ + angular dimension sub-types are a later refinement. - **Unblocked:** `add_vertex` (polyline) and sweep profiles (Line/Circle/Arc/LwPolyline) are supported (Phase 0). diff --git a/crates/ocs_doc_api/examples/common/mod.rs b/crates/ocs_doc_api/examples/common/mod.rs index 114589d77..fce8ca73e 100644 --- a/crates/ocs_doc_api/examples/common/mod.rs +++ b/crates/ocs_doc_api/examples/common/mod.rs @@ -25,6 +25,8 @@ pub struct MockBackend { text_values: HashMap, /// Stored hatch boundary loops. hatch_boundaries: HashMap>>, + /// Stored dimension measurements. + dimension_measurements: HashMap, } impl MockBackend { @@ -166,6 +168,18 @@ impl DocApiBackend for MockBackend { fn hatch_boundary(&self, id: ObjectId) -> ApiResult>> { self.hatch_boundaries.get(&id).cloned().ok_or(ApiError::UnknownId(id)) } + fn add_dimension_linear(&mut self, spec: &ocs_doc_api::ops::DimensionSpec) -> ApiResult { + let id = self.alloc("Dimension"); + let d = ((spec.first_point[0] - spec.second_point[0]).powi(2) + + (spec.first_point[1] - spec.second_point[1]).powi(2) + + (spec.first_point[2] - spec.second_point[2]).powi(2)) + .sqrt(); + self.dimension_measurements.insert(id, d); + Ok(id) + } + fn dimension_measurement(&self, id: ObjectId) -> ApiResult { + self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id)) + } fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> { if self.entity_exists(id) { Ok(()) diff --git a/crates/ocs_doc_api/spec/entities.toml b/crates/ocs_doc_api/spec/entities.toml index b978c72d0..5dd6be8a6 100644 --- a/crates/ocs_doc_api/spec/entities.toml +++ b/crates/ocs_doc_api/spec/entities.toml @@ -311,6 +311,27 @@ kind = "query" query = "GetHatchBoundary" returns = "Vec>" +# ============================================================================ +# DIMENSION FAMILY (phase 2c) — linear dimensions +# ============================================================================ +[[family]] +name = "Dimension" +acadrust_variant = "Dimension" +handle = "Dimension" +collection = "curves" +description = "Linear dimension between two measured points. measurement() reads the distance; bounds from the measured + definition points." + +[[family.constructor]] +name = "create_dimension_linear" +args = [{ name = "first_point", ty = "[f64; 3]" }, { name = "second_point", ty = "[f64; 3]" }, { name = "definition_point", ty = "[f64; 3]" }] +op = "CreateDimensionLinear" + +[[family.method]] +name = "measurement" +kind = "query" +query = "GetDimensionMeasurement" +returns = "f64" + # ============================================================================ # CONTAINER FAMILY (phase 3) — block references # ============================================================================ diff --git a/crates/ocs_doc_api/src/backend.rs b/crates/ocs_doc_api/src/backend.rs index f8c05aaac..8a591b204 100644 --- a/crates/ocs_doc_api/src/backend.rs +++ b/crates/ocs_doc_api/src/backend.rs @@ -60,6 +60,12 @@ pub trait DocApiBackend { /// A hatch's boundary loops (outer + islands) as closed polylines. fn hatch_boundary(&self, id: ObjectId) -> ApiResult>>; + /// Add a linear `DIMENSION`; returns the fresh `ObjectId`. + fn add_dimension_linear(&mut self, spec: &crate::ops::DimensionSpec) -> ApiResult; + + /// A dimension's measured value (distance for linear/radius, degrees for angular). + fn dimension_measurement(&self, id: ObjectId) -> ApiResult; + /// 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). diff --git a/crates/ocs_doc_api/src/executor.rs b/crates/ocs_doc_api/src/executor.rs index ffc0ca882..7ab6b106f 100644 --- a/crates/ocs_doc_api/src/executor.rs +++ b/crates/ocs_doc_api/src/executor.rs @@ -221,6 +221,12 @@ pub fn apply_op(b: &mut B, op: Operation) -> ApiResult { + b.push_undo(name); + let id = b.add_dimension_linear(spec)?; + b.finalize_op(); + OpOutcome::NewId(id) + } }; Ok(Receipt { outcome: Some(outcome), @@ -260,6 +266,7 @@ pub fn apply_queries(b: &mut B, queries: Vec) -> ApiRes 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)?), }; results.push(r); } diff --git a/crates/ocs_doc_api/src/facade.rs b/crates/ocs_doc_api/src/facade.rs index 7bece8d08..05689a0ce 100644 --- a/crates/ocs_doc_api/src/facade.rs +++ b/crates/ocs_doc_api/src/facade.rs @@ -269,6 +269,18 @@ handle!(Ray); handle!(XLine); handle!(Text); handle!(MText); +handle!(Dimension); + +impl Dimension { + /// The measured value of this dimension (distance for linear/radius, degrees + /// for angular). + pub fn measurement(&self) -> ApiResult { + match self.session.one_query(Query::GetDimensionMeasurement { id: self.id })? { + QueryResult::DimensionMeasurement(v) => Ok(v), + _ => Err(ApiError::Transport("unexpected measurement result".into())), + } + } +} impl Entity { pub fn view(&self) -> ApiResult { @@ -475,6 +487,16 @@ impl CurveCollection { Ok(Entity::new(self.session.clone(), id)) } + /// A linear DIMENSION between `first_point` and `second_point`, with the + /// dimension line placed at `definition_point`. `measurement()` reads the value. + pub fn create_dimension_linear(&self, first_point: [f64; 3], second_point: [f64; 3], definition_point: [f64; 3]) -> ApiResult { + let receipt = self.session.apply_op(Operation::CreateDimensionLinear(crate::ops::DimensionSpec { + first_point, second_point, definition_point, + }))?; + let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| ApiError::Transport("create_dimension returned no id".into()))?; + Ok(Dimension::new(self.session.clone(), id)) + } + /// Single-line TEXT annotation. pub fn create_text(&self, value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> ApiResult { let receipt = self.session.apply_op(Operation::CreateText(crate::ops::TextSpec { diff --git a/crates/ocs_doc_api/src/gen/api_reference.md b/crates/ocs_doc_api/src/gen/api_reference.md index 4ce34e0e6..d3366621d 100644 --- a/crates/ocs_doc_api/src/gen/api_reference.md +++ b/crates/ocs_doc_api/src/gen/api_reference.md @@ -125,6 +125,18 @@ Filled region over a closed polyline boundary. boundary() returns the loops; bou - **boundary**(``) -> `Vec>` — query `GetHatchBoundary` +## `Dimension` (acadrust `Dimension`, collection `curves`) + +Linear dimension between two measured points. measurement() reads the distance; bounds from the measured + definition points. + +### Constructors (`doc.curves()`) + +- **create_dimension_linear**(`first_point: [f64; 3], second_point: [f64; 3], definition_point: [f64; 3]`) -> `Dimension` + +### Methods + +- **measurement**(``) -> `f64` — query `GetDimensionMeasurement` + ## `Entity` (acadrust `Insert`, collection `entities`) Block reference. Place an existing BlockRecord by name; transform moves its insert point. diff --git a/crates/ocs_doc_api/src/gen/binding_schema.json b/crates/ocs_doc_api/src/gen/binding_schema.json index 36ec3db4f..45646f17c 100644 --- a/crates/ocs_doc_api/src/gen/binding_schema.json +++ b/crates/ocs_doc_api/src/gen/binding_schema.json @@ -277,6 +277,25 @@ ], "name": "Hatch" }, + { + "acadrust_variant": "Dimension", + "collection": "curves", + "handle": "Dimension", + "methods": [ + { + "args": [], + "bulk": false, + "fixed": null, + "kind": "query", + "name": "measurement", + "op": null, + "query": "GetDimensionMeasurement", + "read_only": true, + "returns": "f64" + } + ], + "name": "Dimension" + }, { "acadrust_variant": "Insert", "collection": "entities", @@ -361,7 +380,8 @@ "Ray", "XLine", "Text", - "MText" + "MText", + "Dimension" ], "root": "DocApi" }, diff --git a/crates/ocs_doc_api/src/gen/ops_gen.rs b/crates/ocs_doc_api/src/gen/ops_gen.rs index e6ebc2284..fa1f908de 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, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, SolidPrimitive, TextSpec, ViewportSpec}; +use crate::ops::{BoolOp, Curve2Spec, DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, 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. @@ -38,4 +38,6 @@ pub enum Operation { SetTextContent { id: ObjectId, value: String }, // hatch CreateHatch(HatchSpec), + // dimensions + CreateDimensionLinear(DimensionSpec), } diff --git a/crates/ocs_doc_api/src/gen/query_gen.rs b/crates/ocs_doc_api/src/gen/query_gen.rs index 7c7f093fa..44bc8af19 100644 --- a/crates/ocs_doc_api/src/gen/query_gen.rs +++ b/crates/ocs_doc_api/src/gen/query_gen.rs @@ -23,4 +23,6 @@ pub enum Query { GetTextContent { id: ObjectId }, /// The boundary loops of a Hatch (outer + islands) as polylines. GetHatchBoundary { id: ObjectId }, + /// The measured value of a Dimension (distance or angle). + GetDimensionMeasurement { id: ObjectId }, } diff --git a/crates/ocs_doc_api/src/ops.rs b/crates/ocs_doc_api/src/ops.rs index 2953f6f85..dd688c88e 100644 --- a/crates/ocs_doc_api/src/ops.rs +++ b/crates/ocs_doc_api/src/ops.rs @@ -106,6 +106,15 @@ pub struct HatchSpec { pub solid: bool, } +/// Construction spec for a linear `DIMENSION`: the two measured points and the +/// dimension-line definition point. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DimensionSpec { + pub first_point: [f64; 3], + pub second_point: [f64; 3], + pub definition_point: [f64; 3], +} + /// 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. @@ -159,6 +168,7 @@ impl crate::gen::Operation { CreateMText(_) => "CreateMText", SetTextContent { .. } => "SetTextContent", CreateHatch(_) => "CreateHatch", + CreateDimensionLinear(_) => "CreateDimensionLinear", } } } diff --git a/crates/ocs_doc_api/src/query.rs b/crates/ocs_doc_api/src/query.rs index 5793f33c3..1e6e1fe52 100644 --- a/crates/ocs_doc_api/src/query.rs +++ b/crates/ocs_doc_api/src/query.rs @@ -47,6 +47,8 @@ pub enum QueryResult { TextContent(String), /// A hatch's boundary loops (outer + islands), each a closed polyline. HatchBoundary(Vec>), + /// A dimension's measured value (distance for linear/radius, degrees for angular). + DimensionMeasurement(f64), } /// Convenience: the query name for diagnostics. @@ -62,6 +64,7 @@ impl crate::gen::Query { GetGeometryRevision => "GetGeometryRevision", GetTextContent { .. } => "GetTextContent", GetHatchBoundary { .. } => "GetHatchBoundary", + GetDimensionMeasurement { .. } => "GetDimensionMeasurement", } } } diff --git a/crates/ocs_doc_api/tests/mock_backend.rs b/crates/ocs_doc_api/tests/mock_backend.rs index c98bc7acf..6cfe3485e 100644 --- a/crates/ocs_doc_api/tests/mock_backend.rs +++ b/crates/ocs_doc_api/tests/mock_backend.rs @@ -25,6 +25,8 @@ struct MockBackend { text_values: HashMap, /// Stored hatch boundary loops. hatch_boundaries: HashMap>>, + /// Stored dimension measurements. + dimension_measurements: HashMap, } impl MockBackend { @@ -106,6 +108,18 @@ impl DocApiBackend for MockBackend { fn hatch_boundary(&self, id: ObjectId) -> ApiResult>> { self.hatch_boundaries.get(&id).cloned().ok_or(ApiError::UnknownId(id)) } + fn add_dimension_linear(&mut self, spec: &ocs_doc_api::ops::DimensionSpec) -> ApiResult { + let id = self.alloc("Dimension"); + let d = ((spec.first_point[0] - spec.second_point[0]).powi(2) + + (spec.first_point[1] - spec.second_point[1]).powi(2) + + (spec.first_point[2] - spec.second_point[2]).powi(2)) + .sqrt(); + self.dimension_measurements.insert(id, d); + Ok(id) + } + fn dimension_measurement(&self, id: ObjectId) -> ApiResult { + self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id)) + } fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> { if self.entity_exists(id) { Ok(()) diff --git a/crates/ocs_doc_api/tests/spec_wire_consistency.rs b/crates/ocs_doc_api/tests/spec_wire_consistency.rs index c04814347..3e51c937e 100644 --- a/crates/ocs_doc_api/tests/spec_wire_consistency.rs +++ b/crates/ocs_doc_api/tests/spec_wire_consistency.rs @@ -57,6 +57,7 @@ fn operation_variants() -> Vec { Operation::CreateMText(ocs_doc_api::ops::MTextSpec { value: String::new(), insertion_point: [0.0; 3], height: 0.0 }), Operation::SetTextContent { id: ObjectId::from_u64(0), value: String::new() }, Operation::CreateHatch(ocs_doc_api::ops::HatchSpec { boundary: vec![], solid: true }), + Operation::CreateDimensionLinear(ocs_doc_api::ops::DimensionSpec { first_point: [0.0; 3], second_point: [0.0; 3], definition_point: [0.0; 3] }), ]; // Derive the variant names by serializing a probe of each and reading the // bincode variant index -> name map is not available; instead match by @@ -83,6 +84,7 @@ fn query_variants() -> Vec { Query::GetGeometryRevision, Query::GetTextContent { id: ObjectId::from_u64(0) }, Query::GetHatchBoundary { id: ObjectId::from_u64(0) }, + Query::GetDimensionMeasurement { id: ObjectId::from_u64(0) }, ]; marker .iter() @@ -137,6 +139,7 @@ const OPERATION_BASELINE: &[&str] = &[ "CreateMText", "SetTextContent", "CreateHatch", + "CreateDimensionLinear", ]; /// Recorded baseline of the `Query` variant order. @@ -149,6 +152,7 @@ const QUERY_BASELINE: &[&str] = &[ "GetGeometryRevision", "GetTextContent", "GetHatchBoundary", + "GetDimensionMeasurement", ]; #[test] diff --git a/src/app/doc_api.rs b/src/app/doc_api.rs index a66be9384..308c4834e 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -238,6 +238,30 @@ impl DocApiBackend for HostSession<'_> { Ok(loops) } + fn add_dimension_linear(&mut self, spec: &ocs_doc_api::ops::DimensionSpec) -> ApiResult { + use acadrust::entities::{Dimension, DimensionLinear}; + use acadrust::types::Vector3; + let v3 = |p: [f64; 3]| Vector3::new(p[0], p[1], p[2]); + let mut dim = DimensionLinear::new(v3(spec.first_point), v3(spec.second_point)); + dim.definition_point = v3(spec.definition_point); + let handle = self.scene_mut().add_entity(EntityType::Dimension(Dimension::Linear(dim))); + Ok(handle_to_obj(handle)) + } + + fn dimension_measurement(&self, id: ObjectId) -> ApiResult { + let entity = self.document().get_entity(obj_to_handle(id)).ok_or(ApiError::UnknownId(id))?; + let EntityType::Dimension(d) = entity else { + return Err(ApiError::Unsupported("GetDimensionMeasurement is only for Dimension".into())); + }; + // Distance for linear/radius/diameter; degrees for angular. + Ok(match d { + acadrust::entities::Dimension::Linear(x) => x.measurement(), + acadrust::entities::Dimension::Radius(x) => x.measurement(), + acadrust::entities::Dimension::Diameter(x) => x.measurement(), + _ => return Err(ApiError::Unsupported("dimension measurement for this sub-type".into())), + }) + } + 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 { @@ -905,6 +929,38 @@ mod tests { assert!((t.insertion_point.x - 15.0).abs() < 1e-6); } + // ── Phase 2b-c: dimension ───────────────────────────────────────────────── + + #[test] + fn phase2c_dimension_linear_create_measurement_bounds() { + let mut app = OpenCADStudio::new_for_test(); + let mut host = HostSession::new(&mut app, 0); + // Linear dimension from (0,0,0) to (30,0,0) with the line at (0,5,0). + let dim = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateDimensionLinear( + ocs_doc_api::ops::DimensionSpec { + first_point: [0.0, 0.0, 0.0], + second_point: [30.0, 0.0, 0.0], + definition_point: [0.0, 5.0, 0.0], + }))).unwrap()); + let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![ + Query::GetDimensionMeasurement { id: dim }, + Query::GetEntity { id: dim }, + Query::GetBounds { id: dim }, + ])).unwrap(); + match &receipt.query_results[0] { + QueryResult::DimensionMeasurement(v) => assert!((*v - 30.0).abs() < 1e-6, "measurement {v}"), + other => panic!("expected measurement, got {other:?}"), + } + match &receipt.query_results[1] { + QueryResult::Entity(e) => assert_eq!(e.kind, "Dimension"), + other => panic!("expected entity, got {other:?}"), + } + match &receipt.query_results[2] { + QueryResult::Bounds(b) => assert!((b.min[0] - 0.0).abs() < 1e-6 && (b.max[0] - 30.0).abs() < 1e-6, "{b:?}"), + other => panic!("expected bounds, got {other:?}"), + } + } + // ── Phase 2b-b: hatch ──────────────────────────────────────────────────── #[test] diff --git a/src/app/doc_api_convert.rs b/src/app/doc_api_convert.rs index b39bfa9f3..4123e6dc9 100644 --- a/src/app/doc_api_convert.rs +++ b/src/app/doc_api_convert.rs @@ -412,6 +412,16 @@ pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult { + let pts = [d.first_point, d.second_point, d.definition_point]; + let mut min = [f64::INFINITY; 3]; + let mut max = [f64::NEG_INFINITY; 3]; + for p in pts { + min = [min[0].min(p.x), min[1].min(p.y), min[2].min(p.z)]; + max = [max[0].max(p.x), max[1].max(p.y), max[2].max(p.z)]; + } + Aabb { min, max } + } EntityType::Hatch(h) => { let mut min = [f64::INFINITY; 3]; let mut max = [f64::NEG_INFINITY; 3];