forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 4 (paper-space viewports)
- Operation::CreateViewport(ViewportSpec) appended to the wire enum (append-only). - backend add_viewport (center/width/height/view_target/view_height). - Viewport bounds (center +- w/2,h/2), transform (moves center), ensure_transformable. - facade EntityCollection::create_viewport; spec Viewport family; api_reference + binding_schema regenerated; mocks updated. - Test phase4_create_viewport_bounds_transform -> 600 host lib green. - IMPLEMENTATION.md: Phase 4 done (set_view/view-query remaining).
This commit is contained in:
1 parent
b03f68737e
commit
7a481c6ce2
13 files changed
+150
-5
No files matched your search
@@ -68,10 +68,11 @@ variants append at enum end.
|
|||||||
get/set; read-only nested block-content traversal.
|
get/set; read-only nested block-content traversal.
|
||||||
|
|
||||||
### Phase 4 — paper-space & viewports
|
### Phase 4 — paper-space & viewports
|
||||||
- **Entities:** viewport entities.
|
- **Done:** `create_viewport(center, width, height, view_target, view_height)` →
|
||||||
- **Methods:** `create_viewport`, set-view (target, center, scale), layout
|
`Operation::CreateViewport`; `bounds()` = center ± width/2,height/2; `transform`
|
||||||
queries.
|
moves the paper-space center. Covered by `phase4_*` test.
|
||||||
- **Blocked by:** a viewport query model (view target/center/scale DTOs).
|
- **Remaining:** `set_view` (retarget/re-zoom an existing viewport) and a
|
||||||
|
viewport-view query (view target/center/scale DTOs).
|
||||||
|
|
||||||
### Phase 5 — media & misc (read-mostly)
|
### Phase 5 — media & misc (read-mostly)
|
||||||
- **Entities:** raster image, underlay, table, light.
|
- **Entities:** raster image, underlay, table, light.
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
}
|
}
|
||||||
Ok(self.alloc("Insert"))
|
Ok(self.alloc("Insert"))
|
||||||
}
|
}
|
||||||
|
fn add_viewport(&mut self, _spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||||
|
Ok(self.alloc("Viewport"))
|
||||||
|
}
|
||||||
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(())
|
||||||
|
|||||||
@@ -252,6 +252,21 @@ name = "create_insert"
|
|||||||
args = [{ name = "block_name", ty = "&str" }, { name = "insert_point", ty = "[f64; 3]" }, { name = "scale", ty = "f64" }, { name = "rotation", ty = "f64" }]
|
args = [{ name = "block_name", ty = "&str" }, { name = "insert_point", ty = "[f64; 3]" }, { name = "scale", ty = "f64" }, { name = "rotation", ty = "f64" }]
|
||||||
op = "CreateInsert"
|
op = "CreateInsert"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# PAPER-SPACE FAMILY (phase 4) — viewports
|
||||||
|
# ============================================================================
|
||||||
|
[[family]]
|
||||||
|
name = "Viewport"
|
||||||
|
acadrust_variant = "Viewport"
|
||||||
|
handle = "Entity"
|
||||||
|
collection = "entities"
|
||||||
|
description = "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."
|
||||||
|
|
||||||
|
[[family.constructor]]
|
||||||
|
name = "create_viewport"
|
||||||
|
args = [{ name = "center", ty = "[f64; 3]" }, { name = "width", ty = "f64" }, { name = "height", ty = "f64" }, { name = "view_target", ty = "[f64; 3]" }, { name = "view_height", ty = "f64" }]
|
||||||
|
op = "CreateViewport"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ pub trait DocApiBackend {
|
|||||||
/// the fresh `ObjectId`. `Validation` if the block is unknown.
|
/// the fresh `ObjectId`. `Validation` if the block is unknown.
|
||||||
fn add_insert(&mut self, spec: &crate::ops::InsertSpec) -> ApiResult<ObjectId>;
|
fn add_insert(&mut self, spec: &crate::ops::InsertSpec) -> ApiResult<ObjectId>;
|
||||||
|
|
||||||
|
/// Add a paper-space `VIEWPORT`; returns the fresh `ObjectId`.
|
||||||
|
fn add_viewport(&mut self, spec: &crate::ops::ViewportSpec) -> ApiResult<ObjectId>;
|
||||||
|
|
||||||
/// Remove any first-level entity by id. Returns `false` if absent; returns
|
/// 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).
|
/// `Err` if the entity exists but cannot be removed (e.g. locked layer).
|
||||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool>;
|
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool>;
|
||||||
|
|||||||
@@ -183,6 +183,12 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
|||||||
b.finalize_op();
|
b.finalize_op();
|
||||||
OpOutcome::NewId(id)
|
OpOutcome::NewId(id)
|
||||||
}
|
}
|
||||||
|
Operation::CreateViewport(spec) => {
|
||||||
|
b.push_undo(name);
|
||||||
|
let id = b.add_viewport(spec)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::NewId(id)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(Receipt {
|
Ok(Receipt {
|
||||||
outcome: Some(outcome),
|
outcome: Some(outcome),
|
||||||
|
|||||||
@@ -464,6 +464,19 @@ pub struct EntityCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl EntityCollection {
|
impl EntityCollection {
|
||||||
|
/// Create a paper-space `VIEWPORT` (a `width`×`height` viewport at `center`
|
||||||
|
/// looking at `view_target` with `view_height` zoom).
|
||||||
|
pub fn create_viewport(&self, center: [f64; 3], width: f64, height: f64, view_target: [f64; 3], view_height: f64) -> ApiResult<Entity> {
|
||||||
|
let receipt = self.session.apply_op(Operation::CreateViewport(crate::ops::ViewportSpec {
|
||||||
|
center, width, height, view_target, view_height,
|
||||||
|
}))?;
|
||||||
|
let id = receipt
|
||||||
|
.outcome
|
||||||
|
.and_then(|o| o.new_id())
|
||||||
|
.ok_or_else(|| ApiError::Transport("create_viewport returned no id".into()))?;
|
||||||
|
Ok(Entity::new(self.session.clone(), id))
|
||||||
|
}
|
||||||
|
|
||||||
/// Place a block reference (`INSERT`) for `block_name` (must exist).
|
/// Place a block reference (`INSERT`) for `block_name` (must exist).
|
||||||
pub fn create_insert(&self, block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64) -> ApiResult<Entity> {
|
pub fn create_insert(&self, block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64) -> ApiResult<Entity> {
|
||||||
let receipt = self.session.apply_op(Operation::CreateInsert(crate::ops::InsertSpec {
|
let receipt = self.session.apply_op(Operation::CreateInsert(crate::ops::InsertSpec {
|
||||||
|
|||||||
@@ -95,6 +95,14 @@ Block reference. Place an existing BlockRecord by name; transform moves its inse
|
|||||||
|
|
||||||
- **create_insert**(`block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64`) -> `Entity`
|
- **create_insert**(`block_name: &str, insert_point: [f64; 3], scale: f64, rotation: f64`) -> `Entity`
|
||||||
|
|
||||||
|
## `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`
|
||||||
|
|
||||||
## Generic methods (every handle)
|
## Generic methods (every handle)
|
||||||
|
|
||||||
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `Entity`) is a
|
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `Entity`) is a
|
||||||
|
|||||||
@@ -194,6 +194,13 @@
|
|||||||
"handle": "Entity",
|
"handle": "Entity",
|
||||||
"methods": [],
|
"methods": [],
|
||||||
"name": "Insert"
|
"name": "Insert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"acadrust_variant": "Viewport",
|
||||||
|
"collection": "entities",
|
||||||
|
"handle": "Entity",
|
||||||
|
"methods": [],
|
||||||
|
"name": "Viewport"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"layouts": {
|
"layouts": {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::id::ObjectId;
|
use crate::id::ObjectId;
|
||||||
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, InsertSpec, PlacementSpec, SolidPrimitive};
|
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, InsertSpec, PlacementSpec, SolidPrimitive, ViewportSpec};
|
||||||
|
|
||||||
/// A typed write operation (plan §5). Each variant is ONE atomic host call = one
|
/// A typed write operation (plan §5). Each variant is ONE atomic host call = one
|
||||||
/// undo step. Append new variants at the END only.
|
/// undo step. Append new variants at the END only.
|
||||||
@@ -29,4 +29,6 @@ pub enum Operation {
|
|||||||
DeleteMany(Vec<ObjectId>),
|
DeleteMany(Vec<ObjectId>),
|
||||||
// containers
|
// containers
|
||||||
CreateInsert(InsertSpec),
|
CreateInsert(InsertSpec),
|
||||||
|
// paper-space
|
||||||
|
CreateViewport(ViewportSpec),
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,18 @@ pub struct InsertSpec {
|
|||||||
pub rotation: f64,
|
pub rotation: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construction spec for a paper-space `VIEWPORT`: a `width`×`height` viewport
|
||||||
|
/// centered at `center` (paper space), looking at `view_target` (model space,
|
||||||
|
/// WCS) with `view_height` (model-space height visible; the viewport's zoom).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ViewportSpec {
|
||||||
|
pub center: [f64; 3],
|
||||||
|
pub width: f64,
|
||||||
|
pub height: f64,
|
||||||
|
pub view_target: [f64; 3],
|
||||||
|
pub view_height: f64,
|
||||||
|
}
|
||||||
|
|
||||||
/// A rigid similarity (rotation + translation + optional uniform scale/reflection)
|
/// A rigid similarity (rotation + translation + optional uniform scale/reflection)
|
||||||
/// as a plain-data mirror of `cadkernel::brep::transform`'s `Placement`
|
/// as a plain-data mirror of `cadkernel::brep::transform`'s `Placement`
|
||||||
/// (non-serde). Columns are the new basis axes; `origin` is the translation.
|
/// (non-serde). Columns are the new basis axes; `origin` is the translation.
|
||||||
@@ -115,6 +127,7 @@ impl crate::gen::Operation {
|
|||||||
TransformMany { .. } => "TransformMany",
|
TransformMany { .. } => "TransformMany",
|
||||||
DeleteMany(_) => "DeleteMany",
|
DeleteMany(_) => "DeleteMany",
|
||||||
CreateInsert(_) => "CreateInsert",
|
CreateInsert(_) => "CreateInsert",
|
||||||
|
CreateViewport(_) => "CreateViewport",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,9 @@ impl DocApiBackend for MockBackend {
|
|||||||
}
|
}
|
||||||
Ok(self.alloc("Insert"))
|
Ok(self.alloc("Insert"))
|
||||||
}
|
}
|
||||||
|
fn add_viewport(&mut self, _spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||||
|
Ok(self.alloc("Viewport"))
|
||||||
|
}
|
||||||
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(())
|
||||||
|
|||||||
@@ -125,6 +125,20 @@ impl DocApiBackend for HostSession<'_> {
|
|||||||
Ok(handle_to_obj(handle))
|
Ok(handle_to_obj(handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||||
|
use acadrust::entities::Viewport;
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
let v3 = |p: [f64; 3]| Vector3::new(p[0], p[1], p[2]);
|
||||||
|
let mut vp = Viewport::new();
|
||||||
|
vp.center = v3(spec.center);
|
||||||
|
vp.width = spec.width;
|
||||||
|
vp.height = spec.height;
|
||||||
|
vp.view_target = v3(spec.view_target);
|
||||||
|
vp.view_height = spec.view_height;
|
||||||
|
let handle = self.scene_mut().add_entity(EntityType::Viewport(vp));
|
||||||
|
Ok(handle_to_obj(handle))
|
||||||
|
}
|
||||||
|
|
||||||
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<()> {
|
||||||
let handle = obj_to_handle(id);
|
let handle = obj_to_handle(id);
|
||||||
let Some(entity) = self.document().get_entity(handle).cloned() else {
|
let Some(entity) = self.document().get_entity(handle).cloned() else {
|
||||||
@@ -183,6 +197,7 @@ impl DocApiBackend for HostSession<'_> {
|
|||||||
| EntityType::Ray(_)
|
| EntityType::Ray(_)
|
||||||
| EntityType::XLine(_)
|
| EntityType::XLine(_)
|
||||||
| EntityType::Insert(_)
|
| EntityType::Insert(_)
|
||||||
|
| EntityType::Viewport(_)
|
||||||
| EntityType::Point(_)
|
| EntityType::Point(_)
|
||||||
| EntityType::LwPolyline(_)
|
| EntityType::LwPolyline(_)
|
||||||
);
|
);
|
||||||
@@ -626,6 +641,44 @@ mod tests {
|
|||||||
assert!((r.base_point.z - 5.0).abs() < 1e-6);
|
assert!((r.base_point.z - 5.0).abs() < 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 4: paper-space viewports ───────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn phase4_create_viewport_bounds_transform() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let mut host = HostSession::new(&mut app, 0);
|
||||||
|
// A 40x30 paper-space viewport at (50,50,0) looking at model origin.
|
||||||
|
let vp = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateViewport(
|
||||||
|
ocs_doc_api::ops::ViewportSpec {
|
||||||
|
center: [50.0, 50.0, 0.0], width: 40.0, height: 30.0,
|
||||||
|
view_target: [0.0, 0.0, 0.0], view_height: 100.0,
|
||||||
|
}))).unwrap());
|
||||||
|
let handle = obj_to_handle(vp);
|
||||||
|
let Some(EntityType::Viewport(v)) = host.document().get_entity(handle) else { panic!("viewport not found") };
|
||||||
|
assert!((v.width - 40.0).abs() < 1e-9 && (v.view_height - 100.0).abs() < 1e-9);
|
||||||
|
|
||||||
|
// Bounds = center ± width/2, height/2.
|
||||||
|
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![
|
||||||
|
Query::GetEntity { id: vp }, Query::GetBounds { id: vp }])).unwrap();
|
||||||
|
match &receipt.query_results[0] {
|
||||||
|
QueryResult::Entity(e) => assert_eq!(e.kind, "Viewport"),
|
||||||
|
other => panic!("expected entity, got {other:?}"),
|
||||||
|
}
|
||||||
|
match &receipt.query_results[1] {
|
||||||
|
QueryResult::Bounds(b) => {
|
||||||
|
assert!((b.min[0] - 30.0).abs() < 1e-6 && (b.max[0] - 70.0).abs() < 1e-6, "{b:?}");
|
||||||
|
assert!((b.min[1] - 35.0).abs() < 1e-6 && (b.max[1] - 65.0).abs() < 1e-6, "{b:?}");
|
||||||
|
}
|
||||||
|
other => panic!("expected bounds, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform moves the viewport's paper-space center.
|
||||||
|
dispatch(&mut host, DocApiEnvelope::op(Operation::Transform {
|
||||||
|
id: vp, placement: ocs_doc_api::PlacementSpec::at([10.0, 0.0, 0.0]) })).unwrap();
|
||||||
|
let Some(EntityType::Viewport(v)) = host.document().get_entity(handle) else { panic!("viewport not found") };
|
||||||
|
assert!((v.center.x - 60.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Phase 3: containers (block references) ───────────────────────────────
|
// ── Phase 3: containers (block references) ───────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ pub fn transform_entity_geometry(
|
|||||||
ins.insert_point = apply(ins.insert_point);
|
ins.insert_point = apply(ins.insert_point);
|
||||||
EntityType::Insert(ins)
|
EntityType::Insert(ins)
|
||||||
}
|
}
|
||||||
|
EntityType::Viewport(vp) => {
|
||||||
|
let mut vp = vp.clone();
|
||||||
|
vp.center = apply(vp.center);
|
||||||
|
EntityType::Viewport(vp)
|
||||||
|
}
|
||||||
EntityType::Point(pt) => {
|
EntityType::Point(pt) => {
|
||||||
let mut pt = pt.clone();
|
let mut pt = pt.clone();
|
||||||
pt.location = apply(pt.location);
|
pt.location = apply(pt.location);
|
||||||
@@ -242,6 +247,7 @@ pub fn entity_kind_name(entity: &EntityType) -> &'static str {
|
|||||||
EntityType::Ray(_) => "Ray",
|
EntityType::Ray(_) => "Ray",
|
||||||
EntityType::XLine(_) => "XLine",
|
EntityType::XLine(_) => "XLine",
|
||||||
EntityType::Insert(_) => "Insert",
|
EntityType::Insert(_) => "Insert",
|
||||||
|
EntityType::Viewport(_) => "Viewport",
|
||||||
EntityType::Text(_) => "Text",
|
EntityType::Text(_) => "Text",
|
||||||
EntityType::MText(_) => "MText",
|
EntityType::MText(_) => "MText",
|
||||||
_ => "Other",
|
_ => "Other",
|
||||||
@@ -311,6 +317,18 @@ pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult<Aab
|
|||||||
}
|
}
|
||||||
Aabb { min, max }
|
Aabb { min, max }
|
||||||
}
|
}
|
||||||
|
EntityType::Viewport(vp) => Aabb {
|
||||||
|
min: [
|
||||||
|
vp.center.x - vp.width / 2.0,
|
||||||
|
vp.center.y - vp.height / 2.0,
|
||||||
|
vp.center.z,
|
||||||
|
],
|
||||||
|
max: [
|
||||||
|
vp.center.x + vp.width / 2.0,
|
||||||
|
vp.center.y + vp.height / 2.0,
|
||||||
|
vp.center.z,
|
||||||
|
],
|
||||||
|
},
|
||||||
// Ray/XLine are unbounded by definition — no finite bounds.
|
// Ray/XLine are unbounded by definition — no finite bounds.
|
||||||
_ => return Err(ApiError::Unsupported("bounds for this entity family is not yet implemented".into())),
|
_ => return Err(ApiError::Unsupported("bounds for this entity family is not yet implemented".into())),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user