feat(solid history): Ctrl+click picks a composite's solid; show history draws them

Ctrl+click on a composite selects it with the solid under the cursor as
the one its palette and grips edit: the solid whose own surface passes
through the picked point. With its history shown, a composite draws the
edges of the solids its booleans joined, each where it sits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-07 12:51:32 +03:00
1 parent 43bd5aa427
commit 33640e050d
4 files changed
+163 -1

No files matched your search

+31 -1
View File
@@ -656,7 +656,7 @@ impl OpenCADStudio {
}
}
fn grip_edit_for_hit(
pub(in crate::app) fn grip_edit_for_hit(
&mut self,
i: usize,
handle: Handle,
@@ -3738,6 +3738,36 @@ impl OpenCADStudio {
}
}
// Ctrl+click on a composite solid picks the solid of it under the
// cursor — the one the Properties palette and grips then edit.
if self.ctrl_down
&& self.tabs[i].active_cmd.is_none()
&& self.tabs[i].scene.current_layout == "Model"
{
let (view_rot, eye) = {
let cam = self.tabs[i].scene.camera.borrow();
(cam.view_proj_rte(bounds), cam.eye())
};
let hit = self.tabs[i].scene.solid_click_hit_point(p, view_rot, eye, bounds);
if let Some((handle, _)) = hit.filter(|(handle, point)| {
crate::scene::model::solid_history::select_operand_at(
&self.tabs[i].scene.document,
*handle,
*point,
)
}) {
self.tabs[i]
.scene
.selection
.borrow_mut()
.clear_left_selection_gesture();
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.refresh_properties();
return Task::none();
}
}
// A command step that takes a point or a pick commits on the press,
// so the cursor moving before the button comes up cannot move it.
// Selection steps and drag-fence picks still wait for the release.
+32
View File
@@ -162,6 +162,11 @@ pub(crate) fn tessellate_entity(
}
wires.extend(construction);
}
for points in model::solid_history::composite_history_edges(document, handle) {
let selected = selected.contains(&handle);
let color = [1.0, 0.25, 0.25, 1.0];
wires.push(WireModel::solid_f64(handle.value().to_string(), points, color, selected));
}
}
wires
}
@@ -10557,6 +10562,33 @@ impl Scene {
)
}
/// The solid under the cursor and where the pick ray meets it.
pub fn solid_click_hit_point(
&self,
cursor: iced::Point,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Option<(Handle, glam::DVec3)> {
let meshes = self.interaction_meshes_arc();
pick::hit_test::mesh_click_handle_point(
cursor,
meshes.iter().filter_map(|set| {
let handle = set.entity_handle()?;
let mesh = set.geometry_lods().first()?;
Some((
handle,
mesh,
set.instance_transform,
mesh_interaction_aabb(set)?,
))
}),
view_rot,
eye,
bounds,
)
}
pub fn solid_planar_face_normal_at(
&mut self,
handle: Handle,
+89
View File
@@ -135,6 +135,95 @@ pub fn select_operand(document: &codec::CadDocument, handle: codec::Handle, labe
}
}
/// Pick the operand of a composite whose own surface passes through
/// `point` — where a click landed on the composite — for editing. False when
/// the solid has no boolean step or no operand surface is there.
///
/// The point comes from the displayed mesh, so it sits off the true surface
/// by the tessellation's chord; the search widens until some operand's
/// surface is in reach, and the first in reach wins.
pub fn select_operand_at(
document: &codec::CadDocument,
handle: codec::Handle,
point: glam::DVec3,
) -> bool {
let operands = placed_operands(document, handle);
let size = operands
.iter()
.filter_map(|(_, body)| kernel::brep::body_bounds(body))
.map(|bounds| {
(0..3)
.map(|axis| bounds.max[axis] - bounds.min[axis])
.fold(0.0_f64, f64::max)
})
.fold(0.0_f64, f64::max);
for reach in [1e-6, 1e-4, 1e-3, 1e-2] {
let found = operands.iter().find(|(_, body)| {
kernel::brep::contains_point(body, point.to_array(), reach * size)
== kernel::brep::Containment::OnBoundary
});
if let Some((id, _)) = found {
EDIT_OPERANDS.with(|operands| operands.borrow_mut().insert(handle, *id));
return true;
}
}
false
}
/// The solids a composite's booleans joined, each rebuilt where it sits in
/// the composite, with its node id. Empty for a solid with no boolean step.
fn placed_operands(
document: &codec::CadDocument,
handle: codec::Handle,
) -> Vec<(i32, kernel::brep::Body)> {
let Some(tree) = document
.solid_history_tree(handle)
.filter(|tree| tree.has_boolean())
else {
return Vec::new();
};
tree.leaves()
.into_iter()
.filter_map(|operation| {
let id = operation.base()?.node_id();
let frame = ancestors_frame(&tree, id)?;
let mut placed = operation.clone();
let base = placed.base_mut()?;
base.transform = (frame * matrix(base.transform)?).to_cols_array();
Some((id, kernel::acis::rebuild_body(&placed).ok()?))
})
.collect()
}
/// With its history shown, a composite draws the solids its booleans joined,
/// each where it sits: their edges, as world polylines.
pub fn composite_history_edges(
document: &codec::CadDocument,
handle: codec::Handle,
) -> Vec<Vec<[f64; 3]>> {
let Some((_, object_show_history, show_history_mode)) = history_flags(document, handle) else {
return Vec::new();
};
if !displayed_history_state(object_show_history, show_history_mode).0 {
return Vec::new();
}
placed_operands(document, handle)
.iter()
.flat_map(|(_, body)| {
kernel::brep::mesh::tessellate_wireframe(
body,
kernel::brep::mesh::TessellationTolerance::new(
kernel::tessellation::DEFAULT_ANGLE,
1e-9,
),
)
.edges
})
.map(|edge| edge.positions)
.filter(|positions| positions.len() >= 2)
.collect()
}
/// The composite's history and the node id of the operand being edited: the
/// one last chosen, or the first.
fn selected_operand(
+11
View File
@@ -904,6 +904,17 @@ pub(crate) fn mesh_click_point<'a>(
mesh_click_result(cursor, meshes, view_rot, eye, bounds).map(|(_, _, point)| point)
}
/// The nearest mesh under the cursor and where the pick ray meets it.
pub(crate) fn mesh_click_handle_point<'a>(
cursor: Point,
meshes: impl Iterator<Item = MeshPickItem<'a>>,
view_rot: Mat4,
eye: glam::DVec3,
bounds: Rectangle,
) -> Option<(Handle, glam::DVec3)> {
mesh_click_result(cursor, meshes, view_rot, eye, bounds).map(|(_, handle, point)| (handle, point))
}
fn mesh_click_result<'a>(
cursor: Point,
meshes: impl Iterator<Item = MeshPickItem<'a>>,