fix(hit_test): correct hatch selection — NaN-aware ray-cast + block-insert resolve
Two related click bugs: 1. `point_in_polygon` treated NaN points (the path separators used by multi-path hatches with islands / holes) as regular vertices, producing spurious closing edges between sub-paths. The cursor inside a real hatch failed the test → the click iterator moved on to the NEXT hatch in HashMap order and silently returned the wrong one. Rewrite the ray-cast to reset the previous-vertex / path-start state at every NaN, so each sub-path closes against its own first vertex and contributes its parity flip correctly. 2. Clicking on a hatch *inside* a block was either missing entirely (after the visible_hatches_for_click filter) or returning the block-defn source hatch handle (misleading — that handle isn't the thing the user sees). AutoCAD selects the parent Insert when a sub-entity of a block is clicked. Add `Scene::insert_hatches_for_click` which walks the same explode path `synced_hatch_models` uses and tags every exploded sub-hatch with its parent Insert handle, plus `hit_test::click_hit_insert_hatch` which iterates that list. The click resolver now tries wire → MSPACE hatch → insert-internal hatch in order, so clicking on a block-internal hatch selects the Insert. Box / lasso select paths are unchanged — wire hit-test already picks up Inserts, and dragging a box over block-internal hatches is rare enough not to need its own resolution path yet. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0f252e8b53
commit
c815be9c6f
3 changed files with 174 additions and 20 deletions
|
|
@ -1998,7 +1998,7 @@ impl H7CAD {
|
||||||
a,
|
a,
|
||||||
p,
|
p,
|
||||||
crossing,
|
crossing,
|
||||||
&self.tabs[i].scene.hatches,
|
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
));
|
));
|
||||||
|
|
@ -2031,7 +2031,7 @@ impl H7CAD {
|
||||||
handles.extend(scene::hit_test::poly_hit_hatch(
|
handles.extend(scene::hit_test::poly_hit_hatch(
|
||||||
&poly_pts,
|
&poly_pts,
|
||||||
crossing,
|
crossing,
|
||||||
&self.tabs[i].scene.hatches,
|
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
));
|
));
|
||||||
|
|
@ -2058,7 +2058,17 @@ impl H7CAD {
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
scene::hit_test::click_hit_hatch(
|
scene::hit_test::click_hit_hatch(
|
||||||
p,
|
p,
|
||||||
&self.tabs[i].scene.hatches,
|
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||||
|
vp_mat,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
// Block-internal hatch: resolve to the
|
||||||
|
// parent Insert (AutoCAD behaviour).
|
||||||
|
scene::hit_test::click_hit_insert_hatch(
|
||||||
|
p,
|
||||||
|
&self.tabs[i].scene.insert_hatches_for_click(),
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
)
|
)
|
||||||
|
|
@ -2096,7 +2106,7 @@ impl H7CAD {
|
||||||
a,
|
a,
|
||||||
p,
|
p,
|
||||||
crossing,
|
crossing,
|
||||||
&self.tabs[i].scene.hatches,
|
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
));
|
));
|
||||||
|
|
|
||||||
|
|
@ -222,16 +222,51 @@ fn world_to_screen(world: Vec3, view_proj: Mat4, bounds: Rectangle) -> Point {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Even-odd ray-casting test: is `p` inside the polygon?
|
/// Even-odd ray-casting test: is `p` inside the polygon?
|
||||||
|
///
|
||||||
|
/// Handles multi-path boundaries: NaN points (used as path separators by
|
||||||
|
/// hatches with islands / holes) reset the previous-vertex tracking so
|
||||||
|
/// that the ray-cast doesn't draw a spurious closing edge between the
|
||||||
|
/// end of one sub-path and the start of the next. Each sub-path with at
|
||||||
|
/// least 2 finite vertices contributes its segments to the parity flip.
|
||||||
fn point_in_polygon(p: Point, poly: &[Point]) -> bool {
|
fn point_in_polygon(p: Point, poly: &[Point]) -> bool {
|
||||||
let n = poly.len();
|
|
||||||
let mut inside = false;
|
let mut inside = false;
|
||||||
let (mut xi, mut yi) = (poly[n - 1].x, poly[n - 1].y);
|
let mut prev: Option<Point> = None;
|
||||||
|
let mut path_start: Option<Point> = None;
|
||||||
for &pt in poly {
|
for &pt in poly {
|
||||||
let (xj, yj) = (pt.x, pt.y);
|
if !pt.x.is_finite() || !pt.y.is_finite() {
|
||||||
if ((yi > p.y) != (yj > p.y)) && (p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi) {
|
// Close the just-finished sub-path before starting the next.
|
||||||
|
if let (Some(prev_v), Some(start_v)) = (prev, path_start) {
|
||||||
|
if ((prev_v.y > p.y) != (start_v.y > p.y))
|
||||||
|
&& (p.x
|
||||||
|
< (start_v.x - prev_v.x) * (p.y - prev_v.y) / (start_v.y - prev_v.y)
|
||||||
|
+ prev_v.x)
|
||||||
|
{
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev = None;
|
||||||
|
path_start = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(prev_v) = prev {
|
||||||
|
if ((prev_v.y > p.y) != (pt.y > p.y))
|
||||||
|
&& (p.x < (pt.x - prev_v.x) * (p.y - prev_v.y) / (pt.y - prev_v.y) + prev_v.x)
|
||||||
|
{
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
path_start = Some(pt);
|
||||||
|
}
|
||||||
|
prev = Some(pt);
|
||||||
|
}
|
||||||
|
// Close the final sub-path.
|
||||||
|
if let (Some(prev_v), Some(start_v)) = (prev, path_start) {
|
||||||
|
if ((prev_v.y > p.y) != (start_v.y > p.y))
|
||||||
|
&& (p.x
|
||||||
|
< (start_v.x - prev_v.x) * (p.y - prev_v.y) / (start_v.y - prev_v.y) + prev_v.x)
|
||||||
|
{
|
||||||
inside = !inside;
|
inside = !inside;
|
||||||
}
|
}
|
||||||
(xi, yi) = (xj, yj);
|
|
||||||
}
|
}
|
||||||
inside
|
inside
|
||||||
}
|
}
|
||||||
|
|
@ -277,6 +312,38 @@ pub fn click_hit_hatch(
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Option<Handle> {
|
) -> Option<Handle> {
|
||||||
for (&handle, hatch) in hatches {
|
for (&handle, hatch) in hatches {
|
||||||
|
if hatch_contains_screen_point(hatch, cursor, view_proj, bounds) {
|
||||||
|
return Some(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as `click_hit_hatch` but iterates `(Handle, HatchModel)` pairs
|
||||||
|
/// where the Handle is the parent Insert handle (block-internal
|
||||||
|
/// hatches). The first matching pair returns its Insert handle so
|
||||||
|
/// clicking a sub-hatch of a block selects the Insert, matching
|
||||||
|
/// AutoCAD's behaviour for block sub-entities.
|
||||||
|
pub fn click_hit_insert_hatch(
|
||||||
|
cursor: Point,
|
||||||
|
insert_hatches: &[(Handle, HatchModel)],
|
||||||
|
view_proj: Mat4,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> Option<Handle> {
|
||||||
|
for (handle, hatch) in insert_hatches {
|
||||||
|
if hatch_contains_screen_point(hatch, cursor, view_proj, bounds) {
|
||||||
|
return Some(*handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hatch_contains_screen_point(
|
||||||
|
hatch: &HatchModel,
|
||||||
|
cursor: Point,
|
||||||
|
view_proj: Mat4,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> bool {
|
||||||
// boundary verts are stored as small f32 offsets from
|
// boundary verts are stored as small f32 offsets from
|
||||||
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
||||||
// projecting to screen.
|
// projecting to screen.
|
||||||
|
|
@ -285,13 +352,17 @@ pub fn click_hit_hatch(
|
||||||
let screen: Vec<Point> = hatch
|
let screen: Vec<Point> = hatch
|
||||||
.boundary
|
.boundary
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds))
|
.map(|&[x, y]| {
|
||||||
|
if x.is_finite() && y.is_finite() {
|
||||||
|
world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds)
|
||||||
|
} else {
|
||||||
|
// Preserve path separators for the NaN-aware
|
||||||
|
// point_in_polygon ray-cast.
|
||||||
|
Point::new(f32::NAN, f32::NAN)
|
||||||
|
}
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if screen.len() >= 3 && point_in_polygon(cursor, &screen) {
|
screen.len() >= 3 && point_in_polygon(cursor, &screen)
|
||||||
return Some(handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return Handles of hatches selected by a completed rectangular selection box.
|
/// Return Handles of hatches selected by a completed rectangular selection box.
|
||||||
|
|
|
||||||
|
|
@ -1103,6 +1103,79 @@ impl Scene {
|
||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hatches eligible for click / box / lasso hit-testing in the current
|
||||||
|
/// layout. Filters out block-internal source hatches (stored in
|
||||||
|
/// `self.hatches` at block-local coords for the block-defn position,
|
||||||
|
/// which doesn't project correctly through the offset-rel view_proj
|
||||||
|
/// and was causing the wrong hatch to be selected on click).
|
||||||
|
pub fn visible_hatches_for_click(&self) -> HashMap<Handle, HatchModel> {
|
||||||
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
self.hatches
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(&h, m)| {
|
||||||
|
let owner = self.document.get_entity(h)?.common().owner_handle;
|
||||||
|
if self.belongs_to_visible_block(h, owner, layout_block) {
|
||||||
|
Some((h, m.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-Insert hatch models in the current layout, keyed by the Insert
|
||||||
|
/// handle so a click on a block-internal hatch can select the parent
|
||||||
|
/// Insert (AutoCAD behaviour: sub-entities of a block aren't directly
|
||||||
|
/// selectable; the click resolves to the Insert).
|
||||||
|
pub fn insert_hatches_for_click(&self) -> Vec<(Handle, HatchModel)> {
|
||||||
|
let layout_block = self.current_layout_block_handle();
|
||||||
|
let hatch_offset = if self.current_layout == "Model" {
|
||||||
|
self.world_offset
|
||||||
|
} else {
|
||||||
|
[0.0; 3]
|
||||||
|
};
|
||||||
|
let layer_hidden = |layer: &str| {
|
||||||
|
self.document
|
||||||
|
.layers
|
||||||
|
.get(layer)
|
||||||
|
.map(|l| l.flags.off || l.flags.frozen)
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
let mut out: Vec<(Handle, HatchModel)> = Vec::new();
|
||||||
|
for entity in self.document.entities() {
|
||||||
|
let EntityType::Insert(ins) = entity else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if ins.common.invisible || layer_hidden(&ins.common.layer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !self.belongs_to_visible_block(
|
||||||
|
ins.common.handle,
|
||||||
|
ins.common.owner_handle,
|
||||||
|
layout_block,
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for sub in ins
|
||||||
|
.explode_from_document(&self.document)
|
||||||
|
.into_iter()
|
||||||
|
.map(crate::modules::home::modify::explode::normalize_insert_entity)
|
||||||
|
{
|
||||||
|
let EntityType::Hatch(dxf) = sub else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if dxf.common.invisible || layer_hidden(&dxf.common.layer) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let color = self.render_style(&EntityType::Hatch(dxf.clone())).0;
|
||||||
|
if let Some(model) = Self::hatch_model_from_dxf(&dxf, color, hatch_offset) {
|
||||||
|
out.push((ins.common.handle, model));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Wires that should participate in hit-testing, snapping, and selection.
|
/// Wires that should participate in hit-testing, snapping, and selection.
|
||||||
///
|
///
|
||||||
/// - Model layout: all entity wires (same as entity_wires).
|
/// - Model layout: all entity wires (same as entity_wires).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue