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
@@ -4,6 +4,12 @@
|
|||||||
`Transport::apply` carries one operation or a read-only query batch to the
|
`Transport::apply` carries one operation or a read-only query batch to the
|
||||||
executor. The native adapter binds each request to its existing `HostSession`.
|
executor. The native adapter binds each request to its existing `HostSession`.
|
||||||
|
|
||||||
|
For out-of-process / worker-thread plugins, `doc_api_for_host(host)` constructs a
|
||||||
|
`DocApi` backed by `OcsPluginApiIpc`, which sends `PluginRequest::DocApiRequest`
|
||||||
|
through the host's `PluginRequestSender` and deserializes
|
||||||
|
`PluginResponse::DocApiResponse`. This keeps plugin code free of manual envelope
|
||||||
|
serialization while reusing the same executor that the in-process transport uses.
|
||||||
|
|
||||||
The executor validates inputs and dispatches through `DocApiBackend`. Backends
|
The executor validates inputs and dispatches through `DocApiBackend`. Backends
|
||||||
must prepare fallible geometry and serialization before changing the document.
|
must prepare fallible geometry and serialization before changing the document.
|
||||||
Bulk writes prepare all entities, begin one undo record, apply the prepared
|
Bulk writes prepare all entities, begin one undo record, apply the prepared
|
||||||
@@ -22,14 +28,18 @@ do not reorder them. The consistency tests verify their bincode discriminants.
|
|||||||
generates the API reference and binding schema. The layout snapshot is curated
|
generates the API reference and binding schema. The layout snapshot is curated
|
||||||
vocabulary, not a reflected binary codec schema.
|
vocabulary, not a reflected binary codec schema.
|
||||||
|
|
||||||
|
Layer CRUD is a document-table family, not an entity family. The API carries
|
||||||
|
layer properties in the plain-data `LayerInfo` DTO and the host translates them
|
||||||
|
to/from `acadrust::tables::Layer`. Layer names are case-insensitive. The host
|
||||||
|
rejects duplicate names, deletion of layer "0" or the current layer, and removal
|
||||||
|
of a layer that still has entities assigned.
|
||||||
|
|
||||||
|
XDATA and XRECORD are serialization roundtrip families: the API carries their
|
||||||
|
payloads as plain-data DTOs and the host stores/retrieves them without semantic
|
||||||
|
interpretation. XDATA records are keyed by registered application name on any
|
||||||
|
entity; XRECORD objects are standalone named dictionaries of group-code/value
|
||||||
|
entries. Both preserve exact bytes/values across save and reload.
|
||||||
|
|
||||||
To extend the API, update the DTO, backend, executor, facade and spec together,
|
To extend the API, update the DTO, backend, executor, facade and spec together,
|
||||||
add a native behavior test, and rebuild the generated reference/schema. New
|
add a native behavior test, and rebuild the generated reference/schema. New
|
||||||
backend methods must reject unsupported behavior explicitly.
|
backend methods must reject unsupported behavior explicitly.
|
||||||
|
|
||||||
Extension workflow:
|
|
||||||
|
|
||||||
1. Add the operation/query DTO and its name mapping, then update `spec/entities.toml`.
|
|
||||||
2. Implement validation, backend preparation, executor dispatch and facade methods.
|
|
||||||
3. Preserve each profile's plane and use cadkernel for geometry.
|
|
||||||
4. Test native behavior, failed-operation atomicity and the binding transport payload.
|
|
||||||
5. Run `cargo test -p ocs_doc_api --all-features` and rebuild the generated snapshots.
|
|
||||||
@@ -34,6 +34,8 @@ default = []
|
|||||||
host = ["acadrust", "cadkernel"]
|
host = ["acadrust", "cadkernel"]
|
||||||
# ipc: pull ocs_plugin_api for the IPC transport adapter (plugin side).
|
# ipc: pull ocs_plugin_api for the IPC transport adapter (plugin side).
|
||||||
ipc = ["acadrust", "ocs_plugin_api"]
|
ipc = ["acadrust", "ocs_plugin_api"]
|
||||||
|
# doc_api_host: convenience helper that builds a DocApi from a HostApi (IPC-only).
|
||||||
|
doc_api_host = ["ipc"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "ocs_doc_api"
|
name = "ocs_doc_api"
|
||||||
|
|||||||
@@ -30,11 +30,109 @@ Transports bind to one document tab. Typed handles from another transport and
|
|||||||
requests for another tab are rejected. Raw `ObjectId` values are relative to the
|
requests for another tab are rejected. Raw `ObjectId` values are relative to the
|
||||||
receiving document. Errors are serialized as `ApiError` variants over IPC.
|
receiving document. Errors are serialized as `ApiError` variants over IPC.
|
||||||
|
|
||||||
|
Layer CRUD is exposed through `Document::layers()`, `create_layer(info)`,
|
||||||
|
`update_layer(name, info)`, `delete_layer(name)` and through the `layer()` /
|
||||||
|
`set_layer(...)` methods on every entity handle (`Line`, `Circle`, `Solid`,
|
||||||
|
`Entity`, etc.). Layer properties are carried as the plain-data `LayerInfo` DTO
|
||||||
|
with `LayerFlags`, `Color` and `LineWeight`. The host enforces CAD-style rules:
|
||||||
|
layer "0" and the current layer cannot be deleted, duplicate names are rejected,
|
||||||
|
and a layer still referenced by entities cannot be removed.
|
||||||
|
|
||||||
|
XDATA and XRECORD payloads are carried as plain-data, serde-compatible DTOs
|
||||||
|
(`XDataRecord`, `XRecordSpec`, and their value/entry types) and roundtripped
|
||||||
|
verbatim by the host. Every `Entity` handle can read/write one XDATA record per
|
||||||
|
registered application name via `xdata(application_name)` and `set_xdata(...)`,
|
||||||
|
and standalone XRECORD objects can be created with `doc.entities().create_xrecord(...)`
|
||||||
|
and updated through the `XRecord` handle.
|
||||||
|
|
||||||
| Feature | Use |
|
| Feature | Use |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Default | DTOs, facade, transport trait and binding schema |
|
| Default | DTOs, facade, transport trait and binding schema |
|
||||||
| `host` | Kernel and entity adapters, executor, `DocApiBackend`, `InProcess` |
|
| `host` | Kernel and entity adapters, executor, `DocApiBackend`, `InProcess` |
|
||||||
| `ipc` | `OcsPluginApiIpc` adapter for a plugin connected to the host |
|
| `ipc` | `OcsPluginApiIpc` adapter for a plugin connected to the host |
|
||||||
|
| `doc_api_host` | Convenience `doc_api_for_host(&dyn HostApi) -> Option<DocApi>` for out-of-process / worker-thread plugins |
|
||||||
|
|
||||||
|
### Connecting from a plugin
|
||||||
|
|
||||||
|
For out-of-process or worker-thread plugins that receive `&dyn HostApi` (or any
|
||||||
|
`ocs_plugin_api::host::HostApi` implementor), enable the `doc_api_host` feature
|
||||||
|
and use the helper instead of manually serializing `DocApiEnvelope`s:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::doc_api_for_host;
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn on_dispatch(host: &mut dyn HostApi) -> Option<()> {
|
||||||
|
let doc_api = doc_api_for_host(host)?;
|
||||||
|
let doc = doc_api.document(host.tab_id());
|
||||||
|
let cube = doc.solids().create_cuboid([0.0; 3], [10.0; 3]).ok()?;
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`doc_api_for_host` returns `None` for hosts that do not expose a
|
||||||
|
`PluginRequestSender` (for example the in-process `HostSession`). In-process code
|
||||||
|
should use `DocApi::in_process(backend, tab_id)` with a concrete
|
||||||
|
`DocApiBackend`.
|
||||||
|
|
||||||
|
### Plugin example: create, read, update and delete a line
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::{doc_api_for_host, ApiResult, Color, LayerInfo, LineWeight};
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn draw_and_modify_line(host: &mut dyn HostApi) -> ApiResult<()> {
|
||||||
|
// Build a DocApi from the host's plugin request channel.
|
||||||
|
let api = doc_api_for_host(host).ok_or_else(|| {
|
||||||
|
ocs_doc_api::ApiError::Transport("host does not expose a DocApi channel".into())
|
||||||
|
})?;
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
|
||||||
|
// Ensure a target layer exists.
|
||||||
|
let mut info = LayerInfo::new("MyLayer");
|
||||||
|
info.color = Color::Index(1);
|
||||||
|
info.line_weight = LineWeight::Value(25);
|
||||||
|
let _ = doc.create_layer(&info); // ok if it already exists
|
||||||
|
|
||||||
|
// Create a line on that layer.
|
||||||
|
let line = doc.curves().create_line([0.0, 0.0, 0.0], [10.0, 0.0, 0.0])?;
|
||||||
|
line.set_layer("MyLayer")?;
|
||||||
|
|
||||||
|
// Read it back.
|
||||||
|
assert_eq!(line.layer()?, "MYLAYER");
|
||||||
|
|
||||||
|
// Move the endpoint in place (same ObjectId).
|
||||||
|
line.transform(ocs_doc_api::PlacementSpec::at([5.0, 0.0, 0.0]))?;
|
||||||
|
|
||||||
|
// Delete it; the document is left unchanged if this failed.
|
||||||
|
line.delete()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plugin example: create a box and read its volume
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::{doc_api_for_host, ApiResult};
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn create_box_and_measure(host: &mut dyn HostApi) -> ApiResult<f64> {
|
||||||
|
let api = doc_api_for_host(host).ok_or_else(|| {
|
||||||
|
ocs_doc_api::ApiError::Transport("host does not expose a DocApi channel".into())
|
||||||
|
})?;
|
||||||
|
let doc = api.document(api.active_tab());
|
||||||
|
|
||||||
|
let box_ = doc.solids().create_cuboid([0.0; 3], [10.0; 3])?;
|
||||||
|
let volume = box_.volume()?;
|
||||||
|
|
||||||
|
// Boolean example: intersect with a second box.
|
||||||
|
let other = doc.solids().create_cuboid([5.0; 3], [10.0; 3])?;
|
||||||
|
let intersection = box_.intersect(&other)?;
|
||||||
|
let _intersection_volume = intersection.volume()?;
|
||||||
|
|
||||||
|
Ok(volume)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
See the [API reference](src/gen/api_reference.md) for constructors and methods,
|
See the [API reference](src/gen/api_reference.md) for constructors and methods,
|
||||||
[architecture](ARCHITECTURE.md) for backend rules, and
|
[architecture](ARCHITECTURE.md) for backend rules, and
|
||||||
|
|||||||
@@ -21,8 +21,39 @@ The bridge converts these objects to the Rust DTOs and bincode 1.3 wire format,
|
|||||||
sends `DocApiRequest { tab_id, bytes }`, and decodes `Result<Receipt, ApiError>`.
|
sends `DocApiRequest { tab_id, bytes }`, and decodes `Result<Receipt, ApiError>`.
|
||||||
Host plugin API version 6 and document envelope version 1 are required.
|
Host plugin API version 6 and document envelope version 1 are required.
|
||||||
|
|
||||||
|
For Rust plugins compiled against `ocs_doc_api` with the `doc_api_host` feature,
|
||||||
|
use the `doc_api_for_host` helper to build a typed `DocApi` from any
|
||||||
|
`ocs_plugin_api::host::HostApi` that exposes a `PluginRequestSender`
|
||||||
|
(out-of-process / worker-thread hosts):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use ocs_doc_api::doc_api_for_host;
|
||||||
|
use ocs_plugin_api::host::HostApi;
|
||||||
|
|
||||||
|
fn on_dispatch(host: &mut dyn HostApi) -> Option<()> {
|
||||||
|
let doc_api = doc_api_for_host(host)?;
|
||||||
|
let doc = doc_api.document(host.tab_id());
|
||||||
|
let cube = doc.solids().create_cuboid([0.0; 3], [10.0; 3]).ok()?;
|
||||||
|
// ...
|
||||||
|
Some(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In-process plugins should use `DocApi::in_process(backend, tab_id)` with a
|
||||||
|
concrete `DocApiBackend`; `doc_api_for_host` returns `None` for hosts that do not
|
||||||
|
expose a `PluginRequestSender`.
|
||||||
|
|
||||||
Decode receipt outcomes as `{"NewId": 42}` or `{"NewIds": [42, 43]}` and query
|
Decode receipt outcomes as `{"NewId": 42}` or `{"NewIds": [42, 43]}` and query
|
||||||
results as externally tagged dictionaries such as `{"Volume": 12.5}`. The facade
|
results as externally tagged dictionaries such as `{"Volume": 12.5}`. The facade
|
||||||
unwraps these into typed handles or values. The bridge must raise an exception
|
unwraps these into typed handles or values. Layer results use `{"Layers": [...]}`
|
||||||
for an error response. Transport binding determines the tab; requesting another
|
and `{"EntityLayer": "WALLS"}`. Layer properties (`LayerInfo`, `LayerFlags`,
|
||||||
tab or combining typed handles from different documents raises an error.
|
`Color`, `LineWeight`) appear as plain dictionaries in operation payloads, e.g.
|
||||||
|
`{"CreateLayer": {"name": "Walls", "flags": {"frozen": false, "locked": false,
|
||||||
|
"frozen_in_new_viewport": false, "off": false}, "color": {"Index": 7},
|
||||||
|
"line_type": "Continuous", "line_weight": {"Default": null},
|
||||||
|
"plot_style": "", "is_plottable": true}}`.
|
||||||
|
XDATA and XRECORD payloads appear as nested externally tagged dictionaries
|
||||||
|
(e.g. `{"XData": {"application_name": ..., "values": [...]}}` or
|
||||||
|
`{"XRecord": {"name": ..., "entries": [...]}}`). The bridge must raise an
|
||||||
|
exception for an error response. Transport binding determines the tab; requesting
|
||||||
|
another tab or combining typed handles from different documents raises an error.
|
||||||
@@ -36,6 +36,12 @@ class Entity:
|
|||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
self._doc._apply_op({"Delete": {"id": self.id}})
|
self._doc._apply_op({"Delete": {"id": self.id}})
|
||||||
|
|
||||||
|
def layer(self):
|
||||||
|
return self._doc._apply_query({"GetEntityLayer": {"id": self.id}})
|
||||||
|
|
||||||
|
def set_layer(self, layer):
|
||||||
|
self._doc._apply_op({"SetEntityLayer": {"id": self.id, "layer": layer}})
|
||||||
'''
|
'''
|
||||||
|
|
||||||
DOCUMENT = '''
|
DOCUMENT = '''
|
||||||
@@ -79,6 +85,18 @@ class Document:
|
|||||||
def curves(self): return Curves(self)
|
def curves(self): return Curves(self)
|
||||||
def entities(self): return Entities(self)
|
def entities(self): return Entities(self)
|
||||||
|
|
||||||
|
def layers(self):
|
||||||
|
return self._apply_query({"ListLayers": {}})
|
||||||
|
|
||||||
|
def create_layer(self, info):
|
||||||
|
self._apply_op({"CreateLayer": info})
|
||||||
|
|
||||||
|
def update_layer(self, name, info):
|
||||||
|
self._apply_op({"UpdateLayer": {"name": name, "info": info}})
|
||||||
|
|
||||||
|
def delete_layer(self, name):
|
||||||
|
self._apply_op({"DeleteLayer": {"name": name}})
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DocApi:
|
class DocApi:
|
||||||
_transport: Any
|
_transport: Any
|
||||||
|
|||||||
@@ -168,10 +168,32 @@ Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
|||||||
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
||||||
solids only.**
|
solids only.**
|
||||||
- **`delete()`** — remove the entity (one undo step).
|
- **`delete()`** — remove the entity (one undo step).
|
||||||
|
- **`layer()` -> `String`** — the name of the layer the entity is on.
|
||||||
|
- **`set_layer(layer)`** — move the entity to an existing layer (one undo step;
|
||||||
|
blocked if the entity or target layer is locked).
|
||||||
|
|
||||||
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
|
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
|
||||||
downcast when `kind == "Solid3D"`).
|
downcast when `kind == "Solid3D"`).
|
||||||
|
|
||||||
|
## Layer table
|
||||||
|
|
||||||
|
Layers are a named document table, not an entity family. `Document::layers()`
|
||||||
|
lists all layers as `Vec<LayerInfo>`. Table-level ops are `CreateLayer(info)`,
|
||||||
|
`UpdateLayer { name, info }` and `DeleteLayer { name }`. `Entity::layer()`
|
||||||
|
returns the layer name and `Entity::set_layer(layer)` moves the entity to an
|
||||||
|
existing layer. The host rejects duplicate names, deletion of layer "0" or the
|
||||||
|
current layer, and removing a layer that still has entities assigned.
|
||||||
|
|
||||||
|
## Transports
|
||||||
|
|
||||||
|
- **`InProcess`** (feature `host`) — drives a `DocApiBackend` directly in the host.
|
||||||
|
- **`OcsPluginApiIpc`** (feature `ipc`) — serializes envelopes over the
|
||||||
|
`ocs_plugin_api` `PluginRequestSender`/`PluginRequest` channel. The host routes
|
||||||
|
`DocApiRequest { tab_id, bytes }` to the same executor.
|
||||||
|
- **`doc_api_for_host(host)`** (feature `doc_api_host`) — convenience helper that
|
||||||
|
builds a `DocApi` from any `ocs_plugin_api::host::HostApi` exposing a
|
||||||
|
`PluginRequestSender` (out-of-process / worker-thread plugins).
|
||||||
|
|
||||||
## Collections & cross-cutting
|
## Collections & cross-cutting
|
||||||
|
|
||||||
- **`DocApi`** — root: `document(tab)`, `active_tab()`, `alive()`.
|
- **`DocApi`** — root: `document(tab)`, `active_tab()`, `alive()`.
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
||||||
use ocs_doc_api::{
|
use ocs_doc_api::{
|
||||||
Aabb, ApiError, ApiResult, Curve2Spec, EntityView, GeometryErrorKind, GeometryRevision,
|
normalize_layer_name, Aabb, ApiError, ApiResult, Curve2Spec, EntityView, GeometryErrorKind,
|
||||||
ObjectId, PlacementSpec,
|
GeometryRevision, LayerInfo, ObjectId, PlacementSpec,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct MockBackend {
|
pub struct MockBackend {
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
revision: u64,
|
revision: u64,
|
||||||
@@ -31,6 +32,38 @@ pub struct MockBackend {
|
|||||||
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
||||||
/// Stored viewport views (target, height).
|
/// Stored viewport views (target, height).
|
||||||
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
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>,
|
||||||
|
/// Stored layer table.
|
||||||
|
layers: HashMap<String, LayerInfo>,
|
||||||
|
/// Layer name assigned to each entity.
|
||||||
|
entity_layers: HashMap<ObjectId, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
curves: HashMap::new(),
|
||||||
|
text_values: HashMap::new(),
|
||||||
|
hatch_boundaries: HashMap::new(),
|
||||||
|
dimension_measurements: HashMap::new(),
|
||||||
|
attributes_store: HashMap::new(),
|
||||||
|
viewport_views: HashMap::new(),
|
||||||
|
xdata_store: HashMap::new(),
|
||||||
|
xrecord_store: HashMap::new(),
|
||||||
|
layers: HashMap::new(),
|
||||||
|
entity_layers: HashMap::new(),
|
||||||
|
};
|
||||||
|
backend.layers.insert("0".to_string(), LayerInfo::new("0"));
|
||||||
|
backend
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockBackend {
|
impl MockBackend {
|
||||||
@@ -52,7 +85,7 @@ impl MockBackend {
|
|||||||
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds).
|
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds).
|
||||||
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||||
match spec {
|
match spec {
|
||||||
Curve2Spec::Line { start, end } => Aabb {
|
Curve2Spec::Line { start, end, .. } => Aabb {
|
||||||
min: [
|
min: [
|
||||||
start[0].min(end[0]),
|
start[0].min(end[0]),
|
||||||
start[1].min(end[1]),
|
start[1].min(end[1]),
|
||||||
@@ -64,11 +97,11 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
|||||||
start[2].max(end[2]),
|
start[2].max(end[2]),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
Curve2Spec::Circle { centre, radius } => Aabb {
|
Curve2Spec::Circle { centre, radius, .. } => Aabb {
|
||||||
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
||||||
max: [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,
|
min: *position,
|
||||||
max: *position,
|
max: *position,
|
||||||
},
|
},
|
||||||
@@ -133,6 +166,15 @@ impl DocApiBackend for MockBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
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 mut next = self.clone();
|
||||||
let ids = specs
|
let ids = specs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -173,10 +215,29 @@ impl DocApiBackend for MockBackend {
|
|||||||
Curve2Spec::Ray { .. } => "Ray",
|
Curve2Spec::Ray { .. } => "Ray",
|
||||||
Curve2Spec::XLine { .. } => "XLine",
|
Curve2Spec::XLine { .. } => "XLine",
|
||||||
};
|
};
|
||||||
|
let layer = spec
|
||||||
|
.layer()
|
||||||
|
.map(|name| {
|
||||||
|
self.lookup_layer(name).map(|info| info.normalized_name())
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
let id = self.alloc(kind);
|
let id = self.alloc(kind);
|
||||||
self.curves.insert(id, spec.clone());
|
self.curves.insert(id, spec.clone());
|
||||||
|
self.entity_layers
|
||||||
|
.insert(id, layer.unwrap_or_else(|| "0".to_string()));
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn lookup_layer(&self, name: &str) -> ApiResult<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> {
|
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||||
if spec.block_name.is_empty() {
|
if spec.block_name.is_empty() {
|
||||||
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
||||||
@@ -355,6 +416,144 @@ impl DocApiBackend for MockBackend {
|
|||||||
.copied()
|
.copied()
|
||||||
.ok_or(ApiError::UnknownId(id))
|
.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: &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: &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<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 add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||||
if self.entity_exists(id) {
|
if self.entity_exists(id) {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -364,17 +563,85 @@ impl DocApiBackend for MockBackend {
|
|||||||
}
|
}
|
||||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
||||||
self.bodies.remove(&id);
|
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())
|
Ok(self.kinds.remove(&id).is_some())
|
||||||
}
|
}
|
||||||
|
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 get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
||||||
let kind = self.kinds.get(&id).ok_or(ApiError::UnknownId(id))?.clone();
|
let kind = self.kinds.get(&id).ok_or(ApiError::UnknownId(id))?.clone();
|
||||||
|
let layer = self
|
||||||
|
.entity_layers
|
||||||
|
.get(&id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "0".to_string());
|
||||||
let bounds = self.bodies.get(&id).and_then(|b| {
|
let bounds = self.bodies.get(&id).and_then(|b| {
|
||||||
cadkernel::brep::body_bounds(b).map(|bb| Aabb {
|
cadkernel::brep::body_bounds(b).map(|bb| Aabb {
|
||||||
min: bb.min,
|
min: bb.min,
|
||||||
max: bb.max,
|
max: bb.max,
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Ok(EntityView { id, kind, bounds })
|
Ok(EntityView {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
layer,
|
||||||
|
bounds,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
||||||
if !self.entity_exists(id) {
|
if !self.entity_exists(id) {
|
||||||
@@ -423,6 +690,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn entity_exists(&self, id: ObjectId) -> bool {
|
fn entity_exists(&self, id: ObjectId) -> bool {
|
||||||
self.kinds.contains_key(&id)
|
self.kinds.contains_key(&id)
|
||||||
}
|
}
|
||||||
|
fn object_exists(&self, id: ObjectId) -> bool {
|
||||||
|
self.xrecord_store.contains_key(&id)
|
||||||
|
}
|
||||||
fn revision(&self) -> GeometryRevision {
|
fn revision(&self) -> GeometryRevision {
|
||||||
GeometryRevision(self.revision)
|
GeometryRevision(self.revision)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,10 +54,7 @@ fn main() -> ocs_doc_api::ApiResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A totally unknown id also surfaces a structured error.
|
// A totally unknown id also surfaces a structured error.
|
||||||
assert!(matches!(
|
assert!(doc.entities().get(ObjectId::from_u64(0xDEAD)).is_err());
|
||||||
doc.entities().get(ObjectId::from_u64(0xDEAD)),
|
|
||||||
Err(_)
|
|
||||||
));
|
|
||||||
println!("failure semantics verified");
|
println!("failure semantics verified");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -457,6 +457,50 @@ name = "create_table"
|
|||||||
args = [{ name = "insertion_point", ty = "[f64; 3]" }, { name = "data", ty = "&[Vec<String>]" }]
|
args = [{ name = "insertion_point", ty = "[f64; 3]" }, { name = "data", ty = "&[Vec<String>]" }]
|
||||||
op = "CreateTable"
|
op = "CreateTable"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# XDATA / XRECORD FAMILY — application-specific extended data roundtrip
|
||||||
|
# ============================================================================
|
||||||
|
[[family]]
|
||||||
|
name = "XRecord"
|
||||||
|
acadrust_variant = "XRecord"
|
||||||
|
handle = "XRecord"
|
||||||
|
collection = "entities"
|
||||||
|
description = "Extended record (XRecord): arbitrary DXF group-code/value pairs. Roundtripped verbatim as an XRecordSpec payload."
|
||||||
|
|
||||||
|
[[family.constructor]]
|
||||||
|
name = "create_xrecord"
|
||||||
|
args = [{ name = "spec", ty = "XRecordSpec" }]
|
||||||
|
op = "CreateXRecord"
|
||||||
|
returns = "Entity"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "payload"
|
||||||
|
kind = "query"
|
||||||
|
query = "GetXRecord"
|
||||||
|
returns = "XRecordSpec"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "set_payload"
|
||||||
|
kind = "op"
|
||||||
|
op = "SetXRecord"
|
||||||
|
args = [{ name = "spec", ty = "XRecordSpec" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
|
# Cross-cutting: every Entity handle can read/write its own XDATA.
|
||||||
|
[[family.method]]
|
||||||
|
name = "xdata"
|
||||||
|
kind = "query"
|
||||||
|
query = "GetXData"
|
||||||
|
args = [{ name = "application_name", ty = "&str" }]
|
||||||
|
returns = "Option<XDataRecord>"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "set_xdata"
|
||||||
|
kind = "op"
|
||||||
|
op = "SetXData"
|
||||||
|
args = [{ name = "application_name", ty = "&str" }, { name = "record", ty = "Option<XDataRecord>" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -469,6 +513,42 @@ op = "CreateTable"
|
|||||||
# - Read-only batching: query_batch(|q| ...) — one round-trip, no revision bump.
|
# - Read-only batching: query_batch(|q| ...) — one round-trip, no revision bump.
|
||||||
# - OpGroup: best-effort client-side failure cleanup (NOT a transaction).
|
# - OpGroup: best-effort client-side failure cleanup (NOT a transaction).
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LAYER family (named drawing layers)
|
||||||
|
# ============================================================================
|
||||||
|
[[family]]
|
||||||
|
name = "Layer"
|
||||||
|
handle = "Layer"
|
||||||
|
collection = "layers"
|
||||||
|
description = "Named drawing layer (color, linetype, lineweight, visibility). Not an entity handle; accessed through Document::layers()."
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "list"
|
||||||
|
kind = "query"
|
||||||
|
query = "ListLayers"
|
||||||
|
returns = "Vec<LayerInfo>"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "create"
|
||||||
|
kind = "op"
|
||||||
|
op = "CreateLayer"
|
||||||
|
args = [{ name = "info", ty = "LayerInfo" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "update"
|
||||||
|
kind = "op"
|
||||||
|
op = "UpdateLayer"
|
||||||
|
args = [{ name = "name", ty = "String" }, { name = "info", ty = "LayerInfo" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "delete"
|
||||||
|
kind = "op"
|
||||||
|
op = "DeleteLayer"
|
||||||
|
args = [{ name = "name", ty = "String" }]
|
||||||
|
returns = "()"
|
||||||
|
|
||||||
# ---- Kernel/op mapping (reference for the executor + docs) ----
|
# ---- Kernel/op mapping (reference for the executor + docs) ----
|
||||||
[[kernel_map]]
|
[[kernel_map]]
|
||||||
op = "CreateSolid.Cuboid"
|
op = "CreateSolid.Cuboid"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
use crate::error::{ApiError, ApiResult};
|
use crate::error::{ApiError, ApiResult};
|
||||||
use crate::id::ObjectId;
|
use crate::id::ObjectId;
|
||||||
use crate::ops::Curve2Spec;
|
use crate::ops::{Curve2Spec, LayerInfo};
|
||||||
use crate::query::{Aabb, EntityView};
|
use crate::query::{Aabb, EntityView};
|
||||||
use crate::revision::GeometryRevision;
|
use crate::revision::GeometryRevision;
|
||||||
|
|
||||||
@@ -152,6 +152,85 @@ pub trait DocApiBackend {
|
|||||||
sections: &[(cadkernel::space::Plane, Vec<cadkernel::geom2d::Curve>)],
|
sections: &[(cadkernel::space::Plane, Vec<cadkernel::geom2d::Curve>)],
|
||||||
) -> ApiResult<ObjectId>;
|
) -> 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,
|
/// 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.
|
/// not on a locked layer)? Read-only pre-check used before mutations.
|
||||||
/// Default: existence + not-locked (backends narrow the family check).
|
/// Default: existence + not-locked (backends narrow the family check).
|
||||||
@@ -178,6 +257,10 @@ pub trait DocApiBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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).
|
/// A generic, untyped view of any entity (id + kind + coarse bounds).
|
||||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView>;
|
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView>;
|
||||||
|
|
||||||
|
|||||||
@@ -113,19 +113,19 @@ pub fn bulge_arc_segment(
|
|||||||
pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
||||||
crate::validation::curve(spec)?;
|
crate::validation::curve(spec)?;
|
||||||
let entity = match spec {
|
let entity = match spec {
|
||||||
Curve2Spec::Line { start, end } => {
|
Curve2Spec::Line { start, end, .. } => {
|
||||||
let mut e = Line::new();
|
let mut e = Line::new();
|
||||||
e.start = v3(*start);
|
e.start = v3(*start);
|
||||||
e.end = v3(*end);
|
e.end = v3(*end);
|
||||||
EntityType::Line(e)
|
EntityType::Line(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Circle { centre, radius } => {
|
Curve2Spec::Circle { centre, radius, .. } => {
|
||||||
let mut e = Circle::new();
|
let mut e = Circle::new();
|
||||||
e.center = v3(*centre);
|
e.center = v3(*centre);
|
||||||
e.radius = *radius;
|
e.radius = *radius;
|
||||||
EntityType::Circle(e)
|
EntityType::Circle(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Polyline { points, closed } => {
|
Curve2Spec::Polyline { points, closed, .. } => {
|
||||||
let mut e = LwPolyline::new();
|
let mut e = LwPolyline::new();
|
||||||
e.vertices = points
|
e.vertices = points
|
||||||
.iter()
|
.iter()
|
||||||
@@ -147,7 +147,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
e.is_closed = *closed;
|
e.is_closed = *closed;
|
||||||
EntityType::LwPolyline(e)
|
EntityType::LwPolyline(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Point { position } => {
|
Curve2Spec::Point { position, .. } => {
|
||||||
let mut e = Point::new();
|
let mut e = Point::new();
|
||||||
e.location = v3(*position);
|
e.location = v3(*position);
|
||||||
EntityType::Point(e)
|
EntityType::Point(e)
|
||||||
@@ -157,6 +157,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
radius,
|
radius,
|
||||||
start_angle,
|
start_angle,
|
||||||
end_angle,
|
end_angle,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let mut e = Arc::new();
|
let mut e = Arc::new();
|
||||||
e.center = v3(*centre);
|
e.center = v3(*centre);
|
||||||
@@ -171,6 +172,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
ratio,
|
ratio,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
// Validate the minor/major ratio so the (major-axis) bounds never
|
// Validate the minor/major ratio so the (major-axis) bounds never
|
||||||
// under-bound: ratio must be in (0, 1].
|
// under-bound: ratio must be in (0, 1].
|
||||||
@@ -193,6 +195,7 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
control_points,
|
control_points,
|
||||||
knots,
|
knots,
|
||||||
weights,
|
weights,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let mut e = Spline::new();
|
let mut e = Spline::new();
|
||||||
e.degree = *degree;
|
e.degree = *degree;
|
||||||
@@ -201,10 +204,10 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|||||||
e.weights = weights.clone();
|
e.weights = weights.clone();
|
||||||
EntityType::Spline(e)
|
EntityType::Spline(e)
|
||||||
}
|
}
|
||||||
Curve2Spec::Ray { origin, direction } => {
|
Curve2Spec::Ray { origin, direction, .. } => {
|
||||||
EntityType::Ray(Ray::new(v3(*origin), v3(*direction)))
|
EntityType::Ray(Ray::new(v3(*origin), v3(*direction)))
|
||||||
}
|
}
|
||||||
Curve2Spec::XLine { origin, direction } => {
|
Curve2Spec::XLine { origin, direction, .. } => {
|
||||||
EntityType::XLine(XLine::new(v3(*origin), v3(*direction)))
|
EntityType::XLine(XLine::new(v3(*origin), v3(*direction)))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,14 +25,35 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
|||||||
fn apply_op_inner<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt> {
|
fn apply_op_inner<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt> {
|
||||||
let name = op.op_name();
|
let name = op.op_name();
|
||||||
crate::validation::operation(&op)?;
|
crate::validation::operation(&op)?;
|
||||||
if let Operation::TransformMany { ids, .. } | Operation::DeleteMany(ids) = &op {
|
match &op {
|
||||||
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
Operation::TransformMany { ids, .. } | Operation::DeleteMany(ids) => {
|
||||||
if ids.is_empty() || unique.len() != ids.len() {
|
let unique: std::collections::HashSet<_> = ids.iter().collect();
|
||||||
return Err(ApiError::validation(
|
if ids.is_empty() || unique.len() != ids.len() {
|
||||||
name,
|
return Err(ApiError::validation(
|
||||||
"batch ids must be nonempty and unique",
|
name,
|
||||||
));
|
"batch ids must be nonempty and unique",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Operation::SetXDataMany(records) => {
|
||||||
|
if records.is_empty() {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
name,
|
||||||
|
"SetXDataMany records must be nonempty",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if records.len() > BULK_ITEM_CAP {
|
||||||
|
return Err(over_cap(name, records.len()));
|
||||||
|
}
|
||||||
|
let unique: std::collections::HashSet<_> = records.iter().map(|(id, _, _)| id).collect();
|
||||||
|
if unique.len() != records.len() {
|
||||||
|
return Err(ApiError::validation(
|
||||||
|
name,
|
||||||
|
"SetXDataMany records must have unique entity ids",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
if let Operation::Transform { placement, .. } | Operation::TransformMany { placement, .. } = &op
|
if let Operation::Transform { placement, .. } | Operation::TransformMany { placement, .. } = &op
|
||||||
{
|
{
|
||||||
@@ -353,6 +374,70 @@ fn apply_op_inner<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Recei
|
|||||||
b.finalize_op();
|
b.finalize_op();
|
||||||
OpOutcome::NewId(id)
|
OpOutcome::NewId(id)
|
||||||
}
|
}
|
||||||
|
Operation::CreateLayer(info) => {
|
||||||
|
b.push_undo(name);
|
||||||
|
b.create_layer(info)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::UpdateLayer { name, info } => {
|
||||||
|
b.push_undo(name);
|
||||||
|
b.update_layer(name, info)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::DeleteLayer { name } => {
|
||||||
|
b.push_undo(name);
|
||||||
|
b.delete_layer(name)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::SetEntityLayer { id, layer } => {
|
||||||
|
require_exists(b, *id, name)?;
|
||||||
|
b.push_undo(name);
|
||||||
|
b.set_entity_layer(*id, layer)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(*id)
|
||||||
|
}
|
||||||
|
Operation::SetXData {
|
||||||
|
id,
|
||||||
|
application_name,
|
||||||
|
record,
|
||||||
|
} => {
|
||||||
|
require_exists(b, *id, name)?;
|
||||||
|
b.push_undo(name);
|
||||||
|
b.set_xdata(*id, application_name, record.as_ref())?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(*id)
|
||||||
|
}
|
||||||
|
Operation::SetXDataMany(records) => {
|
||||||
|
// Pre-validate every entity id before any mutation.
|
||||||
|
for (id, app, _) in records {
|
||||||
|
require_exists(b, *id, name)?;
|
||||||
|
if app.is_empty() {
|
||||||
|
return Err(ApiError::validation(name, "empty application_name"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.push_undo(name);
|
||||||
|
for (id, application_name, record) in records {
|
||||||
|
b.set_xdata(*id, application_name, record.as_ref())?;
|
||||||
|
}
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(ObjectId::NULL)
|
||||||
|
}
|
||||||
|
Operation::CreateXRecord(spec) => {
|
||||||
|
b.push_undo(name);
|
||||||
|
let id = b.add_xrecord(spec)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::NewId(id)
|
||||||
|
}
|
||||||
|
Operation::SetXRecord { id, spec } => {
|
||||||
|
require_object_exists(b, *id, name)?;
|
||||||
|
b.push_undo(name);
|
||||||
|
b.set_xrecord(*id, spec)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::Updated(*id)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(Receipt {
|
Ok(Receipt {
|
||||||
outcome: Some(outcome),
|
outcome: Some(outcome),
|
||||||
@@ -417,6 +502,38 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
|
|||||||
let (target, height) = b.viewport_view(*id).map_err(|e| label(qname, e))?;
|
let (target, height) = b.viewport_view(*id).map_err(|e| label(qname, e))?;
|
||||||
QueryResult::ViewportView { target, height }
|
QueryResult::ViewportView { target, height }
|
||||||
}
|
}
|
||||||
|
Query::GetXData {
|
||||||
|
id,
|
||||||
|
application_name,
|
||||||
|
} => QueryResult::XData(
|
||||||
|
b.xdata(*id, application_name)
|
||||||
|
.map_err(|e| label(qname, e))?
|
||||||
|
.clone(),
|
||||||
|
),
|
||||||
|
Query::GetXRecord { id } => {
|
||||||
|
QueryResult::XRecord(b.xrecord(*id).map_err(|e| label(qname, e))?.ok_or_else(
|
||||||
|
|| ApiError::validation(qname, format!("ObjectId {id:?} is not an XRecord")),
|
||||||
|
)?)
|
||||||
|
}
|
||||||
|
Query::ListLayers => QueryResult::Layers(b.layers().map_err(|e| label(qname, e))?),
|
||||||
|
Query::GetEntityLayer { id } => {
|
||||||
|
QueryResult::EntityLayer(b.entity_layer(*id).map_err(|e| label(qname, e))?)
|
||||||
|
}
|
||||||
|
Query::EnumerateEntities {
|
||||||
|
kind,
|
||||||
|
layer,
|
||||||
|
include_bounds,
|
||||||
|
} => QueryResult::Entities(
|
||||||
|
b.enumerate_entities(kind.as_deref(), layer.as_deref(), *include_bounds)
|
||||||
|
.map_err(|e| label(qname, e))?,
|
||||||
|
),
|
||||||
|
Query::GetPointPosition { id } => QueryResult::PointPosition(
|
||||||
|
b.point_position(*id).map_err(|e| label(qname, e))?,
|
||||||
|
),
|
||||||
|
Query::GetLineGeometry { id } => {
|
||||||
|
let (start, end) = b.line_geometry(*id).map_err(|e| label(qname, e))?;
|
||||||
|
QueryResult::LineGeometry((start, end))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
results.push(r);
|
results.push(r);
|
||||||
}
|
}
|
||||||
@@ -459,6 +576,15 @@ fn require_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> Ap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate that a named object exists before performing an operation.
|
||||||
|
fn require_object_exists<B: DocApiBackend>(b: &B, id: ObjectId, op: &'static str) -> ApiResult<()> {
|
||||||
|
if b.object_exists(id) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ApiError::validation(op, format!("unknown ObjectId {id:?}")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn over_cap(op: &'static str, n: usize) -> ApiError {
|
fn over_cap(op: &'static str, n: usize) -> ApiError {
|
||||||
ApiError::validation(op, format!("bulk op over cap: {n} > {BULK_ITEM_CAP}"))
|
ApiError::validation(op, format!("bulk op over cap: {n} > {BULK_ITEM_CAP}"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::envelope::{DocApiEnvelope, Receipt};
|
|||||||
use crate::error::{ApiError, ApiResult};
|
use crate::error::{ApiError, ApiResult};
|
||||||
use crate::gen::{Operation, Query};
|
use crate::gen::{Operation, Query};
|
||||||
use crate::id::ObjectId;
|
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::query::{Aabb, EntityView, QueryResult};
|
||||||
use crate::revision::GeometryRevision;
|
use crate::revision::GeometryRevision;
|
||||||
use crate::transport::Transport;
|
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).
|
/// Batch of read-only queries in ONE round-trip (safe: no mutation/undo).
|
||||||
/// The closure records queries on a [`QueryBatch`]; the results are returned
|
/// The closure records queries on a [`QueryBatch`]; the results are returned
|
||||||
/// in the same order as a [`QueryResults`] view the caller destructures.
|
/// in the same order as a [`QueryResults`] view the caller destructures.
|
||||||
@@ -336,6 +404,25 @@ macro_rules! handle {
|
|||||||
})?;
|
})?;
|
||||||
Ok(())
|
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!(Text);
|
||||||
handle!(MText);
|
handle!(MText);
|
||||||
handle!(Dimension);
|
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 {
|
impl Dimension {
|
||||||
/// The measured value of this dimension (distance for linear/radius, degrees
|
/// The measured value of this dimension (distance for linear/radius, degrees
|
||||||
@@ -416,6 +522,26 @@ impl Entity {
|
|||||||
})?;
|
})?;
|
||||||
Ok(())
|
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> {
|
pub fn as_solid(&self) -> Option<Solid> {
|
||||||
// Typed downcast is validated by the view's kind.
|
// Typed downcast is validated by the view's kind.
|
||||||
matches!(self.view().ok()?.kind.as_str(), "Solid3D")
|
matches!(self.view().ok()?.kind.as_str(), "Solid3D")
|
||||||
@@ -621,22 +747,80 @@ impl CurveCollection {
|
|||||||
Ok((self.session.clone(), id))
|
Ok((self.session.clone(), id))
|
||||||
}
|
}
|
||||||
pub fn create_line(&self, start: [f64; 3], end: [f64; 3]) -> ApiResult<Line> {
|
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))
|
Ok(Line::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_circle(&self, centre: [f64; 3], radius: f64) -> ApiResult<Circle> {
|
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))
|
Ok(Circle::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_polyline(&self, points: &[[f64; 3]], closed: bool) -> ApiResult<Polyline> {
|
pub fn create_polyline(&self, points: &[[f64; 3]], closed: bool) -> ApiResult<Polyline> {
|
||||||
let (s, id) = self.create_curve(Curve2Spec::Polyline {
|
let (s, id) = self.create_curve(Curve2Spec::Polyline {
|
||||||
points: points.to_vec(),
|
points: points.to_vec(),
|
||||||
closed,
|
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))
|
Ok(Polyline::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_point(&self, position: [f64; 3]) -> ApiResult<Point> {
|
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))
|
Ok(Point::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_arc(
|
pub fn create_arc(
|
||||||
@@ -651,6 +835,24 @@ impl CurveCollection {
|
|||||||
radius,
|
radius,
|
||||||
start_angle,
|
start_angle,
|
||||||
end_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))
|
Ok(ArcCurve::new(s, id))
|
||||||
}
|
}
|
||||||
@@ -668,6 +870,26 @@ impl CurveCollection {
|
|||||||
ratio,
|
ratio,
|
||||||
start,
|
start,
|
||||||
end,
|
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))
|
Ok(Ellipse::new(s, id))
|
||||||
}
|
}
|
||||||
@@ -683,15 +905,67 @@ impl CurveCollection {
|
|||||||
control_points: control_points.to_vec(),
|
control_points: control_points.to_vec(),
|
||||||
knots: knots.to_vec(),
|
knots: knots.to_vec(),
|
||||||
weights: weights.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))
|
Ok(Spline::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_ray(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<Ray> {
|
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))
|
Ok(Ray::new(s, id))
|
||||||
}
|
}
|
||||||
pub fn create_xline(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<XLine> {
|
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))
|
Ok(XLine::new(s, id))
|
||||||
}
|
}
|
||||||
/// A RASTER_IMAGE placed at `insertion_point` (host auto-registers the image definition).
|
/// 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))
|
Ok(MText::new(self.session.clone(), id))
|
||||||
}
|
}
|
||||||
/// Bulk-create many points in ONE op: all-or-nothing, one undo step.
|
/// 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>> {
|
pub fn create_points(&self, positions: &[[f64; 3]]) -> ApiResult<Vec<Point>> {
|
||||||
let specs: Vec<crate::ops::EntitySpec> = positions
|
let specs: Vec<crate::ops::EntitySpec> = positions
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
let receipt = self.session.apply_op(Operation::CreateMany(specs))?;
|
let receipt = self.session.apply_op(Operation::CreateMany(specs))?;
|
||||||
let outcome = receipt
|
let outcome = receipt
|
||||||
@@ -1001,6 +1307,41 @@ impl EntityCollection {
|
|||||||
self.session.apply_op(Operation::Delete { id })?;
|
self.session.apply_op(Operation::Delete { id })?;
|
||||||
Ok(())
|
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).
|
/// Bulk transform (one op, all-or-nothing).
|
||||||
pub fn transform_many(&self, ids: &[ObjectId], placement: PlacementSpec) -> ApiResult<()> {
|
pub fn transform_many(&self, ids: &[ObjectId], placement: PlacementSpec) -> ApiResult<()> {
|
||||||
self.session.apply_op(Operation::TransformMany {
|
self.session.apply_op(Operation::TransformMany {
|
||||||
|
|||||||
@@ -192,6 +192,32 @@ Table: create_table(insertion_point, data[row][col]) builds rows x columns of te
|
|||||||
|
|
||||||
- **create_table**(insertion_point: [f64; 3], data: &[Vec<String>]) -> `Entity` — `CreateTable`
|
- **create_table**(insertion_point: [f64; 3], data: &[Vec<String>]) -> `Entity` — `CreateTable`
|
||||||
|
|
||||||
|
## `XRecord` (acadrust `XRecord`, collection `entities`)
|
||||||
|
|
||||||
|
Extended record (XRecord): arbitrary DXF group-code/value pairs. Roundtripped verbatim as an XRecordSpec payload.
|
||||||
|
|
||||||
|
### Constructors (`doc.entities()`)
|
||||||
|
|
||||||
|
- **create_xrecord**(spec: XRecordSpec) -> `Entity` — `CreateXRecord`
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- **payload**() -> `XRecordSpec` — query `GetXRecord`
|
||||||
|
- **set_payload**(spec: XRecordSpec) -> `()` — op `SetXRecord`
|
||||||
|
- **xdata**(application_name: &str) -> `Option<XDataRecord>` — query `GetXData`
|
||||||
|
- **set_xdata**(application_name: &str, record: Option<XDataRecord>) -> `()` — op `SetXData`
|
||||||
|
|
||||||
|
## `Layer` (acadrust `-`, collection `layers`)
|
||||||
|
|
||||||
|
Named drawing layer (color, linetype, lineweight, visibility). Not an entity handle; accessed through Document::layers().
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- **list**() -> `Vec<LayerInfo>` — query `ListLayers`
|
||||||
|
- **create**(info: LayerInfo) -> `()` — op `CreateLayer`
|
||||||
|
- **update**(name: String, info: LayerInfo) -> `()` — op `UpdateLayer`
|
||||||
|
- **delete**(name: String) -> `()` — op `DeleteLayer`
|
||||||
|
|
||||||
## Generic methods (every handle)
|
## Generic methods (every handle)
|
||||||
|
|
||||||
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
||||||
@@ -204,10 +230,32 @@ Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
|
|||||||
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
|
||||||
solids only.**
|
solids only.**
|
||||||
- **`delete()`** — remove the entity (one undo step).
|
- **`delete()`** — remove the entity (one undo step).
|
||||||
|
- **`layer()` -> `String`** — the name of the layer the entity is on.
|
||||||
|
- **`set_layer(layer)`** — move the entity to an existing layer (one undo step;
|
||||||
|
blocked if the entity or target layer is locked).
|
||||||
|
|
||||||
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
|
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
|
||||||
downcast when `kind == "Solid3D"`).
|
downcast when `kind == "Solid3D"`).
|
||||||
|
|
||||||
|
## Layer table
|
||||||
|
|
||||||
|
Layers are a named document table, not an entity family. `Document::layers()`
|
||||||
|
lists all layers as `Vec<LayerInfo>`. Table-level ops are `CreateLayer(info)`,
|
||||||
|
`UpdateLayer { name, info }` and `DeleteLayer { name }`. `Entity::layer()`
|
||||||
|
returns the layer name and `Entity::set_layer(layer)` moves the entity to an
|
||||||
|
existing layer. The host rejects duplicate names, deletion of layer "0" or the
|
||||||
|
current layer, and removing a layer that still has entities assigned.
|
||||||
|
|
||||||
|
## Transports
|
||||||
|
|
||||||
|
- **`InProcess`** (feature `host`) — drives a `DocApiBackend` directly in the host.
|
||||||
|
- **`OcsPluginApiIpc`** (feature `ipc`) — serializes envelopes over the
|
||||||
|
`ocs_plugin_api` `PluginRequestSender`/`PluginRequest` channel. The host routes
|
||||||
|
`DocApiRequest { tab_id, bytes }` to the same executor.
|
||||||
|
- **`doc_api_for_host(host)`** (feature `doc_api_host`) — convenience helper that
|
||||||
|
builds a `DocApi` from any `ocs_plugin_api::host::HostApi` exposing a
|
||||||
|
`PluginRequestSender` (out-of-process / worker-thread plugins).
|
||||||
|
|
||||||
## Collections & cross-cutting
|
## Collections & cross-cutting
|
||||||
|
|
||||||
- **`DocApi`** — root: `document(tab)`, `active_tab()`, `alive()`.
|
- **`DocApi`** — root: `document(tab)`, `active_tab()`, `alive()`.
|
||||||
@@ -284,6 +332,14 @@ Every `Operation` is ONE atomic write op (one undo step); every `Query` is read-
|
|||||||
- `CreateAttributeDefinition(AttributeDefinitionSpec)`
|
- `CreateAttributeDefinition(AttributeDefinitionSpec)`
|
||||||
- `CreateTable(TableSpec)`
|
- `CreateTable(TableSpec)`
|
||||||
- `CreateDimensionAngular2Ln(DimensionAngularSpec)`
|
- `CreateDimensionAngular2Ln(DimensionAngularSpec)`
|
||||||
|
- `SetXData { id: ObjectId, application_name: String, record: Option<XDataRecord> }`
|
||||||
|
- `CreateXRecord(XRecordSpec)`
|
||||||
|
- `SetXRecord { id: ObjectId, spec: XRecordSpec }`
|
||||||
|
- `CreateLayer(LayerInfo)`
|
||||||
|
- `UpdateLayer { name: String, info: LayerInfo }`
|
||||||
|
- `DeleteLayer { name: String }`
|
||||||
|
- `SetEntityLayer { id: ObjectId, layer: String }`
|
||||||
|
- `SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord)>)`
|
||||||
|
|
||||||
### `Query`
|
### `Query`
|
||||||
|
|
||||||
@@ -299,6 +355,10 @@ Every `Operation` is ONE atomic write op (one undo step); every `Query` is read-
|
|||||||
- `GetAttributes { id: ObjectId }`
|
- `GetAttributes { id: ObjectId }`
|
||||||
- `GetBlockEntities { block_name: String }`
|
- `GetBlockEntities { block_name: String }`
|
||||||
- `GetViewportView { id: ObjectId }`
|
- `GetViewportView { id: ObjectId }`
|
||||||
|
- `GetXData { id: ObjectId, application_name: String }`
|
||||||
|
- `GetXRecord { id: ObjectId }`
|
||||||
|
- `ListLayers`
|
||||||
|
- `GetEntityLayer { id: ObjectId }`
|
||||||
|
|
||||||
## Errors (`ApiError`)
|
## Errors (`ApiError`)
|
||||||
|
|
||||||
|
|||||||
@@ -1124,6 +1124,164 @@
|
|||||||
"handle": "Entity",
|
"handle": "Entity",
|
||||||
"methods": [],
|
"methods": [],
|
||||||
"name": "Table"
|
"name": "Table"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"acadrust_variant": "XRecord",
|
||||||
|
"collection": "entities",
|
||||||
|
"constructors": [
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "spec",
|
||||||
|
"ty": "XRecordSpec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"name": "create_xrecord",
|
||||||
|
"op": "CreateXRecord",
|
||||||
|
"returns": "Entity"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"handle": "XRecord",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "query",
|
||||||
|
"name": "payload",
|
||||||
|
"op": null,
|
||||||
|
"query": "GetXRecord",
|
||||||
|
"read_only": true,
|
||||||
|
"returns": "XRecordSpec"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "spec",
|
||||||
|
"ty": "XRecordSpec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "set_payload",
|
||||||
|
"op": "SetXRecord",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "application_name",
|
||||||
|
"ty": "&str"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "query",
|
||||||
|
"name": "xdata",
|
||||||
|
"op": null,
|
||||||
|
"query": "GetXData",
|
||||||
|
"read_only": true,
|
||||||
|
"returns": "Option<XDataRecord>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "application_name",
|
||||||
|
"ty": "&str"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "record",
|
||||||
|
"ty": "Option<XDataRecord>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "set_xdata",
|
||||||
|
"op": "SetXData",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "XRecord"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"acadrust_variant": null,
|
||||||
|
"collection": "layers",
|
||||||
|
"constructors": [],
|
||||||
|
"handle": "Layer",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "query",
|
||||||
|
"name": "list",
|
||||||
|
"op": null,
|
||||||
|
"query": "ListLayers",
|
||||||
|
"read_only": true,
|
||||||
|
"returns": "Vec<LayerInfo>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "info",
|
||||||
|
"ty": "LayerInfo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "create",
|
||||||
|
"op": "CreateLayer",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ty": "String"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "info",
|
||||||
|
"ty": "LayerInfo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "update",
|
||||||
|
"op": "UpdateLayer",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ty": "String"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "op",
|
||||||
|
"name": "delete",
|
||||||
|
"op": "DeleteLayer",
|
||||||
|
"query": null,
|
||||||
|
"read_only": false,
|
||||||
|
"returns": "()"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Layer"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"layouts": {
|
"layouts": {
|
||||||
@@ -1164,7 +1322,15 @@
|
|||||||
"CreateDimensionAngular(DimensionAngularSpec)",
|
"CreateDimensionAngular(DimensionAngularSpec)",
|
||||||
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
||||||
"CreateTable(TableSpec)",
|
"CreateTable(TableSpec)",
|
||||||
"CreateDimensionAngular2Ln(DimensionAngularSpec)"
|
"CreateDimensionAngular2Ln(DimensionAngularSpec)",
|
||||||
|
"SetXData { id: ObjectId, application_name: String, record: Option<XDataRecord> }",
|
||||||
|
"CreateXRecord(XRecordSpec)",
|
||||||
|
"SetXRecord { id: ObjectId, spec: XRecordSpec }",
|
||||||
|
"CreateLayer(LayerInfo)",
|
||||||
|
"UpdateLayer { name: String, info: LayerInfo }",
|
||||||
|
"DeleteLayer { name: String }",
|
||||||
|
"SetEntityLayer { id: ObjectId, layer: String }",
|
||||||
|
"SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord)>)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Query": {
|
"Query": {
|
||||||
@@ -1180,7 +1346,11 @@
|
|||||||
"GetDimensionMeasurement { id: ObjectId }",
|
"GetDimensionMeasurement { id: ObjectId }",
|
||||||
"GetAttributes { id: ObjectId }",
|
"GetAttributes { id: ObjectId }",
|
||||||
"GetBlockEntities { block_name: String }",
|
"GetBlockEntities { block_name: String }",
|
||||||
"GetViewportView { id: ObjectId }"
|
"GetViewportView { id: ObjectId }",
|
||||||
|
"GetXData { id: ObjectId, application_name: String }",
|
||||||
|
"GetXRecord { id: ObjectId }",
|
||||||
|
"ListLayers",
|
||||||
|
"GetEntityLayer { id: ObjectId }"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1205,7 +1375,9 @@
|
|||||||
"XLine",
|
"XLine",
|
||||||
"Text",
|
"Text",
|
||||||
"MText",
|
"MText",
|
||||||
"Dimension"
|
"Dimension",
|
||||||
|
"XRecord",
|
||||||
|
"Layer"
|
||||||
],
|
],
|
||||||
"root": "DocApi"
|
"root": "DocApi"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,15 @@
|
|||||||
"CreateDimensionAngular(DimensionAngularSpec)",
|
"CreateDimensionAngular(DimensionAngularSpec)",
|
||||||
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
"CreateAttributeDefinition(AttributeDefinitionSpec)",
|
||||||
"CreateTable(TableSpec)",
|
"CreateTable(TableSpec)",
|
||||||
"CreateDimensionAngular2Ln(DimensionAngularSpec)"
|
"CreateDimensionAngular2Ln(DimensionAngularSpec)",
|
||||||
|
"SetXData { id: ObjectId, application_name: String, record: Option<XDataRecord> }",
|
||||||
|
"CreateXRecord(XRecordSpec)",
|
||||||
|
"SetXRecord { id: ObjectId, spec: XRecordSpec }",
|
||||||
|
"CreateLayer(LayerInfo)",
|
||||||
|
"UpdateLayer { name: String, info: LayerInfo }",
|
||||||
|
"DeleteLayer { name: String }",
|
||||||
|
"SetEntityLayer { id: ObjectId, layer: String }",
|
||||||
|
"SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord)>)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Query": {
|
"Query": {
|
||||||
@@ -45,7 +53,11 @@
|
|||||||
"GetDimensionMeasurement { id: ObjectId }",
|
"GetDimensionMeasurement { id: ObjectId }",
|
||||||
"GetAttributes { id: ObjectId }",
|
"GetAttributes { id: ObjectId }",
|
||||||
"GetBlockEntities { block_name: String }",
|
"GetBlockEntities { block_name: String }",
|
||||||
"GetViewportView { id: ObjectId }"
|
"GetViewportView { id: ObjectId }",
|
||||||
|
"GetXData { id: ObjectId, application_name: String }",
|
||||||
|
"GetXRecord { id: ObjectId }",
|
||||||
|
"ListLayers",
|
||||||
|
"GetEntityLayer { id: ObjectId }"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,7 @@ use crate::id::ObjectId;
|
|||||||
use crate::ops::{
|
use crate::ops::{
|
||||||
AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec,
|
AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec,
|
||||||
DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec,
|
DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec,
|
||||||
SolidPrimitive, TableSpec, TextSpec, ViewportSpec,
|
SolidPrimitive, TableSpec, TextSpec, ViewportSpec, XDataRecord, XRecordSpec,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A typed write operation. Each variant is ONE atomic host call = one
|
/// A typed write operation. Each variant is ONE atomic host call = one
|
||||||
@@ -99,4 +99,29 @@ pub enum Operation {
|
|||||||
CreateTable(TableSpec),
|
CreateTable(TableSpec),
|
||||||
// dimension sub-types (2-line angular)
|
// dimension sub-types (2-line angular)
|
||||||
CreateDimensionAngular2Ln(DimensionAngularSpec),
|
CreateDimensionAngular2Ln(DimensionAngularSpec),
|
||||||
|
// xdata / xrecord roundtrip
|
||||||
|
SetXData {
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: String,
|
||||||
|
record: Option<XDataRecord>,
|
||||||
|
},
|
||||||
|
SetXDataMany(Vec<(ObjectId, String, Option<XDataRecord>)>),
|
||||||
|
CreateXRecord(XRecordSpec),
|
||||||
|
SetXRecord {
|
||||||
|
id: ObjectId,
|
||||||
|
spec: XRecordSpec,
|
||||||
|
},
|
||||||
|
// layer CRUD
|
||||||
|
CreateLayer(crate::ops::LayerInfo),
|
||||||
|
UpdateLayer {
|
||||||
|
name: String,
|
||||||
|
info: crate::ops::LayerInfo,
|
||||||
|
},
|
||||||
|
DeleteLayer {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
SetEntityLayer {
|
||||||
|
id: ObjectId,
|
||||||
|
layer: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -55,4 +55,35 @@ pub enum Query {
|
|||||||
GetViewportView {
|
GetViewportView {
|
||||||
id: ObjectId,
|
id: ObjectId,
|
||||||
},
|
},
|
||||||
|
/// The XDATA record for `application_name` attached to an entity (`None` if absent).
|
||||||
|
GetXData {
|
||||||
|
id: ObjectId,
|
||||||
|
application_name: String,
|
||||||
|
},
|
||||||
|
/// The XRECORD object payload by id.
|
||||||
|
GetXRecord {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
|
/// List all layers in the document.
|
||||||
|
ListLayers,
|
||||||
|
/// The layer name of an entity.
|
||||||
|
GetEntityLayer {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
|
/// Enumerate first-class entities, optionally filtered by kind and/or layer.
|
||||||
|
/// `include_bounds` populates `EntityView::bounds`; leave it false for a
|
||||||
|
/// lightweight list view.
|
||||||
|
EnumerateEntities {
|
||||||
|
kind: Option<String>,
|
||||||
|
layer: Option<String>,
|
||||||
|
include_bounds: bool,
|
||||||
|
},
|
||||||
|
/// The WCS location of a Point entity.
|
||||||
|
GetPointPosition {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
|
/// The start and end WCS points of a Line entity.
|
||||||
|
GetLineGeometry {
|
||||||
|
id: ObjectId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//! Shared layer-name normalization rules.
|
||||||
|
//!
|
||||||
|
//! Layer names in OpenCADStudio/acadrust are case-insensitive, leading/trailing
|
||||||
|
//! whitespace is ignored, and empty names are invalid. The host (`normalize_name`
|
||||||
|
//! in `acadrust::tables`) implements the canonical rule set; this module re-exports
|
||||||
|
//! a compatible, dependency-free function for the wire side and for backends that
|
||||||
|
//! do not link against `acadrust` directly.
|
||||||
|
|
||||||
|
/// Normalize a raw layer name into its case-insensitive lookup key.
|
||||||
|
///
|
||||||
|
/// This mirrors `acadrust::tables::normalize_name` used by the host:
|
||||||
|
/// - trims leading and trailing ASCII whitespace
|
||||||
|
/// - converts the result to uppercase
|
||||||
|
/// - returns `""` if the name is empty after trimming
|
||||||
|
pub fn normalize_layer_name(name: &str) -> String {
|
||||||
|
name.trim().to_ascii_uppercase()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A normalized layer name is non-empty and matches the host lookup key.
|
||||||
|
pub fn is_valid_layer_name(name: &str) -> bool {
|
||||||
|
!normalize_layer_name(name).is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normalization_trims_and_uppercases() {
|
||||||
|
assert_eq!(normalize_layer_name(" Walls "), "WALLS");
|
||||||
|
assert_eq!(normalize_layer_name("walls"), "WALLS");
|
||||||
|
assert_eq!(normalize_layer_name("WALLS"), "WALLS");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_and_whitespace_names_are_invalid() {
|
||||||
|
assert_eq!(normalize_layer_name(""), "");
|
||||||
|
assert_eq!(normalize_layer_name(" "), "");
|
||||||
|
assert!(!is_valid_layer_name(""));
|
||||||
|
assert!(!is_valid_layer_name(" "));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ mod validation;
|
|||||||
|
|
||||||
pub mod envelope;
|
pub mod envelope;
|
||||||
pub mod facade;
|
pub mod facade;
|
||||||
|
pub mod layer;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
@@ -41,9 +42,14 @@ pub use envelope::{DocApiEnvelope, EnvelopeBody, OpOutcome, Receipt};
|
|||||||
pub use facade::{
|
pub use facade::{
|
||||||
ArcCurve, Circle, CurveCollection, Dimension, DocApi, Document, Ellipse, Entity,
|
ArcCurve, Circle, CurveCollection, Dimension, DocApi, Document, Ellipse, Entity,
|
||||||
EntityCollection, HasId, Line, MText, OpGroup, Point, Polyline, QueryBatch, QueryResults, Ray,
|
EntityCollection, HasId, Line, MText, OpGroup, Point, Polyline, QueryBatch, QueryResults, Ray,
|
||||||
Solid, SolidCollection, Spline, Text, XLine,
|
Solid, SolidCollection, Spline, Text, XLine, XRecord,
|
||||||
|
};
|
||||||
|
pub use layer::{is_valid_layer_name, normalize_layer_name};
|
||||||
|
pub use ops::{
|
||||||
|
BoolOp, Color, Curve2Spec, EntitySpec, LayerFlags, LayerInfo, LineWeight, Operation,
|
||||||
|
PlacementSpec, SolidPrimitive, XDataRecord, XDataValue, XRecordCloningFlags, XRecordEntry,
|
||||||
|
XRecordSpec, XRecordValue,
|
||||||
};
|
};
|
||||||
pub use ops::{BoolOp, Curve2Spec, EntitySpec, Operation, PlacementSpec, SolidPrimitive};
|
|
||||||
pub use query::{Aabb, EntityView, Query, QueryResult};
|
pub use query::{Aabb, EntityView, Query, QueryResult};
|
||||||
|
|
||||||
#[cfg(feature = "host")]
|
#[cfg(feature = "host")]
|
||||||
@@ -51,6 +57,9 @@ pub use convert::{
|
|||||||
bulge_arc_segment, curve_spec_to_entity, entity_bounds, entity_kind_name,
|
bulge_arc_segment, curve_spec_to_entity, entity_bounds, entity_kind_name,
|
||||||
entity_to_profile_curves, transform_entity_geometry,
|
entity_to_profile_curves, transform_entity_geometry,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "doc_api_host")]
|
||||||
|
pub use transport::ipc::doc_api_for_host;
|
||||||
|
|
||||||
pub use transport::Transport;
|
pub use transport::Transport;
|
||||||
|
|
||||||
/// The crate's own wire-envelope protocol version (`DocApiEnvelope::version`).
|
/// The crate's own wire-envelope protocol version (`DocApiEnvelope::version`).
|
||||||
|
|||||||
@@ -8,6 +8,80 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
pub use crate::gen::Operation;
|
pub use crate::gen::Operation;
|
||||||
|
|
||||||
|
/// An XDATA record payload: one registered application name plus its typed
|
||||||
|
/// value list. This is a plain-data, serde-compatible mirror of
|
||||||
|
/// `acadrust::xdata::ExtendedDataRecord` so the wire DTO stays independent of
|
||||||
|
/// the host dependency.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct XDataRecord {
|
||||||
|
pub application_name: String,
|
||||||
|
pub values: Vec<XDataValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single XDATA value. Mirrors `acadrust::xdata::XDataValue`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum XDataValue {
|
||||||
|
String(String),
|
||||||
|
ControlString(String),
|
||||||
|
LayerName(String),
|
||||||
|
BinaryData(Vec<u8>),
|
||||||
|
Handle(u64),
|
||||||
|
Point3D([f64; 3]),
|
||||||
|
Position3D([f64; 3]),
|
||||||
|
Displacement3D([f64; 3]),
|
||||||
|
Direction3D([f64; 3]),
|
||||||
|
Real(f64),
|
||||||
|
Distance(f64),
|
||||||
|
ScaleFactor(f64),
|
||||||
|
Integer16(i16),
|
||||||
|
Integer32(i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construction spec for an `XRECORD` object. Mirrors the acadrust
|
||||||
|
/// `XRecord` payload (name + cloning flags + group-code entries) in a
|
||||||
|
/// serde-compatible, host-independent form.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct XRecordSpec {
|
||||||
|
pub name: String,
|
||||||
|
pub cloning_flags: XRecordCloningFlags,
|
||||||
|
pub entries: Vec<XRecordEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// XRecord cloning behavior flags. Mirror of
|
||||||
|
/// `acadrust::objects::DictionaryCloningFlags`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub enum XRecordCloningFlags {
|
||||||
|
#[default]
|
||||||
|
NotApplicable,
|
||||||
|
KeepExisting,
|
||||||
|
UseClone,
|
||||||
|
XrefName,
|
||||||
|
Name,
|
||||||
|
UnmangleName,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One XRECORD group-code entry.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct XRecordEntry {
|
||||||
|
pub code: i32,
|
||||||
|
pub value: XRecordValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single XRECORD value. Mirrors `acadrust::objects::XRecordValue`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum XRecordValue {
|
||||||
|
String(String),
|
||||||
|
Double(f64),
|
||||||
|
Int16(i16),
|
||||||
|
Int32(i32),
|
||||||
|
Int64(i64),
|
||||||
|
Byte(u8),
|
||||||
|
Bool(bool),
|
||||||
|
Handle(u64),
|
||||||
|
Point3D([f64; 3]),
|
||||||
|
Chunk(Vec<u8>),
|
||||||
|
}
|
||||||
|
|
||||||
/// Boolean operation kind for [`Operation::SolidBoolean`].
|
/// Boolean operation kind for [`Operation::SolidBoolean`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum BoolOp {
|
pub enum BoolOp {
|
||||||
@@ -16,6 +90,72 @@ pub enum BoolOp {
|
|||||||
Difference,
|
Difference,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A plain-data color value. Mirrors `acadrust::types::Color` for wire
|
||||||
|
/// independence from the host dependency.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Color {
|
||||||
|
ByLayer,
|
||||||
|
None,
|
||||||
|
ByBlock,
|
||||||
|
Index(u8),
|
||||||
|
Rgb { r: u8, g: u8, b: u8 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
pub const WHITE: Color = Color::Index(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plain-data line weight. Mirrors `acadrust::types::LineWeight`.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub enum LineWeight {
|
||||||
|
#[default]
|
||||||
|
ByLayer,
|
||||||
|
ByBlock,
|
||||||
|
Default,
|
||||||
|
/// Specific line weight in 1/100 mm.
|
||||||
|
Value(i16),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Layer visibility/lock flags.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||||
|
pub struct LayerFlags {
|
||||||
|
pub frozen: bool,
|
||||||
|
pub locked: bool,
|
||||||
|
pub frozen_in_new_viewport: bool,
|
||||||
|
pub off: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plain-data layer table entry. Mirrors `acadrust::tables::Layer`.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct LayerInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub flags: LayerFlags,
|
||||||
|
pub color: Color,
|
||||||
|
pub line_type: String,
|
||||||
|
pub line_weight: LineWeight,
|
||||||
|
pub plot_style: String,
|
||||||
|
pub is_plottable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayerInfo {
|
||||||
|
pub fn new(name: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
flags: LayerFlags::default(),
|
||||||
|
color: Color::WHITE,
|
||||||
|
line_type: "Continuous".to_string(),
|
||||||
|
line_weight: LineWeight::Default,
|
||||||
|
plot_style: String::new(),
|
||||||
|
is_plottable: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The normalized (trimmed + uppercased) lookup key for this layer name.
|
||||||
|
pub fn normalized_name(&self) -> String {
|
||||||
|
crate::layer::normalize_layer_name(&self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Construction spec for a B-rep solid primitive (plain-data mirror of the
|
/// Construction spec for a B-rep solid primitive (plain-data mirror of the
|
||||||
/// non-serde `cadkernel::brep::make::*` arguments).
|
/// non-serde `cadkernel::brep::make::*` arguments).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
@@ -55,17 +195,25 @@ pub enum Curve2Spec {
|
|||||||
Line {
|
Line {
|
||||||
start: [f64; 3],
|
start: [f64; 3],
|
||||||
end: [f64; 3],
|
end: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
Circle {
|
Circle {
|
||||||
centre: [f64; 3],
|
centre: [f64; 3],
|
||||||
radius: f64,
|
radius: f64,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
Polyline {
|
Polyline {
|
||||||
points: Vec<[f64; 3]>,
|
points: Vec<[f64; 3]>,
|
||||||
closed: bool,
|
closed: bool,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
Point {
|
Point {
|
||||||
position: [f64; 3],
|
position: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// Circular arc, counter-clockwise from `start_angle` to `end_angle` (radians).
|
/// Circular arc, counter-clockwise from `start_angle` to `end_angle` (radians).
|
||||||
Arc {
|
Arc {
|
||||||
@@ -73,6 +221,8 @@ pub enum Curve2Spec {
|
|||||||
radius: f64,
|
radius: f64,
|
||||||
start_angle: f64,
|
start_angle: f64,
|
||||||
end_angle: f64,
|
end_angle: f64,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// Ellipse: `major_axis` is the major-axis endpoint relative to `centre`;
|
/// Ellipse: `major_axis` is the major-axis endpoint relative to `centre`;
|
||||||
/// `ratio` = minor/major. `start`/`end` parameters 0..2π (full ellipse = 0..2π).
|
/// `ratio` = minor/major. `start`/`end` parameters 0..2π (full ellipse = 0..2π).
|
||||||
@@ -82,6 +232,8 @@ pub enum Curve2Spec {
|
|||||||
ratio: f64,
|
ratio: f64,
|
||||||
start: f64,
|
start: f64,
|
||||||
end: f64,
|
end: f64,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// NURBS spline (control points + knots + weights; `degree` typically 3).
|
/// NURBS spline (control points + knots + weights; `degree` typically 3).
|
||||||
Spline {
|
Spline {
|
||||||
@@ -89,19 +241,42 @@ pub enum Curve2Spec {
|
|||||||
control_points: Vec<[f64; 3]>,
|
control_points: Vec<[f64; 3]>,
|
||||||
knots: Vec<f64>,
|
knots: Vec<f64>,
|
||||||
weights: Vec<f64>,
|
weights: Vec<f64>,
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// A ray from `origin` along `direction` (bounded at origin only).
|
/// A ray from `origin` along `direction` (bounded at origin only).
|
||||||
Ray {
|
Ray {
|
||||||
origin: [f64; 3],
|
origin: [f64; 3],
|
||||||
direction: [f64; 3],
|
direction: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
/// An infinite construction line through `origin` along `direction`.
|
/// An infinite construction line through `origin` along `direction`.
|
||||||
XLine {
|
XLine {
|
||||||
origin: [f64; 3],
|
origin: [f64; 3],
|
||||||
direction: [f64; 3],
|
direction: [f64; 3],
|
||||||
|
/// Target layer name. `None` places the entity on the current layer.
|
||||||
|
layer: Option<String>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Curve2Spec {
|
||||||
|
/// The target layer name, if any.
|
||||||
|
pub fn layer(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
Curve2Spec::Line { layer, .. }
|
||||||
|
| Curve2Spec::Circle { layer, .. }
|
||||||
|
| Curve2Spec::Polyline { layer, .. }
|
||||||
|
| Curve2Spec::Point { layer, .. }
|
||||||
|
| Curve2Spec::Arc { layer, .. }
|
||||||
|
| Curve2Spec::Ellipse { layer, .. }
|
||||||
|
| Curve2Spec::Spline { layer, .. }
|
||||||
|
| Curve2Spec::Ray { layer, .. }
|
||||||
|
| Curve2Spec::XLine { layer, .. } => layer.as_deref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A generic entity-construction payload used by the bulk op
|
/// A generic entity-construction payload used by the bulk op
|
||||||
/// [`crate::ops::Operation::CreateMany`]. Mirrors the per-family construction
|
/// [`crate::ops::Operation::CreateMany`]. Mirrors the per-family construction
|
||||||
/// specs so one bulk op can carry many homogeneous (or mixed) creations.
|
/// specs so one bulk op can carry many homogeneous (or mixed) creations.
|
||||||
@@ -111,6 +286,16 @@ pub enum EntitySpec {
|
|||||||
Solid(SolidPrimitive),
|
Solid(SolidPrimitive),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EntitySpec {
|
||||||
|
/// The target layer name for this spec, if explicitly set.
|
||||||
|
pub fn layer(&self) -> Option<&str> {
|
||||||
|
match self {
|
||||||
|
EntitySpec::Curve(c) => c.layer(),
|
||||||
|
EntitySpec::Solid(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Construction spec for a block reference (`INSERT`): place the block
|
/// Construction spec for a block reference (`INSERT`): place the block
|
||||||
/// `block_name` (must exist as a BlockRecord) at `insert_point` with uniform
|
/// `block_name` (must exist as a BlockRecord) at `insert_point` with uniform
|
||||||
/// `scale` and `rotation` (radians, about Z).
|
/// `scale` and `rotation` (radians, about Z).
|
||||||
@@ -291,6 +476,14 @@ impl crate::gen::Operation {
|
|||||||
CreateAttributeDefinition(_) => "CreateAttributeDefinition",
|
CreateAttributeDefinition(_) => "CreateAttributeDefinition",
|
||||||
CreateTable(_) => "CreateTable",
|
CreateTable(_) => "CreateTable",
|
||||||
CreateDimensionAngular2Ln(_) => "CreateDimensionAngular2Ln",
|
CreateDimensionAngular2Ln(_) => "CreateDimensionAngular2Ln",
|
||||||
|
SetXData { .. } => "SetXData",
|
||||||
|
SetXDataMany(_) => "SetXDataMany",
|
||||||
|
CreateXRecord(_) => "CreateXRecord",
|
||||||
|
SetXRecord { .. } => "SetXRecord",
|
||||||
|
CreateLayer(_) => "CreateLayer",
|
||||||
|
UpdateLayer { .. } => "UpdateLayer",
|
||||||
|
DeleteLayer { .. } => "DeleteLayer",
|
||||||
|
SetEntityLayer { .. } => "SetEntityLayer",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,8 @@ pub struct EntityView {
|
|||||||
/// The acadrust `EntityType` variant name (e.g. "Solid3D", "Line").
|
/// The acadrust `EntityType` variant name (e.g. "Solid3D", "Line").
|
||||||
pub kind: String,
|
pub kind: String,
|
||||||
pub bounds: Option<Aabb>,
|
pub bounds: Option<Aabb>,
|
||||||
|
/// The normalized (uppercase) layer name the entity is on.
|
||||||
|
pub layer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The result of a single [`Query`] (one per query in a `Queries` batch).
|
/// The result of a single [`Query`] (one per query in a `Queries` batch).
|
||||||
@@ -68,6 +70,20 @@ pub enum QueryResult {
|
|||||||
target: [f64; 3],
|
target: [f64; 3],
|
||||||
height: f64,
|
height: f64,
|
||||||
},
|
},
|
||||||
|
/// An XDATA record for a single application (`None` if no such record).
|
||||||
|
XData(Option<crate::ops::XDataRecord>),
|
||||||
|
/// An XRECORD object payload.
|
||||||
|
XRecord(crate::ops::XRecordSpec),
|
||||||
|
/// All layers in the document.
|
||||||
|
Layers(Vec<crate::ops::LayerInfo>),
|
||||||
|
/// The layer name of an entity.
|
||||||
|
EntityLayer(String),
|
||||||
|
/// A filtered list of entity views.
|
||||||
|
Entities(Vec<EntityView>),
|
||||||
|
/// The WCS location of a Point entity.
|
||||||
|
PointPosition([f64; 3]),
|
||||||
|
/// The start and end WCS points of a Line entity.
|
||||||
|
LineGeometry(([f64; 3], [f64; 3])),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: the query name for diagnostics.
|
/// Convenience: the query name for diagnostics.
|
||||||
@@ -87,6 +103,13 @@ impl crate::gen::Query {
|
|||||||
GetAttributes { .. } => "GetAttributes",
|
GetAttributes { .. } => "GetAttributes",
|
||||||
GetBlockEntities { .. } => "GetBlockEntities",
|
GetBlockEntities { .. } => "GetBlockEntities",
|
||||||
GetViewportView { .. } => "GetViewportView",
|
GetViewportView { .. } => "GetViewportView",
|
||||||
|
GetXData { .. } => "GetXData",
|
||||||
|
GetXRecord { .. } => "GetXRecord",
|
||||||
|
ListLayers => "ListLayers",
|
||||||
|
GetEntityLayer { .. } => "GetEntityLayer",
|
||||||
|
EnumerateEntities { .. } => "EnumerateEntities",
|
||||||
|
GetPointPosition { .. } => "GetPointPosition",
|
||||||
|
GetLineGeometry { .. } => "GetLineGeometry",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,11 +5,12 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use ocs_plugin_api::host::{PluginRequestError, PluginRequestSender};
|
use ocs_plugin_api::host::{HostApi, PluginRequestError, PluginRequestSender};
|
||||||
use ocs_plugin_api::ipc::protocol::{PluginRequest, PluginResponse};
|
use ocs_plugin_api::ipc::protocol::{PluginRequest, PluginResponse};
|
||||||
|
|
||||||
use crate::envelope::{DocApiEnvelope, Receipt};
|
use crate::envelope::{DocApiEnvelope, Receipt};
|
||||||
use crate::error::{ApiError, ApiResult};
|
use crate::error::{ApiError, ApiResult};
|
||||||
|
use crate::facade::DocApi;
|
||||||
use crate::transport::Transport;
|
use crate::transport::Transport;
|
||||||
|
|
||||||
/// Out-of-process IPC transport over the `ocs_plugin_api` channel. `Send + Sync`;
|
/// Out-of-process IPC transport over the `ocs_plugin_api` channel. `Send + Sync`;
|
||||||
@@ -29,6 +30,21 @@ impl OcsPluginApiIpc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a typed [`DocApi`] from any host that can provide a
|
||||||
|
/// [`PluginRequestSender`] (out-of-process / V4 worker plugins).
|
||||||
|
///
|
||||||
|
/// Returns `None` for hosts that do not expose a request sender (e.g. in-process
|
||||||
|
/// hosts). In-process code should use [`crate::DocApi::in_process`] with a
|
||||||
|
/// concrete [`crate::backend::DocApiBackend`].
|
||||||
|
pub fn doc_api_for_host(host: &dyn HostApi) -> Option<DocApi> {
|
||||||
|
let sender = host.plugin_request_sender()?;
|
||||||
|
let tab_id = host.tab_id();
|
||||||
|
Some(DocApi::connect(
|
||||||
|
Arc::new(OcsPluginApiIpc::new(sender.into(), tab_id)),
|
||||||
|
tab_id,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
impl Transport for OcsPluginApiIpc {
|
impl Transport for OcsPluginApiIpc {
|
||||||
fn apply(&self, req: DocApiEnvelope) -> ApiResult<Receipt> {
|
fn apply(&self, req: DocApiEnvelope) -> ApiResult<Receipt> {
|
||||||
let bytes = bincode::serialize(&req)
|
let bytes = bincode::serialize(&req)
|
||||||
@@ -56,3 +72,128 @@ impl Transport for OcsPluginApiIpc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::facade::HasId;
|
||||||
|
use crate::ObjectId;
|
||||||
|
use ocs_plugin_api::host::{
|
||||||
|
CadDocument, EntityType, ExtendedDataRecord, Handle, HostApi,
|
||||||
|
};
|
||||||
|
use ocs_plugin_api::ipc::protocol::{PluginRequest, PluginResponse};
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MockSender {
|
||||||
|
captured: Arc<Mutex<Vec<(u64, PluginRequest)>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginRequestSender for MockSender {
|
||||||
|
fn request(&self, req: PluginRequest) -> Result<PluginResponse, PluginRequestError> {
|
||||||
|
let tab_id = match &req {
|
||||||
|
PluginRequest::DocApiRequest { tab_id, .. } => *tab_id,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
self.captured.lock().unwrap().push((tab_id, req));
|
||||||
|
Ok(PluginResponse::DocApiResponse {
|
||||||
|
bytes: bincode::serialize(&crate::ApiResult::Ok(Receipt {
|
||||||
|
outcome: Some(crate::envelope::OpOutcome::NewId(ObjectId::from_u64(7))),
|
||||||
|
query_results: vec![],
|
||||||
|
new_revision: crate::revision::GeometryRevision(1),
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MockHost {
|
||||||
|
sender: Option<MockSender>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostApi for MockHost {
|
||||||
|
fn tab_index(&self) -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
fn document(&self) -> &CadDocument {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn document_mut(&mut self) -> &mut CadDocument {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn add_entity(&mut self, _entity: EntityType) -> Handle {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn bump_geometry(&mut self) {}
|
||||||
|
fn push_undo(&mut self, _label: &str) {}
|
||||||
|
fn set_dirty(&mut self) {}
|
||||||
|
fn push_info(&mut self, _msg: &str) {}
|
||||||
|
fn push_output(&mut self, _msg: &str) {}
|
||||||
|
fn push_error(&mut self, _msg: &str) {}
|
||||||
|
fn start_interactive(&mut self, _command: Box<dyn ocs_plugin_api::host::InteractiveCommand>) {}
|
||||||
|
fn read_record(&self, _handle: Handle, _app_name: &str) -> Option<&ExtendedDataRecord> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn write_record(&mut self, _handle: Handle, _record: ExtendedDataRecord) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn remove_record(&mut self, _handle: Handle, _app_name: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn plugin_state_any(&self, _plugin_id: &str) -> Option<&(dyn Any + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn plugin_state_any_mut(&mut self, _plugin_id: &str) -> Option<&mut (dyn Any + Send + Sync)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn ensure_plugin_state_any(
|
||||||
|
&mut self,
|
||||||
|
_plugin_id: &'static str,
|
||||||
|
_init: &mut dyn FnMut() -> Box<dyn Any + Send + Sync>,
|
||||||
|
) -> &mut (dyn Any + Send + Sync) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn document_reader(&self) -> Box<dyn ocs_plugin_api::host::DocumentReader + '_> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn plugin_request_sender(&self) -> Option<Box<dyn PluginRequestSender>> {
|
||||||
|
self.sender.as_ref().map(|s| Box::new(s.clone()) as Box<dyn PluginRequestSender>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doc_api_for_host_builds_doc_api_and_routes_doc_api_request() {
|
||||||
|
let captured = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let host = MockHost {
|
||||||
|
sender: Some(MockSender {
|
||||||
|
captured: Arc::clone(&captured),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let doc_api = doc_api_for_host(&host).expect("host exposes a sender");
|
||||||
|
let doc = doc_api.document(host.tab_id());
|
||||||
|
let id = doc
|
||||||
|
.solids()
|
||||||
|
.create_cuboid([0.0; 3], [1.0; 3])
|
||||||
|
.expect("mock returns a NewId receipt");
|
||||||
|
assert_eq!(id.id(), ObjectId::from_u64(7));
|
||||||
|
|
||||||
|
let calls = captured.lock().unwrap();
|
||||||
|
assert_eq!(calls.len(), 1);
|
||||||
|
let (tab_id, ref req) = &calls[0];
|
||||||
|
assert_eq!(*tab_id, host.tab_id());
|
||||||
|
match req {
|
||||||
|
PluginRequest::DocApiRequest { tab_id, bytes } => {
|
||||||
|
assert_eq!(*tab_id, host.tab_id());
|
||||||
|
assert!(!bytes.is_empty());
|
||||||
|
}
|
||||||
|
other => panic!("expected DocApiRequest, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn doc_api_for_host_returns_none_without_sender() {
|
||||||
|
let host = MockHost { sender: None };
|
||||||
|
assert!(doc_api_for_host(&host).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Reject invalid numeric inputs before the backend starts an undo operation.
|
//! Reject invalid numeric inputs before the backend starts an undo operation.
|
||||||
use crate::{ops::*, ApiError, ApiResult};
|
use crate::{id::ObjectId, ops::*, ApiError, ApiResult};
|
||||||
|
|
||||||
fn finite(values: &[f64]) -> bool {
|
fn finite(values: &[f64]) -> bool {
|
||||||
values.iter().all(|v| v.is_finite())
|
values.iter().all(|v| v.is_finite())
|
||||||
@@ -24,16 +24,17 @@ fn check(valid: bool, op: &'static str) -> ApiResult<()> {
|
|||||||
pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
||||||
use Curve2Spec::*;
|
use Curve2Spec::*;
|
||||||
let valid = match spec {
|
let valid = match spec {
|
||||||
Line { start, end } => finite(start) && finite(end) && start != end,
|
Line { start, end, .. } => finite(start) && finite(end) && start != end,
|
||||||
Circle { centre, radius } => finite(centre) && positive(*radius),
|
Circle { centre, radius, .. } => finite(centre) && positive(*radius),
|
||||||
Point { position } => finite(position),
|
Point { position, .. } => finite(position),
|
||||||
Arc {
|
Arc {
|
||||||
centre,
|
centre,
|
||||||
radius,
|
radius,
|
||||||
start_angle,
|
start_angle,
|
||||||
end_angle,
|
end_angle,
|
||||||
|
..
|
||||||
} => finite(centre) && positive(*radius) && finite(&[*start_angle, *end_angle]),
|
} => finite(centre) && positive(*radius) && finite(&[*start_angle, *end_angle]),
|
||||||
Polyline { points, closed } => {
|
Polyline { points, closed, .. } => {
|
||||||
points.len() >= if *closed { 3 } else { 2 }
|
points.len() >= if *closed { 3 } else { 2 }
|
||||||
&& points.len() <= BULK_ITEM_CAP
|
&& points.len() <= BULK_ITEM_CAP
|
||||||
&& points.iter().all(|p| finite(p) && p[2] == points[0][2])
|
&& points.iter().all(|p| finite(p) && p[2] == points[0][2])
|
||||||
@@ -44,6 +45,7 @@ pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
|||||||
ratio,
|
ratio,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
finite(centre)
|
finite(centre)
|
||||||
&& direction(major_axis)
|
&& direction(major_axis)
|
||||||
@@ -56,6 +58,7 @@ pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
|||||||
control_points,
|
control_points,
|
||||||
knots,
|
knots,
|
||||||
weights,
|
weights,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
*degree > 0
|
*degree > 0
|
||||||
&& control_points.len() <= BULK_ITEM_CAP
|
&& control_points.len() <= BULK_ITEM_CAP
|
||||||
@@ -76,10 +79,12 @@ pub(crate) fn curve(spec: &Curve2Spec) -> ApiResult<()> {
|
|||||||
Ray {
|
Ray {
|
||||||
origin,
|
origin,
|
||||||
direction: d,
|
direction: d,
|
||||||
|
..
|
||||||
}
|
}
|
||||||
| XLine {
|
| XLine {
|
||||||
origin,
|
origin,
|
||||||
direction: d,
|
direction: d,
|
||||||
|
..
|
||||||
} => finite(origin) && direction(d),
|
} => finite(origin) && direction(d),
|
||||||
};
|
};
|
||||||
check(valid, "CreateCurve")
|
check(valid, "CreateCurve")
|
||||||
@@ -111,6 +116,11 @@ pub(crate) fn operation(op: &Operation) -> ApiResult<()> {
|
|||||||
}
|
}
|
||||||
CreateText(s) => finite(&s.insertion_point) && positive(s.height) && s.rotation.is_finite(),
|
CreateText(s) => finite(&s.insertion_point) && positive(s.height) && s.rotation.is_finite(),
|
||||||
CreateMText(s) => finite(&s.insertion_point) && positive(s.height),
|
CreateMText(s) => finite(&s.insertion_point) && positive(s.height),
|
||||||
|
SetXDataMany(records) => records.iter().all(|(id, app, record)| {
|
||||||
|
let app_ok = !app.is_empty();
|
||||||
|
let record_ok = record.as_ref().map_or(true, |r| !r.application_name.is_empty() && !r.application_name.contains('\0'));
|
||||||
|
*id != ObjectId::NULL && app_ok && record_ok
|
||||||
|
}),
|
||||||
CreateHatch(s) => {
|
CreateHatch(s) => {
|
||||||
(3..=BULK_ITEM_CAP).contains(&s.boundary.len()) && s.boundary.iter().all(|p| finite(p))
|
(3..=BULK_ITEM_CAP).contains(&s.boundary.len()) && s.boundary.iter().all(|p| finite(p))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
use ocs_doc_api::backend::{DocApiBackend, KernelBody};
|
||||||
use ocs_doc_api::{
|
use ocs_doc_api::{
|
||||||
Aabb, ApiError, ApiResult, Curve2Spec, DocApi, EntityView, GeometryErrorKind, GeometryRevision,
|
normalize_layer_name, Aabb, ApiError, ApiResult, Color, Curve2Spec, DocApi, EntityView,
|
||||||
HasId, ObjectId, PlacementSpec,
|
GeometryErrorKind, GeometryRevision, HasId, LayerInfo, LineWeight, ObjectId, PlacementSpec,
|
||||||
|
XDataRecord, XDataValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Coarse bounds for a stored 2D curve spec (mirrors the host's entity_bounds and
|
/// 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).
|
/// the example mock — all three must answer curve bounds identically).
|
||||||
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
||||||
match spec {
|
match spec {
|
||||||
Curve2Spec::Line { start, end } => Aabb {
|
Curve2Spec::Line { start, end, .. } => Aabb {
|
||||||
min: [
|
min: [
|
||||||
start[0].min(end[0]),
|
start[0].min(end[0]),
|
||||||
start[1].min(end[1]),
|
start[1].min(end[1]),
|
||||||
@@ -28,11 +29,12 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
|||||||
start[2].max(end[2]),
|
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]],
|
min: [centre[0] - radius, centre[1] - radius, centre[2]],
|
||||||
max: [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,
|
min: *position,
|
||||||
max: *position,
|
max: *position,
|
||||||
},
|
},
|
||||||
@@ -72,13 +74,17 @@ fn curve_bounds(spec: &Curve2Spec) -> Aabb {
|
|||||||
|
|
||||||
/// In-memory mock backend: HashMap<ObjectId, Body> + a tiny entity table.
|
/// In-memory mock backend: HashMap<ObjectId, Body> + a tiny entity table.
|
||||||
/// Tracks undo steps + revision bumps to assert per-op semantics.
|
/// Tracks undo steps + revision bumps to assert per-op semantics.
|
||||||
#[derive(Default, Clone)]
|
#[derive(Clone)]
|
||||||
struct MockBackend {
|
struct MockBackend {
|
||||||
next_id: u64,
|
next_id: u64,
|
||||||
revision: u64,
|
revision: u64,
|
||||||
undo_steps: u32,
|
undo_steps: u32,
|
||||||
bodies: HashMap<ObjectId, KernelBody>,
|
bodies: HashMap<ObjectId, KernelBody>,
|
||||||
kinds: HashMap<ObjectId, String>,
|
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.
|
/// Stored text content for Text/MText annotations.
|
||||||
text_values: HashMap<ObjectId, String>,
|
text_values: HashMap<ObjectId, String>,
|
||||||
/// Stored hatch boundary loops.
|
/// Stored hatch boundary loops.
|
||||||
@@ -92,6 +98,36 @@ struct MockBackend {
|
|||||||
curves: HashMap<ObjectId, Curve2Spec>,
|
curves: HashMap<ObjectId, Curve2Spec>,
|
||||||
/// Stored viewport views (target, height).
|
/// Stored viewport views (target, height).
|
||||||
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
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 {
|
impl MockBackend {
|
||||||
@@ -104,6 +140,22 @@ impl MockBackend {
|
|||||||
fn body(&self, id: ObjectId) -> ApiResult<&KernelBody> {
|
fn body(&self, id: ObjectId) -> ApiResult<&KernelBody> {
|
||||||
self.bodies.get(&id).ok_or(ApiError::UnknownId(id))
|
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 {
|
impl DocApiBackend for MockBackend {
|
||||||
@@ -113,6 +165,7 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn store_solid(&mut self, body: &KernelBody) -> ApiResult<ObjectId> {
|
fn store_solid(&mut self, body: &KernelBody) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Solid3D");
|
let id = self.alloc("Solid3D");
|
||||||
self.bodies.insert(id, body.clone());
|
self.bodies.insert(id, body.clone());
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn update_solid(&mut self, id: ObjectId, body: &KernelBody) -> ApiResult<()> {
|
fn update_solid(&mut self, id: ObjectId, body: &KernelBody) -> ApiResult<()> {
|
||||||
@@ -122,6 +175,15 @@ impl DocApiBackend for MockBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn create_many(&mut self, specs: &[ocs_doc_api::EntitySpec]) -> ApiResult<Vec<ObjectId>> {
|
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 mut next = self.clone();
|
||||||
let ids = specs
|
let ids = specs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -151,26 +213,35 @@ impl DocApiBackend for MockBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
fn add_curve(&mut self, spec: &Curve2Spec) -> ApiResult<ObjectId> {
|
||||||
let kind = match spec {
|
let layer = spec
|
||||||
Curve2Spec::Line { .. } => "Line",
|
.layer()
|
||||||
Curve2Spec::Circle { .. } => "Circle",
|
.map(|name| {
|
||||||
Curve2Spec::Polyline { .. } => "LwPolyline",
|
self.lookup_layer(name).map(|info| info.normalized_name())
|
||||||
Curve2Spec::Point { .. } => "Point",
|
})
|
||||||
Curve2Spec::Arc { .. } => "Arc",
|
.transpose()?;
|
||||||
Curve2Spec::Ellipse { .. } => "Ellipse",
|
let id = self.add_curve_only(spec)?;
|
||||||
Curve2Spec::Spline { .. } => "Spline",
|
self.entity_layers
|
||||||
Curve2Spec::Ray { .. } => "Ray",
|
.insert(id, layer.unwrap_or_else(|| "0".to_string()));
|
||||||
Curve2Spec::XLine { .. } => "XLine",
|
|
||||||
};
|
|
||||||
let id = self.alloc(kind);
|
|
||||||
self.curves.insert(id, spec.clone());
|
|
||||||
Ok(id)
|
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> {
|
fn add_insert(&mut self, spec: &ocs_doc_api::ops::InsertSpec) -> ApiResult<ObjectId> {
|
||||||
if spec.block_name.is_empty() {
|
if spec.block_name.is_empty() {
|
||||||
return Err(ApiError::validation("CreateInsert", "empty block name"));
|
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> {
|
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Viewport");
|
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> {
|
fn add_text(&mut self, spec: &ocs_doc_api::ops::TextSpec) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Text");
|
let id = self.alloc("Text");
|
||||||
self.text_values.insert(id, spec.value.clone());
|
self.text_values.insert(id, spec.value.clone());
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_mtext(&mut self, spec: &ocs_doc_api::ops::MTextSpec) -> ApiResult<ObjectId> {
|
fn add_mtext(&mut self, spec: &ocs_doc_api::ops::MTextSpec) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("MText");
|
let id = self.alloc("MText");
|
||||||
self.text_values.insert(id, spec.value.clone());
|
self.text_values.insert(id, spec.value.clone());
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn set_text_content(&mut self, id: ObjectId, value: &str) -> ApiResult<()> {
|
fn set_text_content(&mut self, id: ObjectId, value: &str) -> ApiResult<()> {
|
||||||
@@ -206,6 +279,7 @@ impl DocApiBackend for MockBackend {
|
|||||||
let id = self.alloc("Hatch");
|
let id = self.alloc("Hatch");
|
||||||
self.hatch_boundaries
|
self.hatch_boundaries
|
||||||
.insert(id, vec![spec.boundary.clone()]);
|
.insert(id, vec![spec.boundary.clone()]);
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
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))
|
+ (spec.first_point[2] - spec.second_point[2]).powi(2))
|
||||||
.sqrt();
|
.sqrt();
|
||||||
self.dimension_measurements.insert(id, d);
|
self.dimension_measurements.insert(id, d);
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_dimension_radius(
|
fn add_dimension_radius(
|
||||||
@@ -236,13 +311,16 @@ impl DocApiBackend for MockBackend {
|
|||||||
+ (spec.center[2] - spec.point[2]).powi(2))
|
+ (spec.center[2] - spec.point[2]).powi(2))
|
||||||
.sqrt();
|
.sqrt();
|
||||||
self.dimension_measurements.insert(id, d);
|
self.dimension_measurements.insert(id, d);
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn add_dimension_diameter(
|
fn add_dimension_diameter(
|
||||||
&mut self,
|
&mut self,
|
||||||
spec: &ocs_doc_api::ops::DimensionRadialSpec,
|
spec: &ocs_doc_api::ops::DimensionRadialSpec,
|
||||||
) -> ApiResult<ObjectId> {
|
) -> 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(
|
fn add_dimension_angular(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -250,6 +328,7 @@ impl DocApiBackend for MockBackend {
|
|||||||
) -> ApiResult<ObjectId> {
|
) -> ApiResult<ObjectId> {
|
||||||
let id = self.alloc("Dimension");
|
let id = self.alloc("Dimension");
|
||||||
self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees
|
self.dimension_measurements.insert(id, 90.0); // mock: 90 degrees
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64> {
|
||||||
@@ -286,7 +365,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
"table needs a non-empty rectangular grid",
|
"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<()> {
|
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||||
if !self.entity_exists(id) {
|
if !self.entity_exists(id) {
|
||||||
@@ -313,7 +394,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
&mut self,
|
&mut self,
|
||||||
_spec: &ocs_doc_api::ops::RasterImageSpec,
|
_spec: &ocs_doc_api::ops::RasterImageSpec,
|
||||||
) -> ApiResult<ObjectId> {
|
) -> ApiResult<ObjectId> {
|
||||||
Ok(self.alloc("RasterImage"))
|
let id = self.alloc("RasterImage");
|
||||||
|
self.entity_layers.insert(id, "0".to_string());
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
fn loft(
|
fn loft(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -345,6 +428,203 @@ impl DocApiBackend for MockBackend {
|
|||||||
.copied()
|
.copied()
|
||||||
.ok_or(ApiError::UnknownId(id))
|
.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<()> {
|
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||||
if self.entity_exists(id) {
|
if self.entity_exists(id) {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -354,6 +634,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
}
|
}
|
||||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
||||||
self.bodies.remove(&id);
|
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())
|
Ok(self.kinds.remove(&id).is_some())
|
||||||
}
|
}
|
||||||
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
fn get_entity(&mut self, id: ObjectId) -> ApiResult<EntityView> {
|
||||||
@@ -364,7 +647,17 @@ impl DocApiBackend for MockBackend {
|
|||||||
max: bb.max,
|
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<()> {
|
fn transform_entity(&mut self, id: ObjectId, placement: &PlacementSpec) -> ApiResult<()> {
|
||||||
if !self.entity_exists(id) {
|
if !self.entity_exists(id) {
|
||||||
@@ -412,6 +705,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn entity_exists(&self, id: ObjectId) -> bool {
|
fn entity_exists(&self, id: ObjectId) -> bool {
|
||||||
self.kinds.contains_key(&id)
|
self.kinds.contains_key(&id)
|
||||||
}
|
}
|
||||||
|
fn object_exists(&self, id: ObjectId) -> bool {
|
||||||
|
self.xrecord_store.contains_key(&id)
|
||||||
|
}
|
||||||
fn revision(&self) -> GeometryRevision {
|
fn revision(&self) -> GeometryRevision {
|
||||||
GeometryRevision(self.revision)
|
GeometryRevision(self.revision)
|
||||||
}
|
}
|
||||||
@@ -609,3 +905,145 @@ fn typed_handles_reject_foreign_sessions_and_tabs() {
|
|||||||
assert!(group.compensate(&doc).is_err());
|
assert!(group.compensate(&doc).is_err());
|
||||||
assert!(first.document(11).curves().create_point([0.0; 3]).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.
|
/// The variant names of the `Operation` enum, in declaration order.
|
||||||
fn operation_variants() -> Vec<String> {
|
fn operation_variants() -> Vec<String> {
|
||||||
use ocs_doc_api::ops::{
|
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;
|
use ocs_doc_api::ObjectId;
|
||||||
let _marker: Vec<Operation> = vec![
|
let _marker: Vec<Operation> = vec![
|
||||||
@@ -46,7 +47,10 @@ fn operation_variants() -> Vec<String> {
|
|||||||
centre: [0.0; 3],
|
centre: [0.0; 3],
|
||||||
radius: 1.0,
|
radius: 1.0,
|
||||||
}),
|
}),
|
||||||
Operation::CreateCurve(Curve2Spec::Point { position: [0.0; 3] }),
|
Operation::CreateCurve(Curve2Spec::Point {
|
||||||
|
position: [0.0; 3],
|
||||||
|
layer: None,
|
||||||
|
}),
|
||||||
Operation::Extrude {
|
Operation::Extrude {
|
||||||
profile: ObjectId::from_u64(0),
|
profile: ObjectId::from_u64(0),
|
||||||
direction: [0.0; 3],
|
direction: [0.0; 3],
|
||||||
@@ -167,6 +171,37 @@ fn operation_variants() -> Vec<String> {
|
|||||||
second_point: [0.0; 3],
|
second_point: [0.0; 3],
|
||||||
arc_location: [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
|
_marker
|
||||||
.iter()
|
.iter()
|
||||||
@@ -178,7 +213,7 @@ fn operation_variants() -> Vec<String> {
|
|||||||
index
|
index
|
||||||
);
|
);
|
||||||
let dbg = format!("{:?}", op);
|
let dbg = format!("{:?}", op);
|
||||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
dbg.split(['(', ' ', '{'])
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string()
|
.to_string()
|
||||||
@@ -226,6 +261,17 @@ fn query_variants() -> Vec<String> {
|
|||||||
Query::GetViewportView {
|
Query::GetViewportView {
|
||||||
id: ObjectId::from_u64(0),
|
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
|
marker
|
||||||
.iter()
|
.iter()
|
||||||
@@ -237,7 +283,7 @@ fn query_variants() -> Vec<String> {
|
|||||||
index
|
index
|
||||||
);
|
);
|
||||||
let dbg = format!("{:?}", q);
|
let dbg = format!("{:?}", q);
|
||||||
dbg.split(|c: char| c == '(' || c == ' ' || c == '{')
|
dbg.split(['(', ' ', '{'])
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string()
|
.to_string()
|
||||||
@@ -309,6 +355,14 @@ const OPERATION_BASELINE: &[&str] = &[
|
|||||||
"CreateAttributeDefinition",
|
"CreateAttributeDefinition",
|
||||||
"CreateTable",
|
"CreateTable",
|
||||||
"CreateDimensionAngular2Ln",
|
"CreateDimensionAngular2Ln",
|
||||||
|
"SetXData",
|
||||||
|
"SetXDataMany",
|
||||||
|
"CreateXRecord",
|
||||||
|
"SetXRecord",
|
||||||
|
"CreateLayer",
|
||||||
|
"UpdateLayer",
|
||||||
|
"DeleteLayer",
|
||||||
|
"SetEntityLayer",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Recorded baseline of the `Query` variant order.
|
/// Recorded baseline of the `Query` variant order.
|
||||||
@@ -325,6 +379,10 @@ const QUERY_BASELINE: &[&str] = &[
|
|||||||
"GetAttributes",
|
"GetAttributes",
|
||||||
"GetBlockEntities",
|
"GetBlockEntities",
|
||||||
"GetViewportView",
|
"GetViewportView",
|
||||||
|
"GetXData",
|
||||||
|
"GetXRecord",
|
||||||
|
"ListLayers",
|
||||||
|
"GetEntityLayer",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+960
-96
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user