forked from 1b-agent/opencadstudio-upstream
ocs_doc_api: move doc_api_convert.rs into the crate (src/convert.rs)
Move the 503-line host-independent entity-conversion module (acadrust EntityType <-> DTO conversions, transform_entity_geometry, entity_to_profile_curves, bulge_arc_segment, entity_bounds, entity_kind_name, points_aabb) from src/app/doc_api_convert.rs into crates/ocs_doc_api/src/convert.rs (host feature). The host file is now a thin re-export (pub use ocs_doc_api::convert::*) so all existing crate::app::doc_api_convert::* paths keep working unchanged. Zero coupling risk (the module only touches acadrust + crate DTOs). 623 host lib + 8 crate + 3 consistency green.
This commit is contained in:
1 parent
d1b8fc93f5
commit
d746d635fb
3 files changed
+522
-503
No files matched your search
@@ -0,0 +1,505 @@
|
||||
//! Entity conversions (plan §5.1): 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 >= 2 vertices"));
|
||||
}
|
||||
// Convert each segment: straight for bulge==0, a circular arc for
|
||||
// bulge!=0 (bulge = tan(included_angle/4)). This supports bulged
|
||||
// polyline profiles for Extrude/Revolve instead of rejecting them.
|
||||
let n = pl.vertices.len();
|
||||
let count = if pl.is_closed { n } else { n - 1 };
|
||||
let mut segs = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let a = pl.vertices[i];
|
||||
let b = pl.vertices[(i + 1) % n];
|
||||
let start = [a.location.x, a.location.y];
|
||||
let end = [b.location.x, b.location.y];
|
||||
if a.bulge.abs() < 1e-12 {
|
||||
segs.push(KCurve::Line(KLine { start, end }));
|
||||
} else {
|
||||
segs.push(bulge_arc_segment(start, end, a.bulge)?);
|
||||
}
|
||||
}
|
||||
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: &crate::PlacementSpec,
|
||||
) -> ApiResult<EntityType> {
|
||||
// Placement = R·S·v + t where the axis COLUMNS carry rotation AND uniform
|
||||
// scale (|x_axis| = s). A point maps v -> axes·v + origin (axes already embed
|
||||
// s — do NOT pre-multiply by s or scale is applied twice). A direction maps
|
||||
// v -> axes·v (no origin). Scalar fields (radius) scale by 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();
|
||||
// Z-rotation of the placement in the XY plane (for arc angles / insert rotation).
|
||||
let rot_z = p.x_axis[1].atan2(p.x_axis[0]);
|
||||
let apply = |v: Vector3| -> Vector3 {
|
||||
Vector3::new(
|
||||
p.x_axis[0] * v.x + p.y_axis[0] * v.y + p.z_axis[0] * v.z + p.origin[0],
|
||||
p.x_axis[1] * v.x + p.y_axis[1] * v.y + p.z_axis[1] * v.z + p.origin[1],
|
||||
p.x_axis[2] * v.x + p.y_axis[2] * v.y + p.z_axis[2] * v.z + p.origin[2],
|
||||
)
|
||||
};
|
||||
// Direction/vector transform: rotation+scale only, NO translation.
|
||||
let apply_dir = |v: Vector3| -> Vector3 {
|
||||
Vector3::new(
|
||||
p.x_axis[0] * v.x + p.y_axis[0] * v.y + p.z_axis[0] * v.z,
|
||||
p.x_axis[1] * v.x + p.y_axis[1] * v.y + p.z_axis[1] * v.z,
|
||||
p.x_axis[2] * v.x + p.y_axis[2] * v.y + p.z_axis[2] * v.z,
|
||||
)
|
||||
};
|
||||
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;
|
||||
// A Z-rotation shifts both angles by the placement's XY rotation.
|
||||
a.start_angle += rot_z;
|
||||
a.end_angle += rot_z;
|
||||
EntityType::Arc(a)
|
||||
}
|
||||
EntityType::Ellipse(e) => {
|
||||
let mut e = e.clone();
|
||||
e.center = apply(e.center);
|
||||
// The major-axis is a direction+length vector: rotate+scale it (no origin).
|
||||
e.major_axis = apply_dir(e.major_axis);
|
||||
// A Z-rotation also shifts the partial-arc sweep (eccentric-anomaly params).
|
||||
e.start_parameter += rot_z;
|
||||
e.end_parameter += rot_z;
|
||||
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);
|
||||
r.direction = apply_dir(r.direction);
|
||||
EntityType::Ray(r)
|
||||
}
|
||||
EntityType::XLine(x) => {
|
||||
let mut x = x.clone();
|
||||
x.base_point = apply(x.base_point);
|
||||
x.direction = apply_dir(x.direction);
|
||||
EntityType::XLine(x)
|
||||
}
|
||||
EntityType::Insert(ins) => {
|
||||
let mut ins = ins.clone();
|
||||
ins.insert_point = apply(ins.insert_point);
|
||||
// Compose the block rotation with the placement's Z-rotation.
|
||||
ins.rotation += rot_z;
|
||||
EntityType::Insert(ins)
|
||||
}
|
||||
EntityType::Viewport(vp) => {
|
||||
let mut vp = vp.clone();
|
||||
vp.center = apply(vp.center);
|
||||
EntityType::Viewport(vp)
|
||||
}
|
||||
EntityType::Text(t) => {
|
||||
let mut t = t.clone();
|
||||
t.insertion_point = apply(t.insertion_point);
|
||||
EntityType::Text(t)
|
||||
}
|
||||
EntityType::MText(t) => {
|
||||
let mut t = t.clone();
|
||||
t.insertion_point = apply(t.insertion_point);
|
||||
EntityType::MText(t)
|
||||
}
|
||||
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])
|
||||
}
|
||||
|
||||
/// 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 chord_x = end[0] - start[0];
|
||||
let chord_y = end[1] - start[1];
|
||||
let chord_len = (chord_x * chord_x + chord_y * chord_y).sqrt();
|
||||
if chord_len < 1e-12 {
|
||||
return Err(ApiError::validation("profile", "bulge segment with coincident endpoints"));
|
||||
}
|
||||
// Included angle θ = 4·atan(bulge); radius = (chord/2) / sin(θ/2).
|
||||
let theta = 4.0 * bulge.atan();
|
||||
let half = theta / 2.0;
|
||||
let sin_half = half.sin();
|
||||
if sin_half.abs() < 1e-9 {
|
||||
return Err(ApiError::validation("profile", "bulge segment with degenerate angle"));
|
||||
}
|
||||
let radius = (chord_len / 2.0) / sin_half.abs();
|
||||
// Center: chord midpoint + perpendicular offset. The apothem MAGNITUDE is
|
||||
// (chord/2)/|tan(θ/2)| (always positive); the SIGN of the bulge alone picks the
|
||||
// side (CCW=+90° perp, CW=-90° perp). Using signed tan(half) AND the sign flip
|
||||
// would double-count and mirror CW arcs to the wrong side.
|
||||
let mid_x = (start[0] + end[0]) / 2.0;
|
||||
let mid_y = (start[1] + end[1]) / 2.0;
|
||||
let apothem = (chord_len / 2.0) / half.tan().abs();
|
||||
let sign = if bulge >= 0.0 { 1.0 } else { -1.0 };
|
||||
let perp_x = -chord_y / chord_len * sign;
|
||||
let perp_y = chord_x / chord_len * sign;
|
||||
let centre = [mid_x + perp_x * apothem, mid_y + perp_y * apothem];
|
||||
// Start/end angles about the centre.
|
||||
let start_angle = (start[1] - centre[1]).atan2(start[0] - centre[0]);
|
||||
let end_angle = (end[1] - centre[1]).atan2(end[0] - centre[0]);
|
||||
Ok(KCurve::Arc(KArc { centre, 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> {
|
||||
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.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",
|
||||
}
|
||||
}
|
||||
|
||||
/// Coarse bounds for a non-solid entity (solids go through the kernel cache).
|
||||
/// For v1 typed curves we compute the obvious bounds; anything else errors
|
||||
/// `Unsupported` (viewport/annotation bounds land in later phases).
|
||||
pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult<Aabb> {
|
||||
let entity = entity.ok_or(ApiError::UnknownId(id))?;
|
||||
let bb = match entity {
|
||||
EntityType::Line(l) => Aabb {
|
||||
min: [
|
||||
l.start.x.min(l.end.x),
|
||||
l.start.y.min(l.end.y),
|
||||
l.start.z.min(l.end.z),
|
||||
],
|
||||
max: [
|
||||
l.start.x.max(l.end.x),
|
||||
l.start.y.max(l.end.y),
|
||||
l.start.z.max(l.end.z),
|
||||
],
|
||||
},
|
||||
EntityType::Circle(c) => Aabb {
|
||||
min: [c.center.x - c.radius, c.center.y - c.radius, c.center.z],
|
||||
max: [c.center.x + c.radius, c.center.y + c.radius, c.center.z],
|
||||
},
|
||||
EntityType::Point(p) => Aabb {
|
||||
min: [p.location.x, p.location.y, p.location.z],
|
||||
max: [p.location.x, p.location.y, p.location.z],
|
||||
},
|
||||
EntityType::LwPolyline(pl) if !pl.vertices.is_empty() => {
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for vtx in &pl.vertices {
|
||||
let (x, y, z) = (vtx.location.x, vtx.location.y, pl.elevation);
|
||||
min = [min[0].min(x), min[1].min(y), min[2].min(z)];
|
||||
max = [max[0].max(x), max[1].max(y), max[2].max(z)];
|
||||
}
|
||||
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) => points_aabb(s.control_points.iter().collect(), id)?,
|
||||
EntityType::Viewport(vp) => Aabb {
|
||||
min: [
|
||||
vp.center.x - vp.width / 2.0,
|
||||
vp.center.y - vp.height / 2.0,
|
||||
vp.center.z,
|
||||
],
|
||||
max: [
|
||||
vp.center.x + vp.width / 2.0,
|
||||
vp.center.y + vp.height / 2.0,
|
||||
vp.center.z,
|
||||
],
|
||||
},
|
||||
EntityType::RasterImage(img) => {
|
||||
// Bracket ALL FOUR image corners (origin, +u*W, +v*H, +u*W+v*H) per
|
||||
// axis so rotated/flipped images are not under-bounded.
|
||||
let (w, h) = (img.size.x, img.size.y);
|
||||
let ux = img.u_vector.x * w;
|
||||
let uy = img.u_vector.y * w;
|
||||
let vx = img.v_vector.x * h;
|
||||
let vy = img.v_vector.y * h;
|
||||
let x0 = img.insertion_point.x;
|
||||
let y0 = img.insertion_point.y;
|
||||
let xs = [x0, x0 + ux, x0 + vx, x0 + ux + vx];
|
||||
let ys = [y0, y0 + uy, y0 + vy, y0 + uy + vy];
|
||||
let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let min_y = ys.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let max_y = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
Aabb {
|
||||
min: [min_x, min_y, img.insertion_point.z],
|
||||
max: [max_x, max_y, img.insertion_point.z],
|
||||
}
|
||||
}
|
||||
EntityType::Text(t) => Aabb {
|
||||
// Coarse: a square of side height*N around the insertion point.
|
||||
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::MText(t) => Aabb {
|
||||
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::Dimension(acadrust::entities::Dimension::Linear(d)) => points_aabb(
|
||||
[&d.first_point, &d.second_point, &d.definition_point].into_iter().collect(),
|
||||
id,
|
||||
)?,
|
||||
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 }
|
||||
}
|
||||
// Read-mostly families with vertex/point data: bounds from their points.
|
||||
EntityType::Leader(l) => points_aabb(l.vertices.iter().collect(), id)?,
|
||||
EntityType::Mesh(m) => points_aabb(m.vertices.iter().collect(), id)?,
|
||||
EntityType::Face3D(f) => points_aabb(
|
||||
[&f.first_corner, &f.second_corner, &f.third_corner, &f.fourth_corner].into_iter().collect(),
|
||||
id,
|
||||
)?,
|
||||
EntityType::MLine(m) => points_aabb(m.vertices.iter().map(|v| &v.position).collect(), id)?,
|
||||
EntityType::Helix(h) => {
|
||||
// Bound the evaluated spline's control polygon (conservative); the
|
||||
// single start_point alone would be a degenerate zero-size box.
|
||||
if h.spline.control_points.is_empty() {
|
||||
points_aabb([&h.start_point].into_iter().collect(), id)?
|
||||
} else {
|
||||
points_aabb(h.spline.control_points.iter().collect(), id)?
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// AABB over a set of 3D points. A geometry-Empty error if there are no points
|
||||
/// to bound (the entity exists — do NOT misclassify as a stale/deleted id).
|
||||
fn points_aabb(pts: Vec<&Vector3>, _id: ObjectId) -> ApiResult<Aabb> {
|
||||
if pts.is_empty() {
|
||||
return Err(ApiError::geometry(GeometryErrorKind::Empty, "entity has no points to bound"));
|
||||
}
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for p in pts {
|
||||
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)];
|
||||
}
|
||||
Ok(Aabb { min, max })
|
||||
}
|
||||
@@ -21,6 +21,8 @@ pub mod backend;
|
||||
pub mod executor;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod geom;
|
||||
#[cfg(feature = "host")]
|
||||
pub mod convert;
|
||||
|
||||
pub mod envelope;
|
||||
pub mod facade;
|
||||
@@ -52,6 +54,12 @@ pub use facade::{
|
||||
};
|
||||
pub use ops::{BoolOp, Curve2Spec, EntitySpec, Operation, PlacementSpec, SolidPrimitive};
|
||||
pub use query::{Aabb, EntityView, Query, QueryResult};
|
||||
|
||||
#[cfg(feature = "host")]
|
||||
pub use convert::{
|
||||
bulge_arc_segment, curve_spec_to_entity, entity_bounds, entity_kind_name,
|
||||
entity_to_profile_curves, transform_entity_geometry,
|
||||
};
|
||||
pub use transport::Transport;
|
||||
|
||||
/// The crate's own wire-envelope protocol version (`DocApiEnvelope::version`).
|
||||
|
||||
+9
-503
@@ -1,503 +1,9 @@
|
||||
// DocApi v2 entity conversions (plan §5.1): map the crate's plain-data DTOs to
|
||||
// acadrust `EntityType` and back. Lives in the host (uses the real acadrust
|
||||
// types), keeping the crate's DTOs dependency-light.
|
||||
|
||||
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, 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 >= 2 vertices"));
|
||||
}
|
||||
// Convert each segment: straight for bulge==0, a circular arc for
|
||||
// bulge!=0 (bulge = tan(included_angle/4)). This supports bulged
|
||||
// polyline profiles for Extrude/Revolve instead of rejecting them.
|
||||
let n = pl.vertices.len();
|
||||
let count = if pl.is_closed { n } else { n - 1 };
|
||||
let mut segs = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let a = pl.vertices[i];
|
||||
let b = pl.vertices[(i + 1) % n];
|
||||
let start = [a.location.x, a.location.y];
|
||||
let end = [b.location.x, b.location.y];
|
||||
if a.bulge.abs() < 1e-12 {
|
||||
segs.push(KCurve::Line(KLine { start, end }));
|
||||
} else {
|
||||
segs.push(bulge_arc_segment(start, end, a.bulge)?);
|
||||
}
|
||||
}
|
||||
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> {
|
||||
// Placement = R·S·v + t where the axis COLUMNS carry rotation AND uniform
|
||||
// scale (|x_axis| = s). A point maps v -> axes·v + origin (axes already embed
|
||||
// s — do NOT pre-multiply by s or scale is applied twice). A direction maps
|
||||
// v -> axes·v (no origin). Scalar fields (radius) scale by 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();
|
||||
// Z-rotation of the placement in the XY plane (for arc angles / insert rotation).
|
||||
let rot_z = p.x_axis[1].atan2(p.x_axis[0]);
|
||||
let apply = |v: Vector3| -> Vector3 {
|
||||
Vector3::new(
|
||||
p.x_axis[0] * v.x + p.y_axis[0] * v.y + p.z_axis[0] * v.z + p.origin[0],
|
||||
p.x_axis[1] * v.x + p.y_axis[1] * v.y + p.z_axis[1] * v.z + p.origin[1],
|
||||
p.x_axis[2] * v.x + p.y_axis[2] * v.y + p.z_axis[2] * v.z + p.origin[2],
|
||||
)
|
||||
};
|
||||
// Direction/vector transform: rotation+scale only, NO translation.
|
||||
let apply_dir = |v: Vector3| -> Vector3 {
|
||||
Vector3::new(
|
||||
p.x_axis[0] * v.x + p.y_axis[0] * v.y + p.z_axis[0] * v.z,
|
||||
p.x_axis[1] * v.x + p.y_axis[1] * v.y + p.z_axis[1] * v.z,
|
||||
p.x_axis[2] * v.x + p.y_axis[2] * v.y + p.z_axis[2] * v.z,
|
||||
)
|
||||
};
|
||||
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;
|
||||
// A Z-rotation shifts both angles by the placement's XY rotation.
|
||||
a.start_angle += rot_z;
|
||||
a.end_angle += rot_z;
|
||||
EntityType::Arc(a)
|
||||
}
|
||||
EntityType::Ellipse(e) => {
|
||||
let mut e = e.clone();
|
||||
e.center = apply(e.center);
|
||||
// The major-axis is a direction+length vector: rotate+scale it (no origin).
|
||||
e.major_axis = apply_dir(e.major_axis);
|
||||
// A Z-rotation also shifts the partial-arc sweep (eccentric-anomaly params).
|
||||
e.start_parameter += rot_z;
|
||||
e.end_parameter += rot_z;
|
||||
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);
|
||||
r.direction = apply_dir(r.direction);
|
||||
EntityType::Ray(r)
|
||||
}
|
||||
EntityType::XLine(x) => {
|
||||
let mut x = x.clone();
|
||||
x.base_point = apply(x.base_point);
|
||||
x.direction = apply_dir(x.direction);
|
||||
EntityType::XLine(x)
|
||||
}
|
||||
EntityType::Insert(ins) => {
|
||||
let mut ins = ins.clone();
|
||||
ins.insert_point = apply(ins.insert_point);
|
||||
// Compose the block rotation with the placement's Z-rotation.
|
||||
ins.rotation += rot_z;
|
||||
EntityType::Insert(ins)
|
||||
}
|
||||
EntityType::Viewport(vp) => {
|
||||
let mut vp = vp.clone();
|
||||
vp.center = apply(vp.center);
|
||||
EntityType::Viewport(vp)
|
||||
}
|
||||
EntityType::Text(t) => {
|
||||
let mut t = t.clone();
|
||||
t.insertion_point = apply(t.insertion_point);
|
||||
EntityType::Text(t)
|
||||
}
|
||||
EntityType::MText(t) => {
|
||||
let mut t = t.clone();
|
||||
t.insertion_point = apply(t.insertion_point);
|
||||
EntityType::MText(t)
|
||||
}
|
||||
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])
|
||||
}
|
||||
|
||||
/// Convert a polyline bulge segment (start → end, bulge = tan(θ/4)) to a
|
||||
/// `geom2d::Curve::Arc`. Positive bulge = counter-clockwise arc; negative = clockwise.
|
||||
pub(crate) 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 chord_x = end[0] - start[0];
|
||||
let chord_y = end[1] - start[1];
|
||||
let chord_len = (chord_x * chord_x + chord_y * chord_y).sqrt();
|
||||
if chord_len < 1e-12 {
|
||||
return Err(ApiError::validation("profile", "bulge segment with coincident endpoints"));
|
||||
}
|
||||
// Included angle θ = 4·atan(bulge); radius = (chord/2) / sin(θ/2).
|
||||
let theta = 4.0 * bulge.atan();
|
||||
let half = theta / 2.0;
|
||||
let sin_half = half.sin();
|
||||
if sin_half.abs() < 1e-9 {
|
||||
return Err(ApiError::validation("profile", "bulge segment with degenerate angle"));
|
||||
}
|
||||
let radius = (chord_len / 2.0) / sin_half.abs();
|
||||
// Center: chord midpoint + perpendicular offset. The apothem MAGNITUDE is
|
||||
// (chord/2)/|tan(θ/2)| (always positive); the SIGN of the bulge alone picks the
|
||||
// side (CCW=+90° perp, CW=-90° perp). Using signed tan(half) AND the sign flip
|
||||
// would double-count and mirror CW arcs to the wrong side.
|
||||
let mid_x = (start[0] + end[0]) / 2.0;
|
||||
let mid_y = (start[1] + end[1]) / 2.0;
|
||||
let apothem = (chord_len / 2.0) / half.tan().abs();
|
||||
let sign = if bulge >= 0.0 { 1.0 } else { -1.0 };
|
||||
let perp_x = -chord_y / chord_len * sign;
|
||||
let perp_y = chord_x / chord_len * sign;
|
||||
let centre = [mid_x + perp_x * apothem, mid_y + perp_y * apothem];
|
||||
// Start/end angles about the centre.
|
||||
let start_angle = (start[1] - centre[1]).atan2(start[0] - centre[0]);
|
||||
let end_angle = (end[1] - centre[1]).atan2(end[0] - centre[0]);
|
||||
Ok(KCurve::Arc(KArc { centre, 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> {
|
||||
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.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",
|
||||
}
|
||||
}
|
||||
|
||||
/// Coarse bounds for a non-solid entity (solids go through the kernel cache).
|
||||
/// For v1 typed curves we compute the obvious bounds; anything else errors
|
||||
/// `Unsupported` (viewport/annotation bounds land in later phases).
|
||||
pub fn entity_bounds(entity: Option<&EntityType>, id: ObjectId) -> ApiResult<Aabb> {
|
||||
let entity = entity.ok_or(ApiError::UnknownId(id))?;
|
||||
let bb = match entity {
|
||||
EntityType::Line(l) => Aabb {
|
||||
min: [
|
||||
l.start.x.min(l.end.x),
|
||||
l.start.y.min(l.end.y),
|
||||
l.start.z.min(l.end.z),
|
||||
],
|
||||
max: [
|
||||
l.start.x.max(l.end.x),
|
||||
l.start.y.max(l.end.y),
|
||||
l.start.z.max(l.end.z),
|
||||
],
|
||||
},
|
||||
EntityType::Circle(c) => Aabb {
|
||||
min: [c.center.x - c.radius, c.center.y - c.radius, c.center.z],
|
||||
max: [c.center.x + c.radius, c.center.y + c.radius, c.center.z],
|
||||
},
|
||||
EntityType::Point(p) => Aabb {
|
||||
min: [p.location.x, p.location.y, p.location.z],
|
||||
max: [p.location.x, p.location.y, p.location.z],
|
||||
},
|
||||
EntityType::LwPolyline(pl) if !pl.vertices.is_empty() => {
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for vtx in &pl.vertices {
|
||||
let (x, y, z) = (vtx.location.x, vtx.location.y, pl.elevation);
|
||||
min = [min[0].min(x), min[1].min(y), min[2].min(z)];
|
||||
max = [max[0].max(x), max[1].max(y), max[2].max(z)];
|
||||
}
|
||||
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) => points_aabb(s.control_points.iter().collect(), id)?,
|
||||
EntityType::Viewport(vp) => Aabb {
|
||||
min: [
|
||||
vp.center.x - vp.width / 2.0,
|
||||
vp.center.y - vp.height / 2.0,
|
||||
vp.center.z,
|
||||
],
|
||||
max: [
|
||||
vp.center.x + vp.width / 2.0,
|
||||
vp.center.y + vp.height / 2.0,
|
||||
vp.center.z,
|
||||
],
|
||||
},
|
||||
EntityType::RasterImage(img) => {
|
||||
// Bracket ALL FOUR image corners (origin, +u*W, +v*H, +u*W+v*H) per
|
||||
// axis so rotated/flipped images are not under-bounded.
|
||||
let (w, h) = (img.size.x, img.size.y);
|
||||
let ux = img.u_vector.x * w;
|
||||
let uy = img.u_vector.y * w;
|
||||
let vx = img.v_vector.x * h;
|
||||
let vy = img.v_vector.y * h;
|
||||
let x0 = img.insertion_point.x;
|
||||
let y0 = img.insertion_point.y;
|
||||
let xs = [x0, x0 + ux, x0 + vx, x0 + ux + vx];
|
||||
let ys = [y0, y0 + uy, y0 + vy, y0 + uy + vy];
|
||||
let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let min_y = ys.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let max_y = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
Aabb {
|
||||
min: [min_x, min_y, img.insertion_point.z],
|
||||
max: [max_x, max_y, img.insertion_point.z],
|
||||
}
|
||||
}
|
||||
EntityType::Text(t) => Aabb {
|
||||
// Coarse: a square of side height*N around the insertion point.
|
||||
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::MText(t) => Aabb {
|
||||
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::Dimension(acadrust::entities::Dimension::Linear(d)) => points_aabb(
|
||||
[&d.first_point, &d.second_point, &d.definition_point].into_iter().collect(),
|
||||
id,
|
||||
)?,
|
||||
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 }
|
||||
}
|
||||
// Read-mostly families with vertex/point data: bounds from their points.
|
||||
EntityType::Leader(l) => points_aabb(l.vertices.iter().collect(), id)?,
|
||||
EntityType::Mesh(m) => points_aabb(m.vertices.iter().collect(), id)?,
|
||||
EntityType::Face3D(f) => points_aabb(
|
||||
[&f.first_corner, &f.second_corner, &f.third_corner, &f.fourth_corner].into_iter().collect(),
|
||||
id,
|
||||
)?,
|
||||
EntityType::MLine(m) => points_aabb(m.vertices.iter().map(|v| &v.position).collect(), id)?,
|
||||
EntityType::Helix(h) => {
|
||||
// Bound the evaluated spline's control polygon (conservative); the
|
||||
// single start_point alone would be a degenerate zero-size box.
|
||||
if h.spline.control_points.is_empty() {
|
||||
points_aabb([&h.start_point].into_iter().collect(), id)?
|
||||
} else {
|
||||
points_aabb(h.spline.control_points.iter().collect(), id)?
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// AABB over a set of 3D points. A geometry-Empty error if there are no points
|
||||
/// to bound (the entity exists — do NOT misclassify as a stale/deleted id).
|
||||
fn points_aabb(pts: Vec<&Vector3>, _id: ObjectId) -> ApiResult<Aabb> {
|
||||
if pts.is_empty() {
|
||||
return Err(ApiError::geometry(GeometryErrorKind::Empty, "entity has no points to bound"));
|
||||
}
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for p in pts {
|
||||
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)];
|
||||
}
|
||||
Ok(Aabb { min, max })
|
||||
}
|
||||
// DocApi v2 entity conversions now live in the crate (`ocs_doc_api::convert`),
|
||||
// moved out of the host to minimize /src surface (the conversion logic is
|
||||
// host-independent: acadrust + crate DTOs only). Re-exported here so existing
|
||||
// `crate::app::doc_api_convert::*` paths keep working unchanged.
|
||||
#[allow(unused_imports)] // re-export for tests + downstream; non-test build may not use all of it
|
||||
pub use ocs_doc_api::convert::{
|
||||
bulge_arc_segment, curve_spec_to_entity, entity_bounds, entity_kind_name,
|
||||
entity_to_profile_curves, transform_entity_geometry,
|
||||
};
|
||||
Reference in new issue
Block a user