Merge remote-tracking branch 'origin/main' into codex/field-command

# Conflicts:
#	src/ui/window/attdef_dialog.rs
This commit is contained in:
ramox81 committed 2026-10-02 10:46:53 +03:00
commit 762cf1e7e4
68 files changed
+4665 -1048

No files matched your search

+1 -1
View File
@@ -203,7 +203,7 @@ impl CadModule for DrawModule {
// Slide-out: Define / Manage / Synchronize Attributes and
// Set Base Point.
tools: vec![
RibbonItem::LargeTool(insert_block::tool()),
insert_block::gallery(),
RibbonItem::Tool(create_block::tool()),
RibbonItem::Tool(edit_block::tool()),
RibbonItem::Tool(attedit::tool()),
+224 -48
View File
@@ -1108,6 +1108,162 @@ fn extract_sub_polyline(poly: &LwPolyline, s0: f64, s1: f64) -> Option<LwPolylin
Some(new_poly)
}
/// Kernel curve for one LwPolyline segment.
///
/// The boolean tells whether the kernel curve runs opposite to the polyline's
/// own segment direction. Negative-bulge arcs are represented by the kernel as
/// a positive-sweep arc, so their intersection parameter has to be reversed
/// back to the polyline's local 0..1 parameter.
fn lwpoly_segment_curve(
poly: &LwPolyline,
segment: usize,
) -> Option<(Curve, bool)> {
let n = poly.vertices.len();
if n < 2 {
return None;
}
let a = &poly.vertices[segment % n];
let b = &poly.vertices[(segment + 1) % n];
let p0 = [a.location.x, a.location.y];
let p1 = [b.location.x, b.location.y];
if (p1[0] - p0[0]).hypot(p1[1] - p0[1]) < 1e-12 {
return None;
}
if let Some(arc) = BulgeArc::from_bulge(p0, p1, a.bulge) {
if arc.sweep >= 0.0 {
Some((
Curve::Arc(KernelArc {
centre: arc.center,
radius: arc.radius,
start_angle: arc.start_angle,
end_angle: arc.start_angle + arc.sweep,
}),
false,
))
} else {
Some((
Curve::Arc(KernelArc {
centre: arc.center,
radius: arc.radius,
start_angle: arc.end_angle,
end_angle: arc.end_angle - arc.sweep,
}),
true,
))
}
} else {
Some((
Curve::Line(KernelLine {
start: p0,
end: p1,
}),
false,
))
}
}
/// Global polyline parameters at which non-adjacent segments of the same
/// polyline intersect.
///
/// Segment neighbours are deliberately excluded: their common vertex is a
/// normal polyline vertex, not a self-intersection. A polyline cuts itself
/// only when it is one of the cutting edges.
fn lwpoly_self_cut_params(poly: &LwPolyline, geos: &[Geo]) -> Vec<f64> {
let n = poly.vertices.len();
if n < 3 || !geos.iter().any(|geo| geo.handle() == poly.common.handle) {
return Vec::new();
}
let closed = poly.is_closed;
let segment_count = if closed { n } else { n - 1 };
let total = segment_count as f64;
let mut cuts = Vec::new();
for i in 0..segment_count {
let Some((curve_a, reverse_a)) = lwpoly_segment_curve(poly, i) else {
continue;
};
for j in (i + 1)..segment_count {
// Ordinary neighbouring segments meet at their common vertex.
let adjacent = j == i + 1
|| (closed && i == 0 && j + 1 == segment_count);
if adjacent {
continue;
}
let Some((curve_b, reverse_b)) = lwpoly_segment_curve(poly, j) else {
continue;
};
let hits = kernel_intersect(
&curve_a,
&curve_b,
KernelTolerance::new(CUT_TOLERANCE),
);
for hit in hits {
let mut ua = hit.t_a;
let mut ub = hit.t_b;
if reverse_a {
ua = 1.0 - ua;
}
if reverse_b {
ub = 1.0 - ub;
}
if !ua.is_finite()
|| !ub.is_finite()
|| ua < -1e-7
|| ua > 1.0 + 1e-7
|| ub < -1e-7
|| ub > 1.0 + 1e-7
{
continue;
}
ua = ua.clamp(0.0, 1.0);
ub = ub.clamp(0.0, 1.0);
// If both sides only touch at existing vertices, those
// vertices already act as trim limits below.
let a_endpoint = ua <= 1e-7 || ua >= 1.0 - 1e-7;
let b_endpoint = ub <= 1e-7 || ub >= 1.0 - 1e-7;
if a_endpoint && b_endpoint {
continue;
}
let pa = i as f64 + ua;
let pb = j as f64 + ub;
cuts.push(if closed {
pa.rem_euclid(total)
} else {
pa
});
cuts.push(if closed {
pb.rem_euclid(total)
} else {
pb
});
}
}
}
cuts.sort_by(f64::total_cmp);
cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
cuts
}
/// Trim a clicked LwPolyline: remove the portion containing the click, bounded
/// by the nearest boundary intersections on each side. A closed polyline needs
/// ≥2 cuts and becomes an open polyline (the surviving arc); an open one yields
@@ -1131,18 +1287,32 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<
poly.vertices[i % n].bulge
};
// Boundary cuts as global params (segment index + local u).
// External boundary cuts as global params (segment index + local u).
let mut cuts: Vec<f64> = Vec::new();
for i in 0..seg_count {
let p0 = vx(i);
let p1 = vx(i + 1);
let b = seg_bulge(i);
for u in polyline_seg_ts(p0, p1, b, handle, geos) {
let param = i as f64 + u.clamp(0.0, 1.0);
cuts.push(if closed { param.rem_euclid(total) } else { param });
cuts.push(if closed {
param.rem_euclid(total)
} else {
param
});
}
}
cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
// A polyline can also cut itself. These are intersections between
// non-adjacent segments belonging to this same LwPolyline. Its vertices
// are not cuts: the trimmed piece runs on past corners to the next
// cutting edge, and a polyline no edge crosses is left alone.
cuts.extend(lwpoly_self_cut_params(poly, geos));
cuts.sort_by(f64::total_cmp);
cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
if cuts.is_empty() {
return None;
@@ -1589,9 +1759,6 @@ enum TrimMode {
struct CrossingWindow {
min: [f64; 2],
max: [f64; 2],
/// The first corner is the trim-side hint, matching the side from which
/// the crossing window was dragged.
pick: [f64; 2],
}
fn segment_window_range(
@@ -1650,6 +1817,11 @@ fn crossing_trim_lwpolyline(
};
let mut removed = Vec::<(f64, f64)>::new();
// Compute the polyline's own non-adjacent segment intersections once.
// These global parameters will later be mapped back into each source segment.
let self_cuts = lwpoly_self_cut_params(poly, geos);
for i in 0..seg_count {
let a = vertex_xy(i);
let b = vertex_xy(i + 1);
@@ -1678,42 +1850,54 @@ fn crossing_trim_lwpolyline(
let Some((inside_lo, inside_hi)) = window_range else {
continue;
};
let cuts = polyline_seg_ts(a, b, bulge, handle, geos);
if cuts.is_empty() {
continue;
// External intersections on this segment.
let mut cuts = polyline_seg_ts(a, b, bulge, handle, geos);
// Also include self-intersections belonging to this source segment.
// `lwpoly_self_cut_params()` stores them as global polyline parameters:
// segment_index + local_parameter
//
// Convert the ones belonging to segment `i` back to local 0..1.
let seg_start = i as f64;
let seg_end = seg_start + 1.0;
for global_t in self_cuts.iter().copied() {
if global_t > seg_start + 1e-6
&& global_t < seg_end - 1e-6
{
cuts.push(global_t - seg_start);
}
}
let pick_t = if let Some(ba) = BulgeArc::from_bulge(a, b, bulge) {
let angle = (window.pick[1] - ba.center[1]).atan2(window.pick[0] - ba.center[0]);
let travelled = if ba.sweep >= 0.0 {
(angle - ba.start_angle).rem_euclid(TAU)
} else {
-((ba.start_angle - angle).rem_euclid(TAU))
};
(travelled / ba.sweep).clamp(inside_lo, inside_hi)
} else {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let len2 = dx * dx + dy * dy;
let projected = if len2 > 1e-12 {
((window.pick[0] - a[0]) * dx + (window.pick[1] - a[1]) * dy) / len2
} else {
(inside_lo + inside_hi) * 0.5
};
projected.clamp(inside_lo, inside_hi)
};
// The source segment endpoints are also valid trim limits.
cuts.push(0.0);
cuts.push(1.0);
let mut bounds = vec![0.0];
bounds.extend(cuts);
bounds.push(1.0);
bounds.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
bounds.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
if let Some(span) = bounds
.windows(2)
.find(|span| pick_t >= span[0] - 1e-6 && pick_t <= span[1] + 1e-6)
{
if span[1] - span[0] > 1e-6 {
removed.push((i as f64 + span[0], i as f64 + span[1]));
cuts.sort_by(f64::total_cmp);
cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
// Remove every trim span that is actually touched by the selection box.
//
// A single source segment may contain several sub-spans when external
// boundaries cross it. Only the sub-spans overlapping the window are removed.
for span in cuts.windows(2) {
let lo = span[0].clamp(0.0, 1.0);
let hi = span[1].clamp(0.0, 1.0);
if hi - lo <= 1e-6 {
continue;
}
// This trim span participates when its parameter interval overlaps the
// portion of the source segment lying inside the crossing rectangle.
let overlaps_window =
hi >= inside_lo - 1e-6 && lo <= inside_hi + 1e-6;
if overlaps_window {
removed.push((
i as f64 + lo,
i as f64 + hi,
));
}
}
}
@@ -2730,11 +2914,7 @@ impl CadCommand for TrimCommand {
if !matches!(self.mode, TrimMode::Pick) || fence.len() < 2 {
return None;
}
let window = window.map(|(min, max)| CrossingWindow {
min,
max,
pick: fence[0],
});
let window = window.map(|(min, max)| CrossingWindow { min, max });
let replacements = fence_pass(
&self.all_entities,
&self.geos,
@@ -3114,7 +3294,6 @@ impl CadCommand for TrimCommand {
let window = CrossingWindow {
min: [p1[0].min(p2[0]), p1[1].min(p2[1])],
max: [p1[0].max(p2[0]), p1[1].max(p2[1])],
pick: p1,
};
self.fence_run(&rect, Some(window))
}
@@ -3146,7 +3325,6 @@ impl CadCommand for TrimCommand {
let window = CrossingWindow {
min: [p1[0].min(p2[0]), p1[1].min(p2[1])],
max: [p1[0].max(p2[0]), p1[1].max(p2[1])],
pick: p1,
};
out.extend(fence_result_preview(
&self.all_entities,
@@ -3580,7 +3758,6 @@ impl CadCommand for ExtendCommand {
let window = CrossingWindow {
min: [p1[0].min(p2[0]), p1[1].min(p2[1])],
max: [p1[0].max(p2[0]), p1[1].max(p2[1])],
pick: p1,
};
self.fence_run(&rect, Some(window))
}
@@ -3612,7 +3789,6 @@ impl CadCommand for ExtendCommand {
let window = CrossingWindow {
min: [p1[0].min(p2[0]), p1[1].min(p2[1])],
max: [p1[0].max(p2[0]), p1[1].max(p2[1])],
pick: p1,
};
out.extend(fence_result_preview(
&self.all_entities,