forked from 1b-agent/opencadstudio-upstream
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:
1 parent
43bd5aa427
commit
33640e050d
4 files changed
+163
-1
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
Reference in new issue
Block a user