diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index 537f35d69..5bd4113ca 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -15,8 +15,8 @@ content). | Booleans | `intersect` / `union` / `subtract` | `SolidBoolean{op, a, b, erase_sources:true}` → `brep::combine` | | Transform | `transform(placement)` (in-place) | `Transform{id, placement}` → `brep::transform`; solids | | Bounds | `bounds()` | `GetBounds` → `brep::body_bounds` (lift-on-miss) | -| Volume | `volume()` | `GetVolume` → `meshes.metrics` cache, mesh-divergence fallback | -| Centroid | `centroid()` | `GetCentroid` → `meshes.metrics` cache, mesh-divergence fallback | +| Volume | `volume()` | `GetVolume` → fine-tessellation `mesh_body(0.1, 1e-4)` (near-analytic); memoized per handle+epoch; budget-exhausted → approximate `meshes.metrics` | +| Centroid | `centroid()` | `GetCentroid` → same accuracy path as `volume()` | | Delete | `delete()` / `delete_many` | `Delete`/`DeleteMany` | ### Minimal 2D (phase-1 set, used as profiles/inputs) @@ -53,7 +53,7 @@ variants append at enum end. (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. +tested: 618 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` — @@ -121,12 +121,13 @@ tested: 615 host lib + 8 crate unit + 3 spec↔wire consistency tests green. ## 🔧 Outstanding methods on supported families -**Completed (accuracy):** `GetVolume`/`GetCentroid` now compute via a **fine +**Completed (accuracy):** `GetVolume`/`GetCentroid` compute via a **fine tessellation** (`mesh_body(0.1, 1e-4)`) for near-analytic results — the -render-mesh metrics cache is the coarse display LOD and no longer shadows query -accuracy; results memoized per (handle, geometry_epoch). Verified by -`accurate_volume_centroid_sphere_and_cube` (sphere volume within 0.5% of 4/3πr³, -centroid at centre, cube exact). +render-mesh metrics cache is the coarse display LOD and does not drive queries; +results memoized per (handle, geometry_epoch); when the cold-tess budget is +exhausted the path degrades to the approximate `meshes.metrics` value rather than +failing. Verified by `accurate_volume_centroid_sphere_and_cube` (sphere volume +within 0.5% of 4/3πr³, centroid at centre, cube exact). **Completed (outstanding methods):** `loft(profiles)` — `Operation::Loft{profiles}`, resolves each profile to curve sets (all-or-nothing), `brep::loft` (`loft_two_profiles_*` diff --git a/crates/ocs_doc_api/src/gen/api_reference.md b/crates/ocs_doc_api/src/gen/api_reference.md index 1119bcf76..2f40d9acf 100644 --- a/crates/ocs_doc_api/src/gen/api_reference.md +++ b/crates/ocs_doc_api/src/gen/api_reference.md @@ -278,6 +278,12 @@ Every `Operation` is ONE atomic write op (one undo step); every `Query` is read- - `SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }` - `CreateRasterImage(RasterImageSpec)` - `Loft { profiles: Vec }` +- `CreateDimensionRadius(DimensionRadialSpec)` +- `CreateDimensionDiameter(DimensionRadialSpec)` +- `CreateDimensionAngular(DimensionAngularSpec)` +- `CreateAttributeDefinition(AttributeDefinitionSpec)` +- `CreateTable(TableSpec)` +- `CreateDimensionAngular2Ln(DimensionAngularSpec)` ### `Query` diff --git a/crates/ocs_doc_api/src/gen/binding_schema.json b/crates/ocs_doc_api/src/gen/binding_schema.json index ed300c3d8..3484d81e6 100644 --- a/crates/ocs_doc_api/src/gen/binding_schema.json +++ b/crates/ocs_doc_api/src/gen/binding_schema.json @@ -428,7 +428,13 @@ "SetAttribute { id: ObjectId, tag: String, value: String }", "SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }", "CreateRasterImage(RasterImageSpec)", - "Loft { profiles: Vec }" + "Loft { profiles: Vec }", + "CreateDimensionRadius(DimensionRadialSpec)", + "CreateDimensionDiameter(DimensionRadialSpec)", + "CreateDimensionAngular(DimensionAngularSpec)", + "CreateAttributeDefinition(AttributeDefinitionSpec)", + "CreateTable(TableSpec)", + "CreateDimensionAngular2Ln(DimensionAngularSpec)" ] }, "Query": { diff --git a/crates/ocs_doc_api/src/gen/layouts.json b/crates/ocs_doc_api/src/gen/layouts.json index 4a9edb9d5..c743ba25d 100644 --- a/crates/ocs_doc_api/src/gen/layouts.json +++ b/crates/ocs_doc_api/src/gen/layouts.json @@ -23,7 +23,13 @@ "SetAttribute { id: ObjectId, tag: String, value: String }", "SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }", "CreateRasterImage(RasterImageSpec)", - "Loft { profiles: Vec }" + "Loft { profiles: Vec }", + "CreateDimensionRadius(DimensionRadialSpec)", + "CreateDimensionDiameter(DimensionRadialSpec)", + "CreateDimensionAngular(DimensionAngularSpec)", + "CreateAttributeDefinition(AttributeDefinitionSpec)", + "CreateTable(TableSpec)", + "CreateDimensionAngular2Ln(DimensionAngularSpec)" ] }, "Query": { diff --git a/src/app/doc_api.rs b/src/app/doc_api.rs index 9c58225d5..b34d75f6e 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -633,10 +633,18 @@ impl HostSession<'_> { } // Accuracy path: fine tessellation so the divergence volume/centroid // converge to near-analytic (the render-mesh metrics cache is the coarse - // LOD for display, NOT for query accuracy). Bounded by the cold-tess budget; + // display LOD, NOT for query accuracy). Bounded by the cold-tess budget; // memoized per (handle, epoch) so repeated queries on the same solid are O(1). const COLD_TESS_BUDGET: usize = 256; if self.cold_tess_used >= COLD_TESS_BUDGET { + // Budget exhausted: serve a documented-approximate value from the render + // mesh metrics cache rather than failing (large distinct-solid batches + // degrade gracefully instead of erroring). Accuracy-first up to the budget. + if let Some(metrics) = self.scene().meshes.get(&handle).map(|m| m.metrics) { + if metrics.volume.abs() > 0.0 { + return Ok((metrics.volume, metrics.centroid)); + } + } return Err(ApiError::Unsupported( "volume/centroid cold-tessellation budget exceeded for this session; query fewer distinct solids".into(), )); diff --git a/src/app/doc_api_convert.rs b/src/app/doc_api_convert.rs index 784a1a833..9ccfb1aa0 100644 --- a/src/app/doc_api_convert.rs +++ b/src/app/doc_api_convert.rs @@ -5,7 +5,7 @@ use acadrust::entities::{Arc, Circle, Ellipse, Line, LwPolyline, LwVertex, Point, Ray, Spline, XLine}; use acadrust::types::{Vector2, Vector3}; use acadrust::EntityType; -use ocs_doc_api::{Aabb, ApiError, ApiResult, Curve2Spec, ObjectId}; +use ocs_doc_api::{Aabb, ApiError, ApiResult, Curve2Spec, GeometryErrorKind, ObjectId}; /// Convert a profile entity (Line/Circle/Arc/LwPolyline) to `geom2d::Curve`s for /// sweep ops (`Extrude`/`Revolve`). Z is dropped (profiles lie on the XY plane). @@ -481,17 +481,26 @@ pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult points_aabb(m.vertices.iter().map(|v| &v.position).collect(), id)?, - EntityType::Helix(h) => points_aabb([&h.start_point].into_iter().collect(), id)?, + EntityType::Helix(h) => { + // Bound the evaluated spline's control polygon (conservative); the + // single start_point alone would be a degenerate zero-size box. + if h.spline.control_points.is_empty() { + points_aabb([&h.start_point].into_iter().collect(), id)? + } else { + points_aabb(h.spline.control_points.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 { +/// AABB over a set of 3D points. A geometry-Empty error if there are no points +/// to bound (the entity exists — do NOT misclassify as a stale/deleted id). +fn points_aabb(pts: Vec<&Vector3>, _id: ObjectId) -> ApiResult { if pts.is_empty() { - return Err(ApiError::UnknownId(id)); + return Err(ApiError::geometry(GeometryErrorKind::Empty, "entity has no points to bound")); } let mut min = [f64::INFINITY; 3]; let mut max = [f64::NEG_INFINITY; 3];