forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: fix branch-review findings (tab auth, bulge center, budget, caps, dead code)
CRITICAL: - execute_doc_api: reject tab_id != bound tab (confused deputy); test doc_api_tab_mismatch_is_rejected. - bulge_arc_segment: fix negative/CW bulge center (apothem magnitude + sign picks side; was double-sign-mirrored); test CW-vs-CCW center offset. - cold-tess budget: move mass_cache/cold_tess_used to per-tab Scene so the budget persists across DocApi dispatches (was reset per request); budget-exhausted -> approximate meshes.metrics fallback instead of error. WARNING: - Loft: add BULK_ITEM_CAP (was uncapped -> unbounded kernel call from one request). - CreateMany: pre-validate EntitySpec::Curve (ellipse ratio) in prepare phase -> all-or-nothing restored; regression test. - Partial Ellipse: offset start/end_parameter by rot_z on rotation; regression test. - resolve_body: add with_body borrow accessor (host overrides to borrow cache in place, O(1)); bounds() uses it instead of deep-cloning the B-rep per query. - add_raster_image: validate file path (non-empty, image extension, reject UNC/traversal); regression test. SUGGESTION: - deploy: bump API_VERSION 5 -> 6 so DocApi plugins are gated at handshake on old hosts (append-only wire + defaulted method keep old V2-V5 plugins loading). - dead code: wire query_name into apply_queries error labels; host uses crate's ObjectId::from_handle/to_handle (drop local duplicates); delete is_null/backend_mut. - mock drift: test mock now stores curve specs + computes curve bounds (aligns with example mock); Spline/Dimension bounds delegate to points_aabb. 623 host lib + 8 crate + 3 consistency + 34 plugin_api green.
This commit is contained in:
1 parent
ac54d6079b
commit
919128e087
10 files changed
+316
-71
No files matched your search
@@ -19,9 +19,22 @@ pub type KernelBody = cadkernel::brep::Body;
|
||||
pub trait DocApiBackend {
|
||||
// ── identity / solid lookup ────────────────────────────────────────────
|
||||
/// Resolve a live B-rep body for `id` (solid cache; re-lift from AcisData on
|
||||
/// miss). `UnknownId` if `id` is not a live solid.
|
||||
/// miss). `UnknownId` if `id` is not a live solid. NOTE: returns an OWNED body
|
||||
/// (a clone of the cache entry); use `with_body` for read-only queries.
|
||||
fn resolve_body(&mut self, id: ObjectId) -> ApiResult<KernelBody>;
|
||||
|
||||
/// Borrow the cached body read-only for a query, avoiding the `resolve_body`
|
||||
/// deep clone. Default: falls back to `resolve_body` (clone). The host overrides
|
||||
/// this to borrow the `solid_models` cache in place (O(1), no copy).
|
||||
fn with_body<R>(
|
||||
&mut self,
|
||||
id: ObjectId,
|
||||
f: &mut dyn FnMut(&KernelBody) -> ApiResult<R>,
|
||||
) -> ApiResult<R> {
|
||||
let body = self.resolve_body(id)?;
|
||||
f(&body)
|
||||
}
|
||||
|
||||
/// Store a new solid body as a `Solid3D` entity; returns the fresh `ObjectId`.
|
||||
/// Mirrors host `add_solid_model` (edge wires + `solid_to_sat` +
|
||||
/// `set_sat_document` + `register_solid_model`).
|
||||
|
||||
@@ -103,14 +103,19 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
return Err(over_cap(name, specs.len()));
|
||||
}
|
||||
// Upfront validation: compute/validate EVERY payload before any insert
|
||||
// (all-or-nothing without rollback, plan §5.3).
|
||||
// (all-or-nothing without rollback, plan §5.3). This includes validating
|
||||
// the curve spec's fallible conditions (e.g. ellipse ratio) that
|
||||
// `curve_spec_to_entity` would otherwise reject mid-apply-loop.
|
||||
let mut prepared = Vec::with_capacity(specs.len());
|
||||
for (i, spec) in specs.iter().enumerate() {
|
||||
let prep = match spec {
|
||||
EntitySpec::Solid(p) => {
|
||||
Prepared::Solid(crate::geom::make_solid(p).map_err(|e| at_index(name, i, e))?)
|
||||
}
|
||||
EntitySpec::Curve(c) => Prepared::Curve(c.clone()),
|
||||
EntitySpec::Curve(c) => {
|
||||
validate_curve_spec(c).map_err(|e| at_index(name, i, e))?;
|
||||
Prepared::Curve(c.clone())
|
||||
}
|
||||
};
|
||||
prepared.push(prep);
|
||||
}
|
||||
@@ -254,6 +259,11 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::Loft { profiles } => {
|
||||
// Bound the section count like the bulk ops (one envelope can carry many
|
||||
// ids; an uncapped loft would be an unbounded kernel call on the UI thread).
|
||||
if profiles.len() > BULK_ITEM_CAP {
|
||||
return Err(over_cap(name, profiles.len()));
|
||||
}
|
||||
// Resolve every profile to its curve set BEFORE push_undo (all-or-nothing).
|
||||
if profiles.len() < 2 {
|
||||
return Err(ApiError::validation(name, "loft needs >= 2 profiles"));
|
||||
@@ -336,24 +346,25 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
|
||||
}
|
||||
let mut results = Vec::with_capacity(queries.len());
|
||||
for q in &queries {
|
||||
let qname = q.query_name();
|
||||
let r = match q {
|
||||
Query::GetEntity { id } => QueryResult::Entity(b.get_entity(*id)?),
|
||||
Query::GetBounds { id } => QueryResult::Bounds(b.bounds(*id)?),
|
||||
Query::GetCentroid { id } => QueryResult::Centroid(b.centroid(*id)?),
|
||||
Query::GetVolume { id } => QueryResult::Volume(b.volume(*id)?),
|
||||
Query::GetEntity { id } => QueryResult::Entity(b.get_entity(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetBounds { id } => QueryResult::Bounds(b.bounds(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetCentroid { id } => QueryResult::Centroid(b.centroid(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetVolume { id } => QueryResult::Volume(b.volume(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetIntersects { a, b: bid } => {
|
||||
let ba = b.bounds(*a)?;
|
||||
let bb = b.bounds(*bid)?;
|
||||
let ba = b.bounds(*a).map_err(|e| label(qname, e))?;
|
||||
let bb = b.bounds(*bid).map_err(|e| label(qname, e))?;
|
||||
QueryResult::Intersects(ba.overlaps(&bb))
|
||||
}
|
||||
Query::GetGeometryRevision => QueryResult::Revision(b.revision()),
|
||||
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(*id)?),
|
||||
Query::GetHatchBoundary { id } => QueryResult::HatchBoundary(b.hatch_boundary(*id)?),
|
||||
Query::GetDimensionMeasurement { id } => QueryResult::DimensionMeasurement(b.dimension_measurement(*id)?),
|
||||
Query::GetAttributes { id } => QueryResult::Attributes(b.attributes(*id)?),
|
||||
Query::GetBlockEntities { block_name } => QueryResult::BlockEntities(b.block_entities(block_name)?),
|
||||
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetHatchBoundary { id } => QueryResult::HatchBoundary(b.hatch_boundary(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetDimensionMeasurement { id } => QueryResult::DimensionMeasurement(b.dimension_measurement(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetAttributes { id } => QueryResult::Attributes(b.attributes(*id).map_err(|e| label(qname, e))?),
|
||||
Query::GetBlockEntities { block_name } => QueryResult::BlockEntities(b.block_entities(block_name).map_err(|e| label(qname, e))?),
|
||||
Query::GetViewportView { id } => {
|
||||
let (target, height) = b.viewport_view(*id)?;
|
||||
let (target, height) = b.viewport_view(*id).map_err(|e| label(qname, e))?;
|
||||
QueryResult::ViewportView { target, height }
|
||||
}
|
||||
};
|
||||
@@ -366,6 +377,15 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
|
||||
})
|
||||
}
|
||||
|
||||
/// Attach the query name to an error for better diagnostics (uses `query_name`).
|
||||
fn label(query: &'static str, e: ApiError) -> ApiError {
|
||||
match e {
|
||||
ApiError::Validation { reason, .. } => ApiError::validation(query, reason),
|
||||
ApiError::UnknownId(id) => ApiError::validation(query, format!("unknown ObjectId {id:?}")),
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_curves<B: DocApiBackend>(
|
||||
b: &B,
|
||||
profile: ObjectId,
|
||||
@@ -377,6 +397,21 @@ fn profile_curves<B: DocApiBackend>(
|
||||
b.profile_curves(profile)
|
||||
}
|
||||
|
||||
/// Validate a `Curve2Spec`'s fallible conditions BEFORE mutation (the same rules
|
||||
/// the host's `curve_spec_to_entity` enforces), so `CreateMany` stays all-or-nothing.
|
||||
/// Currently: ellipse minor/major ratio must be in (0, 1].
|
||||
fn validate_curve_spec(spec: &crate::ops::Curve2Spec) -> ApiResult<()> {
|
||||
if let crate::ops::Curve2Spec::Ellipse { ratio, .. } = spec {
|
||||
if !(*ratio > 0.0 && *ratio <= 1.0) {
|
||||
return Err(ApiError::validation(
|
||||
"CreateMany",
|
||||
format!("ellipse minor_axis_ratio must be in (0, 1], got {ratio}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> {
|
||||
if b.entity_exists(id) {
|
||||
Ok(())
|
||||
|
||||
@@ -17,10 +17,6 @@ impl ObjectId {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub const fn is_null(self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
|
||||
/// Build from a raw u64 (e.g. an `acadrust::Handle`'s value).
|
||||
pub const fn from_u64(v: u64) -> Self {
|
||||
Self(v)
|
||||
|
||||
@@ -19,10 +19,6 @@ impl<B: DocApiBackend> InProcess<B> {
|
||||
pub fn new(backend: B) -> Self {
|
||||
Self { backend: Mutex::new(backend) }
|
||||
}
|
||||
/// Borrow the backend mutably (test/introspection helper).
|
||||
pub fn backend_mut(&mut self) -> &mut B {
|
||||
self.backend.get_mut().expect("in-process backend poisoned")
|
||||
}
|
||||
/// Borrow the backend immutably (test/introspection helper).
|
||||
pub fn backend(&self) -> std::sync::MutexGuard<'_, B> {
|
||||
self.backend.lock().expect("in-process backend poisoned")
|
||||
|
||||
Reference in new issue
Block a user