forked from 1b-agent/opencadstudio-upstream
fix(pick): require all sub-geometry inside window selection for multi-wire and mesh entities
This commit is contained in:
1 parent
4972bbed70
commit
058cd02042
2 files changed
+387
-151
No files matched your search
+100
-64
@@ -8342,24 +8342,33 @@ impl Scene {
|
|||||||
let Some(indices) = lookup.get(&handle) else {
|
let Some(indices) = lookup.get(&handle) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if indices
|
let test = |set: &MeshLodSet| {
|
||||||
.iter()
|
set.geometry_lods().first().is_some_and(|mesh| {
|
||||||
.filter_map(|&index| meshes.get(index as usize))
|
!pick::hit_test::mesh_box_hit(
|
||||||
.any(|set| {
|
a,
|
||||||
set.geometry_lods().first().is_some_and(|mesh| {
|
b,
|
||||||
!pick::hit_test::mesh_box_hit(
|
crossing,
|
||||||
a,
|
std::iter::once((handle, mesh, set.instance_transform)),
|
||||||
b,
|
view_rot,
|
||||||
crossing,
|
eye,
|
||||||
std::iter::once((handle, mesh, set.instance_transform)),
|
bounds,
|
||||||
view_rot,
|
)
|
||||||
eye,
|
.is_empty()
|
||||||
bounds,
|
|
||||||
)
|
|
||||||
.is_empty()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
{
|
};
|
||||||
|
let mut sets = indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&index| meshes.get(index as usize));
|
||||||
|
let hit = if crossing {
|
||||||
|
sets.any(test)
|
||||||
|
} else {
|
||||||
|
let mut count = 0;
|
||||||
|
sets.all(|set| {
|
||||||
|
count += 1;
|
||||||
|
test(set)
|
||||||
|
}) && count > 0
|
||||||
|
};
|
||||||
|
if hit {
|
||||||
out.push(handle);
|
out.push(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8510,23 +8519,32 @@ impl Scene {
|
|||||||
let Some(indices) = lookup.get(&handle) else {
|
let Some(indices) = lookup.get(&handle) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if indices
|
let test = |set: &MeshLodSet| {
|
||||||
.iter()
|
set.geometry_lods().first().is_some_and(|mesh| {
|
||||||
.filter_map(|&index| meshes.get(index as usize))
|
!pick::hit_test::mesh_poly_hit(
|
||||||
.any(|set| {
|
poly,
|
||||||
set.geometry_lods().first().is_some_and(|mesh| {
|
crossing,
|
||||||
!pick::hit_test::mesh_poly_hit(
|
std::iter::once((handle, mesh, set.instance_transform)),
|
||||||
poly,
|
view_rot,
|
||||||
crossing,
|
eye,
|
||||||
std::iter::once((handle, mesh, set.instance_transform)),
|
bounds,
|
||||||
view_rot,
|
)
|
||||||
eye,
|
.is_empty()
|
||||||
bounds,
|
|
||||||
)
|
|
||||||
.is_empty()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
{
|
};
|
||||||
|
let mut sets = indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&index| meshes.get(index as usize));
|
||||||
|
let hit = if crossing {
|
||||||
|
sets.any(test)
|
||||||
|
} else {
|
||||||
|
let mut count = 0;
|
||||||
|
sets.all(|set| {
|
||||||
|
count += 1;
|
||||||
|
test(set)
|
||||||
|
}) && count > 0
|
||||||
|
};
|
||||||
|
if hit {
|
||||||
out.push(handle);
|
out.push(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8768,23 +8786,32 @@ impl Scene {
|
|||||||
let Some(indices) = lookup.get(&handle) else {
|
let Some(indices) = lookup.get(&handle) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let hit = indices
|
let test = |set: &MeshLodSet| {
|
||||||
|
set.geometry_lods().first().is_some_and(|mesh| {
|
||||||
|
!pick::hit_test::mesh_box_hit(
|
||||||
|
a,
|
||||||
|
b,
|
||||||
|
crossing,
|
||||||
|
std::iter::once((handle, mesh, set.instance_transform)),
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
.is_empty()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let mut sets = indices
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&index| meshes.get(index as usize))
|
.filter_map(|&index| meshes.get(index as usize));
|
||||||
.any(|set| {
|
let hit = if crossing {
|
||||||
set.geometry_lods().first().is_some_and(|mesh| {
|
sets.any(test)
|
||||||
!pick::hit_test::mesh_box_hit(
|
} else {
|
||||||
a,
|
let mut count = 0;
|
||||||
b,
|
sets.all(|set| {
|
||||||
crossing,
|
count += 1;
|
||||||
std::iter::once((handle, mesh, set.instance_transform)),
|
test(set)
|
||||||
view_rot,
|
}) && count > 0
|
||||||
eye,
|
};
|
||||||
bounds,
|
|
||||||
)
|
|
||||||
.is_empty()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if hit {
|
if hit {
|
||||||
out.push(handle);
|
out.push(handle);
|
||||||
}
|
}
|
||||||
@@ -8819,22 +8846,31 @@ impl Scene {
|
|||||||
let Some(indices) = lookup.get(&handle) else {
|
let Some(indices) = lookup.get(&handle) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let hit = indices
|
let test = |set: &MeshLodSet| {
|
||||||
|
set.geometry_lods().first().is_some_and(|mesh| {
|
||||||
|
!pick::hit_test::mesh_poly_hit(
|
||||||
|
poly,
|
||||||
|
crossing,
|
||||||
|
std::iter::once((handle, mesh, set.instance_transform)),
|
||||||
|
view_rot,
|
||||||
|
eye,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
.is_empty()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let mut sets = indices
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|&index| meshes.get(index as usize))
|
.filter_map(|&index| meshes.get(index as usize));
|
||||||
.any(|set| {
|
let hit = if crossing {
|
||||||
set.geometry_lods().first().is_some_and(|mesh| {
|
sets.any(test)
|
||||||
!pick::hit_test::mesh_poly_hit(
|
} else {
|
||||||
poly,
|
let mut count = 0;
|
||||||
crossing,
|
sets.all(|set| {
|
||||||
std::iter::once((handle, mesh, set.instance_transform)),
|
count += 1;
|
||||||
view_rot,
|
test(set)
|
||||||
eye,
|
}) && count > 0
|
||||||
bounds,
|
};
|
||||||
)
|
|
||||||
.is_empty()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if hit {
|
if hit {
|
||||||
out.push(handle);
|
out.push(handle);
|
||||||
}
|
}
|
||||||
|
|||||||
+287
-87
@@ -1615,10 +1615,10 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Q: lazy projection — accumulate screen points without allocating per-wire Vec.
|
if crossing {
|
||||||
wires
|
let mut out = Vec::new();
|
||||||
.iter()
|
let mut seen = HashSet::default();
|
||||||
.filter_map(|wire| {
|
for wire in wires.iter() {
|
||||||
// Fallback: when wire has no line geometry (e.g. greek text emits
|
// Fallback: when wire has no line geometry (e.g. greek text emits
|
||||||
// only fill_tris) treat the AABB rectangle as the hit-test shape
|
// only fill_tris) treat the AABB rectangle as the hit-test shape
|
||||||
// so low-LOD text stays selectable. See #19.
|
// so low-LOD text stays selectable. See #19.
|
||||||
@@ -1639,7 +1639,7 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
|||||||
];
|
];
|
||||||
&aabb_pts
|
&aabb_pts
|
||||||
} else {
|
} else {
|
||||||
return None;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Low residual parallel to `pts` (empty for the AABB fallback,
|
// Low residual parallel to `pts` (empty for the AABB fallback,
|
||||||
@@ -1650,6 +1650,85 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
|||||||
&[]
|
&[]
|
||||||
};
|
};
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
|
let mut prev: Option<Point> = None;
|
||||||
|
|
||||||
|
for (i, &[px, py, pz]) in pts.iter().enumerate() {
|
||||||
|
if px.is_nan() {
|
||||||
|
prev = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let world = if !wire.points.is_empty() {
|
||||||
|
wire_point_world(wire, i, view_rot, eye)
|
||||||
|
} else {
|
||||||
|
wp64([px, py, pz], low, i)
|
||||||
|
};
|
||||||
|
let sp = world_to_screen(world, view_rot, eye, bounds);
|
||||||
|
if inside(sp) {
|
||||||
|
hit = true;
|
||||||
|
}
|
||||||
|
if let Some(p0) = prev {
|
||||||
|
if !hit {
|
||||||
|
hit = segments_intersect(p0, sp, box_tl, box_tr)
|
||||||
|
|| segments_intersect(p0, sp, box_tr, box_br)
|
||||||
|
|| segments_intersect(p0, sp, box_br, box_bl)
|
||||||
|
|| segments_intersect(p0, sp, box_bl, box_tl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev = Some(sp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut glyph_crosses = false;
|
||||||
|
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||||
|
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if [0usize, 3].into_iter().any(|offset| {
|
||||||
|
triangle_crosses_box(
|
||||||
|
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
||||||
|
box_corners,
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
glyph_crosses = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit || glyph_crosses) && seen.insert(wire.name.as_str()) {
|
||||||
|
out.push(wire.name.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
} else {
|
||||||
|
let mut qualified = Vec::new();
|
||||||
|
let mut disqualified = HashSet::default();
|
||||||
|
let mut seen = HashSet::default();
|
||||||
|
|
||||||
|
for wire in wires.iter() {
|
||||||
|
let aabb_pts: Vec<[f32; 3]>;
|
||||||
|
let empty_pts: [[f32; 3]; 0] = [];
|
||||||
|
let pts: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||||
|
&wire.points
|
||||||
|
} else if !wire.text_verts.is_empty() {
|
||||||
|
&empty_pts
|
||||||
|
} else if wire.aabb != WireModel::UNBOUNDED_AABB {
|
||||||
|
let [ax, ay, bx, by] = wire.aabb;
|
||||||
|
aabb_pts = vec![
|
||||||
|
[ax, ay, 0.0],
|
||||||
|
[bx, ay, 0.0],
|
||||||
|
[bx, by, 0.0],
|
||||||
|
[ax, by, 0.0],
|
||||||
|
[ax, ay, 0.0],
|
||||||
|
];
|
||||||
|
&aabb_pts
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||||
|
&wire.points_low
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
let mut all_inside = true;
|
let mut all_inside = true;
|
||||||
let mut prev: Option<Point> = None;
|
let mut prev: Option<Point> = None;
|
||||||
|
|
||||||
@@ -1664,61 +1743,42 @@ pub fn box_hit<'a, W: WireSource + ?Sized>(
|
|||||||
wp64([px, py, pz], low, i)
|
wp64([px, py, pz], low, i)
|
||||||
};
|
};
|
||||||
let sp = world_to_screen(world, view_rot, eye, bounds);
|
let sp = world_to_screen(world, view_rot, eye, bounds);
|
||||||
if crossing {
|
if !inside(sp) {
|
||||||
if inside(sp) {
|
all_inside = false;
|
||||||
hit = true;
|
|
||||||
}
|
|
||||||
if let Some(p0) = prev {
|
|
||||||
if !hit {
|
|
||||||
hit = segments_intersect(p0, sp, box_tl, box_tr)
|
|
||||||
|| segments_intersect(p0, sp, box_tr, box_br)
|
|
||||||
|| segments_intersect(p0, sp, box_br, box_bl)
|
|
||||||
|| segments_intersect(p0, sp, box_bl, box_tl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !inside(sp) {
|
|
||||||
all_inside = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
prev = Some(sp);
|
prev = Some(sp);
|
||||||
}
|
}
|
||||||
|
|
||||||
let glyphs_present = !wire.text_verts.is_empty();
|
let glyphs_present = !wire.text_verts.is_empty();
|
||||||
let mut glyph_crosses = false;
|
|
||||||
let mut glyphs_inside = true;
|
let mut glyphs_inside = true;
|
||||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||||
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if crossing {
|
if !screen.iter().copied().all(inside) {
|
||||||
if [0usize, 3].into_iter().any(|offset| {
|
|
||||||
triangle_crosses_box(
|
|
||||||
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
|
||||||
box_corners,
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
glyph_crosses = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if !screen.iter().copied().all(inside) {
|
|
||||||
glyphs_inside = false;
|
glyphs_inside = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = if crossing {
|
let has_geom = prev.is_some() || glyphs_present;
|
||||||
hit || glyph_crosses
|
if !has_geom {
|
||||||
} else {
|
continue;
|
||||||
all_inside && glyphs_inside && (prev.is_some() || glyphs_present)
|
|
||||||
};
|
|
||||||
if result {
|
|
||||||
Some(wire.name.as_str())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.collect()
|
let name = wire.name.as_str();
|
||||||
|
if all_inside && glyphs_inside {
|
||||||
|
if seen.insert(name) {
|
||||||
|
qualified.push(name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disqualified.insert(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qualified.retain(|name| !disqualified.contains(name));
|
||||||
|
qualified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Polygon / lasso selection ─────────────────────────────────────────────
|
// ── Polygon / lasso selection ─────────────────────────────────────────────
|
||||||
@@ -1743,10 +1803,10 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
|||||||
return indexed_polygon_crossing_hits(wires, poly, view_rot, eye, bounds);
|
return indexed_polygon_crossing_hits(wires, poly, view_rot, eye, bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Q: lazy projection — no Vec allocation per wire.
|
if crossing {
|
||||||
wires
|
let mut out = Vec::new();
|
||||||
.iter()
|
let mut seen = HashSet::default();
|
||||||
.filter_map(|wire| {
|
for wire in wires.iter() {
|
||||||
// Same AABB fallback as `box_hit`: when a wire has no line
|
// Same AABB fallback as `box_hit`: when a wire has no line
|
||||||
// geometry (e.g. greek-LOD text emits only fill_tris) treat the
|
// geometry (e.g. greek-LOD text emits only fill_tris) treat the
|
||||||
// AABB rectangle as the hit-test shape so low-LOD text stays
|
// AABB rectangle as the hit-test shape so low-LOD text stays
|
||||||
@@ -1768,7 +1828,7 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
|||||||
];
|
];
|
||||||
&aabb_pts
|
&aabb_pts
|
||||||
} else {
|
} else {
|
||||||
return None;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||||
@@ -1777,6 +1837,88 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
|||||||
&[]
|
&[]
|
||||||
};
|
};
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
|
let mut prev: Option<Point> = None;
|
||||||
|
|
||||||
|
for (i, &[px, py, pz]) in pts.iter().enumerate() {
|
||||||
|
if px.is_nan() {
|
||||||
|
prev = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let world = if !wire.points.is_empty() {
|
||||||
|
wire_point_world(wire, i, view_rot, eye)
|
||||||
|
} else {
|
||||||
|
wp64([px, py, pz], low, i)
|
||||||
|
};
|
||||||
|
let sp = world_to_screen(world, view_rot, eye, bounds);
|
||||||
|
if sp.x < 0.0 || sp.x > bounds.width || sp.y < 0.0 || sp.y > bounds.height {
|
||||||
|
prev = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if point_in_polygon(sp, poly) {
|
||||||
|
hit = true;
|
||||||
|
}
|
||||||
|
if !hit {
|
||||||
|
if let Some(p0) = prev {
|
||||||
|
if segment_crosses_polygon(p0, sp, poly) {
|
||||||
|
hit = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev = Some(sp);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut glyph_crosses = false;
|
||||||
|
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||||
|
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if [0usize, 3].into_iter().any(|offset| {
|
||||||
|
triangle_crosses_polygon(
|
||||||
|
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
||||||
|
poly,
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
glyph_crosses = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hit || glyph_crosses) && seen.insert(wire.name.as_str()) {
|
||||||
|
out.push(wire.name.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
} else {
|
||||||
|
let mut qualified = Vec::new();
|
||||||
|
let mut disqualified = HashSet::default();
|
||||||
|
let mut seen = HashSet::default();
|
||||||
|
|
||||||
|
for wire in wires.iter() {
|
||||||
|
let aabb_pts: Vec<[f32; 3]>;
|
||||||
|
let empty_pts: [[f32; 3]; 0] = [];
|
||||||
|
let pts: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||||
|
&wire.points
|
||||||
|
} else if !wire.text_verts.is_empty() {
|
||||||
|
&empty_pts
|
||||||
|
} else if wire.aabb != WireModel::UNBOUNDED_AABB {
|
||||||
|
let [ax, ay, bx, by] = wire.aabb;
|
||||||
|
aabb_pts = vec![
|
||||||
|
[ax, ay, 0.0],
|
||||||
|
[bx, ay, 0.0],
|
||||||
|
[bx, by, 0.0],
|
||||||
|
[ax, by, 0.0],
|
||||||
|
[ax, ay, 0.0],
|
||||||
|
];
|
||||||
|
&aabb_pts
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||||
|
&wire.points_low
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
let mut all_inside = true;
|
let mut all_inside = true;
|
||||||
let mut prev: Option<Point> = None;
|
let mut prev: Option<Point> = None;
|
||||||
|
|
||||||
@@ -1798,43 +1940,19 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
|||||||
prev = None;
|
prev = None;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if crossing {
|
if !point_in_polygon(sp, poly) {
|
||||||
if point_in_polygon(sp, poly) {
|
all_inside = false;
|
||||||
hit = true;
|
|
||||||
}
|
|
||||||
if !hit {
|
|
||||||
if let Some(p0) = prev {
|
|
||||||
if segment_crosses_polygon(p0, sp, poly) {
|
|
||||||
hit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !point_in_polygon(sp, poly) {
|
|
||||||
all_inside = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
prev = Some(sp);
|
prev = Some(sp);
|
||||||
}
|
}
|
||||||
|
|
||||||
let glyphs_present = !wire.text_verts.is_empty();
|
let glyphs_present = !wire.text_verts.is_empty();
|
||||||
let mut glyph_crosses = false;
|
|
||||||
let mut glyphs_inside = true;
|
let mut glyphs_inside = true;
|
||||||
for start in (0..wire.text_verts.len()).step_by(6) {
|
for start in (0..wire.text_verts.len()).step_by(6) {
|
||||||
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
let Some(screen) = projected_text_quad(wire, start, view_rot, eye, bounds) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if crossing {
|
if !screen.iter().copied().all(|point| {
|
||||||
if [0usize, 3].into_iter().any(|offset| {
|
|
||||||
triangle_crosses_polygon(
|
|
||||||
[screen[offset], screen[offset + 1], screen[offset + 2]],
|
|
||||||
poly,
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
glyph_crosses = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if !screen.iter().copied().all(|point| {
|
|
||||||
point.x >= 0.0
|
point.x >= 0.0
|
||||||
&& point.x <= bounds.width
|
&& point.x <= bounds.width
|
||||||
&& point.y >= 0.0
|
&& point.y >= 0.0
|
||||||
@@ -1846,18 +1964,24 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = if crossing {
|
let has_geom = prev.is_some() || glyphs_present;
|
||||||
hit || glyph_crosses
|
if !has_geom {
|
||||||
} else {
|
continue;
|
||||||
all_inside && glyphs_inside && (prev.is_some() || glyphs_present)
|
|
||||||
};
|
|
||||||
if result {
|
|
||||||
Some(wire.name.as_str())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.collect()
|
let name = wire.name.as_str();
|
||||||
|
if all_inside && glyphs_inside {
|
||||||
|
if seen.insert(name) {
|
||||||
|
qualified.push(name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disqualified.insert(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qualified.retain(|name| !disqualified.contains(name));
|
||||||
|
qualified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
@@ -2511,4 +2635,80 @@ mod aabb_reject_tests {
|
|||||||
vec!["479"],
|
vec!["479"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_selection_multi_wire_entity_behavior() {
|
||||||
|
let bounds = Rectangle {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
width: 200.0,
|
||||||
|
height: 200.0,
|
||||||
|
};
|
||||||
|
// In identity ortho view:
|
||||||
|
// world (0.0, 0.0) -> screen (100.0, 100.0)
|
||||||
|
// world (0.1, 0.1) -> screen (110.0, 90.0)
|
||||||
|
// world (0.8, 0.8) -> screen (180.0, 20.0)
|
||||||
|
let wire_arc = wire(
|
||||||
|
"4F",
|
||||||
|
vec![[0.0, 0.0, 0.0], [0.1, 0.1, 0.0]],
|
||||||
|
[0.0, 0.0, 0.1, 0.1],
|
||||||
|
);
|
||||||
|
let wire_lines = wire(
|
||||||
|
"4F",
|
||||||
|
vec![[0.1, 0.1, 0.0], [0.8, 0.8, 0.0]],
|
||||||
|
[0.1, 0.1, 0.8, 0.8],
|
||||||
|
);
|
||||||
|
let wires = [wire_arc, wire_lines];
|
||||||
|
|
||||||
|
// Box enclosing ONLY wire_arc (screen 95..115, 85..105):
|
||||||
|
let box_a = Point::new(95.0, 85.0);
|
||||||
|
let box_b = Point::new(115.0, 105.0);
|
||||||
|
let poly_small = [
|
||||||
|
Point::new(95.0, 85.0),
|
||||||
|
Point::new(115.0, 85.0),
|
||||||
|
Point::new(115.0, 105.0),
|
||||||
|
Point::new(95.0, 105.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Crossing mode selects "4F" because wire_arc is inside:
|
||||||
|
assert_eq!(
|
||||||
|
box_hit(box_a, box_b, true, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||||
|
vec!["4F"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
poly_hit(&poly_small, true, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||||
|
vec!["4F"]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Window mode MUST NOT select "4F" because wire_lines has a vertex at (0.8, 0.8) -> (180.0, 20.0) outside:
|
||||||
|
assert_eq!(
|
||||||
|
box_hit(box_a, box_b, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||||
|
Vec::<&str>::new()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
poly_hit(&poly_small, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||||
|
Vec::<&str>::new()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Big box enclosing BOTH wire_arc and wire_lines (screen 50..190, 10..110):
|
||||||
|
let big_a = Point::new(50.0, 10.0);
|
||||||
|
let big_b = Point::new(190.0, 110.0);
|
||||||
|
let poly_big = [
|
||||||
|
Point::new(50.0, 10.0),
|
||||||
|
Point::new(190.0, 10.0),
|
||||||
|
Point::new(190.0, 110.0),
|
||||||
|
Point::new(50.0, 110.0),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Window mode now selects "4F" once (deduplicated):
|
||||||
|
assert_eq!(
|
||||||
|
box_hit(big_a, big_b, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||||
|
vec!["4F"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
poly_hit(&poly_big, false, &wires, Mat4::IDENTITY, glam::DVec3::ZERO, bounds),
|
||||||
|
vec!["4F"]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user