Files
OneCAD-Draft/crates/ocs_doc_api/src/facade.rs
T

1414 lines
47 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The object-centric facade.
//!
//! Hierarchy: [`DocApi`] (root: session + transport) → [`Document`] →
//! collections ([`SolidCollection`], [`CurveCollection`], [`EntityCollection`])
//! → typed handles ([`Solid`], [`Line`], [`Circle`], [`Polyline`], [`Point`],
//! [`Entity`]) carrying methods. Every handle = `{ ObjectId, Arc<Session> }` —
//! `Clone`, `Send`, `Sync`; it never holds kernel geometry. Per-op autocommit:
//! each method is ONE atomic host call = one undo step (no transactions).
use std::sync::Arc;
use crate::envelope::{DocApiEnvelope, Receipt};
use crate::error::{ApiError, ApiResult};
use crate::gen::{Operation, Query};
use crate::id::ObjectId;
use crate::ops::{BoolOp, Curve2Spec, PlacementSpec, SolidPrimitive, XDataRecord, XRecordSpec};
use crate::query::{Aabb, EntityView, QueryResult};
use crate::revision::GeometryRevision;
use crate::transport::Transport;
/// Shared session: the transport. Held by every handle. The transport already
/// carries the tab binding (IPC) or the backend (in-process), so the session
/// stores no per-tab field.
#[derive(Clone)]
pub struct Session {
transport: Arc<dyn Transport>,
valid_tab: bool,
}
impl Session {
fn validate_tab(&self) -> ApiResult<()> {
if !self.valid_tab {
return Err(ApiError::validation(
"document",
"transport is bound to another tab",
));
}
Ok(())
}
fn same_transport(&self, other: &Session) -> ApiResult<()> {
self.validate_tab()?;
other.validate_tab()?;
if !Arc::ptr_eq(&self.transport, &other.transport) {
return Err(ApiError::validation(
"document",
"handles belong to different sessions",
));
}
Ok(())
}
fn apply_op(&self, op: Operation) -> ApiResult<Receipt> {
self.validate_tab()?;
self.transport.apply(DocApiEnvelope::op(op))
}
fn apply_queries(&self, queries: Vec<Query>) -> ApiResult<Receipt> {
self.validate_tab()?;
self.transport.apply(DocApiEnvelope::queries(queries))
}
fn one_query(&self, q: Query) -> ApiResult<QueryResult> {
let mut r = self.apply_queries(vec![q])?;
let result = r.query_results.drain(..).next();
result.ok_or_else(|| ApiError::Transport("empty query result".into()))
}
}
/// The API root: owns the session (transport + active tab). Entry point.
#[derive(Clone)]
pub struct DocApi {
transport: Arc<dyn Transport>,
active_tab: u64,
}
impl DocApi {
/// Connect over an existing transport (IPC or in-process) bound to `active_tab`.
pub fn connect(transport: Arc<dyn Transport>, active_tab: u64) -> Self {
Self {
transport,
active_tab,
}
}
/// Connect in-process over a backend (host feature).
#[cfg(feature = "host")]
pub fn in_process<B: crate::backend::DocApiBackend + Send + 'static>(
backend: B,
active_tab: u64,
) -> Self {
Self::connect(
Arc::new(crate::transport::InProcess::new(backend)),
active_tab,
)
}
/// The active tab id this root is bound to.
pub fn active_tab(&self) -> u64 {
self.active_tab
}
/// Liveness probe of the host/transport.
pub fn alive(&self) -> bool {
self.transport.alive()
}
/// Bind a document to the transport's tab. Requests for another tab fail.
pub fn document(&self, tab: u64) -> Document {
Document {
session: Session {
transport: Arc::clone(&self.transport),
valid_tab: tab == self.active_tab,
},
}
}
}
/// One open document tab: factory + lookup + queries + read-guards.
#[derive(Clone)]
pub struct Document {
session: Session,
}
impl Document {
pub fn solids(&self) -> SolidCollection {
SolidCollection {
session: self.session.clone(),
}
}
pub fn curves(&self) -> CurveCollection {
CurveCollection {
session: self.session.clone(),
}
}
pub fn entities(&self) -> EntityCollection {
EntityCollection {
session: self.session.clone(),
}
}
/// Current geometry revision (query, no bump).
pub fn revision(&self) -> ApiResult<GeometryRevision> {
match self.session.one_query(Query::GetGeometryRevision)? {
QueryResult::Revision(r) => Ok(r),
_ => Err(ApiError::Transport("unexpected revision result".into())),
}
}
/// Standalone read-guard: fail with `ApiError::Validation` if the document's
/// revision moved past `expected` (read-modify-write guard; NOT a batch pre-condition).
pub fn assert_revision(&self, expected: GeometryRevision) -> ApiResult<()> {
let now = self.revision()?;
if now == expected {
Ok(())
} else {
Err(ApiError::Validation {
op: "assert_revision".to_string(),
reason: format!(
"revision moved: expected {expected:?}, now {now:?}; re-read and retry"
),
})
}
}
/// Read-only traversal of a block definition's entities (ids + kinds + bounds).
pub fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>> {
match self.session.one_query(Query::GetBlockEntities {
block_name: block_name.to_string(),
})? {
QueryResult::BlockEntities(v) => Ok(v),
_ => Err(ApiError::Transport(
"unexpected block-entities result".into(),
)),
}
}
/// List all layers in the document.
pub fn layers(&self) -> ApiResult<Vec<crate::ops::LayerInfo>> {
match self.session.one_query(Query::ListLayers)? {
QueryResult::Layers(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected layers result".into())),
}
}
/// Create a new layer. One undo step; fails if the normalized name already
/// exists or is empty.
pub fn create_layer(&self, info: &crate::ops::LayerInfo) -> ApiResult<()> {
if !crate::layer::is_valid_layer_name(&info.name) {
return Err(ApiError::validation(
"CreateLayer",
"empty layer name",
));
}
self.session
.apply_op(Operation::CreateLayer(info.clone()))?;
Ok(())
}
/// Update an existing layer's properties by name. One undo step; `info.name`
/// becomes the new display name (case-insensitive key unchanged).
pub fn update_layer(&self, name: &str, info: &crate::ops::LayerInfo) -> ApiResult<()> {
if !crate::layer::is_valid_layer_name(&info.name) {
return Err(ApiError::validation(
"UpdateLayer",
"empty layer name",
));
}
self.session.apply_op(Operation::UpdateLayer {
name: name.to_string(),
info: info.clone(),
})?;
Ok(())
}
/// Delete a layer by name. One undo step; fails for layer "0", the current
/// layer, or any layer still referenced by entities.
pub fn delete_layer(&self, name: &str) -> ApiResult<()> {
self.session.apply_op(Operation::DeleteLayer {
name: name.to_string(),
})?;
Ok(())
}
/// Set XDATA on many entities in a single atomic operation.
///
/// Each tuple is `(entity_id, application_name, record)`. Pass `None` for
/// `record` to delete the application's data. Fails up-front if any id is
/// invalid; the whole batch is one undo step.
pub fn set_xdata_many<I, S>(&self, items: I) -> ApiResult<()>
where
I: IntoIterator<Item = (ObjectId, S, Option<XDataRecord>)>,
S: Into<String>,
{
let updates: Vec<_> = items
.into_iter()
.map(|(id, app, record)| (id, app.into(), record))
.collect();
if updates.is_empty() {
return Ok(());
}
self.session.apply_op(Operation::SetXDataMany(updates))?;
Ok(())
}
/// Batch of read-only queries in ONE round-trip (safe: no mutation/undo).
/// The closure records queries on a [`QueryBatch`]; the results are returned
/// in the same order as a [`QueryResults`] view the caller destructures.
pub fn query_batch<F: FnOnce(&mut QueryBatch)>(&self, f: F) -> ApiResult<QueryResults> {
let mut qb = QueryBatch {
queries: Vec::new(),
session: self.session.clone(),
error: None,
};
f(&mut qb);
if let Some(error) = qb.error {
return Err(error);
}
let receipt = self.session.apply_queries(qb.queries)?;
Ok(QueryResults {
results: receipt.query_results,
})
}
}
/// Records queries for [`Document::query_batch`]. Each method appends one query.
pub struct QueryBatch {
queries: Vec<Query>,
session: Session,
error: Option<ApiError>,
}
impl QueryBatch {
fn check(&mut self, e: &impl HasId) {
if let Err(error) = e.validate_session(&self.session) {
self.error = Some(error);
}
}
pub fn bounds(&mut self, e: &impl HasId) {
self.check(e);
self.queries.push(Query::GetBounds { id: e.id() });
}
pub fn volume(&mut self, e: &impl HasId) {
self.check(e);
self.queries.push(Query::GetVolume { id: e.id() });
}
pub fn centroid(&mut self, e: &impl HasId) {
self.check(e);
self.queries.push(Query::GetCentroid { id: e.id() });
}
pub fn intersects(&mut self, a: &impl HasId, b: &impl HasId) {
self.check(a);
self.check(b);
self.queries.push(Query::GetIntersects {
a: a.id(),
b: b.id(),
});
}
pub fn revision(&mut self) {
self.queries.push(Query::GetGeometryRevision);
}
}
/// Ordered results of a [`Document::query_batch`], one per recorded query.
pub struct QueryResults {
results: Vec<QueryResult>,
}
impl QueryResults {
fn at(&self, i: usize) -> ApiResult<&QueryResult> {
self.results
.get(i)
.ok_or_else(|| ApiError::Transport("query result missing".into()))
}
pub fn bounds(&self, i: usize) -> ApiResult<Aabb> {
match self.at(i)? {
QueryResult::Bounds(b) => Ok(*b),
_ => Err(ApiError::Transport("result is not bounds".into())),
}
}
pub fn volume(&self, i: usize) -> ApiResult<f64> {
match self.at(i)? {
QueryResult::Volume(v) => Ok(*v),
_ => Err(ApiError::Transport("result is not volume".into())),
}
}
pub fn centroid(&self, i: usize) -> ApiResult<[f64; 3]> {
match self.at(i)? {
QueryResult::Centroid(c) => Ok(*c),
_ => Err(ApiError::Transport("result is not centroid".into())),
}
}
pub fn intersects(&self, i: usize) -> ApiResult<bool> {
match self.at(i)? {
QueryResult::Intersects(x) => Ok(*x),
_ => Err(ApiError::Transport("result is not intersects".into())),
}
}
pub fn revision(&self, i: usize) -> ApiResult<GeometryRevision> {
match self.at(i)? {
QueryResult::Revision(r) => Ok(*r),
_ => Err(ApiError::Transport("result is not revision".into())),
}
}
}
/// Something with an `ObjectId` (all handles + raw ids).
pub trait HasId {
fn id(&self) -> ObjectId;
/// Typed handles retain their session; raw ids are relative to the receiver.
fn session(&self) -> Option<&Session> {
None
}
fn validate_session(&self, target: &Session) -> ApiResult<()> {
if let Some(session) = self.session() {
session.same_transport(target)?;
}
Ok(())
}
}
impl HasId for ObjectId {
fn id(&self) -> ObjectId {
*self
}
}
// ── Typed handles ────────────────────────────────────────────────────────────
macro_rules! handle {
($name:ident) => {
#[derive(Clone)]
pub struct $name {
session: Session,
id: ObjectId,
}
impl std::fmt::Debug for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, concat!(stringify!($name), "({:?})"), self.id)
}
}
impl $name {
fn new(session: Session, id: ObjectId) -> Self {
Self { session, id }
}
}
impl HasId for $name {
fn session(&self) -> Option<&Session> {
Some(&self.session)
}
fn id(&self) -> ObjectId {
self.id
}
}
};
}
macro_rules! entity_handle {
($name:ident) => {
handle!($name);
impl $name {
pub fn bounds(&self) -> ApiResult<Aabb> {
match self.session.one_query(Query::GetBounds { id: self.id })? {
QueryResult::Bounds(b) => Ok(b),
_ => Err(ApiError::Transport("unexpected bounds result".into())),
}
}
pub fn delete(&self) -> ApiResult<()> {
self.session.apply_op(Operation::Delete { id: self.id })?;
Ok(())
}
pub fn transform(&self, placement: PlacementSpec) -> ApiResult<()> {
self.session.apply_op(Operation::Transform {
id: self.id,
placement,
})?;
Ok(())
}
/// Return the entity's current layer name (normalized uppercase).
pub fn layer(&self) -> ApiResult<String> {
match self
.session
.one_query(Query::GetEntityLayer { id: self.id })?
{
QueryResult::EntityLayer(name) => Ok(name),
_ => Err(ApiError::Transport("unexpected entity layer result".into())),
}
}
/// Move the entity to a different layer. One undo step; fails if the
/// target layer does not exist or is locked.
pub fn set_layer(&self, layer: &str) -> ApiResult<()> {
self.session.apply_op(Operation::SetEntityLayer {
id: self.id,
layer: layer.to_string(),
})?;
Ok(())
}
}
};
}
entity_handle!(Entity);
entity_handle!(Solid);
entity_handle!(Line);
entity_handle!(Circle);
entity_handle!(Polyline);
entity_handle!(Point);
entity_handle!(ArcCurve);
entity_handle!(Ellipse);
entity_handle!(Spline);
entity_handle!(Ray);
entity_handle!(XLine);
entity_handle!(Text);
entity_handle!(MText);
entity_handle!(Dimension);
handle!(XRecord);
impl XRecord {
/// Read the XRECORD payload.
pub fn payload(&self) -> ApiResult<XRecordSpec> {
match self.session.one_query(Query::GetXRecord { id: self.id })? {
QueryResult::XRecord(spec) => Ok(spec),
_ => Err(ApiError::Transport("unexpected xrecord result".into())),
}
}
/// Replace the XRECORD payload in place (one undo step).
pub fn set_payload(&self, spec: &XRecordSpec) -> ApiResult<()> {
self.session.apply_op(Operation::SetXRecord {
id: self.id,
spec: spec.clone(),
})?;
Ok(())
}
}
impl Dimension {
/// The measured value of this dimension (distance for linear/radius, degrees
/// for angular).
pub fn measurement(&self) -> ApiResult<f64> {
match self
.session
.one_query(Query::GetDimensionMeasurement { id: self.id })?
{
QueryResult::DimensionMeasurement(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected measurement result".into())),
}
}
}
impl Entity {
pub fn view(&self) -> ApiResult<EntityView> {
match self.session.one_query(Query::GetEntity { id: self.id })? {
QueryResult::Entity(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected entity result".into())),
}
}
/// This insert's attributes as (tag, value) pairs. Insert-only.
pub fn attributes(&self) -> ApiResult<Vec<(String, String)>> {
match self
.session
.one_query(Query::GetAttributes { id: self.id })?
{
QueryResult::Attributes(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected attributes result".into())),
}
}
/// Set an attribute `value` for `tag` on this insert (adds if absent). One undo step.
pub fn set_attribute(&self, tag: &str, value: &str) -> ApiResult<()> {
self.session.apply_op(Operation::SetAttribute {
id: self.id,
tag: tag.to_string(),
value: value.to_string(),
})?;
Ok(())
}
/// This viewport's view (target WCS + zoom height). Viewport-only.
pub fn viewport_view(&self) -> ApiResult<([f64; 3], f64)> {
match self
.session
.one_query(Query::GetViewportView { id: self.id })?
{
QueryResult::ViewportView { target, height } => Ok((target, height)),
_ => Err(ApiError::Transport(
"unexpected viewport-view result".into(),
)),
}
}
/// Retarget / re-zoom this viewport (one undo step). Viewport-only.
pub fn set_view(&self, view_target: [f64; 3], view_height: f64) -> ApiResult<()> {
self.session.apply_op(Operation::SetViewportView {
id: self.id,
view_target,
view_height,
})?;
Ok(())
}
/// The XDATA record for `application_name` on this entity (`None` if absent).
pub fn xdata(&self, application_name: &str) -> ApiResult<Option<XDataRecord>> {
match self.session.one_query(Query::GetXData {
id: self.id,
application_name: application_name.to_string(),
})? {
QueryResult::XData(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected xdata result".into())),
}
}
/// Attach or replace an XDATA record for `application_name` on this entity.
/// Pass `None` to remove the record (one undo step).
pub fn set_xdata(&self, application_name: &str, record: Option<XDataRecord>) -> ApiResult<()> {
self.session.apply_op(Operation::SetXData {
id: self.id,
application_name: application_name.to_string(),
record,
})?;
Ok(())
}
pub fn as_solid(&self) -> Option<Solid> {
// Typed downcast is validated by the view's kind.
matches!(self.view().ok()?.kind.as_str(), "Solid3D")
.then(|| Solid::new(self.session.clone(), self.id))
}
}
impl Solid {
fn boolean(&self, op: BoolOp, other: &Solid) -> ApiResult<Solid> {
self.session.same_transport(&other.session)?;
let receipt = self.session.apply_op(Operation::SolidBoolean {
op,
a: self.id,
b: other.id,
erase_sources: true,
})?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("boolean returned no id".into()))?;
Ok(Solid::new(self.session.clone(), id))
}
pub fn intersect(&self, other: &Solid) -> ApiResult<Solid> {
self.boolean(BoolOp::Intersection, other)
}
pub fn union(&self, other: &Solid) -> ApiResult<Solid> {
self.boolean(BoolOp::Union, other)
}
pub fn subtract(&self, other: &Solid) -> ApiResult<Solid> {
self.boolean(BoolOp::Difference, other)
}
pub fn volume(&self) -> ApiResult<f64> {
match self.session.one_query(Query::GetVolume { id: self.id })? {
QueryResult::Volume(v) => Ok(v),
_ => Err(ApiError::Transport("unexpected volume result".into())),
}
}
pub fn centroid(&self) -> ApiResult<[f64; 3]> {
match self.session.one_query(Query::GetCentroid { id: self.id })? {
QueryResult::Centroid(c) => Ok(c),
_ => Err(ApiError::Transport("unexpected centroid result".into())),
}
}
pub fn intersects(&self, other: &Solid) -> ApiResult<bool> {
self.session.same_transport(&other.session)?;
match self.session.one_query(Query::GetIntersects {
a: self.id,
b: other.id,
})? {
QueryResult::Intersects(x) => Ok(x),
_ => Err(ApiError::Transport("unexpected intersects result".into())),
}
}
}
impl Polyline {
pub fn add_vertex(&self, at: usize, point: [f64; 3]) -> ApiResult<()> {
self.session.apply_op(Operation::AddVertex {
id: self.id,
at,
point,
})?;
Ok(())
}
}
macro_rules! text_handle {
($name:ident) => {
impl $name {
/// The text content of this annotation.
pub fn content(&self) -> ApiResult<String> {
match self
.session
.one_query(Query::GetTextContent { id: self.id })?
{
QueryResult::TextContent(s) => Ok(s),
_ => Err(ApiError::Transport("unexpected content result".into())),
}
}
/// Replace the text content (one undo step).
pub fn set_content(&self, value: &str) -> ApiResult<()> {
self.session.apply_op(Operation::SetTextContent {
id: self.id,
value: value.to_string(),
})?;
Ok(())
}
}
};
}
text_handle!(Text);
text_handle!(MText);
// ── Collections (construction / lookup) ─────────────────────────────────────
#[derive(Clone)]
pub struct SolidCollection {
session: Session,
}
impl SolidCollection {
fn create(&self, prim: SolidPrimitive) -> ApiResult<Solid> {
let receipt = self.session.apply_op(Operation::CreateSolid(prim))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_solid returned no id".into()))?;
Ok(Solid::new(self.session.clone(), id))
}
pub fn create_cuboid(&self, origin: [f64; 3], size: [f64; 3]) -> ApiResult<Solid> {
self.create(SolidPrimitive::Cuboid { origin, size })
}
pub fn create_sphere(&self, centre: [f64; 3], radius: f64) -> ApiResult<Solid> {
self.create(SolidPrimitive::Sphere { centre, radius })
}
pub fn create_cylinder(&self, base: [f64; 3], radius: f64, height: f64) -> ApiResult<Solid> {
self.create(SolidPrimitive::Cylinder {
base,
radius,
height,
})
}
pub fn create_cone(&self, base: [f64; 3], radius: f64, height: f64) -> ApiResult<Solid> {
self.create(SolidPrimitive::Cone {
base,
radius,
height,
})
}
pub fn create_torus(
&self,
centre: [f64; 3],
major_radius: f64,
minor_radius: f64,
) -> ApiResult<Solid> {
self.create(SolidPrimitive::Torus {
centre,
major_radius,
minor_radius,
})
}
pub fn create_wedge(&self, origin: [f64; 3], size: [f64; 3]) -> ApiResult<Solid> {
self.create(SolidPrimitive::Wedge { origin, size })
}
pub fn extrude(&self, profile: &impl HasId, direction: [f64; 3]) -> ApiResult<Solid> {
profile.validate_session(&self.session)?;
let receipt = self.session.apply_op(Operation::Extrude {
profile: profile.id(),
direction,
})?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("extrude returned no id".into()))?;
Ok(Solid::new(self.session.clone(), id))
}
pub fn revolve(
&self,
profile: &impl HasId,
pivot: [f64; 3],
axis: [f64; 3],
angle: f64,
) -> ApiResult<Solid> {
profile.validate_session(&self.session)?;
let receipt = self.session.apply_op(Operation::Revolve {
profile: profile.id(),
axis: (pivot, axis),
angle,
})?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("revolve returned no id".into()))?;
Ok(Solid::new(self.session.clone(), id))
}
/// Loft a solid through >= 2 profile entities (polylines/circles/arcs).
pub fn loft(&self, profiles: &[impl HasId]) -> ApiResult<Solid> {
for profile in profiles {
profile.validate_session(&self.session)?;
}
let ids: Vec<ObjectId> = profiles.iter().map(|p| p.id()).collect();
let receipt = self.session.apply_op(Operation::Loft { profiles: ids })?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("loft returned no id".into()))?;
Ok(Solid::new(self.session.clone(), id))
}
}
#[derive(Clone)]
pub struct CurveCollection {
session: Session,
}
impl CurveCollection {
fn create_curve(&self, spec: Curve2Spec) -> ApiResult<(Session, ObjectId)> {
let receipt = self.session.apply_op(Operation::CreateCurve(spec))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_curve returned no id".into()))?;
Ok((self.session.clone(), id))
}
pub fn create_line(&self, start: [f64; 3], end: [f64; 3]) -> ApiResult<Line> {
let (s, id) = self.create_curve(Curve2Spec::Line {
start,
end,
layer: None,
})?;
Ok(Line::new(s, id))
}
pub fn create_line_on_layer(
&self,
start: [f64; 3],
end: [f64; 3],
layer: &str,
) -> ApiResult<Line> {
let (s, id) = self.create_curve(Curve2Spec::Line {
start,
end,
layer: Some(layer.to_string()),
})?;
Ok(Line::new(s, id))
}
pub fn create_circle(&self, centre: [f64; 3], radius: f64) -> ApiResult<Circle> {
let (s, id) = self.create_curve(Curve2Spec::Circle {
centre,
radius,
layer: None,
})?;
Ok(Circle::new(s, id))
}
pub fn create_circle_on_layer(
&self,
centre: [f64; 3],
radius: f64,
layer: &str,
) -> ApiResult<Circle> {
let (s, id) = self.create_curve(Curve2Spec::Circle {
centre,
radius,
layer: Some(layer.to_string()),
})?;
Ok(Circle::new(s, id))
}
pub fn create_polyline(&self, points: &[[f64; 3]], closed: bool) -> ApiResult<Polyline> {
let (s, id) = self.create_curve(Curve2Spec::Polyline {
points: points.to_vec(),
closed,
layer: None,
})?;
Ok(Polyline::new(s, id))
}
pub fn create_polyline_on_layer(
&self,
points: &[[f64; 3]],
closed: bool,
layer: &str,
) -> ApiResult<Polyline> {
let (s, id) = self.create_curve(Curve2Spec::Polyline {
points: points.to_vec(),
closed,
layer: Some(layer.to_string()),
})?;
Ok(Polyline::new(s, id))
}
pub fn create_point(&self, position: [f64; 3]) -> ApiResult<Point> {
let (s, id) = self.create_curve(Curve2Spec::Point {
position,
layer: None,
})?;
Ok(Point::new(s, id))
}
pub fn create_point_on_layer(&self, position: [f64; 3], layer: &str) -> ApiResult<Point> {
let (s, id) = self.create_curve(Curve2Spec::Point {
position,
layer: Some(layer.to_string()),
})?;
Ok(Point::new(s, id))
}
pub fn create_arc(
&self,
centre: [f64; 3],
radius: f64,
start_angle: f64,
end_angle: f64,
) -> ApiResult<ArcCurve> {
let (s, id) = self.create_curve(Curve2Spec::Arc {
centre,
radius,
start_angle,
end_angle,
layer: None,
})?;
Ok(ArcCurve::new(s, id))
}
pub fn create_arc_on_layer(
&self,
centre: [f64; 3],
radius: f64,
start_angle: f64,
end_angle: f64,
layer: &str,
) -> ApiResult<ArcCurve> {
let (s, id) = self.create_curve(Curve2Spec::Arc {
centre,
radius,
start_angle,
end_angle,
layer: Some(layer.to_string()),
})?;
Ok(ArcCurve::new(s, id))
}
pub fn create_ellipse(
&self,
centre: [f64; 3],
major_axis: [f64; 3],
ratio: f64,
start: f64,
end: f64,
) -> ApiResult<Ellipse> {
let (s, id) = self.create_curve(Curve2Spec::Ellipse {
centre,
major_axis,
ratio,
start,
end,
layer: None,
})?;
Ok(Ellipse::new(s, id))
}
pub fn create_ellipse_on_layer(
&self,
centre: [f64; 3],
major_axis: [f64; 3],
ratio: f64,
start: f64,
end: f64,
layer: &str,
) -> ApiResult<Ellipse> {
let (s, id) = self.create_curve(Curve2Spec::Ellipse {
centre,
major_axis,
ratio,
start,
end,
layer: Some(layer.to_string()),
})?;
Ok(Ellipse::new(s, id))
}
pub fn create_spline(
&self,
degree: i32,
control_points: &[[f64; 3]],
knots: &[f64],
weights: &[f64],
) -> ApiResult<Spline> {
let (s, id) = self.create_curve(Curve2Spec::Spline {
degree,
control_points: control_points.to_vec(),
knots: knots.to_vec(),
weights: weights.to_vec(),
layer: None,
})?;
Ok(Spline::new(s, id))
}
pub fn create_spline_on_layer(
&self,
degree: i32,
control_points: &[[f64; 3]],
knots: &[f64],
weights: &[f64],
layer: &str,
) -> ApiResult<Spline> {
let (s, id) = self.create_curve(Curve2Spec::Spline {
degree,
control_points: control_points.to_vec(),
knots: knots.to_vec(),
weights: weights.to_vec(),
layer: Some(layer.to_string()),
})?;
Ok(Spline::new(s, id))
}
pub fn create_ray(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<Ray> {
let (s, id) = self.create_curve(Curve2Spec::Ray {
origin,
direction,
layer: None,
})?;
Ok(Ray::new(s, id))
}
pub fn create_ray_on_layer(
&self,
origin: [f64; 3],
direction: [f64; 3],
layer: &str,
) -> ApiResult<Ray> {
let (s, id) = self.create_curve(Curve2Spec::Ray {
origin,
direction,
layer: Some(layer.to_string()),
})?;
Ok(Ray::new(s, id))
}
pub fn create_xline(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<XLine> {
let (s, id) = self.create_curve(Curve2Spec::XLine {
origin,
direction,
layer: None,
})?;
Ok(XLine::new(s, id))
}
pub fn create_xline_on_layer(
&self,
origin: [f64; 3],
direction: [f64; 3],
layer: &str,
) -> ApiResult<XLine> {
let (s, id) = self.create_curve(Curve2Spec::XLine {
origin,
direction,
layer: Some(layer.to_string()),
})?;
Ok(XLine::new(s, id))
}
/// A RASTER_IMAGE placed at `insertion_point` (host auto-registers the image definition).
pub fn create_raster_image(
&self,
file_path: &str,
insertion_point: [f64; 3],
u_vector: [f64; 3],
v_vector: [f64; 3],
size: [f64; 2],
) -> ApiResult<Entity> {
let receipt =
self.session
.apply_op(Operation::CreateRasterImage(crate::ops::RasterImageSpec {
file_path: file_path.to_string(),
insertion_point,
u_vector,
v_vector,
size,
}))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_raster_image returned no id".into()))?;
Ok(Entity::new(self.session.clone(), id))
}
/// A solid (or pattern) HATCH over a single closed polyline boundary.
pub fn create_hatch(&self, boundary: &[[f64; 2]], solid: bool) -> ApiResult<Entity> {
let receipt = self
.session
.apply_op(Operation::CreateHatch(crate::ops::HatchSpec {
boundary: boundary.to_vec(),
solid,
}))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_hatch returned no id".into()))?;
Ok(Entity::new(self.session.clone(), id))
}
/// A 2-line angular DIMENSION: angle between lines (vertex→first) and (vertex→second).
pub fn create_dimension_angular2ln(
&self,
vertex: [f64; 3],
first_point: [f64; 3],
second_point: [f64; 3],
arc_location: [f64; 3],
) -> ApiResult<Dimension> {
let receipt = self.session.apply_op(Operation::CreateDimensionAngular2Ln(
crate::ops::DimensionAngularSpec {
vertex,
first_point,
second_point,
arc_location,
},
))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_dimension returned no id".into()))?;
Ok(Dimension::new(self.session.clone(), id))
}
/// A radial DIMENSION for the circle centered at `center` through `point`.
pub fn create_dimension_radius(
&self,
center: [f64; 3],
point: [f64; 3],
) -> ApiResult<Dimension> {
self.create_dim_radial(Operation::CreateDimensionRadius, center, point)
}
/// A diameter DIMENSION for the circle with chord points `center`/`point`.
pub fn create_dimension_diameter(
&self,
center: [f64; 3],
point: [f64; 3],
) -> ApiResult<Dimension> {
self.create_dim_radial(Operation::CreateDimensionDiameter, center, point)
}
fn create_dim_radial(
&self,
op: impl Fn(crate::ops::DimensionRadialSpec) -> Operation,
center: [f64; 3],
point: [f64; 3],
) -> ApiResult<Dimension> {
let receipt = self
.session
.apply_op(op(crate::ops::DimensionRadialSpec { center, point }))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_dimension returned no id".into()))?;
Ok(Dimension::new(self.session.clone(), id))
}
/// A 3-point angular DIMENSION (vertex + one point on each leg).
pub fn create_dimension_angular(
&self,
vertex: [f64; 3],
first_point: [f64; 3],
second_point: [f64; 3],
arc_location: [f64; 3],
) -> ApiResult<Dimension> {
let receipt = self.session.apply_op(Operation::CreateDimensionAngular(
crate::ops::DimensionAngularSpec {
vertex,
first_point,
second_point,
arc_location,
},
))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_dimension returned no id".into()))?;
Ok(Dimension::new(self.session.clone(), id))
}
/// A linear DIMENSION between `first_point` and `second_point`, with the
/// dimension line placed at `definition_point`. `measurement()` reads the value.
pub fn create_dimension_linear(
&self,
first_point: [f64; 3],
second_point: [f64; 3],
definition_point: [f64; 3],
) -> ApiResult<Dimension> {
let receipt = self.session.apply_op(Operation::CreateDimensionLinear(
crate::ops::DimensionSpec {
first_point,
second_point,
definition_point,
},
))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_dimension returned no id".into()))?;
Ok(Dimension::new(self.session.clone(), id))
}
/// Single-line TEXT annotation.
pub fn create_text(
&self,
value: &str,
insertion_point: [f64; 3],
height: f64,
rotation: f64,
) -> ApiResult<Text> {
let receipt = self
.session
.apply_op(Operation::CreateText(crate::ops::TextSpec {
value: value.to_string(),
insertion_point,
height,
rotation,
}))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_text returned no id".into()))?;
Ok(Text::new(self.session.clone(), id))
}
/// Multi-line MTEXT annotation.
pub fn create_mtext(
&self,
value: &str,
insertion_point: [f64; 3],
height: f64,
) -> ApiResult<MText> {
let receipt = self
.session
.apply_op(Operation::CreateMText(crate::ops::MTextSpec {
value: value.to_string(),
insertion_point,
height,
}))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_mtext returned no id".into()))?;
Ok(MText::new(self.session.clone(), id))
}
/// Bulk-create many points in ONE op: all-or-nothing, one undo step.
/// Places points on the current layer (`layer: None`).
pub fn create_points(&self, positions: &[[f64; 3]]) -> ApiResult<Vec<Point>> {
let specs: Vec<crate::ops::EntitySpec> = positions
.iter()
.map(|&p| {
crate::ops::EntitySpec::Curve(crate::ops::Curve2Spec::Point {
position: p,
layer: None,
})
})
.collect();
let receipt = self.session.apply_op(Operation::CreateMany(specs))?;
let outcome = receipt
.outcome
.ok_or_else(|| ApiError::Transport("create_many returned no outcome".into()))?;
Ok(outcome
.new_ids()
.iter()
.map(|&id| Point::new(self.session.clone(), id))
.collect())
}
/// Bulk-create many points on a specific layer in ONE op.
pub fn create_points_on_layer(
&self,
positions: &[[f64; 3]],
layer: &str,
) -> ApiResult<Vec<Point>> {
let specs: Vec<crate::ops::EntitySpec> = positions
.iter()
.map(|&p| {
crate::ops::EntitySpec::Curve(crate::ops::Curve2Spec::Point {
position: p,
layer: Some(layer.to_string()),
})
})
.collect();
let receipt = self.session.apply_op(Operation::CreateMany(specs))?;
let outcome = receipt
.outcome
.ok_or_else(|| ApiError::Transport("create_many returned no outcome".into()))?;
Ok(outcome
.new_ids()
.iter()
.map(|&id| Point::new(self.session.clone(), id))
.collect())
}
}
#[derive(Clone)]
pub struct EntityCollection {
session: Session,
}
impl EntityCollection {
/// Create a paper-space `VIEWPORT` (a `width`×`height` viewport at `center`
/// looking at `view_target` with `view_height` zoom).
pub fn create_viewport(
&self,
center: [f64; 3],
width: f64,
height: f64,
view_target: [f64; 3],
view_height: f64,
) -> ApiResult<Entity> {
let receipt =
self.session
.apply_op(Operation::CreateViewport(crate::ops::ViewportSpec {
center,
width,
height,
view_target,
view_height,
}))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_viewport returned no id".into()))?;
Ok(Entity::new(self.session.clone(), id))
}
/// Create a TABLE from a grid of cell strings (`data[row][column]`), at a point.
pub fn create_table(
&self,
insertion_point: [f64; 3],
data: &[Vec<String>],
) -> ApiResult<Entity> {
let receipt = self
.session
.apply_op(Operation::CreateTable(crate::ops::TableSpec {
insertion_point,
data: data.to_vec(),
}))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_table returned no id".into()))?;
Ok(Entity::new(self.session.clone(), id))
}
/// Create an ATTDEF (in-block attribute definition): tag/prompt/default at a point.
pub fn create_attribute_definition(
&self,
tag: &str,
prompt: &str,
default_value: &str,
insertion_point: [f64; 3],
height: f64,
rotation: f64,
) -> ApiResult<Entity> {
let receipt = self.session.apply_op(Operation::CreateAttributeDefinition(
crate::ops::AttributeDefinitionSpec {
tag: tag.to_string(),
prompt: prompt.to_string(),
default_value: default_value.to_string(),
insertion_point,
height,
rotation,
},
))?;
let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| {
ApiError::Transport("create_attribute_definition returned no id".into())
})?;
Ok(Entity::new(self.session.clone(), id))
}
/// Place a block reference (`INSERT`) for `block_name` (must exist).
pub fn create_insert(
&self,
block_name: &str,
insert_point: [f64; 3],
scale: f64,
rotation: f64,
) -> ApiResult<Entity> {
let receipt = self
.session
.apply_op(Operation::CreateInsert(crate::ops::InsertSpec {
block_name: block_name.to_string(),
insert_point,
scale,
rotation,
}))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_insert returned no id".into()))?;
Ok(Entity::new(self.session.clone(), id))
}
/// Generic lookup by id (any family) → `Entity` (downcast via `as_solid()`, …).
pub fn get(&self, id: ObjectId) -> ApiResult<Entity> {
// Validate existence eagerly so `get` errors surface as `UnknownId`.
self.session.one_query(Query::GetEntity { id })?;
Ok(Entity::new(self.session.clone(), id))
}
pub fn delete(&self, id: ObjectId) -> ApiResult<()> {
self.session.apply_op(Operation::Delete { id })?;
Ok(())
}
/// Create a standalone `XRECORD` object.
pub fn create_xrecord(&self, spec: &XRecordSpec) -> ApiResult<XRecord> {
let receipt = self
.session
.apply_op(Operation::CreateXRecord(spec.clone()))?;
let id = receipt
.outcome
.and_then(|o| o.new_id())
.ok_or_else(|| ApiError::Transport("create_xrecord returned no id".into()))?;
Ok(XRecord::new(self.session.clone(), id))
}
/// Lookup an `XRECORD` object by id and return a typed accessor handle.
pub fn get_xrecord(&self, id: ObjectId) -> ApiResult<XRecord> {
// Validate existence + that the payload is readable as an XRecord.
match self.session.one_query(Query::GetXRecord { id })? {
QueryResult::XRecord(_) => Ok(XRecord::new(self.session.clone(), id)),
_ => Err(ApiError::Transport("unexpected xrecord result".into())),
}
}
/// Bulk-create many entities in ONE op (all-or-nothing, one undo step).
pub fn create_many(&self, specs: &[crate::ops::EntitySpec]) -> ApiResult<Vec<Entity>> {
let receipt = self.session.apply_op(Operation::CreateMany(specs.to_vec()))?;
let outcome = receipt
.outcome
.ok_or_else(|| ApiError::Transport("create_many returned no outcome".into()))?;
Ok(outcome
.new_ids()
.iter()
.map(|&id| Entity::new(self.session.clone(), id))
.collect())
}
/// Bulk transform (one op, all-or-nothing).
pub fn transform_many(&self, ids: &[ObjectId], placement: PlacementSpec) -> ApiResult<()> {
self.session.apply_op(Operation::TransformMany {
ids: ids.to_vec(),
placement,
})?;
Ok(())
}
/// Bulk delete (one op, all-or-nothing). Used by `OpGroup::compensate`.
pub fn delete_many(&self, ids: &[ObjectId]) -> ApiResult<()> {
self.session.apply_op(Operation::DeleteMany(ids.to_vec()))?;
Ok(())
}
}
// ── OpGroup: client-side failure-cleanup (NOT a transaction) ────────────────
/// Tracks handles created during a logical operation so a later failure can be
/// cleaned up with a bulk delete. Separate writes remain committed;
/// no host involvement, no rollback.
#[derive(Default)]
pub struct OpGroup {
created: Vec<ObjectId>,
session: Option<Session>,
}
impl OpGroup {
pub fn new() -> Self {
Self {
created: Vec::new(),
session: None,
}
}
/// Record a created handle (propagating any construction error); returns it
/// for fluent `grp.track(doc.solids().create_cuboid(..)?)?` use.
pub fn track<T: HasId>(&mut self, handle: ApiResult<T>) -> ApiResult<T> {
let handle = handle?;
if let Some(session) = handle.session() {
if let Some(previous) = &self.session {
previous.same_transport(session)?;
} else {
self.session = Some(session.clone());
}
}
if !self.created.contains(&handle.id()) {
self.created.push(handle.id());
}
Ok(handle)
}
/// Keep everything: drop the tracking without deleting.
pub fn commit(self) {}
/// Best-effort cleanup: delete every tracked entity (one `DeleteMany` op).
/// Does not resurrect already-consumed/deleted entities.
pub fn compensate(self, doc: &Document) -> ApiResult<()> {
if let Some(session) = &self.session {
session.same_transport(&doc.session)?;
}
if self.created.is_empty() {
return Ok(());
}
doc.entities().delete_many(&self.created)
}
}