forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 5-ii — typed Table create
- CreateTable(TableSpec{insertion_point, data[row][col]}) op (append-only).
- backend add_table via Table::new(insertion_point, rows, cols) + set_cell_text
(Table::default() pre-creates 3x3, so use Table::new for exact dims).
- executor validates non-empty rectangular grid; facade create_table.
- spec Table constructor; api_reference + binding_schema regenerated; mocks + baseline.
- Test phase5ii_create_table_from_grid (3x2 grid, non-rectangular rejected)
-> 615 host lib + 3 consistency green. Phase 5 complete.
This commit is contained in:
1 parent
f8ae54a351
commit
240831edcd
12 files changed
+117
-7
No files matched your search
@@ -107,10 +107,11 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal
|
||||
`RasterImage` has coarse bounds (insertion + u·width + v·height); generic
|
||||
`delete` works on all. Covered by `phase5_*` test.
|
||||
- **Done (typed create):** `create_raster_image(file_path, insertion, u, v, size)`
|
||||
→ `Operation::CreateRasterImage`; the host auto-registers the `ImageDefinition`
|
||||
(asserted by `phase5_create_raster_image_registers_definition`). Covered by that
|
||||
test. `Table`/light stay read-only `EntityView` DTOs (typed ops only where the
|
||||
spec declares an action mapping).
|
||||
→ `Operation::CreateRasterImage` (host auto-registers the ImageDefinition), and
|
||||
`create_table(insertion_point, data[row][col])` → `Operation::CreateTable`
|
||||
(non-rectangular grid → `Validation`; cells built as text). Covered by
|
||||
`phase5_create_raster_image_*` and `phase5ii_create_table_*` tests. `light` stays
|
||||
read-only (no meaningful geometric action).
|
||||
|
||||
## 🔧 Outstanding methods on supported families
|
||||
|
||||
|
||||
@@ -206,6 +206,13 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
Ok(self.alloc("AttributeDefinition"))
|
||||
}
|
||||
fn add_table(&mut self, spec: &ocs_doc_api::ops::TableSpec) -> ApiResult<ObjectId> {
|
||||
let cols = spec.data.first().map(|r| r.len()).unwrap_or(0);
|
||||
if spec.data.is_empty() || cols == 0 || spec.data.iter().any(|r| r.len() != cols) {
|
||||
return Err(ApiError::validation("CreateTable", "table needs a non-empty rectangular grid"));
|
||||
}
|
||||
Ok(self.alloc("Table"))
|
||||
}
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
|
||||
@@ -445,7 +445,12 @@ name = "Table"
|
||||
acadrust_variant = "Table"
|
||||
handle = "Entity"
|
||||
collection = "entities"
|
||||
description = "Table (read-only): GetEntity kind; generic delete. No typed create/bounds in v1."
|
||||
description = "Table: create_table(insertion_point, data[row][col]) builds rows x columns of text cells; non-rectangular grid -> Validation; delete."
|
||||
|
||||
[[family.constructor]]
|
||||
name = "create_table"
|
||||
args = [{ name = "insertion_point", ty = "[f64; 3]" }, { name = "data", ty = "&[Vec<String>]" }]
|
||||
op = "CreateTable"
|
||||
|
||||
# ============================================================================
|
||||
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
||||
|
||||
@@ -75,6 +75,9 @@ pub trait DocApiBackend {
|
||||
/// Add an `ATTDEF` (in-block attribute definition); returns the fresh `ObjectId`.
|
||||
fn add_attribute_definition(&mut self, spec: &crate::ops::AttributeDefinitionSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// Add a `TABLE` (rows × columns grid); returns the fresh `ObjectId`.
|
||||
fn add_table(&mut self, spec: &crate::ops::TableSpec) -> ApiResult<ObjectId>;
|
||||
|
||||
/// A dimension's measured value (distance for linear/radius, degrees for angular).
|
||||
fn dimension_measurement(&self, id: ObjectId) -> ApiResult<f64>;
|
||||
|
||||
|
||||
@@ -293,6 +293,17 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
Operation::CreateTable(spec) => {
|
||||
// Validate a non-empty, rectangular grid before committing.
|
||||
let cols = spec.data.first().map(|r| r.len()).unwrap_or(0);
|
||||
if spec.data.is_empty() || cols == 0 || spec.data.iter().any(|r| r.len() != cols) {
|
||||
return Err(ApiError::validation(name, "table needs a non-empty rectangular grid"));
|
||||
}
|
||||
b.push_undo(name);
|
||||
let id = b.add_table(spec)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::NewId(id)
|
||||
}
|
||||
};
|
||||
Ok(Receipt {
|
||||
outcome: Some(outcome),
|
||||
|
||||
@@ -631,6 +631,16 @@ impl EntityCollection {
|
||||
Ok(Entity::new(self.session.clone(), id))
|
||||
}
|
||||
|
||||
/// Create a TABLE from a grid of cell strings (`data[row][column]`), at a point.
|
||||
pub fn create_table(&self, insertion_point: [f64; 3], data: &[Vec<String>]) -> ApiResult<Entity> {
|
||||
let receipt = self.session.apply_op(Operation::CreateTable(crate::ops::TableSpec {
|
||||
insertion_point,
|
||||
data: data.to_vec(),
|
||||
}))?;
|
||||
let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| ApiError::Transport("create_table returned no id".into()))?;
|
||||
Ok(Entity::new(self.session.clone(), id))
|
||||
}
|
||||
|
||||
/// Create an ATTDEF (in-block attribute definition): tag/prompt/default at a point.
|
||||
pub fn create_attribute_definition(&self, tag: &str, prompt: &str, default_value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> ApiResult<Entity> {
|
||||
let receipt = self.session.apply_op(Operation::CreateAttributeDefinition(crate::ops::AttributeDefinitionSpec {
|
||||
|
||||
@@ -185,7 +185,11 @@ Raster image: create_raster_image(file_path, insertion, u, v, size) places an im
|
||||
|
||||
## `Entity` (acadrust `Table`, collection `entities`)
|
||||
|
||||
Table (read-only): GetEntity kind; generic delete. No typed create/bounds in v1.
|
||||
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)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::id::ObjectId;
|
||||
use crate::ops::{AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec, DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec, SolidPrimitive, TextSpec, ViewportSpec};
|
||||
use crate::ops::{AttributeDefinitionSpec, BoolOp, Curve2Spec, DimensionAngularSpec, DimensionRadialSpec, DimensionSpec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, RasterImageSpec, SolidPrimitive, TableSpec, 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.
|
||||
@@ -54,4 +54,6 @@ pub enum Operation {
|
||||
CreateDimensionAngular(DimensionAngularSpec),
|
||||
// attribute definitions
|
||||
CreateAttributeDefinition(AttributeDefinitionSpec),
|
||||
// tables
|
||||
CreateTable(TableSpec),
|
||||
}
|
||||
@@ -148,6 +148,15 @@ pub struct AttributeDefinitionSpec {
|
||||
pub rotation: f64,
|
||||
}
|
||||
|
||||
/// Construction spec for a `TABLE`: an insertion point plus a grid of cell
|
||||
/// strings (`data[row][column]`).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TableSpec {
|
||||
pub insertion_point: [f64; 3],
|
||||
/// Grid of cell text values, rows × columns.
|
||||
pub data: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Construction spec for a `RASTER_IMAGE`: an image file placed at
|
||||
/// `insertion_point` with per-pixel world-unit vectors `u_vector`/`v_vector` and
|
||||
/// pixel `size` (width, height). The host auto-registers the ImageDefinition.
|
||||
@@ -223,6 +232,7 @@ impl crate::gen::Operation {
|
||||
CreateDimensionDiameter(_) => "CreateDimensionDiameter",
|
||||
CreateDimensionAngular(_) => "CreateDimensionAngular",
|
||||
CreateAttributeDefinition(_) => "CreateAttributeDefinition",
|
||||
CreateTable(_) => "CreateTable",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,13 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
Ok(self.alloc("AttributeDefinition"))
|
||||
}
|
||||
fn add_table(&mut self, spec: &ocs_doc_api::ops::TableSpec) -> ApiResult<ObjectId> {
|
||||
let cols = spec.data.first().map(|r| r.len()).unwrap_or(0);
|
||||
if spec.data.is_empty() || cols == 0 || spec.data.iter().any(|r| r.len() != cols) {
|
||||
return Err(ApiError::validation("CreateTable", "table needs a non-empty rectangular grid"));
|
||||
}
|
||||
Ok(self.alloc("Table"))
|
||||
}
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
|
||||
@@ -70,6 +70,7 @@ fn operation_variants() -> Vec<String> {
|
||||
Operation::CreateAttributeDefinition(ocs_doc_api::ops::AttributeDefinitionSpec {
|
||||
tag: String::new(), prompt: String::new(), default_value: String::new(), insertion_point: [0.0; 3], height: 0.0, rotation: 0.0,
|
||||
}),
|
||||
Operation::CreateTable(ocs_doc_api::ops::TableSpec { insertion_point: [0.0; 3], data: vec![] }),
|
||||
];
|
||||
// Derive the variant names by serializing a probe of each and reading the
|
||||
// bincode variant index -> name map is not available; instead match by
|
||||
@@ -163,6 +164,7 @@ const OPERATION_BASELINE: &[&str] = &[
|
||||
"CreateDimensionDiameter",
|
||||
"CreateDimensionAngular",
|
||||
"CreateAttributeDefinition",
|
||||
"CreateTable",
|
||||
];
|
||||
|
||||
/// Recorded baseline of the `Query` variant order.
|
||||
|
||||
@@ -306,6 +306,25 @@ impl DocApiBackend for HostSession<'_> {
|
||||
Ok(handle_to_obj(handle))
|
||||
}
|
||||
|
||||
fn add_table(&mut self, spec: &ocs_doc_api::ops::TableSpec) -> ApiResult<ObjectId> {
|
||||
use acadrust::entities::Table;
|
||||
use acadrust::types::Vector3;
|
||||
let cols = spec.data.first().map(|r| r.len()).unwrap_or(0);
|
||||
let mut table = Table::new(
|
||||
Vector3::new(spec.insertion_point[0], spec.insertion_point[1], spec.insertion_point[2]),
|
||||
spec.data.len(),
|
||||
cols,
|
||||
);
|
||||
// Fill cells from the grid (each cell = text).
|
||||
for (r, row) in spec.data.iter().enumerate() {
|
||||
for (c, text) in row.iter().enumerate() {
|
||||
table.set_cell_text(r, c, text);
|
||||
}
|
||||
}
|
||||
let handle = self.scene_mut().add_entity(EntityType::Table(table));
|
||||
Ok(handle_to_obj(handle))
|
||||
}
|
||||
|
||||
fn set_attribute(&mut self, id: ObjectId, tag: &str, value: &str) -> ApiResult<()> {
|
||||
let handle = obj_to_handle(id);
|
||||
let entity = self.document().get_entity(handle).cloned().ok_or(ApiError::UnknownId(id))?;
|
||||
@@ -1186,6 +1205,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 5-ii: typed Table create ───────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn phase5ii_create_table_from_grid() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let data = vec![
|
||||
vec!["Name".to_string(), "Value".to_string()],
|
||||
vec!["Length".to_string(), "100.0".to_string()],
|
||||
vec!["Width".to_string(), "50.0".to_string()],
|
||||
];
|
||||
let table = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateTable(
|
||||
ocs_doc_api::ops::TableSpec { insertion_point: [10.0, 10.0, 0.0], data: data.clone() }))).unwrap());
|
||||
let handle = obj_to_handle(table);
|
||||
let Some(EntityType::Table(t)) = host.document().get_entity(handle) else { panic!("table not found") };
|
||||
assert_eq!(t.rows.len(), 3);
|
||||
assert_eq!(t.columns.len(), 2);
|
||||
// Non-rectangular grid is a validation error.
|
||||
let bad = vec![vec!["a".to_string()], vec!["b".to_string(), "c".to_string()]];
|
||||
assert!(dispatch(&mut host, DocApiEnvelope::op(Operation::CreateTable(
|
||||
ocs_doc_api::ops::TableSpec { insertion_point: [0.0; 3], data: bad }))).is_err());
|
||||
// GetEntity reports kind Table.
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetEntity { id: table }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Entity(e) => assert_eq!(e.kind, "Table"),
|
||||
other => panic!("expected entity, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 5 remaining: typed raster image create ─────────────────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user