//! 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> { 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 { 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 { 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 { 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 { 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 }) }