Merge PR #1028: add document API with atomic edits

Add the typed document facade and plugin IPC adapter. Prepare bulk edits
before mutation, preserve undo and dirty state, keep profile elevations,
validate numeric inputs and session bindings, and generate Python constructors
with typed results.

Reuse existing entity transforms and profile conversion. Compute mass
properties and closed-surface bounds in cadkernel; pin its main revision
6a5e6fc2822f6af6b2611cd605b44437b35158f0. Keep plugin API metadata at version 6.

Validation: 646 native application tests, 597 kernel tests, 12 document API
tests, 91 plugin API tests, 3 Python tests, default/IPC feature builds and
wasm32 check passed.
This commit is contained in:
Hakan Seven committed 2026-09-06 21:43:02 +03:00
commit 3385160493
50 files changed
+11481 -6

No files matched your search

+323
View File
@@ -0,0 +1,323 @@
# ocs_doc_api — API reference (auto-generated)
Envelope version: `1`
Do NOT edit: regenerated by `build.rs` from `spec/entities.toml`.
## `Solid` (acadrust `Solid3D`, collection `solids`)
B-rep solid. Host resolves bodies via the solid_models cache (lift-on-miss from ACIS SAT).
### Constructors (`doc.solids()`)
- **create_cuboid**(origin: [f64; 3], size: [f64; 3]) -> `Solid` — `CreateSolid`; fixed primitive="Cuboid"
- **create_sphere**(centre: [f64; 3], radius: f64) -> `Solid` — `CreateSolid`; fixed primitive="Sphere"
- **create_cylinder**(base: [f64; 3], radius: f64, height: f64) -> `Solid` — `CreateSolid`; fixed primitive="Cylinder"
- **create_cone**(base: [f64; 3], radius: f64, height: f64) -> `Solid` — `CreateSolid`; fixed primitive="Cone"
- **create_torus**(centre: [f64; 3], major_radius: f64, minor_radius: f64) -> `Solid` — `CreateSolid`; fixed primitive="Torus"
- **create_wedge**(origin: [f64; 3], size: [f64; 3]) -> `Solid` — `CreateSolid`; fixed primitive="Wedge"
- **extrude**(profile: EntityRef, direction: [f64; 3]) -> `Solid` — `Extrude`
- **revolve**(profile: EntityRef, pivot: [f64; 3], axis: [f64; 3], angle: f64) -> `Solid` — `Revolve`
- **loft**(profiles: &[EntityRef]) -> `Solid` — `Loft`
### Methods
- **intersect**(other: Solid) -> `Solid` — op `SolidBoolean`; fixed erase_sources=true, op="Intersection"
- **union**(other: Solid) -> `Solid` — op `SolidBoolean`; fixed erase_sources=true, op="Union"
- **subtract**(other: Solid) -> `Solid` — op `SolidBoolean`; fixed erase_sources=true, op="Difference"
- **volume**() -> `f64` — query `GetVolume`
- **centroid**() -> `[f64; 3]` — query `GetCentroid`
- **intersects**(other: Solid) -> `bool` — query `GetIntersects`
## `Line` (acadrust `Line`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_line**(start: [f64; 3], end: [f64; 3]) -> `Line` — `CreateCurve`; fixed curve="Line"
## `Circle` (acadrust `Circle`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_circle**(centre: [f64; 3], radius: f64) -> `Circle` — `CreateCurve`; fixed curve="Circle"
## `Polyline` (acadrust `LwPolyline`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_polyline**(points: &[[f64; 3]], closed: bool) -> `Polyline` — `CreateCurve`; fixed curve="Polyline"
### Methods
- **add_vertex**(at: usize, point: [f64; 3]) -> `()` — op `AddVertex`
## `Point` (acadrust `Point`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_point**(position: [f64; 3]) -> `Point` — `CreateCurve`; fixed curve="Point"
- **create_points**(positions: &[[f64; 3]]) -> `Vec<Point>` *(bulk op)* — `CreateMany`; fixed curve="Point"
## `ArcCurve` (acadrust `Arc`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_arc**(centre: [f64; 3], radius: f64, start_angle: f64, end_angle: f64) -> `ArcCurve` — `CreateCurve`; fixed curve="Arc"
## `Ellipse` (acadrust `Ellipse`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_ellipse**(centre: [f64; 3], major_axis: [f64; 3], ratio: f64, start: f64, end: f64) -> `Ellipse` — `CreateCurve`; fixed curve="Ellipse"
## `Spline` (acadrust `Spline`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_spline**(degree: i32, control_points: &[[f64; 3]], knots: &[f64], weights: &[f64]) -> `Spline` — `CreateCurve`; fixed curve="Spline"
## `Ray` (acadrust `Ray`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_ray**(origin: [f64; 3], direction: [f64; 3]) -> `Ray` — `CreateCurve`; fixed curve="Ray"
## `XLine` (acadrust `XLine`, collection `curves`)
### Constructors (`doc.curves()`)
- **create_xline**(origin: [f64; 3], direction: [f64; 3]) -> `XLine` — `CreateCurve`; fixed curve="XLine"
## `Text` (acadrust `Text`, collection `curves`)
Single-line text annotation. content()/set_content() read+replace the value; transform moves insertion point.
### Constructors (`doc.curves()`)
- **create_text**(value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> `Text` — `CreateText`
### Methods
- **content**() -> `String` — query `GetTextContent`
- **set_content**(value: &str) -> `()` — op `SetTextContent`
## `MText` (acadrust `MText`, collection `curves`)
Multi-line text annotation. content()/set_content() read+replace the value; transform moves insertion point.
### Constructors (`doc.curves()`)
- **create_mtext**(value: &str, insertion_point: [f64; 3], height: f64) -> `MText` — `CreateMText`
### Methods
- **content**() -> `String` — query `GetTextContent`
- **set_content**(value: &str) -> `()` — op `SetTextContent`
## `Entity` (acadrust `Hatch`, collection `curves`)
Filled region over a closed polyline boundary. boundary() returns the loops; bounds from boundary; generic delete.
### Constructors (`doc.curves()`)
- **create_hatch**(boundary: &[[f64; 2]], solid: bool) -> `Entity` — `CreateHatch`
### Methods
- **boundary**() -> `Vec<Vec<[f64; 2]>>` — query `GetHatchBoundary`
## `Dimension` (acadrust `Dimension`, collection `curves`)
Linear dimension between two measured points. measurement() reads the distance; bounds from the measured + definition points.
### Constructors (`doc.curves()`)
- **create_dimension_linear**(first_point: [f64; 3], second_point: [f64; 3], definition_point: [f64; 3]) -> `Dimension` — `CreateDimensionLinear`
- **create_dimension_radius**(center: [f64; 3], point: [f64; 3]) -> `Dimension` — `CreateDimensionRadius`
- **create_dimension_diameter**(center: [f64; 3], point: [f64; 3]) -> `Dimension` — `CreateDimensionDiameter`
- **create_dimension_angular**(vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> `Dimension` — `CreateDimensionAngular`
- **create_dimension_angular2ln**(vertex: [f64; 3], first_point: [f64; 3], second_point: [f64; 3], arc_location: [f64; 3]) -> `Dimension` — `CreateDimensionAngular2Ln`
### Methods
- **measurement**() -> `f64` — query `GetDimensionMeasurement`
## `Entity` (acadrust `Insert`, collection `entities`)
Block reference. Place an existing BlockRecord by name; transform moves its insert point.
### Constructors (`doc.entities()`)
- **create_insert**(block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64) -> `Entity` — `CreateInsert`
### Methods
- **attributes**() -> `Vec<(String, String)>` — query `GetAttributes`
- **set_attribute**(tag: &str, value: &str) -> `()` — op `SetAttribute`
## `Entity` (acadrust `AttributeDefinition`, collection `entities`)
In-block attribute definition (ATTDEF): tag/prompt/default at an insertion point.
### Constructors (`doc.entities()`)
- **create_attribute_definition**(tag: &str, prompt: &str, default_value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> `Entity` — `CreateAttributeDefinition`
## `Entity` (acadrust `Viewport`, collection `entities`)
Paper-space viewport: a width×height window at center looking at view_target with view_height zoom. bounds = center ± w/2,h/2; transform moves center.
### Constructors (`doc.entities()`)
- **create_viewport**(center: [f64; 3], width: f64, height: f64, view_target: [f64; 3], view_height: f64) -> `Entity` — `CreateViewport`
### Methods
- **viewport_view**() -> `([f64; 3], f64)` — query `GetViewportView`
- **set_view**(view_target: [f64; 3], view_height: f64) -> `()` — op `SetViewportView`
## `Entity` (acadrust `RasterImage`, collection `entities`)
Raster image: create_raster_image(file_path, insertion, u, v, size) places an image (host auto-registers the ImageDefinition); bounds (4-corner bracket); delete.
### Constructors (`doc.entities()`)
- **create_raster_image**(file_path: &str, insertion_point: [f64; 3], u_vector: [f64; 3], v_vector: [f64; 3], size: [f64; 2]) -> `Entity` — `CreateRasterImage`
## `Entity` (acadrust `Table`, collection `entities`)
Table: create_table(insertion_point, data[row][col]) builds rows x columns of text cells; non-rectangular grid -> Validation; delete.
### Constructors (`doc.entities()`)
- **create_table**(insertion_point: [f64; 3], data: &[Vec<String>]) -> `Entity` — `CreateTable`
## Generic methods (every handle)
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `ArcCurve`,
`Ellipse`, `Spline`, `Ray`, `XLine`, `Text`, `MText`, `Dimension`, `Entity`) is a
`Clone`, `Send`, `Sync` token `{ ObjectId, session }` and supports:
- **`id()` -> `ObjectId`** — the stable entity identity.
- **`bounds()` -> `Aabb`** — coarse bounding box (solids lift-on-miss; 2D entities
computed; later families may be `Unsupported`).
- **`transform(placement)`** — in-place rigid similarity (same `ObjectId`). **v1:
solids only.**
- **`delete()`** — remove the entity (one undo step).
`Entity` additionally has `view()` (id + kind + bounds) and `as_solid()` (typed
downcast when `kind == "Solid3D"`).
## Collections & cross-cutting
- **`DocApi`** — root: `document(tab)`, `active_tab()`, `alive()`.
- **`Document`** — `solids()` / `curves()` / `entities()` factories + lookup,
`revision()`, `assert_revision(rev)` (read-guard), `query_batch(|q| …)` (read-only
batch, one round-trip, **no revision bump**).
- **`EntityCollection`** — `get(id) -> Entity`, `delete(id)`,
`transform_many(&ids, placement)`, `delete_many(&ids)`.
- **`OpGroup`** — best-effort client-side failure cleanup (`track` / `commit` /
`compensate`). **Not** a transaction: no atomicity, no rollback.
## Bulk ops — single op, all-or-nothing, item-capped
`CreateMany` / `TransformMany` / `DeleteMany` are **single** write ops: one undo
step, one publish, and **all-or-nothing via upfront validation** (no rollback).
Item-capped at `BULK_ITEM_CAP = 100_000` to bound host execution. Use them for
bulk data (N ≳ a few hundred); per-op autocommit is the right tool for
modeling-scale work.
## Semantics
- **Per-op autocommit, no write transactions.** Every write op is ONE atomic host
call = one undo step. Multi-op write transactions were removed (security/
integrity review).
- **Per-op atomicity.** Validate + compute geometry **before** any mutation; a
failed op applies nothing and records no undo step. `erase_sources` booleans run
only on success.
- **Structured errors.** `ApiError::{Validation, Geometry, UnknownId, Unsupported,
Transport}`; bulk validation names the failing index; a stale handle returns
`UnknownId` (never a panic).
## Kernel mapping
| op | kernel call | returns |
|---|---|---|
| CreateSolid.Cuboid | `cadkernel::brep::make::cuboid` | Option<Body> |
| CreateSolid.Sphere | `cadkernel::brep::make::sphere` | Option<Body> |
| SolidBoolean | `cadkernel::brep::combine` | Result<Body, Snag> |
| Transform | `cadkernel::brep::transform` | Option<Body> |
| GetBounds | `cadkernel::brep::body_bounds` | Option<Aabb> |
| GetVolume | `cadkernel::brep::mesh_body + divergence` | computed |
## Wire vocabulary
Every `Operation` is ONE atomic write op (one undo step); every `Query` is read-only (safe to batch). Variants are append-only (bincode discriminant stability). Requests ride `DocApiEnvelope`: `Op(Operation)` for a write, `Queries(Vec<Query>)` for a read batch.
### `Operation`
- `CreateSolid(SolidPrimitive)`
- `CreateCurve(Curve2Spec)`
- `Extrude { profile: ObjectId, direction: [f64; 3] }`
- `Revolve { profile: ObjectId, axis: ([f64; 3], [f64; 3]), angle: f64 }`
- `Transform { id: ObjectId, placement: PlacementSpec }`
- `SolidBoolean { op: BoolOp, a: ObjectId, b: ObjectId, erase_sources: bool }`
- `AddVertex { id: ObjectId, at: usize, point: [f64; 3] }`
- `Delete { id: ObjectId }`
- `CreateMany(Vec<EntitySpec>)`
- `TransformMany { ids: Vec<ObjectId>, placement: PlacementSpec }`
- `DeleteMany(Vec<ObjectId>)`
- `CreateInsert(InsertSpec)`
- `CreateViewport(ViewportSpec)`
- `CreateText(TextSpec)`
- `CreateMText(MTextSpec)`
- `SetTextContent { id: ObjectId, value: String }`
- `CreateHatch(HatchSpec)`
- `CreateDimensionLinear(DimensionSpec)`
- `SetAttribute { id: ObjectId, tag: String, value: String }`
- `SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }`
- `CreateRasterImage(RasterImageSpec)`
- `Loft { profiles: Vec<ObjectId> }`
- `CreateDimensionRadius(DimensionRadialSpec)`
- `CreateDimensionDiameter(DimensionRadialSpec)`
- `CreateDimensionAngular(DimensionAngularSpec)`
- `CreateAttributeDefinition(AttributeDefinitionSpec)`
- `CreateTable(TableSpec)`
- `CreateDimensionAngular2Ln(DimensionAngularSpec)`
### `Query`
- `GetEntity { id: ObjectId }`
- `GetBounds { id: ObjectId }`
- `GetCentroid { id: ObjectId }`
- `GetVolume { id: ObjectId }`
- `GetIntersects { a: ObjectId, b: ObjectId }`
- `GetGeometryRevision`
- `GetTextContent { id: ObjectId }`
- `GetHatchBoundary { id: ObjectId }`
- `GetDimensionMeasurement { id: ObjectId }`
- `GetAttributes { id: ObjectId }`
- `GetBlockEntities { block_name: String }`
- `GetViewportView { id: ObjectId }`
## Errors (`ApiError`)
| Variant | Meaning |
|---|---|
| `Validation { op, reason }` | Input rejected before any document mutation. |
| `Geometry { kind, msg }` | The kernel failed (boolean refused, invalid input, ACIS lift/lower). |
| `UnknownId(ObjectId)` | The handle is stale / deleted / never existed (never a panic). |
| `Unsupported(String)` | Capability not supported for this entity or backend. |
| `Transport(String)` | The channel failed (disconnected / oversized / timeout). |
`ApiError` is serializable, so IPC returns the same structured error the in-process
executor produced.
## Envelope & transport
`DocApiEnvelope { version, body }` — `body` is `Op(Operation)` (a single write) or
`Queries(Vec<Query>)` (a read batch). bincode-serialized; `version` is the crate's
envelope version (bridges must check it). Transports: `InProcess` (host) and
`OcsPluginApiIpc` (plugin over `ocs_plugin_api`), both behind the `Transport:
Send + Sync` trait (`apply(envelope) -> Receipt`, `alive()`). `Receipt` carries
`outcome` (per-op result), `query_results`, and `new_revision`.
File diff suppressed because it is too large. Load diff
+51
View File
@@ -0,0 +1,51 @@
{
"ObjectId": { "struct": [ { "name": "0", "type": "u64" } ] },
"Operation": {
"enum": [
"CreateSolid(SolidPrimitive)",
"CreateCurve(Curve2Spec)",
"Extrude { profile: ObjectId, direction: [f64; 3] }",
"Revolve { profile: ObjectId, axis: ([f64; 3], [f64; 3]), angle: f64 }",
"Transform { id: ObjectId, placement: PlacementSpec }",
"SolidBoolean { op: BoolOp, a: ObjectId, b: ObjectId, erase_sources: bool }",
"AddVertex { id: ObjectId, at: usize, point: [f64; 3] }",
"Delete { id: ObjectId }",
"CreateMany(Vec<EntitySpec>)",
"TransformMany { ids: Vec<ObjectId>, placement: PlacementSpec }",
"DeleteMany(Vec<ObjectId>)",
"CreateInsert(InsertSpec)",
"CreateViewport(ViewportSpec)",
"CreateText(TextSpec)",
"CreateMText(MTextSpec)",
"SetTextContent { id: ObjectId, value: String }",
"CreateHatch(HatchSpec)",
"CreateDimensionLinear(DimensionSpec)",
"SetAttribute { id: ObjectId, tag: String, value: String }",
"SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 }",
"CreateRasterImage(RasterImageSpec)",
"Loft { profiles: Vec<ObjectId> }",
"CreateDimensionRadius(DimensionRadialSpec)",
"CreateDimensionDiameter(DimensionRadialSpec)",
"CreateDimensionAngular(DimensionAngularSpec)",
"CreateAttributeDefinition(AttributeDefinitionSpec)",
"CreateTable(TableSpec)",
"CreateDimensionAngular2Ln(DimensionAngularSpec)"
]
},
"Query": {
"enum": [
"GetEntity { id: ObjectId }",
"GetBounds { id: ObjectId }",
"GetCentroid { id: ObjectId }",
"GetVolume { id: ObjectId }",
"GetIntersects { a: ObjectId, b: ObjectId }",
"GetGeometryRevision",
"GetTextContent { id: ObjectId }",
"GetHatchBoundary { id: ObjectId }",
"GetDimensionMeasurement { id: ObjectId }",
"GetAttributes { id: ObjectId }",
"GetBlockEntities { block_name: String }",
"GetViewportView { id: ObjectId }"
]
}
}
+102
View File
@@ -0,0 +1,102 @@
// HAND-MAINTAINED wire vocabulary (the Operation enum). This file is NOT derived
// from spec/entities.toml by build.rs — the enum is the canonical append-only
// wire vocabulary; the spec's `op` names must each map to a variant here
// (enforced by a test). Append new variants at the END only (bincode discriminant
// stability). Do NOT reorder or remove variants.
use serde::{Deserialize, Serialize};
use crate::id::ObjectId;
use crate::ops::{
AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec,
DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec,
SolidPrimitive, TableSpec, TextSpec, ViewportSpec,
};
/// A typed write operation. Each variant is ONE atomic host call = one
/// undo step. Append new variants at the END only.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Operation {
// construction
CreateSolid(SolidPrimitive),
CreateCurve(Curve2Spec),
Extrude {
profile: ObjectId,
direction: [f64; 3],
},
Revolve {
profile: ObjectId,
axis: ([f64; 3], [f64; 3]),
angle: f64,
},
// modification
Transform {
id: ObjectId,
placement: PlacementSpec,
},
SolidBoolean {
op: BoolOp,
a: ObjectId,
b: ObjectId,
erase_sources: bool,
},
AddVertex {
id: ObjectId,
at: usize,
point: [f64; 3],
},
Delete {
id: ObjectId,
},
// Bulk ops (single op, upfront-validation all-or-nothing — NOT a transaction, §5.3)
CreateMany(Vec<EntitySpec>),
TransformMany {
ids: Vec<ObjectId>,
placement: PlacementSpec,
},
DeleteMany(Vec<ObjectId>),
// containers
CreateInsert(InsertSpec),
// paper-space
CreateViewport(ViewportSpec),
// annotations
CreateText(TextSpec),
CreateMText(MTextSpec),
SetTextContent {
id: ObjectId,
value: String,
},
// hatch
CreateHatch(HatchSpec),
// dimensions
CreateDimensionLinear(DimensionSpec),
// attributes
SetAttribute {
id: ObjectId,
tag: String,
value: String,
},
// viewport view
SetViewportView {
id: ObjectId,
view_target: [f64; 3],
view_height: f64,
},
// media
CreateRasterImage(RasterImageSpec),
// multi-profile solid
Loft {
profiles: Vec<ObjectId>,
},
// dimension sub-types
CreateDimensionRadius(DimensionRadialSpec),
CreateDimensionDiameter(DimensionRadialSpec),
CreateDimensionAngular(DimensionAngularSpec),
// attribute definitions
CreateAttributeDefinition(AttributeDefinitionSpec),
// tables
CreateTable(TableSpec),
// dimension sub-types (2-line angular)
CreateDimensionAngular2Ln(DimensionAngularSpec),
}
+58
View File
@@ -0,0 +1,58 @@
// HAND-MAINTAINED wire vocabulary (the Query enum). NOT derived from the spec by
// build.rs — the enum is the canonical append-only wire vocabulary; the spec's
// `query` names must each map to a variant here (enforced by a test). Append new
// variants at the END only (bincode discriminant stability).
use serde::{Deserialize, Serialize};
use crate::id::ObjectId;
/// A typed read query. Read-only: no mutation, no undo, no revision
/// bump. Safe to batch (`EnvelopeBody::Queries`). Append new variants at the END only.
/// (Not `Copy`: some variants carry owned data like a block name.)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Query {
GetEntity {
id: ObjectId,
},
GetBounds {
id: ObjectId,
},
GetCentroid {
id: ObjectId,
},
GetVolume {
id: ObjectId,
},
/// Whether two entities' bounds overlap (two-phase conditional modelling).
GetIntersects {
a: ObjectId,
b: ObjectId,
},
GetGeometryRevision,
/// The text content of a Text/MText annotation.
GetTextContent {
id: ObjectId,
},
/// The boundary loops of a Hatch (outer + islands) as polylines.
GetHatchBoundary {
id: ObjectId,
},
/// The measured value of a Dimension (distance or angle).
GetDimensionMeasurement {
id: ObjectId,
},
/// An insert's attributes as (tag, value) pairs.
GetAttributes {
id: ObjectId,
},
/// The entity ids + kinds inside a block definition (read-only traversal).
GetBlockEntities {
block_name: String,
},
/// A viewport's view (target + zoom height).
GetViewportView {
id: ObjectId,
},
}