forked from 1b-agent/opencadstudio-upstream
feat(select): offer the selection modes at "Select objects"
Only Previous and Last were on offer, so Window, Crossing, Fence, the two polygons and All could not be reached at all -- and with no keyboard to type them into, the buttons were the only way in. Window and Crossing fix the sense of the next box. Dragging derives that from the direction the corner travels and would overwrite it the moment the corner moved, so the derivation stands down while a keyword holds it. Add and Remove decide what a pick does for the rest of the round: Remove subtracts the way Shift already does, rather than through PICKADD, which decides whether a pick *replaces* the set and so would have wiped everything gathered so far. Both are dropped when the next command starts, since neither outlives the selection that asked for them. Fence and the polygons are picked point by point, which TRIM and EXTEND already ask for. That gesture -- the points, the rubber-banded dashed preview, whether the path closes -- moves into one place both bind to, and the two preview builders duplicated in trim.rs go with it. A fence is not the polygon test with crossing set: that closes the point list and counts whatever lies inside, so a line drawn past a row of objects would take everything behind it too. It gets a hit test that only counts a real intersection, and skips the area-based geometry entirely. The lasso gathered its hits from wires, hatches, block hatches, meshes and block meshes inline; the new modes need the same list, and a second copy would drift. Both now come through `Scene::path_hit_handles`. Closes #596 Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
22890a9411
commit
5c5c7606a1
12 files changed
+613
-151
No files matched your search
@@ -7678,6 +7678,127 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// Top-level solid handles caught by a lasso polygon.
|
||||
/// Everything a picked path takes, gathered from every kind of geometry the
|
||||
/// selection can reach: wires, hatches, hatches inside blocks, meshes and
|
||||
/// block meshes, narrowed first to what the path's own bounds can touch.
|
||||
///
|
||||
/// `open_fence` distinguishes the two gestures that arrive here. A fence is
|
||||
/// a line drawn through the drawing and takes only what it actually cuts; a
|
||||
/// polygon closes back on itself and can also take what it encloses, or
|
||||
/// merely touches when `crossing`. The lasso and the Fence / WPolygon /
|
||||
/// CPolygon keywords both come through this, so they cannot drift apart on
|
||||
/// which geometry they remember to look at. (#596)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn path_hit_handles(
|
||||
&self,
|
||||
poly_pts: &[iced::Point],
|
||||
crossing: bool,
|
||||
open_fence: bool,
|
||||
all_wires: std::sync::Arc<Vec<WireModel>>,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
to_world: impl Fn(iced::Point) -> glam::DVec3,
|
||||
) -> Vec<Handle> {
|
||||
let world_aabb = poly_pts.iter().fold(
|
||||
[
|
||||
f64::INFINITY,
|
||||
f64::INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
f64::NEG_INFINITY,
|
||||
],
|
||||
|mut aabb, &point| {
|
||||
let world = to_world(point);
|
||||
aabb[0] = aabb[0].min(world.x);
|
||||
aabb[1] = aabb[1].min(world.y);
|
||||
aabb[2] = aabb[2].max(world.x);
|
||||
aabb[3] = aabb[3].max(world.y);
|
||||
aabb
|
||||
},
|
||||
);
|
||||
let screen_rect = poly_pts.iter().fold(
|
||||
[
|
||||
f32::INFINITY,
|
||||
f32::INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
f32::NEG_INFINITY,
|
||||
],
|
||||
|mut rect, point| {
|
||||
rect[0] = rect[0].min(point.x);
|
||||
rect[1] = rect[1].min(point.y);
|
||||
rect[2] = rect[2].max(point.x);
|
||||
rect[3] = rect[3].max(point.y);
|
||||
rect
|
||||
},
|
||||
);
|
||||
let area_candidates =
|
||||
self.interaction_candidates_in_aabb(all_wires, world_aabb, screen_rect, view_rot, eye, bounds);
|
||||
let candidate_handles = self.interaction_candidate_handles(&area_candidates);
|
||||
let wire_hits = if open_fence {
|
||||
crate::scene::pick::hit_test::poly_fence_hit(
|
||||
poly_pts,
|
||||
&area_candidates,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
} else {
|
||||
crate::scene::pick::hit_test::poly_hit(
|
||||
poly_pts,
|
||||
crossing,
|
||||
&area_candidates,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
};
|
||||
let mut handles: Vec<Handle> = wire_hits
|
||||
.into_iter()
|
||||
.filter_map(Self::handle_from_wire_name)
|
||||
.collect();
|
||||
// An open fence has no interior, so the area-based geometry can only be
|
||||
// reached by the polygon gestures.
|
||||
if !open_fence {
|
||||
handles.extend(crate::scene::pick::hit_test::poly_hit_hatch(
|
||||
poly_pts,
|
||||
crossing,
|
||||
&self.visible_hatches_for_click(candidate_handles.as_ref()),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(crate::scene::pick::hit_test::poly_hit_insert_hatch(
|
||||
poly_pts,
|
||||
crossing,
|
||||
self.insert_hatches_for_click().as_ref(),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(self.mesh_poly_hit(
|
||||
poly_pts,
|
||||
crossing,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
handles.extend(self.block_mesh_poly_hit(
|
||||
poly_pts,
|
||||
crossing,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
candidate_handles.as_ref(),
|
||||
));
|
||||
}
|
||||
handles.retain(|&h| self.passes_selection_filter(h));
|
||||
handles.retain(|&h| !self.is_layer_locked(h));
|
||||
handles
|
||||
}
|
||||
|
||||
pub fn mesh_poly_hit(
|
||||
&self,
|
||||
poly: &[iced::Point],
|
||||
|
||||
@@ -1038,6 +1038,86 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
out
|
||||
}
|
||||
|
||||
/// Wires the open polyline `fence` actually crosses.
|
||||
///
|
||||
/// The Fence selection mode draws a line through a drawing and takes whatever
|
||||
/// it cuts. That is not the polygon test with `crossing` set: the polygon one
|
||||
/// closes the point list back to its start and counts anything lying inside the
|
||||
/// area that closure encloses, so a fence drawn past a group of objects would
|
||||
/// sweep up everything behind it as well. Here the chain stays open and only a
|
||||
/// real intersection counts. (#596)
|
||||
pub fn poly_fence_hit<'a, W: WireSource + ?Sized>(
|
||||
fence: &[Point],
|
||||
wires: &'a W,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<&'a str> {
|
||||
if fence.len() < 2 {
|
||||
return vec![];
|
||||
}
|
||||
let cuts = |a: Point, b: Point| {
|
||||
fence
|
||||
.windows(2)
|
||||
.any(|leg| segments_intersect(a, b, leg[0], leg[1]))
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
let mut seen: HashSet<&str> = HashSet::default();
|
||||
// Indexed segments when the source has them (the same fast path the
|
||||
// polygon test uses), otherwise walk each wire's own points.
|
||||
if let Some(segments) = wires.segments() {
|
||||
for segment in segments {
|
||||
let Some(wire) = wires.source_wire(segment.wire) else {
|
||||
continue;
|
||||
};
|
||||
let start = segment.start as usize;
|
||||
if start + 1 >= wire.points.len() || seen.contains(wire.name.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let a = world_to_screen(
|
||||
wp64(wire.points[start], &wire.points_low, start),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
let b = world_to_screen(
|
||||
wp64(wire.points[start + 1], &wire.points_low, start + 1),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
if cuts(a, b) && seen.insert(wire.name.as_str()) {
|
||||
out.push(wire.name.as_str());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
for wire in wires.iter() {
|
||||
if wire.points.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
let hit = (0..wire.points.len() - 1).any(|k| {
|
||||
let a = world_to_screen(
|
||||
wp64(wire.points[k], &wire.points_low, k),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
let b = world_to_screen(
|
||||
wp64(wire.points[k + 1], &wire.points_low, k + 1),
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
cuts(a, b)
|
||||
});
|
||||
if hit && seen.insert(wire.name.as_str()) {
|
||||
out.push(wire.name.as_str());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>(
|
||||
wires: &'a W,
|
||||
poly: &[Point],
|
||||
@@ -1431,7 +1511,7 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn world_to_screen(
|
||||
pub(crate) fn world_to_screen(
|
||||
world: glam::DVec3,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
|
||||
@@ -14,6 +14,11 @@ pub struct SelectionState {
|
||||
pub box_current: Option<Point>,
|
||||
pub box_last: Option<(Point, Point)>,
|
||||
pub box_crossing: bool,
|
||||
/// Set when a Window / Crossing selection keyword fixed the sense of the
|
||||
/// box being drawn. Dragging normally decides it from the direction the
|
||||
/// corner travels, which would immediately overwrite what the user just
|
||||
/// asked for, so that derivation stands down while this holds.
|
||||
pub box_crossing_locked: bool,
|
||||
pub box_last_crossing: bool,
|
||||
/// A preview-only selection marquee `(anchor, current, crossing)` in canvas
|
||||
/// pixels, drawn identically to a real box-selection (green crossing fill /
|
||||
|
||||
Reference in new issue
Block a user