//! 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; /// 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( &mut self, id: ObjectId, f: &mut dyn FnMut(&KernelBody) -> ApiResult, ) -> ApiResult { 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; /// 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; /// Prepare every entity before recording one undo step and committing the batch. fn create_many(&mut self, specs: &[crate::ops::EntitySpec]) -> ApiResult>; /// 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; /// Add a paper-space `VIEWPORT`; returns the fresh `ObjectId`. fn add_viewport(&mut self, spec: &crate::ops::ViewportSpec) -> ApiResult; /// Add a single-line `TEXT` annotation; returns the fresh `ObjectId`. fn add_text(&mut self, spec: &crate::ops::TextSpec) -> ApiResult; /// Add a multi-line `MTEXT` annotation; returns the fresh `ObjectId`. fn add_mtext(&mut self, spec: &crate::ops::MTextSpec) -> ApiResult; /// 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; /// Add a `HATCH` (single closed polyline boundary); returns the fresh `ObjectId`. fn add_hatch(&mut self, spec: &crate::ops::HatchSpec) -> ApiResult; /// A hatch's boundary loops (outer + islands) as closed polylines. fn hatch_boundary(&self, id: ObjectId) -> ApiResult>>; /// Add a linear `DIMENSION`; returns the fresh `ObjectId`. fn add_dimension_linear(&mut self, spec: &crate::ops::DimensionSpec) -> ApiResult; /// Add a radial `DIMENSION`; returns the fresh `ObjectId`. fn add_dimension_radius( &mut self, spec: &crate::ops::DimensionRadialSpec, ) -> ApiResult; /// Add a diameter `DIMENSION`; returns the fresh `ObjectId`. fn add_dimension_diameter( &mut self, spec: &crate::ops::DimensionRadialSpec, ) -> ApiResult; /// Add a 3-point angular `DIMENSION`; returns the fresh `ObjectId`. fn add_dimension_angular( &mut self, spec: &crate::ops::DimensionAngularSpec, ) -> ApiResult; /// Add an `ATTDEF` (in-block attribute definition); returns the fresh `ObjectId`. fn add_attribute_definition( &mut self, spec: &crate::ops::AttributeDefinitionSpec, ) -> ApiResult; /// Add a `TABLE` (rows × columns grid); returns the fresh `ObjectId`. fn add_table(&mut self, spec: &crate::ops::TableSpec) -> ApiResult; /// Add a 2-line angular `DIMENSION`; returns the fresh `ObjectId`. fn add_dimension_angular2ln( &mut self, spec: &crate::ops::DimensionAngularSpec, ) -> ApiResult; /// A dimension's measured value (distance for linear/radius, degrees for angular). fn dimension_measurement(&self, id: ObjectId) -> ApiResult; /// 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>; /// The entities inside a block definition (read-only traversal), as views. fn block_entities(&self, block_name: &str) -> ApiResult>; /// 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; /// 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)], ) -> ApiResult; /// 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>; /// Create a standalone `XRECORD` object from a spec; returns the fresh `ObjectId`. fn add_xrecord(&mut self, spec: &crate::ops::XRecordSpec) -> ApiResult; /// 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>; // ── 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 { 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>; /// The layer name of an entity. fn entity_layer(&self, id: ObjectId) -> ApiResult; /// 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>; /// 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; /// 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; /// 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>; fn profile_plane(&self, _id: ObjectId) -> ApiResult { 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; fn centroid(&mut self, id: ObjectId) -> ApiResult<[f64; 3]>; fn volume(&mut self, id: ObjectId) -> ApiResult; 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) {} }