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 995def90..abc8737a 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 44f9f73c..c6f7c851 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 a5862ff5..ab9846ad 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 766e0dfc..7afa22f5 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 b8f19876..52c8ce84 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 2deb6271..43dba462 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 f02c4a78..8dc007c3 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 5a6c7b1f..015be6fa 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 b56f1a05..1a30d12b 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 cd1a79f5..49d32507 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 08a79d93..dca06998 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 25c19d47..64097b73 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 608465ec..5f1fb441 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 37907b88..3761de58 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 eb718f5d..7691dcee 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 ebb00103..ff8ae5fa 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 f2f3c094..2b2c4ee1 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 00a3ab25..af7c8ce9 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 cd60f720..3c615e9f 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 88365210..e0c5f631 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 8a062a0d..46474cb6 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 b4794b17..d4aebbeb 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 2dea2ff0..900e93a0 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 3f0bf9f8..387492b9 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 6c3f6eff..b3b6ce7f 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 97c14f4d..b88d006d 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 38c8ecd1..89ac1403 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 21a5a185..74ede212 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 8e45d0ed..ab015494 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 05ef9304..e98539bc 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 834a2c50..2945ac6b 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 aedfb0c9..2e32c314 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 f2932a10..5ada24bd 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 8ab13b65..d2465af5 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 b7d55d61..79539e96 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 dbf6689a..e9a07111 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 8e20fe32..43096410 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 4b9a1689..5338d57d 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 11ba80be..64f9a834 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 7ce6abb4..999f56cf 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 b7dd939d..78b278d3 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 e81d24d2..fe0c417f 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 bbba18e6..62cd8de2 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 d428a7e6..1360161c 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 f8eaa595..13373420 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 5d7cfdb6..750b3a03 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 1cdb78b8..fc0df117 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 e70b19ee..637a8684 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 14e7a5f1..e5280556 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 ad5dd452..3a031448 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 aed18704..3df30c9a 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 e45f2fad..0bbe5e2e 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 7c415853..b1f76751 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 44d358d7..388b2943 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 abb98f3c..ccba95fb 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 1682f7eb..d3ea1df6 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 451d24bd..54301269 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 e987eff7..7c0f0f53 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 06fd12bd..23393410 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 6f48a7b8..e7fb92ee 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 4d5c8a80..05821c27 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 ceb8afb5..375925f6 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 5382c827..337e092a 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,