From 8d8c1208806cede2b7b87d285c9903aa950206bf Mon Sep 17 00:00:00 2001 From: Sebastian <106036+schoeller@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:31:23 +0200 Subject: [PATCH] =?UTF-8?q?ocs=5Fdoc=5Fapi:=20Phase=202c-ii=20=E2=80=94=20?= =?UTF-8?q?dimension=20sub-types=20(radius/diameter/angular)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateDimensionRadius/CreateDimensionDiameter (DimensionRadialSpec) + CreateDimensionAngular (DimensionAngularSpec) ops (append-only). - backend add_dimension_radius/diameter (DimensionRadius/DimensionDiameter::new) + add_dimension_angular (DimensionAngular3Pt::new computes angle); dimension_measurement returns degrees for angular sub-types. - facade create_dimension_radius/diameter/angular; spec constructors; api_reference + binding_schema regenerated; mocks + baseline. - Test phase2cii_dimension_radius_diameter_angular -> 613 host lib + 3 consistency green. --- crates/ocs_doc_api/IMPLEMENTATION.md | 7 +- crates/ocs_doc_api/examples/common/mod.rs | 14 ++++ crates/ocs_doc_api/spec/entities.toml | 15 +++++ crates/ocs_doc_api/src/backend.rs | 9 +++ crates/ocs_doc_api/src/executor.rs | 18 +++++ crates/ocs_doc_api/src/facade.rs | 26 +++++++ crates/ocs_doc_api/src/gen/api_reference.md | 3 + crates/ocs_doc_api/src/gen/ops_gen.rs | 6 +- crates/ocs_doc_api/src/ops.rs | 23 +++++++ crates/ocs_doc_api/tests/mock_backend.rs | 14 ++++ .../tests/spec_wire_consistency.rs | 6 ++ src/app/doc_api.rs | 67 +++++++++++++++++++ 12 files changed, 205 insertions(+), 3 deletions(-) diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index c9fe0a5eb..a1af7e731 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -70,8 +70,11 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal boundary edges, delete. Covered by `phase2b_hatch_*` test. - **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. + (`GetDimensionMeasurement`), bounds. Covered by `phase2c_*` test. +- **Done (2c-ii, sub-types):** `create_dimension_radius` / `create_dimension_diameter` + (radial chord) and `create_dimension_angular` (vertex + 2 legs) → radial/degrees + measurements. Covered by `phase2cii_*` test. (`Angular2Ln` variant handled in + `measurement`; a dedicated constructor is 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 bb47211c0..c6fa42fe8 100644 --- a/crates/ocs_doc_api/examples/common/mod.rs +++ b/crates/ocs_doc_api/examples/common/mod.rs @@ -183,6 +183,20 @@ impl DocApiBackend for MockBackend { self.dimension_measurements.insert(id, d); Ok(id) } + fn add_dimension_radius(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult { + let id = self.alloc("Dimension"); + let d = ((spec.center[0] - spec.point[0]).powi(2) + (spec.center[1] - spec.point[1]).powi(2) + (spec.center[2] - spec.point[2]).powi(2)).sqrt(); + self.dimension_measurements.insert(id, d); + Ok(id) + } + fn add_dimension_diameter(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult { + self.add_dimension_radius(spec) + } + fn add_dimension_angular(&mut self, _spec: &ocs_doc_api::ops::DimensionAngularSpec) -> ApiResult { + let id = self.alloc("Dimension"); + self.dimension_measurements.insert(id, 90.0); + Ok(id) + } fn dimension_measurement(&self, id: ObjectId) -> ApiResult { self.dimension_measurements.get(&id).copied().ok_or(ApiError::UnknownId(id)) } diff --git a/crates/ocs_doc_api/spec/entities.toml b/crates/ocs_doc_api/spec/entities.toml index 16f1ec48f..969a3893e 100644 --- a/crates/ocs_doc_api/spec/entities.toml +++ b/crates/ocs_doc_api/spec/entities.toml @@ -331,6 +331,21 @@ 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.constructor]] +name = "create_dimension_radius" +args = [{ name = "center", ty = "[f64; 3]" }, { name = "point", ty = "[f64; 3]" }] +op = "CreateDimensionRadius" + +[[family.constructor]] +name = "create_dimension_diameter" +args = [{ name = "center", ty = "[f64; 3]" }, { name = "point", ty = "[f64; 3]" }] +op = "CreateDimensionDiameter" + +[[family.constructor]] +name = "create_dimension_angular" +args = [{ name = "vertex", ty = "[f64; 3]" }, { name = "first_point", ty = "[f64; 3]" }, { name = "second_point", ty = "[f64; 3]" }, { name = "arc_location", ty = "[f64; 3]" }] +op = "CreateDimensionAngular" + [[family.method]] name = "measurement" kind = "query" diff --git a/crates/ocs_doc_api/src/backend.rs b/crates/ocs_doc_api/src/backend.rs index 47991ec74..3f1af4237 100644 --- a/crates/ocs_doc_api/src/backend.rs +++ b/crates/ocs_doc_api/src/backend.rs @@ -63,6 +63,15 @@ pub trait DocApiBackend { /// Add a linear `DIMENSION`; returns the fresh `ObjectId`. fn add_dimension_linear(&mut self, spec: &crate::ops::DimensionSpec) -> ApiResult; + /// Add a radial `DIMENSION`; returns the fresh `ObjectId`. + fn add_dimension_radius(&mut self, spec: &crate::ops::DimensionRadialSpec) -> ApiResult; + + /// Add a diameter `DIMENSION`; returns the fresh `ObjectId`. + fn add_dimension_diameter(&mut self, spec: &crate::ops::DimensionRadialSpec) -> ApiResult; + + /// Add a 3-point angular `DIMENSION`; returns the fresh `ObjectId`. + fn add_dimension_angular(&mut self, spec: &crate::ops::DimensionAngularSpec) -> 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 7af52512a..ee22506d8 100644 --- a/crates/ocs_doc_api/src/executor.rs +++ b/crates/ocs_doc_api/src/executor.rs @@ -269,6 +269,24 @@ pub fn apply_op(b: &mut B, op: Operation) -> ApiResult { + b.push_undo(name); + let id = b.add_dimension_radius(spec)?; + b.finalize_op(); + OpOutcome::NewId(id) + } + Operation::CreateDimensionDiameter(spec) => { + b.push_undo(name); + let id = b.add_dimension_diameter(spec)?; + b.finalize_op(); + OpOutcome::NewId(id) + } + Operation::CreateDimensionAngular(spec) => { + b.push_undo(name); + let id = b.add_dimension_angular(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 43cb6d75a..d0874dac9 100644 --- a/crates/ocs_doc_api/src/facade.rs +++ b/crates/ocs_doc_api/src/facade.rs @@ -539,6 +539,32 @@ impl CurveCollection { Ok(Entity::new(self.session.clone(), id)) } + /// A radial DIMENSION for the circle centered at `center` through `point`. + pub fn create_dimension_radius(&self, center: [f64; 3], point: [f64; 3]) -> ApiResult { + self.create_dim_radial(Operation::CreateDimensionRadius, center, point) + } + /// A diameter DIMENSION for the circle with chord points `center`/`point`. + pub fn create_dimension_diameter(&self, center: [f64; 3], point: [f64; 3]) -> ApiResult { + self.create_dim_radial(Operation::CreateDimensionDiameter, center, point) + } + fn create_dim_radial( + &self, + op: impl Fn(crate::ops::DimensionRadialSpec) -> Operation, + center: [f64; 3], + point: [f64; 3], + ) -> ApiResult { + let receipt = self.session.apply_op(op(crate::ops::DimensionRadialSpec { center, 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)) + } + /// A 3-point angular DIMENSION (vertex + one point on each leg). + pub fn create_dimension_angular(&self, vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> ApiResult { + let receipt = self.session.apply_op(Operation::CreateDimensionAngular(crate::ops::DimensionAngularSpec { + vertex, first_point, second_point, arc_location, + }))?; + 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)) + } /// 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 { diff --git a/crates/ocs_doc_api/src/gen/api_reference.md b/crates/ocs_doc_api/src/gen/api_reference.md index ef6245751..4fad81f5f 100644 --- a/crates/ocs_doc_api/src/gen/api_reference.md +++ b/crates/ocs_doc_api/src/gen/api_reference.md @@ -133,6 +133,9 @@ Linear dimension between two measured points. measurement() reads the distance; ### Constructors (`doc.curves()`) - **create_dimension_linear**(first_point: [f64; 3], second_point: [f64; 3], definition_point: [f64; 3]) -> `Dimension` — `CreateDimensionLinear` +- **create_dimension_radius**(center: [f64; 3], point: [f64; 3]) -> `Dimension` — `CreateDimensionRadius` +- **create_dimension_diameter**(center: [f64; 3], point: [f64; 3]) -> `Dimension` — `CreateDimensionDiameter` +- **create_dimension_angular**(vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> `Dimension` — `CreateDimensionAngular` ### Methods diff --git a/crates/ocs_doc_api/src/gen/ops_gen.rs b/crates/ocs_doc_api/src/gen/ops_gen.rs index 29a2a5792..d9dcd5e51 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, DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec, SolidPrimitive, TextSpec, ViewportSpec}; +use crate::ops::{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. @@ -48,4 +48,8 @@ pub enum Operation { CreateRasterImage(RasterImageSpec), // multi-profile solid Loft { profiles: Vec }, + // dimension sub-types + CreateDimensionRadius(DimensionRadialSpec), + CreateDimensionDiameter(DimensionRadialSpec), + CreateDimensionAngular(DimensionAngularSpec), } diff --git a/crates/ocs_doc_api/src/ops.rs b/crates/ocs_doc_api/src/ops.rs index a1f47bf00..69fb5f900 100644 --- a/crates/ocs_doc_api/src/ops.rs +++ b/crates/ocs_doc_api/src/ops.rs @@ -116,6 +116,26 @@ pub struct DimensionSpec { pub definition_point: [f64; 3], } +/// Construction spec for a radial/diameter `DIMENSION`: the circle center and a +/// point on the circle (radius) or a chord point (diameter). +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DimensionRadialSpec { + /// Center of the arc/circle (radius) or first chord point (diameter). + pub center: [f64; 3], + /// A point on the arc/circle (radius) or the far chord point (diameter). + pub point: [f64; 3], +} + +/// Construction spec for a 3-point angular `DIMENSION`: vertex + one point on +/// each leg, plus the dimension-arc location. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct DimensionAngularSpec { + pub vertex: [f64; 3], + pub first_point: [f64; 3], + pub second_point: [f64; 3], + pub arc_location: [f64; 3], +} + /// 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. @@ -187,6 +207,9 @@ impl crate::gen::Operation { SetViewportView { .. } => "SetViewportView", CreateRasterImage(_) => "CreateRasterImage", Loft { .. } => "Loft", + CreateDimensionRadius(_) => "CreateDimensionRadius", + CreateDimensionDiameter(_) => "CreateDimensionDiameter", + CreateDimensionAngular(_) => "CreateDimensionAngular", } } } diff --git a/crates/ocs_doc_api/tests/mock_backend.rs b/crates/ocs_doc_api/tests/mock_backend.rs index 685a44103..f83592fee 100644 --- a/crates/ocs_doc_api/tests/mock_backend.rs +++ b/crates/ocs_doc_api/tests/mock_backend.rs @@ -123,6 +123,20 @@ impl DocApiBackend for MockBackend { self.dimension_measurements.insert(id, d); Ok(id) } + fn add_dimension_radius(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult { + let id = self.alloc("Dimension"); + let d = ((spec.center[0] - spec.point[0]).powi(2) + (spec.center[1] - spec.point[1]).powi(2) + (spec.center[2] - spec.point[2]).powi(2)).sqrt(); + self.dimension_measurements.insert(id, d); + Ok(id) + } + fn add_dimension_diameter(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult { + self.add_dimension_radius(spec) + } + fn add_dimension_angular(&mut self, _spec: &ocs_doc_api::ops::DimensionAngularSpec) -> ApiResult { + let id = self.alloc("Dimension"); + self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees + Ok(id) + } fn dimension_measurement(&self, id: ObjectId) -> ApiResult { self.dimension_measurements.get(&id).copied().ok_or(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 94303ab7e..0d9418f43 100644 --- a/crates/ocs_doc_api/tests/spec_wire_consistency.rs +++ b/crates/ocs_doc_api/tests/spec_wire_consistency.rs @@ -64,6 +64,9 @@ fn operation_variants() -> Vec { file_path: String::new(), insertion_point: [0.0; 3], u_vector: [0.0; 3], v_vector: [0.0; 3], size: [0.0; 2], }), Operation::Loft { profiles: 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] }), ]; // Derive the variant names by serializing a probe of each and reading the // bincode variant index -> name map is not available; instead match by @@ -153,6 +156,9 @@ const OPERATION_BASELINE: &[&str] = &[ "SetViewportView", "CreateRasterImage", "Loft", + "CreateDimensionRadius", + "CreateDimensionDiameter", + "CreateDimensionAngular", ]; /// Recorded baseline of the `Query` variant order. diff --git a/src/app/doc_api.rs b/src/app/doc_api.rs index 13c3ec1d4..186b2f6ba 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -248,6 +248,34 @@ impl DocApiBackend for HostSession<'_> { Ok(handle_to_obj(handle)) } + fn add_dimension_radius(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult { + use acadrust::entities::{Dimension, DimensionRadius}; + use acadrust::types::Vector3; + let v3 = |p: [f64; 3]| Vector3::new(p[0], p[1], p[2]); + let dim = DimensionRadius::new(v3(spec.center), v3(spec.point)); + let handle = self.scene_mut().add_entity(EntityType::Dimension(Dimension::Radius(dim))); + Ok(handle_to_obj(handle)) + } + + fn add_dimension_diameter(&mut self, spec: &ocs_doc_api::ops::DimensionRadialSpec) -> ApiResult { + use acadrust::entities::{Dimension, DimensionDiameter}; + use acadrust::types::Vector3; + let v3 = |p: [f64; 3]| Vector3::new(p[0], p[1], p[2]); + let dim = DimensionDiameter::new(v3(spec.center), v3(spec.point)); + let handle = self.scene_mut().add_entity(EntityType::Dimension(Dimension::Diameter(dim))); + Ok(handle_to_obj(handle)) + } + + fn add_dimension_angular(&mut self, spec: &ocs_doc_api::ops::DimensionAngularSpec) -> ApiResult { + use acadrust::entities::{Dimension, DimensionAngular3Pt}; + use acadrust::types::Vector3; + let v3 = |p: [f64; 3]| Vector3::new(p[0], p[1], p[2]); + let mut dim = DimensionAngular3Pt::new(v3(spec.vertex), v3(spec.first_point), v3(spec.second_point)); + dim.definition_point = v3(spec.arc_location); + let handle = self.scene_mut().add_entity(EntityType::Dimension(Dimension::Angular3Pt(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 { @@ -258,6 +286,8 @@ impl DocApiBackend for HostSession<'_> { acadrust::entities::Dimension::Linear(x) => x.measurement(), acadrust::entities::Dimension::Radius(x) => x.measurement(), acadrust::entities::Dimension::Diameter(x) => x.measurement(), + acadrust::entities::Dimension::Angular3Pt(x) => x.measurement_degrees(), + acadrust::entities::Dimension::Angular2Ln(x) => x.measurement_degrees(), _ => return Err(ApiError::Unsupported("dimension measurement for this sub-type".into())), }) } @@ -1081,6 +1111,43 @@ mod tests { } } + // ── Phase 2c-ii: dimension sub-types (radius/diameter/angular) ────────── + + #[test] + fn phase2cii_dimension_radius_diameter_angular() { + let mut app = OpenCADStudio::new_for_test(); + let mut host = HostSession::new(&mut app, 0); + // Radius: center (0,0,0), point on circle (4,0,0) -> radius 4. + let rad = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateDimensionRadius( + ocs_doc_api::ops::DimensionRadialSpec { center: [0.0; 3], point: [4.0, 0.0, 0.0] }))).unwrap()); + // Diameter: chord points (0,0,0) and (6,0,0) -> diameter 6. + let dia = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateDimensionDiameter( + ocs_doc_api::ops::DimensionRadialSpec { center: [0.0; 3], point: [6.0, 0.0, 0.0] }))).unwrap()); + // Angular: vertex (0,0,0), leg points (10,0,0) and (0,10,0) -> 90 degrees. + let ang = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateDimensionAngular( + ocs_doc_api::ops::DimensionAngularSpec { + vertex: [0.0; 3], first_point: [10.0, 0.0, 0.0], second_point: [0.0, 10.0, 0.0], arc_location: [5.0, 5.0, 0.0], + }))).unwrap()); + + let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![ + Query::GetDimensionMeasurement { id: rad }, + Query::GetDimensionMeasurement { id: dia }, + Query::GetDimensionMeasurement { id: ang }, + ])).unwrap(); + match &receipt.query_results[0] { + QueryResult::DimensionMeasurement(v) => assert!((*v - 4.0).abs() < 1e-4, "radius {v}"), + other => panic!("expected measurement, got {other:?}"), + } + match &receipt.query_results[1] { + QueryResult::DimensionMeasurement(v) => assert!((*v - 6.0).abs() < 1e-4, "diameter {v}"), + other => panic!("expected measurement, got {other:?}"), + } + match &receipt.query_results[2] { + QueryResult::DimensionMeasurement(v) => assert!((*v - 90.0).abs() < 0.1, "angle {v}"), + other => panic!("expected measurement, got {other:?}"), + } + } + // ── Phase 5 remaining: typed raster image create ───────────────────────── #[test]