Files
OneCAD-Draft/src/modules/draw/fence.rs
T
Hakan SevenandClaude Opus 5 5c5c7606a1 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 <noreply@anthropic.com>
2026-08-07 13:32:11 +03:00

117 lines
3.7 KiB
Rust

// A path picked point by point, and the way it is drawn while it is being
// picked.
//
// Two features ask the user for the same gesture. TRIM and EXTEND take a fence
// through the drawing and cut whatever it crosses; the Fence / WPolygon /
// CPolygon selection modes take one and select whatever it crosses or encloses.
// Only what happens to the result differs, so the gesture itself — the points,
// the rubber-banded dashed preview, and whether the path closes back on itself
// — lives here and both bind to it.
use crate::scene::model::wire_model::WireModel;
/// Dash rhythm shared by every in-progress selection gesture, so a fence being
/// picked reads the same wherever it is offered.
const DASH_LENGTH: f32 = 0.8;
const DASH_PATTERN: [f32; 8] = [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
/// The crossing-box green, matching the selection marquee the option imitates.
const CROSSING_GREEN: [f32; 4] = [0.35, 0.85, 0.35, 0.9];
/// A path being picked one point at a time.
#[derive(Clone, Debug, Default)]
pub struct FencePick {
/// Points committed so far, in drawing coordinates.
pub points: Vec<[f64; 2]>,
/// Whether the path closes back to its first point. A polygon encloses an
/// area and can be asked what lies inside it; an open fence only ever
/// answers what it cuts.
pub closed: bool,
}
impl FencePick {
/// An open fence: it selects what it crosses, and encloses nothing.
pub fn fence() -> Self {
Self {
points: Vec::new(),
closed: false,
}
}
/// A closed polygon, which encloses an area as well as cutting through one.
pub fn polygon() -> Self {
Self {
points: Vec::new(),
closed: true,
}
}
pub fn push(&mut self, point: [f64; 2]) {
self.points.push(point);
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
/// Enough points to mean anything: two for a fence to cut with, three for a
/// polygon to enclose with.
pub fn is_usable(&self) -> bool {
self.points.len() >= if self.closed { 3 } else { 2 }
}
/// The picked path, brought back to its first point when it is a polygon.
pub fn path(&self) -> Vec<[f64; 2]> {
let mut path = self.points.clone();
if self.closed {
if let Some(first) = self.points.first().copied() {
path.push(first);
}
}
path
}
/// The path so far, trailing the cursor, dashed.
pub fn preview(&self, cursor: [f64; 2], name: &str) -> WireModel {
let mut path = self.points.clone();
path.push(cursor);
if self.closed {
if let Some(first) = self.points.first().copied() {
path.push(first);
}
}
dashed(path, name, WireModel::CYAN)
}
}
/// Rubber-banded crossing rectangle, drawn like the selection marquee it stands
/// in for so the option reads as the selection it performs. (#336)
pub fn crossing_box_preview(corner: [f64; 2], cursor: [f64; 2], name: &str) -> WireModel {
dashed(
vec![
corner,
[corner[0], cursor[1]],
cursor,
[cursor[0], corner[1]],
corner,
],
name,
CROSSING_GREEN,
)
}
fn dashed(path: Vec<[f64; 2]>, name: &str, color: [f32; 4]) -> WireModel {
let points: Vec<[f32; 3]> = path
.into_iter()
.map(|p| [p[0] as f32, p[1] as f32, 0.0])
.collect();
let mut wire = WireModel::solid(name.into(), points, color, false);
wire.pattern_length = DASH_LENGTH;
wire.pattern = DASH_PATTERN;
wire
}