forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
0f252e8b53
commit
c815be9c6f
3 files changed
+174
-20
No files matched your search
+14
-4
@@ -1998,7 +1998,7 @@ impl H7CAD {
|
||||
a,
|
||||
p,
|
||||
crossing,
|
||||
&self.tabs[i].scene.hatches,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
vp_mat,
|
||||
bounds,
|
||||
));
|
||||
@@ -2031,7 +2031,7 @@ impl H7CAD {
|
||||
handles.extend(scene::hit_test::poly_hit_hatch(
|
||||
&poly_pts,
|
||||
crossing,
|
||||
&self.tabs[i].scene.hatches,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
vp_mat,
|
||||
bounds,
|
||||
));
|
||||
@@ -2058,7 +2058,17 @@ impl H7CAD {
|
||||
.or_else(|| {
|
||||
scene::hit_test::click_hit_hatch(
|
||||
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,
|
||||
bounds,
|
||||
)
|
||||
@@ -2096,7 +2106,7 @@ impl H7CAD {
|
||||
a,
|
||||
p,
|
||||
crossing,
|
||||
&self.tabs[i].scene.hatches,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
vp_mat,
|
||||
bounds,
|
||||
));
|
||||
|
||||
+87
-16
@@ -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?
|
||||
///
|
||||
/// 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 {
|
||||
let n = poly.len();
|
||||
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 {
|
||||
let (xj, yj) = (pt.x, pt.y);
|
||||
if ((yi > p.y) != (yj > p.y)) && (p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi) {
|
||||
if !pt.x.is_finite() || !pt.y.is_finite() {
|
||||
// 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;
|
||||
}
|
||||
(xi, yi) = (xj, yj);
|
||||
}
|
||||
inside
|
||||
}
|
||||
@@ -277,23 +312,59 @@ pub fn click_hit_hatch(
|
||||
bounds: Rectangle,
|
||||
) -> Option<Handle> {
|
||||
for (&handle, hatch) in hatches {
|
||||
// boundary verts are stored as small f32 offsets from
|
||||
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
||||
// projecting to screen.
|
||||
let ox = hatch.world_origin[0] as f32;
|
||||
let oy = hatch.world_origin[1] as f32;
|
||||
let screen: Vec<Point> = hatch
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds))
|
||||
.collect();
|
||||
if screen.len() >= 3 && point_in_polygon(cursor, &screen) {
|
||||
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
|
||||
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
||||
// projecting to screen.
|
||||
let ox = hatch.world_origin[0] as f32;
|
||||
let oy = hatch.world_origin[1] as f32;
|
||||
let screen: Vec<Point> = hatch
|
||||
.boundary
|
||||
.iter()
|
||||
.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();
|
||||
screen.len() >= 3 && point_in_polygon(cursor, &screen)
|
||||
}
|
||||
|
||||
/// Return Handles of hatches selected by a completed rectangular selection box.
|
||||
pub fn box_hit_hatch(
|
||||
corner_a: Point,
|
||||
|
||||
@@ -1103,6 +1103,79 @@ impl Scene {
|
||||
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.
|
||||
///
|
||||
/// - Model layout: all entity wires (same as entity_wires).
|
||||
|
||||
Reference in new issue
Block a user