ocs_doc_api: Phase 2b-a (Text/MText annotations)

- CreateText/CreateMText/SetTextContent ops + GetTextContent query (append-only).
- backend add_text/add_mtext/set_text_content/text_content (Text.value/MText.value).
- facade create_text/create_mtext + Text/MText handles with content()/set_content().
- Text/MText coarse bounds + transform (insertion point); ensure_transformable.
- spec Text/MText families; api_reference + binding_schema regenerated; mocks.
- can_modify backend hook (exists + not-locked) for SetTextContent validation.
- consistency baseline extended with the appended variants.
- Test phase2b_text_mtext_create_content_transform -> 605 host lib + 3 consistency green.
This commit is contained in:
Sebastian committed 2026-09-06 09:07:41 +02:00
1 parent 1925a83a25
commit fc0e2c2c9d
16 files changed
+449 -4

No files matched your search

+23
View File
@@ -42,6 +42,29 @@ pub trait DocApiBackend {
/// Add a paper-space `VIEWPORT`; returns the fresh `ObjectId`.
fn add_viewport(&mut self, spec: &crate::ops::ViewportSpec) -> ApiResult<ObjectId>;
/// Add a single-line `TEXT` annotation; returns the fresh `ObjectId`.
fn add_text(&mut self, spec: &crate::ops::TextSpec) -> ApiResult<ObjectId>;
/// Add a multi-line `MTEXT` annotation; returns the fresh `ObjectId`.
fn add_mtext(&mut self, spec: &crate::ops::MTextSpec) -> ApiResult<ObjectId>;
/// Set the text content of a Text/MText annotation in place.
fn set_text_content(&mut self, id: ObjectId, value: &str) -> ApiResult<()>;
/// The text content of a Text/MText annotation. `Unsupported` for other kinds.
fn text_content(&self, id: ObjectId) -> ApiResult<String>;
/// 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.
/// Default: existence + not-locked (backends narrow the family check).
fn can_modify(&self, id: ObjectId) -> ApiResult<()> {
if self.entity_exists(id) {
Ok(())
} else {
Err(ApiError::UnknownId(id))
}
}
/// Remove any first-level entity by id. Returns `false` if absent; returns
/// `Err` if the entity exists but cannot be removed (e.g. locked layer).
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool>;
+23
View File
@@ -189,6 +189,28 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
b.finalize_op();
OpOutcome::NewId(id)
}
Operation::CreateText(spec) => {
b.push_undo(name);
let id = b.add_text(spec)?;
b.finalize_op();
OpOutcome::NewId(id)
}
Operation::CreateMText(spec) => {
b.push_undo(name);
let id = b.add_mtext(spec)?;
b.finalize_op();
OpOutcome::NewId(id)
}
Operation::SetTextContent { id, value } => {
b.can_modify(*id).map_err(|e| match e {
ApiError::UnknownId(_) => ApiError::validation(name, format!("unknown ObjectId {id:?}")),
other => other,
})?;
b.push_undo(name);
b.set_text_content(*id, value)?;
b.finalize_op();
OpOutcome::Updated(*id)
}
};
Ok(Receipt {
outcome: Some(outcome),
@@ -226,6 +248,7 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
QueryResult::Intersects(ba.overlaps(&bb))
}
Query::GetGeometryRevision => QueryResult::Revision(b.revision()),
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(id)?),
};
results.push(r);
}
+44
View File
@@ -267,6 +267,8 @@ handle!(Ellipse);
handle!(Spline);
handle!(Ray);
handle!(XLine);
handle!(Text);
handle!(MText);
impl Entity {
pub fn view(&self) -> ApiResult<EntityView> {
@@ -331,6 +333,27 @@ impl Polyline {
}
}
macro_rules! text_handle {
($name:ident) => {
impl $name {
/// The text content of this annotation.
pub fn content(&self) -> ApiResult<String> {
match self.session.one_query(Query::GetTextContent { id: self.id })? {
QueryResult::TextContent(s) => Ok(s),
_ => Err(ApiError::Transport("unexpected content result".into())),
}
}
/// Replace the text content (one undo step).
pub fn set_content(&self, value: &str) -> ApiResult<()> {
self.session.apply_op(Operation::SetTextContent { id: self.id, value: value.to_string() })?;
Ok(())
}
}
};
}
text_handle!(Text);
text_handle!(MText);
// ── Collections (construction / lookup) ─────────────────────────────────────
#[derive(Clone)]
@@ -442,6 +465,27 @@ impl CurveCollection {
let (s, id) = self.create_curve(Curve2Spec::XLine { origin, direction })?;
Ok(XLine::new(s, id))
}
/// Single-line TEXT annotation.
pub fn create_text(&self, value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> ApiResult<Text> {
let receipt = self.session.apply_op(Operation::CreateText(crate::ops::TextSpec {
value: value.to_string(),
insertion_point,
height,
rotation,
}))?;
let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| ApiError::Transport("create_text returned no id".into()))?;
Ok(Text::new(self.session.clone(), id))
}
/// Multi-line MTEXT annotation.
pub fn create_mtext(&self, value: &str, insertion_point: [f64; 3], height: f64) -> ApiResult<MText> {
let receipt = self.session.apply_op(Operation::CreateMText(crate::ops::MTextSpec {
value: value.to_string(),
insertion_point,
height,
}))?;
let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| ApiError::Transport("create_mtext returned no id".into()))?;
Ok(MText::new(self.session.clone(), id))
}
/// Bulk-create many points in ONE op (plan §5.3): all-or-nothing, one undo step.
pub fn create_points(&self, positions: &[[f64; 3]]) -> ApiResult<Vec<Point>> {
let specs: Vec<crate::ops::EntitySpec> = positions
@@ -87,6 +87,32 @@ B-rep solid. Host resolves bodies via the solid_models cache (lift-on-miss from
- **create_xline**(`origin: [f64; 3], direction: [f64; 3]`) -> `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`
### 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`
### Methods
- **content**(``) -> `String` — query `GetTextContent`
- **set_content**(`value: &str`) -> `()` — op `SetTextContent`
## `Entity` (acadrust `Insert`, collection `entities`)
Block reference. Place an existing BlockRecord by name; transform moves its insert point.
+73 -1
View File
@@ -188,6 +188,76 @@
"methods": [],
"name": "XLine"
},
{
"acadrust_variant": "Text",
"collection": "curves",
"handle": "Text",
"methods": [
{
"args": [],
"bulk": false,
"fixed": null,
"kind": "query",
"name": "content",
"op": null,
"query": "GetTextContent",
"read_only": true,
"returns": "String"
},
{
"args": [
{
"name": "value",
"ty": "&str"
}
],
"bulk": false,
"fixed": null,
"kind": "op",
"name": "set_content",
"op": "SetTextContent",
"query": null,
"read_only": false,
"returns": "()"
}
],
"name": "Text"
},
{
"acadrust_variant": "MText",
"collection": "curves",
"handle": "MText",
"methods": [
{
"args": [],
"bulk": false,
"fixed": null,
"kind": "query",
"name": "content",
"op": null,
"query": "GetTextContent",
"read_only": true,
"returns": "String"
},
{
"args": [
{
"name": "value",
"ty": "&str"
}
],
"bulk": false,
"fixed": null,
"kind": "op",
"name": "set_content",
"op": "SetTextContent",
"query": null,
"read_only": false,
"returns": "()"
}
],
"name": "MText"
},
{
"acadrust_variant": "Insert",
"collection": "entities",
@@ -270,7 +340,9 @@
"Ellipse",
"Spline",
"Ray",
"XLine"
"XLine",
"Text",
"MText"
],
"root": "DocApi"
},
+5 -1
View File
@@ -7,7 +7,7 @@
use serde::{Deserialize, Serialize};
use crate::id::ObjectId;
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, InsertSpec, PlacementSpec, SolidPrimitive, ViewportSpec};
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, InsertSpec, MTextSpec, PlacementSpec, SolidPrimitive, TextSpec, ViewportSpec};
/// A typed write operation (plan §5). Each variant is ONE atomic host call = one
/// undo step. Append new variants at the END only.
@@ -32,4 +32,8 @@ pub enum Operation {
CreateInsert(InsertSpec),
// paper-space
CreateViewport(ViewportSpec),
// annotations
CreateText(TextSpec),
CreateMText(MTextSpec),
SetTextContent { id: ObjectId, value: String },
}
+2
View File
@@ -19,4 +19,6 @@ pub enum Query {
/// 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 },
}
+20
View File
@@ -79,6 +79,23 @@ pub struct ViewportSpec {
pub view_height: f64,
}
/// Construction spec for a single-line `TEXT` annotation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TextSpec {
pub value: String,
pub insertion_point: [f64; 3],
pub height: f64,
pub rotation: f64,
}
/// Construction spec for a multi-line `MTEXT` annotation.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MTextSpec {
pub value: String,
pub insertion_point: [f64; 3],
pub height: f64,
}
/// A rigid similarity (rotation + translation + optional uniform scale/reflection)
/// as a plain-data mirror of `cadkernel::brep::transform`'s `Placement`
/// (non-serde). Columns are the new basis axes; `origin` is the translation.
@@ -128,6 +145,9 @@ impl crate::gen::Operation {
DeleteMany(_) => "DeleteMany",
CreateInsert(_) => "CreateInsert",
CreateViewport(_) => "CreateViewport",
CreateText(_) => "CreateText",
CreateMText(_) => "CreateMText",
SetTextContent { .. } => "SetTextContent",
}
}
}
+3
View File
@@ -43,6 +43,8 @@ pub enum QueryResult {
/// Whether two entities' bounds overlap (two-phase conditional modelling).
Intersects(bool),
Revision(GeometryRevision),
/// The text content of a Text/MText annotation.
TextContent(String),
}
/// Convenience: the query name for diagnostics.
@@ -56,6 +58,7 @@ impl crate::gen::Query {
GetVolume { .. } => "GetVolume",
GetIntersects { .. } => "GetIntersects",
GetGeometryRevision => "GetGeometryRevision",
GetTextContent { .. } => "GetTextContent",
}
}
}