forked from 1b-agent/opencadstudio-upstream
314 lines
14 KiB
Rust
314 lines
14 KiB
Rust
//! 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, LayerInfo};
|
||
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>;
|
||
|
||
/// Attach or replace an XDATA record for `application_name` on `id`.
|
||
/// `None` removes any existing record for that application.
|
||
fn set_xdata(
|
||
&mut self,
|
||
id: ObjectId,
|
||
application_name: &str,
|
||
record: Option<&crate::ops::XDataRecord>,
|
||
) -> ApiResult<()>;
|
||
|
||
/// Read the XDATA record for `application_name` on `id` (`None` if absent).
|
||
fn xdata(
|
||
&self,
|
||
id: ObjectId,
|
||
application_name: &str,
|
||
) -> ApiResult<Option<crate::ops::XDataRecord>>;
|
||
|
||
/// Create a standalone `XRECORD` object from a spec; returns the fresh `ObjectId`.
|
||
fn add_xrecord(&mut self, spec: &crate::ops::XRecordSpec) -> ApiResult<ObjectId>;
|
||
|
||
/// Replace an existing `XRECORD` object's payload in place.
|
||
fn set_xrecord(&mut self, id: ObjectId, spec: &crate::ops::XRecordSpec) -> ApiResult<()>;
|
||
|
||
/// Read an existing `XRECORD` object's payload (`None` if `id` is not an XRecord).
|
||
fn xrecord(&self, id: ObjectId) -> ApiResult<Option<crate::ops::XRecordSpec>>;
|
||
|
||
// ── layer CRUD ─────────────────────────────────────────────────────────
|
||
/// Create a new layer. Fails with `Validation` if the name already exists or is reserved.
|
||
fn create_layer(&mut self, info: &crate::ops::LayerInfo) -> ApiResult<()>;
|
||
|
||
/// Update an existing layer's properties by name. Fails with `Validation` if absent.
|
||
fn update_layer(&mut self, name: &str, info: &crate::ops::LayerInfo) -> ApiResult<()>;
|
||
|
||
/// Delete a layer by name. Fails with `Validation` if the layer is "0",
|
||
/// the current layer, does not exist, or has entities.
|
||
fn delete_layer(&mut self, name: &str) -> ApiResult<()>;
|
||
|
||
/// Look up a layer by raw name, normalizing it. Default implementation scans
|
||
/// `layers()`; backends with O(1) lookup should override.
|
||
fn lookup_layer(&self, name: &str) -> ApiResult<LayerInfo> {
|
||
let norm = crate::layer::normalize_layer_name(name);
|
||
if norm.is_empty() {
|
||
return Err(ApiError::validation("LookupLayer", "empty layer name"));
|
||
}
|
||
self.layers()?
|
||
.into_iter()
|
||
.find(|l| l.normalized_name() == norm)
|
||
.ok_or_else(|| {
|
||
ApiError::validation(
|
||
"LookupLayer",
|
||
format!("layer '{name}' does not exist"),
|
||
)
|
||
})
|
||
}
|
||
|
||
/// Move an entity to a different layer. The target layer must exist.
|
||
fn set_entity_layer(&mut self, id: ObjectId, layer: &str) -> ApiResult<()>;
|
||
|
||
/// List all layers in the document.
|
||
fn layers(&self) -> ApiResult<Vec<crate::ops::LayerInfo>>;
|
||
|
||
/// The layer name of an entity.
|
||
fn entity_layer(&self, id: ObjectId) -> ApiResult<String>;
|
||
|
||
/// Enumerate first-class entities, optionally filtered by kind and/or layer.
|
||
/// `include_bounds` controls whether `EntityView::bounds` is populated; skip it
|
||
/// for lightweight list views.
|
||
fn enumerate_entities(
|
||
&self,
|
||
kind: Option<&str>,
|
||
layer: Option<&str>,
|
||
include_bounds: bool,
|
||
) -> ApiResult<Vec<EntityView>>;
|
||
|
||
/// The WCS location of a Point entity.
|
||
fn point_position(&self, id: ObjectId) -> ApiResult<[f64; 3]>;
|
||
|
||
/// The start and end WCS points of a Line entity.
|
||
fn line_geometry(&self, id: ObjectId) -> ApiResult<([f64; 3], [f64; 3])>;
|
||
|
||
/// 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))
|
||
}
|
||
}
|
||
|
||
/// Whether an object (named-object / XRecord / dictionary) exists. Distinct
|
||
/// from `entity_exists`, which is for first-class entities only.
|
||
fn object_exists(&self, id: ObjectId) -> bool;
|
||
|
||
/// 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) {}
|
||
}
|