forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 2b-b (Hatch)
- CreateHatch(HatchSpec) op + GetHatchBoundary query (append-only).
- HatchSpec{boundary: Vec<[f64;2]>, solid}; executor validates >= 3 boundary points.
- backend add_hatch (Line-edge BoundaryPath, solid/pattern) + hatch_boundary
(reconstruct loops from Line edges; non-line edges -> Unsupported).
- Hatch bounds from boundary edges; facade create_hatch + Entity handle.
- spec Hatch family; api_reference + binding_schema regenerated; mocks + baseline.
- Test phase2b_hatch_create_boundary_bounds_delete -> 606 host lib + 8 crate + 3 consistency green.
This commit is contained in:
1 parent
fc0e2c2c9d
commit
ba8e5ed0cd
16 files changed
+223
-3
No files matched your search
@@ -65,8 +65,10 @@ Phase 2b (annotations/hatch/dimension typed ops), attributes + nested traversal
|
|||||||
(`CreateText`/`CreateMText` wire ops), `content()`/`set_content()` (new
|
(`CreateText`/`CreateMText` wire ops), `content()`/`set_content()` (new
|
||||||
`GetTextContent` query + `SetTextContent` op), transform (insertion point),
|
`GetTextContent` query + `SetTextContent` op), transform (insertion point),
|
||||||
coarse bounds. Covered by `phase2b_*` test.
|
coarse bounds. Covered by `phase2b_*` test.
|
||||||
- **Remaining (2b-b/c):** `Hatch` (create + boundary query), `Dimension` (typed
|
- **Done (2b-b):** `Hatch` — `create_hatch(boundary, solid)` (`CreateHatch`,
|
||||||
sub-types).
|
validates ≥3 points), `boundary()` → loops (`GetHatchBoundary`), bounds from
|
||||||
|
boundary edges, delete. Covered by `phase2b_hatch_*` test.
|
||||||
|
- **Remaining (2b-c):** `Dimension` (typed sub-types).
|
||||||
- **Unblocked:** `add_vertex` (polyline) and sweep profiles (Line/Circle/Arc/LwPolyline)
|
- **Unblocked:** `add_vertex` (polyline) and sweep profiles (Line/Circle/Arc/LwPolyline)
|
||||||
are supported (Phase 0).
|
are supported (Phase 0).
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ pub struct MockBackend {
|
|||||||
curves: HashMap<ObjectId, Curve2Spec>,
|
curves: HashMap<ObjectId, Curve2Spec>,
|
||||||
/// Stored text content for Text/MText annotations.
|
/// Stored text content for Text/MText annotations.
|
||||||
text_values: HashMap<ObjectId, String>,
|
text_values: HashMap<ObjectId, String>,
|
||||||
|
/// Stored hatch boundary loops.
|
||||||
|
hatch_boundaries: HashMap<ObjectId, Vec<Vec<[f64; 2]>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockBackend {
|
impl MockBackend {
|
||||||
@@ -156,6 +158,14 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn text_content(&self, id: ObjectId) -> ApiResult<String> {
|
fn text_content(&self, id: ObjectId) -> ApiResult<String> {
|
||||||
self.text_values.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
self.text_values.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||||
}
|
}
|
||||||
|
fn add_hatch(&mut self, spec: &ocs_doc_api::ops::HatchSpec) -> ApiResult<ObjectId> {
|
||||||
|
let id = self.alloc("Hatch");
|
||||||
|
self.hatch_boundaries.insert(id, vec![spec.boundary.clone()]);
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
||||||
|
self.hatch_boundaries.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||||
|
}
|
||||||
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(())
|
||||||
|
|||||||
@@ -290,6 +290,27 @@ op = "SetTextContent"
|
|||||||
args = [{ name = "value", ty = "&str" }]
|
args = [{ name = "value", ty = "&str" }]
|
||||||
returns = "()"
|
returns = "()"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# HATCH FAMILY (phase 2b) — filled regions
|
||||||
|
# ============================================================================
|
||||||
|
[[family]]
|
||||||
|
name = "Hatch"
|
||||||
|
acadrust_variant = "Hatch"
|
||||||
|
handle = "Entity"
|
||||||
|
collection = "curves"
|
||||||
|
description = "Filled region over a closed polyline boundary. boundary() returns the loops; bounds from boundary; generic delete."
|
||||||
|
|
||||||
|
[[family.constructor]]
|
||||||
|
name = "create_hatch"
|
||||||
|
args = [{ name = "boundary", ty = "&[[f64; 2]]" }, { name = "solid", ty = "bool" }]
|
||||||
|
op = "CreateHatch"
|
||||||
|
|
||||||
|
[[family.method]]
|
||||||
|
name = "boundary"
|
||||||
|
kind = "query"
|
||||||
|
query = "GetHatchBoundary"
|
||||||
|
returns = "Vec<Vec<[f64; 2]>>"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONTAINER FAMILY (phase 3) — block references
|
# CONTAINER FAMILY (phase 3) — block references
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ pub trait DocApiBackend {
|
|||||||
/// The text content of a Text/MText annotation. `Unsupported` for other kinds.
|
/// The text content of a Text/MText annotation. `Unsupported` for other kinds.
|
||||||
fn text_content(&self, id: ObjectId) -> ApiResult<String>;
|
fn text_content(&self, id: ObjectId) -> ApiResult<String>;
|
||||||
|
|
||||||
|
/// Add a `HATCH` (single closed polyline boundary); returns the fresh `ObjectId`.
|
||||||
|
fn add_hatch(&mut self, spec: &crate::ops::HatchSpec) -> ApiResult<ObjectId>;
|
||||||
|
|
||||||
|
/// A hatch's boundary loops (outer + islands) as closed polylines.
|
||||||
|
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>>;
|
||||||
|
|
||||||
/// Can `id` be modified in place right now (exists, is the expected family,
|
/// 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.
|
/// not on a locked layer)? Read-only pre-check used before mutations.
|
||||||
/// Default: existence + not-locked (backends narrow the family check).
|
/// Default: existence + not-locked (backends narrow the family check).
|
||||||
|
|||||||
@@ -211,6 +211,16 @@ pub fn apply_op<B: DocApiBackend>(b: &mut B, op: Operation) -> ApiResult<Receipt
|
|||||||
b.finalize_op();
|
b.finalize_op();
|
||||||
OpOutcome::Updated(*id)
|
OpOutcome::Updated(*id)
|
||||||
}
|
}
|
||||||
|
Operation::CreateHatch(spec) => {
|
||||||
|
// Validate the boundary (>= 3 points) before committing.
|
||||||
|
if spec.boundary.len() < 3 {
|
||||||
|
return Err(ApiError::validation(name, "hatch boundary needs >= 3 points"));
|
||||||
|
}
|
||||||
|
b.push_undo(name);
|
||||||
|
let id = b.add_hatch(spec)?;
|
||||||
|
b.finalize_op();
|
||||||
|
OpOutcome::NewId(id)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(Receipt {
|
Ok(Receipt {
|
||||||
outcome: Some(outcome),
|
outcome: Some(outcome),
|
||||||
@@ -249,6 +259,7 @@ pub fn apply_queries<B: DocApiBackend>(b: &mut B, queries: Vec<Query>) -> ApiRes
|
|||||||
}
|
}
|
||||||
Query::GetGeometryRevision => QueryResult::Revision(b.revision()),
|
Query::GetGeometryRevision => QueryResult::Revision(b.revision()),
|
||||||
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(id)?),
|
Query::GetTextContent { id } => QueryResult::TextContent(b.text_content(id)?),
|
||||||
|
Query::GetHatchBoundary { id } => QueryResult::HatchBoundary(b.hatch_boundary(id)?),
|
||||||
};
|
};
|
||||||
results.push(r);
|
results.push(r);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -465,6 +465,16 @@ impl CurveCollection {
|
|||||||
let (s, id) = self.create_curve(Curve2Spec::XLine { origin, direction })?;
|
let (s, id) = self.create_curve(Curve2Spec::XLine { origin, direction })?;
|
||||||
Ok(XLine::new(s, id))
|
Ok(XLine::new(s, id))
|
||||||
}
|
}
|
||||||
|
/// A solid (or pattern) HATCH over a single closed polyline boundary.
|
||||||
|
pub fn create_hatch(&self, boundary: &[[f64; 2]], solid: bool) -> ApiResult<Entity> {
|
||||||
|
let receipt = self.session.apply_op(Operation::CreateHatch(crate::ops::HatchSpec {
|
||||||
|
boundary: boundary.to_vec(),
|
||||||
|
solid,
|
||||||
|
}))?;
|
||||||
|
let id = receipt.outcome.and_then(|o| o.new_id()).ok_or_else(|| ApiError::Transport("create_hatch returned no id".into()))?;
|
||||||
|
Ok(Entity::new(self.session.clone(), id))
|
||||||
|
}
|
||||||
|
|
||||||
/// Single-line TEXT annotation.
|
/// Single-line TEXT annotation.
|
||||||
pub fn create_text(&self, value: &str, insertion_point: [f64; 3], height: f64, rotation: f64) -> ApiResult<Text> {
|
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 {
|
let receipt = self.session.apply_op(Operation::CreateText(crate::ops::TextSpec {
|
||||||
|
|||||||
@@ -113,6 +113,18 @@ Multi-line text annotation. content()/set_content() read+replace the value; tran
|
|||||||
- **content**(``) -> `String` — query `GetTextContent`
|
- **content**(``) -> `String` — query `GetTextContent`
|
||||||
- **set_content**(`value: &str`) -> `()` — op `SetTextContent`
|
- **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`
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
- **boundary**(``) -> `Vec<Vec<[f64; 2]>>` — query `GetHatchBoundary`
|
||||||
|
|
||||||
## `Entity` (acadrust `Insert`, collection `entities`)
|
## `Entity` (acadrust `Insert`, collection `entities`)
|
||||||
|
|
||||||
Block reference. Place an existing BlockRecord by name; transform moves its insert point.
|
Block reference. Place an existing BlockRecord by name; transform moves its insert point.
|
||||||
|
|||||||
@@ -258,6 +258,25 @@
|
|||||||
],
|
],
|
||||||
"name": "MText"
|
"name": "MText"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"acadrust_variant": "Hatch",
|
||||||
|
"collection": "curves",
|
||||||
|
"handle": "Entity",
|
||||||
|
"methods": [
|
||||||
|
{
|
||||||
|
"args": [],
|
||||||
|
"bulk": false,
|
||||||
|
"fixed": null,
|
||||||
|
"kind": "query",
|
||||||
|
"name": "boundary",
|
||||||
|
"op": null,
|
||||||
|
"query": "GetHatchBoundary",
|
||||||
|
"read_only": true,
|
||||||
|
"returns": "Vec<Vec<[f64; 2]>>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Hatch"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"acadrust_variant": "Insert",
|
"acadrust_variant": "Insert",
|
||||||
"collection": "entities",
|
"collection": "entities",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::id::ObjectId;
|
use crate::id::ObjectId;
|
||||||
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, InsertSpec, MTextSpec, PlacementSpec, SolidPrimitive, TextSpec, ViewportSpec};
|
use crate::ops::{BoolOp, Curve2Spec, EntitySpec, HatchSpec, InsertSpec, MTextSpec, PlacementSpec, SolidPrimitive, TextSpec, 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.
|
||||||
@@ -36,4 +36,6 @@ pub enum Operation {
|
|||||||
CreateText(TextSpec),
|
CreateText(TextSpec),
|
||||||
CreateMText(MTextSpec),
|
CreateMText(MTextSpec),
|
||||||
SetTextContent { id: ObjectId, value: String },
|
SetTextContent { id: ObjectId, value: String },
|
||||||
|
// hatch
|
||||||
|
CreateHatch(HatchSpec),
|
||||||
}
|
}
|
||||||
@@ -21,4 +21,6 @@ pub enum Query {
|
|||||||
GetGeometryRevision,
|
GetGeometryRevision,
|
||||||
/// The text content of a Text/MText annotation.
|
/// The text content of a Text/MText annotation.
|
||||||
GetTextContent { id: ObjectId },
|
GetTextContent { id: ObjectId },
|
||||||
|
/// The boundary loops of a Hatch (outer + islands) as polylines.
|
||||||
|
GetHatchBoundary { id: ObjectId },
|
||||||
}
|
}
|
||||||
@@ -96,6 +96,16 @@ pub struct MTextSpec {
|
|||||||
pub height: f64,
|
pub height: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construction spec for a `HATCH` with a single closed polyline boundary
|
||||||
|
/// (straight segments). `solid` selects solid fill vs a pattern (pattern naming
|
||||||
|
/// is a later refinement).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct HatchSpec {
|
||||||
|
/// Closed boundary as an ordered polyline (first==last implied; closed).
|
||||||
|
pub boundary: Vec<[f64; 2]>,
|
||||||
|
pub solid: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
@@ -148,6 +158,7 @@ impl crate::gen::Operation {
|
|||||||
CreateText(_) => "CreateText",
|
CreateText(_) => "CreateText",
|
||||||
CreateMText(_) => "CreateMText",
|
CreateMText(_) => "CreateMText",
|
||||||
SetTextContent { .. } => "SetTextContent",
|
SetTextContent { .. } => "SetTextContent",
|
||||||
|
CreateHatch(_) => "CreateHatch",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,8 @@ pub enum QueryResult {
|
|||||||
Revision(GeometryRevision),
|
Revision(GeometryRevision),
|
||||||
/// The text content of a Text/MText annotation.
|
/// The text content of a Text/MText annotation.
|
||||||
TextContent(String),
|
TextContent(String),
|
||||||
|
/// A hatch's boundary loops (outer + islands), each a closed polyline.
|
||||||
|
HatchBoundary(Vec<Vec<[f64; 2]>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience: the query name for diagnostics.
|
/// Convenience: the query name for diagnostics.
|
||||||
@@ -59,6 +61,7 @@ impl crate::gen::Query {
|
|||||||
GetIntersects { .. } => "GetIntersects",
|
GetIntersects { .. } => "GetIntersects",
|
||||||
GetGeometryRevision => "GetGeometryRevision",
|
GetGeometryRevision => "GetGeometryRevision",
|
||||||
GetTextContent { .. } => "GetTextContent",
|
GetTextContent { .. } => "GetTextContent",
|
||||||
|
GetHatchBoundary { .. } => "GetHatchBoundary",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,8 @@ struct MockBackend {
|
|||||||
kinds: HashMap<ObjectId, String>,
|
kinds: HashMap<ObjectId, String>,
|
||||||
/// Stored text content for Text/MText annotations.
|
/// Stored text content for Text/MText annotations.
|
||||||
text_values: HashMap<ObjectId, String>,
|
text_values: HashMap<ObjectId, String>,
|
||||||
|
/// Stored hatch boundary loops.
|
||||||
|
hatch_boundaries: HashMap<ObjectId, Vec<Vec<[f64; 2]>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockBackend {
|
impl MockBackend {
|
||||||
@@ -96,6 +98,14 @@ impl DocApiBackend for MockBackend {
|
|||||||
fn text_content(&self, id: ObjectId) -> ApiResult<String> {
|
fn text_content(&self, id: ObjectId) -> ApiResult<String> {
|
||||||
self.text_values.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
self.text_values.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||||
}
|
}
|
||||||
|
fn add_hatch(&mut self, spec: &ocs_doc_api::ops::HatchSpec) -> ApiResult<ObjectId> {
|
||||||
|
let id = self.alloc("Hatch");
|
||||||
|
self.hatch_boundaries.insert(id, vec![spec.boundary.clone()]);
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
||||||
|
self.hatch_boundaries.get(&id).cloned().ok_or(ApiError::UnknownId(id))
|
||||||
|
}
|
||||||
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(())
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ fn operation_variants() -> Vec<String> {
|
|||||||
Operation::CreateText(ocs_doc_api::ops::TextSpec { value: String::new(), insertion_point: [0.0; 3], height: 0.0, rotation: 0.0 }),
|
Operation::CreateText(ocs_doc_api::ops::TextSpec { value: String::new(), insertion_point: [0.0; 3], height: 0.0, rotation: 0.0 }),
|
||||||
Operation::CreateMText(ocs_doc_api::ops::MTextSpec { value: String::new(), insertion_point: [0.0; 3], height: 0.0 }),
|
Operation::CreateMText(ocs_doc_api::ops::MTextSpec { value: String::new(), insertion_point: [0.0; 3], height: 0.0 }),
|
||||||
Operation::SetTextContent { id: ObjectId::from_u64(0), value: String::new() },
|
Operation::SetTextContent { id: ObjectId::from_u64(0), value: String::new() },
|
||||||
|
Operation::CreateHatch(ocs_doc_api::ops::HatchSpec { boundary: vec![], solid: true }),
|
||||||
];
|
];
|
||||||
// Derive the variant names by serializing a probe of each and reading the
|
// Derive the variant names by serializing a probe of each and reading the
|
||||||
// bincode variant index -> name map is not available; instead match by
|
// bincode variant index -> name map is not available; instead match by
|
||||||
@@ -81,6 +82,7 @@ fn query_variants() -> Vec<String> {
|
|||||||
Query::GetIntersects { a: ObjectId::from_u64(0), b: ObjectId::from_u64(0) },
|
Query::GetIntersects { a: ObjectId::from_u64(0), b: ObjectId::from_u64(0) },
|
||||||
Query::GetGeometryRevision,
|
Query::GetGeometryRevision,
|
||||||
Query::GetTextContent { id: ObjectId::from_u64(0) },
|
Query::GetTextContent { id: ObjectId::from_u64(0) },
|
||||||
|
Query::GetHatchBoundary { id: ObjectId::from_u64(0) },
|
||||||
];
|
];
|
||||||
marker
|
marker
|
||||||
.iter()
|
.iter()
|
||||||
@@ -134,6 +136,7 @@ const OPERATION_BASELINE: &[&str] = &[
|
|||||||
"CreateText",
|
"CreateText",
|
||||||
"CreateMText",
|
"CreateMText",
|
||||||
"SetTextContent",
|
"SetTextContent",
|
||||||
|
"CreateHatch",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Recorded baseline of the `Query` variant order.
|
/// Recorded baseline of the `Query` variant order.
|
||||||
@@ -145,6 +148,7 @@ const QUERY_BASELINE: &[&str] = &[
|
|||||||
"GetIntersects",
|
"GetIntersects",
|
||||||
"GetGeometryRevision",
|
"GetGeometryRevision",
|
||||||
"GetTextContent",
|
"GetTextContent",
|
||||||
|
"GetHatchBoundary",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -197,6 +197,47 @@ impl DocApiBackend for HostSession<'_> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_hatch(&mut self, spec: &ocs_doc_api::ops::HatchSpec) -> ApiResult<ObjectId> {
|
||||||
|
use acadrust::entities::{BoundaryEdge, BoundaryPath, Hatch, LineEdge};
|
||||||
|
use acadrust::types::Vector2;
|
||||||
|
let mut hatch = if spec.solid { Hatch::solid() } else { Hatch::default() };
|
||||||
|
// Build a BoundaryPath of Line edges around the closed polyline.
|
||||||
|
let n = spec.boundary.len();
|
||||||
|
let mut path = BoundaryPath::new();
|
||||||
|
for i in 0..n {
|
||||||
|
let start = spec.boundary[i];
|
||||||
|
let end = spec.boundary[(i + 1) % n];
|
||||||
|
path.edges.push(BoundaryEdge::Line(LineEdge {
|
||||||
|
start: Vector2::new(start[0], start[1]),
|
||||||
|
end: Vector2::new(end[0], end[1]),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
hatch.paths.push(path);
|
||||||
|
let handle = self.scene_mut().add_entity(EntityType::Hatch(hatch));
|
||||||
|
Ok(handle_to_obj(handle))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hatch_boundary(&self, id: ObjectId) -> ApiResult<Vec<Vec<[f64; 2]>>> {
|
||||||
|
let entity = self.document().get_entity(obj_to_handle(id)).ok_or(ApiError::UnknownId(id))?;
|
||||||
|
let EntityType::Hatch(h) = entity else {
|
||||||
|
return Err(ApiError::Unsupported("GetHatchBoundary is only for Hatch".into()));
|
||||||
|
};
|
||||||
|
// Reconstruct each boundary loop's vertices from its Line edges.
|
||||||
|
let mut loops = Vec::with_capacity(h.paths.len());
|
||||||
|
for path in &h.paths {
|
||||||
|
let mut pts = Vec::with_capacity(path.edges.len());
|
||||||
|
for edge in &path.edges {
|
||||||
|
match edge {
|
||||||
|
acadrust::entities::BoundaryEdge::Line(le) => pts.push([le.start.x, le.start.y]),
|
||||||
|
// Arc/ellipse boundary edges are a later refinement (no vertex list).
|
||||||
|
_ => return Err(ApiError::Unsupported("hatch boundary contains non-line edges".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loops.push(pts);
|
||||||
|
}
|
||||||
|
Ok(loops)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -864,6 +905,42 @@ mod tests {
|
|||||||
assert!((t.insertion_point.x - 15.0).abs() < 1e-6);
|
assert!((t.insertion_point.x - 15.0).abs() < 1e-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Phase 2b-b: hatch ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn phase2b_hatch_create_boundary_bounds_delete() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
let mut host = HostSession::new(&mut app, 0);
|
||||||
|
// Solid hatch over a unit square boundary.
|
||||||
|
let sq = vec![[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]];
|
||||||
|
let hatch = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateHatch(
|
||||||
|
ocs_doc_api::ops::HatchSpec { boundary: sq.clone(), solid: true }))).unwrap());
|
||||||
|
|
||||||
|
// Boundary round-trips (one loop, 4 vertices).
|
||||||
|
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![
|
||||||
|
Query::GetHatchBoundary { id: hatch }, Query::GetBounds { id: hatch }])).unwrap();
|
||||||
|
match &receipt.query_results[0] {
|
||||||
|
QueryResult::HatchBoundary(loops) => {
|
||||||
|
assert_eq!(loops.len(), 1);
|
||||||
|
assert_eq!(loops[0].len(), 4);
|
||||||
|
assert!((loops[0][0][0] - 0.0).abs() < 1e-9 && (loops[0][2][0] - 10.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
other => panic!("expected boundary, got {other:?}"),
|
||||||
|
}
|
||||||
|
match &receipt.query_results[1] {
|
||||||
|
QueryResult::Bounds(b) => assert!((b.min[0] - 0.0).abs() < 1e-6 && (b.max[1] - 10.0).abs() < 1e-6, "{b:?}"),
|
||||||
|
other => panic!("expected bounds, got {other:?}"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boundary with < 3 points is a validation error.
|
||||||
|
assert!(dispatch(&mut host, DocApiEnvelope::op(Operation::CreateHatch(
|
||||||
|
ocs_doc_api::ops::HatchSpec { boundary: vec![[0.0, 0.0], [1.0, 1.0]], solid: true }))).is_err());
|
||||||
|
|
||||||
|
// Generic delete works.
|
||||||
|
dispatch(&mut host, DocApiEnvelope::op(Operation::Delete { id: hatch })).unwrap();
|
||||||
|
assert!(host.document().get_entity(obj_to_handle(hatch)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
// ── Phase 5: media & misc (read-mostly) ──────────────────────────────────
|
// ── Phase 5: media & misc (read-mostly) ──────────────────────────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -412,6 +412,26 @@ pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult<Aab
|
|||||||
min: [t.insertion_point.x, t.insertion_point.y - t.height, t.insertion_point.z],
|
min: [t.insertion_point.x, t.insertion_point.y - t.height, t.insertion_point.z],
|
||||||
max: [t.insertion_point.x + t.height * t.value.len() as f64, t.insertion_point.y, t.insertion_point.z],
|
max: [t.insertion_point.x + t.height * t.value.len() as f64, t.insertion_point.y, t.insertion_point.z],
|
||||||
},
|
},
|
||||||
|
EntityType::Hatch(h) => {
|
||||||
|
let mut min = [f64::INFINITY; 3];
|
||||||
|
let mut max = [f64::NEG_INFINITY; 3];
|
||||||
|
let mut any = false;
|
||||||
|
for path in &h.paths {
|
||||||
|
for edge in &path.edges {
|
||||||
|
if let acadrust::entities::BoundaryEdge::Line(le) = edge {
|
||||||
|
for (x, y) in [(le.start.x, le.start.y), (le.end.x, le.end.y)] {
|
||||||
|
min = [min[0].min(x), min[1].min(y), min[2].min(0.0)];
|
||||||
|
max = [max[0].max(x), max[1].max(y), max[2].max(0.0)];
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !any {
|
||||||
|
return Err(ApiError::Unsupported("hatch has no line-edge boundary".into()));
|
||||||
|
}
|
||||||
|
Aabb { min, max }
|
||||||
|
}
|
||||||
// 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