ocs_doc_api: layer CRUD, per-curve layer assignment, XDATA/XRECORD support, and bulk SetXDataMany

This commit is contained in:
Sebastian committed 2026-09-10 10:43:15 +02:00
1 parent b19992cbcb
commit 8bf5eac3fc
27 files changed
+3340 -181

No files matched your search

+348 -7
View File
@@ -13,7 +13,7 @@ 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};
use crate::ops::{BoolOp, Curve2Spec, PlacementSpec, SolidPrimitive, XDataRecord, XRecordSpec};
use crate::query::{Aabb, EntityView, QueryResult};
use crate::revision::GeometryRevision;
use crate::transport::Transport;
@@ -170,6 +170,74 @@ impl Document {
}
}
/// 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.
@@ -336,6 +404,25 @@ macro_rules! handle {
})?;
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(())
}
}
};
}
@@ -354,6 +441,25 @@ handle!(XLine);
handle!(Text);
handle!(MText);
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
@@ -416,6 +522,26 @@ impl Entity {
})?;
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")
@@ -621,22 +747,80 @@ impl CurveCollection {
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 })?;
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 })?;
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 })?;
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(
@@ -651,6 +835,24 @@ impl CurveCollection {
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))
}
@@ -668,6 +870,26 @@ impl CurveCollection {
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))
}
@@ -683,15 +905,67 @@ impl CurveCollection {
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 })?;
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 })?;
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).
@@ -874,10 +1148,42 @@ impl CurveCollection {
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(Curve2Spec::Point { position: p }))
.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
@@ -1001,6 +1307,41 @@ impl EntityCollection {
self.session.apply_op(Operation::Delete { id })?;
Ok(())
}
/// Create a standalone `XRECORD` object (returns an `Entity` handle).
pub fn create_xrecord(&self, spec: &XRecordSpec) -> ApiResult<Entity> {
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(Entity::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 {