forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: layer CRUD, per-curve layer assignment, XDATA/XRECORD support, and bulk SetXDataMany
This commit is contained in:
1 parent
b19992cbcb
commit
8bf5eac3fc
27 files changed
+3340
-181
No files matched your search
@@ -8,15 +8,16 @@ use std::sync::Arc;
|
||||
|
||||
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
||||
use ocs_doc_api::{
|
||||
Aabb, ApiError, ApiResult, Curve2Spec, DocApi, EntityView, GeometryErrorKind, GeometryRevision,
|
||||
HasId, ObjectId, PlacementSpec,
|
||||
normalize_layer_name, Aabb, ApiError, ApiResult, Color, Curve2Spec, DocApi, EntityView,
|
||||
GeometryErrorKind, GeometryRevision, HasId, LayerInfo, LineWeight, ObjectId, PlacementSpec,
|
||||
XDataRecord, XDataValue,
|
||||
};
|
||||
|
||||
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds and
|
||||
/// the example mock — all three must answer curve bounds identically).
|
||||
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||
match spec {
|
||||
Curve2Spec::Line { start, end } => Aabb {
|
||||
Curve2Spec::Line { start, end, .. } => Aabb {
|
||||
min: [
|
||||
start[0].min(end[0]),
|
||||
start[1].min(end[1]),
|
||||
@@ -28,11 +29,12 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||
start[2].max(end[2]),
|
||||
],
|
||||
},
|
||||
Curve2Spec::Circle { centre, radius } | Curve2Spec::Arc { centre, radius, .. } => Aabb {
|
||||
Curve2Spec::Circle { centre, radius, .. }
|
||||
| Curve2Spec::Arc { centre, radius, .. } => Aabb {
|
||||
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
||||
max: [centre[0] + radius, centre[1] + radius, centre[2]],
|
||||
},
|
||||
Curve2Spec::Point { position } => Aabb {
|
||||
Curve2Spec::Point { position, .. } => Aabb {
|
||||
min: *position,
|
||||
max: *position,
|
||||
},
|
||||
@@ -72,13 +74,17 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||
|
||||
/// In-memory mock backend: HashMap<ObjectId, Body> + a tiny entity table.
|
||||
/// Tracks undo steps + revision bumps to assert per-op semantics.
|
||||
#[derive(Default, Clone)]
|
||||
#[derive(Clone)]
|
||||
struct MockBackend {
|
||||
next_id: u64,
|
||||
revision: u64,
|
||||
undo_steps: u32,
|
||||
bodies: HashMap<ObjectId, KernelBody>,
|
||||
kinds: HashMap<ObjectId, String>,
|
||||
/// Layer name assigned to each entity.
|
||||
entity_layers: HashMap<ObjectId, String>,
|
||||
/// Stored layer table.
|
||||
layers: HashMap<String, ocs_doc_api::LayerInfo>,
|
||||
/// Stored text content for Text/MText annotations.
|
||||
text_values: HashMap<ObjectId, String>,
|
||||
/// Stored hatch boundary loops.
|
||||
@@ -92,6 +98,36 @@ struct MockBackend {
|
||||
curves: HashMap<ObjectId, Curve2Spec>,
|
||||
/// Stored viewport views (target, height).
|
||||
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
||||
/// Stored XDATA records keyed by (entity id, application name).
|
||||
xdata_store: HashMap<(ObjectId, String), ocs_doc_api::XDataRecord>,
|
||||
/// Stored XRECORD payloads keyed by entity id.
|
||||
xrecord_store: HashMap<ObjectId, ocs_doc_api::XRecordSpec>,
|
||||
}
|
||||
|
||||
impl Default for MockBackend {
|
||||
fn default() -> Self {
|
||||
let mut backend = Self {
|
||||
next_id: 0,
|
||||
revision: 0,
|
||||
undo_steps: 0,
|
||||
bodies: HashMap::new(),
|
||||
kinds: HashMap::new(),
|
||||
entity_layers: HashMap::new(),
|
||||
layers: HashMap::new(),
|
||||
text_values: HashMap::new(),
|
||||
hatch_boundaries: HashMap::new(),
|
||||
dimension_measurements: HashMap::new(),
|
||||
attributes_store: HashMap::new(),
|
||||
curves: HashMap::new(),
|
||||
viewport_views: HashMap::new(),
|
||||
xdata_store: HashMap::new(),
|
||||
xrecord_store: HashMap::new(),
|
||||
};
|
||||
backend
|
||||
.layers
|
||||
.insert("0".to_string(), ocs_doc_api::LayerInfo::new("0"));
|
||||
backend
|
||||
}
|
||||
}
|
||||
|
||||
impl MockBackend {
|
||||
@@ -104,6 +140,22 @@ impl MockBackend {
|
||||
fn body(&self, id: ObjectId) -> ApiResult<&KernelBody> {
|
||||
self.bodies.get(&id).ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn add_curve_only(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
||||
let kind = match spec {
|
||||
Curve2Spec::Line { .. } => "Line",
|
||||
Curve2Spec::Circle { .. } => "Circle",
|
||||
Curve2Spec::Polyline { .. } => "LwPolyline",
|
||||
Curve2Spec::Point { .. } => "Point",
|
||||
Curve2Spec::Arc { .. } => "Arc",
|
||||
Curve2Spec::Ellipse { .. } => "Ellipse",
|
||||
Curve2Spec::Spline { .. } => "Spline",
|
||||
Curve2Spec::Ray { .. } => "Ray",
|
||||
Curve2Spec::XLine { .. } => "XLine",
|
||||
};
|
||||
let id = self.alloc(kind);
|
||||
self.curves.insert(id, spec.clone());
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl DocApiBackend for MockBackend {
|
||||
@@ -113,6 +165,7 @@ impl DocApiBackend for MockBackend {
|
||||
fn store_solid(&mut self, body: &KernelBody) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Solid3D");
|
||||
self.bodies.insert(id, body.clone());
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn update_solid(&mut self, id: ObjectId, body: &KernelBody) -> ApiResult<()> {
|
||||
@@ -122,6 +175,15 @@ impl DocApiBackend for MockBackend {
|
||||
Ok(())
|
||||
}
|
||||
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
||||
// Validate per-spec layers up front so the op is atomic.
|
||||
for spec in specs {
|
||||
if let Some(name) = spec.layer() {
|
||||
self.lookup_layer(name).map_err(|e| ApiError::validation(
|
||||
"CreateMany",
|
||||
e.to_string(),
|
||||
))?;
|
||||
}
|
||||
}
|
||||
let mut next = self.clone();
|
||||
let ids = specs
|
||||
.iter()
|
||||
@@ -151,26 +213,35 @@ impl DocApiBackend for MockBackend {
|
||||
Ok(())
|
||||
}
|
||||
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
||||
let kind = match spec {
|
||||
Curve2Spec::Line { .. } => "Line",
|
||||
Curve2Spec::Circle { .. } => "Circle",
|
||||
Curve2Spec::Polyline { .. } => "LwPolyline",
|
||||
Curve2Spec::Point { .. } => "Point",
|
||||
Curve2Spec::Arc { .. } => "Arc",
|
||||
Curve2Spec::Ellipse { .. } => "Ellipse",
|
||||
Curve2Spec::Spline { .. } => "Spline",
|
||||
Curve2Spec::Ray { .. } => "Ray",
|
||||
Curve2Spec::XLine { .. } => "XLine",
|
||||
};
|
||||
let id = self.alloc(kind);
|
||||
self.curves.insert(id, spec.clone());
|
||||
let layer = spec
|
||||
.layer()
|
||||
.map(|name| {
|
||||
self.lookup_layer(name).map(|info| info.normalized_name())
|
||||
})
|
||||
.transpose()?;
|
||||
let id = self.add_curve_only(spec)?;
|
||||
self.entity_layers
|
||||
.insert(id, layer.unwrap_or_else(|| "0".to_string()));
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
fn lookup_layer(&self, name: &str) -> ApiResult<ocs_doc_api::LayerInfo> {
|
||||
let norm = normalize_layer_name(name);
|
||||
if norm.is_empty() {
|
||||
return Err(ApiError::validation("LookupLayer", "empty layer name"));
|
||||
}
|
||||
self.layers
|
||||
.get(&norm)
|
||||
.cloned()
|
||||
.ok_or_else(|| ApiError::validation("LookupLayer", format!("layer '{name}' does not exist")))
|
||||
}
|
||||
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||
if spec.block_name.is_empty() {
|
||||
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
||||
}
|
||||
Ok(self.alloc("Insert"))
|
||||
let id = self.alloc("Insert");
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Viewport");
|
||||
@@ -181,11 +252,13 @@ impl DocApiBackend for MockBackend {
|
||||
fn add_text(&mut self, spec: &ocs_doc_api::ops::TextSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Text");
|
||||
self.text_values.insert(id, spec.value.clone());
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn add_mtext(&mut self, spec: &ocs_doc_api::ops::MTextSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("MText");
|
||||
self.text_values.insert(id, spec.value.clone());
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn set_text_content(&mut self, id: ObjectId, value: &str) -> ApiResult<()> {
|
||||
@@ -206,6 +279,7 @@ impl DocApiBackend for MockBackend {
|
||||
let id = self.alloc("Hatch");
|
||||
self.hatch_boundaries
|
||||
.insert(id, vec![spec.boundary.clone()]);
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
||||
@@ -224,6 +298,7 @@ impl DocApiBackend for MockBackend {
|
||||
+ (spec.first_point[2] - spec.second_point[2]).powi(2))
|
||||
.sqrt();
|
||||
self.dimension_measurements.insert(id, d);
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn add_dimension_radius(
|
||||
@@ -236,13 +311,16 @@ impl DocApiBackend for MockBackend {
|
||||
+ (spec.center[2] - spec.point[2]).powi(2))
|
||||
.sqrt();
|
||||
self.dimension_measurements.insert(id, d);
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn add_dimension_diameter(
|
||||
&mut self,
|
||||
spec: &ocs_doc_api::ops::DimensionRadialSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
self.add_dimension_radius(spec)
|
||||
let id = self.add_dimension_radius(spec)?;
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn add_dimension_angular(
|
||||
&mut self,
|
||||
@@ -250,6 +328,7 @@ impl DocApiBackend for MockBackend {
|
||||
) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Dimension");
|
||||
self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||
@@ -286,7 +365,9 @@ impl DocApiBackend for MockBackend {
|
||||
"table needs a non-empty rectangular grid",
|
||||
));
|
||||
}
|
||||
Ok(self.alloc("Table"))
|
||||
let id = self.alloc("Table");
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
@@ -313,7 +394,9 @@ impl DocApiBackend for MockBackend {
|
||||
&mut self,
|
||||
_spec: &ocs_doc_api::ops::RasterImageSpec,
|
||||
) -> ApiResult<ObjectId> {
|
||||
Ok(self.alloc("RasterImage"))
|
||||
let id = self.alloc("RasterImage");
|
||||
self.entity_layers.insert(id, "0".to_string());
|
||||
Ok(id)
|
||||
}
|
||||
fn loft(
|
||||
&mut self,
|
||||
@@ -345,6 +428,203 @@ impl DocApiBackend for MockBackend {
|
||||
.copied()
|
||||
.ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn set_xdata(
|
||||
&mut self,
|
||||
id: ObjectId,
|
||||
application_name: &str,
|
||||
record: Option<&ocs_doc_api::XDataRecord>,
|
||||
) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
let key = (id, application_name.to_string());
|
||||
match record {
|
||||
Some(r) => {
|
||||
self.xdata_store.insert(key, r.clone());
|
||||
}
|
||||
None => {
|
||||
self.xdata_store.remove(&key);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn xdata(
|
||||
&self,
|
||||
id: ObjectId,
|
||||
application_name: &str,
|
||||
) -> ApiResult<Option<ocs_doc_api::XDataRecord>> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
Ok(self
|
||||
.xdata_store
|
||||
.get(&(id, application_name.to_string()))
|
||||
.cloned())
|
||||
}
|
||||
fn add_xrecord(&mut self, spec: &ocs_doc_api::XRecordSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("XRecord");
|
||||
self.xrecord_store.insert(id, spec.clone());
|
||||
Ok(id)
|
||||
}
|
||||
fn set_xrecord(&mut self, id: ObjectId, spec: &ocs_doc_api::XRecordSpec) -> ApiResult<()> {
|
||||
if !self.object_exists(id) {
|
||||
return Err(ApiError::validation(
|
||||
"SetXRecord",
|
||||
format!("unknown ObjectId {id:?}"),
|
||||
));
|
||||
}
|
||||
self.xrecord_store.insert(id, spec.clone());
|
||||
Ok(())
|
||||
}
|
||||
fn xrecord(&self, id: ObjectId) -> ApiResult<Option<ocs_doc_api::XRecordSpec>> {
|
||||
if !self.object_exists(id) {
|
||||
return Err(ApiError::validation(
|
||||
"GetXRecord",
|
||||
format!("unknown ObjectId {id:?}"),
|
||||
));
|
||||
}
|
||||
Ok(self.xrecord_store.get(&id).cloned())
|
||||
}
|
||||
fn create_layer(&mut self, info: &ocs_doc_api::LayerInfo) -> ApiResult<()> {
|
||||
let name = normalize_layer_name(&info.name);
|
||||
if name.is_empty() {
|
||||
return Err(ApiError::validation("CreateLayer", "empty layer name"));
|
||||
}
|
||||
if self.layers.contains_key(&name) {
|
||||
return Err(ApiError::validation(
|
||||
"CreateLayer",
|
||||
format!("layer '{name}' already exists"),
|
||||
));
|
||||
}
|
||||
let mut stored = info.clone();
|
||||
stored.name = name.clone();
|
||||
self.layers.insert(name, stored);
|
||||
Ok(())
|
||||
}
|
||||
fn update_layer(&mut self, name: &str, info: &ocs_doc_api::LayerInfo) -> ApiResult<()> {
|
||||
let name_upper = normalize_layer_name(name);
|
||||
if !self.layers.contains_key(&name_upper) {
|
||||
return Err(ApiError::validation(
|
||||
"UpdateLayer",
|
||||
format!("layer '{name}' does not exist"),
|
||||
));
|
||||
}
|
||||
let mut stored = info.clone();
|
||||
stored.name = name_upper.clone();
|
||||
self.layers.insert(name_upper, stored);
|
||||
Ok(())
|
||||
}
|
||||
fn delete_layer(&mut self, name: &str) -> ApiResult<()> {
|
||||
let name_upper = normalize_layer_name(name);
|
||||
if name_upper == "0" {
|
||||
return Err(ApiError::validation("DeleteLayer", "cannot delete layer 0"));
|
||||
}
|
||||
if self
|
||||
.entity_layers
|
||||
.values()
|
||||
.any(|l| normalize_layer_name(l) == name_upper)
|
||||
{
|
||||
return Err(ApiError::validation(
|
||||
"DeleteLayer",
|
||||
format!("layer '{name_upper}' still in use"),
|
||||
));
|
||||
}
|
||||
if self.layers.remove(&name_upper).is_none() {
|
||||
return Err(ApiError::validation(
|
||||
"DeleteLayer",
|
||||
format!("layer '{name}' does not exist"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn set_entity_layer(&mut self, id: ObjectId, layer: &str) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
let layer_upper = normalize_layer_name(layer);
|
||||
let target = self
|
||||
.layers
|
||||
.get(&layer_upper)
|
||||
.ok_or_else(|| {
|
||||
ApiError::validation(
|
||||
"SetEntityLayer",
|
||||
format!("layer '{layer_upper}' does not exist"),
|
||||
)
|
||||
})?;
|
||||
if target.flags.locked {
|
||||
return Err(ApiError::Unsupported(format!(
|
||||
"entity {id:?} cannot be moved to locked layer '{layer_upper}'"
|
||||
)));
|
||||
}
|
||||
self.entity_layers.insert(id, layer_upper);
|
||||
Ok(())
|
||||
}
|
||||
fn layers(&self) -> ApiResult<Vec<ocs_doc_api::LayerInfo>> {
|
||||
let mut v: Vec<_> = self.layers.values().cloned().collect();
|
||||
v.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
Ok(v)
|
||||
}
|
||||
fn entity_layer(&self, id: ObjectId) -> ApiResult<String> {
|
||||
self.entity_layers
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
fn enumerate_entities(
|
||||
&self,
|
||||
kind: Option<&str>,
|
||||
layer: Option<&str>,
|
||||
include_bounds: bool,
|
||||
) -> ApiResult<Vec<EntityView>> {
|
||||
let mut out = Vec::new();
|
||||
for (id, kind_name) in &self.kinds {
|
||||
if let Some(k) = kind {
|
||||
if !k.eq_ignore_ascii_case(kind_name) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let entity_layer = self
|
||||
.entity_layers
|
||||
.get(id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "0".to_string());
|
||||
if let Some(l) = layer {
|
||||
if !l.eq_ignore_ascii_case(&entity_layer) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let bounds = if include_bounds {
|
||||
self.curves.get(id).map(curve_bounds)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
out.push(EntityView {
|
||||
id: *id,
|
||||
kind: kind_name.clone(),
|
||||
layer: entity_layer,
|
||||
bounds,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
fn point_position(&self, id: ObjectId) -> ApiResult<[f64; 3]> {
|
||||
let spec = self.curves.get(&id).ok_or(ApiError::UnknownId(id))?;
|
||||
match spec {
|
||||
Curve2Spec::Point { position, .. } => Ok(*position),
|
||||
_ => Err(ApiError::Unsupported(
|
||||
"GetPointPosition is only for Point entities".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
fn line_geometry(&self, id: ObjectId) -> ApiResult<([f64; 3], [f64; 3])> {
|
||||
let spec = self.curves.get(&id).ok_or(ApiError::UnknownId(id))?;
|
||||
match spec {
|
||||
Curve2Spec::Line { start, end, .. } => Ok((*start, *end)),
|
||||
_ => Err(ApiError::Unsupported(
|
||||
"GetLineGeometry is only for Line entities".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
Ok(())
|
||||
@@ -354,6 +634,9 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
||||
self.bodies.remove(&id);
|
||||
self.xdata_store.retain(|(k, _), _| *k != id);
|
||||
self.xrecord_store.remove(&id);
|
||||
self.entity_layers.remove(&id);
|
||||
Ok(self.kinds.remove(&id).is_some())
|
||||
}
|
||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
||||
@@ -364,7 +647,17 @@ impl DocApiBackend for MockBackend {
|
||||
max: bb.max,
|
||||
})
|
||||
});
|
||||
Ok(EntityView { id, kind, bounds })
|
||||
let layer = self
|
||||
.entity_layers
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "0".to_string());
|
||||
Ok(EntityView {
|
||||
id,
|
||||
kind,
|
||||
layer,
|
||||
bounds,
|
||||
})
|
||||
}
|
||||
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
@@ -412,6 +705,9 @@ impl DocApiBackend for MockBackend {
|
||||
fn entity_exists(&self, id: ObjectId) -> bool {
|
||||
self.kinds.contains_key(&id)
|
||||
}
|
||||
fn object_exists(&self, id: ObjectId) -> bool {
|
||||
self.xrecord_store.contains_key(&id)
|
||||
}
|
||||
fn revision(&self) -> GeometryRevision {
|
||||
GeometryRevision(self.revision)
|
||||
}
|
||||
@@ -609,3 +905,145 @@ fn typed_handles_reject_foreign_sessions_and_tabs() {
|
||||
assert!(group.compensate(&doc).is_err());
|
||||
assert!(first.document(11).curves().create_point([0.0; 3]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_api_layer_crud_via_facade_and_entity_assignment() {
|
||||
let (api, _tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
|
||||
let mut info = LayerInfo::new("Walls");
|
||||
info.color = Color::Index(1);
|
||||
info.line_weight = LineWeight::Value(25);
|
||||
doc.create_layer(&info).unwrap();
|
||||
|
||||
let layers = doc.layers().unwrap();
|
||||
assert!(layers.iter().any(|l| l.name == "WALLS"));
|
||||
|
||||
let mut updated = info.clone();
|
||||
updated.color = Color::Index(2);
|
||||
doc.update_layer("walls", &updated).unwrap();
|
||||
|
||||
let line = doc.curves().create_line([0.0; 3], [1.0; 3]).unwrap();
|
||||
line.set_layer("Walls").unwrap();
|
||||
assert_eq!(line.layer().unwrap(), "WALLS");
|
||||
|
||||
let err = doc.delete_layer("Walls").unwrap_err();
|
||||
assert!(matches!(err, ApiError::Validation { .. }));
|
||||
|
||||
let other = LayerInfo::new("Other");
|
||||
doc.create_layer(&other).unwrap();
|
||||
line.set_layer("Other").unwrap();
|
||||
doc.delete_layer("Walls").unwrap();
|
||||
assert!(!doc.layers().unwrap().iter().any(|l| l.name == "WALLS"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_xdata_many_is_atomic_and_assigns_records() {
|
||||
let (api, _tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let line = doc.curves().create_line([0.0; 3], [1.0; 3]).unwrap();
|
||||
let circle = doc.curves().create_circle([0.0; 3], 1.0).unwrap();
|
||||
|
||||
let record1 = XDataRecord {
|
||||
application_name: "APP1".to_string(),
|
||||
values: vec![XDataValue::Integer32(42)],
|
||||
};
|
||||
let record2 = XDataRecord {
|
||||
application_name: "APP2".to_string(),
|
||||
values: vec![XDataValue::Real(3.14)],
|
||||
};
|
||||
doc.set_xdata_many([
|
||||
(line.id(), "APP1", Some(record1.clone())),
|
||||
(circle.id(), "APP2", Some(record2.clone())),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let r1 = doc.entities().get(line.id()).unwrap().xdata("APP1").unwrap().unwrap();
|
||||
assert_eq!(r1.values, vec![XDataValue::Integer32(42)]);
|
||||
let r2 = doc.entities().get(circle.id()).unwrap().xdata("APP2").unwrap().unwrap();
|
||||
assert_eq!(r2.values, vec![XDataValue::Real(3.14)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_xdata_many_with_unknown_id_fails_atomically() {
|
||||
let (api, _tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let line = doc.curves().create_line([0.0; 3], [1.0; 3]).unwrap();
|
||||
let stale = ObjectId::from_u64(999_999);
|
||||
|
||||
let record = XDataRecord {
|
||||
application_name: "APP1".to_string(),
|
||||
values: vec![XDataValue::Integer32(42)],
|
||||
};
|
||||
let err = doc
|
||||
.set_xdata_many([
|
||||
(line.id(), "APP1", Some(record.clone())),
|
||||
(stale, "APP2", Some(record)),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ApiError::UnknownId(..) | ApiError::Validation { .. }));
|
||||
assert!(doc.entities().get(line.id()).unwrap().xdata("APP1").unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_many_applies_per_curve_layer() {
|
||||
let (api, _tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
doc.create_layer(&LayerInfo::new("Walls")).unwrap();
|
||||
|
||||
let ids = doc
|
||||
.entities()
|
||||
.create_many(&[
|
||||
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Line {
|
||||
start: [0.0; 3],
|
||||
end: [1.0; 3],
|
||||
layer: Some("Walls".to_string()),
|
||||
}),
|
||||
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Point {
|
||||
position: [2.0; 3],
|
||||
layer: None,
|
||||
}),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(ids[0].layer().unwrap(), "WALLS");
|
||||
assert_eq!(ids[1].layer().unwrap(), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_many_rejects_missing_layer_before_mutating() {
|
||||
let (api, tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let rev0 = doc.revision().unwrap().as_u64();
|
||||
|
||||
let err = doc
|
||||
.entities()
|
||||
.create_many(&[
|
||||
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Line {
|
||||
start: [0.0; 3],
|
||||
end: [1.0; 3],
|
||||
layer: Some("Missing".to_string()),
|
||||
}),
|
||||
ocs_doc_api::EntitySpec::Curve(Curve2Spec::Point {
|
||||
position: [2.0; 3],
|
||||
layer: None,
|
||||
}),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ApiError::Validation { .. }));
|
||||
|
||||
let backend = tp.backend();
|
||||
assert_eq!(backend.revision, rev0);
|
||||
assert_eq!(backend.next_id, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_line_on_layer_requires_existing_layer() {
|
||||
let (api, _tp) = api();
|
||||
let doc = api.document(api.active_tab());
|
||||
let err = doc
|
||||
.curves()
|
||||
.create_line_on_layer([0.0; 3], [1.0; 3], "Missing")
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, ApiError::Validation { .. }));
|
||||
}
|
||||
@@ -38,7 +38,8 @@ fn spec() -> Spec {
|
||||
/// The variant names of the `Operation` enum, in declaration order.
|
||||
fn operation_variants() -> Vec<String> {
|
||||
use ocs_doc_api::ops::{
|
||||
BoolOp, Curve2Spec, InsertSpec, Operation, PlacementSpec, SolidPrimitive, ViewportSpec,
|
||||
BoolOp, Curve2Spec, InsertSpec, LayerInfo, Operation, PlacementSpec, SolidPrimitive,
|
||||
ViewportSpec,
|
||||
};
|
||||
use ocs_doc_api::ObjectId;
|
||||
let _marker: Vec<Operation> = vec![
|
||||
@@ -46,7 +47,10 @@ fn operation_variants() -> Vec<String> {
|
||||
centre: [0.0; 3],
|
||||
radius: 1.0,
|
||||
}),
|
||||
Operation::CreateCurve(Curve2Spec::Point { position: [0.0; 3] }),
|
||||
Operation::CreateCurve(Curve2Spec::Point {
|
||||
position: [0.0; 3],
|
||||
layer: None,
|
||||
}),
|
||||
Operation::Extrude {
|
||||
profile: ObjectId::from_u64(0),
|
||||
direction: [0.0; 3],
|
||||
@@ -167,6 +171,37 @@ fn operation_variants() -> Vec<String> {
|
||||
second_point: [0.0; 3],
|
||||
arc_location: [0.0; 3],
|
||||
}),
|
||||
Operation::SetXData {
|
||||
id: ObjectId::from_u64(0),
|
||||
application_name: String::new(),
|
||||
record: None,
|
||||
},
|
||||
Operation::SetXDataMany(vec![]),
|
||||
Operation::CreateXRecord(ocs_doc_api::ops::XRecordSpec {
|
||||
name: String::new(),
|
||||
cloning_flags: ocs_doc_api::ops::XRecordCloningFlags::NotApplicable,
|
||||
entries: vec![],
|
||||
}),
|
||||
Operation::SetXRecord {
|
||||
id: ObjectId::from_u64(0),
|
||||
spec: ocs_doc_api::ops::XRecordSpec {
|
||||
name: String::new(),
|
||||
cloning_flags: ocs_doc_api::ops::XRecordCloningFlags::NotApplicable,
|
||||
entries: vec![],
|
||||
},
|
||||
},
|
||||
Operation::CreateLayer(LayerInfo::new("LAYER")),
|
||||
Operation::UpdateLayer {
|
||||
name: String::new(),
|
||||
info: LayerInfo::new("LAYER"),
|
||||
},
|
||||
Operation::DeleteLayer {
|
||||
name: String::new(),
|
||||
},
|
||||
Operation::SetEntityLayer {
|
||||
id: ObjectId::from_u64(0),
|
||||
layer: String::new(),
|
||||
},
|
||||
];
|
||||
_marker
|
||||
.iter()
|
||||
@@ -178,7 +213,7 @@ fn operation_variants() -> Vec<String> {
|
||||
index
|
||||
);
|
||||
let dbg = format!("{:?}", op);
|
||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
||||
dbg.split(['(', ' ', '{'])
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
@@ -226,6 +261,17 @@ fn query_variants() -> Vec<String> {
|
||||
Query::GetViewportView {
|
||||
id: ObjectId::from_u64(0),
|
||||
},
|
||||
Query::GetXData {
|
||||
id: ObjectId::from_u64(0),
|
||||
application_name: String::new(),
|
||||
},
|
||||
Query::GetXRecord {
|
||||
id: ObjectId::from_u64(0),
|
||||
},
|
||||
Query::ListLayers,
|
||||
Query::GetEntityLayer {
|
||||
id: ObjectId::from_u64(0),
|
||||
},
|
||||
];
|
||||
marker
|
||||
.iter()
|
||||
@@ -237,7 +283,7 @@ fn query_variants() -> Vec<String> {
|
||||
index
|
||||
);
|
||||
let dbg = format!("{:?}", q);
|
||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
||||
dbg.split(['(', ' ', '{'])
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
@@ -309,6 +355,14 @@ const OPERATION_BASELINE: &[&str] = &[
|
||||
"CreateAttributeDefinition",
|
||||
"CreateTable",
|
||||
"CreateDimensionAngular2Ln",
|
||||
"SetXData",
|
||||
"SetXDataMany",
|
||||
"CreateXRecord",
|
||||
"SetXRecord",
|
||||
"CreateLayer",
|
||||
"UpdateLayer",
|
||||
"DeleteLayer",
|
||||
"SetEntityLayer",
|
||||
];
|
||||
|
||||
/// Recorded baseline of the `Query` variant order.
|
||||
@@ -325,6 +379,10 @@ const QUERY_BASELINE: &[&str] = &[
|
||||
"GetAttributes",
|
||||
"GetBlockEntities",
|
||||
"GetViewportView",
|
||||
"GetXData",
|
||||
"GetXRecord",
|
||||
"ListLayers",
|
||||
"GetEntityLayer",
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user