diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index 6666c36a0..537f35d69 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -106,8 +106,12 @@ tested: 615 host lib + 8 crate unit + 3 spec↔wire consistency tests green. - **Done:** media/misc families report a **real `kind`** via `GetEntity` (`RasterImage`, `Table`, `Leader`, `MultiLeader`, `MLine`, `Mesh`, `Helix`, `Region`, `Body`, `Surface`, `Face3D`, `Dimension`, `Hatch`) instead of "Other"; - `RasterImage` has coarse bounds (insertion + u·width + v·height); generic - `delete` works on all. Covered by `phase5_*` test. + generic `delete` works on all. Covered by `phase5_*` test. +- **Done (read-mostly bounds):** `bounds()` now works for the vertex/point-carrying + families — `Leader`, `Mesh`, `Face3D` (4 corners), `MLine` (vertex positions), + `Helix` (start point) — via a points-to-AABB helper (`read_mostly_family_bounds_*` + test). `RasterImage` has 4-corner bounds. `Region`/`Body`/`Surface`/`MultiLeader` + stay `Unsupported` for bounds (complex/nested geometry). - **Done (typed create):** `create_raster_image(file_path, insertion, u, v, size)` → `Operation::CreateRasterImage` (host auto-registers the ImageDefinition), and `create_table(insertion_point, data[row][col])` → `Operation::CreateTable` diff --git a/src/app/doc_api.rs b/src/app/doc_api.rs index 160279bc7..9c58225d5 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -1301,6 +1301,54 @@ mod tests { } } + // ── Read-mostly family bounds (Leader/Mesh/Face3D/MLine/Helix) ────────── + + #[test] + fn read_mostly_family_bounds_leader_and_mesh() { + let mut app = OpenCADStudio::new_for_test(); + let mut host = HostSession::new(&mut app, 0); + // Leader with vertices at (0,0,0),(5,5,0),(10,0,0) -> bounds x[0,10] y[0,5]. + let mut leader = acadrust::entities::Leader::default(); + leader.vertices = vec![ + acadrust::types::Vector3::new(0.0, 0.0, 0.0), + acadrust::types::Vector3::new(5.0, 5.0, 0.0), + acadrust::types::Vector3::new(10.0, 0.0, 0.0), + ]; + let leader_h = host.document_mut().add_entity(EntityType::Leader(leader)).unwrap(); + // Mesh with vertices in a unit cube at (2..3, 2..3, 0). + let mut mesh = acadrust::entities::Mesh::default(); + mesh.vertices = vec![ + acadrust::types::Vector3::new(2.0, 2.0, 0.0), + acadrust::types::Vector3::new(3.0, 2.0, 0.0), + acadrust::types::Vector3::new(3.0, 3.0, 0.0), + acadrust::types::Vector3::new(2.0, 3.0, 0.0), + ]; + let mesh_h = host.document_mut().add_entity(EntityType::Mesh(mesh)).unwrap(); + + let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![ + Query::GetBounds { id: handle_to_obj(leader_h) }, + Query::GetBounds { id: handle_to_obj(mesh_h) }, + Query::GetEntity { id: handle_to_obj(leader_h) }, + ])).unwrap(); + match &receipt.query_results[0] { + QueryResult::Bounds(b) => { + assert!((b.min[0] - 0.0).abs() < 1e-6 && (b.max[0] - 10.0).abs() < 1e-6, "{b:?}"); + assert!((b.min[1] - 0.0).abs() < 1e-6 && (b.max[1] - 5.0).abs() < 1e-6, "{b:?}"); + } + other => panic!("expected bounds, got {other:?}"), + } + match &receipt.query_results[1] { + QueryResult::Bounds(b) => { + assert!((b.min[0] - 2.0).abs() < 1e-6 && (b.max[0] - 3.0).abs() < 1e-6, "{b:?}"); + } + other => panic!("expected bounds, got {other:?}"), + } + match &receipt.query_results[2] { + QueryResult::Entity(e) => assert_eq!(e.kind, "Leader"), + other => panic!("expected entity, got {other:?}"), + } + } + // ── Phase 5 remaining: typed raster image create ───────────────────────── #[test] diff --git a/src/app/doc_api_convert.rs b/src/app/doc_api_convert.rs index 17be1df27..784a1a833 100644 --- a/src/app/doc_api_convert.rs +++ b/src/app/doc_api_convert.rs @@ -473,8 +473,31 @@ pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult points_aabb(l.vertices.iter().collect(), id)?, + EntityType::Mesh(m) => points_aabb(m.vertices.iter().collect(), id)?, + EntityType::Face3D(f) => points_aabb( + [&f.first_corner, &f.second_corner, &f.third_corner, &f.fourth_corner].into_iter().collect(), + id, + )?, + EntityType::MLine(m) => points_aabb(m.vertices.iter().map(|v| &v.position).collect(), id)?, + EntityType::Helix(h) => points_aabb([&h.start_point].into_iter().collect(), id)?, // Ray/XLine are unbounded by definition — no finite bounds. _ => return Err(ApiError::Unsupported("bounds for this entity family is not yet implemented".into())), }; Ok(bb) } + +/// AABB over a set of 3D points. `UnknownId` if empty (no points to bound). +fn points_aabb(pts: Vec<&Vector3>, id: ObjectId) -> ApiResult { + if pts.is_empty() { + return Err(ApiError::UnknownId(id)); + } + 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)]; + } + Ok(Aabb { min, max }) +}