diff --git a/src/scene/hit_test.rs b/src/scene/hit_test.rs index 2c6e1e05d..1bb10e64d 100644 --- a/src/scene/hit_test.rs +++ b/src/scene/hit_test.rs @@ -344,45 +344,48 @@ fn world_to_screen(world: Vec3, view_proj: Mat4, bounds: Rectangle) -> Point { /// 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 { + // Ray-cast crossing test for a single edge a→b. + fn cross(p: Point, a: Point, b: Point, inside: &mut bool) { + if (a.y > p.y) != (b.y > p.y) + && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x + { + *inside = !*inside; + } + } + let mut inside = false; let mut prev: Option = None; let mut path_start: Option = None; + // Vertices in the current sub-path. A boundary can be encoded either as a + // ring (`[v0,v1,v2,v3]`, needs an implicit closing edge) or as an explicit + // edge list (`[v0,v1, NaN, v1,v2, NaN, …]`, already closed). Only close a + // sub-path that is a real ring (≥3 verts); closing a 2-point explicit edge + // would add a degenerate back-edge that cancels its own crossing. + let mut count = 0usize; + let close = |prev: Option, path_start: Option, count: usize, inside: &mut bool| { + if count >= 3 { + if let (Some(pv), Some(sv)) = (prev, path_start) { + cross(p, pv, sv, inside); + } + } + }; for &pt in poly { 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; - } - } + close(prev, path_start, count, &mut inside); prev = None; path_start = None; + count = 0; 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; - } + cross(p, prev_v, pt, &mut inside); } else { path_start = Some(pt); } prev = Some(pt); + count += 1; } - // 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; - } - } + close(prev, path_start, count, &mut inside); inside } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 50c20c2d2..16327b9f1 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -662,7 +662,10 @@ pub struct Scene { /// is handled at draw time via `hatch_skip_flags` in the pipeline, /// not at build time — that lets the GPU buffer stay stable across /// pan/zoom while still skipping out-of-view hatches. - hatch_cache: RefCell>)>>, + /// Keyed by `(geometry_epoch, selection_generation)` — selected hatches + /// are tinted, so a select/deselect must rebuild even when the geometry + /// is unchanged (issue #71). + hatch_cache: RefCell>)>>, /// Cached wipeout fill models, keyed by geometry_epoch. Same /// reasoning as `hatch_cache`. wipeout_cache: RefCell>)>>, @@ -1874,14 +1877,17 @@ impl Scene { pub(super) fn hatch_models_arc(&self) -> Arc> { { let cache = self.hatch_cache.borrow(); - if let Some((cached_epoch, ref arc)) = *cache { - if cached_epoch == self.geometry_epoch { + if let Some((cached_epoch, cached_sel, ref arc)) = *cache { + if cached_epoch == self.geometry_epoch + && cached_sel == self.selection_generation + { return Arc::clone(arc); } } } let arc = Arc::new(self.synced_hatch_models()); - *self.hatch_cache.borrow_mut() = Some((self.geometry_epoch, Arc::clone(&arc))); + *self.hatch_cache.borrow_mut() = + Some((self.geometry_epoch, self.selection_generation, Arc::clone(&arc))); arc } @@ -1969,11 +1975,29 @@ impl Scene { /// 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(); + let model_block = self.model_space_block_handle(); + let layer_hidden = |layer: &str| { + self.document + .layers + .get(layer) + .map(|l| l.flags.off || l.flags.frozen) + .unwrap_or(false) + }; 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) { + let c = self.document.get_entity(h)?.common(); + if c.invisible || layer_hidden(&c.layer) { + return None; + } + // Mirror `synced_hatch_models`' visibility test (which drives + // the fill render) so anything drawn is also clickable on its + // fill, not just its boundary wire. The model-space fallback + // matters when the layout block handle differs from the + // entity's owner (issue: hatch fill not selectable). + if self.belongs_to_visible_block(h, c.owner_handle, layout_block) + || self.belongs_to_visible_block(h, c.owner_handle, model_block) + { Some((h, m.clone())) } else { None @@ -4326,7 +4350,17 @@ impl Scene { model.pattern, crate::scene::hatch_model::HatchPattern::Solid ); - let [wx, wy] = model.world_origin; + // The boundary points arrive in local render space (world_offset + // already subtracted). The stored DXF entity must hold WCS, so add the + // offset back — otherwise the boundary wire, re-projected through the + // normal entity path, lands `world_offset` away from the fill. + let off = if self.current_layout == "Model" { + self.world_offset + } else { + [0.0; 3] + }; + let wx = model.world_origin[0] + off[0]; + let wy = model.world_origin[1] + off[1]; let verts: Vec = model .boundary .iter() diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index cc7da472c..de6f6462a 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -119,7 +119,10 @@ pub struct Pipeline { /// Last `(geometry_epoch, camera_generation)` value for which GPU buffers /// were uploaded. We re-upload when either side changes — pan/zoom bumps /// camera_generation, which triggers re-culling and a fresh upload. - pub cached_epoch: (u64, u64), + /// `(geometry_epoch, camera_generation, selection_generation)` of the + /// last static-buffer upload. Selection is tracked so the selected-hatch + /// tint refreshes on select / deselect (issue #71). + pub cached_epoch: (u64, u64, u64), /// Content id of the wire buffer currently resident on the GPU (Phase /// 3.2). When the incoming `ViewportData.wire_content_id` matches, the /// world-space wire vertices are unchanged (e.g. a pure pan reused the @@ -899,7 +902,7 @@ impl Pipeline { gpu_face3d_fill: None, gpu_face3d_edges: vec![], viewcube, - cached_epoch: (u64::MAX, u64::MAX), + cached_epoch: (u64::MAX, u64::MAX, u64::MAX), cached_wire_id: u64::MAX, cached_selection: (u64::MAX, u64::MAX), } diff --git a/src/scene/render.rs b/src/scene/render.rs index 9d640f288..7c88a9862 100644 --- a/src/scene/render.rs +++ b/src/scene/render.rs @@ -221,7 +221,7 @@ impl shader::Primitive for Primitive { let (uo_y, us_y) = uv_crop_axis(sr.y, sr.height); inner.upload_blit_uv(queue, [uo_x, uo_y], [us_x, us_y]); inner.upload_uniforms(queue, &vp.uniforms); - let cur_key = (vp.geometry_epoch, vp.camera_generation); + let cur_key = (vp.geometry_epoch, vp.camera_generation, vp.selection_generation); let fill_mode = vp.fill_mode; // 3D face fill requires *both* the doc-level FILLMODE *and* the // per-view Solid toggle. Hatches / wipeouts deliberately ignore @@ -229,9 +229,12 @@ impl shader::Primitive for Primitive { // the Wireframe overlay style. let face3d_fill_active = fill_mode && !vp.view_wireframe; if cur_key != inner.cached_epoch { - // Static buffers (hatches/images/meshes) only need refresh on - // a real geometry change, not on every camera tick. - if vp.geometry_epoch != inner.cached_epoch.0 { + // Hatches carry a selected-tint, so re-upload on a geometry OR + // a selection change (issue #71); images / meshes only need a + // geometry change. + let geo_changed = vp.geometry_epoch != inner.cached_epoch.0; + let sel_changed = vp.selection_generation != inner.cached_epoch.2; + if geo_changed || sel_changed { if fill_mode { inner.upload_hatches(device, &vp.hatches[..]); inner.upload_wipeouts(device, &vp.wipeout_hatches[..]); @@ -239,6 +242,8 @@ impl shader::Primitive for Primitive { inner.upload_hatches(device, &[]); inner.upload_wipeouts(device, &[]); } + } + if geo_changed { inner.upload_images(device, queue, &vp.images[..]); inner.upload_meshes(device, &vp.meshes[..]); }