forked from 1b-agent/opencadstudio-upstream
Merge PR #1028: add document API with atomic edits
Add the typed document facade and plugin IPC adapter. Prepare bulk edits before mutation, preserve undo and dirty state, keep profile elevations, validate numeric inputs and session bindings, and generate Python constructors with typed results. Reuse existing entity transforms and profile conversion. Compute mass properties and closed-surface bounds in cadkernel; pin its main revision 6a5e6fc2822f6af6b2611cd605b44437b35158f0. Keep plugin API metadata at version 6. Validation: 646 native application tests, 597 kernel tests, 12 document API tests, 91 plugin API tests, 3 Python tests, default/IPC feature builds and wasm32 check passed.
This commit is contained in:
commit
3385160493
50 files changed
+11481
-6
No files matched your search
@@ -0,0 +1,230 @@
|
||||
//! The thin host hook. The host implements this trait;
|
||||
//! the crate's [`crate::executor`] drives it. Both the in-process transport and
|
||||
//! the host's IPC executor call into it. Logic lives in the versioned crate; the
|
||||
//! host only provides scene primitives.
|
||||
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
use crate::id::ObjectId;
|
||||
use crate::ops::Curve2Spec;
|
||||
use crate::query::{Aabb, EntityView};
|
||||
use crate::revision::GeometryRevision;
|
||||
|
||||
/// A kernel B-rep solid body, opaque to the wire. On `host` this is
|
||||
/// `cadkernel::brep::Body`; kernel keys never cross the API (decision #7).
|
||||
pub type KernelBody = cadkernel::brep::Body;
|
||||
|
||||
/// The per-op host hook. Every method maps to an already-present scene primitive.
|
||||
/// The executor calls `push_undo` BEFORE applying a write op and `finalize_op`
|
||||
/// AFTER — recording exactly one undo step + one geometry bump per applied op.
|
||||
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. 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`).
|
||||
fn store_solid(&mut self, body: &KernelBody) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Replace the body of an existing solid in place (same `ObjectId`).
|
||||
/// Re-embeds SAT and refreshes the solid cache.
|
||||
fn update_solid(&mut self, id: ObjectId, body: &KernelBody) -> ApiResult<()>;
|
||||
|
||||
// ── plain (2D) entities ────────────────────────────────────────────────
|
||||
/// Add a 2D curve entity; returns the fresh `ObjectId`.
|
||||
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Prepare every entity before recording one undo step and committing the batch.
|
||||
fn create_many(&mut self, specs: &[crate::ops::EntitySpec]) -> ApiResult<Vec<ObjectId>>;
|
||||
|
||||
/// Prepare all transformed geometry before committing any entity.
|
||||
fn transform_many(
|
||||
&mut self,
|
||||
ids: &[ObjectId],
|
||||
placement: &crate::PlacementSpec,
|
||||
) -> ApiResult<()>;
|
||||
|
||||
/// Add a block reference (`INSERT`). Validates `block_name` exists; returns
|
||||
/// the fresh `ObjectId`. `Validation` if the block is unknown.
|
||||
fn add_insert(&mut self, spec: &crate::ops::InsertSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a paper-space `VIEWPORT`; returns the fresh `ObjectId`.
|
||||
fn add_viewport(&mut self, spec: &crate::ops::ViewportSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a single-line `TEXT` annotation; returns the fresh `ObjectId`.
|
||||
fn add_text(&mut self, spec: &crate::ops::TextSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a multi-line `MTEXT` annotation; returns the fresh `ObjectId`.
|
||||
fn add_mtext(&mut self, spec: &crate::ops::MTextSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Set the text content of a Text/MText annotation in place.
|
||||
fn set_text_content(&mut self, id: ObjectId, value: &str) -> ApiResult<()>;
|
||||
|
||||
/// The text content of a Text/MText annotation. `Unsupported` for other kinds.
|
||||
fn text_content(&self, id: ObjectId) -> ApiResult<String>;
|
||||
|
||||
/// Add a `HATCH` (single closed polyline boundary); returns the fresh `ObjectId`.
|
||||
fn add_hatch(&mut self, spec: &crate::ops::HatchSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// A hatch's boundary loops (outer + islands) as closed polylines.
|
||||
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>>;
|
||||
|
||||
/// Add a linear `DIMENSION`; returns the fresh `ObjectId`.
|
||||
fn add_dimension_linear(&mut self, spec: &crate::ops::DimensionSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a radial `DIMENSION`; returns the fresh `ObjectId`.
|
||||
fn add_dimension_radius(
|
||||
&mut self,
|
||||
spec: &crate::ops::DimensionRadialSpec,
|
||||
) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a diameter `DIMENSION`; returns the fresh `ObjectId`.
|
||||
fn add_dimension_diameter(
|
||||
&mut self,
|
||||
spec: &crate::ops::DimensionRadialSpec,
|
||||
) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a 3-point angular `DIMENSION`; returns the fresh `ObjectId`.
|
||||
fn add_dimension_angular(
|
||||
&mut self,
|
||||
spec: &crate::ops::DimensionAngularSpec,
|
||||
) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add an `ATTDEF` (in-block attribute definition); returns the fresh `ObjectId`.
|
||||
fn add_attribute_definition(
|
||||
&mut self,
|
||||
spec: &crate::ops::AttributeDefinitionSpec,
|
||||
) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a `TABLE` (rows × columns grid); returns the fresh `ObjectId`.
|
||||
fn add_table(&mut self, spec: &crate::ops::TableSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a 2-line angular `DIMENSION`; returns the fresh `ObjectId`.
|
||||
fn add_dimension_angular2ln(
|
||||
&mut self,
|
||||
spec: &crate::ops::DimensionAngularSpec,
|
||||
) -> ApiResult<ObjectId>;
|
||||
|
||||
/// A dimension's measured value (distance for linear/radius, degrees for angular).
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64>;
|
||||
|
||||
/// Set an attribute `value` for `tag` on an insert (adds if absent). Insert-only.
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()>;
|
||||
|
||||
/// An insert's attributes as (tag, value) pairs. Insert-only.
|
||||
fn attributes(&self, id: ObjectId) -> ApiResult<Vec<(String, String)>>;
|
||||
|
||||
/// The entities inside a block definition (read-only traversal), as views.
|
||||
fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>>;
|
||||
|
||||
/// Set a viewport's view target + zoom height in place. Viewport-only.
|
||||
fn set_viewport_view(
|
||||
&mut self,
|
||||
id: ObjectId,
|
||||
view_target: [f64; 3],
|
||||
view_height: f64,
|
||||
) -> ApiResult<()>;
|
||||
|
||||
/// A viewport's view (target WCS + zoom height). Viewport-only.
|
||||
fn viewport_view(&self, id: ObjectId) -> ApiResult<([f64; 3], f64)>;
|
||||
|
||||
/// Add a `RASTER_IMAGE` (host auto-registers the ImageDefinition).
|
||||
fn add_raster_image(&mut self, spec: &crate::ops::RasterImageSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Loft a solid through >= 2 profile curve sets (each already resolved to
|
||||
/// `geom2d::Curve`s on the XY plane). Returns the fresh `ObjectId`.
|
||||
fn loft(
|
||||
&mut self,
|
||||
sections: &[(cadkernel::space::Plane, Vec<cadkernel::geom2d::Curve>)],
|
||||
) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Can `id` be modified in place right now (exists, is the expected family,
|
||||
/// not on a locked layer)? Read-only pre-check used before mutations.
|
||||
/// Default: existence + not-locked (backends narrow the family check).
|
||||
fn can_modify(&self, id: ObjectId) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::UnknownId(id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove any first-level entity by id. Returns `false` if absent; returns
|
||||
/// `Err` if the entity exists but cannot be removed (e.g. locked layer).
|
||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool>;
|
||||
|
||||
/// Can `id` be removed right now (exists and is not blocked, e.g. by a locked
|
||||
/// layer)? Read-only; used by the boolean-erase path to validate BEFORE any
|
||||
/// mutation so a mid-op failure cannot leave partial state. Default: existence-only.
|
||||
fn can_remove(&self, id: ObjectId) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::UnknownId(id))
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic, untyped view of any entity (id + kind + coarse bounds).
|
||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView>;
|
||||
|
||||
/// The 2D curve geometry of a profile entity (polyline/closed curve) for
|
||||
/// sweep ops (`Extrude`/`Revolve`). `UnknownId`/`Unsupported` if not a profile.
|
||||
fn profile_curves(&self, id: ObjectId) -> ApiResult<Vec<cadkernel::geom2d::Curve>>;
|
||||
|
||||
fn profile_plane(&self, _id: ObjectId) -> ApiResult<cadkernel::space::Plane> {
|
||||
Ok(cadkernel::space::Plane::XY)
|
||||
}
|
||||
|
||||
/// Apply a rigid similarity to any entity in place (same `ObjectId`).
|
||||
fn transform_entity(
|
||||
&mut self,
|
||||
id: ObjectId,
|
||||
placement: &crate::ops::PlacementSpec,
|
||||
) -> ApiResult<()>;
|
||||
|
||||
/// Validate that `id` exists and can be transformed, WITHOUT mutating.
|
||||
/// Used by `TransformMany` to pre-validate all ids before `push_undo` so the
|
||||
/// apply loop cannot fail part-way (all-or-nothing, the bulk contract). Default:
|
||||
/// existence-only (backends that restrict transform to a family override).
|
||||
fn ensure_transformable(&mut self, id: ObjectId) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::UnknownId(id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a vertex into a polyline at index `at` (polyline family only).
|
||||
fn add_vertex(&mut self, id: ObjectId, at: usize, point: [f64; 3]) -> ApiResult<()>;
|
||||
|
||||
// ── queries (no undo, no bump; `&mut` because solid resolution may
|
||||
// populate the kernel body cache on miss) ─────────────────────────────
|
||||
fn bounds(&mut self, id: ObjectId) -> ApiResult<Aabb>;
|
||||
fn centroid(&mut self, id: ObjectId) -> ApiResult<[f64; 3]>;
|
||||
fn volume(&mut self, id: ObjectId) -> ApiResult<f64>;
|
||||
fn entity_exists(&self, id: ObjectId) -> bool;
|
||||
fn revision(&self) -> GeometryRevision;
|
||||
|
||||
// ── per-op boundary ────────────────────────────────────────────────────
|
||||
/// Called BEFORE applying a write op (records the before-state undo snapshot).
|
||||
fn push_undo(&mut self, label: &str);
|
||||
/// Called AFTER a successful write op (one DeltaSnapshot + one geometry bump
|
||||
/// + one publish). NOT called on failure (a failed op records nothing).
|
||||
fn finalize_op(&mut self);
|
||||
/// Discard an undo capture after preparation or validation failed.
|
||||
fn cancel_op(&mut self) {}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
//! Entity conversions: map the crate's plain-data DTOs to acadrust
|
||||
//! `EntityType` and back. Host feature only (uses the real acadrust types). This
|
||||
//! module is host-side-capable but host-independent — it operates purely on
|
||||
//! acadrust entities and crate DTOs, so any host can reuse it.
|
||||
|
||||
use acadrust::entities::{
|
||||
Arc, Circle, Ellipse, Line, LwPolyline, LwVertex, Point, Ray, Spline, XLine,
|
||||
};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
|
||||
use crate::{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).
|
||||
pub fn entity_to_profile_curves(entity: &EntityType) -> ApiResult<Vec<cadkernel::geom2d::Curve>> {
|
||||
use cadkernel::geom2d::{Arc as KArc, Circle as KCircle, Curve as KCurve, Line as KLine};
|
||||
let curves = match entity {
|
||||
EntityType::Line(l) => vec![KCurve::Line(KLine {
|
||||
start: [l.start.x, l.start.y],
|
||||
end: [l.end.x, l.end.y],
|
||||
})],
|
||||
EntityType::Circle(c) => vec![KCurve::Circle(KCircle {
|
||||
centre: [c.center.x, c.center.y],
|
||||
radius: c.radius,
|
||||
})],
|
||||
EntityType::Arc(a) => vec![KCurve::Arc(KArc {
|
||||
centre: [a.center.x, a.center.y],
|
||||
radius: a.radius,
|
||||
start_angle: a.start_angle,
|
||||
end_angle: a.end_angle,
|
||||
})],
|
||||
EntityType::LwPolyline(pl) => {
|
||||
if pl.vertices.len() < 2 {
|
||||
return Err(ApiError::validation(
|
||||
"profile",
|
||||
"polyline profile needs at least two vertices",
|
||||
));
|
||||
}
|
||||
KCurve::Polyline(cadkernel::geom2d::Polyline {
|
||||
vertices: pl
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| cadkernel::geom2d::PolylineVertex {
|
||||
position: [v.location.x, v.location.y],
|
||||
bulge: v.bulge,
|
||||
})
|
||||
.collect(),
|
||||
closed: pl.is_closed,
|
||||
})
|
||||
.segments()
|
||||
}
|
||||
_ => {
|
||||
return Err(ApiError::Unsupported(
|
||||
"profile conversion is supported for Line/Circle/Arc/LwPolyline only".into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(curves)
|
||||
}
|
||||
|
||||
/// Apply a rigid similarity (rotation+translation+uniform scale) to a non-solid
|
||||
/// entity in place. Supports the common 2D families; `Solid3D` goes through the
|
||||
/// kernel path instead. Returns the transformed entity (same handle) for the
|
||||
/// caller to `update_entity`.
|
||||
pub fn transform_entity_geometry(
|
||||
entity: &EntityType,
|
||||
p: &crate::PlacementSpec,
|
||||
) -> ApiResult<EntityType> {
|
||||
crate::geom::validate_placement(p)?;
|
||||
let matrix = acadrust::types::Matrix4 {
|
||||
m: [
|
||||
[p.x_axis[0], p.y_axis[0], p.z_axis[0], p.origin[0]],
|
||||
[p.x_axis[1], p.y_axis[1], p.z_axis[1], p.origin[1]],
|
||||
[p.x_axis[2], p.y_axis[2], p.z_axis[2], p.origin[2]],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
],
|
||||
};
|
||||
let mut moved = entity.clone();
|
||||
moved.apply_transform(&acadrust::types::Transform::from_matrix(matrix));
|
||||
Ok(moved)
|
||||
}
|
||||
|
||||
fn v3(p: [f64; 3]) -> Vector3 {
|
||||
Vector3::new(p[0], p[1], p[2])
|
||||
}
|
||||
|
||||
/// Convert a polyline bulge segment (start → end, bulge = tan(θ/4)) to a
|
||||
/// `geom2d::Curve::Arc`. Positive bulge = counter-clockwise arc; negative = clockwise.
|
||||
pub fn bulge_arc_segment(
|
||||
start: [f64; 2],
|
||||
end: [f64; 2],
|
||||
bulge: f64,
|
||||
) -> ApiResult<cadkernel::geom2d::Curve> {
|
||||
use cadkernel::geom2d::{Arc as KArc, Curve as KCurve};
|
||||
let arc = cadkernel::geom2d::BulgeArc::from_bulge(start, end, bulge)
|
||||
.ok_or_else(|| ApiError::validation("profile", "invalid bulge segment"))?;
|
||||
// The kernel's Arc is counter-clockwise; reverse clockwise endpoints.
|
||||
let (start_angle, end_angle) = if arc.sweep < 0.0 {
|
||||
(arc.end_angle, arc.start_angle)
|
||||
} else {
|
||||
(arc.start_angle, arc.end_angle)
|
||||
};
|
||||
Ok(KCurve::Arc(KArc {
|
||||
centre: arc.center,
|
||||
radius: arc.radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Build an acadrust entity for a 2D curve construction spec.
|
||||
pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
||||
crate::validation::curve(spec)?;
|
||||
let entity = match spec {
|
||||
Curve2Spec::Line { start, end } => {
|
||||
let mut e = Line::new();
|
||||
e.start = v3(*start);
|
||||
e.end = v3(*end);
|
||||
EntityType::Line(e)
|
||||
}
|
||||
Curve2Spec::Circle { centre, radius } => {
|
||||
let mut e = Circle::new();
|
||||
e.center = v3(*centre);
|
||||
e.radius = *radius;
|
||||
EntityType::Circle(e)
|
||||
}
|
||||
Curve2Spec::Polyline { points, closed } => {
|
||||
let mut e = LwPolyline::new();
|
||||
e.vertices = points
|
||||
.iter()
|
||||
.map(|p| LwVertex {
|
||||
location: Vector2::new(p[0], p[1]),
|
||||
bulge: 0.0,
|
||||
start_width: 0.0,
|
||||
end_width: 0.0,
|
||||
vertex_id: 0,
|
||||
})
|
||||
.collect();
|
||||
e.elevation = points.first().map_or(0.0, |p| p[2]);
|
||||
if points.iter().any(|p| p[2] != e.elevation) {
|
||||
return Err(ApiError::validation(
|
||||
"CreateCurve",
|
||||
"polyline points must share an elevation",
|
||||
));
|
||||
}
|
||||
e.is_closed = *closed;
|
||||
EntityType::LwPolyline(e)
|
||||
}
|
||||
Curve2Spec::Point { position } => {
|
||||
let mut e = Point::new();
|
||||
e.location = v3(*position);
|
||||
EntityType::Point(e)
|
||||
}
|
||||
Curve2Spec::Arc {
|
||||
centre,
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
} => {
|
||||
let mut e = Arc::new();
|
||||
e.center = v3(*centre);
|
||||
e.radius = *radius;
|
||||
e.start_angle = *start_angle;
|
||||
e.end_angle = *end_angle;
|
||||
EntityType::Arc(e)
|
||||
}
|
||||
Curve2Spec::Ellipse {
|
||||
centre,
|
||||
major_axis,
|
||||
ratio,
|
||||
start,
|
||||
end,
|
||||
} => {
|
||||
// Validate the minor/major ratio so the (major-axis) bounds never
|
||||
// under-bound: ratio must be in (0, 1].
|
||||
if !(*ratio > 0.0 && *ratio <= 1.0) {
|
||||
return Err(ApiError::validation(
|
||||
"CreateCurve",
|
||||
format!("ellipse minor_axis_ratio must be in (0, 1], got {ratio}"),
|
||||
));
|
||||
}
|
||||
let mut e = Ellipse::new();
|
||||
e.center = v3(*centre);
|
||||
e.major_axis = v3(*major_axis);
|
||||
e.minor_axis_ratio = *ratio;
|
||||
e.start_parameter = *start;
|
||||
e.end_parameter = *end;
|
||||
EntityType::Ellipse(e)
|
||||
}
|
||||
Curve2Spec::Spline {
|
||||
degree,
|
||||
control_points,
|
||||
knots,
|
||||
weights,
|
||||
} => {
|
||||
let mut e = Spline::new();
|
||||
e.degree = *degree;
|
||||
e.control_points = control_points.iter().map(|p| v3(*p)).collect();
|
||||
e.knots = knots.clone();
|
||||
e.weights = weights.clone();
|
||||
EntityType::Spline(e)
|
||||
}
|
||||
Curve2Spec::Ray { origin, direction } => {
|
||||
EntityType::Ray(Ray::new(v3(*origin), v3(*direction)))
|
||||
}
|
||||
Curve2Spec::XLine { origin, direction } => {
|
||||
EntityType::XLine(XLine::new(v3(*origin), v3(*direction)))
|
||||
}
|
||||
};
|
||||
Ok(entity)
|
||||
}
|
||||
|
||||
/// The acadrust `EntityType` variant name (for `EntityView::kind` / downcasts).
|
||||
pub fn entity_kind_name(entity: &EntityType) -> &'static str {
|
||||
match entity {
|
||||
EntityType::Solid3D(_) => "Solid3D",
|
||||
EntityType::Line(_) => "Line",
|
||||
EntityType::Circle(_) => "Circle",
|
||||
EntityType::LwPolyline(_) => "LwPolyline",
|
||||
EntityType::Polyline(_) => "Polyline",
|
||||
EntityType::Point(_) => "Point",
|
||||
EntityType::Arc(_) => "Arc",
|
||||
EntityType::Ellipse(_) => "Ellipse",
|
||||
EntityType::Spline(_) => "Spline",
|
||||
EntityType::Ray(_) => "Ray",
|
||||
EntityType::XLine(_) => "XLine",
|
||||
EntityType::Insert(_) => "Insert",
|
||||
EntityType::Viewport(_) => "Viewport",
|
||||
EntityType::Text(_) => "Text",
|
||||
EntityType::MText(_) => "MText",
|
||||
EntityType::AttributeDefinition(_) => "AttributeDefinition",
|
||||
EntityType::AttributeEntity(_) => "AttributeEntity",
|
||||
// Media & misc (phase 5, read-mostly): named so GetEntity reports a real kind.
|
||||
EntityType::RasterImage(_) => "RasterImage",
|
||||
EntityType::Table(_) => "Table",
|
||||
EntityType::Leader(_) => "Leader",
|
||||
EntityType::MultiLeader(_) => "MultiLeader",
|
||||
EntityType::MLine(_) => "MLine",
|
||||
EntityType::Mesh(_) => "Mesh",
|
||||
EntityType::Helix(_) => "Helix",
|
||||
EntityType::Region(_) => "Region",
|
||||
EntityType::Body(_) => "Body",
|
||||
EntityType::Surface(_) => "Surface",
|
||||
EntityType::Face3D(_) => "Face3D",
|
||||
EntityType::Dimension(_) => "Dimension",
|
||||
EntityType::Hatch(_) => "Hatch",
|
||||
_ => "Other",
|
||||
}
|
||||
}
|
||||
|
||||
/// Existing entity bounds; unbounded or empty geometry returns an error.
|
||||
pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult<Aabb> {
|
||||
let entity = entity.ok_or(ApiError::UnknownId(id))?;
|
||||
if matches!(entity, EntityType::Ray(_) | EntityType::XLine(_)) {
|
||||
return Err(ApiError::Unsupported("unbounded entity".into()));
|
||||
}
|
||||
let bounds = entity.as_entity().bounding_box();
|
||||
let min = [bounds.min.x, bounds.min.y, bounds.min.z];
|
||||
let max = [bounds.max.x, bounds.max.y, bounds.max.z];
|
||||
if min.iter().chain(&max).any(|v| !v.is_finite()) {
|
||||
return Err(ApiError::geometry(
|
||||
GeometryErrorKind::Empty,
|
||||
"entity has no finite bounds",
|
||||
));
|
||||
}
|
||||
Ok(Aabb { min, max })
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//! The wire envelope. One request = ONE write op OR a query batch
|
||||
//! (read-only batching is safe: no mutation, no undo, no rollback).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::gen::{Operation, Query};
|
||||
use crate::id::ObjectId;
|
||||
use crate::query::QueryResult;
|
||||
use crate::revision::GeometryRevision;
|
||||
use crate::ENVELOPE_VERSION;
|
||||
|
||||
/// The single request unit crossing the transport.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DocApiEnvelope {
|
||||
/// Crate protocol version (`ENVELOPE_VERSION`). Bridges must check this.
|
||||
pub version: u16,
|
||||
pub body: EnvelopeBody,
|
||||
}
|
||||
|
||||
impl DocApiEnvelope {
|
||||
pub fn validate_version(&self) -> crate::ApiResult<()> {
|
||||
if self.version != ENVELOPE_VERSION {
|
||||
return Err(crate::ApiError::Unsupported(format!(
|
||||
"envelope version {}",
|
||||
self.version
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn op(operation: Operation) -> Self {
|
||||
Self {
|
||||
version: ENVELOPE_VERSION,
|
||||
body: EnvelopeBody::Op(operation),
|
||||
}
|
||||
}
|
||||
pub fn queries(queries: Vec<Query>) -> Self {
|
||||
Self {
|
||||
version: ENVELOPE_VERSION,
|
||||
body: EnvelopeBody::Queries(queries),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EnvelopeBody {
|
||||
/// Exactly ONE write op (write batching removed, plan §13). Atomic by itself.
|
||||
Op(Operation),
|
||||
/// One-or-more read-only queries answered together (safe: no mutation/undo).
|
||||
Queries(Vec<Query>),
|
||||
}
|
||||
|
||||
/// The outcome of an applied write op.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum OpOutcome {
|
||||
/// A new entity was created; carries its fresh `ObjectId`.
|
||||
NewId(ObjectId),
|
||||
/// An existing entity was updated in place (same `ObjectId` preserved).
|
||||
Updated(ObjectId),
|
||||
/// Entities were deleted; carries the deleted ids (one for `Delete`, N for `DeleteMany`).
|
||||
Deleted(Vec<ObjectId>),
|
||||
/// Many entities were created (bulk `CreateMany`); carries all fresh ids in order.
|
||||
NewIds(Vec<ObjectId>),
|
||||
}
|
||||
|
||||
impl OpOutcome {
|
||||
/// The single fresh id, for ops that create exactly one entity.
|
||||
pub fn new_id(&self) -> Option<ObjectId> {
|
||||
match self {
|
||||
OpOutcome::NewId(id) => Some(*id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// All fresh ids, for bulk creates.
|
||||
pub fn new_ids(&self) -> &[ObjectId] {
|
||||
match self {
|
||||
OpOutcome::NewIds(ids) => ids,
|
||||
OpOutcome::NewId(id) => std::slice::from_ref(id),
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The per-request result (and the query-batch answer carrier).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Receipt {
|
||||
/// Present for `EnvelopeBody::Op`; absent for pure queries.
|
||||
pub outcome: Option<OpOutcome>,
|
||||
/// One `QueryResult` per `Query` in a `Queries` batch (in order).
|
||||
pub query_results: Vec<QueryResult>,
|
||||
/// The geometry revision after this request (== before, for pure queries).
|
||||
pub new_revision: GeometryRevision,
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Structured error model. Serializable so IPC returns the
|
||||
//! same error the in-process executor produced.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::id::ObjectId;
|
||||
|
||||
/// Which kind of kernel failure occurred (a mapped `cadkernel` `Snag`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum GeometryErrorKind {
|
||||
/// The operation produced an empty / degenerate body.
|
||||
Empty,
|
||||
/// Boolean refused (coincident geometry, unsupported cut, …).
|
||||
BooleanFailed,
|
||||
/// Invalid input to a kernel constructor (e.g. non-positive radius).
|
||||
InvalidInput,
|
||||
/// ACIS lift/parse/lower failed.
|
||||
Acis,
|
||||
/// Any other kernel error (message carries detail).
|
||||
Other,
|
||||
}
|
||||
|
||||
/// The single error type for the whole API. `commit`/ops return
|
||||
/// `Result<_, ApiError>`; on a failed op nothing is applied and no undo step is
|
||||
/// recorded (per-op atomicity).
|
||||
#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ApiError {
|
||||
/// Input failed validation before any mutation (names the op + reason).
|
||||
#[error("validation failed on {op}: {reason}")]
|
||||
Validation { op: String, reason: String },
|
||||
|
||||
/// The geometry kernel reported a failure (a mapped `Snag`).
|
||||
#[error("geometry kernel: {kind:?}: {msg}")]
|
||||
Geometry {
|
||||
kind: GeometryErrorKind,
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// An `ObjectId` does not resolve to a live entity (stale/deleted/never existed).
|
||||
#[error("unknown ObjectId {0:?}")]
|
||||
UnknownId(ObjectId),
|
||||
|
||||
/// The requested capability is not supported (e.g. over-cap bulk op, unimplemented family).
|
||||
#[error("unsupported capability: {0}")]
|
||||
Unsupported(String),
|
||||
|
||||
/// The transport (IPC / channel) failed — disconnected, oversized, timeout.
|
||||
#[error("transport: {0}")]
|
||||
Transport(String),
|
||||
}
|
||||
|
||||
pub type ApiResult<T> = Result<T, ApiError>;
|
||||
|
||||
impl ApiError {
|
||||
pub fn validation(op: &'static str, reason: impl Into<String>) -> Self {
|
||||
ApiError::Validation {
|
||||
op: op.to_string(),
|
||||
reason: reason.into(),
|
||||
}
|
||||
}
|
||||
pub fn geometry(kind: GeometryErrorKind, msg: impl Into<String>) -> Self {
|
||||
ApiError::Geometry {
|
||||
kind,
|
||||
msg: msg.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
//! The op executor: crate-owned logic mapping each
|
||||
//! `Operation`/`Query` to `DocApiBackend` calls. Both the in-process transport
|
||||
//! and the host's IPC executor use this — one implementation, versioned in the
|
||||
//! crate. Per-op atomicity: a write op validates its inputs and computes its
|
||||
//! geometry BEFORE calling `push_undo`, mutates, then `finalize_op` once.
|
||||
|
||||
use crate::backend::DocApiBackend;
|
||||
use crate::envelope::{OpOutcome, Receipt};
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
use crate::gen::{Operation, Query};
|
||||
use crate::id::ObjectId;
|
||||
use crate::ops::{BoolOp, EntitySpec, BULK_ITEM_CAP};
|
||||
use crate::query::QueryResult;
|
||||
|
||||
/// Apply ONE write op atomically. On success: one undo step and an advanced revision.
|
||||
/// On failure: nothing applied, no undo, no bump (the op validates before mutating).
|
||||
pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt> {
|
||||
let result = apply_op_inner(b, op);
|
||||
if result.is_err() {
|
||||
b.cancel_op();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn apply_op_inner<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt> {
|
||||
let name = op.op_name();
|
||||
crate::validation::operation(&op)?;
|
||||
if let Operation::TransformMany { ids, .. } | Operation::DeleteMany(ids) = &op {
|
||||
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
||||
if ids.is_empty() || unique.len() != ids.len() {
|
||||
return Err(ApiError::validation(
|
||||
name,
|
||||
"batch ids must be nonempty and unique",
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Operation::Transform { placement, .. } | Operation::TransformMany { placement, .. } = &op
|
||||
{
|
||||
crate::geom::validate_placement(placement)?;
|
||||
}
|
||||
let outcome = match &op {
|
||||
Operation::CreateSolid(prim) => {
|
||||
let body = crate::geom::make_solid(prim)?;
|
||||
b.push_undo(name);
|
||||
let id = b.store_solid(&body)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateCurve(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_curve(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::Extrude { profile, direction } => {
|
||||
let curves = profile_curves(b, *profile, name)?;
|
||||
let body = crate::geom::extrude(b.profile_plane(*profile)?, &curves, *direction)?;
|
||||
b.push_undo(name);
|
||||
let id = b.store_solid(&body)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::Revolve {
|
||||
profile,
|
||||
axis,
|
||||
angle,
|
||||
} => {
|
||||
let curves = profile_curves(b, *profile, name)?;
|
||||
let (pivot, axis_dir) = *axis;
|
||||
let body =
|
||||
crate::geom::revolve(b.profile_plane(*profile)?, &curves, pivot, axis_dir, *angle)?;
|
||||
b.push_undo(name);
|
||||
let id = b.store_solid(&body)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::Transform { id, placement } => {
|
||||
require_exists(b, *id, name)?;
|
||||
b.push_undo(name);
|
||||
b.transform_entity(*id, placement)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
Operation::SolidBoolean {
|
||||
op: bop,
|
||||
a,
|
||||
b: bid,
|
||||
erase_sources,
|
||||
} => {
|
||||
// Resolve + combine BEFORE push_undo: pure computation, no mutation.
|
||||
let ba = b.resolve_body(*a).map_err(|_| ApiError::UnknownId(*a))?;
|
||||
let bb = b
|
||||
.resolve_body(*bid)
|
||||
.map_err(|_| ApiError::UnknownId(*bid))?;
|
||||
let result = crate::geom::boolean(&ba, &bb, *bop)?;
|
||||
// Validate the erase BEFORE mutating: if `b` cannot be removed (e.g.
|
||||
// locked layer), fail now rather than after `update_solid(a)` has
|
||||
// already committed a partial mutation.
|
||||
if *erase_sources {
|
||||
if a == bid {
|
||||
return Err(ApiError::validation(name, "boolean source ids must differ"));
|
||||
}
|
||||
b.can_modify(*a)?;
|
||||
b.can_remove(*bid)?;
|
||||
}
|
||||
b.push_undo(name);
|
||||
// `erase_sources`: the result lives at `a` (updated in place), `b` erased.
|
||||
// Otherwise store a fresh solid and leave both inputs untouched.
|
||||
let out_id = if *erase_sources {
|
||||
b.update_solid(*a, &result)?;
|
||||
b.remove_entity(*bid)?;
|
||||
*a
|
||||
} else {
|
||||
b.store_solid(&result)?
|
||||
};
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(out_id)
|
||||
}
|
||||
Operation::AddVertex { id, at, point } => {
|
||||
require_exists(b, *id, name)?;
|
||||
b.push_undo(name);
|
||||
b.add_vertex(*id, *at, *point)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
Operation::Delete { id } => {
|
||||
// can_remove validates existence AND removability (locked layer) before
|
||||
// any mutation, so a locked entity errors cleanly instead of a no-op
|
||||
// reported as Deleted.
|
||||
b.can_remove(*id).map_err(|e| match e {
|
||||
ApiError::UnknownId(_) => {
|
||||
ApiError::validation(name, format!("unknown ObjectId {id:?}"))
|
||||
}
|
||||
other => other,
|
||||
})?;
|
||||
b.push_undo(name);
|
||||
b.remove_entity(*id)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Deleted(vec![*id])
|
||||
}
|
||||
Operation::CreateMany(specs) => {
|
||||
if specs.len() > BULK_ITEM_CAP {
|
||||
return Err(over_cap(name, specs.len()));
|
||||
}
|
||||
if specs.is_empty() {
|
||||
return Err(ApiError::validation(name, "batch must not be empty"));
|
||||
}
|
||||
for spec in specs {
|
||||
if let EntitySpec::Curve(curve) = spec {
|
||||
crate::validation::curve(curve)?;
|
||||
}
|
||||
}
|
||||
let ids = b.create_many(specs)?;
|
||||
OpOutcome::NewIds(ids)
|
||||
}
|
||||
Operation::TransformMany { ids, placement } => {
|
||||
if ids.len() > BULK_ITEM_CAP {
|
||||
return Err(over_cap(name, ids.len()));
|
||||
}
|
||||
// Pre-validate EVERYTHING fallible before push_undo (all-or-nothing,
|
||||
// the bulk contract): every id must exist AND be transformable, so the apply
|
||||
// loop below cannot fail part-way. `transform_entity` after this is
|
||||
// infallible-by-construction for the validated ids.
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
b.ensure_transformable(*id).map_err(|e| match e {
|
||||
ApiError::UnknownId(_) => stale_index(name, i, *id),
|
||||
other => match other {
|
||||
ApiError::Unsupported(msg) => ApiError::Validation {
|
||||
op: name.to_string(),
|
||||
reason: format!("index {i}: {msg}"),
|
||||
},
|
||||
o => o,
|
||||
},
|
||||
})?;
|
||||
}
|
||||
b.transform_many(ids, placement)?;
|
||||
OpOutcome::Updated(*ids.first().unwrap_or(&ObjectId::NULL))
|
||||
}
|
||||
Operation::DeleteMany(ids) => {
|
||||
if ids.len() > BULK_ITEM_CAP {
|
||||
return Err(over_cap(name, ids.len()));
|
||||
}
|
||||
// Pre-validate existence AND removability (locked layer) for every id
|
||||
// before push_undo, so the apply loop cannot fail part-way.
|
||||
for (i, id) in ids.iter().enumerate() {
|
||||
b.can_remove(*id).map_err(|e| match e {
|
||||
ApiError::UnknownId(_) => stale_index(name, i, *id),
|
||||
ApiError::Unsupported(msg) => ApiError::Validation {
|
||||
op: name.to_string(),
|
||||
reason: format!("index {i}: {msg}"),
|
||||
},
|
||||
other => other,
|
||||
})?;
|
||||
}
|
||||
b.push_undo(name);
|
||||
for id in ids {
|
||||
b.remove_entity(*id)?;
|
||||
}
|
||||
b.finalize_op();
|
||||
OpOutcome::Deleted(ids.clone())
|
||||
}
|
||||
Operation::CreateInsert(spec) => {
|
||||
// Validate the block name exists before committing (no rollback needed).
|
||||
b.push_undo(name);
|
||||
let id = b.add_insert(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateViewport(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_viewport(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateText(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_text(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateMText(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_mtext(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::SetTextContent { id, value } => {
|
||||
b.can_modify(*id).map_err(|e| match e {
|
||||
ApiError::UnknownId(_) => {
|
||||
ApiError::validation(name, format!("unknown ObjectId {id:?}"))
|
||||
}
|
||||
other => other,
|
||||
})?;
|
||||
b.push_undo(name);
|
||||
b.set_text_content(*id, value)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
Operation::CreateHatch(spec) => {
|
||||
// Validate the boundary (>= 3 points) before committing.
|
||||
if spec.boundary.len() < 3 {
|
||||
return Err(ApiError::validation(
|
||||
name,
|
||||
"hatch boundary needs >= 3 points",
|
||||
));
|
||||
}
|
||||
b.push_undo(name);
|
||||
let id = b.add_hatch(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateDimensionLinear(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_dimension_linear(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::SetAttribute { id, tag, value } => {
|
||||
b.can_modify(*id).map_err(|e| match e {
|
||||
ApiError::UnknownId(_) => {
|
||||
ApiError::validation(name, format!("unknown ObjectId {id:?}"))
|
||||
}
|
||||
other => other,
|
||||
})?;
|
||||
b.push_undo(name);
|
||||
b.set_attribute(*id, tag, value)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
Operation::SetViewportView {
|
||||
id,
|
||||
view_target,
|
||||
view_height,
|
||||
} => {
|
||||
b.can_modify(*id).map_err(|e| match e {
|
||||
ApiError::UnknownId(_) => {
|
||||
ApiError::validation(name, format!("unknown ObjectId {id:?}"))
|
||||
}
|
||||
other => other,
|
||||
})?;
|
||||
b.push_undo(name);
|
||||
b.set_viewport_view(*id, *view_target, *view_height)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
Operation::CreateRasterImage(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_raster_image(spec)?;
|
||||
b.finalize_op();
|
||||
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"));
|
||||
}
|
||||
let mut sections = Vec::with_capacity(profiles.len());
|
||||
for (i, p) in profiles.iter().enumerate() {
|
||||
let curves = profile_curves(b, *p, name).map_err(|e| at_index(name, i, e))?;
|
||||
sections.push((b.profile_plane(*p)?, curves));
|
||||
}
|
||||
b.push_undo(name);
|
||||
let id = b.loft(§ions)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateDimensionRadius(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_dimension_radius(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateDimensionDiameter(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_dimension_diameter(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateDimensionAngular(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_dimension_angular(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateAttributeDefinition(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_attribute_definition(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateTable(spec) => {
|
||||
// Validate a non-empty, rectangular grid before committing.
|
||||
let cols = spec.data.first().map(|r| r.len()).unwrap_or(0);
|
||||
if spec.data.is_empty() || cols == 0 || spec.data.iter().any(|r| r.len() != cols) {
|
||||
return Err(ApiError::validation(
|
||||
name,
|
||||
"table needs a non-empty rectangular grid",
|
||||
));
|
||||
}
|
||||
b.push_undo(name);
|
||||
let id = b.add_table(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateDimensionAngular2Ln(spec) => {
|
||||
b.push_undo(name);
|
||||
let id = b.add_dimension_angular2ln(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
};
|
||||
Ok(Receipt {
|
||||
outcome: Some(outcome),
|
||||
query_results: Vec::new(),
|
||||
new_revision: b.revision(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply a batch of read-only queries (no undo, no bump; `&mut` because solid
|
||||
/// resolution may populate the kernel body cache on miss). Bounded like the bulk
|
||||
/// ops: a batch over `BULK_ITEM_CAP` is rejected before any work (the 64 MiB frame
|
||||
/// cap bounds message size, not count).
|
||||
pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiResult<Receipt> {
|
||||
if queries.len() > BULK_ITEM_CAP {
|
||||
return Err(ApiError::validation(
|
||||
"Queries",
|
||||
format!(
|
||||
"query batch over cap: {} > {}",
|
||||
queries.len(),
|
||||
BULK_ITEM_CAP
|
||||
),
|
||||
));
|
||||
}
|
||||
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).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).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).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).map_err(|e| label(qname, e))?;
|
||||
QueryResult::ViewportView { target, height }
|
||||
}
|
||||
};
|
||||
results.push(r);
|
||||
}
|
||||
Ok(Receipt {
|
||||
outcome: None,
|
||||
query_results: results,
|
||||
new_revision: b.revision(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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,
|
||||
op: &'static str,
|
||||
) -> ApiResult<Vec<cadkernel::geom2d::Curve>> {
|
||||
if !b.entity_exists(profile) {
|
||||
return Err(ApiError::validation(
|
||||
op,
|
||||
format!("unknown ObjectId {profile:?}"),
|
||||
));
|
||||
}
|
||||
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 require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> {
|
||||
if b.entity_exists(id) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::validation(op, format!("unknown ObjectId {id:?}")))
|
||||
}
|
||||
}
|
||||
|
||||
fn over_cap(op: &'static str, n: usize) -> ApiError {
|
||||
ApiError::validation(op, format!("bulk op over cap: {n} > {BULK_ITEM_CAP}"))
|
||||
}
|
||||
|
||||
fn stale_index(op: &'static str, i: usize, id: ObjectId) -> ApiError {
|
||||
ApiError::validation(op, format!("stale ObjectId at index {i}: {id:?}"))
|
||||
}
|
||||
|
||||
fn at_index(op: &'static str, i: usize, e: ApiError) -> ApiError {
|
||||
match e {
|
||||
ApiError::Validation { reason, .. } => {
|
||||
ApiError::validation(op, format!("index {i}: {reason}"))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// Boolean op kind → kernel `Operation` (host feature only).
|
||||
#[cfg(feature = "host")]
|
||||
pub(crate) fn kernel_bool_op(op: BoolOp) -> cadkernel::brep::Operation {
|
||||
match op {
|
||||
BoolOp::Union => cadkernel::brep::Operation::Union,
|
||||
BoolOp::Intersection => cadkernel::brep::Operation::Intersection,
|
||||
BoolOp::Difference => cadkernel::brep::Operation::Difference,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,323 @@
|
||||
# ocs_doc_api — API reference (auto-generated)
|
||||
|
||||
Envelope version: `1`
|
||||
|
||||
Do NOT edit: regenerated by `build.rs` from `spec/entities.toml`.
|
||||
|
||||
## `Solid` (acadrust `Solid3D`, collection `solids`)
|
||||
|
||||
B-rep solid. Host resolves bodies via the solid_models cache (lift-on-miss from ACIS SAT).
|
||||
|
||||
### Constructors (`doc.solids()`)
|
||||
|
||||
- **create_cuboid**(origin: [f64; 3], size: [f64; 3]) -> `Solid` — `CreateSolid`; fixed primitive="Cuboid"
|
||||
- **create_sphere**(centre: [f64; 3], radius: f64) -> `Solid` — `CreateSolid`; fixed primitive="Sphere"
|
||||
- **create_cylinder**(base: [f64; 3], radius: f64, height: f64) -> `Solid` — `CreateSolid`; fixed primitive="Cylinder"
|
||||
- **create_cone**(base: [f64; 3], radius: f64, height: f64) -> `Solid` — `CreateSolid`; fixed primitive="Cone"
|
||||
- **create_torus**(centre: [f64; 3], major_radius: f64, minor_radius: f64) -> `Solid` — `CreateSolid`; fixed primitive="Torus"
|
||||
- **create_wedge**(origin: [f64; 3], size: [f64; 3]) -> `Solid` — `CreateSolid`; fixed primitive="Wedge"
|
||||
- **extrude**(profile: EntityRef, direction: [f64; 3]) -> `Solid` — `Extrude`
|
||||
- **revolve**(profile: EntityRef, pivot: [f64; 3], axis: [f64; 3], angle: f64) -> `Solid` — `Revolve`
|
||||
- **loft**(profiles: &[EntityRef]) -> `Solid` — `Loft`
|
||||
|
||||
### Methods
|
||||
|
||||
- **intersect**(other: Solid) -> `Solid` — op `SolidBoolean`; fixed erase_sources=true, op="Intersection"
|
||||
- **union**(other: Solid) -> `Solid` — op `SolidBoolean`; fixed erase_sources=true, op="Union"
|
||||
- **subtract**(other: Solid) -> `Solid` — op `SolidBoolean`; fixed erase_sources=true, op="Difference"
|
||||
- **volume**() -> `f64` — query `GetVolume`
|
||||
- **centroid**() -> `[f64; 3]` — query `GetCentroid`
|
||||
- **intersects**(other: Solid) -> `bool` — query `GetIntersects`
|
||||
|
||||
## `Line` (acadrust `Line`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_line**(start: [f64; 3], end: [f64; 3]) -> `Line` — `CreateCurve`; fixed curve="Line"
|
||||
|
||||
## `Circle` (acadrust `Circle`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_circle**(centre: [f64; 3], radius: f64) -> `Circle` — `CreateCurve`; fixed curve="Circle"
|
||||
|
||||
## `Polyline` (acadrust `LwPolyline`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_polyline**(points: &[[f64; 3]], closed: bool) -> `Polyline` — `CreateCurve`; fixed curve="Polyline"
|
||||
|
||||
### Methods
|
||||
|
||||
- **add_vertex**(at: usize, point: [f64; 3]) -> `()` — op `AddVertex`
|
||||
|
||||
## `Point` (acadrust `Point`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_point**(position: [f64; 3]) -> `Point` — `CreateCurve`; fixed curve="Point"
|
||||
- **create_points**(positions: &[[f64; 3]]) -> `Vec<Point>` *(bulk op)* — `CreateMany`; fixed curve="Point"
|
||||
|
||||
## `ArcCurve` (acadrust `Arc`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_arc**(centre: [f64; 3], radius: f64, start_angle: f64, end_angle: f64) -> `ArcCurve` — `CreateCurve`; fixed curve="Arc"
|
||||
|
||||
## `Ellipse` (acadrust `Ellipse`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_ellipse**(centre: [f64; 3], major_axis: [f64; 3], ratio: f64, start: f64, end: f64) -> `Ellipse` — `CreateCurve`; fixed curve="Ellipse"
|
||||
|
||||
## `Spline` (acadrust `Spline`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_spline**(degree: i32, control_points: &[[f64; 3]], knots: &[f64], weights: &[f64]) -> `Spline` — `CreateCurve`; fixed curve="Spline"
|
||||
|
||||
## `Ray` (acadrust `Ray`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_ray**(origin: [f64; 3], direction: [f64; 3]) -> `Ray` — `CreateCurve`; fixed curve="Ray"
|
||||
|
||||
## `XLine` (acadrust `XLine`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_xline**(origin: [f64; 3], direction: [f64; 3]) -> `XLine` — `CreateCurve`; fixed curve="XLine"
|
||||
|
||||
## `Text` (acadrust `Text`, collection `curves`)
|
||||
|
||||
Single-line text annotation. content()/set_content() read+replace the value; transform moves insertion point.
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_text**(value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> `Text` — `CreateText`
|
||||
|
||||
### Methods
|
||||
|
||||
- **content**() -> `String` — query `GetTextContent`
|
||||
- **set_content**(value: &str) -> `()` — op `SetTextContent`
|
||||
|
||||
## `MText` (acadrust `MText`, collection `curves`)
|
||||
|
||||
Multi-line text annotation. content()/set_content() read+replace the value; transform moves insertion point.
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_mtext**(value: &str, insertion_point: [f64; 3], height: f64) -> `MText` — `CreateMText`
|
||||
|
||||
### Methods
|
||||
|
||||
- **content**() -> `String` — query `GetTextContent`
|
||||
- **set_content**(value: &str) -> `()` — op `SetTextContent`
|
||||
|
||||
## `Entity` (acadrust `Hatch`, collection `curves`)
|
||||
|
||||
Filled region over a closed polyline boundary. boundary() returns the loops; bounds from boundary; generic delete.
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_hatch**(boundary: &[[f64; 2]], solid: bool) -> `Entity` — `CreateHatch`
|
||||
|
||||
### Methods
|
||||
|
||||
- **boundary**() -> `Vec<Vec<[f64; 2]>>` — query `GetHatchBoundary`
|
||||
|
||||
## `Dimension` (acadrust `Dimension`, collection `curves`)
|
||||
|
||||
Linear dimension between two measured points. measurement() reads the distance; bounds from the measured + definition points.
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_dimension_linear**(first_point: [f64; 3], second_point: [f64; 3], definition_point: [f64; 3]) -> `Dimension` — `CreateDimensionLinear`
|
||||
- **create_dimension_radius**(center: [f64; 3], point: [f64; 3]) -> `Dimension` — `CreateDimensionRadius`
|
||||
- **create_dimension_diameter**(center: [f64; 3], point: [f64; 3]) -> `Dimension` — `CreateDimensionDiameter`
|
||||
- **create_dimension_angular**(vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> `Dimension` — `CreateDimensionAngular`
|
||||
- **create_dimension_angular2ln**(vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> `Dimension` — `CreateDimensionAngular2Ln`
|
||||
|
||||
### Methods
|
||||
|
||||
- **measurement**() -> `f64` — query `GetDimensionMeasurement`
|
||||
|
||||
## `Entity` (acadrust `Insert`, collection `entities`)
|
||||
|
||||
Block reference. Place an existing BlockRecord by name; transform moves its insert point.
|
||||
|
||||
### Constructors (`doc.entities()`)
|
||||
|
||||
- **create_insert**(block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64) -> `Entity` — `CreateInsert`
|
||||
|
||||
### Methods
|
||||
|
||||
- **attributes**() -> `Vec<(String, String)>` — query `GetAttributes`
|
||||
- **set_attribute**(tag: &str, value: &str) -> `()` — op `SetAttribute`
|
||||
|
||||
## `Entity` (acadrust `AttributeDefinition`, collection `entities`)
|
||||
|
||||
In-block attribute definition (ATTDEF): tag/prompt/default at an insertion point.
|
||||
|
||||
### Constructors (`doc.entities()`)
|
||||
|
||||
- **create_attribute_definition**(tag: &str, prompt: &str, default_value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> `Entity` — `CreateAttributeDefinition`
|
||||
|
||||
## `Entity` (acadrust `Viewport`, collection `entities`)
|
||||
|
||||
Paper-space viewport: a width×height window at center looking at view_target with view_height zoom. bounds = center ± w/2,h/2; transform moves center.
|
||||
|
||||
### Constructors (`doc.entities()`)
|
||||
|
||||
- **create_viewport**(center: [f64; 3], width: f64, height: f64, view_target: [f64; 3], view_height: f64) -> `Entity` — `CreateViewport`
|
||||
|
||||
### Methods
|
||||
|
||||
- **viewport_view**() -> `([f64; 3], f64)` — query `GetViewportView`
|
||||
- **set_view**(view_target: [f64; 3], view_height: f64) -> `()` — op `SetViewportView`
|
||||
|
||||
## `Entity` (acadrust `RasterImage`, collection `entities`)
|
||||
|
||||
Raster image: create_raster_image(file_path, insertion, u, v, size) places an image (host auto-registers the ImageDefinition); bounds (4-corner bracket); delete.
|
||||
|
||||
### Constructors (`doc.entities()`)
|
||||
|
||||
- **create_raster_image**(file_path: &str, insertion_point: [f64; 3], u_vector: [f64; 3], v_vector: [f64; 3], size: [f64; 2]) -> `Entity` — `CreateRasterImage`
|
||||
|
||||
## `Entity` (acadrust `Table`, collection `entities`)
|
||||
|
||||
Table: create_table(insertion_point, data[row][col]) builds rows x columns of text cells; non-rectangular grid -> Validation; delete.
|
||||
|
||||
### Constructors (`doc.entities()`)
|
||||
|
||||
- **create_table**(insertion_point: [f64; 3], data: &[Vec<String>]) -> `Entity` — `CreateTable`
|
||||
|
||||
## Generic methods (every handle)
|
||||
|
||||
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
||||
`Ellipse`, `Spline`, `Ray`, `XLine`, `Text`, `MText`, `Dimension`, `Entity`) is a
|
||||
`Clone`, `Send`, `Sync` token `{ ObjectId, session }` and supports:
|
||||
|
||||
- **`id()` -> `ObjectId`** — the stable entity identity.
|
||||
- **`bounds()` -> `Aabb`** — coarse bounding box (solids lift-on-miss; 2D entities
|
||||
computed; later families may be `Unsupported`).
|
||||
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
||||
solids only.**
|
||||
- **`delete()`** — remove the entity (one undo step).
|
||||
|
||||
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
|
||||
downcast when `kind == "Solid3D"`).
|
||||
|
||||
## Collections & cross-cutting
|
||||
|
||||
- **`DocApi`** — root: `document(tab)`, `active_tab()`, `alive()`.
|
||||
- **`Document`** — `solids()` / `curves()` / `entities()` factories + lookup,
|
||||
`revision()`, `assert_revision(rev)` (read-guard), `query_batch(|q| …)` (read-only
|
||||
batch, one round-trip, **no revision bump**).
|
||||
- **`EntityCollection`** — `get(id) -> Entity`, `delete(id)`,
|
||||
`transform_many(&ids, placement)`, `delete_many(&ids)`.
|
||||
- **`OpGroup`** — best-effort client-side failure cleanup (`track` / `commit` /
|
||||
`compensate`). **Not** a transaction: no atomicity, no rollback.
|
||||
|
||||
## Bulk ops — single op, all-or-nothing, item-capped
|
||||
|
||||
`CreateMany` / `TransformMany` / `DeleteMany` are **single** write ops: one undo
|
||||
step, one publish, and **all-or-nothing via upfront validation** (no rollback).
|
||||
Item-capped at `BULK_ITEM_CAP = 100_000` to bound host execution. Use them for
|
||||
bulk data (N ≳ a few hundred); per-op autocommit is the right tool for
|
||||
modeling-scale work.
|
||||
|
||||
## Semantics
|
||||
|
||||
- **Per-op autocommit, no write transactions.** Every write op is ONE atomic host
|
||||
call = one undo step. Multi-op write transactions were removed (security/
|
||||
integrity review).
|
||||
- **Per-op atomicity.** Validate + compute geometry **before** any mutation; a
|
||||
failed op applies nothing and records no undo step. `erase_sources` booleans run
|
||||
only on success.
|
||||
- **Structured errors.** `ApiError::{Validation, Geometry, UnknownId, Unsupported,
|
||||
Transport}`; bulk validation names the failing index; a stale handle returns
|
||||
`UnknownId` (never a panic).
|
||||
|
||||
## Kernel mapping
|
||||
|
||||
| op | kernel call | returns |
|
||||
|---|---|---|
|
||||
| CreateSolid.Cuboid | `cadkernel::brep::make::cuboid` | Option<Body> |
|
||||
| CreateSolid.Sphere | `cadkernel::brep::make::sphere` | Option<Body> |
|
||||
| SolidBoolean | `cadkernel::brep::combine` | Result<Body, Snag> |
|
||||
| Transform | `cadkernel::brep::transform` | Option<Body> |
|
||||
| GetBounds | `cadkernel::brep::body_bounds` | Option<Aabb> |
|
||||
| GetVolume | `cadkernel::brep::mesh_body + divergence` | computed |
|
||||
|
||||
## Wire vocabulary
|
||||
|
||||
Every `Operation` is ONE atomic write op (one undo step); every `Query` is read-only (safe to batch). Variants are append-only (bincode discriminant stability). Requests ride `DocApiEnvelope`: `Op(Operation)` for a write, `Queries(Vec<Query>)` for a read batch.
|
||||
|
||||
### `Operation`
|
||||
|
||||
- `CreateSolid(SolidPrimitive)`
|
||||
- `CreateCurve(Curve2Spec)`
|
||||
- `Extrude { profile: ObjectId, direction: [f64; 3] }`
|
||||
- `Revolve { profile: ObjectId, axis: ([f64; 3], [f64; 3]), angle: f64 }`
|
||||
- `Transform { id: ObjectId, placement: PlacementSpec }`
|
||||
- `SolidBoolean { op: BoolOp, a: ObjectId, b: ObjectId, erase_sources: bool }`
|
||||
- `AddVertex { id: ObjectId, at: usize, point: [f64; 3] }`
|
||||
- `Delete { id: ObjectId }`
|
||||
- `CreateMany(Vec<EntitySpec>)`
|
||||
- `TransformMany { ids: Vec<ObjectId>, placement: PlacementSpec }`
|
||||
- `DeleteMany(Vec<ObjectId>)`
|
||||
- `CreateInsert(InsertSpec)`
|
||||
- `CreateViewport(ViewportSpec)`
|
||||
- `CreateText(TextSpec)`
|
||||
- `CreateMText(MTextSpec)`
|
||||
- `SetTextContent { id: ObjectId, value: String }`
|
||||
- `CreateHatch(HatchSpec)`
|
||||
- `CreateDimensionLinear(DimensionSpec)`
|
||||
- `SetAttribute { id: ObjectId, tag: String, value: String }`
|
||||
- `SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }`
|
||||
- `CreateRasterImage(RasterImageSpec)`
|
||||
- `Loft { profiles: Vec<ObjectId> }`
|
||||
- `CreateDimensionRadius(DimensionRadialSpec)`
|
||||
- `CreateDimensionDiameter(DimensionRadialSpec)`
|
||||
- `CreateDimensionAngular(DimensionAngularSpec)`
|
||||
- `CreateAttributeDefinition(AttributeDefinitionSpec)`
|
||||
- `CreateTable(TableSpec)`
|
||||
- `CreateDimensionAngular2Ln(DimensionAngularSpec)`
|
||||
|
||||
### `Query`
|
||||
|
||||
- `GetEntity { id: ObjectId }`
|
||||
- `GetBounds { id: ObjectId }`
|
||||
- `GetCentroid { id: ObjectId }`
|
||||
- `GetVolume { id: ObjectId }`
|
||||
- `GetIntersects { a: ObjectId, b: ObjectId }`
|
||||
- `GetGeometryRevision`
|
||||
- `GetTextContent { id: ObjectId }`
|
||||
- `GetHatchBoundary { id: ObjectId }`
|
||||
- `GetDimensionMeasurement { id: ObjectId }`
|
||||
- `GetAttributes { id: ObjectId }`
|
||||
- `GetBlockEntities { block_name: String }`
|
||||
- `GetViewportView { id: ObjectId }`
|
||||
|
||||
## Errors (`ApiError`)
|
||||
|
||||
| Variant | Meaning |
|
||||
|---|---|
|
||||
| `Validation { op, reason }` | Input rejected before any document mutation. |
|
||||
| `Geometry { kind, msg }` | The kernel failed (boolean refused, invalid input, ACIS lift/lower). |
|
||||
| `UnknownId(ObjectId)` | The handle is stale / deleted / never existed (never a panic). |
|
||||
| `Unsupported(String)` | Capability not supported for this entity or backend. |
|
||||
| `Transport(String)` | The channel failed (disconnected / oversized / timeout). |
|
||||
|
||||
`ApiError` is serializable, so IPC returns the same structured error the in-process
|
||||
executor produced.
|
||||
|
||||
## Envelope & transport
|
||||
|
||||
`DocApiEnvelope { version, body }` — `body` is `Op(Operation)` (a single write) or
|
||||
`Queries(Vec<Query>)` (a read batch). bincode-serialized; `version` is the crate's
|
||||
envelope version (bridges must check it). Transports: `InProcess` (host) and
|
||||
`OcsPluginApiIpc` (plugin over `ocs_plugin_api`), both behind the `Transport:
|
||||
Send + Sync` trait (`apply(envelope) -> Receipt`, `alive()`). `Receipt` carries
|
||||
`outcome` (per-op result), `query_results`, and `new_revision`.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"ObjectId": { "struct": [ { "name": "0", "type": "u64" } ] },
|
||||
"Operation": {
|
||||
"enum": [
|
||||
"CreateSolid(SolidPrimitive)",
|
||||
"CreateCurve(Curve2Spec)",
|
||||
"Extrude { profile: ObjectId, direction: [f64; 3] }",
|
||||
"Revolve { profile: ObjectId, axis: ([f64; 3], [f64; 3]), angle: f64 }",
|
||||
"Transform { id: ObjectId, placement: PlacementSpec }",
|
||||
"SolidBoolean { op: BoolOp, a: ObjectId, b: ObjectId, erase_sources: bool }",
|
||||
"AddVertex { id: ObjectId, at: usize, point: [f64; 3] }",
|
||||
"Delete { id: ObjectId }",
|
||||
"CreateMany(Vec<EntitySpec>)",
|
||||
"TransformMany { ids: Vec<ObjectId>, placement: PlacementSpec }",
|
||||
"DeleteMany(Vec<ObjectId>)",
|
||||
"CreateInsert(InsertSpec)",
|
||||
"CreateViewport(ViewportSpec)",
|
||||
"CreateText(TextSpec)",
|
||||
"CreateMText(MTextSpec)",
|
||||
"SetTextContent { id: ObjectId, value: String }",
|
||||
"CreateHatch(HatchSpec)",
|
||||
"CreateDimensionLinear(DimensionSpec)",
|
||||
"SetAttribute { id: ObjectId, tag: String, value: String }",
|
||||
"SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }",
|
||||
"CreateRasterImage(RasterImageSpec)",
|
||||
"Loft { profiles: Vec<ObjectId> }",
|
||||
"CreateDimensionRadius(DimensionRadialSpec)",
|
||||
"CreateDimensionDiameter(DimensionRadialSpec)",
|
||||
"CreateDimensionAngular(DimensionAngularSpec)",
|
||||
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
||||
"CreateTable(TableSpec)",
|
||||
"CreateDimensionAngular2Ln(DimensionAngularSpec)"
|
||||
]
|
||||
},
|
||||
"Query": {
|
||||
"enum": [
|
||||
"GetEntity { id: ObjectId }",
|
||||
"GetBounds { id: ObjectId }",
|
||||
"GetCentroid { id: ObjectId }",
|
||||
"GetVolume { id: ObjectId }",
|
||||
"GetIntersects { a: ObjectId, b: ObjectId }",
|
||||
"GetGeometryRevision",
|
||||
"GetTextContent { id: ObjectId }",
|
||||
"GetHatchBoundary { id: ObjectId }",
|
||||
"GetDimensionMeasurement { id: ObjectId }",
|
||||
"GetAttributes { id: ObjectId }",
|
||||
"GetBlockEntities { block_name: String }",
|
||||
"GetViewportView { id: ObjectId }"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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). Do NOT reorder or remove variants.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::id::ObjectId;
|
||||
use crate::ops::{
|
||||
AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec,
|
||||
DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec,
|
||||
SolidPrimitive, TableSpec, TextSpec, ViewportSpec,
|
||||
};
|
||||
|
||||
/// A typed write operation. Each variant is ONE atomic host call = one
|
||||
/// undo step. Append new variants at the END only.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum Operation {
|
||||
// construction
|
||||
CreateSolid(SolidPrimitive),
|
||||
CreateCurve(Curve2Spec),
|
||||
Extrude {
|
||||
profile: ObjectId,
|
||||
direction: [f64; 3],
|
||||
},
|
||||
Revolve {
|
||||
profile: ObjectId,
|
||||
axis: ([f64; 3], [f64; 3]),
|
||||
angle: f64,
|
||||
},
|
||||
// modification
|
||||
Transform {
|
||||
id: ObjectId,
|
||||
placement: PlacementSpec,
|
||||
},
|
||||
SolidBoolean {
|
||||
op: BoolOp,
|
||||
a: ObjectId,
|
||||
b: ObjectId,
|
||||
erase_sources: bool,
|
||||
},
|
||||
AddVertex {
|
||||
id: ObjectId,
|
||||
at: usize,
|
||||
point: [f64; 3],
|
||||
},
|
||||
Delete {
|
||||
id: ObjectId,
|
||||
},
|
||||
// Bulk ops (single op, upfront-validation all-or-nothing — NOT a transaction, §5.3)
|
||||
CreateMany(Vec<EntitySpec>),
|
||||
TransformMany {
|
||||
ids: Vec<ObjectId>,
|
||||
placement: PlacementSpec,
|
||||
},
|
||||
DeleteMany(Vec<ObjectId>),
|
||||
// containers
|
||||
CreateInsert(InsertSpec),
|
||||
// paper-space
|
||||
CreateViewport(ViewportSpec),
|
||||
// annotations
|
||||
CreateText(TextSpec),
|
||||
CreateMText(MTextSpec),
|
||||
SetTextContent {
|
||||
id: ObjectId,
|
||||
value: String,
|
||||
},
|
||||
// hatch
|
||||
CreateHatch(HatchSpec),
|
||||
// dimensions
|
||||
CreateDimensionLinear(DimensionSpec),
|
||||
// attributes
|
||||
SetAttribute {
|
||||
id: ObjectId,
|
||||
tag: String,
|
||||
value: String,
|
||||
},
|
||||
// viewport view
|
||||
SetViewportView {
|
||||
id: ObjectId,
|
||||
view_target: [f64; 3],
|
||||
view_height: f64,
|
||||
},
|
||||
// media
|
||||
CreateRasterImage(RasterImageSpec),
|
||||
// multi-profile solid
|
||||
Loft {
|
||||
profiles: Vec<ObjectId>,
|
||||
},
|
||||
// dimension sub-types
|
||||
CreateDimensionRadius(DimensionRadialSpec),
|
||||
CreateDimensionDiameter(DimensionRadialSpec),
|
||||
CreateDimensionAngular(DimensionAngularSpec),
|
||||
// attribute definitions
|
||||
CreateAttributeDefinition(AttributeDefinitionSpec),
|
||||
// tables
|
||||
CreateTable(TableSpec),
|
||||
// dimension sub-types (2-line angular)
|
||||
CreateDimensionAngular2Ln(DimensionAngularSpec),
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::id::ObjectId;
|
||||
|
||||
/// A typed read query. Read-only: no mutation, no undo, no revision
|
||||
/// bump. Safe to batch (`EnvelopeBody::Queries`). Append new variants at the END only.
|
||||
/// (Not `Copy`: some variants carry owned data like a block name.)
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum Query {
|
||||
GetEntity {
|
||||
id: ObjectId,
|
||||
},
|
||||
GetBounds {
|
||||
id: ObjectId,
|
||||
},
|
||||
GetCentroid {
|
||||
id: ObjectId,
|
||||
},
|
||||
GetVolume {
|
||||
id: ObjectId,
|
||||
},
|
||||
/// Whether two entities' bounds overlap (two-phase conditional modelling).
|
||||
GetIntersects {
|
||||
a: ObjectId,
|
||||
b: ObjectId,
|
||||
},
|
||||
GetGeometryRevision,
|
||||
/// The text content of a Text/MText annotation.
|
||||
GetTextContent {
|
||||
id: ObjectId,
|
||||
},
|
||||
/// The boundary loops of a Hatch (outer + islands) as polylines.
|
||||
GetHatchBoundary {
|
||||
id: ObjectId,
|
||||
},
|
||||
/// The measured value of a Dimension (distance or angle).
|
||||
GetDimensionMeasurement {
|
||||
id: ObjectId,
|
||||
},
|
||||
/// An insert's attributes as (tag, value) pairs.
|
||||
GetAttributes {
|
||||
id: ObjectId,
|
||||
},
|
||||
/// The entity ids + kinds inside a block definition (read-only traversal).
|
||||
GetBlockEntities {
|
||||
block_name: String,
|
||||
},
|
||||
/// A viewport's view (target + zoom height).
|
||||
GetViewportView {
|
||||
id: ObjectId,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//! Kernel geometry helpers (host feature, plan §10 code-mapping table). Thin
|
||||
//! wrappers over `cadkernel::brep::*` mapping the crate's plain-data DTOs to the
|
||||
//! non-serde kernel types. Kernel keys never cross the API (decision #7).
|
||||
|
||||
use cadkernel::geom2d::Curve as Curve2;
|
||||
use cadkernel::space::Plane;
|
||||
|
||||
use crate::backend::KernelBody;
|
||||
use crate::error::{ApiError, ApiResult, GeometryErrorKind};
|
||||
use crate::ops::{BoolOp, SolidPrimitive};
|
||||
|
||||
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]) {
|
||||
mesh.mass_properties().unwrap_or((0.0, [0.0; 3]))
|
||||
}
|
||||
|
||||
pub fn validate_placement(p: &crate::PlacementSpec) -> ApiResult<()> {
|
||||
let placement = cadkernel::brep::Placement {
|
||||
origin: p.origin,
|
||||
x_axis: p.x_axis,
|
||||
y_axis: p.y_axis,
|
||||
z_axis: p.z_axis,
|
||||
};
|
||||
if placement.scale().is_none() || p.origin.iter().any(|v| !v.is_finite()) {
|
||||
return Err(ApiError::validation(
|
||||
"Transform",
|
||||
"placement must be a finite, nonzero similarity",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Construct a solid primitive.
|
||||
pub fn make_solid(p: &SolidPrimitive) -> ApiResult<KernelBody> {
|
||||
crate::validation::solid(p)?;
|
||||
use cadkernel::brep::make;
|
||||
let body = match *p {
|
||||
SolidPrimitive::Cuboid { origin, size } => make::cuboid(origin, size),
|
||||
SolidPrimitive::Sphere { centre, radius } => make::sphere(centre, radius),
|
||||
SolidPrimitive::Cylinder {
|
||||
base,
|
||||
radius,
|
||||
height,
|
||||
} => make::cylinder(base, radius, height),
|
||||
SolidPrimitive::Cone {
|
||||
base,
|
||||
radius,
|
||||
height,
|
||||
} => make::cone(base, radius, height),
|
||||
SolidPrimitive::Torus {
|
||||
centre,
|
||||
major_radius,
|
||||
minor_radius,
|
||||
} => make::torus(centre, major_radius, minor_radius),
|
||||
SolidPrimitive::Wedge { origin, size } => make::wedge(origin, size[0], size[1], size[2]),
|
||||
};
|
||||
body.ok_or_else(|| {
|
||||
geom_err(
|
||||
GeometryErrorKind::InvalidInput,
|
||||
"kernel make returned empty body",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Boolean combine (pure): `brep::combine(a, b, op, operation_tolerance(&[&a,&b]))`.
|
||||
/// Maps the kernel `Snag` to `ApiError::Geometry`. Does NOT mutate the inputs.
|
||||
pub(crate) fn boolean(a: &KernelBody, b: &KernelBody, op: BoolOp) -> ApiResult<KernelBody> {
|
||||
let kernel_op = crate::executor::kernel_bool_op(op);
|
||||
let tol = cadkernel::brep::operation_tolerance(&[a, b]);
|
||||
cadkernel::brep::combine(a.clone(), b.clone(), kernel_op, tol)
|
||||
.map_err(|e| geom_err(GeometryErrorKind::BooleanFailed, format!("{e:?}")))
|
||||
}
|
||||
|
||||
/// Extrude a closed 2D profile into a solid (`brep::extrude`).
|
||||
pub(crate) fn extrude(
|
||||
plane: Plane,
|
||||
profile: &[Curve2],
|
||||
direction: [f64; 3],
|
||||
) -> ApiResult<KernelBody> {
|
||||
cadkernel::brep::extrude(plane, profile, direction)
|
||||
.ok_or_else(|| geom_err(GeometryErrorKind::InvalidInput, "extrude failed"))
|
||||
}
|
||||
|
||||
/// Revolve a closed 2D profile about an axis into a solid (`brep::revolve`).
|
||||
pub(crate) fn revolve(
|
||||
plane: Plane,
|
||||
profile: &[Curve2],
|
||||
pivot: [f64; 3],
|
||||
axis: [f64; 3],
|
||||
angle: f64,
|
||||
) -> ApiResult<KernelBody> {
|
||||
cadkernel::brep::revolve(plane, profile, pivot, axis, angle)
|
||||
.ok_or_else(|| geom_err(GeometryErrorKind::InvalidInput, "revolve failed"))
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! Stable entity identity.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A stable u64 handle to a first-level document entity, decoupled from any
|
||||
/// internal engine/kernel pointer. Value-compatible with `acadrust::Handle` —
|
||||
/// kernel keys never cross the API boundary.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct ObjectId(pub u64);
|
||||
|
||||
impl ObjectId {
|
||||
/// The null / absent handle (`acadrust::Handle::NULL` value `0`).
|
||||
pub const NULL: ObjectId = ObjectId(0);
|
||||
|
||||
pub const fn as_u64(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Build from a raw u64 (e.g. an `acadrust::Handle`'s value).
|
||||
pub const fn from_u64(v: u64) -> Self {
|
||||
Self(v)
|
||||
}
|
||||
|
||||
/// Build from an `acadrust::Handle` (host/ipc feature).
|
||||
#[cfg(any(feature = "host", feature = "ipc"))]
|
||||
pub fn from_handle(h: acadrust::Handle) -> Self {
|
||||
Self(h.value())
|
||||
}
|
||||
|
||||
/// Convert to an `acadrust::Handle` (host/ipc feature).
|
||||
#[cfg(any(feature = "host", feature = "ipc"))]
|
||||
pub fn to_handle(self) -> acadrust::Handle {
|
||||
acadrust::Handle::new(self.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Typed document operations, queries and facades over in-process or IPC transports.
|
||||
|
||||
pub mod error;
|
||||
pub mod id;
|
||||
pub mod revision;
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
pub mod backend;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod convert;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod executor;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod geom;
|
||||
#[cfg(feature = "host")]
|
||||
mod validation;
|
||||
|
||||
pub mod envelope;
|
||||
pub mod facade;
|
||||
pub mod ops;
|
||||
pub mod query;
|
||||
pub mod transport;
|
||||
|
||||
// Append-only wire enums, maintained alongside the DTOs and spec.
|
||||
pub mod gen {
|
||||
pub mod ops {
|
||||
include!("gen/ops_gen.rs");
|
||||
}
|
||||
pub mod query {
|
||||
include!("gen/query_gen.rs");
|
||||
}
|
||||
pub use ops::Operation;
|
||||
pub use query::Query;
|
||||
}
|
||||
|
||||
pub use error::{ApiError, ApiResult, GeometryErrorKind};
|
||||
pub use id::ObjectId;
|
||||
pub use revision::GeometryRevision;
|
||||
|
||||
pub use envelope::{DocApiEnvelope, EnvelopeBody, OpOutcome, Receipt};
|
||||
pub use facade::{
|
||||
ArcCurve, Circle, CurveCollection, Dimension, DocApi, Document, Ellipse, Entity,
|
||||
EntityCollection, HasId, Line, MText, OpGroup, Point, Polyline, QueryBatch, QueryResults, Ray,
|
||||
Solid, SolidCollection, Spline, Text, XLine,
|
||||
};
|
||||
pub use ops::{BoolOp, Curve2Spec, EntitySpec, Operation, PlacementSpec, SolidPrimitive};
|
||||
pub use query::{Aabb, EntityView, Query, QueryResult};
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
pub use convert::{
|
||||
bulge_arc_segment, curve_spec_to_entity, entity_bounds, entity_kind_name,
|
||||
entity_to_profile_curves, transform_entity_geometry,
|
||||
};
|
||||
pub use transport::Transport;
|
||||
|
||||
/// The crate's own wire-envelope protocol version (`DocApiEnvelope::version`).
|
||||
/// Bump when the envelope layout or `Operation`/`Query` discriminants change.
|
||||
/// Bridges must check this before speaking (see `bindings/README.md`).
|
||||
pub const ENVELOPE_VERSION: u16 = 1;
|
||||
|
||||
/// The generated binding handover schema: object model + method
|
||||
/// signatures + op/query mapping with curated wire vocabulary. Single,
|
||||
/// self-contained, versioned with [`ENVELOPE_VERSION`].
|
||||
pub fn binding_schema_json() -> &'static str {
|
||||
include_str!("gen/binding_schema.json")
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
//! Write operations — the typed, transport-agnostic vocabulary for changing the
|
||||
//! document. The `Operation` enum is **hand-maintained** (append-only)
|
||||
//! in `src/gen/ops_gen.rs` — it is NOT derived from the spec by build.rs; the
|
||||
//! spec's `op` names must each map to a variant here (asserted by a test). This
|
||||
//! module re-exports it and holds the hand-written payload mirrors.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use crate::gen::Operation;
|
||||
|
||||
/// Boolean operation kind for [`Operation::SolidBoolean`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BoolOp {
|
||||
Union,
|
||||
Intersection,
|
||||
Difference,
|
||||
}
|
||||
|
||||
/// Construction spec for a B-rep solid primitive (plain-data mirror of the
|
||||
/// non-serde `cadkernel::brep::make::*` arguments).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SolidPrimitive {
|
||||
Cuboid {
|
||||
origin: [f64; 3],
|
||||
size: [f64; 3],
|
||||
},
|
||||
Sphere {
|
||||
centre: [f64; 3],
|
||||
radius: f64,
|
||||
},
|
||||
Cylinder {
|
||||
base: [f64; 3],
|
||||
radius: f64,
|
||||
height: f64,
|
||||
},
|
||||
Cone {
|
||||
base: [f64; 3],
|
||||
radius: f64,
|
||||
height: f64,
|
||||
},
|
||||
Torus {
|
||||
centre: [f64; 3],
|
||||
major_radius: f64,
|
||||
minor_radius: f64,
|
||||
},
|
||||
Wedge {
|
||||
origin: [f64; 3],
|
||||
size: [f64; 3],
|
||||
},
|
||||
}
|
||||
|
||||
/// Construction spec for a minimal 2D curve entity (phase-1 set; profiles/inputs).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Curve2Spec {
|
||||
Line {
|
||||
start: [f64; 3],
|
||||
end: [f64; 3],
|
||||
},
|
||||
Circle {
|
||||
centre: [f64; 3],
|
||||
radius: f64,
|
||||
},
|
||||
Polyline {
|
||||
points: Vec<[f64; 3]>,
|
||||
closed: bool,
|
||||
},
|
||||
Point {
|
||||
position: [f64; 3],
|
||||
},
|
||||
/// Circular arc, counter-clockwise from `start_angle` to `end_angle` (radians).
|
||||
Arc {
|
||||
centre: [f64; 3],
|
||||
radius: f64,
|
||||
start_angle: f64,
|
||||
end_angle: f64,
|
||||
},
|
||||
/// Ellipse: `major_axis` is the major-axis endpoint relative to `centre`;
|
||||
/// `ratio` = minor/major. `start`/`end` parameters 0..2π (full ellipse = 0..2π).
|
||||
Ellipse {
|
||||
centre: [f64; 3],
|
||||
major_axis: [f64; 3],
|
||||
ratio: f64,
|
||||
start: f64,
|
||||
end: f64,
|
||||
},
|
||||
/// NURBS spline (control points + knots + weights; `degree` typically 3).
|
||||
Spline {
|
||||
degree: i32,
|
||||
control_points: Vec<[f64; 3]>,
|
||||
knots: Vec<f64>,
|
||||
weights: Vec<f64>,
|
||||
},
|
||||
/// A ray from `origin` along `direction` (bounded at origin only).
|
||||
Ray {
|
||||
origin: [f64; 3],
|
||||
direction: [f64; 3],
|
||||
},
|
||||
/// An infinite construction line through `origin` along `direction`.
|
||||
XLine {
|
||||
origin: [f64; 3],
|
||||
direction: [f64; 3],
|
||||
},
|
||||
}
|
||||
|
||||
/// A generic entity-construction payload used by the bulk op
|
||||
/// [`crate::ops::Operation::CreateMany`]. Mirrors the per-family construction
|
||||
/// specs so one bulk op can carry many homogeneous (or mixed) creations.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EntitySpec {
|
||||
Curve(Curve2Spec),
|
||||
Solid(SolidPrimitive),
|
||||
}
|
||||
|
||||
/// Construction spec for a block reference (`INSERT`): place the block
|
||||
/// `block_name` (must exist as a BlockRecord) at `insert_point` with uniform
|
||||
/// `scale` and `rotation` (radians, about Z).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct InsertSpec {
|
||||
pub block_name: String,
|
||||
pub insert_point: [f64; 3],
|
||||
pub scale: f64,
|
||||
pub rotation: f64,
|
||||
}
|
||||
|
||||
/// Construction spec for a paper-space `VIEWPORT`: a `width`×`height` viewport
|
||||
/// centered at `center` (paper space), looking at `view_target` (model space,
|
||||
/// WCS) with `view_height` (model-space height visible; the viewport's zoom).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ViewportSpec {
|
||||
pub center: [f64; 3],
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
pub view_target: [f64; 3],
|
||||
pub view_height: f64,
|
||||
}
|
||||
|
||||
/// Construction spec for a single-line `TEXT` annotation.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TextSpec {
|
||||
pub value: String,
|
||||
pub insertion_point: [f64; 3],
|
||||
pub height: f64,
|
||||
pub rotation: f64,
|
||||
}
|
||||
|
||||
/// Construction spec for a multi-line `MTEXT` annotation.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MTextSpec {
|
||||
pub value: String,
|
||||
pub insertion_point: [f64; 3],
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
/// Construction spec for a `HATCH` with a single closed polyline boundary
|
||||
/// (straight segments). `solid` selects solid fill vs a pattern (pattern naming
|
||||
/// is a later refinement).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct HatchSpec {
|
||||
/// Closed boundary as an ordered polyline (first==last implied; closed).
|
||||
pub boundary: Vec<[f64; 2]>,
|
||||
pub solid: bool,
|
||||
}
|
||||
|
||||
/// Construction spec for a linear `DIMENSION`: the two measured points and the
|
||||
/// dimension-line definition point.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DimensionSpec {
|
||||
pub first_point: [f64; 3],
|
||||
pub second_point: [f64; 3],
|
||||
pub definition_point: [f64; 3],
|
||||
}
|
||||
|
||||
/// Construction spec for a radial/diameter `DIMENSION`: the circle center and a
|
||||
/// point on the circle (radius) or a chord point (diameter).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DimensionRadialSpec {
|
||||
/// Center of the arc/circle (radius) or first chord point (diameter).
|
||||
pub center: [f64; 3],
|
||||
/// A point on the arc/circle (radius) or the far chord point (diameter).
|
||||
pub point: [f64; 3],
|
||||
}
|
||||
|
||||
/// Construction spec for a 3-point angular `DIMENSION`: vertex + one point on
|
||||
/// each leg, plus the dimension-arc location.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct DimensionAngularSpec {
|
||||
pub vertex: [f64; 3],
|
||||
pub first_point: [f64; 3],
|
||||
pub second_point: [f64; 3],
|
||||
pub arc_location: [f64; 3],
|
||||
}
|
||||
|
||||
/// Construction spec for an `ATTDEF` (attribute definition): the tag, prompt,
|
||||
/// and default value for an in-block attribute, at an insertion point.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AttributeDefinitionSpec {
|
||||
pub tag: String,
|
||||
pub prompt: String,
|
||||
pub default_value: String,
|
||||
pub insertion_point: [f64; 3],
|
||||
pub height: f64,
|
||||
pub rotation: f64,
|
||||
}
|
||||
|
||||
/// Construction spec for a `TABLE`: an insertion point plus a grid of cell
|
||||
/// strings (`data[row][column]`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TableSpec {
|
||||
pub insertion_point: [f64; 3],
|
||||
/// Grid of cell text values, rows × columns.
|
||||
pub data: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Construction spec for a `RASTER_IMAGE`: an image file placed at
|
||||
/// `insertion_point` with per-pixel world-unit vectors `u_vector`/`v_vector` and
|
||||
/// pixel `size` (width, height). The host auto-registers the ImageDefinition.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RasterImageSpec {
|
||||
pub file_path: String,
|
||||
pub insertion_point: [f64; 3],
|
||||
pub u_vector: [f64; 3],
|
||||
pub v_vector: [f64; 3],
|
||||
/// Pixel size (width, height).
|
||||
pub size: [f64; 2],
|
||||
}
|
||||
|
||||
/// A rigid similarity (rotation + translation + optional uniform scale/reflection)
|
||||
/// as a plain-data mirror of `cadkernel::brep::transform`'s `Placement`
|
||||
/// (non-serde). Columns are the new basis axes; `origin` is the translation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PlacementSpec {
|
||||
pub x_axis: [f64; 3],
|
||||
pub y_axis: [f64; 3],
|
||||
pub z_axis: [f64; 3],
|
||||
pub origin: [f64; 3],
|
||||
}
|
||||
|
||||
impl PlacementSpec {
|
||||
/// Identity placement (no transform).
|
||||
pub const IDENTITY: PlacementSpec = PlacementSpec {
|
||||
x_axis: [1.0, 0.0, 0.0],
|
||||
y_axis: [0.0, 1.0, 0.0],
|
||||
z_axis: [0.0, 0.0, 1.0],
|
||||
origin: [0.0, 0.0, 0.0],
|
||||
};
|
||||
|
||||
/// Pure translation to `origin` (axes unchanged).
|
||||
pub fn at(origin: [f64; 3]) -> Self {
|
||||
Self {
|
||||
origin,
|
||||
..Self::IDENTITY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The item-count cap for a single bulk op envelope: bounds
|
||||
/// host-side execution time and payload. Over-cap → `ApiError::Validation`.
|
||||
pub const BULK_ITEM_CAP: usize = 100_000;
|
||||
|
||||
/// Convenience accessors used by the facade/executor.
|
||||
impl crate::gen::Operation {
|
||||
/// The op name used in `ApiError::Validation { op, .. }`.
|
||||
pub fn op_name(&self) -> &'static str {
|
||||
use crate::gen::Operation::*;
|
||||
match self {
|
||||
CreateSolid(_) => "CreateSolid",
|
||||
CreateCurve(_) => "CreateCurve",
|
||||
Extrude { .. } => "Extrude",
|
||||
Revolve { .. } => "Revolve",
|
||||
Transform { .. } => "Transform",
|
||||
SolidBoolean { .. } => "SolidBoolean",
|
||||
AddVertex { .. } => "AddVertex",
|
||||
Delete { .. } => "Delete",
|
||||
CreateMany(_) => "CreateMany",
|
||||
TransformMany { .. } => "TransformMany",
|
||||
DeleteMany(_) => "DeleteMany",
|
||||
CreateInsert(_) => "CreateInsert",
|
||||
CreateViewport(_) => "CreateViewport",
|
||||
CreateText(_) => "CreateText",
|
||||
CreateMText(_) => "CreateMText",
|
||||
SetTextContent { .. } => "SetTextContent",
|
||||
CreateHatch(_) => "CreateHatch",
|
||||
CreateDimensionLinear(_) => "CreateDimensionLinear",
|
||||
SetAttribute { .. } => "SetAttribute",
|
||||
SetViewportView { .. } => "SetViewportView",
|
||||
CreateRasterImage(_) => "CreateRasterImage",
|
||||
Loft { .. } => "Loft",
|
||||
CreateDimensionRadius(_) => "CreateDimensionRadius",
|
||||
CreateDimensionDiameter(_) => "CreateDimensionDiameter",
|
||||
CreateDimensionAngular(_) => "CreateDimensionAngular",
|
||||
CreateAttributeDefinition(_) => "CreateAttributeDefinition",
|
||||
CreateTable(_) => "CreateTable",
|
||||
CreateDimensionAngular2Ln(_) => "CreateDimensionAngular2Ln",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//! Read queries — typed, transport-agnostic reads of entity state.
|
||||
//! The `Query` enum is **hand-maintained** (append-only) in `src/gen/query_gen.rs`
|
||||
//! — NOT derived from the spec by build.rs (the spec's `query` names must map to
|
||||
//! a variant here, asserted by a test). This module re-exports it and holds the
|
||||
//! hand-written result DTOs.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::id::ObjectId;
|
||||
use crate::revision::GeometryRevision;
|
||||
|
||||
pub use crate::gen::Query;
|
||||
|
||||
/// Axis-aligned bounding box (plain-data mirror of the kernel's non-serde `Aabb`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Aabb {
|
||||
pub min: [f64; 3],
|
||||
pub max: [f64; 3],
|
||||
}
|
||||
|
||||
impl Aabb {
|
||||
#[cfg(feature = "host")]
|
||||
pub fn overlaps(&self, other: &Aabb) -> bool {
|
||||
cadkernel::brep::Aabb {
|
||||
min: self.min,
|
||||
max: self.max,
|
||||
}
|
||||
.overlaps(&cadkernel::brep::Aabb {
|
||||
min: other.min,
|
||||
max: other.max,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic, untyped view of an entity returned by [`Query::GetEntity`].
|
||||
/// Family-specific data lives in the typed construction specs; this carries
|
||||
/// identity + kind + a coarse bounding box so any first-level entity is readable.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct EntityView {
|
||||
pub id: ObjectId,
|
||||
/// The acadrust `EntityType` variant name (e.g. "Solid3D", "Line").
|
||||
pub kind: String,
|
||||
pub bounds: Option<Aabb>,
|
||||
}
|
||||
|
||||
/// The result of a single [`Query`] (one per query in a `Queries` batch).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum QueryResult {
|
||||
Entity(EntityView),
|
||||
Bounds(Aabb),
|
||||
Centroid([f64; 3]),
|
||||
Volume(f64),
|
||||
/// Whether two entities' bounds overlap (two-phase conditional modelling).
|
||||
Intersects(bool),
|
||||
Revision(GeometryRevision),
|
||||
/// The text content of a Text/MText annotation.
|
||||
TextContent(String),
|
||||
/// A hatch's boundary loops (outer + islands), each a closed polyline.
|
||||
HatchBoundary(Vec<Vec<[f64; 2]>>),
|
||||
/// A dimension's measured value (distance for linear/radius, degrees for angular).
|
||||
DimensionMeasurement(f64),
|
||||
/// An insert's attributes as (tag, value) pairs.
|
||||
Attributes(Vec<(String, String)>),
|
||||
/// The entities inside a block definition (read-only traversal).
|
||||
BlockEntities(Vec<EntityView>),
|
||||
/// A viewport's view: target point (WCS) + zoom height.
|
||||
ViewportView {
|
||||
target: [f64; 3],
|
||||
height: f64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Convenience: the query name for diagnostics.
|
||||
impl crate::gen::Query {
|
||||
pub fn query_name(&self) -> &'static str {
|
||||
use crate::gen::Query::*;
|
||||
match self {
|
||||
GetEntity { .. } => "GetEntity",
|
||||
GetBounds { .. } => "GetBounds",
|
||||
GetCentroid { .. } => "GetCentroid",
|
||||
GetVolume { .. } => "GetVolume",
|
||||
GetIntersects { .. } => "GetIntersects",
|
||||
GetGeometryRevision => "GetGeometryRevision",
|
||||
GetTextContent { .. } => "GetTextContent",
|
||||
GetHatchBoundary { .. } => "GetHatchBoundary",
|
||||
GetDimensionMeasurement { .. } => "GetDimensionMeasurement",
|
||||
GetAttributes { .. } => "GetAttributes",
|
||||
GetBlockEntities { .. } => "GetBlockEntities",
|
||||
GetViewportView { .. } => "GetViewportView",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Geometry revision counter (mirrors the host scene's `geometry_epoch`).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Monotonic revision of a document's geometry. Bumped exactly once per applied
|
||||
/// write op (`push_undo` + `finalize_op`). Pure queries return the current
|
||||
/// revision without bumping it.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct GeometryRevision(pub u64);
|
||||
|
||||
impl GeometryRevision {
|
||||
pub const fn as_u64(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! In-process transport: drives a `DocApiBackend` directly, inline.
|
||||
//! Used by in-tree/built-in plugins and host tests. Thread-safe via a mutex so
|
||||
//! many handles across many threads share it (calls are serialized).
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::backend::DocApiBackend;
|
||||
use crate::envelope::{DocApiEnvelope, EnvelopeBody, Receipt};
|
||||
use crate::error::ApiResult;
|
||||
use crate::executor;
|
||||
use crate::transport::Transport;
|
||||
|
||||
/// In-process transport: holds the backend behind a mutex (reentrant `&self`).
|
||||
pub struct InProcess<B: DocApiBackend> {
|
||||
backend: Mutex<B>,
|
||||
}
|
||||
|
||||
impl<B: DocApiBackend> InProcess<B> {
|
||||
pub fn new(backend: B) -> Self {
|
||||
Self {
|
||||
backend: Mutex::new(backend),
|
||||
}
|
||||
}
|
||||
/// Borrow the backend immutably (test/introspection helper).
|
||||
pub fn backend(&self) -> std::sync::MutexGuard<'_, B> {
|
||||
self.backend.lock().expect("in-process backend poisoned")
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: DocApiBackend + Send> Transport for InProcess<B> {
|
||||
fn apply(&self, req: DocApiEnvelope) -> ApiResult<Receipt> {
|
||||
req.validate_version()?;
|
||||
let mut backend = self
|
||||
.backend
|
||||
.lock()
|
||||
.map_err(|_| crate::error::ApiError::Transport("in-process backend poisoned".into()))?;
|
||||
match req.body {
|
||||
EnvelopeBody::Op(op) => executor::apply_op(&mut *backend, op),
|
||||
EnvelopeBody::Queries(queries) => executor::apply_queries(&mut *backend, queries),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//! IPC transport: serializes `DocApiEnvelope` into the
|
||||
//! append-only `PluginRequest::DocApiRequest { tab_id, bytes }` variant and ships
|
||||
//! it through the plugin's `PluginRequestSender`. The host routes that variant to
|
||||
//! the same crate executor (`ocs_doc_api::executor`) — one implementation.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use ocs_plugin_api::host::{PluginRequestError, PluginRequestSender};
|
||||
use ocs_plugin_api::ipc::protocol::{PluginRequest, PluginResponse};
|
||||
|
||||
use crate::envelope::{DocApiEnvelope, Receipt};
|
||||
use crate::error::{ApiError, ApiResult};
|
||||
use crate::transport::Transport;
|
||||
|
||||
/// Out-of-process IPC transport over the `ocs_plugin_api` channel. `Send + Sync`;
|
||||
/// the underlying `PluginRequestSender` already serializes + correlates (V4).
|
||||
pub struct OcsPluginApiIpc {
|
||||
sender: Arc<dyn PluginRequestSender>,
|
||||
tab_id: u64,
|
||||
}
|
||||
|
||||
impl OcsPluginApiIpc {
|
||||
pub fn new(sender: Arc<dyn PluginRequestSender>, tab_id: u64) -> Self {
|
||||
Self { sender, tab_id }
|
||||
}
|
||||
|
||||
fn transport_err(e: PluginRequestError) -> ApiError {
|
||||
ApiError::Transport(e.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for OcsPluginApiIpc {
|
||||
fn apply(&self, req: DocApiEnvelope) -> ApiResult<Receipt> {
|
||||
let bytes = bincode::serialize(&req)
|
||||
.map_err(|e| ApiError::Transport(format!("envelope serialize: {e}")))?;
|
||||
let resp = self
|
||||
.sender
|
||||
.request(PluginRequest::DocApiRequest {
|
||||
tab_id: self.tab_id,
|
||||
bytes,
|
||||
})
|
||||
.map_err(Self::transport_err)?;
|
||||
match resp {
|
||||
PluginResponse::DocApiResponse { bytes } => {
|
||||
// The host serializes `ApiResult<Receipt>` so op/query errors
|
||||
// (validation, geometry, unknown id) surface as the SAME
|
||||
// structured `ApiError` the in-process executor produced.
|
||||
let result: ApiResult<Receipt> = bincode::deserialize(&bytes)
|
||||
.map_err(|e| ApiError::Transport(format!("receipt deserialize: {e}")))?;
|
||||
result
|
||||
}
|
||||
PluginResponse::Error(msg) => Err(ApiError::Transport(msg)),
|
||||
other => Err(ApiError::Transport(format!(
|
||||
"unexpected DocApi response variant: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//! The transport boundary — the ONLY plugin↔host boundary.
|
||||
//!
|
||||
//! Portability contract (SPSC / flume / shared-memory safe): a transport impl
|
||||
//! must (a) accept `&self` calls from many threads and serialize sends internally,
|
||||
//! (b) demultiplex responses to the right caller by a correlation id, (c)
|
||||
//! block-with-timeout, (d) expose liveness. This matches the existing V4 channel
|
||||
//! shape, so swapping socket → shm changes only the impl, never the facade.
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
pub mod inproc;
|
||||
#[cfg(feature = "ipc")]
|
||||
pub mod ipc;
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
pub use inproc::InProcess;
|
||||
#[cfg(feature = "ipc")]
|
||||
pub use ipc::OcsPluginApiIpc;
|
||||
|
||||
use crate::envelope::{DocApiEnvelope, Receipt};
|
||||
use crate::error::ApiResult;
|
||||
|
||||
/// The only plugin↔host boundary. Object-centric: one method per unit of work —
|
||||
/// a single write op, or a query batch. `&self` + `Send + Sync` so many entity
|
||||
/// handles across many threads share one transport concurrently.
|
||||
pub trait Transport: Send + Sync {
|
||||
/// Apply `req` (ONE write op, or a query batch) and return its `Receipt`.
|
||||
/// A write request carries exactly one `Operation`; a read request carries
|
||||
/// one-or-more `Query` items (read-only batching is safe). Blocks up to an
|
||||
/// internal timeout. Reentrant: safe to call concurrently from many threads.
|
||||
fn apply(&self, req: DocApiEnvelope) -> ApiResult<Receipt>;
|
||||
|
||||
/// Cheap host-liveness probe (shm has no socket-disconnect to detect a dead peer).
|
||||
fn alive(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Reject invalid numeric inputs before the backend starts an undo operation.
|
||||
use crate::{ops::*, ApiError, ApiResult};
|
||||
|
||||
fn finite(values: &[f64]) -> bool {
|
||||
values.iter().all(|v| v.is_finite())
|
||||
}
|
||||
fn positive(value: f64) -> bool {
|
||||
value.is_finite() && value > 0.0
|
||||
}
|
||||
fn direction(value: &[f64; 3]) -> bool {
|
||||
finite(value) && value.iter().any(|v| *v != 0.0)
|
||||
}
|
||||
fn check(valid: bool, op: &'static str) -> ApiResult<()> {
|
||||
if valid {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::validation(
|
||||
op,
|
||||
"invalid or nonfinite geometry input",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
||||
use Curve2Spec::*;
|
||||
let valid = match spec {
|
||||
Line { start, end } => finite(start) && finite(end) && start != end,
|
||||
Circle { centre, radius } => finite(centre) && positive(*radius),
|
||||
Point { position } => finite(position),
|
||||
Arc {
|
||||
centre,
|
||||
radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
} => finite(centre) && positive(*radius) && finite(&[*start_angle, *end_angle]),
|
||||
Polyline { points, closed } => {
|
||||
points.len() >= if *closed { 3 } else { 2 }
|
||||
&& points.len() <= BULK_ITEM_CAP
|
||||
&& points.iter().all(|p| finite(p) && p[2] == points[0][2])
|
||||
}
|
||||
Ellipse {
|
||||
centre,
|
||||
major_axis,
|
||||
ratio,
|
||||
start,
|
||||
end,
|
||||
} => {
|
||||
finite(centre)
|
||||
&& direction(major_axis)
|
||||
&& positive(*ratio)
|
||||
&& *ratio <= 1.0
|
||||
&& finite(&[*start, *end])
|
||||
}
|
||||
Spline {
|
||||
degree,
|
||||
control_points,
|
||||
knots,
|
||||
weights,
|
||||
} => {
|
||||
*degree > 0
|
||||
&& control_points.len() <= BULK_ITEM_CAP
|
||||
&& knots.len() <= BULK_ITEM_CAP
|
||||
&& weights.len() <= BULK_ITEM_CAP
|
||||
&& cadkernel::space::NurbsCurve3::new_strict(
|
||||
*degree as usize,
|
||||
control_points.clone(),
|
||||
knots.clone(),
|
||||
if weights.is_empty() {
|
||||
vec![1.0; control_points.len()]
|
||||
} else {
|
||||
weights.clone()
|
||||
},
|
||||
)
|
||||
.is_some()
|
||||
}
|
||||
Ray {
|
||||
origin,
|
||||
direction: d,
|
||||
}
|
||||
| XLine {
|
||||
origin,
|
||||
direction: d,
|
||||
} => finite(origin) && direction(d),
|
||||
};
|
||||
check(valid, "CreateCurve")
|
||||
}
|
||||
|
||||
pub(crate) fn operation(op: &Operation) -> ApiResult<()> {
|
||||
use Operation::*;
|
||||
let valid = match op {
|
||||
CreateCurve(spec) => return curve(spec),
|
||||
Extrude { direction: d, .. } => direction(d),
|
||||
Revolve {
|
||||
axis: (pivot, d),
|
||||
angle,
|
||||
..
|
||||
} => finite(pivot) && direction(d) && angle.is_finite() && *angle != 0.0,
|
||||
AddVertex { point, .. } => finite(point),
|
||||
CreateInsert(s) => {
|
||||
finite(&s.insert_point)
|
||||
&& s.scale.is_finite()
|
||||
&& s.scale != 0.0
|
||||
&& s.rotation.is_finite()
|
||||
}
|
||||
CreateViewport(s) => {
|
||||
finite(&s.center)
|
||||
&& finite(&s.view_target)
|
||||
&& positive(s.width)
|
||||
&& positive(s.height)
|
||||
&& positive(s.view_height)
|
||||
}
|
||||
CreateText(s) => finite(&s.insertion_point) && positive(s.height) && s.rotation.is_finite(),
|
||||
CreateMText(s) => finite(&s.insertion_point) && positive(s.height),
|
||||
CreateHatch(s) => {
|
||||
(3..=BULK_ITEM_CAP).contains(&s.boundary.len()) && s.boundary.iter().all(|p| finite(p))
|
||||
}
|
||||
CreateDimensionLinear(s) => {
|
||||
finite(&s.first_point) && finite(&s.second_point) && finite(&s.definition_point)
|
||||
}
|
||||
CreateDimensionRadius(s) | CreateDimensionDiameter(s) => {
|
||||
finite(&s.center) && finite(&s.point)
|
||||
}
|
||||
CreateDimensionAngular(s) | CreateDimensionAngular2Ln(s) => {
|
||||
finite(&s.vertex)
|
||||
&& finite(&s.first_point)
|
||||
&& finite(&s.second_point)
|
||||
&& finite(&s.arc_location)
|
||||
}
|
||||
CreateAttributeDefinition(s) => {
|
||||
finite(&s.insertion_point) && positive(s.height) && s.rotation.is_finite()
|
||||
}
|
||||
SetViewportView {
|
||||
view_target,
|
||||
view_height,
|
||||
..
|
||||
} => finite(view_target) && positive(*view_height),
|
||||
CreateRasterImage(s) => {
|
||||
finite(&s.insertion_point)
|
||||
&& direction(&s.u_vector)
|
||||
&& direction(&s.v_vector)
|
||||
&& s.size.iter().all(|v| positive(*v))
|
||||
}
|
||||
CreateTable(s) => {
|
||||
finite(&s.insertion_point)
|
||||
&& !s.data.is_empty()
|
||||
&& !s.data[0].is_empty()
|
||||
&& s.data
|
||||
.len()
|
||||
.checked_mul(s.data[0].len())
|
||||
.is_some_and(|n| n <= BULK_ITEM_CAP)
|
||||
&& s.data.iter().all(|r| r.len() == s.data[0].len())
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
check(valid, op.op_name())
|
||||
}
|
||||
|
||||
pub(crate) fn solid(spec: &SolidPrimitive) -> ApiResult<()> {
|
||||
use SolidPrimitive::*;
|
||||
let valid = match spec {
|
||||
Cuboid { origin, size } | Wedge { origin, size } => {
|
||||
finite(origin) && size.iter().all(|v| positive(*v))
|
||||
}
|
||||
Sphere { centre, radius } => finite(centre) && positive(*radius),
|
||||
Cylinder {
|
||||
base,
|
||||
radius,
|
||||
height,
|
||||
}
|
||||
| Cone {
|
||||
base,
|
||||
radius,
|
||||
height,
|
||||
} => finite(base) && positive(*radius) && positive(*height),
|
||||
Torus {
|
||||
centre,
|
||||
major_radius,
|
||||
minor_radius,
|
||||
} => finite(centre) && positive(*major_radius) && positive(*minor_radius),
|
||||
};
|
||||
check(valid, "CreateSolid")
|
||||
}
|
||||
Reference in new issue
Block a user