diff --git a/src/app/update.rs b/src/app/update.rs index e4fa529ad..81a6e6236 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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, )); diff --git a/src/scene/hit_test.rs b/src/scene/hit_test.rs index 64f0212f2..34979bb29 100644 --- a/src/scene/hit_test.rs +++ b/src/scene/hit_test.rs @@ -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 = None; + let mut path_start: Option = 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 { 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 = 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 { + 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 = 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, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 55eb004b4..6457b766a 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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 { + 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).