diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index a0630584d..2e6bc0ca7 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -49,11 +49,11 @@ Phases are **spec-additive**: each is a `[[family]]`/`[[family.method]]` block i `ARCHITECTURE.md` § Workflow). No protocol change; new `Operation`/`Query` variants append at enum end. -**Status:** Phase 0 (supported-family methods), Phase 2a (full 2D curves), -Phase 3 (block references), Phase 4 (viewports), Phase 5 (media read-mostly) are -**done** (all tested; 601 host lib + 8 crate unit tests green). Remaining sub-items: -Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal -(P3), set_view + view query (P4), typed media ops (P5). +**Status:** all five phases complete — Phase 0 (supported-family methods), Phase 2 +(full 2D curves + annotations incl. dimension sub-types), Phase 3 (block references ++ attributes + traversal + attribute definitions), Phase 4 (viewports + set_view + +view query), Phase 5 (media read-mostly + typed raster image + typed table). All +tested: 615 host lib + 8 crate unit + 3 spec↔wire consistency tests green. ### Phase 2 — full 2D + annotations - **Done (2a):** typed curve families `ArcCurve`/`Ellipse`/`Spline`/`Ray`/`XLine` — @@ -73,8 +73,10 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal (`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.) + measurements. Covered by `phase2cii_*` test. +- **Done (2c-iii):** `create_dimension_angular2ln(vertex, first, second, arc_location)` + → `CreateDimensionAngular2Ln` (2-line angular; `DimensionAngular2Ln::new` computes + the angle). Covered by `phase2ciii_*` test. All documented dimension sub-types done. - **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 5e49cb300..3d982cc62 100644 --- a/crates/ocs_doc_api/examples/common/mod.rs +++ b/crates/ocs_doc_api/examples/common/mod.rs @@ -206,6 +206,11 @@ impl DocApiBackend for MockBackend { } Ok(self.alloc("AttributeDefinition")) } + fn add_dimension_angular2ln(&mut self, _spec: &ocs_doc_api::ops::DimensionAngularSpec) -> ApiResult { + let id = self.alloc("Dimension"); + self.dimension_measurements.insert(id, 90.0); + Ok(id) + } fn add_table(&mut self, spec: &ocs_doc_api::ops::TableSpec) -> ApiResult { let cols = spec.data.first().map(|r| r.len()).unwrap_or(0); if spec.data.is_empty() || cols == 0 || spec.data.iter().any(|r| r.len() != cols) { diff --git a/crates/ocs_doc_api/spec/entities.toml b/crates/ocs_doc_api/spec/entities.toml index e8442a563..b885693dd 100644 --- a/crates/ocs_doc_api/spec/entities.toml +++ b/crates/ocs_doc_api/spec/entities.toml @@ -346,6 +346,11 @@ 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.constructor]] +name = "create_dimension_angular2ln" +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 = "CreateDimensionAngular2Ln" + [[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 3bed14d8e..0e004d6e3 100644 --- a/crates/ocs_doc_api/src/backend.rs +++ b/crates/ocs_doc_api/src/backend.rs @@ -78,6 +78,9 @@ pub trait DocApiBackend { /// Add a `TABLE` (rows × columns grid); returns the fresh `ObjectId`. fn add_table(&mut self, spec: &crate::ops::TableSpec) -> ApiResult; + /// Add a 2-line angular `DIMENSION`; returns the fresh `ObjectId`. + fn add_dimension_angular2ln(&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 735ed8bc3..e6c626e42 100644 --- a/crates/ocs_doc_api/src/executor.rs +++ b/crates/ocs_doc_api/src/executor.rs @@ -304,6 +304,12 @@ pub fn apply_op(b: &mut B, op: Operation) -> ApiResult { + b.push_undo(name); + let id = b.add_dimension_angular2ln(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 d7ce04372..dabc2d8be 100644 --- a/crates/ocs_doc_api/src/facade.rs +++ b/crates/ocs_doc_api/src/facade.rs @@ -539,6 +539,14 @@ impl CurveCollection { Ok(Entity::new(self.session.clone(), id)) } + /// A 2-line angular DIMENSION: angle between lines (vertex→first) and (vertex→second). + pub fn create_dimension_angular2ln(&self, vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> ApiResult { + let receipt = self.session.apply_op(Operation::CreateDimensionAngular2Ln(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 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) diff --git a/crates/ocs_doc_api/src/gen/api_reference.md b/crates/ocs_doc_api/src/gen/api_reference.md index ed7c2b9ac..1119bcf76 100644 --- a/crates/ocs_doc_api/src/gen/api_reference.md +++ b/crates/ocs_doc_api/src/gen/api_reference.md @@ -136,6 +136,7 @@ Linear dimension between two measured points. measurement() reads the distance; - **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` +- **create_dimension_angular2ln**(vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> `Dimension` — `CreateDimensionAngular2Ln` ### Methods diff --git a/crates/ocs_doc_api/src/gen/ops_gen.rs b/crates/ocs_doc_api/src/gen/ops_gen.rs index c4c1f6266..be0246f20 100644 --- a/crates/ocs_doc_api/src/gen/ops_gen.rs +++ b/crates/ocs_doc_api/src/gen/ops_gen.rs @@ -56,4 +56,6 @@ pub enum Operation { CreateAttributeDefinition(AttributeDefinitionSpec), // tables CreateTable(TableSpec), + // dimension sub-types (2-line angular) + CreateDimensionAngular2Ln(DimensionAngularSpec), } diff --git a/crates/ocs_doc_api/src/ops.rs b/crates/ocs_doc_api/src/ops.rs index 32e02250c..f32284dce 100644 --- a/crates/ocs_doc_api/src/ops.rs +++ b/crates/ocs_doc_api/src/ops.rs @@ -233,6 +233,7 @@ impl crate::gen::Operation { CreateDimensionAngular(_) => "CreateDimensionAngular", CreateAttributeDefinition(_) => "CreateAttributeDefinition", CreateTable(_) => "CreateTable", + CreateDimensionAngular2Ln(_) => "CreateDimensionAngular2Ln", } } } diff --git a/crates/ocs_doc_api/tests/mock_backend.rs b/crates/ocs_doc_api/tests/mock_backend.rs index 2488f29df..82909cd86 100644 --- a/crates/ocs_doc_api/tests/mock_backend.rs +++ b/crates/ocs_doc_api/tests/mock_backend.rs @@ -146,6 +146,11 @@ impl DocApiBackend for MockBackend { } Ok(self.alloc("AttributeDefinition")) } + fn add_dimension_angular2ln(&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 add_table(&mut self, spec: &ocs_doc_api::ops::TableSpec) -> ApiResult { let cols = spec.data.first().map(|r| r.len()).unwrap_or(0); if spec.data.is_empty() || cols == 0 || spec.data.iter().any(|r| r.len() != cols) { diff --git a/crates/ocs_doc_api/tests/spec_wire_consistency.rs b/crates/ocs_doc_api/tests/spec_wire_consistency.rs index c646aabbe..012137b27 100644 --- a/crates/ocs_doc_api/tests/spec_wire_consistency.rs +++ b/crates/ocs_doc_api/tests/spec_wire_consistency.rs @@ -71,6 +71,7 @@ fn operation_variants() -> Vec { tag: String::new(), prompt: String::new(), default_value: String::new(), insertion_point: [0.0; 3], height: 0.0, rotation: 0.0, }), Operation::CreateTable(ocs_doc_api::ops::TableSpec { insertion_point: [0.0; 3], data: vec![] }), + Operation::CreateDimensionAngular2Ln(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 @@ -165,6 +166,7 @@ const OPERATION_BASELINE: &[&str] = &[ "CreateDimensionAngular", "CreateAttributeDefinition", "CreateTable", + "CreateDimensionAngular2Ln", ]; /// Recorded baseline of the `Query` variant order. diff --git a/src/app/doc_api.rs b/src/app/doc_api.rs index 1a2bb9b21..1c3197257 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -306,6 +306,16 @@ impl DocApiBackend for HostSession<'_> { Ok(handle_to_obj(handle)) } + fn add_dimension_angular2ln(&mut self, spec: &ocs_doc_api::ops::DimensionAngularSpec) -> ApiResult { + use acadrust::entities::{Dimension, DimensionAngular2Ln}; + use acadrust::types::Vector3; + let v3 = |p: [f64; 3]| Vector3::new(p[0], p[1], p[2]); + let mut dim = DimensionAngular2Ln::new(v3(spec.vertex), v3(spec.first_point), v3(spec.second_point)); + dim.dimension_arc = v3(spec.arc_location); + let handle = self.scene_mut().add_entity(EntityType::Dimension(Dimension::Angular2Ln(dim))); + Ok(handle_to_obj(handle)) + } + fn add_table(&mut self, spec: &ocs_doc_api::ops::TableSpec) -> ApiResult { use acadrust::entities::Table; use acadrust::types::Vector3; @@ -1234,6 +1244,31 @@ mod tests { } } + // ── Phase 2c-iii: 2-line angular dimension ──────────────────────────────── + + #[test] + fn phase2ciii_dimension_angular2ln() { + let mut app = OpenCADStudio::new_for_test(); + let mut host = HostSession::new(&mut app, 0); + // Angle between line (0,0,0)->(10,0,0) and (0,0,0)->(0,10,0) = 90 degrees. + let ang = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateDimensionAngular2Ln( + 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: ang }, + Query::GetEntity { id: ang }, + ])).unwrap(); + match &receipt.query_results[0] { + QueryResult::DimensionMeasurement(v) => assert!((*v - 90.0).abs() < 0.1, "angle {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:?}"), + } + } + // ── Phase 5 remaining: typed raster image create ───────────────────────── #[test]