Complete PRESSPULL selection, face editing and extrusion history

This commit is contained in:
ramox81 committed 2026-09-04 15:31:30 +03:00
1 parent 509e4b7e7f
commit 9279384e9a
16 files changed
+1284 -247

No files matched your search

+5 -55
View File
@@ -3792,61 +3792,11 @@ impl OpenCADStudio {
self.refresh_properties();
}
CmdResult::PresspullEntity {
handle,
pick,
distance,
drag,
color,
} => {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_))
) {
let task = self.solid_face_presspull(handle, pick, distance, drag);
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
return task;
}
if self.reject_locked_edit(i, handle) {
self.tabs[i].active_cmd = None;
return Task::none();
}
use crate::modules::insert::solid3d_cmds::empty_solid3d;
use crate::scene::model::{solid_history, sweep_model};
let result = self.tabs[i].scene.document.get_entity(handle).and_then(|entity| {
let distance = match drag {
Some(point) => sweep_model::projected_drag(entity, pick, point)?,
None => distance,
};
sweep_model::extruded(entity, distance)
});
if let Some(solid) = result {
let pending = self.begin_undo(i, "PRESSPULL", 1, true);
let history = solid_history::brep_op(&solid);
let created = self.add_solid_model(empty_solid3d(), solid, history);
let _ = color;
if !created.is_null() {
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::t!("PRESSPULL: solid created.").as_ref());
if let Some(delta) = pending {
self.commit_undo_delta(i, delta);
}
}
} else {
self.command_line.push_error(
crate::t!("PRESSPULL: select a closed profile or planar solid face.")
.as_ref(),
);
}
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire();
self.restore_pre_cmd_tangent();
CmdResult::PresspullPick { handle, point, offset, multiple: _ } => {
self.presspull_pick(handle, point, offset);
}
CmdResult::PresspullApply { targets, distance, color: _ } => {
self.presspull_apply(targets, distance);
}
// ── REVOLVE ────────────────────────────────────────────────────
+2 -14
View File
@@ -771,22 +771,10 @@ impl OpenCADStudio {
"PRESSPULL" => {
use crate::modules::insert::solid3d_cmds::PresspullCommand;
let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer);
let mut command = PresspullCommand::new(color);
if selected.len() == 1
&& !matches!(selected[0].1, acadrust::EntityType::Solid3D(_))
{
if let Some(curve) = crate::entities::curve::entity_curve(selected[0].1) {
command.set_entity_pick_direction(
curve.plane.normal().map(glam::DVec3::from_array),
);
command.on_entity_pick(
selected[0].0,
glam::DVec3::from_array(curve.plane.origin),
);
}
}
command.set_isolines(self.tabs[i].scene.document.header.isolines.max(0) as usize);
command.set_preselection(self.presspull_preselection());
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
+1
View File
@@ -17,6 +17,7 @@ mod mtext_editor;
#[cfg(not(target_arch = "wasm32"))]
pub mod plugin_host;
mod properties;
mod presspull_ops;
mod recent;
pub(crate) mod settings;
mod shortcuts;
+7 -54
View File
@@ -119,6 +119,12 @@ impl super::OpenCADStudio {
entity.history_handle = None;
entity.set_sat_document(&document);
let Some(display) = self.tabs[i].scene.prepare_solid_model_display(handle, &result)
.filter(|display| display.0.complete)
else {
self.command_line.push_error(crate::t!("The result could not be displayed completely. The original solid was retained.").as_ref());
return false;
};
self.push_undo_snapshot(i, label);
self.tabs[i].scene.delete_solid_history(handle);
if !self.tabs[i]
@@ -131,7 +137,7 @@ impl super::OpenCADStudio {
}
let history = solid_history::brep_op(&result);
self.tabs[i].scene.create_solid_history(handle, history);
self.tabs[i].scene.register_solid_model(handle, result);
self.tabs[i].scene.register_prepared_solid_model(handle, result, display);
self.tabs[i].scene.deselect_all();
self.tabs[i].scene.select_entity(handle, false);
self.tabs[i].dirty = true;
@@ -189,59 +195,6 @@ impl super::OpenCADStudio {
Task::none()
}
pub(super) fn solid_face_presspull(
&mut self,
handle: Handle,
pick: glam::DVec3,
distance: f64,
drag: Option<glam::DVec3>,
) -> Task<Message> {
let i = self.active_tab;
if self.reject_locked_edit(i, handle) {
return Task::none();
}
self.tabs[i].scene.restore_solid_models(&[handle]);
let Some(body) = self.tabs[i].scene.solid_models.get(&handle).cloned() else {
self.command_line
.push_error(crate::t!("The solid geometry could not be restored.").as_ref());
return Task::none();
};
let Some(face) = solid_model::nearest_planar_face(&body, pick.to_array()) else {
self.command_line
.push_error(crate::t!("Select a planar solid face.").as_ref());
return Task::none();
};
let distance = match drag {
Some(point) => {
let Some(normal) = solid_model::planar_face_normal(&body, face) else {
self.command_line
.push_error(crate::t!("Select a planar solid face.").as_ref());
return Task::none();
};
(point - pick).dot(glam::DVec3::from_array(normal))
}
None => distance,
};
if !distance.is_finite() || distance.abs() <= 1e-6 {
self.command_line.push_error(
crate::t!("PRESSPULL: drag along the selected face normal.").as_ref(),
);
return Task::none();
}
let Some(result) = cadkernel::brep::presspull(&body, face, distance) else {
self.command_line.push_error(
crate::t!("PRESSPULL: the face move would collapse or invalidate the solid.")
.as_ref(),
);
return Task::none();
};
if self.replace_solid_body(handle, result, "PRESSPULL") {
self.command_line
.push_output(crate::t!("PRESSPULL: solid updated.").as_ref());
}
Task::none()
}
/// Run a boolean over the selected solids in selection order.
pub(super) fn solid_boolean(&mut self, op: BoolOp) -> Task<Message> {
let i = self.active_tab;
+244
View File
@@ -0,0 +1,244 @@
//! PRESSPULL scene integration. Resolve first; build and validate the complete
//! batch before recording undo or changing any persistent entity.
use acadrust::entities::{AcisData, EmbeddedEntity, Solid3D, Surface, SurfaceKind, Wire};
use acadrust::objects::{DynamicBlockData, ObjectType, SolidHistoryNodeBase, SolidHistoryOperation, SolidHistorySweep};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use cadkernel::brep::{self, Body, PlanarFaceProfile, PresspullMode};
use glam::DVec3;
use crate::scene::model::{solid_history, sweep_model};
use crate::scene::model::mesh_model::MeshLodSet;
use crate::scene::model::presspull_model::{self, PresspullTarget, PresspullTargetKind};
struct PreparedPresspull {
owner: Option<Handle>,
entity: EntityType,
body: Body,
history: SolidHistoryOperation,
display: (MeshLodSet, Vec<Wire>, [f64; 3]),
}
fn extrusion_history(entity: &EntityType, direction: DVec3, anchor: DVec3) -> Option<SolidHistoryOperation> {
if let EntityType::Region(region) = entity {
let mut base = SolidHistoryNodeBase::new(1);
base.transform = glam::DMat4::IDENTITY.to_cols_array();
return Some(SolidHistoryOperation::Extrusion(SolidHistorySweep {
base,
operation_major: 1,
sweep_entity: Some(EmbeddedEntity::Region(region.clone())),
direction: Vector3::new(direction.x, direction.y, direction.z),
end_draft_distance: direction.length(),
scale_factor: 1.0,
sweep_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(),
reference_point: Vector3::new(anchor.x, anchor.y, anchor.z),
..SolidHistorySweep::default()
}));
}
sweep_model::extrusion_history(entity, None, direction.to_array(), 0.0, anchor.to_array())
}
impl super::OpenCADStudio {
fn restore_presspull_bodies(&mut self) {
let scene = &mut self.tabs[self.active_tab].scene;
let handles = scene.document.entities().filter_map(|entity| {
matches!(entity, EntityType::Solid3D(_)).then_some(entity.common().handle)
}).collect::<Vec<_>>();
scene.restore_solid_models(&handles);
}
pub(super) fn presspull_preselection(&mut self) -> Vec<PresspullTarget> {
self.restore_presspull_bodies();
let tab = &self.tabs[self.active_tab];
let plane = tab.ucs_xform().working_plane();
tab.scene.selected_handles_in_order().into_iter().filter_map(|handle| {
let entity = tab.scene.document.get_entity(handle)?;
let (profile_plane, loops, _) = presspull_model::profile_geometry(entity)?;
let first = loops.first()?.first()?.point_at(0.0);
let anchor = DVec3::from_array(profile_plane.point_at(first));
presspull_model::resolve_target(&tab.scene, Some(handle), anchor, plane, false).ok()
}).collect()
}
pub(super) fn presspull_pick(&mut self, handle: Option<Handle>, point: DVec3, offset: bool) {
self.restore_presspull_bodies();
let i = self.active_tab;
let target = presspull_model::resolve_target(
&self.tabs[i].scene, handle, point, self.tabs[i].ucs_xform().working_plane(), offset,
);
match target {
Ok(target) => {
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.on_presspull_target(target);
}
self.tabs[i].scene.set_hover_highlight(None);
}
Err(error) => self.command_line.push_error(&error),
}
self.refresh_presspull_prompt();
}
fn refresh_presspull_prompt(&mut self) {
if let Some(command) = self.tabs[self.active_tab].active_cmd.as_ref() {
self.command_line.push_info(&command.prompt());
self.command_line.set_step_options(command.options());
}
self.sync_dyn_fields();
}
fn prepare_presspull(&self, targets: &[PresspullTarget], distance: f64) -> Result<Vec<PreparedPresspull>, String> {
if targets.is_empty() || !distance.is_finite() || distance.abs() <= 1e-9 {
return Err("PRESSPULL: enter a finite non-zero height.".into());
}
let scene = &self.tabs[self.active_tab].scene;
let mut results: Vec<(Option<Handle>, EntityType, Body, SolidHistoryOperation)> = Vec::new();
for target in targets {
let owner = match &target.kind {
PresspullTargetKind::Profile { source, entity, owner } => {
if let Some(source) = source {
if scene.is_layer_locked(*source) || scene.document.get_entity(*source) != Some(entity) {
return Err("PRESSPULL: a selected source changed. Select it again.".into());
}
}
*owner
}
PresspullTargetKind::Face { handle, .. } => Some(*handle),
};
if owner.is_some_and(|handle| scene.is_layer_locked(handle)) {
return Err("PRESSPULL: the solid is on a locked layer.".into());
}
let previous = owner.and_then(|handle| results.iter().position(|result| result.0 == Some(handle)));
let current = previous.map(|index| &results[index].2)
.or_else(|| owner.and_then(|handle| scene.solid_models.get(&handle)));
let (body, history, mut entity) = match &target.kind {
PresspullTargetKind::Profile { entity, .. } => {
let (plane, loops, closed) = presspull_model::profile_geometry(entity)
.ok_or("PRESSPULL: the profile could not be recovered exactly.")?;
if owner.is_some() {
let current = current.ok_or("PRESSPULL: the original solid could not be restored.")?;
if !closed {
return Err("PRESSPULL: an attached region must be closed.".into());
}
let profile = PlanarFaceProfile { plane, loops, outward: target.direction.to_array() };
let body = brep::presspull_region(current, &profile, distance)
.ok_or("PRESSPULL: the region could not be added to or cut from the solid.")?;
let history = solid_history::brep_op(&body);
(body, history, scene.document.get_entity(owner.unwrap()).cloned()
.ok_or("PRESSPULL: the original solid no longer exists.")?)
} else {
let direction = target.direction * distance;
let body = presspull_model::extrusion_body(entity, direction.to_array())
.ok_or("PRESSPULL: the profile could not be extruded with this height.")?;
let history = extrusion_history(entity, direction, target.anchor)
.ok_or("PRESSPULL: the editable extrusion profile could not be retained.")?;
let entity = if closed {
EntityType::Solid3D(Solid3D::new())
} else {
let mut surface = Surface::new(SurfaceKind::Extruded);
if let SolidHistoryOperation::Extrusion(record) = &history {
surface.surface_data = solid_history::extrusion_surface_data(record)
.ok_or("PRESSPULL: the surface parameters could not be retained.")?;
}
EntityType::Surface(surface)
};
(body, history, entity)
}
}
PresspullTargetKind::Face { face, offset, .. } => {
let current = current.ok_or("PRESSPULL: the original solid could not be restored.")?;
// Earlier targets in the same batch may have rebuilt face keys.
let face = if previous.is_some() {
brep::planar_face_at_point(current, target.anchor.to_array(), 1e-6)
.ok_or("PRESSPULL: an earlier selected region changed this face.")?
} else { *face };
let body = brep::presspull_face(current, face, distance,
if *offset { PresspullMode::Offset } else { PresspullMode::Extrude })
.ok_or("PRESSPULL: the requested face operation would invalidate the solid.")?;
let history = solid_history::brep_op(&body);
(body, history, scene.document.get_entity(owner.unwrap()).cloned()
.ok_or("PRESSPULL: the original solid no longer exists.")?)
}
};
let sat = crate::scene::convert::acis_export::solid_to_sat(&body)
.ok_or("PRESSPULL: the result could not be encoded. The original objects were retained.")?;
if owner.is_none() { entity.common_mut().plotstyle_flags = 2; }
match &mut entity {
EntityType::Solid3D(solid) => {
solid.history_handle = None;
solid.silhouettes.clear();
solid.set_sat_document(&sat);
}
EntityType::Surface(surface) => {
surface.history_handle = None;
surface.silhouettes.clear();
surface.acis_data = AcisData::from_sat(&sat.to_sat_string());
}
_ => return Err("PRESSPULL: unsupported result object.".into()),
}
let result = (owner, entity, body, history);
if let Some(index) = previous { results[index] = result; } else { results.push(result); }
}
results.into_iter().map(|(owner, entity, body, history)| {
let display = scene.prepare_solid_model_display(owner.unwrap_or(Handle::NULL), &body)
.filter(|display| display.0.complete && display.0.lods.iter().any(|mesh| !mesh.indices.is_empty()))
.ok_or("PRESSPULL: the complete result could not be displayed. The original objects were retained.")?;
Ok(PreparedPresspull { owner, entity, body, history, display })
}).collect()
}
pub(super) fn presspull_apply(&mut self, targets: Vec<PresspullTarget>, distance: f64) {
let i = self.active_tab;
let prepared = match self.prepare_presspull(&targets, distance) {
Ok(prepared) => prepared,
Err(error) => {
self.command_line.push_error(&error);
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.on_presspull_applied(false);
}
self.refresh_presspull_prompt();
return;
}
};
let pending = self.begin_undo(i, "PRESSPULL", prepared.len(), true);
let mut handles = Vec::new();
// All fallible geometry, persistence and display work finished above.
// No source curve is deleted by PRESSPULL.
for result in prepared {
let handle = if let Some(handle) = result.owner {
self.tabs[i].scene.delete_solid_history(handle);
self.tabs[i].scene.update_entity(result.entity);
handle
} else {
self.commit_entity_handle(result.entity).expect("validated solid/surface insertion")
};
self.tabs[i].scene.create_solid_history(handle, result.history);
// PRESSPULL keeps editable construction parameters without enabling
// recording of subsequent solid operations by default.
if result.owner.is_none() {
let document = &mut self.tabs[i].scene.document;
if let Some(graph) = document.solid_history_graph(handle) {
if let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&graph.root) {
if let DynamicBlockData::SolidHistory(history) = &mut object.data {
history.record_history = false;
}
}
}
}
self.tabs[i].scene.register_prepared_solid_model(handle, result.body, result.display);
handles.push(handle);
}
self.tabs[i].scene.deselect_all();
for handle in &handles { self.tabs[i].scene.select_entity(*handle, false); }
self.tabs[i].dirty = true;
if let Some(pending) = pending { self.commit_undo_delta(i, pending); }
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].snap_result = None;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.on_presspull_applied(true);
}
self.command_line.push_output(crate::tf!("PRESSPULL: %{count} object(s) updated.", count = handles.len()).as_ref());
self.refresh_properties();
self.refresh_presspull_prompt();
}
}
+1 -6
View File
@@ -448,12 +448,7 @@ impl OpenCADStudio {
let mut sections =
dispatch::properties_sectioned(handle, entity, &text_style_names);
if specialized_primitive {
sections.retain(|section| {
!section.props.iter().any(|property| {
property.field.starts_with("acis_")
|| property.field.starts_with("s3d_")
})
});
retain_specialized_primitive_sections(&mut sections);
}
sections.extend(
crate::scene::model::solid_history::primitive_properties(
+135 -13
View File
@@ -494,6 +494,25 @@ impl OpenCADStudio {
}
}
/// An acquired planar profile can lie away from the current UCS plane.
/// Reproject the cursor onto that profile before using it as a drag anchor
/// or looking for its supporting solid face.
fn profile_pick_point(
&self, i: usize, handle: Handle,
edit_cam: &Option<crate::scene::view::camera::Camera>,
cursor: iced::Point, bounds: iced::Rectangle,
) -> Option<glam::DVec3> {
let entity = self.tabs[i].scene.document.get_entity(handle)?;
let (plane, _, _) = crate::scene::model::presspull_model::profile_geometry(entity)?;
let normal = glam::DVec3::from_array(plane.normal()?).as_vec3();
let origin = glam::DVec3::from_array(plane.origin);
let point = match edit_cam {
Some(camera) => camera.pick_on_plane(cursor, bounds, normal, origin),
None => self.tabs[i].scene.camera.borrow().pick_on_plane(cursor, bounds, normal, origin),
};
point.is_finite().then_some(point)
}
/// Projection + hit-test wires for the active pane. Inside a floating
/// viewport (`edit_cam` Some) it returns the viewport camera and the live
/// **model** wires, so wire / hatch picking lands on the entity under the
@@ -922,6 +941,12 @@ impl OpenCADStudio {
return Task::none();
}
let i = self.active_tab;
// Modifier-driven selection must be known before cursor_plane/axis and
// drafting constraints are read, not merely before the final callback.
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
let perf_move = crate::perf::enabled();
let move_started = Instant::now();
@@ -1765,7 +1790,9 @@ impl OpenCADStudio {
// O(N) per frame and stalls the cursor on large drawings,
// so each move resets the dwell timer — `HoverDwellTick` runs the hit-test only
// once the cursor has been still for `HOVER_DWELL_MS`.
if !dragging && self.tabs[i].active_cmd.is_none() {
let deferred_command_hover = self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.needs_entity_pick() && command.entity_pick_deferred_hover());
if !dragging && (self.tabs[i].active_cmd.is_none() || deferred_command_hover) {
// On dense drawings, clearing a rollover immediately schedules a
// second full scene frame just as motion resumes. Keep the previous
// highlight until the next settled pick replaces it.
@@ -2090,21 +2117,25 @@ impl OpenCADStudio {
}
pt
};
let effective = self.tabs[i]
let effective = if needs_entity && self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
cursor_world
} else { self.tabs[i]
.active_cmd
.as_ref()
.and_then(|command| command.cursor_axis())
.and_then(|(origin, direction)| {
cursor_on_projected_axis(p, bounds, view_rot, eye, origin, direction)
})
.unwrap_or(effective);
.unwrap_or(effective) };
// Dynamic-input locked fields constrain the preview point
// (#356): a typed angle pins the direction, a typed
// distance pins the radius — the same resolution the Enter
// commit uses, so preview and commit agree. The lock wins
// over osnap/ortho/polar.
let effective = {
let locked = self.tabs[i].active_cmd.is_some()
let locked = !needs_entity && self.tabs[i].active_cmd.is_some()
&& self.tabs[i].dyn_fields.iter().any(|f| f.buffer.is_some());
if locked {
self.tabs[i].last_cursor_world = effective;
@@ -2224,6 +2255,10 @@ impl OpenCADStudio {
p.extend(cmd.on_preview_wires(effective));
}
p
} else if needs_entity && deferred_command_hover {
// The dwell callback resolves and outlines bounded areas once.
// Moving the cursor clears that outline without rebuilding it.
Vec::new()
} else if needs_entity {
let include_fills = self.tabs[i]
.active_cmd
@@ -2459,6 +2494,12 @@ impl OpenCADStudio {
pub(crate) fn on_viewport_exit(&mut self) -> Task<Message> {
let i = self.active_tab;
self.hover_dwell = None;
if self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_deferred_hover())
{
self.tabs[i].scene.clear_preview_wire();
}
let mut sel = self.tabs[i].scene.selection.borrow_mut();
sel.left_down = false;
sel.left_press_pos = None;
@@ -2795,6 +2836,10 @@ impl OpenCADStudio {
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
let i = self.active_tab;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
// A left-click during a command resets the right-click cycle, so
// the next right-click acts as Enter again rather than opening
// the context menu.
@@ -3074,6 +3119,10 @@ impl OpenCADStudio {
pub(super) fn on_viewport_left_release(&mut self) -> Task<Message> {
let i = self.active_tab;
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
// Navigation mode: end this drag but keep the tool armed for the next
// left drag (exit is Esc / another command). Mirror of the press.
@@ -3493,7 +3542,7 @@ impl OpenCADStudio {
// A click while dynamic-input fields hold typed values
// commits the CONSTRAINED point — the same resolution
// the preview shows and Enter would commit (#356).
if self.tabs[i].active_cmd.is_some()
if !needs_entity_click && self.tabs[i].active_cmd.is_some()
&& self.tabs[i].dyn_fields.iter().any(|f| f.buffer.is_some())
{
self.tabs[i].last_cursor_world = pt;
@@ -3501,7 +3550,13 @@ impl OpenCADStudio {
pt = r;
}
}
pt
if needs_entity_click && self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
raw
} else {
pt
}
};
// `world_pt` is in offset-relative (local) space, matching
@@ -3520,6 +3575,10 @@ impl OpenCADStudio {
world_pt + glam::DVec3::new(wo[0], wo[1], wo[2])
};
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
let result = if self.tabs[i]
.active_cmd
.as_ref()
@@ -3631,11 +3690,26 @@ impl OpenCADStudio {
self.tabs[i]
.scene
.solid_click_point_for(p, view_rot2, eye2, bounds, handle)
.unwrap_or(pick_wcs)
.or_else(|| self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_deferred_hover())
.then(|| self.profile_pick_point(i, handle, &edit_cam, p, bounds)).flatten())
.unwrap_or_else(|| {
let bounded_pick = self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_deferred_hover());
if bounded_pick && matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_)),
) {
// The aperture caught an edge outside its face.
// Do not reinterpret the working-plane point as
// a hit on another cap of the same body.
glam::DVec3::NAN
} else { pick_wcs }
})
} else {
pick_wcs
};
let entity_pick_direction = if uses_surface_point {
let entity_pick_direction = if uses_surface_point && entity_pick_point.is_finite() {
if matches!(
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Solid3D(_))
@@ -3749,6 +3823,10 @@ impl OpenCADStudio {
}
}
result
} else if self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.entity_pick_accepts_points())
{
self.tabs[i].active_cmd.as_mut().map(|command| command.on_point(pick_wcs))
} else if self.tabs[i]
.active_cmd
.as_ref()
@@ -4771,7 +4849,9 @@ impl OpenCADStudio {
/// ends. Repeated wheel events replace this timestamp, so the pick runs only
/// after the final zoom step settles.
pub(in crate::app) fn arm_hover_after_navigation(&mut self, i: usize) {
if i >= self.tabs.len() || self.tabs[i].active_cmd.is_some() {
if i >= self.tabs.len() || self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| !command.entity_pick_deferred_hover())
{
return;
}
let (cursor, canvas_size) = {
@@ -5062,12 +5142,19 @@ impl OpenCADStudio {
let i = dwell.tab;
// Re-check the gate — drag / command may have started
// between the move that armed the dwell and this tick.
if i >= self.tabs.len() {
if i >= self.tabs.len() || i != self.active_tab {
self.hover_dwell = None;
return Task::none();
}
self.push_ucs_to_cmd(i);
if let Some(command) = self.tabs[i].active_cmd.as_mut() {
command.set_ctrl(self.ctrl_down);
command.set_shift(self.shift_down);
}
let deferred_command = self.tabs[i].active_cmd.as_ref()
.is_some_and(|command| command.needs_entity_pick() && command.entity_pick_deferred_hover());
let navigating = self.tabs[i].scene.selection.borrow().middle_down;
if self.tabs[i].active_cmd.is_some() || navigating {
if (self.tabs[i].active_cmd.is_some() && !deferred_command) || navigating {
self.clear_navigation_hover(i);
return Task::none();
}
@@ -5158,16 +5245,51 @@ impl OpenCADStudio {
}
if hovered.is_none() {
let started = Instant::now();
hovered = self.tabs[i].scene.solid_edge_hover_hit(
hovered = if deferred_command { self.tabs[i].scene.solid_click_hit(
p, view_rot, eye, bounds, candidate_handles.as_ref(),
) } else { self.tabs[i].scene.solid_edge_hover_hit(
p,
view_rot,
eye,
bounds,
candidate_handles.as_ref(),
crate::ui::overlay::pick_box_aperture_px(self.pick_box),
);
) };
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
}
if deferred_command {
let surface_point = hovered.and_then(|handle| {
self.tabs[i].scene.solid_click_point_for(p, view_rot, eye, bounds, handle)
.or_else(|| self.profile_pick_point(i, handle, &edit_cam, p, bounds))
});
let point = surface_point.unwrap_or(hover_world);
let is_solid = hovered.is_some_and(|handle| matches!(
self.tabs[i].scene.document.get_entity(handle), Some(acadrust::EntityType::Solid3D(_)),
));
// An aperture may catch an edge beside, rather than on, a face.
// Never send its XY-plane fallback as a 3D face pick.
if is_solid && surface_point.is_none() {
self.tabs[i].scene.set_hover_highlight(None);
self.tabs[i].scene.clear_preview_wire();
self.hover_dwell = None;
return Task::none();
}
let handles = self.tabs[i].scene.document.entities().filter_map(|entity| {
matches!(entity, acadrust::EntityType::Solid3D(_)).then_some(entity.common().handle)
}).collect::<Vec<_>>();
self.tabs[i].scene.restore_solid_models(&handles);
let show = (self.model_space.selection_preview & 2) != 0;
let previews = if show {
let tab = &mut self.tabs[i];
tab.active_cmd.as_mut().map(|command| {
command.on_deferred_entity_hover(&tab.scene, hovered, point)
}).unwrap_or_default()
} else { Vec::new() };
self.tabs[i].scene.set_hover_highlight(None);
self.tabs[i].scene.set_preview_wires(previews);
self.hover_dwell = None;
return Task::none();
}
let preview_idle = (self.model_space.selection_preview & 1) != 0;
if preview_idle {
self.tabs[i].scene.set_hover_highlight(hovered);