forked from 1b-agent/opencadstudio-upstream
Add the typed document facade and plugin IPC adapter. Prepare bulk edits before mutation, preserve undo and dirty state, keep profile elevations, validate numeric inputs and session bindings, and generate Python constructors with typed results. Reuse existing entity transforms and profile conversion. Compute mass properties and closed-surface bounds in cadkernel; pin its main revision 6a5e6fc2822f6af6b2611cd605b44437b35158f0. Keep plugin API metadata at version 6. Validation: 646 native application tests, 597 kernel tests, 12 document API tests, 91 plugin API tests, 3 Python tests, default/IPC feature builds and wasm32 check passed.
269 lines
9.8 KiB
Rust
269 lines
9.8 KiB
Rust
//! Entity conversions: map the crate's plain-data DTOs to acadrust
|
|
//! `EntityType` and back. Host feature only (uses the real acadrust types). This
|
|
//! module is host-side-capable but host-independent — it operates purely on
|
|
//! acadrust entities and crate DTOs, so any host can reuse it.
|
|
|
|
use acadrust::entities::{
|
|
Arc, Circle, Ellipse, Line, LwPolyline, LwVertex, Point, Ray, Spline, XLine,
|
|
};
|
|
use acadrust::types::{Vector2, Vector3};
|
|
use acadrust::EntityType;
|
|
|
|
use crate::{Aabb, ApiError, ApiResult, Curve2Spec, GeometryErrorKind, 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 at least two vertices",
|
|
));
|
|
}
|
|
KCurve::Polyline(cadkernel::geom2d::Polyline {
|
|
vertices: pl
|
|
.vertices
|
|
.iter()
|
|
.map(|v| cadkernel::geom2d::PolylineVertex {
|
|
position: [v.location.x, v.location.y],
|
|
bulge: v.bulge,
|
|
})
|
|
.collect(),
|
|
closed: pl.is_closed,
|
|
})
|
|
.segments()
|
|
}
|
|
_ => {
|
|
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: &crate::PlacementSpec,
|
|
) -> ApiResult<EntityType> {
|
|
crate::geom::validate_placement(p)?;
|
|
let matrix = acadrust::types::Matrix4 {
|
|
m: [
|
|
[p.x_axis[0], p.y_axis[0], p.z_axis[0], p.origin[0]],
|
|
[p.x_axis[1], p.y_axis[1], p.z_axis[1], p.origin[1]],
|
|
[p.x_axis[2], p.y_axis[2], p.z_axis[2], p.origin[2]],
|
|
[0.0, 0.0, 0.0, 1.0],
|
|
],
|
|
};
|
|
let mut moved = entity.clone();
|
|
moved.apply_transform(&acadrust::types::Transform::from_matrix(matrix));
|
|
Ok(moved)
|
|
}
|
|
|
|
fn v3(p: [f64; 3]) -> Vector3 {
|
|
Vector3::new(p[0], p[1], p[2])
|
|
}
|
|
|
|
/// Convert a polyline bulge segment (start → end, bulge = tan(θ/4)) to a
|
|
/// `geom2d::Curve::Arc`. Positive bulge = counter-clockwise arc; negative = clockwise.
|
|
pub fn bulge_arc_segment(
|
|
start: [f64; 2],
|
|
end: [f64; 2],
|
|
bulge: f64,
|
|
) -> ApiResult<cadkernel::geom2d::Curve> {
|
|
use cadkernel::geom2d::{Arc as KArc, Curve as KCurve};
|
|
let arc = cadkernel::geom2d::BulgeArc::from_bulge(start, end, bulge)
|
|
.ok_or_else(|| ApiError::validation("profile", "invalid bulge segment"))?;
|
|
// The kernel's Arc is counter-clockwise; reverse clockwise endpoints.
|
|
let (start_angle, end_angle) = if arc.sweep < 0.0 {
|
|
(arc.end_angle, arc.start_angle)
|
|
} else {
|
|
(arc.start_angle, arc.end_angle)
|
|
};
|
|
Ok(KCurve::Arc(KArc {
|
|
centre: arc.center,
|
|
radius: arc.radius,
|
|
start_angle,
|
|
end_angle,
|
|
}))
|
|
}
|
|
|
|
/// Build an acadrust entity for a 2D curve construction spec.
|
|
pub fn curve_spec_to_entity(spec: &Curve2Spec) -> ApiResult<EntityType> {
|
|
crate::validation::curve(spec)?;
|
|
let entity = match spec {
|
|
Curve2Spec::Line { start, end } => {
|
|
let mut e = Line::new();
|
|
e.start = v3(*start);
|
|
e.end = v3(*end);
|
|
EntityType::Line(e)
|
|
}
|
|
Curve2Spec::Circle { centre, radius } => {
|
|
let mut e = Circle::new();
|
|
e.center = v3(*centre);
|
|
e.radius = *radius;
|
|
EntityType::Circle(e)
|
|
}
|
|
Curve2Spec::Polyline { points, closed } => {
|
|
let mut e = LwPolyline::new();
|
|
e.vertices = points
|
|
.iter()
|
|
.map(|p| LwVertex {
|
|
location: Vector2::new(p[0], p[1]),
|
|
bulge: 0.0,
|
|
start_width: 0.0,
|
|
end_width: 0.0,
|
|
vertex_id: 0,
|
|
})
|
|
.collect();
|
|
e.elevation = points.first().map_or(0.0, |p| p[2]);
|
|
if points.iter().any(|p| p[2] != e.elevation) {
|
|
return Err(ApiError::validation(
|
|
"CreateCurve",
|
|
"polyline points must share an elevation",
|
|
));
|
|
}
|
|
e.is_closed = *closed;
|
|
EntityType::LwPolyline(e)
|
|
}
|
|
Curve2Spec::Point { position } => {
|
|
let mut e = Point::new();
|
|
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,
|
|
} => {
|
|
// Validate the minor/major ratio so the (major-axis) bounds never
|
|
// under-bound: ratio must be in (0, 1].
|
|
if !(*ratio > 0.0 && *ratio <= 1.0) {
|
|
return Err(ApiError::validation(
|
|
"CreateCurve",
|
|
format!("ellipse minor_axis_ratio must be in (0, 1], got {ratio}"),
|
|
));
|
|
}
|
|
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)
|
|
}
|
|
|
|
/// The acadrust `EntityType` variant name (for `EntityView::kind` / downcasts).
|
|
pub fn entity_kind_name(entity: &EntityType) -> &'static str {
|
|
match entity {
|
|
EntityType::Solid3D(_) => "Solid3D",
|
|
EntityType::Line(_) => "Line",
|
|
EntityType::Circle(_) => "Circle",
|
|
EntityType::LwPolyline(_) => "LwPolyline",
|
|
EntityType::Polyline(_) => "Polyline",
|
|
EntityType::Point(_) => "Point",
|
|
EntityType::Arc(_) => "Arc",
|
|
EntityType::Ellipse(_) => "Ellipse",
|
|
EntityType::Spline(_) => "Spline",
|
|
EntityType::Ray(_) => "Ray",
|
|
EntityType::XLine(_) => "XLine",
|
|
EntityType::Insert(_) => "Insert",
|
|
EntityType::Viewport(_) => "Viewport",
|
|
EntityType::Text(_) => "Text",
|
|
EntityType::MText(_) => "MText",
|
|
EntityType::AttributeDefinition(_) => "AttributeDefinition",
|
|
EntityType::AttributeEntity(_) => "AttributeEntity",
|
|
// Media & misc (phase 5, read-mostly): named so GetEntity reports a real kind.
|
|
EntityType::RasterImage(_) => "RasterImage",
|
|
EntityType::Table(_) => "Table",
|
|
EntityType::Leader(_) => "Leader",
|
|
EntityType::MultiLeader(_) => "MultiLeader",
|
|
EntityType::MLine(_) => "MLine",
|
|
EntityType::Mesh(_) => "Mesh",
|
|
EntityType::Helix(_) => "Helix",
|
|
EntityType::Region(_) => "Region",
|
|
EntityType::Body(_) => "Body",
|
|
EntityType::Surface(_) => "Surface",
|
|
EntityType::Face3D(_) => "Face3D",
|
|
EntityType::Dimension(_) => "Dimension",
|
|
EntityType::Hatch(_) => "Hatch",
|
|
_ => "Other",
|
|
}
|
|
}
|
|
|
|
/// Existing entity bounds; unbounded or empty geometry returns an error.
|
|
pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult<Aabb> {
|
|
let entity = entity.ok_or(ApiError::UnknownId(id))?;
|
|
if matches!(entity, EntityType::Ray(_) | EntityType::XLine(_)) {
|
|
return Err(ApiError::Unsupported("unbounded entity".into()));
|
|
}
|
|
let bounds = entity.as_entity().bounding_box();
|
|
let min = [bounds.min.x, bounds.min.y, bounds.min.z];
|
|
let max = [bounds.max.x, bounds.max.y, bounds.max.z];
|
|
if min.iter().chain(&max).any(|v| !v.is_finite()) {
|
|
return Err(ApiError::geometry(
|
|
GeometryErrorKind::Empty,
|
|
"entity has no finite bounds",
|
|
));
|
|
}
|
|
Ok(Aabb { min, max })
|
|
}
|