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,