forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 4 set_view + viewport-view query
- SetViewportView op + GetViewportView query (append-only). - backend set_viewport_view (update Viewport.view_target/view_height) + viewport_view. - facade Entity::set_view/viewport_view (Viewport returns Entity). - spec Viewport methods; api_reference + binding_schema regenerated; mocks + baseline. - Test phase4_set_view_and_view_query -> 609 host lib + 3 consistency green. - Phase 4 complete.
This commit is contained in:
1 parent
67655add72
commit
e6c132e0eb
15 files changed
+187
-7
No files matched your search
@@ -91,8 +91,10 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal
|
||||
- **Done:** `create_viewport(center, width, height, view_target, view_height)` →
|
||||
`Operation::CreateViewport`; `bounds()` = center ± width/2,height/2; `transform`
|
||||
moves the paper-space center. Covered by `phase4_*` test.
|
||||
- **Remaining:** `set_view` (retarget/re-zoom an existing viewport) and a
|
||||
viewport-view query (view target/center/scale DTOs).
|
||||
- **Done (set_view + view query):** `set_view(view_target, view_height)` retargets/
|
||||
re-zooms in place (`SetViewportView` op), `viewport_view()` reads the view back
|
||||
(`GetViewportView` query → target + zoom height). Covered by
|
||||
`phase4_set_view_and_view_query` test.
|
||||
|
||||
### Phase 5 — media & misc (read-mostly)
|
||||
- **Done:** media/misc families report a **real `kind`** via `GetEntity`
|
||||
|
||||
@@ -29,6 +29,8 @@ pub struct MockBackend {
|
||||
dimension_measurements: HashMap<ObjectId, f64>,
|
||||
/// Stored insert attributes.
|
||||
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
||||
/// Stored viewport views (target, height).
|
||||
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
||||
}
|
||||
|
||||
impl MockBackend {
|
||||
@@ -138,8 +140,10 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
Ok(self.alloc("Insert"))
|
||||
}
|
||||
fn add_viewport(&mut self, _spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||
Ok(self.alloc("Viewport"))
|
||||
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Viewport");
|
||||
self.viewport_views.insert(id, (spec.view_target, spec.view_height));
|
||||
Ok(id)
|
||||
}
|
||||
fn add_text(&mut self, spec: &ocs_doc_api::ops::TextSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Text");
|
||||
@@ -200,6 +204,16 @@ impl DocApiBackend for MockBackend {
|
||||
fn block_entities(&self, _block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn set_viewport_view(&mut self, id: ObjectId, view_target: [f64; 3], view_height: f64) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
self.viewport_views.insert(id, (view_target, view_height));
|
||||
Ok(())
|
||||
}
|
||||
fn viewport_view(&self, id: ObjectId) -> ApiResult<([f64; 3], f64)> {
|
||||
self.viewport_views.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
|
||||
@@ -375,6 +375,19 @@ 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"
|
||||
|
||||
[[family.method]]
|
||||
name = "viewport_view"
|
||||
kind = "query"
|
||||
query = "GetViewportView"
|
||||
returns = "([f64; 3], f64)"
|
||||
|
||||
[[family.method]]
|
||||
name = "set_view"
|
||||
kind = "op"
|
||||
op = "SetViewportView"
|
||||
args = [{ name = "view_target", ty = "[f64; 3]" }, { name = "view_height", ty = "f64" }]
|
||||
returns = "()"
|
||||
|
||||
# ============================================================================
|
||||
# MEDIA & MISC (phase 5, read-mostly) — read-only EntityView, no typed create.
|
||||
# These families report a real `kind` via GetEntity and coarse bounds where
|
||||
|
||||
@@ -75,6 +75,12 @@ pub trait DocApiBackend {
|
||||
/// The entities inside a block definition (read-only traversal), as views.
|
||||
fn block_entities(&self, block_name: &str) -> ApiResult<Vec<EntityView>>;
|
||||
|
||||
/// Set a viewport's view target + zoom height in place. Viewport-only.
|
||||
fn set_viewport_view(&mut self, id: ObjectId, view_target: [f64; 3], view_height: f64) -> ApiResult<()>;
|
||||
|
||||
/// A viewport's view (target WCS + zoom height). Viewport-only.
|
||||
fn viewport_view(&self, id: ObjectId) -> ApiResult<([f64; 3], f64)>;
|
||||
|
||||
/// 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).
|
||||
|
||||
@@ -237,6 +237,16 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
Operation::SetViewportView { id, view_target, view_height } => {
|
||||
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_viewport_view(*id, *view_target, *view_height)?;
|
||||
b.finalize_op();
|
||||
OpOutcome::Updated(*id)
|
||||
}
|
||||
};
|
||||
Ok(Receipt {
|
||||
outcome: Some(outcome),
|
||||
@@ -279,6 +289,10 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
|
||||
Query::GetDimensionMeasurement { id } => QueryResult::DimensionMeasurement(b.dimension_measurement(*id)?),
|
||||
Query::GetAttributes { id } => QueryResult::Attributes(b.attributes(*id)?),
|
||||
Query::GetBlockEntities { block_name } => QueryResult::BlockEntities(b.block_entities(block_name)?),
|
||||
Query::GetViewportView { id } => {
|
||||
let (target, height) = b.viewport_view(*id)?;
|
||||
QueryResult::ViewportView { target, height }
|
||||
}
|
||||
};
|
||||
results.push(r);
|
||||
}
|
||||
|
||||
@@ -309,6 +309,18 @@ impl Entity {
|
||||
self.session.apply_op(Operation::SetAttribute { id: self.id, tag: tag.to_string(), value: value.to_string() })?;
|
||||
Ok(())
|
||||
}
|
||||
/// This viewport's view (target WCS + zoom height). Viewport-only.
|
||||
pub fn viewport_view(&self) -> ApiResult<([f64; 3], f64)> {
|
||||
match self.session.one_query(Query::GetViewportView { id: self.id })? {
|
||||
QueryResult::ViewportView { target, height } => Ok((target, height)),
|
||||
_ => Err(ApiError::Transport("unexpected viewport-view result".into())),
|
||||
}
|
||||
}
|
||||
/// Retarget / re-zoom this viewport (one undo step). Viewport-only.
|
||||
pub fn set_view(&self, view_target: [f64; 3], view_height: f64) -> ApiResult<()> {
|
||||
self.session.apply_op(Operation::SetViewportView { id: self.id, view_target, view_height })?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn as_solid(&self) -> Option<Solid> {
|
||||
// Typed downcast is validated by the view's kind.
|
||||
matches!(self.view().ok()?.kind.as_str(), "Solid3D").then(|| Solid::new(self.session.clone(), self.id))
|
||||
|
||||
@@ -158,6 +158,11 @@ Paper-space viewport: a width×height window at center looking at view_target wi
|
||||
|
||||
- **create_viewport**(`center: [f64; 3], width: f64, height: f64, view_target: [f64; 3], view_height: f64`) -> `Entity`
|
||||
|
||||
### 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 (read-mostly): GetEntity kind + coarse bounds (insertion + u*width + v*height); generic delete. No typed create in v1.
|
||||
|
||||
@@ -339,7 +339,39 @@
|
||||
"acadrust_variant": "Viewport",
|
||||
"collection": "entities",
|
||||
"handle": "Entity",
|
||||
"methods": [],
|
||||
"methods": [
|
||||
{
|
||||
"args": [],
|
||||
"bulk": false,
|
||||
"fixed": null,
|
||||
"kind": "query",
|
||||
"name": "viewport_view",
|
||||
"op": null,
|
||||
"query": "GetViewportView",
|
||||
"read_only": true,
|
||||
"returns": "([f64; 3], f64)"
|
||||
},
|
||||
{
|
||||
"args": [
|
||||
{
|
||||
"name": "view_target",
|
||||
"ty": "[f64; 3]"
|
||||
},
|
||||
{
|
||||
"name": "view_height",
|
||||
"ty": "f64"
|
||||
}
|
||||
],
|
||||
"bulk": false,
|
||||
"fixed": null,
|
||||
"kind": "op",
|
||||
"name": "set_view",
|
||||
"op": "SetViewportView",
|
||||
"query": null,
|
||||
"read_only": false,
|
||||
"returns": "()"
|
||||
}
|
||||
],
|
||||
"name": "Viewport"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -42,4 +42,6 @@ pub enum Operation {
|
||||
CreateDimensionLinear(DimensionSpec),
|
||||
// attributes
|
||||
SetAttribute { id: ObjectId, tag: String, value: String },
|
||||
// viewport view
|
||||
SetViewportView { id: ObjectId, view_target: [f64; 3], view_height: f64 },
|
||||
}
|
||||
@@ -30,4 +30,6 @@ pub enum Query {
|
||||
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 },
|
||||
}
|
||||
@@ -170,6 +170,7 @@ impl crate::gen::Operation {
|
||||
CreateHatch(_) => "CreateHatch",
|
||||
CreateDimensionLinear(_) => "CreateDimensionLinear",
|
||||
SetAttribute { .. } => "SetAttribute",
|
||||
SetViewportView { .. } => "SetViewportView",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ pub enum QueryResult {
|
||||
Attributes(Vec<(String, String)>),
|
||||
/// The entities inside a block definition (read-only traversal).
|
||||
BlockEntities(Vec<EntityView>),
|
||||
/// A viewport's view: target point (WCS) + zoom height.
|
||||
ViewportView { target: [f64; 3], height: f64 },
|
||||
}
|
||||
|
||||
/// Convenience: the query name for diagnostics.
|
||||
@@ -71,6 +73,7 @@ impl crate::gen::Query {
|
||||
GetDimensionMeasurement { .. } => "GetDimensionMeasurement",
|
||||
GetAttributes { .. } => "GetAttributes",
|
||||
GetBlockEntities { .. } => "GetBlockEntities",
|
||||
GetViewportView { .. } => "GetViewportView",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ struct MockBackend {
|
||||
dimension_measurements: HashMap<ObjectId, f64>,
|
||||
/// Stored insert attributes.
|
||||
attributes_store: HashMap<ObjectId, Vec<(String, String)>>,
|
||||
/// Stored viewport views (target, height).
|
||||
viewport_views: HashMap<ObjectId, ([f64; 3], f64)>,
|
||||
}
|
||||
|
||||
impl MockBackend {
|
||||
@@ -78,8 +80,10 @@ impl DocApiBackend for MockBackend {
|
||||
}
|
||||
Ok(self.alloc("Insert"))
|
||||
}
|
||||
fn add_viewport(&mut self, _spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||
Ok(self.alloc("Viewport"))
|
||||
fn add_viewport(&mut self, spec: &ocs_doc_api::ops::ViewportSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Viewport");
|
||||
self.viewport_views.insert(id, (spec.view_target, spec.view_height));
|
||||
Ok(id)
|
||||
}
|
||||
fn add_text(&mut self, spec: &ocs_doc_api::ops::TextSpec) -> ApiResult<ObjectId> {
|
||||
let id = self.alloc("Text");
|
||||
@@ -140,6 +144,16 @@ impl DocApiBackend for MockBackend {
|
||||
fn block_entities(&self, _block_name: &str) -> ApiResult<Vec<EntityView>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
fn set_viewport_view(&mut self, id: ObjectId, view_target: [f64; 3], view_height: f64) -> ApiResult<()> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
self.viewport_views.insert(id, (view_target, view_height));
|
||||
Ok(())
|
||||
}
|
||||
fn viewport_view(&self, id: ObjectId) -> ApiResult<([f64; 3], f64)> {
|
||||
self.viewport_views.get(&id).copied().ok_or(ApiError::UnknownId(id))
|
||||
}
|
||||
}
|
||||
fn add_vertex(&mut self, id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
if self.entity_exists(id) {
|
||||
|
||||
@@ -59,6 +59,7 @@ fn operation_variants() -> Vec<String> {
|
||||
Operation::CreateHatch(ocs_doc_api::ops::HatchSpec { boundary: vec![], solid: true }),
|
||||
Operation::CreateDimensionLinear(ocs_doc_api::ops::DimensionSpec { first_point: [0.0; 3], second_point: [0.0; 3], definition_point: [0.0; 3] }),
|
||||
Operation::SetAttribute { id: ObjectId::from_u64(0), tag: String::new(), value: String::new() },
|
||||
Operation::SetViewportView { id: ObjectId::from_u64(0), view_target: [0.0; 3], view_height: 0.0 },
|
||||
];
|
||||
// Derive the variant names by serializing a probe of each and reading the
|
||||
// bincode variant index -> name map is not available; instead match by
|
||||
@@ -88,6 +89,7 @@ fn query_variants() -> Vec<String> {
|
||||
Query::GetDimensionMeasurement { id: ObjectId::from_u64(0) },
|
||||
Query::GetAttributes { id: ObjectId::from_u64(0) },
|
||||
Query::GetBlockEntities { block_name: String::new() },
|
||||
Query::GetViewportView { id: ObjectId::from_u64(0) },
|
||||
];
|
||||
marker
|
||||
.iter()
|
||||
@@ -144,6 +146,7 @@ const OPERATION_BASELINE: &[&str] = &[
|
||||
"CreateHatch",
|
||||
"CreateDimensionLinear",
|
||||
"SetAttribute",
|
||||
"SetViewportView",
|
||||
];
|
||||
|
||||
/// Recorded baseline of the `Query` variant order.
|
||||
@@ -159,6 +162,7 @@ const QUERY_BASELINE: &[&str] = &[
|
||||
"GetDimensionMeasurement",
|
||||
"GetAttributes",
|
||||
"GetBlockEntities",
|
||||
"GetViewportView",
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -311,6 +311,28 @@ impl DocApiBackend for HostSession<'_> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn set_viewport_view(&mut self, id: ObjectId, view_target: [f64; 3], view_height: f64) -> ApiResult<()> {
|
||||
let handle = obj_to_handle(id);
|
||||
let entity = self.document().get_entity(handle).cloned().ok_or(ApiError::UnknownId(id))?;
|
||||
let EntityType::Viewport(mut vp) = entity else {
|
||||
return Err(ApiError::Unsupported("SetViewportView is only for Viewport".into()));
|
||||
};
|
||||
vp.view_target = acadrust::types::Vector3::new(view_target[0], view_target[1], view_target[2]);
|
||||
vp.view_height = view_height;
|
||||
if !self.scene_mut().update_entity(EntityType::Viewport(vp)) {
|
||||
return Err(ApiError::Unsupported(format!("entity {id:?} is on a locked layer")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn viewport_view(&self, id: ObjectId) -> ApiResult<([f64; 3], f64)> {
|
||||
let entity = self.document().get_entity(obj_to_handle(id)).ok_or(ApiError::UnknownId(id))?;
|
||||
let EntityType::Viewport(vp) = entity else {
|
||||
return Err(ApiError::Unsupported("GetViewportView is only for Viewport".into()));
|
||||
};
|
||||
Ok(([vp.view_target.x, vp.view_target.y, vp.view_target.z], vp.view_height))
|
||||
}
|
||||
|
||||
fn add_vertex(&mut self, id: ObjectId, at: usize, point: [f64; 3]) -> ApiResult<()> {
|
||||
let handle = obj_to_handle(id);
|
||||
let Some(entity) = self.document().get_entity(handle).cloned() else {
|
||||
@@ -978,6 +1000,40 @@ mod tests {
|
||||
assert!((t.insertion_point.x - 15.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
// ── Phase 4 remaining: set_view + viewport-view query ────────────────────
|
||||
|
||||
#[test]
|
||||
fn phase4_set_view_and_view_query() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
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());
|
||||
|
||||
// set_view retargets + re-zooms in place.
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::SetViewportView {
|
||||
id: vp, view_target: [20.0, 30.0, 0.0], view_height: 50.0 })).unwrap();
|
||||
let Some(EntityType::Viewport(v)) = host.document().get_entity(obj_to_handle(vp)) else { panic!("viewport not found") };
|
||||
assert!((v.view_target.x - 20.0).abs() < 1e-6 && (v.view_height - 50.0).abs() < 1e-6);
|
||||
|
||||
// viewport_view query reads it back.
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetViewportView { id: vp }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::ViewportView { target, height } => {
|
||||
assert!((target[0] - 20.0).abs() < 1e-6 && (*height - 50.0).abs() < 1e-6);
|
||||
}
|
||||
other => panic!("expected viewport view, got {other:?}"),
|
||||
}
|
||||
|
||||
// set_view on a non-viewport is Unsupported.
|
||||
let line = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::Line { start: [0.0; 3], end: [1.0; 3] }))).unwrap());
|
||||
assert!(dispatch(&mut host, DocApiEnvelope::op(Operation::SetViewportView {
|
||||
id: line, view_target: [0.0; 3], view_height: 1.0 })).is_err());
|
||||
}
|
||||
|
||||
// ── Phase 3 remaining: attributes + nested block traversal ──────────────
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user