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
@@ -197,6 +197,47 @@ impl DocApiBackend for HostSession<'_> {
|
||||
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<()> {
|
||||
let handle = obj_to_handle(id);
|
||||
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);
|
||||
}
|
||||
|
||||
// ── 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) ──────────────────────────────────
|
||||
|
||||
#[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],
|
||||
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.
|
||||
_ => return Err(ApiError::Unsupported("bounds for this entity family is not yet implemented".into())),
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user