fix(3d): box/lasso-select 3D solids and refresh them after a transform
Box, window, crossing and lasso selection only tested wires and hatches, so 3D solids (which render as meshes) could never be selected that way. Add mesh_box_hit / mesh_poly_hit that project the solid mesh and test it against the box / lasso (window = all vertices inside, crossing = any vertex inside or the region sitting inside the solid), and wire them into the selection paths. Also re-tessellate solids after a move/rotate/scale/mirror/copy: the entity moved but its cached mesh didn't, so the solid stayed drawn at its old position. Rebuild the mesh caches when a transformed/copied handle is an ACIS volume. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c5969b4776
commit
d5f1348f1b
4 changed files with 167 additions and 0 deletions
|
|
@ -77,6 +77,13 @@ impl OpenCADStudio {
|
||||||
let label = self.history_label_from_active_cmd(i, "MOVE");
|
let label = self.history_label_from_active_cmd(i, "MOVE");
|
||||||
self.push_undo_snapshot(i, label);
|
self.push_undo_snapshot(i, label);
|
||||||
self.tabs[i].scene.transform_entities(&handles, &transform);
|
self.tabs[i].scene.transform_entities(&handles, &transform);
|
||||||
|
// ACIS solids render from a cached mesh, so a move/rotate/
|
||||||
|
// scale/mirror needs the mesh re-tessellated from the now-moved
|
||||||
|
// body — wire re-tessellation alone leaves the solid drawn at
|
||||||
|
// its old spot. (#135)
|
||||||
|
if self.tabs[i].scene.any_solid(&handles) {
|
||||||
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.clear_preview_wire();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
|
|
@ -88,6 +95,9 @@ impl OpenCADStudio {
|
||||||
let label = self.history_label_from_active_cmd(i, "COPY");
|
let label = self.history_label_from_active_cmd(i, "COPY");
|
||||||
self.push_undo_snapshot(i, label);
|
self.push_undo_snapshot(i, label);
|
||||||
let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
|
let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
|
||||||
|
if self.tabs[i].scene.any_solid(&new_handles) {
|
||||||
|
self.tabs[i].scene.populate_meshes_from_document();
|
||||||
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
for h in new_handles {
|
for h in new_handles {
|
||||||
|
|
|
||||||
|
|
@ -3739,6 +3739,9 @@ impl OpenCADStudio {
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
));
|
));
|
||||||
|
handles.extend(
|
||||||
|
self.tabs[i].scene.mesh_box_hit(a, p, crossing, vp_mat, bounds),
|
||||||
|
);
|
||||||
// Box/lasso accumulates like individual picks
|
// Box/lasso accumulates like individual picks
|
||||||
// (issue #83): a plain box adds to the current
|
// (issue #83): a plain box adds to the current
|
||||||
// selection, Shift+box removes the boxed
|
// selection, Shift+box removes the boxed
|
||||||
|
|
@ -3790,6 +3793,9 @@ impl OpenCADStudio {
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
));
|
));
|
||||||
|
handles.extend(
|
||||||
|
self.tabs[i].scene.mesh_poly_hit(&poly_pts, crossing, vp_mat, bounds),
|
||||||
|
);
|
||||||
// Selection filter: keep only allowed types.
|
// Selection filter: keep only allowed types.
|
||||||
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
||||||
// Accumulate like the box path (issue #83): plain
|
// Accumulate like the box path (issue #83): plain
|
||||||
|
|
@ -3926,6 +3932,9 @@ impl OpenCADStudio {
|
||||||
vp_mat,
|
vp_mat,
|
||||||
bounds,
|
bounds,
|
||||||
));
|
));
|
||||||
|
handles.extend(
|
||||||
|
self.tabs[i].scene.mesh_box_hit(a, p, crossing, vp_mat, bounds),
|
||||||
|
);
|
||||||
// Selection filter: keep only allowed types.
|
// Selection filter: keep only allowed types.
|
||||||
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
||||||
// Accumulate (issue #83): a plain box adds to the
|
// Accumulate (issue #83): a plain box adds to the
|
||||||
|
|
|
||||||
|
|
@ -2264,6 +2264,52 @@ impl Scene {
|
||||||
pick::hit_test::mesh_click_hit(cursor, iter, view_proj, bounds)
|
pick::hit_test::mesh_click_hit(cursor, iter, view_proj, bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// True when any handle resolves to an ACIS volume entity (3D solid /
|
||||||
|
/// region / body / surface) — i.e. one whose render geometry is a cached
|
||||||
|
/// mesh that must be re-tessellated after an edit.
|
||||||
|
pub fn any_solid(&self, handles: &[Handle]) -> bool {
|
||||||
|
handles.iter().any(|&h| {
|
||||||
|
matches!(
|
||||||
|
self.document.get_entity(h),
|
||||||
|
Some(EntityType::Solid3D(_))
|
||||||
|
| Some(EntityType::Region(_))
|
||||||
|
| Some(EntityType::Body(_))
|
||||||
|
| Some(EntityType::Surface(_))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top-level solid handles caught by a rectangular selection box.
|
||||||
|
pub fn mesh_box_hit(
|
||||||
|
&self,
|
||||||
|
a: iced::Point,
|
||||||
|
b: iced::Point,
|
||||||
|
crossing: bool,
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
) -> Vec<Handle> {
|
||||||
|
let iter = self
|
||||||
|
.meshes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(h, set)| set.lods.first().map(|m| (*h, m)));
|
||||||
|
pick::hit_test::mesh_box_hit(a, b, crossing, iter, view_proj, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top-level solid handles caught by a lasso polygon.
|
||||||
|
pub fn mesh_poly_hit(
|
||||||
|
&self,
|
||||||
|
poly: &[iced::Point],
|
||||||
|
crossing: bool,
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
bounds: iced::Rectangle,
|
||||||
|
) -> Vec<Handle> {
|
||||||
|
let iter = self
|
||||||
|
.meshes
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(h, set)| set.lods.first().map(|m| (*h, m)));
|
||||||
|
pick::hit_test::mesh_poly_hit(poly, crossing, iter, view_proj, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
/// Tessellate all non-invisible entities owned by `block_handle`.
|
/// Tessellate all non-invisible entities owned by `block_handle`.
|
||||||
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
|
fn wires_for_block(&self, block_handle: Handle) -> Vec<WireModel> {
|
||||||
// Default culling is driven by the live `Scene::camera`. Multi-tile
|
// Default culling is driven by the live `Scene::camera`. Multi-tile
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,108 @@ pub fn mesh_click_hit<'a>(
|
||||||
best.map(|(_, h)| h)
|
best.map(|(_, h)| h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Project a mesh's vertices to screen space.
|
||||||
|
fn project_mesh_verts(mesh: &MeshModel, view_proj: Mat4, bounds: Rectangle) -> Vec<Point> {
|
||||||
|
mesh.verts
|
||||||
|
.iter()
|
||||||
|
.map(|w| {
|
||||||
|
let ndc = view_proj.project_point3(Vec3::new(w[0], w[1], w[2]));
|
||||||
|
Point::new(
|
||||||
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when any of `mesh`'s projected triangles contains one of `pts`
|
||||||
|
/// (used so a crossing box / lasso entirely inside a solid still selects it).
|
||||||
|
fn mesh_covers_any(proj: &[Point], indices: &[u32], pts: &[Point]) -> bool {
|
||||||
|
let mut t = 0;
|
||||||
|
while t + 2 < indices.len() {
|
||||||
|
let tri = [
|
||||||
|
proj[indices[t] as usize],
|
||||||
|
proj[indices[t + 1] as usize],
|
||||||
|
proj[indices[t + 2] as usize],
|
||||||
|
];
|
||||||
|
t += 3;
|
||||||
|
if pts.iter().any(|p| point_in_polygon(*p, &tri)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Solid (mesh) handles caught by a rectangular selection box. Window mode
|
||||||
|
/// (`crossing == false`) needs every projected vertex inside the box;
|
||||||
|
/// crossing mode needs any vertex inside, or the box to sit inside the solid.
|
||||||
|
pub fn mesh_box_hit<'a>(
|
||||||
|
a: Point,
|
||||||
|
b: Point,
|
||||||
|
crossing: bool,
|
||||||
|
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
|
||||||
|
view_proj: Mat4,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> Vec<Handle> {
|
||||||
|
let (min_x, max_x) = (a.x.min(b.x), a.x.max(b.x));
|
||||||
|
let (min_y, max_y) = (a.y.min(b.y), a.y.max(b.y));
|
||||||
|
let in_box = |p: &Point| p.x >= min_x && p.x <= max_x && p.y >= min_y && p.y <= max_y;
|
||||||
|
let corners = [
|
||||||
|
Point::new(min_x, min_y),
|
||||||
|
Point::new(max_x, min_y),
|
||||||
|
Point::new(max_x, max_y),
|
||||||
|
Point::new(min_x, max_y),
|
||||||
|
];
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (h, mesh) in meshes {
|
||||||
|
let proj = project_mesh_verts(mesh, view_proj, bounds);
|
||||||
|
if proj.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let hit = if crossing {
|
||||||
|
proj.iter().any(in_box) || mesh_covers_any(&proj, &mesh.indices, &corners)
|
||||||
|
} else {
|
||||||
|
proj.iter().all(in_box)
|
||||||
|
};
|
||||||
|
if hit {
|
||||||
|
out.push(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Solid (mesh) handles caught by a lasso polygon. Window mode needs every
|
||||||
|
/// projected vertex inside the lasso; crossing mode needs any vertex inside,
|
||||||
|
/// or the lasso to sit inside the solid.
|
||||||
|
pub fn mesh_poly_hit<'a>(
|
||||||
|
poly: &[Point],
|
||||||
|
crossing: bool,
|
||||||
|
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
|
||||||
|
view_proj: Mat4,
|
||||||
|
bounds: Rectangle,
|
||||||
|
) -> Vec<Handle> {
|
||||||
|
if poly.len() < 3 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (h, mesh) in meshes {
|
||||||
|
let proj = project_mesh_verts(mesh, view_proj, bounds);
|
||||||
|
if proj.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let hit = if crossing {
|
||||||
|
proj.iter().any(|p| point_in_polygon(*p, poly))
|
||||||
|
|| mesh_covers_any(&proj, &mesh.indices, poly)
|
||||||
|
} else {
|
||||||
|
proj.iter().all(|p| point_in_polygon(*p, poly))
|
||||||
|
};
|
||||||
|
if hit {
|
||||||
|
out.push(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
// ── Box / window selection ────────────────────────────────────────────────
|
// ── Box / window selection ────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Return the names of wires selected by a completed rectangular selection box.
|
/// Return the names of wires selected by a completed rectangular selection box.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue