forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: fix review findings (scale/rotation/all-or-nothing/DoS/bounds/codegen)
CRITICAL: - transform_entity_geometry: fix s^2 double-scale (axes already carry scale; drop the *s pre-multiply so 2D entities scale by s like solids), and handle rotation for direction/angle fields (apply_dir for Ray/XLine direction + Ellipse major_axis; offset Arc angles; compose Insert rotation). - ensure_transformable: check layer-lock + resolve solid bodies (not just type), restoring TransformMany all-or-nothing; regression test with a locked layer. WARNING: - volume/centroid: memoize per (handle, geometry_epoch) + cap cold-cache tessellation budget (query-batch DoS). - entity_to_profile_curves: fail Unsupported on polyline bulge (no silent chord-substituted sweep profile). - Ellipse: validate minor_axis_ratio in (0,1] so bounds never under-bound. - RasterImage bounds: bracket all four corners (rotated/flipped images). SUGGESTION: - binding_schema.json handles derived from spec families (includes ArcCurve/ Ellipse/Spline/Ray/XLine). - build.rs header: enums are hand-maintained (not 'GENERATED'); ops_gen/query_gen headers corrected to match. - Dedupe divergence mesh_volume_centroid into the crate's geom module; host + both mocks delegate. Tests: +3 regression tests (scale-once, rotation, locked-layer all-or-nothing) -> 604 host lib green; 8 crate unit tests green.
This commit is contained in:
1 parent
fabdeae8bf
commit
c74504f30c
10 files changed
+271
-150
No files matched your search
@@ -1,7 +1,11 @@
|
||||
//! Codegen driver (plan §10): reads `spec/entities.toml` and regenerates the
|
||||
//! committed snapshots in `src/gen/` (ops/query enums, api_reference.md,
|
||||
//! binding_schema.json). CI runs `cargo build` and fails on drift (the committed
|
||||
//! files are the reviewable truth; build.rs rewrites them deterministically).
|
||||
//! derived snapshots in `src/gen/` (`api_reference.md`, `binding_schema.json`).
|
||||
//! CI runs `cargo build` and fails on drift for those two (the committed files
|
||||
//! are the reviewable truth; build.rs rewrites them deterministically).
|
||||
//!
|
||||
//! NOTE: the `Operation`/`Query` wire enums (`ops_gen.rs`/`query_gen.rs`) are
|
||||
//! HAND-MAINTAINED, append-only, and NOT derived here — the spec's `op`/`query`
|
||||
//! names must map to enum variants (asserted by a test), never the reverse.
|
||||
//!
|
||||
//! The wire-layout tracing for `binding_schema.json` uses serde-reflection on the
|
||||
//! DTO payload types (same mechanism as `ocs_plugin_api`'s `type_registry.json`),
|
||||
@@ -264,6 +268,19 @@ fn api_reference_md(spec: &Spec) -> String {
|
||||
|
||||
/// The merged, self-contained binding handover schema (§10.2). Object model +
|
||||
/// method signatures + op/query mapping + traced wire layouts inlined per node.
|
||||
/// The handle types for the binding schema, derived from the spec's families
|
||||
/// ("Entity" always first, then the deduped family handles in spec order).
|
||||
fn handles_for_schema(spec: &Spec) -> Vec<String> {
|
||||
let mut out = vec!["Entity".to_string()];
|
||||
for fam in &spec.family {
|
||||
let h = fam.handle.as_deref().unwrap_or(&fam.name);
|
||||
if h != "Entity" && !out.iter().any(|x| x == h) {
|
||||
out.push(h.to_string());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn binding_schema_json(spec: &Spec) -> String {
|
||||
// Trace the wire layouts of the DTO payload types via serde-reflection so
|
||||
// the inlined layouts are mechanically correct (mirrors the wire types).
|
||||
@@ -307,7 +324,9 @@ fn binding_schema_json(spec: &Spec) -> String {
|
||||
"root": "DocApi",
|
||||
"document": "Document",
|
||||
"collections": ["solids", "curves", "entities"],
|
||||
"handles": ["Entity", "Solid", "Line", "Circle", "Polyline", "Point"],
|
||||
// Derived from the spec's family handles (deduped, "Entity" first) so
|
||||
// new handle families appear in the handover automatically.
|
||||
"handles": handles_for_schema(spec),
|
||||
},
|
||||
"families": families,
|
||||
// Traced wire layouts for every Operation/Query/payload DTO (inlined).
|
||||
|
||||
@@ -204,25 +204,8 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Signed volume + centroid of a closed mesh via the divergence theorem.
|
||||
/// Signed volume + centroid of a closed mesh — delegates to the crate's single
|
||||
/// source of truth (divergence theorem) so the example mock can't drift.
|
||||
fn mesh_vc(mesh: &cadkernel::brep::Mesh) -> (f64, [f64; 3]) {
|
||||
let mut vol = 0.0;
|
||||
let mut c = [0.0; 3];
|
||||
for t in &mesh.triangles {
|
||||
let (v0, v1, v2) = (mesh.positions[t[0]], mesh.positions[t[1]], mesh.positions[t[2]]);
|
||||
let cross = [
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
];
|
||||
let tet = (v0[0] * cross[0] + v0[1] * cross[1] + v0[2] * cross[2]) / 6.0;
|
||||
vol += tet;
|
||||
for i in 0..3 {
|
||||
c[i] += (v0[i] + v1[i] + v2[i]) * tet;
|
||||
}
|
||||
}
|
||||
if vol.abs() < 1e-12 {
|
||||
return (0.0, [0.0; 3]);
|
||||
}
|
||||
(vol, [c[0] / (4.0 * vol), c[1] / (4.0 * vol), c[2] / (4.0 * vol)])
|
||||
ocs_doc_api::geom::mesh_volume_centroid(mesh)
|
||||
}
|
||||
@@ -265,7 +265,12 @@
|
||||
"Line",
|
||||
"Circle",
|
||||
"Polyline",
|
||||
"Point"
|
||||
"Point",
|
||||
"ArcCurve",
|
||||
"Ellipse",
|
||||
"Spline",
|
||||
"Ray",
|
||||
"XLine"
|
||||
],
|
||||
"root": "DocApi"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// GENERATED by build.rs from spec/entities.toml — do NOT edit by hand.
|
||||
// Committed snapshot for reviewability (plan §10); build.rs regenerates and CI
|
||||
// fails on drift (`--check`). Append new variants at the END only (bincode
|
||||
// discriminant stability, plan §7).
|
||||
// HAND-MAINTAINED wire vocabulary (the Operation enum). This file is NOT derived
|
||||
// from spec/entities.toml by build.rs — the enum is the canonical append-only
|
||||
// wire vocabulary; the spec's `op` names must each map to a variant here
|
||||
// (enforced by a test). Append new variants at the END only (bincode discriminant
|
||||
// stability, plan §7). Do NOT reorder or remove variants.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// GENERATED by build.rs from spec/entities.toml — do NOT edit by hand.
|
||||
// Committed snapshot for reviewability (plan §10); build.rs regenerates and CI
|
||||
// fails on drift (`--check`). Append new variants at the END only (bincode
|
||||
// discriminant stability, plan §7).
|
||||
// HAND-MAINTAINED wire vocabulary (the Query enum). NOT derived from the spec by
|
||||
// build.rs — the enum is the canonical append-only wire vocabulary; the spec's
|
||||
// `query` names must each map to a variant here (enforced by a test). Append new
|
||||
// variants at the END only (bincode discriminant stability, plan §7).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@@ -13,6 +13,31 @@ fn geom_err(kind: GeometryErrorKind, msg: impl Into<String>) -> ApiError {
|
||||
ApiError::geometry(kind, msg)
|
||||
}
|
||||
|
||||
/// Signed volume and centroid of a closed triangle mesh via the divergence
|
||||
/// theorem: V = Σ v0·(v1×v2)/6, C = Σ (v0+v1+v2)·tetra_vol / (4V). Single source
|
||||
/// of truth shared by the host backend and the test/example mock backends.
|
||||
pub fn mesh_volume_centroid(mesh: &cadkernel::brep::Mesh) -> (f64, [f64; 3]) {
|
||||
let mut vol = 0.0;
|
||||
let mut c = [0.0; 3];
|
||||
for t in &mesh.triangles {
|
||||
let (v0, v1, v2) = (mesh.positions[t[0]], mesh.positions[t[1]], mesh.positions[t[2]]);
|
||||
let cross = [
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
];
|
||||
let tet = (v0[0] * cross[0] + v0[1] * cross[1] + v0[2] * cross[2]) / 6.0;
|
||||
vol += tet;
|
||||
for i in 0..3 {
|
||||
c[i] += (v0[i] + v1[i] + v2[i]) * tet;
|
||||
}
|
||||
}
|
||||
if vol.abs() < 1e-12 {
|
||||
return (0.0, [0.0; 3]);
|
||||
}
|
||||
(vol, [c[0] / (4.0 * vol), c[1] / (4.0 * vol), c[2] / (4.0 * vol)])
|
||||
}
|
||||
|
||||
/// The XY work plane (origin at 0, X=(1,0,0), normal=(0,0,1)).
|
||||
fn xy_plane() -> ApiResult<Plane> {
|
||||
Plane::orthonormal([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Unit tests over an in-memory mock `DocApiBackend` (plan §11.1): no host.
|
||||
//! Covers the §9 examples: per-op atomicity, boolean erase_sources, OpGroup
|
||||
//! Unit tests over an in-memory mock `DocApiBackend` (plan §11.1): no host.
|
||||
//! Covers the §9 examples: per-op atomicity, boolean erase_sources, OpGroup
|
||||
//! compensation, bulk all-or-nothing, revision bumps, read-only query batching,
|
||||
//! and stale-handle `UnknownId` semantics.
|
||||
|
||||
@@ -119,11 +119,11 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
fn centroid(&mut self, id: ObjectId) -> ApiResult<[f64; 3]> {
|
||||
let body = self.body(id)?;
|
||||
Ok(cadkernel::brep::mesh_body(body, 0.5, 1e-3).centroid_approx())
|
||||
Ok(ocs_doc_api::geom::mesh_volume_centroid(&cadkernel::brep::mesh_body(body, 0.5, 1e-3)).1)
|
||||
}
|
||||
fn volume(&mut self, id: ObjectId) -> ApiResult<f64> {
|
||||
let body = self.body(id)?;
|
||||
Ok(cadkernel::brep::mesh_body(body, 0.5, 1e-3).volume_approx())
|
||||
Ok(ocs_doc_api::geom::mesh_volume_centroid(&cadkernel::brep::mesh_body(body, 0.5, 1e-3)).0)
|
||||
}
|
||||
fn entity_exists(&self, id: ObjectId) -> bool {
|
||||
self.kinds.contains_key(&id)
|
||||
@@ -138,51 +138,6 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Approximate mesh volume/centroid for the mock (divergence over triangles).
|
||||
trait MeshApprox {
|
||||
fn volume_approx(&self) -> f64;
|
||||
fn centroid_approx(&self) -> [f64; 3];
|
||||
}
|
||||
impl MeshApprox for cadkernel::brep::Mesh {
|
||||
fn volume_approx(&self) -> f64 {
|
||||
let mut vol = 0.0;
|
||||
for t in &self.triangles {
|
||||
let v0 = self.positions[t[0]];
|
||||
let v1 = self.positions[t[1]];
|
||||
let v2 = self.positions[t[2]];
|
||||
let cross = [
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
];
|
||||
vol += (v0[0] * cross[0] + v0[1] * cross[1] + v0[2] * cross[2]) / 6.0;
|
||||
}
|
||||
vol
|
||||
}
|
||||
fn centroid_approx(&self) -> [f64; 3] {
|
||||
let mut vol = 0.0;
|
||||
let mut c = [0.0; 3];
|
||||
for t in &self.triangles {
|
||||
let v0 = self.positions[t[0]];
|
||||
let v1 = self.positions[t[1]];
|
||||
let v2 = self.positions[t[2]];
|
||||
let cross = [
|
||||
v1[1] * v2[2] - v1[2] * v2[1],
|
||||
v1[2] * v2[0] - v1[0] * v2[2],
|
||||
v1[0] * v2[1] - v1[1] * v2[0],
|
||||
];
|
||||
let tet = (v0[0] * cross[0] + v0[1] * cross[1] + v0[2] * cross[2]) / 6.0;
|
||||
vol += tet;
|
||||
for i in 0..3 {
|
||||
c[i] += (v0[i] + v1[i] + v2[i]) * tet;
|
||||
}
|
||||
}
|
||||
if vol.abs() < 1e-12 {
|
||||
return [0.0; 3];
|
||||
}
|
||||
[c[0] / (4.0 * vol), c[1] / (4.0 * vol), c[2] / (4.0 * vol)]
|
||||
}
|
||||
}
|
||||
|
||||
fn api() -> (DocApi, Arc<ocs_doc_api::transport::InProcess<MockBackend>>) {
|
||||
let tp = Arc::new(ocs_doc_api::transport::InProcess::new(MockBackend::default()));
|
||||
@@ -196,7 +151,7 @@ fn cuboid_intersect_cuboid_atomic_ops_and_revision() {
|
||||
let mut grp = ocs_doc_api::OpGroup::new();
|
||||
|
||||
let rev0 = doc.revision().unwrap();
|
||||
// Two overlapping boxes — the kernel's own boolean test case (boolean.rs pair()).
|
||||
// Two overlapping boxes — the kernel's own boolean test case (boolean.rs pair()).
|
||||
let block = grp.track(doc.solids().create_cuboid([0.0, 0.0, 0.0], [10.0, 10.0, 10.0])).unwrap();
|
||||
let other = grp.track(doc.solids().create_cuboid([5.0, 5.0, 5.0], [10.0, 10.0, 10.0])).unwrap();
|
||||
assert_eq!(doc.revision().unwrap().as_u64(), rev0.as_u64() + 2);
|
||||
@@ -215,7 +170,7 @@ fn cuboid_intersect_cuboid_atomic_ops_and_revision() {
|
||||
.unwrap();
|
||||
let bb = res.bounds(0).unwrap();
|
||||
let vol = res.volume(1).unwrap();
|
||||
// Intersection of [0,10]³ ∩ [5,15]³ = [5,10]³ → bounds + volume 125.
|
||||
// Intersection of [0,10]³ ∩ [5,15]³ = [5,10]³ → bounds + volume 125.
|
||||
assert!((bb.min[0] - 5.0).abs() < 1e-6 && (bb.max[0] - 10.0).abs() < 1e-6, "bounds {bb:?}");
|
||||
assert!(vol > 0.0, "intersection volume must be positive");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user