forked from 1b-agent/opencadstudio-upstream
Merge pull request #1205 from Ramox81/codex/fix-break-curve-spans-20260911
This commit is contained in:
commit
f51d492994
2 files changed
+135
-199
No files matched your search
@@ -4038,12 +4038,36 @@ impl OpenCADStudio {
|
||||
.and_then(|e| break_entity(e, p1, p2));
|
||||
match replacement {
|
||||
Some(frags) => {
|
||||
let unchanged = frags.len() == 1 && self.tabs[i].scene.document.get_entity(handle)
|
||||
.is_some_and(|original| {
|
||||
let mut fragment = frags[0].clone();
|
||||
fragment.common_mut().handle = handle;
|
||||
&fragment == original
|
||||
});
|
||||
if unchanged {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_output("BREAK: no geometry changed.");
|
||||
return Task::none();
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "BREAK");
|
||||
self.push_undo_snapshot(i, label);
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
let count = frags.len();
|
||||
for e in frags {
|
||||
self.tabs[i].scene.add_entity(e);
|
||||
let owner = self.tabs[i].scene.document.get_entity(handle)
|
||||
.map(|entity| entity.common().owner_handle);
|
||||
let mut fragments = frags.into_iter();
|
||||
if let Some(mut first) = fragments.next() {
|
||||
first.common_mut().handle = handle;
|
||||
self.tabs[i].scene.update_entity(first);
|
||||
for mut fragment in fragments {
|
||||
fragment.common_mut().handle = Handle::NULL;
|
||||
if let Some(owner) = owner { fragment.common_mut().owner_handle = owner; }
|
||||
self.tabs[i].scene.add_entity(fragment);
|
||||
}
|
||||
} else {
|
||||
self.tabs[i].scene.erase_entities(&[handle]);
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
|
||||
@@ -41,10 +41,7 @@ pub fn break_entity(entity: &EntityType, p1: DVec3, p2: DVec3) -> Option<Vec<Ent
|
||||
EntityType::Line(line) => Some(break_line(line, p1, p2)),
|
||||
EntityType::Arc(arc) => Some(break_arc(arc, p1, p2)),
|
||||
EntityType::Circle(c) => Some(break_circle(c, p1, p2)),
|
||||
EntityType::LwPolyline(p) => {
|
||||
let p = crate::entities::curve::lwpolyline_world_xy(p)?;
|
||||
Some(break_lwpolyline(&p, p1, p2))
|
||||
}
|
||||
EntityType::LwPolyline(p) => Some(break_lwpolyline(p, p1, p2)),
|
||||
EntityType::Ellipse(e) => Some(break_ellipse(e, p1, p2)),
|
||||
EntityType::Spline(s) => Some(break_spline(s, p1, p2)),
|
||||
_ => None,
|
||||
@@ -91,215 +88,99 @@ fn break_line(line: &LineEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||
result
|
||||
}
|
||||
|
||||
fn picked_spans(curve: &cadkernel::space::PlanarCurve, p1: DVec3, p2: DVec3) -> Option<Vec<[f64; 2]>> {
|
||||
let first = curve.plane.project(p1.to_array())?;
|
||||
let second = curve.plane.project(p2.to_array())?;
|
||||
let a = cadkernel::geom2d::closest_point(&curve.curve, first).t;
|
||||
let b = cadkernel::geom2d::closest_point(&curve.curve, second).t;
|
||||
cadkernel::geom2d::break_spans(&curve.curve, a, b, cadkernel::geom2d::Tolerance::new(1e-9))
|
||||
}
|
||||
|
||||
fn break_arc(arc: &ArcEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||
let cx = arc.center.x;
|
||||
let cy = arc.center.y;
|
||||
let r = arc.radius;
|
||||
|
||||
let plane = crate::entities::curve::arc_curve(arc).plane;
|
||||
let Some(q1) = plane.project(p1.to_array()) else {
|
||||
let curve = crate::entities::curve::arc_curve(arc);
|
||||
let Some(spans) = picked_spans(&curve, p1, p2) else {
|
||||
return vec![EntityType::Arc(arc.clone())];
|
||||
};
|
||||
let Some(q2) = plane.project(p2.to_array()) else {
|
||||
return vec![EntityType::Arc(arc.clone())];
|
||||
};
|
||||
let a1 = angle_on_arc(cx, cy, DVec3::new(q1[0], q1[1], 0.0));
|
||||
let a2 = angle_on_arc(cx, cy, DVec3::new(q2[0], q2[1], 0.0));
|
||||
|
||||
let start = arc.start_angle;
|
||||
let end = arc.end_angle;
|
||||
|
||||
// Normalize: clamp a1 to arc range, then remove CCW from a1 to a2
|
||||
let a1_on = clamp_to_arc(a1, start, end);
|
||||
let a2_on = clamp_to_arc(a2, start, end);
|
||||
|
||||
// Keep the span outside the two break points.
|
||||
if (a1_on - a2_on).abs() < 0.01 {
|
||||
// Single-point break: return original unchanged (no gap)
|
||||
return vec![EntityType::Arc(arc.clone())];
|
||||
}
|
||||
|
||||
let _ = r; // radius unchanged
|
||||
let mut result = arc.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
result.start_angle = a2_on;
|
||||
result.end_angle = a1_on;
|
||||
vec![EntityType::Arc(result)]
|
||||
let cadkernel::geom2d::Curve::Arc(geometry) = &curve.curve else { unreachable!() };
|
||||
spans.into_iter().map(|[from, to]| {
|
||||
let mut result = arc.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
result.start_angle = (geometry.start_angle + from * geometry.sweep()).rem_euclid(std::f64::consts::TAU);
|
||||
result.end_angle = (geometry.start_angle + to * geometry.sweep()).rem_euclid(std::f64::consts::TAU);
|
||||
EntityType::Arc(result)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn break_circle(circle: &acadrust::entities::Circle, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||
let cx = circle.center.x;
|
||||
let cy = circle.center.y;
|
||||
|
||||
let plane = crate::entities::curve::circle_curve(circle).plane;
|
||||
let Some(q1) = plane.project(p1.to_array()) else {
|
||||
let curve = crate::entities::curve::circle_curve(circle);
|
||||
let Some(spans) = picked_spans(&curve, p1, p2) else {
|
||||
return vec![EntityType::Circle(circle.clone())];
|
||||
};
|
||||
let Some(q2) = plane.project(p2.to_array()) else {
|
||||
return vec![EntityType::Circle(circle.clone())];
|
||||
};
|
||||
let a1 = angle_on_arc(cx, cy, DVec3::new(q1[0], q1[1], 0.0));
|
||||
let a2 = angle_on_arc(cx, cy, DVec3::new(q2[0], q2[1], 0.0));
|
||||
|
||||
if (a1 - a2).abs() < 0.01 {
|
||||
return vec![EntityType::Circle(circle.clone())];
|
||||
}
|
||||
|
||||
// Convert circle to arc, removing CCW from a1 to a2
|
||||
let mut arc = ArcEnt::new();
|
||||
arc.common = circle.common.clone();
|
||||
arc.common.handle = Handle::NULL;
|
||||
arc.center = circle.center.clone();
|
||||
arc.radius = circle.radius;
|
||||
arc.thickness = circle.thickness;
|
||||
arc.normal = circle.normal.clone();
|
||||
arc.start_angle = a2;
|
||||
arc.end_angle = a1;
|
||||
vec![EntityType::Arc(arc)]
|
||||
spans.into_iter().map(|[from, to]| {
|
||||
let mut arc = ArcEnt::new();
|
||||
arc.common = circle.common.clone();
|
||||
arc.common.handle = Handle::NULL;
|
||||
arc.center = circle.center;
|
||||
arc.radius = circle.radius;
|
||||
arc.thickness = circle.thickness;
|
||||
arc.normal = circle.normal;
|
||||
arc.start_angle = from * std::f64::consts::TAU;
|
||||
arc.end_angle = (to * std::f64::consts::TAU).rem_euclid(std::f64::consts::TAU);
|
||||
EntityType::Arc(arc)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
fn break_lwpolyline(p: &LwPolyline, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||
// For LwPolyline, find the nearest vertex indices for p1 and p2,
|
||||
// then split into two polylines at those vertices.
|
||||
let n = p.vertices.len();
|
||||
if n < 2 {
|
||||
return vec![EntityType::LwPolyline(p.clone())];
|
||||
}
|
||||
|
||||
let t1 = nearest_pline_param(p, p1);
|
||||
let t2 = nearest_pline_param(p, p2);
|
||||
let (ta, tb) = if t1 <= t2 { (t1, t2) } else { (t2, t1) };
|
||||
|
||||
// Build two polylines: [0..ta] and [tb..end]
|
||||
let idx_a = ta.min(n - 1);
|
||||
let idx_b = tb.min(n - 1);
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
// First piece
|
||||
if idx_a > 0 {
|
||||
let mut first = p.clone();
|
||||
first.common.handle = Handle::NULL;
|
||||
first.vertices = p.vertices[..=idx_a].to_vec();
|
||||
first.is_closed = false;
|
||||
result.push(EntityType::LwPolyline(first));
|
||||
}
|
||||
|
||||
// Second piece
|
||||
if idx_b < n - 1 {
|
||||
let mut second = p.clone();
|
||||
second.common.handle = Handle::NULL;
|
||||
second.vertices = p.vertices[idx_b..].to_vec();
|
||||
second.is_closed = false;
|
||||
result.push(EntityType::LwPolyline(second));
|
||||
}
|
||||
|
||||
if result.is_empty() {
|
||||
vec![EntityType::LwPolyline(p.clone())]
|
||||
} else {
|
||||
result
|
||||
let unchanged = || vec![EntityType::LwPolyline(p.clone())];
|
||||
let Some(curve) = crate::entities::curve::lwpolyline_curve(p) else { return unchanged(); };
|
||||
let Some(spans) = picked_spans(&curve, p1, p2) else { return unchanged(); };
|
||||
let cadkernel::geom2d::Curve::Polyline(geometry) = &curve.curve else { unreachable!() };
|
||||
let mut result = Vec::with_capacity(spans.len());
|
||||
for [from, to] in spans {
|
||||
let Some(range) = geometry.ranged(from, to) else { return unchanged(); };
|
||||
let mut fragment = p.clone();
|
||||
fragment.common.handle = Handle::NULL;
|
||||
fragment.is_closed = false;
|
||||
fragment.vertices.clear();
|
||||
for (index, vertex) in range.polyline.vertices.iter().enumerate() {
|
||||
// The final vertex has no outgoing segment. Keep its source metadata
|
||||
// when it is an original endpoint, otherwise the preceding segment's.
|
||||
let segment = &range.segments[index.min(range.segments.len() - 1)];
|
||||
let last = index == range.segments.len();
|
||||
let source = if last && segment.to == 1.0 {
|
||||
(segment.source_index + 1) % p.vertices.len()
|
||||
} else { segment.source_index };
|
||||
let mut output = p.vertices[source].clone();
|
||||
output.location.x = vertex.position[0];
|
||||
output.location.y = vertex.position[1];
|
||||
if !last || segment.to != 1.0 {
|
||||
let original = &p.vertices[segment.source_index];
|
||||
let widths = segment.interpolate(original.start_width, original.end_width);
|
||||
output.start_width = widths[0];
|
||||
output.end_width = widths[1];
|
||||
output.bulge = if last { range.polyline.vertices[index - 1].bulge } else { vertex.bulge };
|
||||
}
|
||||
fragment.vertices.push(output);
|
||||
}
|
||||
result.push(EntityType::LwPolyline(fragment));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn break_ellipse(ell: &EllipseEnt, p1: DVec3, p2: DVec3) -> Vec<EntityType> {
|
||||
// Compute the eccentric-anomaly parameter of a world point relative to ellipse.
|
||||
// World = DXF (XY drawing plane).
|
||||
let cx = ell.center.x;
|
||||
let cy = ell.center.y;
|
||||
let a = (ell.major_axis.x.powi(2) + ell.major_axis.y.powi(2)).sqrt();
|
||||
if a < 1e-9 {
|
||||
let Some(curve) = crate::entities::curve::ellipse_curve(ell) else {
|
||||
return vec![EntityType::Ellipse(ell.clone())];
|
||||
}
|
||||
let _b = a * ell.minor_axis_ratio;
|
||||
let nx = ell.major_axis.x / a;
|
||||
let ny = ell.major_axis.y / a;
|
||||
|
||||
// Project a point onto the ellipse parameter (eccentric anomaly)
|
||||
let param_of = |pt: DVec3| -> f64 {
|
||||
let rx = pt.x - cx;
|
||||
let ry = pt.y - cy;
|
||||
let xl = rx * nx + ry * ny;
|
||||
let yl = -rx * ny + ry * nx;
|
||||
yl.atan2(xl) // atan2(yl/b*b, xl/a*a) simplifies to atan2(yl,xl) for ordering
|
||||
};
|
||||
|
||||
let t0 = ell.start_parameter;
|
||||
let t1 = ell.end_parameter;
|
||||
|
||||
let pa1 = param_of(p1);
|
||||
let pa2 = param_of(p2);
|
||||
|
||||
// Clamp both params to arc range (same logic as clamp_to_arc for arcs)
|
||||
let span_deg = {
|
||||
let s = t1 - t0;
|
||||
if s <= 0.0 {
|
||||
s + std::f64::consts::TAU
|
||||
} else {
|
||||
s
|
||||
}
|
||||
};
|
||||
let clamp = |a: f64| -> f64 {
|
||||
let rel =
|
||||
((a - t0) % std::f64::consts::TAU + std::f64::consts::TAU) % std::f64::consts::TAU;
|
||||
if rel <= span_deg {
|
||||
a
|
||||
} else if rel < span_deg + (std::f64::consts::TAU - span_deg) / 2.0 {
|
||||
t1
|
||||
} else {
|
||||
t0
|
||||
}
|
||||
};
|
||||
let a1_on = clamp(pa1);
|
||||
let a2_on = clamp(pa2);
|
||||
|
||||
if (a1_on - a2_on).abs() < 1e-4 {
|
||||
let Some(spans) = picked_spans(&curve, p1, p2) else {
|
||||
return vec![EntityType::Ellipse(ell.clone())];
|
||||
}
|
||||
|
||||
// Remove CCW from a1_on to a2_on → result goes from a2_on to a1_on
|
||||
let mut result = ell.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
result.start_parameter = a2_on;
|
||||
result.end_parameter = a1_on;
|
||||
vec![EntityType::Ellipse(result)]
|
||||
};
|
||||
let cadkernel::geom2d::Curve::Ellipse(geometry) = &curve.curve else { unreachable!() };
|
||||
spans.into_iter().map(|[from, to]| {
|
||||
let mut result = ell.clone();
|
||||
result.common.handle = Handle::NULL;
|
||||
result.start_parameter = geometry.start_parameter + from * geometry.sweep();
|
||||
result.end_parameter = geometry.start_parameter + to * geometry.sweep();
|
||||
EntityType::Ellipse(result)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
// ── Small utilities ────────────────────────────────────────────────────────
|
||||
|
||||
/// Returns the angle (radians, 0-2π) of `pt` viewed from (cx, cy) in the world XY plane.
|
||||
fn angle_on_arc(cx: f64, cy: f64, pt: DVec3) -> f64 {
|
||||
let dx = pt.x - cx;
|
||||
let dy = pt.y - cy;
|
||||
dy.atan2(dx).rem_euclid(std::f64::consts::TAU)
|
||||
}
|
||||
|
||||
/// Clamp angle `a` to within the arc's angular range (CCW from `start` to `end`).
|
||||
fn clamp_to_arc(a: f64, start: f64, end: f64) -> f64 {
|
||||
let span = (end - start).rem_euclid(std::f64::consts::TAU);
|
||||
let rel = (a - start).rem_euclid(std::f64::consts::TAU);
|
||||
if rel <= span {
|
||||
a
|
||||
} else if rel < span + (std::f64::consts::TAU - span) / 2.0 {
|
||||
end
|
||||
} else {
|
||||
start
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the index of the polyline vertex closest to `pt`.
|
||||
fn nearest_pline_param(p: &LwPolyline, pt: DVec3) -> usize {
|
||||
p.vertices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.min_by_key(|(_, v)| {
|
||||
let dx = v.location.x - pt.x;
|
||||
let dy = v.location.y - pt.y;
|
||||
((dx * dx + dy * dy) * 1e6) as i64
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn world_to_dxf(v: DVec3) -> DVec3 {
|
||||
// World = DXF (identity).
|
||||
DVec3::new(v.x, v.y, v.z)
|
||||
@@ -506,6 +387,8 @@ inventory::submit!(crate::command::CommandRegistration { names: &["BREAK"] });
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use acadrust::entities::LwVertex;
|
||||
use acadrust::types::Vector2;
|
||||
|
||||
#[test]
|
||||
fn first_option_replaces_the_selection_point_and_at_reuses_it() {
|
||||
@@ -526,4 +409,33 @@ mod tests {
|
||||
if result == handle && p1 == replacement && p2 == replacement
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curved_polyline_break_preserves_partial_bulges_widths_and_plane() {
|
||||
let mut polyline = LwPolyline::new();
|
||||
polyline.normal = Vector3::new(0.0, 1.0, 0.0);
|
||||
polyline.elevation = 5.0;
|
||||
let mut first_vertex = LwVertex::new(Vector2::new(0.0, 0.0));
|
||||
first_vertex.bulge = 1.0;
|
||||
first_vertex.start_width = 2.0;
|
||||
first_vertex.end_width = 4.0;
|
||||
polyline.vertices = vec![first_vertex, LwVertex::from_coords(10.0, 0.0)];
|
||||
let curve = crate::entities::curve::lwpolyline_curve(&polyline).unwrap();
|
||||
let first = DVec3::from_array(curve.point_at(0.25));
|
||||
let second = DVec3::from_array(curve.point_at(0.75));
|
||||
|
||||
let fragments = break_lwpolyline(&polyline, first, second);
|
||||
assert_eq!(fragments.len(), 2);
|
||||
let EntityType::LwPolyline(before) = &fragments[0] else { panic!("expected polyline") };
|
||||
let EntityType::LwPolyline(after) = &fragments[1] else { panic!("expected polyline") };
|
||||
assert_eq!(before.normal, polyline.normal);
|
||||
assert_eq!(before.elevation, polyline.elevation);
|
||||
assert!((before.vertices[0].start_width - 2.0).abs() < 1e-12);
|
||||
assert!((before.vertices[0].end_width - 2.5).abs() < 1e-12);
|
||||
assert!((after.vertices[0].start_width - 3.5).abs() < 1e-12);
|
||||
assert!((after.vertices[0].end_width - 4.0).abs() < 1e-12);
|
||||
let expected_bulge = (std::f64::consts::PI / 16.0).tan();
|
||||
assert!((before.vertices[0].bulge - expected_bulge).abs() < 1e-12);
|
||||
assert!((after.vertices[0].bulge - expected_bulge).abs() < 1e-12);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user