diff --git a/src/entities/common.rs b/src/entities/common.rs index f3d634169..2443aea4c 100644 --- a/src/entities/common.rs +++ b/src/entities/common.rs @@ -119,6 +119,13 @@ pub fn format_angle(value_rad: f64) -> String { } } +/// Two interior triangles covering a quad (flat list, 6 vertices) — the +/// click-anywhere pick surface for frame-like entities (image, OLE frame, +/// underlay, wipeout). Corners in ring order. +pub fn quad_pick_tris(c: &[[f64; 3]; 4]) -> Vec<[f64; 3]> { + vec![c[0], c[1], c[2], c[0], c[2], c[3]] +} + pub fn square_grip(id: usize, world: glam::DVec3) -> GripDef { GripDef { id, diff --git a/src/entities/ole2frame.rs b/src/entities/ole2frame.rs index c4324860d..761aad554 100644 --- a/src/entities/ole2frame.rs +++ b/src/entities/ole2frame.rs @@ -43,7 +43,14 @@ fn to_truck(ole: &Ole2Frame) -> TruckEntity { ]; let center = glam::DVec3::new(cx, cy, z); TruckEntity { - pick_tris: Vec::new(), + // Interior pick surface: the frame selects on a click anywhere + // inside, not just on its border. + pick_tris: crate::entities::common::quad_pick_tris(&[ + [x0, y0, z], + [x1, y0, z], + [x1, y1, z], + [x0, y1, z], + ]), object: TruckObject::Lines(pts), snap_pts: vec![(center, SnapHint::Center)], tangent_geoms: vec![], diff --git a/src/entities/raster_image.rs b/src/entities/raster_image.rs index d12a7e1c7..377353338 100644 --- a/src/entities/raster_image.rs +++ b/src/entities/raster_image.rs @@ -136,7 +136,9 @@ impl TruckConvertible for RasterImage { path.is_empty() || crate::scene::model::image_model::resolve_image(path).is_some(); if resolvable { return Some(TruckEntity { - pick_tris: Vec::new(), + // Interior pick surface: the image selects on a click anywhere + // inside its frame, not just on the border. + pick_tris: crate::entities::common::quad_pick_tris(&corners), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], @@ -223,7 +225,7 @@ impl TruckConvertible for RasterImage { } Some(TruckEntity { - pick_tris: Vec::new(), + pick_tris: crate::entities::common::quad_pick_tris(&corners), object: TruckObject::Text(groups), snap_pts: vec![], tangent_geoms: vec![], @@ -457,7 +459,9 @@ impl TruckConvertible for Wipeout { }; Some(TruckEntity { - pick_tris: Vec::new(), + // Interior pick surface — a wipeout reads as a solid patch, so a + // click anywhere on it should select it. + pick_tris: crate::entities::common::quad_pick_tris(&corners), object: TruckObject::Lines(pts), snap_pts: vec![], tangent_geoms: vec![], diff --git a/src/entities/underlay.rs b/src/entities/underlay.rs index b3b6ce7f1..9c78ac896 100644 --- a/src/entities/underlay.rs +++ b/src/entities/underlay.rs @@ -52,8 +52,12 @@ impl TruckConvertible for Underlay { } // Insertion grip. let key: Vec<[f64; 3]> = pts.clone(); + // Interior pick surface over the clip polygon so the underlay + // selects on a click anywhere inside, not just on its outline. + let ring: Vec<[f64; 3]> = world_verts.iter().map(|v| [v.x, v.y, v.z]).collect(); + let pick_tris = crate::entities::mesh::triangulate_planar(&ring); Some(TruckEntity { - pick_tris: Vec::new(), + pick_tris, 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/scene/convert/tess.rs b/src/scene/convert/tess.rs index 749c13b90..9204562de 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -624,9 +624,64 @@ pub(crate) fn tessellate_entity( line_weight_px, ); } - // Drawing-view borders are non-plotting aids: nothing is drawn. - if let EntityType::ViewBorder(_) = e { - return vec![]; + // Drawing-view borders are non-plotting aids: nothing is drawn normally, + // but the view must still be click-selectable anywhere inside its + // rectangle — emit an invisible pick-only wire carrying the border rect as + // its interior pick surface. When selected, the rect outline is drawn so + // the selection has visible feedback. + if let EntityType::ViewBorder(b) = e { + let (x0, y0) = (b.min[0], b.min[1]); + let (x1, y1) = (b.max[0], b.max[1]); + if !(x1 > x0 && y1 > y0) { + return vec![]; + } + let (pick_tris, pick_tris_low) = + super::tessellate::points_to_ds(crate::entities::common::quad_pick_tris(&[ + [x0, y0, 0.0], + [x1, y0, 0.0], + [x1, y1, 0.0], + [x0, y1, 0.0], + ])); + let points: Vec<[f32; 3]> = if sel { + vec![ + [x0 as f32, y0 as f32, 0.0], + [x1 as f32, y0 as f32, 0.0], + [x1 as f32, y1 as f32, 0.0], + [x0 as f32, y1 as f32, 0.0], + [x0 as f32, y0 as f32, 0.0], + ] + } else { + Vec::new() + }; + return vec![WireModel { + taper_widths: Vec::new(), + world_width: 0.0, + fill_is_3d: false, + pick_tris, + pick_tris_low, + dash_from_start: false, + dash_align_end: None, + text_verts: Vec::new(), + name: h.value().to_string(), + points, + points_low: Vec::new(), + color: WireModel::SELECTED, + selected: sel, + aci: 0, + pattern_length: 0.0, + pattern: [0.0; 8], + line_weight_px: 1.0, + snap_pts: vec![], + tangent_geoms: vec![], + key_vertices: vec![ + [x0, y0, 0.0], + [x1, y1, 0.0], + ], + aabb: [x0 as f32, y0 as f32, x1 as f32, y1 as f32], + plinegen: true, + fill_tris: vec![], + fill_tris_low: Vec::new(), + }]; } // ── Dimension baked-block fast path ───────────────────────────────────── diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 4a1c45fc3..ef3a56e1d 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -1392,12 +1392,41 @@ pub fn tessellate( // f64 for the WireModel's double-single-era snap buffer. let snap_pts: Vec<(glam::DVec3, SnapHint)> = snap_pts.into_iter().map(|(p, h)| (p.as_dvec3(), h)).collect(); + // A paper-space viewport is a window, not a wireframe: give it an interior + // pick surface so a click anywhere inside the frame selects it. Ranked + // below edge and fill hits, so content drawn inside still wins the click. + // The sheet ("overall") viewport is the layout's own invisible camera + // frame covering the whole page — never pickable, or it would swallow + // every click over the real viewports beneath it. It is identified by the + // Layout object's viewport link (authoritative — DWG files carry id = 0 + // and this file class centres the sheet viewport off-origin, so neither + // the id nor the geometry heuristic alone is reliable), with + // `is_content_viewport` as the fallback classifier. + let is_sheet_vp = |vp: &acadrust::entities::Viewport| { + let h = vp.common.handle; + document.objects.values().any(|obj| { + matches!(obj, acadrust::objects::ObjectType::Layout(l) if l.viewport == h) + }) || !crate::scene::Scene::is_content_viewport(vp) + }; + let (pick_tris, pick_tris_low) = match entity { + EntityType::Viewport(vp) if !is_sheet_vp(vp) => { + let (cx, cy, cz) = (vp.center.x, vp.center.y, vp.center.z); + let (hw, hh) = (vp.width / 2.0, vp.height / 2.0); + points_to_ds(crate::entities::common::quad_pick_tris(&[ + [cx - hw, cy - hh, cz], + [cx + hw, cy - hh, cz], + [cx + hw, cy + hh, cz], + [cx - hw, cy + hh, cz], + ])) + } + _ => (Vec::new(), Vec::new()), + }; vec![WireModel { taper_widths: Vec::new(), world_width: 0.0, fill_is_3d: false, - pick_tris: Vec::new(), - pick_tris_low: Vec::new(), + pick_tris, + pick_tris_low, dash_from_start: false, dash_align_end: None, text_verts: Vec::new(),