From 3f060a570ac2c22dc36d3bab2b81eb3130966df2 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 17 Jul 2026 14:58:55 +0300 Subject: [PATCH] fix(pick): hit-test lineweight width and thickness walls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Selection tested a flat 8 px radius around each wire's centreline and nothing else, so two things the user can see were not pickable. A wire renders as a band `line_weight_px` wide, so pick now widens to that rendered half-width via `pick_tolerance_px`, mirroring the wire shader's `select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for every standard weight (the widest, 2.11 mm, renders 7.97 px half-width — just under the 8 px threshold); this keeps the two sides from drifting apart if the display boost in `lineweight_to_px` ever changes. Entities extruded by a DXF thickness draw their wall as four edges with nothing in between, so the interior was a hole the cursor fell through. WireModel gains `pick_tris`: triangles hit-testing treats as solid and the renderer never uploads. Kept apart from `fill_tris` because that channel reaches the GPU, and a wall that rendered shaded would change how every thickness drawing looks. Ranked below `fill_tris` — those are drawn, these are not, so where they overlap the drawn face is what the user means. Walls are dropped when a wire is projected into a floating viewport: `points` are reprojected and clipped into paper coords there, so cloned model-space triangles would hit-test at a model-scale offset. Extruded entities stay selectable by their edges, which do get projected. The wall loop runs on every hover that misses everything else, so it rejects on the wire's AABB first — an extruded circle is ~128 triangles and a drawing full of thickness would otherwise project every wall on each mouse move. Co-Authored-By: Claude Opus 4.8 --- src/app/update/viewport.rs | 17 +- src/entities/arc.rs | 22 ++- src/entities/attribute.rs | 2 + src/entities/circle.rs | 18 +- src/entities/dimension.rs | 10 + src/entities/ellipse.rs | 2 + src/entities/leader.rs | 5 + src/entities/line.rs | 4 +- src/entities/lwpolyline.rs | 6 +- src/entities/mesh.rs | 4 + src/entities/mline.rs | 1 + src/entities/mtext.rs | 1 + src/entities/multileader.rs | 9 + src/entities/ole2frame.rs | 2 + src/entities/point.rs | 3 + src/entities/polyline.rs | 7 +- src/entities/raster_image.rs | 2 + src/entities/ray.rs | 2 + src/entities/shape.rs | 1 + src/entities/solid.rs | 1 + src/entities/spline.rs | 3 + src/entities/table.rs | 3 + src/entities/text.rs | 1 + src/entities/tolerance.rs | 1 + src/entities/underlay.rs | 2 + src/modules/annotate/aligned_dim.rs | 4 + src/modules/annotate/angular_dim.rs | 2 + src/modules/annotate/diameter_dim.rs | 2 + src/modules/annotate/dim_baseline.rs | 2 + src/modules/annotate/dim_continue.rs | 2 + src/modules/annotate/dimjogline.rs | 2 + src/modules/annotate/dimtedit.rs | 2 + src/modules/annotate/leader_cmd.rs | 2 + src/modules/annotate/linear_dim.rs | 2 + src/modules/annotate/mleader_cmd.rs | 2 + src/modules/annotate/mleader_edit.rs | 2 + src/modules/annotate/ordinate_dim.rs | 2 + src/modules/annotate/qdim.rs | 2 + src/modules/annotate/radius_dim.rs | 2 + src/modules/annotate/table_cmd.rs | 2 + src/modules/annotate/tolerance_cmd.rs | 2 + src/modules/draw/draw/attdef.rs | 2 + src/modules/draw/draw/mline.rs | 2 + src/modules/draw/draw/raster_image.rs | 2 + src/modules/draw/draw/ray.rs | 4 + src/modules/draw/draw/revcloud.rs | 2 + src/modules/draw/draw/wipeout.rs | 4 + src/modules/draw/inquiry/area.rs | 2 + src/modules/draw/inquiry/dist.rs | 2 + src/modules/draw/inquiry/measuregeom.rs | 2 + src/modules/draw/modify/trim.rs | 2 + src/modules/layout/mview.rs | 2 + src/modules/model/primitive_cmd.rs | 2 + src/modules/view/plot_window.rs | 2 + src/modules/view/zoom_window.rs | 2 + src/scene/cache/block_cache.rs | 40 +++- src/scene/convert/acad_to_truck.rs | 36 ++++ src/scene/convert/tess.rs | 6 + src/scene/convert/tessellate.rs | 39 ++++ src/scene/model/wire_model.rs | 17 ++ src/scene/pick/hit_test.rs | 252 ++++++++++++++++++------ src/scene/project.rs | 8 + src/scene/text/complex_lt.rs | 4 + 63 files changed, 514 insertions(+), 83 deletions(-) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 995def907..abc8737af 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1172,7 +1172,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { p } else if needs_entity { let hover_handle = - scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds) + scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds, self.tabs[i].scene.document.header.lineweight_display) .and_then(|s| Scene::handle_from_wire_name(s)) .unwrap_or(acadrust::Handle::NULL); let mut p = self.tabs[i] @@ -1235,6 +1235,8 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let far_pos = base + dir * far; let far_neg = base - dir * far; let guide = crate::scene::WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1271,6 +1273,8 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let far_pos = h.base + h.dir * far; let far_neg = h.base - h.dir * far; previews.push(crate::scene::WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -2017,7 +2021,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { .unwrap_or(false) { let (view_rot2, eye2, all_wires2) = self.pick_view(i, &edit_cam, bounds); - let hit = scene::pick::hit_test::click_hit(p, &all_wires2[..], view_rot2, eye2, bounds) + let hit = scene::pick::hit_test::click_hit(p, &all_wires2[..], view_rot2, eye2, bounds, self.tabs[i].scene.document.header.lineweight_display) .and_then(|s| Scene::handle_from_wire_name(s)); if let Some(handle) = hit { // Some commands (e.g. SS_CATCHMENT) need the entity @@ -2358,6 +2362,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { view_rot, eye, bounds, + self.tabs[i].scene.document.header.lineweight_display, ) .into_iter() .filter_map(|s| Scene::handle_from_wire_name(s)) @@ -2373,7 +2378,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { if !handled_by_cycling { let hit = - scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds) + scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds, self.tabs[i].scene.document.header.lineweight_display) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { scene::pick::hit_test::click_hit_hatch( @@ -2568,7 +2573,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { // Resolve the double-clicked object — its wire, or (for a // block/solid with no wire under the cursor) its shaded // body, which maps to the parent INSERT. - let hit = scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds) + let hit = scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds, self.tabs[i].scene.document.header.lineweight_display) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| self.tabs[i].scene.solid_click_hit(p, view_rot, eye, bounds)); if let Some(handle) = hit { @@ -2657,7 +2662,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { let wire_hit: Option = { let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye()) }; let all_wires = self.tabs[i].scene.hit_test_wires(); - scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds) + scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds, self.tabs[i].scene.document.header.lineweight_display) .and_then(|s| Scene::handle_from_wire_name(s)) .and_then(|h| { if let Some(AcadEntityType::Viewport(vp)) = @@ -2968,7 +2973,7 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { // Mirror the click-selection pick order so the rollover // highlights every selectable object: wire → hatch → // block-internal hatch → shaded 3D solid body. - let hovered = scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds) + let hovered = scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds, self.tabs[i].scene.document.header.lineweight_display) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { scene::pick::hit_test::click_hit_hatch( diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 44f9f73cb..c6f7c8511 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -6,7 +6,7 @@ use crate::entities::common::{ center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::{SnapHint, TangentGeom}; @@ -56,17 +56,17 @@ fn to_truck(arc: &Arc) -> TruckEntity { let n = 32usize; let ccw_end = if ea >= sa { ea } else { ea + TAU }; let (start_a, end_a) = (sa, ccw_end); + let base: Vec<[f64; 3]> = (0..=n) + .map(|i| { + let p = arc_pt(start_a + (end_a - start_a) * (i as f64 / n as f64)); + [p.x, p.y, p.z] + }) + .collect(); let mut pts: Vec<[f64; 3]> = Vec::with_capacity((n + 1) * 2 + 8); - for i in 0..=n { - let a = start_a + (end_a - start_a) * (i as f64 / n as f64); - let p = arc_pt(a); - pts.push([p.x, p.y, p.z]); - } + pts.extend_from_slice(&base); pts.push([f64::NAN; 3]); - for i in 0..=n { - let a = start_a + (end_a - start_a) * (i as f64 / n as f64); - let p = arc_pt(a); - pts.push([p.x + t * nx, p.y + t * ny, p.z + t * nz]); + for &[x, y, z] in &base { + pts.push([x + t * nx, y + t * ny, z + t * nz]); } pts.push([f64::NAN; 3]); let ps = arc_pt(sa); @@ -77,6 +77,7 @@ fn to_truck(arc: &Arc) -> TruckEntity { pts.push([pe.x, pe.y, pe.z]); pts.push([pe.x + t * nx, pe.y + t * ny, pe.z + t * nz]); return TruckEntity { + pick_tris: extrusion_wall_tris(&base, [t * nx, t * ny, t * nz]), object: TruckObject::Lines(pts), snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)], tangent_geoms: vec![tangent], @@ -92,6 +93,7 @@ fn to_truck(arc: &Arc) -> TruckEntity { let v_end = builder::vertex(p_end); let edge = builder::circle_arc(&v_start, &v_end, p_mid); TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Curve(edge), snap_pts: vec![(cv, SnapHint::Center), (mv, SnapHint::Midpoint)], tangent_geoms: vec![tangent], diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index a5862ff57..ab9846ad0 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -216,6 +216,7 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) let _ = input.line_count; let _ = input.is_multiline; return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Text(layout.strokes), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], @@ -313,6 +314,7 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) let _ = input.line_count; // round-trip only — recomputed above TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Text(strokes_all), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 766e0dfcf..7afa22f5f 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -6,7 +6,7 @@ use crate::entities::common::{ center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::{SnapHint, TangentGeom}; @@ -64,14 +64,16 @@ fn to_truck(circle: &Circle) -> TruckEntity { cwz + r * (c * ax.2 + s * ay.2), ) }; + let base: Vec<[f64; 3]> = (0..=n) + .map(|i| { + let (x, y, z) = circ_pt(i as f64 * tau / n as f64); + [x, y, z] + }) + .collect(); let mut pts: Vec<[f64; 3]> = Vec::with_capacity((n + 1) * 2 + 4 * 3); - for i in 0..=n { - let (x, y, z) = circ_pt(i as f64 * tau / n as f64); - pts.push([x, y, z]); - } + pts.extend_from_slice(&base); pts.push([f64::NAN; 3]); - for i in 0..=n { - let (x, y, z) = circ_pt(i as f64 * tau / n as f64); + for &[x, y, z] in &base { pts.push([x + t * nx, y + t * ny, z + t * nz]); } pts.push([f64::NAN; 3]); @@ -84,6 +86,7 @@ fn to_truck(circle: &Circle) -> TruckEntity { } } return TruckEntity { + pick_tris: extrusion_wall_tris(&base, [t * nx, t * ny, t * nz]), object: TruckObject::Lines(pts), snap_pts, tangent_geoms: vec![tangent], @@ -98,6 +101,7 @@ fn to_truck(circle: &Circle) -> TruckEntity { let lower = builder::circle_arc(&left, &right, p_bot); let wire: Wire = [upper, lower].into_iter().collect(); TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Contour(wire), snap_pts, tangent_geoms: vec![tangent], diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index b8f198766..52c8ce841 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1352,6 +1352,8 @@ fn tessellate_dimension_inner( let (ext1, ext2) = split_ext_lines(&geom.ext_lines); if !ext1.is_empty() { wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1376,6 +1378,8 @@ fn tessellate_dimension_inner( } if !ext2.is_empty() { wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1400,6 +1404,8 @@ fn tessellate_dimension_inner( } } else { wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1425,6 +1431,8 @@ fn tessellate_dimension_inner( } wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1466,6 +1474,8 @@ fn tessellate_dimension_inner( aci_to_rgba(&c) }; wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 2deb62715..43dba4627 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -96,6 +96,7 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { (q(0.0, -r_minor), SnapHint::Quadrant), ]; TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Contour(wire), snap_pts, tangent_geoms: vec![], @@ -128,6 +129,7 @@ fn to_truck(ell: &Ellipse) -> TruckEntity { [pt_end.x, pt_end.y, pt_end.z], ]; TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Curve(edge), snap_pts: vec![(center_v3, SnapHint::Center)], tangent_geoms: vec![], diff --git a/src/entities/leader.rs b/src/entities/leader.rs index f02c4a785..8dc007c3c 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -96,6 +96,7 @@ fn to_truck(leader: &Leader) -> TruckEntity { } TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(points), snap_pts: vec![], tangent_geoms: tangents, @@ -589,6 +590,8 @@ impl LeaderTess for Leader { if verts.len() < 2 { return WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -693,6 +696,8 @@ impl LeaderTess for Leader { } WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/entities/line.rs b/src/entities/line.rs index 5a6c7b1f7..015be6faf 100644 --- a/src/entities/line.rs +++ b/src/entities/line.rs @@ -6,7 +6,7 @@ use crate::entities::common::{ center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, }; use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; use crate::scene::model::wire_model::TangentGeom; @@ -45,6 +45,7 @@ fn to_truck(line: &Line) -> TruckEntity { p1t, ]; return TruckEntity { + pick_tris: extrusion_wall_tris(&kv, [t * nx, t * ny, t * nz]), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![tangent], @@ -57,6 +58,7 @@ fn to_truck(line: &Line) -> TruckEntity { let v1 = builder::vertex(p1); let edge = builder::line(&v0, &v1); TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Curve(edge), snap_pts: vec![], tangent_geoms: vec![tangent], diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index b56f1a055..1a30d12b6 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -7,7 +7,7 @@ use crate::entities::common::{ stepper_prop as stepper, }; use crate::entities::traits::TruckConvertible; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::TangentGeom; @@ -91,6 +91,7 @@ fn thick_segments( } } TruckEntity { + pick_tris: extrusion_wall_tris(path_pts, [t * nx, t * ny, t * nz]), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: tangents, @@ -103,6 +104,7 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { let verts = &pline.vertices; if verts.is_empty() { return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Point(builder::vertex(Point3::new(0.0, 0.0, 0.0))), snap_pts: vec![], tangent_geoms: vec![], @@ -221,6 +223,7 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { } } return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::SegmentedLines(pts), snap_pts: vec![], tangent_geoms: tgs, @@ -269,6 +272,7 @@ fn to_truck(pline: &LwPolyline) -> TruckEntity { } TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Contour(edges.into_iter().collect::()), snap_pts: vec![], tangent_geoms: tangents, diff --git a/src/entities/mesh.rs b/src/entities/mesh.rs index cd1a79f54..49d325077 100644 --- a/src/entities/mesh.rs +++ b/src/entities/mesh.rs @@ -195,6 +195,7 @@ impl TruckConvertible for Face3D { } Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![ (Vec3::from(p0f).as_dvec3(), SnapHint::Node), @@ -369,6 +370,7 @@ impl TruckConvertible for PolygonMesh { } Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], @@ -524,6 +526,7 @@ impl TruckConvertible for PolyfaceMesh { } Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], @@ -690,6 +693,7 @@ impl TruckConvertible for Mesh { let key_vertices: Vec<[f64; 3]> = self.vertices.iter().map(|v| [v.x, v.y, v.z]).collect(); Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts, tangent_geoms: vec![], diff --git a/src/entities/mline.rs b/src/entities/mline.rs index 08a79d93e..dca069980 100644 --- a/src/entities/mline.rs +++ b/src/entities/mline.rs @@ -162,6 +162,7 @@ impl TruckConvertible for MLine { .collect(); Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts, tangent_geoms: vec![], diff --git a/src/entities/mtext.rs b/src/entities/mtext.rs index 25c19d47e..64097b734 100644 --- a/src/entities/mtext.rs +++ b/src/entities/mtext.rs @@ -150,6 +150,7 @@ fn to_truck(t: &MText, document: &acadrust::CadDocument) -> TruckEntity { t.insertion_point.z, ); TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Text(layout.strokes), snap_pts: vec![(insertion, SnapHint::Insertion)], tangent_geoms: vec![], diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 608465ecb..5f1fb4419 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -298,6 +298,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option = Vec::new(); wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1684,6 +1687,8 @@ impl MultiLeaderTess for MultiLeader { xy = xy.max(y); } wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: sdf_verts, @@ -1749,6 +1754,8 @@ impl MultiLeaderTess for MultiLeader { wcs_corners[3], ]; wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1782,6 +1789,8 @@ impl MultiLeaderTess for MultiLeader { wcs_corners[0], ]; wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/entities/ole2frame.rs b/src/entities/ole2frame.rs index 37907b883..3761de581 100644 --- a/src/entities/ole2frame.rs +++ b/src/entities/ole2frame.rs @@ -17,6 +17,7 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { if (x1 - x0).abs() < 1e-6 && (y1 - y0).abs() < 1e-6 { let s = 0.5_f64; return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(vec![[-s, 0.0, z], [s, 0.0, z]]), snap_pts: vec![], tangent_geoms: vec![], @@ -46,6 +47,7 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { ]; let center = glam::DVec3::new(cx, cy, z); TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![(center, SnapHint::Center)], tangent_geoms: vec![], diff --git a/src/entities/point.rs b/src/entities/point.rs index eb718f5df..7691dcee9 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -40,6 +40,7 @@ fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity { // Default: a single vertex (driver handles the dot pixel). let p = Point3::new(wx, wy, wz); return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Point(builder::vertex(p)), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], @@ -51,6 +52,7 @@ fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity { if pts.is_empty() { // PDMODE 1 = nothing — emit an empty Lines wire so picking still works. return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(vec![]), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], @@ -59,6 +61,7 @@ fn point_truck(pt: &Point, pdmode: i16, s: f64) -> TruckEntity { }; } TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![(snap, SnapHint::Node)], tangent_geoms: vec![], diff --git a/src/entities/polyline.rs b/src/entities/polyline.rs index ebb00103d..ff8ae5fae 100644 --- a/src/entities/polyline.rs +++ b/src/entities/polyline.rs @@ -6,7 +6,7 @@ use crate::entities::common::{ edit_prop as edit, parse_f64, ro_prop as ro, square_grip, stepper_prop as stepper, }; use crate::entities::traits::{Grippable, PropertyEditable, Transformable, TruckConvertible}; -use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject}; +use crate::scene::convert::acad_to_truck::{extrusion_wall_tris, TruckEntity, TruckObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property}; use crate::scene::model::wire_model::TangentGeom; @@ -26,6 +26,7 @@ fn tessellate_polyline(pl: &Polyline) -> TruckEntity { let key_verts = pts.clone(); TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(points), snap_pts: vec![], tangent_geoms: vec![], @@ -169,6 +170,7 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { let verts = &pl.vertices; if verts.is_empty() { return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(vec![]), snap_pts: vec![], tangent_geoms: vec![], @@ -253,6 +255,7 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { } } return TruckEntity { + pick_tris: extrusion_wall_tris(&path, [t * nx, t * ny, t * nz]), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: tgs, @@ -301,6 +304,7 @@ fn tessellate_polyline2d(pl: &Polyline2D) -> TruckEntity { } TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Contour(edges.into_iter().collect::()), snap_pts: vec![], tangent_geoms: tangents, @@ -578,6 +582,7 @@ fn tessellate_polyline3d(pl: &Polyline3D) -> TruckEntity { } TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(points), snap_pts: vec![], tangent_geoms: vec![], diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index f2f3c0948..2b2c4ee1c 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -115,6 +115,7 @@ impl TruckConvertible for RasterImage { }; Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], @@ -348,6 +349,7 @@ impl TruckConvertible for Wipeout { }; Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], diff --git a/src/entities/ray.rs b/src/entities/ray.rs index 00a3ab253..af7c8ce9a 100644 --- a/src/entities/ray.rs +++ b/src/entities/ray.rs @@ -29,6 +29,7 @@ impl TruckConvertible for Ray { ]; let start: [f64; 3] = [bp.x, bp.y, bp.z]; Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(vec![start, far]), snap_pts: vec![], tangent_geoms: vec![], @@ -169,6 +170,7 @@ impl TruckConvertible for XLine { bp.z - nz * DISPLAY_EXTENT, ]; Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(vec![far_neg, far_pos]), snap_pts: vec![], tangent_geoms: vec![], diff --git a/src/entities/shape.rs b/src/entities/shape.rs index cd60f7205..3c615e9f2 100644 --- a/src/entities/shape.rs +++ b/src/entities/shape.rs @@ -74,6 +74,7 @@ impl TruckConvertible for Shape { ); Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], diff --git a/src/entities/solid.rs b/src/entities/solid.rs index 88365210c..e0c5f631f 100644 --- a/src/entities/solid.rs +++ b/src/entities/solid.rs @@ -54,6 +54,7 @@ impl TruckConvertible for Solid { ]; Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: snap, tangent_geoms: vec![], diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 8a062a0d5..46474cb60 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -23,6 +23,7 @@ fn to_truck(spl: &Spline) -> TruckEntity { let key_vertices: Vec<[f64; 3]> = spl.fit_points.iter().map(|p| [p.x, p.y, p.z]).collect(); return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], @@ -31,6 +32,7 @@ fn to_truck(spl: &Spline) -> TruckEntity { }; } return TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Point(builder::vertex(Point3::new(0.0, 0.0, 0.0))), snap_pts: vec![], tangent_geoms: vec![], @@ -106,6 +108,7 @@ fn to_truck(spl: &Spline) -> TruckEntity { }; TruckEntity { + pick_tris: Vec::new(), object, snap_pts: vec![], tangent_geoms: vec![], diff --git a/src/entities/table.rs b/src/entities/table.rs index b4794b170..d4aebbeb8 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -345,6 +345,7 @@ impl TruckConvertible for Table { }) .collect(); Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts_f64), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Insertion)], tangent_geoms: vec![], @@ -748,6 +749,8 @@ pub fn tessellate_table( let mk = |color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/entities/text.rs b/src/entities/text.rs index 2dea2ff02..900e93a09 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -113,6 +113,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { &value, ); TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Text(vec![TextStroke { strokes, origin: p.origin, diff --git a/src/entities/tolerance.rs b/src/entities/tolerance.rs index 3f0bf9f8e..387492b93 100644 --- a/src/entities/tolerance.rs +++ b/src/entities/tolerance.rs @@ -493,6 +493,7 @@ impl TruckConvertible for Tolerance { } Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Text(groups), snap_pts: vec![(snap_pt, SnapHint::Insertion)], tangent_geoms: vec![], diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index 6c3f6eff3..b3b6ce7f1 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -53,6 +53,7 @@ impl TruckConvertible for Underlay { // Insertion grip. let key: Vec<[f64; 3]> = pts.clone(); Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Node)], tangent_geoms: vec![], @@ -63,6 +64,7 @@ impl TruckConvertible for Underlay { // No clip boundary: draw a cross at insertion point. let pts = cross_wire(origin, 1.0); Some(TruckEntity { + pick_tris: Vec::new(), object: TruckObject::Lines(pts), snap_pts: vec![(glam::DVec3::new(self.insertion_point.x, self.insertion_point.y, self.insertion_point.z), SnapHint::Node)], tangent_geoms: vec![], diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 97c14f4db..b88d006d3 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -186,6 +186,8 @@ impl CadCommand for AlignedDimensionCommand { let p1 = p1.as_vec3(); let p2 = p2.as_vec3(); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -235,6 +237,8 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { let d1 = d1.as_vec3(); let d2 = d2.as_vec3(); WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 38c8ecd16..89ac14038 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -130,6 +130,8 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index 21a5a1858..74ede2120 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -180,6 +180,8 @@ fn v3(p: DVec3) -> Vector3 { fn preview_line(a: Vec3, b: Vec3) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 8e45d0eda..ab0154941 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -179,6 +179,8 @@ impl CadCommand for DimBaselineCommand { let dim_line_pt2 = dim_line_pt2.as_vec3(); let pt = pt.as_vec3(); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 05ef93048..e98539bce 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -159,6 +159,8 @@ impl CadCommand for DimContinueCommand { let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp)); let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp)); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index 834a2c508..2945ac6b5 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -85,6 +85,8 @@ impl CadCommand for DimJogLineCommand { } let d = 0.3_f32; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index aedfb0c96..2e32c3142 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -100,6 +100,8 @@ impl CadCommand for DimTeditCommand { } let d = 0.2_f32; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index f2932a10b..5ada24bd4 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -163,6 +163,8 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { points.push([w2.x, w2.y, w2.z]); } WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index 8ab13b655..d2465af57 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -218,6 +218,8 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index b7d55d614..79539e969 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -150,6 +150,8 @@ fn preview_wire(pts: &[Vec3]) -> WireModel { points.push([w2.x, w2.y, w2.z]); } WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index dbf6689a8..e9a071110 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -458,6 +458,8 @@ impl CadCommand for MLeaderCollectCommand { fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index 8e20fe328..430964102 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -121,6 +121,8 @@ fn ordinate_elbow(feature: DVec3, leader: DVec3, is_x: bool) -> DVec3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index 4b9a16894..5338d57d5 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -90,6 +90,8 @@ impl CadCommand for QdimCommand { } let d = 0.5_f32; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 11ba80bec..64f9a8340 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -185,6 +185,8 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index 7ce6abb4f..999f56cfd 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -139,6 +139,8 @@ impl CadCommand for TableCommand { let y = pt.y; let z = pt.z; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index b7dd939de..78b278d3d 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -84,6 +84,8 @@ impl CadCommand for ToleranceCommand { } let d = 0.15_f32; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index e81d24d22..fe0c417f4 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -160,6 +160,8 @@ impl CadCommand for AttdefCommand { // Show a small cross at the insertion point. let d = 0.15_f32; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index bbba18e6c..62cd8de2b 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -136,6 +136,8 @@ impl CadCommand for MlineCommand { .collect(); pts.push([pt.x, pt.y, pt.z]); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index d428a7e6c..1360161cf 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -108,6 +108,8 @@ impl CadCommand for ImageCommand { let p3 = [origin.x, origin.y + world_height, origin.z]; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index f8eaa5950..13373420e 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -84,6 +84,8 @@ impl CadCommand for RayCommand { let dir = (pt - base).normalize_or_zero(); let far = base + dir * DISPLAY_EXTENT; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -177,6 +179,8 @@ impl CadCommand for XLineCommand { let far_pos = base + dir * DISPLAY_EXTENT; let far_neg = base - dir * DISPLAY_EXTENT; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/draw/revcloud.rs b/src/modules/draw/draw/revcloud.rs index 5d7cfdb63..750b3a03e 100644 --- a/src/modules/draw/draw/revcloud.rs +++ b/src/modules/draw/draw/revcloud.rs @@ -89,6 +89,8 @@ impl CadCommand for RevCloudCommand { self.points[0].z as f32, ]); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/draw/wipeout.rs b/src/modules/draw/draw/wipeout.rs index 1cdb78b81..fc0df1178 100644 --- a/src/modules/draw/draw/wipeout.rs +++ b/src/modules/draw/draw/wipeout.rs @@ -114,6 +114,8 @@ impl CadCommand for WipeoutCommand { let min = p1.min(pt); let max = p1.max(pt); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -158,6 +160,8 @@ impl CadCommand for WipeoutCommand { self.poly_pts[0].z as f32, ]); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index e70b19ee8..637a86847 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -69,6 +69,8 @@ impl CadCommand for AreaCommand { pts.push(f(pt)); pts.push(f(self.points[0])); Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index 14e7a5f1d..e5280556f 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -63,6 +63,8 @@ impl CadCommand for DistCommand { let p1 = self.first?; // The preview wire is purely visual, so f32 vertices are fine here. Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index ad5dd452e..3a0314481 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -67,6 +67,8 @@ impl MeasureGeomCommand { /// Build a cyan preview wire connecting the picked points and the cursor. fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index aed187048..3df30c9a5 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3059,6 +3059,8 @@ fn collect_runs(pts: &[[f64; 2]], take: &dyn Fn([f64; 2]) -> bool, out: &mut Vec /// Build a preview wire from a NaN-break point list. fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/layout/mview.rs b/src/modules/layout/mview.rs index e45f2fad0..0bbe5e2e3 100644 --- a/src/modules/layout/mview.rs +++ b/src/modules/layout/mview.rs @@ -81,6 +81,8 @@ impl CadCommand for MviewCommand { let pt = pt.as_vec3(); let c1 = self.corner1?; Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 7c415853d..b1f767514 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -305,6 +305,8 @@ inventory::submit!(crate::command::CommandRegistration { fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index 44d358d76..388b29435 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -48,6 +48,8 @@ impl CadCommand for PlotWindowCommand { let p1 = self.p1?.as_vec3(); // Draw the selection rectangle. Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index abb98f3c3..ccba95fbb 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -65,6 +65,8 @@ impl CadCommand for ZoomWindowCommand { let max = p1.max(pt); // Draw a rectangle preview Some(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index 1682f7eb1..d3ea1df68 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -48,6 +48,11 @@ pub struct LocalWire { pub tangent_geoms: Vec, pub fill_tris: Vec<[f32; 3]>, pub fill_tris_low: Vec<[f32; 3]>, + /// Thickness-wall pick geometry, transformed by the insert like `points` + /// so a block child's extruded wall stays selectable at the instance's + /// place and scale. Never reaches the GPU. + pub pick_tris: Vec<[f32; 3]>, + pub pick_tris_low: Vec<[f32; 3]>, /// Per-wire colour from the tessellator output. For most entities this /// equals the sub-entity's resolved colour. For colour-split MTEXT /// (`\C`/`\c` inline overrides) each wire carries its own override colour. @@ -464,6 +469,8 @@ fn tessellate_sub_local( tangent_geoms: wire.tangent_geoms, fill_tris: wire.fill_tris, fill_tris_low: wire.fill_tris_low, + pick_tris: wire.pick_tris, + pick_tris_low: wire.pick_tris_low, color: wire.color, aci, pattern_length: pat_len, @@ -748,6 +755,10 @@ struct BatchEntry { /// reconstructs `high + low`). Without it absolute f32 fills quantize to /// ~0.5 m and the greek-text rectangles shear. fill_tris_low: Vec<[f32; 3]>, + /// Accumulated thickness-wall pick geometry, paired high/low like + /// `fill_tris`. Pick-only — no GPU batch reads this. + pick_tris: Vec<[f32; 3]>, + pick_tris_low: Vec<[f32; 3]>, /// Accumulated SDF glyph quads (world space) for block-instance text. text_verts: Vec, min_x: f32, @@ -826,6 +837,8 @@ impl Batches { // the cached defn. let color = crate::scene::view::render::adapt_to_bg(b.color, bg_color); WireModel { + pick_tris: b.pick_tris, + pick_tris_low: b.pick_tris_low, dash_from_start: false, dash_align_end: None, text_verts: b.text_verts, @@ -1086,7 +1099,11 @@ fn emit_wire( ctx: &ExpandCtx, out: &mut Batches, ) { - if lw.points.is_empty() && lw.fill_tris.is_empty() && lw.text_verts.is_empty() { + if lw.points.is_empty() + && lw.fill_tris.is_empty() + && lw.text_verts.is_empty() + && lw.pick_tris.is_empty() + { return; } @@ -1246,6 +1263,27 @@ fn emit_wire( entry.fill_tris.push([hx, hy, hz]); entry.fill_tris_low.push([lx, ly, lz]); } + // Thickness walls: same reconstruct → transform → re-split as the fills + // above, so a block child's wall tracks the insert's placement and scale. + debug_assert!( + lw.pick_tris_low.is_empty() || lw.pick_tris.len() == lw.pick_tris_low.len(), + "pick_tris_low must be empty or the same length as pick_tris (got {} vs {})", + lw.pick_tris.len(), + lw.pick_tris_low.len(), + ); + for (idx, p) in lw.pick_tris.iter().enumerate() { + let pl = lw.pick_tris_low.get(idx).copied().unwrap_or([0.0; 3]); + let v = accum_xform.apply(Vector3::new( + p[0] as f64 + pl[0] as f64, + p[1] as f64 + pl[1] as f64, + p[2] as f64 + pl[2] as f64, + )); + let (hx, lx) = WireModel::split_ds(v.x); + let (hy, ly) = WireModel::split_ds(v.y); + let (hz, lz) = WireModel::split_ds(v.z); + entry.pick_tris.push([hx, hy, hz]); + entry.pick_tris_low.push([lx, ly, lz]); + } // SDF glyph quads: reconstruct each block-local f64 position, apply the // insert transform, re-split — same path as points/fills so block-instance // text lands at the right world place and scale. Colour resolves to the diff --git a/src/scene/convert/acad_to_truck.rs b/src/scene/convert/acad_to_truck.rs index 451d24bda..543012694 100644 --- a/src/scene/convert/acad_to_truck.rs +++ b/src/scene/convert/acad_to_truck.rs @@ -70,8 +70,44 @@ pub struct TruckEntity { /// triangle. Non-empty for mesh-like entities (PolyfaceMesh, PolygonMesh) /// that need solid fill. pub fill_tris: Vec<[f64; 3]>, + /// Pre-triangulated pick-only geometry: flat list of WCS f64 vertices, 3 + /// per triangle. Non-empty for entities carrying a DXF thickness (code 39) + /// — the swept wall between each base segment and its extruded copy. Kept + /// apart from `fill_tris` because only that one reaches the GPU; see + /// [`WireModel::pick_tris`](crate::scene::model::wire_model::WireModel::pick_tris). + pub pick_tris: Vec<[f64; 3]>, } pub fn convert(entity: &EntityType, document: &CadDocument) -> Option { entity.to_truck_entity(document) } + +/// Triangulate the wall swept by extruding `base` along `extrusion` — the +/// geometry a DXF thickness (code 39) adds to an entity. Two triangles per +/// base segment, in `TruckEntity::pick_tris` order (flat WCS f64, 3 per tri). +/// +/// `base` is a point chain in the same form a wire uses, so a NaN entry breaks +/// the sweep exactly as it breaks a segment chain — a polyline with disjoint +/// runs extrudes into one wall per run, not a wall bridging the gap. +pub fn extrusion_wall_tris(base: &[[f64; 3]], extrusion: [f64; 3]) -> Vec<[f64; 3]> { + let top = |p: [f64; 3]| { + [ + p[0] + extrusion[0], + p[1] + extrusion[1], + p[2] + extrusion[2], + ] + }; + let mut out = Vec::new(); + let mut prev: Option<[f64; 3]> = None; + for &p in base { + if p[0].is_nan() { + prev = None; + continue; + } + if let Some(a) = prev { + out.extend_from_slice(&[a, p, top(p), a, top(p), top(a)]); + } + prev = Some(p); + } + out +} diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index e987eff73..7c0f0f53f 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -502,6 +502,8 @@ pub(crate) fn tessellate_entity( (ins.insert_point.z) as f32, ); let marker = WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -880,6 +882,8 @@ fn lod_stub_wire( // LOD boundary. #19. let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -945,6 +949,8 @@ fn lod_stub_wire_3d( } let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 06fd12bdc..233934105 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -370,6 +370,8 @@ pub fn tessellate( None }; elem_wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), // MLINE dashes: A-type aligned, but the end-dash length is // shared across all parallel elements (`dash_align_end`) so // their interiors stay in phase (perpendicular dashes line up) @@ -650,6 +652,8 @@ pub fn tessellate( ftl.push(lo); } wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -685,6 +689,8 @@ pub fn tessellate( fpl.push(lo); } wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -727,6 +733,8 @@ pub fn tessellate( low.push(ll); } wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -750,6 +758,8 @@ pub fn tessellate( }); } wires.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: sdf_verts, @@ -803,6 +813,8 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -838,6 +850,8 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -870,6 +884,8 @@ pub fn tessellate( // are empty → the early-return path above). if !sdf_verts.is_empty() { out.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: sdf_verts, @@ -895,6 +911,8 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -942,6 +960,8 @@ pub fn tessellate( .map(|[kx, ky, kz]| [kx, ky, kz]) .collect(); return vec![WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -988,6 +1008,8 @@ pub fn tessellate( .map(|[x, y, z]| [x, y, z]) .collect(); return vec![WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1023,6 +1045,8 @@ pub fn tessellate( .map(|[x, y, z]| [x, y, z]) .collect(); return vec![WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1062,6 +1086,11 @@ pub fn tessellate( .map(|[x, y, z]| [x, y, z]) .collect(); let (fill_tris, fill_tris_low) = points_to_ds(te.fill_tris); + // Thickness walls ride on the wire that carries their edges, not + // on a wire of their own: they are pick geometry for that entity, + // and `fill_tris` below deliberately splits off into a fill-only + // wire (`is_fill_only`) which has no `points` to hang them from. + let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); let mut out = Vec::new(); let mut is_first = true; @@ -1077,6 +1106,8 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + pick_tris, + pick_tris_low, dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1111,6 +1142,8 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1136,6 +1169,8 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1171,6 +1206,8 @@ pub fn tessellate( .map(|[x, y, z]| [x, y, z]) .collect(); return vec![WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -1237,6 +1274,8 @@ pub fn tessellate( let snap_pts: Vec<(glam::DVec3, SnapHint)> = snap_pts.into_iter().map(|(p, h)| (p.as_dvec3(), h)).collect(); vec![WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 6f48a7b82..e7fb92ee9 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -95,6 +95,19 @@ pub struct WireModel { /// Low residual paired with [`fill_tris`] (double-single). Empty = all-zero; /// tessellation from CAD f64 fills it so fills stay precise at UTM scale. pub fill_tris_low: Vec<[f32; 3]>, + /// Pre-triangulated pick-only geometry: flat vertex list, 3 per triangle. + /// Hit-testing treats these as solid, the renderer never uploads them. + /// + /// Non-empty only for entities extruded by a DXF thickness (code 39): the + /// swept wall between each base segment and its extruded copy. The wall is + /// drawn as its four wireframe edges (which live in [`points`]), so it has + /// no fill geometry to pick — without this the interior is a hole the + /// cursor falls through. Separate from [`fill_tris`] because that channel + /// reaches the GPU, and a wall that renders shaded would change how every + /// thickness drawing looks. + pub pick_tris: Vec<[f32; 3]>, + /// Low residual paired with [`pick_tris`] (double-single). Empty = all-zero. + pub pick_tris_low: Vec<[f32; 3]>, /// SDF glyph quads for this entity's text (TEXT / MTEXT / dimension text / /// block-internal text). Non-empty only when SDF text is enabled and this /// wire carries a text run. Rides with the wire so it is cached by the @@ -149,6 +162,8 @@ impl WireModel { /// Create a solid wire (no dash pattern, 1px weight). pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self { Self { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), text_verts: Vec::new(), name, points, @@ -379,6 +394,8 @@ impl Default for WireModel { vp_scissor: None, fill_tris: Vec::new(), fill_tris_low: Vec::new(), + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), } } } diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index 4d5c8a800..05821c27f 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -15,12 +15,115 @@ use crate::scene::model::mesh_model::MeshModel; use crate::scene::model::wire_model::WireModel; /// Pixel radius used for single-click wire detection. -const CLICK_THRESHOLD_PX: f32 = 8.0; +pub const CLICK_THRESHOLD_PX: f32 = 8.0; + +/// Pick radius for one wire, in screen pixels. +/// +/// A wire renders as a band `line_weight_px` wide, so testing every wire at the +/// bare [`CLICK_THRESHOLD_PX`] would leave the outer part of a heavy line +/// unselectable — the cursor would sit on solid ink and miss. Widening to the +/// rendered half-width keeps "looks like I'm on it" and "picks it" the same +/// thing at any zoom: both quantities are screen-space, so the relation holds +/// however far in the view is. +/// +/// `lw_display` mirrors the wire shader's `select(0.5, half_width, ...)` +/// (`wire.wgsl`) — with lineweight display off the line collapses to 1 px, so +/// the pick band must collapse with it rather than stay secretly fat. +/// +/// The standard weights all land under the threshold today (the widest, 2.11 mm, +/// renders 7.97 px half-width), so this only bites for out-of-range weights — +/// and it keeps the two sides from silently drifting apart if the display boost +/// in `view::render::lineweight_to_px` ever changes. +pub fn pick_tolerance_px(wire: &WireModel, lw_display: bool) -> f32 { + let half_width = if lw_display { + wire.line_weight_px * 0.5 + } else { + 0.5 + }; + CLICK_THRESHOLD_PX.max(half_width) +} + +/// Is `aabb` — a wire's world-space XY box — further than `tol` pixels from +/// `cursor` once projected, so the wire can be skipped without touching its +/// geometry? +/// +/// Only sound in a flat (untilted) view, where a point's screen x/y depends on +/// its world x/y alone and the box therefore projects exactly. Callers must +/// check that themselves, and must skip the unbounded sentinel. +fn aabb_rejects( + aabb: [f32; 4], + cursor: Point, + tol: f32, + view_rot: Mat4, + eye: glam::DVec3, + bounds: Rectangle, +) -> bool { + let [minx, miny, maxx, maxy] = aabb; + // Project all four corners — a plan view can be rotated about Z, so the + // screen footprint isn't axis-aligned and the two diagonal corners alone + // wouldn't bound it. + let (mut sx0, mut sy0, mut sx1, mut sy1) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN); + for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] { + let s = world_to_screen( + glam::DVec3::new(cx as f64, cy as f64, 0.0), + view_rot, + eye, + bounds, + ); + sx0 = sx0.min(s.x); + sx1 = sx1.max(s.x); + sy0 = sy0.min(s.y); + sy1 = sy1.max(s.y); + } + cursor.x < sx0 - tol || cursor.x > sx1 + tol || cursor.y < sy0 - tol || cursor.y > sy1 + tol +} + +/// Depth of the first triangle in `tris` whose screen projection contains +/// `cursor`, as the mean NDC z of its corners; `None` when none do. +/// +/// `tris` is a flat vertex list, 3 per triangle, and `tris_low` its +/// double-single residual — empty meaning an all-zero low half, per the +/// [`WireModel`] contract. +fn tris_hit_depth( + cursor: Point, + tris: &[[f32; 3]], + tris_low: &[[f32; 3]], + view_rot: Mat4, + eye: glam::DVec3, + bounds: Rectangle, +) -> Option { + let mut t = 0; + while t + 2 < tris.len() { + let mut sp = [Point::ORIGIN; 3]; + let mut depth = 0.0f32; + for j in 0..3 { + let k = t + j; + let hi = tris[k]; + let lo = tris_low.get(k).copied().unwrap_or([0.0; 3]); + let world = glam::DVec3::new( + hi[0] as f64 + lo[0] as f64, + hi[1] as f64 + lo[1] as f64, + hi[2] as f64 + lo[2] as f64, + ); + let ndc = view_rot.project_point3((world - eye).as_vec3()); + sp[j] = Point::new( + (ndc.x + 1.0) * 0.5 * bounds.width, + (1.0 - ndc.y) * 0.5 * bounds.height, + ); + depth += ndc.z; + } + t += 3; + if point_in_polygon(cursor, &sp) { + return Some(depth / 3.0); + } + } + None +} // ── Single-click hit test ───────────────────────────────────────────────── /// Return the `name` of the closest wire whose screen-space segments pass -/// within `CLICK_THRESHOLD_PX` pixels of `cursor`. +/// within that wire's [`pick_tolerance_px`] of `cursor`. /// /// Returns `None` when no wire is close enough. pub fn click_hit<'a>( @@ -29,13 +132,16 @@ pub fn click_hit<'a>( view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle, + lw_display: bool, ) -> Option<&'a str> { // A click outside the pane rectangle (e.g. on the paper around a floating // viewport) must not reach geometry scissored out of the viewport. if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height { return None; } - let mut best_dist = CLICK_THRESHOLD_PX; + // Each wire brings its own threshold now (a heavy line catches over its full + // rendered width), so the running best can't double as the cut-off. + let mut best_dist = f32::MAX; let mut best: Option<&str> = None; // World z only shifts the *screen* x/y when the view is tilted (orbit / @@ -48,29 +154,14 @@ pub fn click_hit<'a>( // Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain. for wire in wires { + let tol = pick_tolerance_px(wire, lw_display); // Cheap AABB pre-reject (flat view only; never for the unbounded // sentinel used by previews / greeked text). - if z_flat && wire.aabb != WireModel::UNBOUNDED_AABB { - let [minx, miny, maxx, maxy] = wire.aabb; - // Project all four corners — a plan view can be rotated about Z, so - // the screen footprint isn't axis-aligned and the two diagonal - // corners alone wouldn't bound it. - let mut sx0 = f32::MAX; - let mut sy0 = f32::MAX; - let mut sx1 = f32::MIN; - let mut sy1 = f32::MIN; - for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] { - let s = world_to_screen(glam::DVec3::new(cx as f64, cy as f64, 0.0), view_rot, eye, bounds); - sx0 = sx0.min(s.x); - sx1 = sx1.max(s.x); - sy0 = sy0.min(s.y); - sy1 = sy1.max(s.y); - } - let t = CLICK_THRESHOLD_PX; - if cursor.x < sx0 - t || cursor.x > sx1 + t || cursor.y < sy0 - t || cursor.y > sy1 + t - { - continue; - } + if z_flat + && wire.aabb != WireModel::UNBOUNDED_AABB + && aabb_rejects(wire.aabb, cursor, tol, view_rot, eye, bounds) + { + continue; } let mut prev: Option = None; for (i, &[px, py, pz]) in wire.points.iter().enumerate() { @@ -81,7 +172,7 @@ pub fn click_hit<'a>( let cur = world_to_screen(wp64([px, py, pz], &wire.points_low, i), view_rot, eye, bounds); if let Some(p0) = prev { let d = dist_point_to_segment(cursor, p0, cur); - if d < best_dist { + if d < tol && d < best_dist { best_dist = d; best = Some(&wire.name); } @@ -104,33 +195,16 @@ pub fn click_hit<'a>( if wire.fill_tris.is_empty() { continue; } - let mut t = 0; - while t + 2 < wire.fill_tris.len() { - let mut sp = [Point::ORIGIN; 3]; - let mut depth = 0.0f32; - for j in 0..3 { - let k = t + j; - let hi = wire.fill_tris[k]; - let lo = wire.fill_tris_low.get(k).copied().unwrap_or([0.0; 3]); - let world = glam::DVec3::new( - hi[0] as f64 + lo[0] as f64, - hi[1] as f64 + lo[1] as f64, - hi[2] as f64 + lo[2] as f64, - ); - let ndc = view_rot.project_point3((world - eye).as_vec3()); - sp[j] = Point::new( - (ndc.x + 1.0) * 0.5 * bounds.width, - (1.0 - ndc.y) * 0.5 * bounds.height, - ); - depth += ndc.z; - } - t += 3; - if point_in_polygon(cursor, &sp) { - let d = depth / 3.0; - if best_fill.map_or(true, |(bd, _)| d < bd) { - best_fill = Some((d, wire.name.as_str())); - } - break; + if let Some(d) = tris_hit_depth( + cursor, + &wire.fill_tris, + &wire.fill_tris_low, + view_rot, + eye, + bounds, + ) { + if best_fill.map_or(true, |(bd, _)| d < bd) { + best_fill = Some((d, wire.name.as_str())); } } } @@ -138,6 +212,47 @@ pub fn click_hit<'a>( return Some(n); } + // No drawn surface either. Entities extruded by a thickness draw their wall + // as four edges and nothing in between, so the interior is a hole the cursor + // falls straight through even though the wall reads as a face. Test the + // pick-only wall geometry to close it, front-most first. + // + // Ranked below `fill_tris` on purpose: those triangles are actually + // rendered, these are invisible, and where an opaque face overlaps a wall + // the user is looking at the face — so the face is what they mean to click. + let mut best_wall: Option<(f32, &str)> = None; + for wire in wires { + if wire.pick_tris.is_empty() { + continue; + } + // This runs on every hover that misses everything else — the common case + // over empty space — and a wall is two triangles per base segment, so an + // extruded circle alone is ~128 of them. Reject on the box first or a + // drawing full of thickness turns every mouse move into a projection of + // its every wall. + if z_flat + && wire.aabb != WireModel::UNBOUNDED_AABB + && aabb_rejects(wire.aabb, cursor, 0.0, view_rot, eye, bounds) + { + continue; + } + if let Some(d) = tris_hit_depth( + cursor, + &wire.pick_tris, + &wire.pick_tris_low, + view_rot, + eye, + bounds, + ) { + if best_wall.map_or(true, |(bd, _)| d < bd) { + best_wall = Some((d, wire.name.as_str())); + } + } + } + if let Some((_, n)) = best_wall { + return Some(n); + } + // SDF text renders as glyph quads, not strokes — its only pick target is // the empty wire that carries the quads (`text_verts`) and the text's own // tight AABB. Fall back to AABB containment so such text stays click- @@ -184,14 +299,16 @@ pub fn click_hits_all<'a>( view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle, + lw_display: bool, ) -> Vec<&'a str> { if cursor.x < 0.0 || cursor.x > bounds.width || cursor.y < 0.0 || cursor.y > bounds.height { return Vec::new(); } let mut hits: Vec<(f32, &str)> = Vec::new(); for wire in wires { + let tol = pick_tolerance_px(wire, lw_display); let mut prev: Option = None; - let mut best_for_wire = CLICK_THRESHOLD_PX; + let mut best_for_wire = tol; let mut hit = false; for (i, &[px, py, pz]) in wire.points.iter().enumerate() { if px.is_nan() { @@ -212,6 +329,29 @@ pub fn click_hits_all<'a>( hits.push((best_for_wire, &wire.name)); } } + // Thickness walls join the cycle so an extruded entity picked on its wall + // can be stepped past to whatever sits behind it. Ranked at the threshold, + // below every proximity hit — same convention the text boxes below use. + // + // A wall's own edges live on the same wire, so skip any wire the loop above + // already caught: cycling must not offer one entity twice. + for wire in wires { + if wire.pick_tris.is_empty() || hits.iter().any(|&(_, n)| n == wire.name) { + continue; + } + if tris_hit_depth( + cursor, + &wire.pick_tris, + &wire.pick_tris_low, + view_rot, + eye, + bounds, + ) + .is_some() + { + hits.push((CLICK_THRESHOLD_PX, &wire.name)); + } + } // SDF text: include a text whose box contains the cursor (an empty-stroke // wire carrying glyph quads) so selection cycling steps through text too — // the same discriminator/fallback `click_hit` uses. Ranked after real @@ -974,9 +1114,9 @@ mod aabb_reject_tests { let far = wire("9", vec![[0.9, 0.9, 0.0], [0.95, 0.9, 0.0]], [0.9, 0.9, 0.95, 0.9]); let eye = glam::DVec3::ZERO; - assert_eq!(click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds), Some("5")); - assert_eq!(click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds), None); + assert_eq!(click_hit(cursor, std::slice::from_ref(&near), vp, eye, bounds, true), Some("5")); + assert_eq!(click_hit(cursor, std::slice::from_ref(&far), vp, eye, bounds, true), None); // The far wire must be rejected without hiding the near one. - assert_eq!(click_hit(cursor, &[far, near], vp, eye, bounds), Some("5")); + assert_eq!(click_hit(cursor, &[far, near], vp, eye, bounds, true), Some("5")); } } diff --git a/src/scene/project.rs b/src/scene/project.rs index ceb8afb56..375925f67 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -313,6 +313,14 @@ impl Scene { // re-expressed in paper coords — drop it (no tangent snap on // projected viewport content) rather than snap to UTM. out.tangent_geoms = Vec::new(); + // Same for thickness walls: `points` above were projected and + // clipped into paper coords, and the clone's wall triangles are + // still model-space. Left in place they would hit-test at a + // model-scale offset — a stray catch somewhere out on the sheet. + // Drop them: an extruded entity inside a floating viewport stays + // selectable by its edges, which do get projected. + out.pick_tris = Vec::new(); + out.pick_tris_low = Vec::new(); out.aabb = if pmnx.is_finite() { [pmnx, pmny, pmxx, pmxy] } else { diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 5382c8270..337e092ad 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -305,6 +305,8 @@ pub fn apply_along( .into_iter() .filter(|s| s.len() >= 2) .map(|pts| WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), @@ -342,6 +344,8 @@ pub fn apply_along( xy = xy.max(y); } out.push(WireModel { + pick_tris: Vec::new(), + pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts,