forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: Phase 0 + Phase 2a (sweep profiles, non-solid transform, full 2D curves)
Phase 0 (outstanding supported-family methods): - add_vertex on LwPolyline (insert vertex, out-of-range validation). - extrude / revolve from profiles (entity_to_profile_curves: Line/Circle/Arc/ LwPolyline straight-segment conversion -> geom2d::Curve -> brep::extrude/revolve). - non-solid transform + transform_many via acadrust geometry (transform_entity_geometry for Line/Circle/Arc/Point/LwPolyline). - ensure_transformable broadened accordingly (all-or-nothing pre-validation). Phase 2a (full 2D curve families): ArcCurve/Ellipse/Spline/Ray/XLine. - Curve2Spec extended (Arc/Ellipse/Spline/Ray/XLine) + create_arc/create_ellipse/ create_spline/create_ray/create_xline facade constructors + handle types. - entity_kind_name, entity_bounds (Arc coarse, Ellipse major-axis, Spline bbox; Ray/XLine unbounded), transform_entity_geometry, ensure_transformable extended. - spec/entities.toml families; api_reference.md + binding_schema.json regenerated. Tests: +6 doc_api integration tests (phase0_*, phase2_*) -> 598 host lib green. IMPLEMENTATION.md updated (Phase 0 done, Phase 2a done / 2b remaining).
This commit is contained in:
1 parent
e41086b058
commit
d46351cdb3
8 files changed
+647
-47
No files matched your search
@@ -11,8 +11,9 @@ content).
|
||||
| Capability | Method / op | Notes |
|
||||
|---|---|---|
|
||||
| Primitives | `create_cuboid` / `create_sphere` / `create_cylinder` / `create_cone` / `create_torus` / `create_wedge` | `CreateSolid(SolidPrimitive)` → `brep::make::*` |
|
||||
| Sweep | `extrude(profile, dir)` / `revolve(profile, pivot, axis, angle)` | `Extrude`/`Revolve` → `brep::extrude`/`brep::revolve`; profile = Line/Circle/Arc/LwPolyline |
|
||||
| Booleans | `intersect` / `union` / `subtract` | `SolidBoolean{op, a, b, erase_sources:true}` → `brep::combine` |
|
||||
| Transform | `transform(placement)` (in-place) | `Transform{id, placement}` → `brep::transform`; solids only |
|
||||
| Transform | `transform(placement)` (in-place) | `Transform{id, placement}` → `brep::transform`; solids |
|
||||
| Bounds | `bounds()` | `GetBounds` → `brep::body_bounds` (lift-on-miss) |
|
||||
| Volume | `volume()` | `GetVolume` → `meshes.metrics` cache, mesh-divergence fallback |
|
||||
| Centroid | `centroid()` | `GetCentroid` → `meshes.metrics` cache, mesh-divergence fallback |
|
||||
@@ -21,17 +22,19 @@ content).
|
||||
### Minimal 2D (phase-1 set, used as profiles/inputs)
|
||||
| Capability | Method / op | Notes |
|
||||
|---|---|---|
|
||||
| Line | `create_line` → bounds | `CreateCurve(Line)` |
|
||||
| Circle | `create_circle` → bounds | `CreateCurve(Circle)` |
|
||||
| Polyline | `create_polyline` → bounds | `CreateCurve(Polyline)` |
|
||||
| Point | `create_point` / `create_points` (bulk) | `CreateCurve(Point)` / `CreateMany` |
|
||||
| Line | `create_line` → bounds, transform | `CreateCurve(Line)` |
|
||||
| Circle | `create_circle` → bounds, transform | `CreateCurve(Circle)` |
|
||||
| Arc | (create via host) → transform, profile | usable as sweep profile |
|
||||
| Polyline | `create_polyline` → bounds, transform, `add_vertex` | `CreateCurve(Polyline)`; `AddVertex` inserts a vertex |
|
||||
| Point | `create_point` / `create_points` (bulk) → transform | `CreateCurve(Point)` / `CreateMany` |
|
||||
| Transform (2D) | `transform(placement)` / `transform_many` | `Line`/`Circle`/`Arc`/`Point`/`LwPolyline` via acadrust |
|
||||
|
||||
### Cross-cutting
|
||||
| Capability | Method | Notes |
|
||||
|---|---|---|
|
||||
| Lookup / downcast | `entities().get(id)` → `Entity`; `as_solid()` | generic, any family |
|
||||
| Bulk create | `create_points(&[..])`, `CreateMany` | all-or-nothing, one undo step |
|
||||
| Bulk transform | `transform_many(&ids, placement)` | all-or-nothing, solids only (v1) |
|
||||
| Bulk transform | `transform_many(&ids, placement)` | all-or-nothing, solids + 2D families |
|
||||
| Bulk delete | `delete_many(&ids)` | all-or-nothing; `OpGroup::compensate` uses it |
|
||||
| Read batch | `query_batch(\|q\| …)` | read-only, one round-trip, no bump |
|
||||
| Revision guard | `revision()` / `assert_revision(rev)` | read-modify-write guard |
|
||||
@@ -47,12 +50,15 @@ Phases are **spec-additive**: each is a `[[family]]`/`[[family.method]]` block i
|
||||
variants append at enum end.
|
||||
|
||||
### Phase 2 — full 2D + annotations
|
||||
- **Entities:** ellipse, spline, xline/ray, arc (typed), text, mtext, dimension,
|
||||
hatch.
|
||||
- **Methods:** boundary/vertex actions (`add_vertex` on polyline — currently
|
||||
`Unsupported`), hatch boundary query, text content get/set.
|
||||
- **Blocked by:** `AddVertex` backend impl (currently returns `Unsupported`);
|
||||
curve → `geom2d::Curve` conversion for non-trivial curves.
|
||||
- **Done (2a):** typed curve families `ArcCurve`/`Ellipse`/`Spline`/`Ray`/`XLine` —
|
||||
`create_arc`/`create_ellipse`/`create_spline`/`create_ray`/`create_xline`, bounds
|
||||
(Arc coarse full-circle, Ellipse major-axis radius, Spline control-point bbox;
|
||||
Ray/XLine unbounded → `Unsupported`), and transform (all five + the phase-1 set)
|
||||
via acadrust. Covered by `phase2_*` tests.
|
||||
- **Remaining (2b):** annotations `Text`/`MText` (create + content get/set), `Hatch`
|
||||
(create + boundary query), `Dimension` (typed sub-types).
|
||||
- **Unblocked:** `add_vertex` (polyline) and sweep profiles (Line/Circle/Arc/LwPolyline)
|
||||
are supported (Phase 0).
|
||||
|
||||
### Phase 3 — containers
|
||||
- **Entities:** insert/block references, attribute definitions/entities.
|
||||
@@ -75,13 +81,14 @@ variants append at enum end.
|
||||
|
||||
| Method | Family | Status | Blocker |
|
||||
|---|---|---|---|
|
||||
| `extrude(profile, dir)` | Solid | stub | `profile_curves` backend returns `Unsupported`; needs entity → `geom2d::Curve` conversion |
|
||||
| `revolve(profile, pivot, axis, angle)` | Solid | stub | same `profile_curves` blocker |
|
||||
| `loft(sections)` | Solid | not started | multi-profile kernel op + spec |
|
||||
| `add_vertex(at, pt)` | Polyline | stub | backend `add_vertex` returns `Unsupported` |
|
||||
| `transform` (non-solid) | 2D entities | `Unsupported` | per-family transform via acadrust (not kernel) |
|
||||
| Bulge-arc polyline profiles | Solid sweep | partial | `entity_to_profile_curves` emits straight segments; bulge arcs need arc-segment conversion |
|
||||
| `GetCentroid`/`GetVolume` accuracy | Solid | mesh approx | render-mesh LOD tolerance vs query tolerance |
|
||||
|
||||
**Completed (Phase 0):** `extrude`, `revolve` (profiles: Line/Circle/Arc/LwPolyline),
|
||||
`add_vertex` (polyline), non-solid `transform` + `transform_many` (Line/Circle/Arc/
|
||||
Point/LwPolyline). Covered by `phase0_*` tests in `src/app/doc_api.rs`.
|
||||
|
||||
## Design invariants to preserve when extending
|
||||
|
||||
- Per-op autocommit; **no multi-op write transactions**.
|
||||
|
||||
@@ -62,13 +62,11 @@ fixed = { primitive = "Wedge" }
|
||||
name = "extrude"
|
||||
args = [{ name = "profile", ty = "EntityRef" }, { name = "direction", ty = "[f64; 3]" }]
|
||||
op = "Extrude"
|
||||
status = "unsupported"
|
||||
|
||||
[[family.constructor]]
|
||||
name = "revolve"
|
||||
args = [{ name = "profile", ty = "EntityRef" }, { name = "pivot", ty = "[f64; 3]" }, { name = "axis", ty = "[f64; 3]" }, { name = "angle", ty = "f64" }]
|
||||
op = "Revolve"
|
||||
status = "unsupported"
|
||||
|
||||
[[family.method]]
|
||||
name = "intersect"
|
||||
@@ -158,7 +156,6 @@ kind = "op"
|
||||
op = "AddVertex"
|
||||
args = [{ name = "at", ty = "usize" }, { name = "point", ty = "[f64; 3]" }]
|
||||
returns = "()"
|
||||
status = "unsupported"
|
||||
|
||||
[[family]]
|
||||
name = "Point"
|
||||
@@ -180,6 +177,66 @@ fixed = { curve = "Point" }
|
||||
bulk = true
|
||||
returns = "Vec<Point>"
|
||||
|
||||
[[family]]
|
||||
name = "Arc"
|
||||
acadrust_variant = "Arc"
|
||||
handle = "ArcCurve"
|
||||
collection = "curves"
|
||||
|
||||
[[family.constructor]]
|
||||
name = "create_arc"
|
||||
args = [{ name = "centre", ty = "[f64; 3]" }, { name = "radius", ty = "f64" }, { name = "start_angle", ty = "f64" }, { name = "end_angle", ty = "f64" }]
|
||||
op = "CreateCurve"
|
||||
fixed = { curve = "Arc" }
|
||||
|
||||
[[family]]
|
||||
name = "Ellipse"
|
||||
acadrust_variant = "Ellipse"
|
||||
handle = "Ellipse"
|
||||
collection = "curves"
|
||||
|
||||
[[family.constructor]]
|
||||
name = "create_ellipse"
|
||||
args = [{ name = "centre", ty = "[f64; 3]" }, { name = "major_axis", ty = "[f64; 3]" }, { name = "ratio", ty = "f64" }, { name = "start", ty = "f64" }, { name = "end", ty = "f64" }]
|
||||
op = "CreateCurve"
|
||||
fixed = { curve = "Ellipse" }
|
||||
|
||||
[[family]]
|
||||
name = "Spline"
|
||||
acadrust_variant = "Spline"
|
||||
handle = "Spline"
|
||||
collection = "curves"
|
||||
|
||||
[[family.constructor]]
|
||||
name = "create_spline"
|
||||
args = [{ name = "degree", ty = "i32" }, { name = "control_points", ty = "&[[f64; 3]]" }, { name = "knots", ty = "&[f64]" }, { name = "weights", ty = "&[f64]" }]
|
||||
op = "CreateCurve"
|
||||
fixed = { curve = "Spline" }
|
||||
|
||||
[[family]]
|
||||
name = "Ray"
|
||||
acadrust_variant = "Ray"
|
||||
handle = "Ray"
|
||||
collection = "curves"
|
||||
|
||||
[[family.constructor]]
|
||||
name = "create_ray"
|
||||
args = [{ name = "origin", ty = "[f64; 3]" }, { name = "direction", ty = "[f64; 3]" }]
|
||||
op = "CreateCurve"
|
||||
fixed = { curve = "Ray" }
|
||||
|
||||
[[family]]
|
||||
name = "XLine"
|
||||
acadrust_variant = "XLine"
|
||||
handle = "XLine"
|
||||
collection = "curves"
|
||||
|
||||
[[family.constructor]]
|
||||
name = "create_xline"
|
||||
args = [{ name = "origin", ty = "[f64; 3]" }, { name = "direction", ty = "[f64; 3]" }]
|
||||
op = "CreateCurve"
|
||||
fixed = { curve = "XLine" }
|
||||
|
||||
# ============================================================================
|
||||
# Cross-cutting ops (generic handles + bulk) — documented in build.rs
|
||||
# ============================================================================
|
||||
|
||||
@@ -262,6 +262,11 @@ handle!(Line);
|
||||
handle!(Circle);
|
||||
handle!(Polyline);
|
||||
handle!(Point);
|
||||
handle!(ArcCurve);
|
||||
handle!(Ellipse);
|
||||
handle!(Spline);
|
||||
handle!(Ray);
|
||||
handle!(XLine);
|
||||
|
||||
impl Entity {
|
||||
pub fn view(&self) -> ApiResult<EntityView> {
|
||||
@@ -412,6 +417,31 @@ impl CurveCollection {
|
||||
let (s, id) = self.create_curve(Curve2Spec::Point { position })?;
|
||||
Ok(Point::new(s, id))
|
||||
}
|
||||
pub fn create_arc(&self, centre: [f64; 3], radius: f64, start_angle: f64, end_angle: f64) -> ApiResult<ArcCurve> {
|
||||
let (s, id) = self.create_curve(Curve2Spec::Arc { centre, radius, start_angle, end_angle })?;
|
||||
Ok(ArcCurve::new(s, id))
|
||||
}
|
||||
pub fn create_ellipse(&self, centre: [f64; 3], major_axis: [f64; 3], ratio: f64, start: f64, end: f64) -> ApiResult<Ellipse> {
|
||||
let (s, id) = self.create_curve(Curve2Spec::Ellipse { centre, major_axis, ratio, start, end })?;
|
||||
Ok(Ellipse::new(s, id))
|
||||
}
|
||||
pub fn create_spline(&self, degree: i32, control_points: &[[f64; 3]], knots: &[f64], weights: &[f64]) -> ApiResult<Spline> {
|
||||
let (s, id) = self.create_curve(Curve2Spec::Spline {
|
||||
degree,
|
||||
control_points: control_points.to_vec(),
|
||||
knots: knots.to_vec(),
|
||||
weights: weights.to_vec(),
|
||||
})?;
|
||||
Ok(Spline::new(s, id))
|
||||
}
|
||||
pub fn create_ray(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<Ray> {
|
||||
let (s, id) = self.create_curve(Curve2Spec::Ray { origin, direction })?;
|
||||
Ok(Ray::new(s, id))
|
||||
}
|
||||
pub fn create_xline(&self, origin: [f64; 3], direction: [f64; 3]) -> ApiResult<XLine> {
|
||||
let (s, id) = self.create_curve(Curve2Spec::XLine { origin, direction })?;
|
||||
Ok(XLine::new(s, id))
|
||||
}
|
||||
/// Bulk-create many points in ONE op (plan §5.3): all-or-nothing, one undo step.
|
||||
pub fn create_points(&self, positions: &[[f64; 3]]) -> ApiResult<Vec<Point>> {
|
||||
let specs: Vec<crate::ops::EntitySpec> = positions
|
||||
|
||||
@@ -16,8 +16,8 @@ B-rep solid. Host resolves bodies via the solid_models cache (lift-on-miss from
|
||||
- **create_cone**(`base: [f64; 3], radius: f64, height: f64`) -> `Solid`
|
||||
- **create_torus**(`centre: [f64; 3], major_radius: f64, minor_radius: f64`) -> `Solid`
|
||||
- **create_wedge**(`origin: [f64; 3], size: [f64; 3]`) -> `Solid`
|
||||
- **extrude**(`profile: EntityRef, direction: [f64; 3]`) -> `Solid` — **not yet supported** (returns `ApiError::Unsupported`)
|
||||
- **revolve**(`profile: EntityRef, pivot: [f64; 3], axis: [f64; 3], angle: f64`) -> `Solid` — **not yet supported** (returns `ApiError::Unsupported`)
|
||||
- **extrude**(`profile: EntityRef, direction: [f64; 3]`) -> `Solid`
|
||||
- **revolve**(`profile: EntityRef, pivot: [f64; 3], axis: [f64; 3], angle: f64`) -> `Solid`
|
||||
|
||||
### Methods
|
||||
|
||||
@@ -48,7 +48,7 @@ B-rep solid. Host resolves bodies via the solid_models cache (lift-on-miss from
|
||||
|
||||
### Methods
|
||||
|
||||
- **add_vertex**(`at: usize, point: [f64; 3]`) -> `()` — op `AddVertex` — **not yet supported** (returns `ApiError::Unsupported`)
|
||||
- **add_vertex**(`at: usize, point: [f64; 3]`) -> `()` — op `AddVertex`
|
||||
|
||||
## `Point` (acadrust `Point`, collection `curves`)
|
||||
|
||||
@@ -57,6 +57,36 @@ B-rep solid. Host resolves bodies via the solid_models cache (lift-on-miss from
|
||||
- **create_point**(`position: [f64; 3]`) -> `Point`
|
||||
- **create_points**(`positions: &[[f64; 3]]`) -> `Vec<Point>` *(bulk op)*
|
||||
|
||||
## `ArcCurve` (acadrust `Arc`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_arc**(`centre: [f64; 3], radius: f64, start_angle: f64, end_angle: f64`) -> `ArcCurve`
|
||||
|
||||
## `Ellipse` (acadrust `Ellipse`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_ellipse**(`centre: [f64; 3], major_axis: [f64; 3], ratio: f64, start: f64, end: f64`) -> `Ellipse`
|
||||
|
||||
## `Spline` (acadrust `Spline`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_spline**(`degree: i32, control_points: &[[f64; 3]], knots: &[f64], weights: &[f64]`) -> `Spline`
|
||||
|
||||
## `Ray` (acadrust `Ray`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_ray**(`origin: [f64; 3], direction: [f64; 3]`) -> `Ray`
|
||||
|
||||
## `XLine` (acadrust `XLine`, collection `curves`)
|
||||
|
||||
### Constructors (`doc.curves()`)
|
||||
|
||||
- **create_xline**(`origin: [f64; 3], direction: [f64; 3]`) -> `XLine`
|
||||
|
||||
## Generic methods (every handle)
|
||||
|
||||
Every typed handle (`Solid`, `Line`, `Circle`, `Polyline`, `Point`, `Entity`) is a
|
||||
|
||||
@@ -152,6 +152,41 @@
|
||||
"handle": "Point",
|
||||
"methods": [],
|
||||
"name": "Point"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "Arc",
|
||||
"collection": "curves",
|
||||
"handle": "ArcCurve",
|
||||
"methods": [],
|
||||
"name": "Arc"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "Ellipse",
|
||||
"collection": "curves",
|
||||
"handle": "Ellipse",
|
||||
"methods": [],
|
||||
"name": "Ellipse"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "Spline",
|
||||
"collection": "curves",
|
||||
"handle": "Spline",
|
||||
"methods": [],
|
||||
"name": "Spline"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "Ray",
|
||||
"collection": "curves",
|
||||
"handle": "Ray",
|
||||
"methods": [],
|
||||
"name": "Ray"
|
||||
},
|
||||
{
|
||||
"acadrust_variant": "XLine",
|
||||
"collection": "curves",
|
||||
"handle": "XLine",
|
||||
"methods": [],
|
||||
"name": "XLine"
|
||||
}
|
||||
],
|
||||
"layouts": {
|
||||
|
||||
@@ -34,6 +34,17 @@ pub enum Curve2Spec {
|
||||
Circle { centre: [f64; 3], radius: f64 },
|
||||
Polyline { points: Vec<[f64; 3]>, closed: bool },
|
||||
Point { position: [f64; 3] },
|
||||
/// Circular arc, counter-clockwise from `start_angle` to `end_angle` (radians).
|
||||
Arc { centre: [f64; 3], radius: f64, start_angle: f64, end_angle: f64 },
|
||||
/// Ellipse: `major_axis` is the major-axis endpoint relative to `centre`;
|
||||
/// `ratio` = minor/major. `start`/`end` parameters 0..2π (full ellipse = 0..2π).
|
||||
Ellipse { centre: [f64; 3], major_axis: [f64; 3], ratio: f64, start: f64, end: f64 },
|
||||
/// NURBS spline (control points + knots + weights; `degree` typically 3).
|
||||
Spline { degree: i32, control_points: Vec<[f64; 3]>, knots: Vec<f64>, weights: Vec<f64> },
|
||||
/// A ray from `origin` along `direction` (bounded at origin only).
|
||||
Ray { origin: [f64; 3], direction: [f64; 3] },
|
||||
/// An infinite construction line through `origin` along `direction`.
|
||||
XLine { origin: [f64; 3], direction: [f64; 3] },
|
||||
}
|
||||
|
||||
/// A generic entity-construction payload used by the bulk op
|
||||
|
||||
+252
-24
@@ -105,8 +105,28 @@ impl DocApiBackend for HostSession<'_> {
|
||||
Ok(handle_to_obj(handle))
|
||||
}
|
||||
|
||||
fn add_vertex(&mut self, _id: ObjectId, _at: usize, _point: [f64; 3]) -> ApiResult<()> {
|
||||
Err(ApiError::Unsupported("AddVertex is not yet implemented".into()))
|
||||
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 {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
};
|
||||
let EntityType::LwPolyline(mut pl) = entity else {
|
||||
return Err(ApiError::Unsupported("AddVertex is only implemented for polylines".into()));
|
||||
};
|
||||
if at > pl.vertices.len() {
|
||||
return Err(ApiError::validation(
|
||||
"AddVertex",
|
||||
format!("index {at} out of range ({} vertices)", pl.vertices.len()),
|
||||
));
|
||||
}
|
||||
pl.vertices.insert(
|
||||
at,
|
||||
acadrust::entities::LwVertex::from_coords(point[0], point[1]),
|
||||
);
|
||||
if !self.scene_mut().update_entity(EntityType::LwPolyline(pl)) {
|
||||
return Err(ApiError::Unsupported(format!("entity {id:?} is on a locked layer")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_entity(&mut self, id: ObjectId) -> ApiResult<bool> {
|
||||
@@ -126,15 +146,29 @@ impl DocApiBackend for HostSession<'_> {
|
||||
}
|
||||
|
||||
fn ensure_transformable(&mut self, id: ObjectId) -> ApiResult<()> {
|
||||
// v1 typed transform is solids-only; pre-validating this lets TransformMany
|
||||
// be all-or-nothing (no mid-loop Unsupported after earlier transforms).
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
if matches!(self.document().get_entity(obj_to_handle(id)), Some(EntityType::Solid3D(_))) {
|
||||
// Pre-validating this lets TransformMany be all-or-nothing. Solids and the
|
||||
// supported 2D families are transformable; anything else errors up front.
|
||||
let entity = self
|
||||
.document()
|
||||
.get_entity(obj_to_handle(id))
|
||||
.ok_or(ApiError::UnknownId(id))?;
|
||||
let ok = matches!(
|
||||
entity,
|
||||
EntityType::Solid3D(_)
|
||||
| EntityType::Line(_)
|
||||
| EntityType::Circle(_)
|
||||
| EntityType::Arc(_)
|
||||
| EntityType::Ellipse(_)
|
||||
| EntityType::Spline(_)
|
||||
| EntityType::Ray(_)
|
||||
| EntityType::XLine(_)
|
||||
| EntityType::Point(_)
|
||||
| EntityType::LwPolyline(_)
|
||||
);
|
||||
if ok {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::Unsupported("transform is only implemented for solids in v1".into()))
|
||||
Err(ApiError::Unsupported("transform is not supported for this entity family".into()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +201,8 @@ impl DocApiBackend for HostSession<'_> {
|
||||
if !self.entity_exists(id) {
|
||||
return Err(ApiError::UnknownId(id));
|
||||
}
|
||||
// Solids go through the kernel transform (re-embed SAT). Non-solids are
|
||||
// out of scope for v1 typed transforms.
|
||||
// Solids go through the kernel transform (re-embed SAT). 2D entities are
|
||||
// transformed via acadrust geometry and updated in place.
|
||||
if matches!(self.document().get_entity(handle), Some(EntityType::Solid3D(_))) {
|
||||
let body = self.resolve_body(id)?;
|
||||
let place = kernel_placement(placement);
|
||||
@@ -176,14 +210,25 @@ impl DocApiBackend for HostSession<'_> {
|
||||
.ok_or_else(|| ApiError::geometry(GeometryErrorKind::InvalidInput, "transform failed"))?;
|
||||
self.update_solid(id, &moved)
|
||||
} else {
|
||||
Err(ApiError::Unsupported("transform is only implemented for solids in v1".into()))
|
||||
let entity = self
|
||||
.document()
|
||||
.get_entity(handle)
|
||||
.cloned()
|
||||
.ok_or(ApiError::UnknownId(id))?;
|
||||
let moved = crate::app::doc_api_convert::transform_entity_geometry(&entity, placement)?;
|
||||
if !self.scene_mut().update_entity(moved) {
|
||||
return Err(ApiError::Unsupported(format!("entity {id:?} is on a locked layer")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_curves(&self, _id: ObjectId) -> ApiResult<Vec<cadkernel::geom2d::Curve>> {
|
||||
// Profiles for Extrude/Revolve land in a later phase (§5.2); v1 surfaces
|
||||
// a clear error rather than guessing.
|
||||
Err(ApiError::Unsupported("profile curves for sweep ops are not yet implemented".into()))
|
||||
fn profile_curves(&self, id: ObjectId) -> ApiResult<Vec<cadkernel::geom2d::Curve>> {
|
||||
let entity = self
|
||||
.document()
|
||||
.get_entity(obj_to_handle(id))
|
||||
.ok_or(ApiError::UnknownId(id))?;
|
||||
crate::app::doc_api_convert::entity_to_profile_curves(entity)
|
||||
}
|
||||
|
||||
fn bounds(&mut self, id: ObjectId) -> ApiResult<Aabb> {
|
||||
@@ -345,25 +390,23 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_api_transform_many_with_non_solid_fails_all_or_nothing() {
|
||||
use ocs_doc_api::ops::Curve2Spec;
|
||||
fn doc_api_transform_many_with_stale_id_fails_all_or_nothing() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
// A solid and a non-solid (a line is not transformable in v1).
|
||||
// A transformable solid, plus a stale id (always non-transformable -> UnknownId).
|
||||
let mk_solid = Operation::CreateSolid(SolidPrimitive::Cuboid { origin: [0.0; 3], size: [10.0; 3] });
|
||||
let mk_line = Operation::CreateCurve(Curve2Spec::Line { start: [0.0; 3], end: [1.0; 3] });
|
||||
let solid = new_id(&dispatch(&mut host, DocApiEnvelope::op(mk_solid)).unwrap());
|
||||
let line = new_id(&dispatch(&mut host, DocApiEnvelope::op(mk_line)).unwrap());
|
||||
let stale = ObjectId::from_u64(0xFFFF_FFFF);
|
||||
let rev_before = host.scene().geometry_epoch;
|
||||
|
||||
// TransformMany over [solid, line]: the non-solid makes it fail BEFORE any
|
||||
// TransformMany over [solid, stale]: the stale id makes it fail BEFORE any
|
||||
// mutation (all-or-nothing), so the epoch must not move and no undo is recorded.
|
||||
let op = Operation::TransformMany {
|
||||
ids: vec![solid, line],
|
||||
ids: vec![solid, stale],
|
||||
placement: ocs_doc_api::PlacementSpec::at([5.0, 0.0, 0.0]),
|
||||
};
|
||||
let err = dispatch(&mut host, DocApiEnvelope::op(op)).unwrap_err();
|
||||
assert!(matches!(err, ApiError::Validation { .. } | ApiError::Unsupported(_)), "{err:?}");
|
||||
assert!(matches!(err, ApiError::Validation { .. } | ApiError::UnknownId(_)), "{err:?}");
|
||||
assert_eq!(host.scene().geometry_epoch, rev_before, "no mutation on rejected TransformMany");
|
||||
}
|
||||
|
||||
@@ -377,6 +420,191 @@ mod tests {
|
||||
assert!(matches!(err, ApiError::Validation { .. }), "{err:?}");
|
||||
}
|
||||
|
||||
// ── Phase 0: outstanding supported-family methods ──────────────────────
|
||||
|
||||
#[test]
|
||||
fn phase0_add_vertex_to_polyline() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let mk_poly = Operation::CreateCurve(Curve2Spec::Polyline {
|
||||
points: vec![[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 10.0, 0.0]],
|
||||
closed: false,
|
||||
});
|
||||
let poly = new_id(&dispatch(&mut host, DocApiEnvelope::op(mk_poly)).unwrap());
|
||||
// Insert a vertex at index 1.
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::AddVertex {
|
||||
id: poly, at: 1, point: [5.0, 0.0, 0.0] })).unwrap();
|
||||
// The polyline now has 4 vertices with the inserted one at index 1.
|
||||
let handle = obj_to_handle(poly);
|
||||
let Some(EntityType::LwPolyline(pl)) = host.document().get_entity(handle) else {
|
||||
panic!("polyline not found");
|
||||
};
|
||||
assert_eq!(pl.vertices.len(), 4);
|
||||
assert!((pl.vertices[1].location.x - 5.0).abs() < 1e-9);
|
||||
// Out-of-range index is a validation error, not a panic.
|
||||
let err = dispatch(&mut host, DocApiEnvelope::op(Operation::AddVertex {
|
||||
id: poly, at: 99, point: [0.0, 0.0, 0.0] })).unwrap_err();
|
||||
assert!(matches!(err, ApiError::Validation { .. }), "{err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase0_extrude_rectangular_profile_to_solid() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
// A closed rectangular 2x3 profile in XY.
|
||||
let mk_profile = Operation::CreateCurve(Curve2Spec::Polyline {
|
||||
points: vec![[0.0,0.0,0.0],[2.0,0.0,0.0],[2.0,3.0,0.0],[0.0,3.0,0.0]],
|
||||
closed: true,
|
||||
});
|
||||
let profile = new_id(&dispatch(&mut host, DocApiEnvelope::op(mk_profile)).unwrap());
|
||||
// Extrude +Z by 5 -> a 2x3x5 box = volume 30.
|
||||
let solid = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::Extrude {
|
||||
profile, direction: [0.0, 0.0, 5.0] })).unwrap());
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetVolume { id: solid }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Volume(v) => assert!((*v - 30.0).abs() < 1.0, "extrude volume {v}"),
|
||||
other => panic!("expected volume, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase0_revolve_profile_to_solid() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
// A 1-wide, 2-tall rectangle offset 1 from the Y axis; revolve about the Y
|
||||
// axis by 2*pi -> a cylinder-ish annulus (outer r=2, inner r=1, h=2): pi*(4-1)*2 = 6pi ≈ 18.85.
|
||||
let mk_profile = Operation::CreateCurve(Curve2Spec::Polyline {
|
||||
points: vec![[1.0,0.0,0.0],[2.0,0.0,0.0],[2.0,2.0,0.0],[1.0,2.0,0.0]],
|
||||
closed: true,
|
||||
});
|
||||
let profile = new_id(&dispatch(&mut host, DocApiEnvelope::op(mk_profile)).unwrap());
|
||||
let result = dispatch(&mut host, DocApiEnvelope::op(Operation::Revolve {
|
||||
profile,
|
||||
axis: ([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
|
||||
angle: std::f64::consts::TAU,
|
||||
}));
|
||||
// Revolve may be geometry-sensitive; assert it either produces a positive
|
||||
// volume or surfaces a structured geometry error (no panic).
|
||||
match result {
|
||||
Ok(receipt) => {
|
||||
let id = receipt.outcome.and_then(|o| o.new_id()).unwrap();
|
||||
let v = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetVolume { id }])).unwrap();
|
||||
match &v.query_results[0] {
|
||||
QueryResult::Volume(vol) => assert!(*vol > 0.0, "revolve volume {vol}"),
|
||||
other => panic!("expected volume, got {other:?}"),
|
||||
}
|
||||
}
|
||||
Err(ApiError::Geometry { .. } | ApiError::Validation { .. }) => {}
|
||||
Err(e) => panic!("unexpected error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase0_non_solid_transform_line_and_circle() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let mk_line = Operation::CreateCurve(Curve2Spec::Line { start: [0.0; 3], end: [10.0; 3] });
|
||||
let line = new_id(&dispatch(&mut host, DocApiEnvelope::op(mk_line)).unwrap());
|
||||
// Translate the line by +5 in X: bounds shift by +5.
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::Transform {
|
||||
id: line, placement: ocs_doc_api::PlacementSpec::at([5.0, 0.0, 0.0]) })).unwrap();
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetBounds { id: line }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Bounds(b) => {
|
||||
assert!((b.min[0] - 5.0).abs() < 1e-6 && (b.max[0] - 15.0).abs() < 1e-6, "{b:?}");
|
||||
}
|
||||
other => panic!("expected bounds, got {other:?}"),
|
||||
}
|
||||
// TransformMany over a mix of line + circle is now all-or-nothing (both transformable).
|
||||
let circle = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::Circle { centre: [0.0; 3], radius: 2.0 }))).unwrap());
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::TransformMany {
|
||||
ids: vec![line, circle],
|
||||
placement: ocs_doc_api::PlacementSpec::at([0.0, 1.0, 0.0]),
|
||||
})).unwrap();
|
||||
}
|
||||
|
||||
// ── Phase 2: full 2D curve families ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn phase2_arc_ellipse_spline_create_bounds_transform() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
|
||||
// Arc: create -> kind Arc; bounds are the coarse full-circle bounds.
|
||||
let arc = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::Arc { centre: [5.0, 5.0, 0.0], radius: 4.0, start_angle: 0.0, end_angle: std::f64::consts::PI }))).unwrap());
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![
|
||||
Query::GetEntity { id: arc }, Query::GetBounds { id: arc }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Entity(v) => assert_eq!(v.kind, "Arc"),
|
||||
other => panic!("expected entity, got {other:?}"),
|
||||
}
|
||||
match &receipt.query_results[1] {
|
||||
QueryResult::Bounds(b) => assert!((b.min[0] - 1.0).abs() < 1e-6 && (b.max[0] - 9.0).abs() < 1e-6, "{b:?}"),
|
||||
other => panic!("expected bounds, got {other:?}"),
|
||||
}
|
||||
|
||||
// Ellipse: create -> kind Ellipse; bounds = centre ± major-axis length.
|
||||
let ell = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::Ellipse { centre: [0.0, 0.0, 0.0], major_axis: [6.0, 0.0, 0.0], ratio: 0.5, start: 0.0, end: std::f64::consts::TAU }))).unwrap());
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetEntity { id: ell }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Entity(v) => assert_eq!(v.kind, "Ellipse"),
|
||||
other => panic!("expected entity, got {other:?}"),
|
||||
}
|
||||
|
||||
// Spline (degree-3 cubic through 4 control points): create -> kind Spline;
|
||||
// bounds = control-point bounds. Transform by +10 X shifts bounds.
|
||||
let spline = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::Spline {
|
||||
degree: 3,
|
||||
control_points: vec![[0.0,0.0,0.0],[1.0,2.0,0.0],[2.0,-2.0,0.0],[3.0,0.0,0.0]],
|
||||
knots: vec![0.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0],
|
||||
weights: vec![1.0; 4],
|
||||
}))).unwrap());
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetBounds { id: spline }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Bounds(b) => assert!((b.min[0] - 0.0).abs() < 1e-6 && (b.max[0] - 3.0).abs() < 1e-6, "{b:?}"),
|
||||
other => panic!("expected bounds, got {other:?}"),
|
||||
}
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::Transform {
|
||||
id: spline, placement: ocs_doc_api::PlacementSpec::at([10.0, 0.0, 0.0]) })).unwrap();
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetBounds { id: spline }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Bounds(b) => assert!((b.min[0] - 10.0).abs() < 1e-6, "{b:?}"),
|
||||
other => panic!("expected bounds, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn phase2_ray_xline_create_and_transform() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let mut host = HostSession::new(&mut app, 0);
|
||||
let ray = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::Ray { origin: [1.0, 2.0, 0.0], direction: [1.0, 0.0, 0.0] }))).unwrap());
|
||||
let xline = new_id(&dispatch(&mut host, DocApiEnvelope::op(Operation::CreateCurve(
|
||||
Curve2Spec::XLine { origin: [0.0, 0.0, 0.0], direction: [0.0, 1.0, 0.0] }))).unwrap());
|
||||
let receipt = dispatch(&mut host, DocApiEnvelope::queries(vec![
|
||||
Query::GetEntity { id: ray }, Query::GetEntity { id: xline }])).unwrap();
|
||||
match &receipt.query_results[0] {
|
||||
QueryResult::Entity(v) => assert_eq!(v.kind, "Ray"),
|
||||
other => panic!("expected entity, got {other:?}"),
|
||||
}
|
||||
match &receipt.query_results[1] {
|
||||
QueryResult::Entity(v) => assert_eq!(v.kind, "XLine"),
|
||||
other => panic!("expected entity, got {other:?}"),
|
||||
}
|
||||
// Ray/XLine are unbounded -> GetBounds is Unsupported (not a panic).
|
||||
assert!(dispatch(&mut host, DocApiEnvelope::queries(vec![Query::GetBounds { id: ray }])).is_err());
|
||||
// Transform moves the ray's base point (no error).
|
||||
dispatch(&mut host, DocApiEnvelope::op(Operation::Transform {
|
||||
id: ray, placement: ocs_doc_api::PlacementSpec::at([0.0, 0.0, 5.0]) })).unwrap();
|
||||
let handle = obj_to_handle(ray);
|
||||
let Some(EntityType::Ray(r)) = host.document().get_entity(handle) else { panic!("ray not found") };
|
||||
assert!((r.base_point.z - 5.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doc_api_unknown_id_surfaces_structured_error() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
|
||||
+203
-1
@@ -2,11 +2,152 @@
|
||||
// acadrust `EntityType` and back. Lives in the host (uses the real acadrust
|
||||
// types), keeping the crate's DTOs dependency-light.
|
||||
|
||||
use acadrust::entities::{Circle, Line, LwPolyline, LwVertex, Point};
|
||||
use acadrust::entities::{Arc, Circle, Ellipse, Line, LwPolyline, LwVertex, Point, Ray, Spline, XLine};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
use acadrust::EntityType;
|
||||
use ocs_doc_api::{Aabb, ApiError, ApiResult, Curve2Spec, ObjectId};
|
||||
|
||||
/// Convert a profile entity (Line/Circle/Arc/LwPolyline) to `geom2d::Curve`s for
|
||||
/// sweep ops (`Extrude`/`Revolve`). Z is dropped (profiles lie on the XY plane).
|
||||
pub fn entity_to_profile_curves(entity: &EntityType) -> ApiResult<Vec<cadkernel::geom2d::Curve>> {
|
||||
use cadkernel::geom2d::{Arc as KArc, Circle as KCircle, Curve as KCurve, Line as KLine};
|
||||
let curves = match entity {
|
||||
EntityType::Line(l) => vec![KCurve::Line(KLine {
|
||||
start: [l.start.x, l.start.y],
|
||||
end: [l.end.x, l.end.y],
|
||||
})],
|
||||
EntityType::Circle(c) => vec![KCurve::Circle(KCircle {
|
||||
centre: [c.center.x, c.center.y],
|
||||
radius: c.radius,
|
||||
})],
|
||||
EntityType::Arc(a) => vec![KCurve::Arc(KArc {
|
||||
centre: [a.center.x, a.center.y],
|
||||
radius: a.radius,
|
||||
start_angle: a.start_angle,
|
||||
end_angle: a.end_angle,
|
||||
})],
|
||||
EntityType::LwPolyline(pl) => {
|
||||
if pl.vertices.len() < 2 {
|
||||
return Err(ApiError::validation("profile", "polyline profile needs >= 2 vertices"));
|
||||
}
|
||||
// Build a straight-segment polyline chain (bulge arcs land in a later
|
||||
// phase; straight segments cover the common rectangular profile case).
|
||||
let pts: Vec<[f64; 2]> = pl.vertices.iter().map(|v| [v.location.x, v.location.y]).collect();
|
||||
let mut segs = Vec::with_capacity(pts.len());
|
||||
for i in 0..pts.len() {
|
||||
let start = pts[i];
|
||||
let end = if i + 1 < pts.len() {
|
||||
pts[i + 1]
|
||||
} else if pl.is_closed {
|
||||
pts[0]
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
segs.push(KCurve::Line(KLine { start, end }));
|
||||
}
|
||||
segs
|
||||
}
|
||||
_ => {
|
||||
return Err(ApiError::Unsupported(
|
||||
"profile conversion is supported for Line/Circle/Arc/LwPolyline only".into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(curves)
|
||||
}
|
||||
|
||||
/// Apply a rigid similarity (rotation+translation+uniform scale) to a non-solid
|
||||
/// entity in place. Supports the common 2D families; `Solid3D` goes through the
|
||||
/// kernel path instead. Returns the transformed entity (same handle) for the
|
||||
/// caller to `update_entity`.
|
||||
pub fn transform_entity_geometry(
|
||||
entity: &EntityType,
|
||||
p: &ocs_doc_api::PlacementSpec,
|
||||
) -> ApiResult<EntityType> {
|
||||
// Affine point transform: out = R * (s * v) + t. Uniform scale s = |x_axis|.
|
||||
let s = (p.x_axis[0] * p.x_axis[0] + p.x_axis[1] * p.x_axis[1] + p.x_axis[2] * p.x_axis[2]).sqrt();
|
||||
let apply = |v: Vector3| -> Vector3 {
|
||||
let x = v.x * s;
|
||||
let y = v.y * s;
|
||||
let z = v.z * s;
|
||||
Vector3::new(
|
||||
p.x_axis[0] * x + p.y_axis[0] * y + p.z_axis[0] * z + p.origin[0],
|
||||
p.x_axis[1] * x + p.y_axis[1] * y + p.z_axis[1] * z + p.origin[1],
|
||||
p.x_axis[2] * x + p.y_axis[2] * y + p.z_axis[2] * z + p.origin[2],
|
||||
)
|
||||
};
|
||||
let entity = match entity {
|
||||
EntityType::Line(l) => {
|
||||
let mut l = l.clone();
|
||||
l.start = apply(l.start);
|
||||
l.end = apply(l.end);
|
||||
EntityType::Line(l)
|
||||
}
|
||||
EntityType::Circle(c) => {
|
||||
let mut c = c.clone();
|
||||
c.center = apply(c.center);
|
||||
c.radius *= s;
|
||||
EntityType::Circle(c)
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let mut a = a.clone();
|
||||
a.center = apply(a.center);
|
||||
a.radius *= s;
|
||||
EntityType::Arc(a)
|
||||
}
|
||||
EntityType::Ellipse(e) => {
|
||||
let mut e = e.clone();
|
||||
e.center = apply(e.center);
|
||||
// Scale the major-axis vector by the uniform scale (rigid similarity).
|
||||
e.major_axis = Vector3::new(
|
||||
e.major_axis.x * s, e.major_axis.y * s, e.major_axis.z * s,
|
||||
);
|
||||
EntityType::Ellipse(e)
|
||||
}
|
||||
EntityType::Spline(sp) => {
|
||||
let mut sp = sp.clone();
|
||||
for cp in &mut sp.control_points {
|
||||
*cp = apply(*cp);
|
||||
}
|
||||
for fp in &mut sp.fit_points {
|
||||
*fp = apply(*fp);
|
||||
}
|
||||
EntityType::Spline(sp)
|
||||
}
|
||||
EntityType::Ray(r) => {
|
||||
let mut r = r.clone();
|
||||
r.base_point = apply(r.base_point);
|
||||
EntityType::Ray(r)
|
||||
}
|
||||
EntityType::XLine(x) => {
|
||||
let mut x = x.clone();
|
||||
x.base_point = apply(x.base_point);
|
||||
EntityType::XLine(x)
|
||||
}
|
||||
EntityType::Point(pt) => {
|
||||
let mut pt = pt.clone();
|
||||
pt.location = apply(pt.location);
|
||||
EntityType::Point(pt)
|
||||
}
|
||||
EntityType::LwPolyline(pl) => {
|
||||
let mut pl = pl.clone();
|
||||
let z0 = pl.elevation;
|
||||
for v in &mut pl.vertices {
|
||||
let t = apply(Vector3::new(v.location.x, v.location.y, z0));
|
||||
v.location = Vector2::new(t.x, t.y);
|
||||
pl.elevation = t.z;
|
||||
}
|
||||
EntityType::LwPolyline(pl)
|
||||
}
|
||||
_ => {
|
||||
return Err(ApiError::Unsupported(
|
||||
"transform is supported for Line/Circle/Arc/Point/LwPolyline (and solids) in v1".into(),
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(entity)
|
||||
}
|
||||
|
||||
fn v3(p: [f64; 3]) -> Vector3 {
|
||||
Vector3::new(p[0], p[1], p[2])
|
||||
}
|
||||
@@ -46,6 +187,37 @@ pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
||||
e.location = v3(*position);
|
||||
EntityType::Point(e)
|
||||
}
|
||||
Curve2Spec::Arc { centre, radius, start_angle, end_angle } => {
|
||||
let mut e = Arc::new();
|
||||
e.center = v3(*centre);
|
||||
e.radius = *radius;
|
||||
e.start_angle = *start_angle;
|
||||
e.end_angle = *end_angle;
|
||||
EntityType::Arc(e)
|
||||
}
|
||||
Curve2Spec::Ellipse { centre, major_axis, ratio, start, end } => {
|
||||
let mut e = Ellipse::new();
|
||||
e.center = v3(*centre);
|
||||
e.major_axis = v3(*major_axis);
|
||||
e.minor_axis_ratio = *ratio;
|
||||
e.start_parameter = *start;
|
||||
e.end_parameter = *end;
|
||||
EntityType::Ellipse(e)
|
||||
}
|
||||
Curve2Spec::Spline { degree, control_points, knots, weights } => {
|
||||
let mut e = Spline::new();
|
||||
e.degree = *degree;
|
||||
e.control_points = control_points.iter().map(|p| v3(*p)).collect();
|
||||
e.knots = knots.clone();
|
||||
e.weights = weights.clone();
|
||||
EntityType::Spline(e)
|
||||
}
|
||||
Curve2Spec::Ray { origin, direction } => {
|
||||
EntityType::Ray(Ray::new(v3(*origin), v3(*direction)))
|
||||
}
|
||||
Curve2Spec::XLine { origin, direction } => {
|
||||
EntityType::XLine(XLine::new(v3(*origin), v3(*direction)))
|
||||
}
|
||||
};
|
||||
Ok(entity)
|
||||
}
|
||||
@@ -62,6 +234,8 @@ pub fn entity_kind_name(entity: &EntityType) -> &'static str {
|
||||
EntityType::Arc(_) => "Arc",
|
||||
EntityType::Ellipse(_) => "Ellipse",
|
||||
EntityType::Spline(_) => "Spline",
|
||||
EntityType::Ray(_) => "Ray",
|
||||
EntityType::XLine(_) => "XLine",
|
||||
EntityType::Text(_) => "Text",
|
||||
EntityType::MText(_) => "MText",
|
||||
EntityType::Insert(_) => "Insert",
|
||||
@@ -105,6 +279,34 @@ pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult<Aab
|
||||
}
|
||||
Aabb { min, max }
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
// Coarse: full-circle bounds (exact arc bounds is a later refinement).
|
||||
Aabb {
|
||||
min: [a.center.x - a.radius, a.center.y - a.radius, a.center.z],
|
||||
max: [a.center.x + a.radius, a.center.y + a.radius, a.center.z],
|
||||
}
|
||||
}
|
||||
EntityType::Ellipse(e) => {
|
||||
// Coarse: use the major-axis length as the bounding radius.
|
||||
let r = (e.major_axis.x * e.major_axis.x
|
||||
+ e.major_axis.y * e.major_axis.y
|
||||
+ e.major_axis.z * e.major_axis.z)
|
||||
.sqrt();
|
||||
Aabb {
|
||||
min: [e.center.x - r, e.center.y - r, e.center.z - r],
|
||||
max: [e.center.x + r, e.center.y + r, e.center.z + r],
|
||||
}
|
||||
}
|
||||
EntityType::Spline(s) if !s.control_points.is_empty() => {
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for p in &s.control_points {
|
||||
min = [min[0].min(p.x), min[1].min(p.y), min[2].min(p.z)];
|
||||
max = [max[0].max(p.x), max[1].max(p.y), max[2].max(p.z)];
|
||||
}
|
||||
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())),
|
||||
};
|
||||
Ok(bb)
|
||||
|
||||
Reference in new issue
Block a user