forked from 1b-agent/opencadstudio-upstream
feat(explode): support exploding ACIS and wire Region entities
This commit is contained in:
1 parent
26071f2021
commit
758ab28636
2 files changed
+845
No files matched your search
@@ -11,6 +11,8 @@
|
|||||||
// Insert → constituent entities (via opencadcodec explode_from_document)
|
// Insert → constituent entities (via opencadcodec explode_from_document)
|
||||||
// MLine → Lines (spine + offset lines per miter direction)
|
// MLine → Lines (spine + offset lines per miter direction)
|
||||||
// Dimension → Lines (extension + dimension + arrows) + Text
|
// Dimension → Lines (extension + dimension + arrows) + Text
|
||||||
|
// MText → Text lines
|
||||||
|
// Region → Lines, Arcs, Circles, Ellipses, Splines (from ACIS B-Rep loops or wires)
|
||||||
//
|
//
|
||||||
// Unsupported entity types are skipped silently.
|
// Unsupported entity types are skipped silently.
|
||||||
|
|
||||||
@@ -106,6 +108,7 @@ pub fn explode_entity(entity: &EntityType, document: &CadDocument) -> Vec<Entity
|
|||||||
EntityType::MLine(ml) => explode_mline(ml),
|
EntityType::MLine(ml) => explode_mline(ml),
|
||||||
EntityType::Dimension(dim) => explode_dimension(dim, document),
|
EntityType::Dimension(dim) => explode_dimension(dim, document),
|
||||||
EntityType::MText(t) => crate::entities::mtext::explode_mtext(t, document),
|
EntityType::MText(t) => crate::entities::mtext::explode_mtext(t, document),
|
||||||
|
EntityType::Region(region) => explode_region(region),
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -427,6 +430,446 @@ fn explode_mline(ml: &MLine) -> Vec<EntityType> {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Region explode ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn region_piece_common(region: &codec::entities::Region) -> EntityCommon {
|
||||||
|
let mut common = region.common.clone();
|
||||||
|
common.handle = Handle::NULL;
|
||||||
|
common.owner_handle = Handle::NULL;
|
||||||
|
common.xdictionary_handle = None;
|
||||||
|
common.reactors.clear();
|
||||||
|
common
|
||||||
|
}
|
||||||
|
|
||||||
|
fn explode_region(region: &codec::entities::Region) -> Vec<EntityType> {
|
||||||
|
let common = region_piece_common(region);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
|
||||||
|
if region.acis_data.has_data() {
|
||||||
|
let sat = region.parse_sat().or_else(|| {
|
||||||
|
(region.acis_data.is_binary && !region.acis_data.sab_data.is_empty())
|
||||||
|
.then(|| codec::entities::acis::SabReader::read(®ion.acis_data.sab_data).ok())
|
||||||
|
.flatten()
|
||||||
|
});
|
||||||
|
if let Some(sat) = sat {
|
||||||
|
let (bodies, _loss) = kernel::acis::lift(&sat);
|
||||||
|
for body in &bodies {
|
||||||
|
for face in body.face_keys() {
|
||||||
|
if let Some(profile) = kernel::brep::planar_face_profile(body, face) {
|
||||||
|
append_region_loops(&profile.plane, &profile.loops, &common, &mut out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out.is_empty() {
|
||||||
|
if let Some((plane, loops, _)) = crate::scene::model::presspull_model::profile_geometry(
|
||||||
|
&EntityType::Region(region.clone()),
|
||||||
|
) {
|
||||||
|
append_region_loops(&plane, &loops, &common, &mut out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for synthetic/legacy wire-only regions without ACIS payload.
|
||||||
|
for wire in ®ion.wires {
|
||||||
|
for pair in wire.points.windows(2) {
|
||||||
|
let (a, b) = (pair[0], pair[1]);
|
||||||
|
if (b - a).length() <= 1e-12 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(EntityType::Line(LineEnt {
|
||||||
|
common: common.clone(),
|
||||||
|
start: a,
|
||||||
|
end: b,
|
||||||
|
..LineEnt::new()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct CircularPiece {
|
||||||
|
centre: [f64; 2],
|
||||||
|
radius: f64,
|
||||||
|
start_pt: [f64; 2],
|
||||||
|
mid_pt: [f64; 2],
|
||||||
|
end_pt: [f64; 2],
|
||||||
|
sweep: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_circular_piece(curve: &kernel::geom2d::Curve) -> Option<CircularPiece> {
|
||||||
|
use kernel::geom2d::Curve;
|
||||||
|
match curve {
|
||||||
|
Curve::Circle(circle) if circle.radius > 1e-12 => {
|
||||||
|
let centre = circle.centre;
|
||||||
|
let radius = circle.radius;
|
||||||
|
let start_pt = [centre[0] + radius, centre[1]];
|
||||||
|
let mid_pt = [centre[0] - radius, centre[1]];
|
||||||
|
let end_pt = start_pt;
|
||||||
|
Some(CircularPiece {
|
||||||
|
centre,
|
||||||
|
radius,
|
||||||
|
start_pt,
|
||||||
|
mid_pt,
|
||||||
|
end_pt,
|
||||||
|
sweep: std::f64::consts::TAU,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Curve::Arc(arc) if arc.radius > 1e-12 => {
|
||||||
|
let centre = arc.centre;
|
||||||
|
let radius = arc.radius;
|
||||||
|
let pt_at = |ang: f64| [centre[0] + radius * ang.cos(), centre[1] + radius * ang.sin()];
|
||||||
|
let start_pt = pt_at(arc.start_angle);
|
||||||
|
let end_pt = pt_at(arc.end_angle);
|
||||||
|
let diff = (arc.end_angle - arc.start_angle).abs();
|
||||||
|
let sweep = if diff <= 1e-12 || (diff - std::f64::consts::TAU).abs() <= 1e-6 {
|
||||||
|
std::f64::consts::TAU
|
||||||
|
} else {
|
||||||
|
diff
|
||||||
|
};
|
||||||
|
let mid_pt = if (sweep - std::f64::consts::TAU).abs() <= 1e-6 {
|
||||||
|
pt_at(arc.start_angle + std::f64::consts::PI)
|
||||||
|
} else {
|
||||||
|
pt_at((arc.start_angle + arc.end_angle) * 0.5)
|
||||||
|
};
|
||||||
|
Some(CircularPiece {
|
||||||
|
centre,
|
||||||
|
radius,
|
||||||
|
start_pt,
|
||||||
|
mid_pt,
|
||||||
|
end_pt,
|
||||||
|
sweep,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Curve::Ellipse(arc) => {
|
||||||
|
let maj = arc.ellipse.major_radius;
|
||||||
|
let min = arc.ellipse.minor_radius;
|
||||||
|
if maj > 1e-12 && (min - maj).abs() <= 1e-6 * (1.0 + maj) {
|
||||||
|
let centre = arc.ellipse.centre;
|
||||||
|
let radius = (maj + min) * 0.5;
|
||||||
|
let start_pt = arc.ellipse.point_at(arc.start_parameter);
|
||||||
|
let end_pt = arc.ellipse.point_at(arc.end_parameter);
|
||||||
|
let diff = (arc.end_parameter - arc.start_parameter).abs();
|
||||||
|
let sweep = if (arc.sweep() - std::f64::consts::TAU).abs() <= 1e-6
|
||||||
|
|| diff <= 1e-12
|
||||||
|
|| (diff - std::f64::consts::TAU).abs() <= 1e-6
|
||||||
|
{
|
||||||
|
std::f64::consts::TAU
|
||||||
|
} else {
|
||||||
|
diff
|
||||||
|
};
|
||||||
|
let mid_pt = if (sweep - std::f64::consts::TAU).abs() <= 1e-6 {
|
||||||
|
arc.ellipse.point_at(arc.start_parameter + std::f64::consts::PI)
|
||||||
|
} else {
|
||||||
|
arc.ellipse.point_at((arc.start_parameter + arc.end_parameter) * 0.5)
|
||||||
|
};
|
||||||
|
Some(CircularPiece {
|
||||||
|
centre,
|
||||||
|
radius,
|
||||||
|
start_pt,
|
||||||
|
mid_pt,
|
||||||
|
end_pt,
|
||||||
|
sweep,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Curve::Nurbs(nurbs) => {
|
||||||
|
let p0 = nurbs.point_at(0.0);
|
||||||
|
let p1 = nurbs.point_at(0.5);
|
||||||
|
let p2 = nurbs.point_at(1.0);
|
||||||
|
let d = 2.0 * (p0[0] * (p1[1] - p2[1]) + p1[0] * (p2[1] - p0[1]) + p2[0] * (p0[1] - p1[1]));
|
||||||
|
if d.abs() > 1e-12 {
|
||||||
|
let p0_sq = p0[0] * p0[0] + p0[1] * p0[1];
|
||||||
|
let p1_sq = p1[0] * p1[0] + p1[1] * p1[1];
|
||||||
|
let p2_sq = p2[0] * p2[0] + p2[1] * p2[1];
|
||||||
|
let cx = (p0_sq * (p1[1] - p2[1]) + p1_sq * (p2[1] - p0[1]) + p2_sq * (p0[1] - p1[1])) / d;
|
||||||
|
let cy = (p0_sq * (p2[0] - p1[0]) + p1_sq * (p0[0] - p2[0]) + p2_sq * (p1[0] - p0[0])) / d;
|
||||||
|
let r = (p0[0] - cx).hypot(p0[1] - cy);
|
||||||
|
if r > 1e-12 {
|
||||||
|
let tol = 1e-5 * (1.0 + r);
|
||||||
|
let mut is_circle = true;
|
||||||
|
for step in 1..16 {
|
||||||
|
let t = step as f64 / 16.0;
|
||||||
|
let pt = nurbs.point_at(t);
|
||||||
|
if ((pt[0] - cx).hypot(pt[1] - cy) - r).abs() > tol {
|
||||||
|
is_circle = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if is_circle {
|
||||||
|
let th0 = (p0[1] - cy).atan2(p0[0] - cx);
|
||||||
|
let th1 = (p1[1] - cy).atan2(p1[0] - cx);
|
||||||
|
let th2 = (p2[1] - cy).atan2(p2[0] - cx);
|
||||||
|
let ccw_span = |from: f64, to: f64| {
|
||||||
|
let mut s = to - from;
|
||||||
|
while s < 0.0 {
|
||||||
|
s += std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
while s >= std::f64::consts::TAU {
|
||||||
|
s -= std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let sweep = if ccw_span(th0, th1) <= ccw_span(th0, th2) {
|
||||||
|
ccw_span(th0, th2)
|
||||||
|
} else {
|
||||||
|
ccw_span(th2, th0)
|
||||||
|
};
|
||||||
|
let sweep = if sweep <= 1e-12 {
|
||||||
|
std::f64::consts::TAU
|
||||||
|
} else {
|
||||||
|
sweep
|
||||||
|
};
|
||||||
|
return Some(CircularPiece {
|
||||||
|
centre: [cx, cy],
|
||||||
|
radius: r,
|
||||||
|
start_pt: p0,
|
||||||
|
mid_pt: p1,
|
||||||
|
end_pt: p2,
|
||||||
|
sweep,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_region_loops(
|
||||||
|
plane: &kernel::space::Plane,
|
||||||
|
loops: &[Vec<kernel::geom2d::Curve>],
|
||||||
|
common: &EntityCommon,
|
||||||
|
out: &mut Vec<EntityType>,
|
||||||
|
) {
|
||||||
|
use kernel::geom2d::Curve;
|
||||||
|
let Some(normal_arr) = plane.normal() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let normal = Vector3::new(normal_arr[0], normal_arr[1], normal_arr[2]);
|
||||||
|
let normal_tuple = (normal.x, normal.y, normal.z);
|
||||||
|
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal_tuple);
|
||||||
|
let ocs_x = DVec3::new(ax.0, ax.1, ax.2);
|
||||||
|
let ocs_y = DVec3::new(ay.0, ay.1, ay.2);
|
||||||
|
|
||||||
|
let make_circle = |centre: [f64; 2], radius: f64| -> EntityType {
|
||||||
|
let wcs_c = plane.point_at(centre);
|
||||||
|
let (ox, oy, oz) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
|
(wcs_c[0], wcs_c[1], wcs_c[2]),
|
||||||
|
normal_tuple,
|
||||||
|
);
|
||||||
|
let mut circle = CircleEnt::new();
|
||||||
|
circle.common = common.clone();
|
||||||
|
circle.center = Vector3::new(ox, oy, oz);
|
||||||
|
circle.radius = radius;
|
||||||
|
circle.normal = normal;
|
||||||
|
EntityType::Circle(circle)
|
||||||
|
};
|
||||||
|
|
||||||
|
let make_ellipse = |ellipse: &kernel::geom2d::Ellipse, start_param: f64, end_param: f64| -> Option<EntityType> {
|
||||||
|
if ellipse.major_radius <= 1e-12 || ellipse.minor_radius <= 1e-12 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let wcs_c = plane.point_at(ellipse.centre);
|
||||||
|
let tip = plane.point_at([
|
||||||
|
ellipse.centre[0] + ellipse.major_axis[0] * ellipse.major_radius,
|
||||||
|
ellipse.centre[1] + ellipse.major_axis[1] * ellipse.major_radius,
|
||||||
|
]);
|
||||||
|
let mut ent = codec::entities::Ellipse::new();
|
||||||
|
ent.common = common.clone();
|
||||||
|
ent.center = Vector3::new(wcs_c[0], wcs_c[1], wcs_c[2]);
|
||||||
|
ent.major_axis = Vector3::new(tip[0] - wcs_c[0], tip[1] - wcs_c[1], tip[2] - wcs_c[2]);
|
||||||
|
ent.minor_axis_ratio = ellipse.minor_radius / ellipse.major_radius;
|
||||||
|
ent.start_parameter = start_param;
|
||||||
|
ent.end_parameter = end_param;
|
||||||
|
ent.normal = normal;
|
||||||
|
Some(EntityType::Ellipse(ent))
|
||||||
|
};
|
||||||
|
|
||||||
|
let make_arc_from_piece = |piece: &CircularPiece| -> EntityType {
|
||||||
|
let wcs_c = DVec3::from_array(plane.point_at(piece.centre));
|
||||||
|
let (ox, oy, oz) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
|
(wcs_c.x, wcs_c.y, wcs_c.z),
|
||||||
|
normal_tuple,
|
||||||
|
);
|
||||||
|
let wcs_start = DVec3::from_array(plane.point_at(piece.start_pt));
|
||||||
|
let wcs_mid = DVec3::from_array(plane.point_at(piece.mid_pt));
|
||||||
|
let wcs_end = DVec3::from_array(plane.point_at(piece.end_pt));
|
||||||
|
|
||||||
|
let start_dir = wcs_start - wcs_c;
|
||||||
|
let mid_dir = wcs_mid - wcs_c;
|
||||||
|
let end_dir = wcs_end - wcs_c;
|
||||||
|
|
||||||
|
let ang_start = norm_angle(start_dir.dot(ocs_y).atan2(start_dir.dot(ocs_x)));
|
||||||
|
let ang_mid = norm_angle(mid_dir.dot(ocs_y).atan2(mid_dir.dot(ocs_x)));
|
||||||
|
let ang_end = norm_angle(end_dir.dot(ocs_y).atan2(end_dir.dot(ocs_x)));
|
||||||
|
|
||||||
|
let ccw_span = |from: f64, to: f64| {
|
||||||
|
let mut d = to - from;
|
||||||
|
while d < 0.0 {
|
||||||
|
d += std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
while d >= std::f64::consts::TAU {
|
||||||
|
d -= std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
d
|
||||||
|
};
|
||||||
|
|
||||||
|
let total_ccw = ccw_span(ang_start, ang_end);
|
||||||
|
let mid_ccw = ccw_span(ang_start, ang_mid);
|
||||||
|
|
||||||
|
let (start_angle, end_angle) = if mid_ccw <= total_ccw {
|
||||||
|
(ang_start, ang_end)
|
||||||
|
} else {
|
||||||
|
(ang_end, ang_start)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut arc = ArcEnt::new();
|
||||||
|
arc.common = common.clone();
|
||||||
|
arc.center = Vector3::new(ox, oy, oz);
|
||||||
|
arc.radius = piece.radius;
|
||||||
|
arc.start_angle = start_angle;
|
||||||
|
arc.end_angle = end_angle;
|
||||||
|
arc.normal = normal;
|
||||||
|
EntityType::Arc(arc)
|
||||||
|
};
|
||||||
|
|
||||||
|
for ring in loops {
|
||||||
|
// Collapse a closed loop of circular arcs/curves into one Circle.
|
||||||
|
let circular_pieces: Vec<CircularPiece> = ring.iter().filter_map(as_circular_piece).collect();
|
||||||
|
if circular_pieces.len() == ring.len() && !circular_pieces.is_empty() {
|
||||||
|
let first = &circular_pieces[0];
|
||||||
|
let tol = 1e-5 * (1.0 + first.radius);
|
||||||
|
let same_circle = circular_pieces.iter().all(|p| {
|
||||||
|
(p.centre[0] - first.centre[0]).hypot(p.centre[1] - first.centre[1]) <= tol
|
||||||
|
&& (p.radius - first.radius).abs() <= tol
|
||||||
|
});
|
||||||
|
let total_sweep: f64 = circular_pieces.iter().map(|p| p.sweep).sum();
|
||||||
|
if same_circle && (total_sweep - std::f64::consts::TAU).abs() <= 1e-4 {
|
||||||
|
out.push(make_circle(first.centre, first.radius));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse a closed loop of coaxial equal-axes elliptical arcs into one full Ellipse.
|
||||||
|
if let [Curve::Ellipse(first), rest @ ..] = ring.as_slice() {
|
||||||
|
let tol = 1e-6 * (1.0 + first.ellipse.major_radius);
|
||||||
|
let same_ellipse = rest.iter().all(|piece| {
|
||||||
|
matches!(
|
||||||
|
piece,
|
||||||
|
Curve::Ellipse(arc)
|
||||||
|
if (arc.ellipse.centre[0] - first.ellipse.centre[0])
|
||||||
|
.hypot(arc.ellipse.centre[1] - first.ellipse.centre[1])
|
||||||
|
<= tol
|
||||||
|
&& (arc.ellipse.major_radius - first.ellipse.major_radius).abs() <= tol
|
||||||
|
&& (arc.ellipse.minor_radius - first.ellipse.minor_radius).abs() <= tol
|
||||||
|
&& (arc.ellipse.major_axis[0] * first.ellipse.major_axis[0]
|
||||||
|
+ arc.ellipse.major_axis[1] * first.ellipse.major_axis[1]
|
||||||
|
- 1.0)
|
||||||
|
.abs()
|
||||||
|
<= 1e-6
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let total_sweep: f64 = ring
|
||||||
|
.iter()
|
||||||
|
.map(|piece| match piece {
|
||||||
|
Curve::Ellipse(arc) => {
|
||||||
|
let diff = (arc.end_parameter - arc.start_parameter).abs();
|
||||||
|
if (arc.sweep() - std::f64::consts::TAU).abs() <= 1e-6
|
||||||
|
|| diff <= 1e-12
|
||||||
|
|| (diff - std::f64::consts::TAU).abs() <= 1e-6
|
||||||
|
{
|
||||||
|
std::f64::consts::TAU
|
||||||
|
} else {
|
||||||
|
diff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
if same_ellipse && (total_sweep - std::f64::consts::TAU).abs() <= 1e-5 {
|
||||||
|
if let Some(ent) = make_ellipse(&first.ellipse, 0.0, std::f64::consts::TAU) {
|
||||||
|
out.push(ent);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for piece in ring {
|
||||||
|
if let Some(circ) = as_circular_piece(piece) {
|
||||||
|
if (circ.sweep - std::f64::consts::TAU).abs() <= 1e-6 {
|
||||||
|
out.push(make_circle(circ.centre, circ.radius));
|
||||||
|
} else {
|
||||||
|
out.push(make_arc_from_piece(&circ));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match piece {
|
||||||
|
Curve::Line(line) => {
|
||||||
|
let start = plane.point_at(line.start);
|
||||||
|
let end = plane.point_at(line.end);
|
||||||
|
let s = Vector3::new(start[0], start[1], start[2]);
|
||||||
|
let e = Vector3::new(end[0], end[1], end[2]);
|
||||||
|
if (e - s).length() <= 1e-12 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(EntityType::Line(LineEnt {
|
||||||
|
common: common.clone(),
|
||||||
|
start: s,
|
||||||
|
end: e,
|
||||||
|
normal,
|
||||||
|
..LineEnt::new()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Curve::Ellipse(arc) => {
|
||||||
|
let (start_param, end_param) =
|
||||||
|
if (arc.sweep() - std::f64::consts::TAU).abs() <= 1e-6 {
|
||||||
|
(0.0, std::f64::consts::TAU)
|
||||||
|
} else if arc.end_parameter > arc.start_parameter {
|
||||||
|
(arc.start_parameter, arc.end_parameter)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
arc.start_parameter,
|
||||||
|
arc.end_parameter + std::f64::consts::TAU,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if let Some(ent) = make_ellipse(&arc.ellipse, start_param, end_param) {
|
||||||
|
out.push(ent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Curve::Nurbs(nurbs) => {
|
||||||
|
let mut spline = codec::entities::Spline::new();
|
||||||
|
spline.common = common.clone();
|
||||||
|
spline.degree = nurbs.degree() as i32;
|
||||||
|
spline.knots = nurbs.knots().to_vec();
|
||||||
|
spline.control_points = nurbs
|
||||||
|
.control_points()
|
||||||
|
.iter()
|
||||||
|
.map(|&uv| {
|
||||||
|
let p = plane.point_at(uv);
|
||||||
|
Vector3::new(p[0], p[1], p[2])
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if nurbs.is_rational() {
|
||||||
|
spline.weights = nurbs.weights().to_vec();
|
||||||
|
spline.flags.rational = true;
|
||||||
|
}
|
||||||
|
spline.flags.planar = true;
|
||||||
|
spline.flags.closed = nurbs.is_closed();
|
||||||
|
spline.normal = normal;
|
||||||
|
out.push(EntityType::Spline(spline));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Dimension explode ──────────────────────────────────────────────────────
|
// ── Dimension explode ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Convert a Dimension entity into Lines (geometry) + Text (label).
|
/// Convert a Dimension entity into Lines (geometry) + Text (label).
|
||||||
|
|||||||
@@ -467,3 +467,405 @@ fn apply_explode_replacements_matches_legacy_loop() {
|
|||||||
)];
|
)];
|
||||||
assert_eq!(explode_batch(&items, &scene_batched.document), vec![vec![]]);
|
assert_eq!(explode_batch(&items, &scene_batched.document), vec![vec![]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explode_region_acis_sat_and_binary_sab_with_hole_and_style() {
|
||||||
|
use codec::entities::acis::{SabWriter, SatDocument};
|
||||||
|
use codec::entities::{AcisData, Region};
|
||||||
|
use kernel::brep;
|
||||||
|
use kernel::geom2d::{Arc as KArc, Curve as KCurve, Line as KLine};
|
||||||
|
use kernel::space::Plane;
|
||||||
|
|
||||||
|
let plane = Plane::from_axes([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||||
|
let outer = vec![
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [0.0, 0.0],
|
||||||
|
end: [20.0, 0.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [20.0, 0.0],
|
||||||
|
end: [20.0, 10.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [20.0, 10.0],
|
||||||
|
end: [0.0, 10.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [0.0, 10.0],
|
||||||
|
end: [0.0, 0.0],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let hole: Vec<KCurve> = (0..4)
|
||||||
|
.map(|q| {
|
||||||
|
let start = std::f64::consts::FRAC_PI_2 * q as f64;
|
||||||
|
KCurve::Arc(KArc {
|
||||||
|
centre: [10.0, 5.0],
|
||||||
|
radius: 2.0,
|
||||||
|
start_angle: start,
|
||||||
|
end_angle: start + std::f64::consts::FRAC_PI_2,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let body = brep::planar_region(plane, &[outer, hole]).expect("planar region with hole");
|
||||||
|
let mut sat = SatDocument::new();
|
||||||
|
kernel::acis::append(&body, &mut sat).expect("sat append");
|
||||||
|
let sat_text = sat.to_sat_string();
|
||||||
|
let parsed_sat = SatDocument::parse(&sat_text).expect("sat parse");
|
||||||
|
let sab_bytes = SabWriter::write(&parsed_sat);
|
||||||
|
|
||||||
|
let doc = CadDocument::new();
|
||||||
|
|
||||||
|
for (label, acis_data) in [
|
||||||
|
("SAT", AcisData::from_sat(&sat_text)),
|
||||||
|
(
|
||||||
|
"SAB",
|
||||||
|
AcisData {
|
||||||
|
is_binary: true,
|
||||||
|
sab_data: sab_bytes,
|
||||||
|
..AcisData::default()
|
||||||
|
},
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let mut region = Region::new();
|
||||||
|
region.acis_data = acis_data;
|
||||||
|
region.common.layer = "REGIONS".to_string();
|
||||||
|
region.common.color = codec::types::Color::from_index(3);
|
||||||
|
region.common.linetype = "DASHED".to_string();
|
||||||
|
region.common.xdictionary_handle = Some(Handle::new(999));
|
||||||
|
|
||||||
|
let pieces = explode_entity(&EntityType::Region(region), &doc);
|
||||||
|
let lines: Vec<_> = pieces
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| match p {
|
||||||
|
EntityType::Line(l) => Some(l),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let circles: Vec<_> = pieces
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| match p {
|
||||||
|
EntityType::Circle(c) => Some(c),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(lines.len(), 4, "{label}: outer rectangle must yield 4 lines");
|
||||||
|
assert_eq!(circles.len(), 1, "{label}: circular hole must yield 1 circle");
|
||||||
|
assert!((circles[0].radius - 2.0).abs() < 1e-6);
|
||||||
|
assert!((circles[0].center.x - 10.0).abs() < 1e-6);
|
||||||
|
assert!((circles[0].center.y - 5.0).abs() < 1e-6);
|
||||||
|
|
||||||
|
for piece in &pieces {
|
||||||
|
let c = piece.common();
|
||||||
|
assert_eq!(c.layer, "REGIONS", "{label}: layer preserved");
|
||||||
|
assert_eq!(c.color, codec::types::Color::from_index(3), "{label}: color preserved");
|
||||||
|
assert_eq!(c.linetype, "DASHED", "{label}: linetype preserved");
|
||||||
|
assert!(c.handle.is_null(), "{label}: handle cleared");
|
||||||
|
assert!(c.owner_handle.is_null(), "{label}: owner_handle cleared");
|
||||||
|
assert!(c.xdictionary_handle.is_none(), "{label}: xdict must not leak");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explode_region_tilted_plane_arc_and_multi_island() {
|
||||||
|
use codec::entities::acis::SatDocument;
|
||||||
|
use codec::entities::{AcisData, Region};
|
||||||
|
use kernel::brep;
|
||||||
|
use kernel::geom2d::{Arc as KArc, Curve as KCurve, Line as KLine};
|
||||||
|
use kernel::space::Plane;
|
||||||
|
|
||||||
|
// Semicircle + closing chord on tilted plane (XZ plane at y = 4.0).
|
||||||
|
let plane = Plane::from_axes([0.0, 4.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]);
|
||||||
|
let d_loop = vec![
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [-5.0, 0.0],
|
||||||
|
end: [5.0, 0.0],
|
||||||
|
}),
|
||||||
|
KCurve::Arc(KArc {
|
||||||
|
centre: [0.0, 0.0],
|
||||||
|
radius: 5.0,
|
||||||
|
start_angle: 0.0,
|
||||||
|
end_angle: std::f64::consts::PI,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let body = brep::planar_region(plane, &[d_loop]).expect("tilted D region");
|
||||||
|
let mut sat = SatDocument::new();
|
||||||
|
kernel::acis::append(&body, &mut sat).expect("sat append");
|
||||||
|
let mut region = Region::new();
|
||||||
|
region.acis_data = AcisData::from_sat(&sat.to_sat_string());
|
||||||
|
|
||||||
|
let doc = CadDocument::new();
|
||||||
|
let pieces = explode_entity(&EntityType::Region(region), &doc);
|
||||||
|
assert_eq!(pieces.len(), 2, "D-region must explode to 1 Line + 1 Arc");
|
||||||
|
|
||||||
|
let line = pieces
|
||||||
|
.iter()
|
||||||
|
.find_map(|p| match p {
|
||||||
|
EntityType::Line(l) => Some(l),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("expected Line");
|
||||||
|
assert!((line.start.y - 4.0).abs() < 1e-6 && (line.end.y - 4.0).abs() < 1e-6);
|
||||||
|
|
||||||
|
let arc = pieces
|
||||||
|
.iter()
|
||||||
|
.find_map(|p| match p {
|
||||||
|
EntityType::Arc(a) => Some(a),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("expected Arc");
|
||||||
|
let arc_planar = OpenCADStudio::entities::curve::arc_curve(arc);
|
||||||
|
let mid = arc_planar.point_at(0.5);
|
||||||
|
assert!(
|
||||||
|
(mid[0] - 0.0).abs() < 1e-6 && (mid[1] - 4.0).abs() < 1e-6 && (mid[2] - 5.0).abs() < 1e-6,
|
||||||
|
"exploded arc midpoint in WCS should sit at (0, 4, 5), got {mid:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Multi-island region (two disjoint rectangles unioned in one Region B-Rep).
|
||||||
|
let xy = Plane::from_axes([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||||
|
let rect_loop = |x0: f64, x1: f64| {
|
||||||
|
vec![
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [x0, 0.0],
|
||||||
|
end: [x1, 0.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [x1, 0.0],
|
||||||
|
end: [x1, 5.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [x1, 5.0],
|
||||||
|
end: [x0, 5.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [x0, 5.0],
|
||||||
|
end: [x0, 0.0],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
let r1 = brep::planar_region(xy, &[rect_loop(0.0, 5.0)]).unwrap();
|
||||||
|
let r2 = brep::planar_region(xy, &[rect_loop(10.0, 15.0)]).unwrap();
|
||||||
|
let union_body = brep::union_planar_regions(&[r1, r2], 1e-6).unwrap();
|
||||||
|
let mut union_sat = SatDocument::new();
|
||||||
|
kernel::acis::append(&union_body, &mut union_sat).unwrap();
|
||||||
|
let mut multi_region = Region::new();
|
||||||
|
multi_region.acis_data = AcisData::from_sat(&union_sat.to_sat_string());
|
||||||
|
let multi_pieces = explode_entity(&EntityType::Region(multi_region), &doc);
|
||||||
|
assert_eq!(
|
||||||
|
multi_pieces.len(),
|
||||||
|
8,
|
||||||
|
"two-island region must explode to 8 lines"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wire-only fallback region (no ACIS data, display wires present).
|
||||||
|
let mut wire_region = Region::new();
|
||||||
|
wire_region.wires = vec![codec::entities::Wire::from_points(vec![
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
Vector3::new(4.0, 0.0, 0.0),
|
||||||
|
Vector3::new(4.0, 3.0, 0.0),
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
])];
|
||||||
|
let wire_pieces = explode_entity(&EntityType::Region(wire_region), &doc);
|
||||||
|
assert_eq!(
|
||||||
|
wire_pieces.len(),
|
||||||
|
3,
|
||||||
|
"wire-only triangle region must explode to 3 lines"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explode_region_acis_with_body_transform() {
|
||||||
|
use codec::entities::acis::SatDocument;
|
||||||
|
use codec::entities::{AcisData, Region};
|
||||||
|
use kernel::brep;
|
||||||
|
use kernel::geom2d::{Curve as KCurve, Line as KLine};
|
||||||
|
use kernel::space::Plane;
|
||||||
|
|
||||||
|
let plane = Plane::from_axes([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||||
|
let rect = vec![
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [0.0, 0.0],
|
||||||
|
end: [10.0, 0.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [10.0, 0.0],
|
||||||
|
end: [10.0, 5.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [10.0, 5.0],
|
||||||
|
end: [0.0, 5.0],
|
||||||
|
}),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [0.0, 5.0],
|
||||||
|
end: [0.0, 0.0],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let body = brep::planar_region(plane, &[rect]).expect("planar region");
|
||||||
|
let mut sat = SatDocument::new();
|
||||||
|
kernel::acis::append(&body, &mut sat).expect("sat append");
|
||||||
|
|
||||||
|
// AutoCAD writes a body transform record for placed regions.
|
||||||
|
// Translate by [50.0, 100.0, 0.0].
|
||||||
|
sat.set_placement(
|
||||||
|
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
|
||||||
|
[50.0, 100.0, 0.0],
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut region = Region::new();
|
||||||
|
region.acis_data = AcisData::from_sat(&sat.to_sat_string());
|
||||||
|
region.common.layer = "TRANSFORMED".to_string();
|
||||||
|
|
||||||
|
let doc = CadDocument::new();
|
||||||
|
let pieces = explode_entity(&EntityType::Region(region), &doc);
|
||||||
|
assert_eq!(pieces.len(), 4, "placed region must explode to 4 lines");
|
||||||
|
|
||||||
|
let lines: Vec<_> = pieces
|
||||||
|
.iter()
|
||||||
|
.filter_map(|p| match p {
|
||||||
|
EntityType::Line(l) => Some(l),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(lines.len(), 4);
|
||||||
|
|
||||||
|
// Verify all line endpoints were shifted by [50, 100, 0]
|
||||||
|
for l in &lines {
|
||||||
|
assert!(l.start.x >= 50.0 - 1e-6 && l.start.x <= 60.0 + 1e-6);
|
||||||
|
assert!(l.start.y >= 100.0 - 1e-6 && l.start.y <= 105.0 + 1e-6);
|
||||||
|
assert!(l.end.x >= 50.0 - 1e-6 && l.end.x <= 60.0 + 1e-6);
|
||||||
|
assert!(l.end.y >= 100.0 - 1e-6 && l.end.y <= 105.0 + 1e-6);
|
||||||
|
assert_eq!(l.common.layer, "TRANSFORMED");
|
||||||
|
assert!(l.common.handle.is_null());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explode_region_elliptical_and_spline_boundaries() {
|
||||||
|
use codec::entities::acis::SatDocument;
|
||||||
|
use codec::entities::{AcisData, Region};
|
||||||
|
use kernel::brep;
|
||||||
|
use kernel::geom2d::{
|
||||||
|
Curve as KCurve, Ellipse as KEllipse, EllipseArc as KEllipseArc, Line as KLine,
|
||||||
|
NurbsCurve as KNurbs,
|
||||||
|
};
|
||||||
|
use kernel::space::Plane;
|
||||||
|
|
||||||
|
let plane = Plane::from_axes([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
|
||||||
|
let doc = CadDocument::new();
|
||||||
|
|
||||||
|
// 1. Full non-circular ellipse region (major 8.0, minor 4.0).
|
||||||
|
let ell_loop = vec![KCurve::Ellipse(KEllipseArc::full(KEllipse {
|
||||||
|
centre: [15.0, 20.0],
|
||||||
|
major_radius: 8.0,
|
||||||
|
minor_radius: 4.0,
|
||||||
|
major_axis: [1.0, 0.0],
|
||||||
|
}))];
|
||||||
|
let body = brep::planar_region(plane, &[ell_loop]).expect("ellipse region");
|
||||||
|
let mut sat = SatDocument::new();
|
||||||
|
kernel::acis::append(&body, &mut sat).expect("sat append");
|
||||||
|
let mut region = Region::new();
|
||||||
|
region.acis_data = AcisData::from_sat(&sat.to_sat_string());
|
||||||
|
region.common.layer = "ELLIPSES".to_string();
|
||||||
|
|
||||||
|
let pieces = explode_entity(&EntityType::Region(region), &doc);
|
||||||
|
assert_eq!(
|
||||||
|
pieces.len(),
|
||||||
|
1,
|
||||||
|
"full ellipse region must explode to 1 Ellipse"
|
||||||
|
);
|
||||||
|
let ell = match &pieces[0] {
|
||||||
|
EntityType::Ellipse(e) => e,
|
||||||
|
other => panic!("expected Ellipse entity, got {other:?}"),
|
||||||
|
};
|
||||||
|
assert!((ell.center.x - 15.0).abs() < 1e-6);
|
||||||
|
assert!((ell.center.y - 20.0).abs() < 1e-6);
|
||||||
|
assert!((ell.major_axis_length() - 8.0).abs() < 1e-6);
|
||||||
|
assert!((ell.minor_axis_ratio - 0.5).abs() < 1e-6);
|
||||||
|
assert_eq!(ell.common.layer, "ELLIPSES");
|
||||||
|
assert!(ell.common.handle.is_null());
|
||||||
|
|
||||||
|
// 2. Region with a cubic NURBS/Spline edge closed by a straight line.
|
||||||
|
let nurbs = KNurbs::new(
|
||||||
|
3,
|
||||||
|
vec![
|
||||||
|
[0.0, 0.0],
|
||||||
|
[3.0, 5.0],
|
||||||
|
[7.0, -2.0],
|
||||||
|
[10.0, 0.0],
|
||||||
|
],
|
||||||
|
vec![0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.expect("valid cubic nurbs");
|
||||||
|
let spline_loop = vec![
|
||||||
|
KCurve::Nurbs(nurbs),
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [10.0, 0.0],
|
||||||
|
end: [0.0, 0.0],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let body_spline = brep::planar_region(plane, &[spline_loop]).expect("spline region");
|
||||||
|
let mut sat_spline = SatDocument::new();
|
||||||
|
kernel::acis::append(&body_spline, &mut sat_spline).expect("sat append");
|
||||||
|
let mut region_spline = Region::new();
|
||||||
|
region_spline.acis_data = AcisData::from_sat(&sat_spline.to_sat_string());
|
||||||
|
region_spline.common.layer = "SPLINES".to_string();
|
||||||
|
|
||||||
|
let pieces_spline = explode_entity(&EntityType::Region(region_spline), &doc);
|
||||||
|
assert_eq!(
|
||||||
|
pieces_spline.len(),
|
||||||
|
2,
|
||||||
|
"spline region must explode to 1 Spline and 1 Line"
|
||||||
|
);
|
||||||
|
let has_line = pieces_spline.iter().any(|p| matches!(p, EntityType::Line(_)));
|
||||||
|
let has_spline = pieces_spline.iter().any(|p| matches!(p, EntityType::Spline(_)));
|
||||||
|
assert!(has_line, "must contain a Line");
|
||||||
|
assert!(has_spline, "must contain a Spline");
|
||||||
|
for p in &pieces_spline {
|
||||||
|
assert_eq!(p.common().layer, "SPLINES");
|
||||||
|
assert!(p.common().handle.is_null());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Region with a circular arc represented as equal-radii Ellipse arc + lines.
|
||||||
|
let equal_ell_arc = KCurve::Ellipse(KEllipseArc {
|
||||||
|
ellipse: KEllipse {
|
||||||
|
centre: [0.0, 0.0],
|
||||||
|
major_radius: 5.0,
|
||||||
|
minor_radius: 5.0,
|
||||||
|
major_axis: [1.0, 0.0],
|
||||||
|
},
|
||||||
|
start_parameter: 0.0,
|
||||||
|
end_parameter: std::f64::consts::FRAC_PI_2,
|
||||||
|
});
|
||||||
|
let d_equal_loop = vec![
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [0.0, 0.0],
|
||||||
|
end: [5.0, 0.0],
|
||||||
|
}),
|
||||||
|
equal_ell_arc,
|
||||||
|
KCurve::Line(KLine {
|
||||||
|
start: [0.0, 5.0],
|
||||||
|
end: [0.0, 0.0],
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
let body_arc = brep::planar_region(plane, &[d_equal_loop]).expect("equal-radii ellipse region");
|
||||||
|
let mut sat_arc = SatDocument::new();
|
||||||
|
kernel::acis::append(&body_arc, &mut sat_arc).expect("sat append");
|
||||||
|
let mut region_arc = Region::new();
|
||||||
|
region_arc.acis_data = AcisData::from_sat(&sat_arc.to_sat_string());
|
||||||
|
|
||||||
|
let pieces_arc = explode_entity(&EntityType::Region(region_arc), &doc);
|
||||||
|
assert_eq!(pieces_arc.len(), 3, "must yield 2 lines and 1 arc");
|
||||||
|
let arc_piece = pieces_arc
|
||||||
|
.iter()
|
||||||
|
.find_map(|p| match p {
|
||||||
|
EntityType::Arc(a) => Some(a),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("equal-radii ellipse arc must emit EntityType::Arc");
|
||||||
|
assert!((arc_piece.radius - 5.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user