diff --git a/crates/ocs_doc_api/IMPLEMENTATION.md b/crates/ocs_doc_api/IMPLEMENTATION.md index 2e6bc0ca7..6666c36a0 100644 --- a/crates/ocs_doc_api/IMPLEMENTATION.md +++ b/crates/ocs_doc_api/IMPLEMENTATION.md @@ -117,9 +117,12 @@ tested: 615 host lib + 8 crate unit + 3 spec↔wire consistency tests green. ## πŸ”§ Outstanding methods on supported families -| Method | Family | Status | Blocker | -|---|---|---|---| -| `GetCentroid`/`GetVolume` accuracy | Solid | mesh approx | render-mesh LOD tolerance vs query tolerance | +**Completed (accuracy):** `GetVolume`/`GetCentroid` now 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). **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/src/app/doc_api.rs b/src/app/doc_api.rs index 1c3197257..160279bc7 100644 --- a/src/app/doc_api.rs +++ b/src/app/doc_api.rs @@ -631,23 +631,19 @@ impl HostSession<'_> { return Ok((v, c)); } } - // Render-mesh metrics cache (avoids re-tessellation + B-rep clone). - if let Some(metrics) = self.scene().meshes.get(&handle).map(|m| m.metrics) { - if metrics.volume.abs() > 0.0 { - self.mass_cache.insert(handle, (epoch, metrics.volume, metrics.centroid)); - return Ok((metrics.volume, metrics.centroid)); - } - } - // Cold-cache fallback: bounded full tessellation. + // 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; + // 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 { return Err(ApiError::Unsupported( - "volume/centroid cold-tessellation budget exceeded for this session; regenerate the mesh or query fewer distinct solids".into(), + "volume/centroid cold-tessellation budget exceeded for this session; query fewer distinct solids".into(), )); } self.cold_tess_used += 1; let body = self.resolve_body(id)?; - let mesh = cadkernel::brep::mesh_body(&body, 0.5, 1e-3); + let mesh = cadkernel::brep::mesh_body(&body, 0.1, 1e-4); let (v, c) = mesh_volume_centroid(&mesh); self.mass_cache.insert(handle, (epoch, v, c)); Ok((v, c)) @@ -1244,6 +1240,42 @@ mod tests { } } + // ── Outstanding method: accurate volume/centroid (fine tessellation) ───── + + #[test] + fn accurate_volume_centroid_sphere_and_cube() { + let mut app = OpenCADStudio::new_for_test(); + let mut host = HostSession::new(&mut app, 0); + // Sphere r=5: analytic volume = 4/3 * pi * 125 β‰ˆ 523.599; centroid at centre. + let ball = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateSolid( + SolidPrimitive::Sphere { centre: [10.0, 20.0, 30.0], radius: 5.0 }))).unwrap()); + let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![ + Query::GetVolume { id: ball }, Query::GetCentroid { id: ball }])).unwrap(); + let analytic = 4.0 / 3.0 * std::f64::consts::PI * 125.0; + match &receipt.query_results[0] { + QueryResult::Volume(v) => { + let err = ((*v - analytic) / analytic).abs(); + assert!(err < 0.005, "sphere volume {v} vs analytic {analytic} (err {err:.4})"); + } + other => panic!("expected volume, got {other:?}"), + } + match &receipt.query_results[1] { + QueryResult::Centroid(c) => { + assert!((c[0] - 10.0).abs() < 0.05 && (c[1] - 20.0).abs() < 0.05 && (c[2] - 30.0).abs() < 0.05, + "sphere centroid {c:?}"); + } + other => panic!("expected centroid, got {other:?}"), + } + // Cube 10^3 is exact (planar faces: divergence is exact regardless of LOD). + let cube = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateSolid( + SolidPrimitive::Cuboid { origin: [0.0; 3], size: [10.0; 3] }))).unwrap()); + let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetVolume { id: cube }])).unwrap(); + match &receipt.query_results[0] { + QueryResult::Volume(v) => assert!((*v - 1000.0).abs() < 1.0, "cube volume {v}"), + other => panic!("expected volume, got {other:?}"), + } + } + // ── Phase 2c-iii: 2-line angular dimension ──────────────────────────────── #[test]