parent
a418710cbd
commit
a66940a3f0
13 changed files with 368 additions and 170 deletions
|
|
@ -898,10 +898,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
// click places it (click-move-click) — same as picking the
|
||||
// grip directly in the viewport. Without this the menu just
|
||||
// closed and the grip never became hot (issue #48).
|
||||
if let Some(g) = self.tabs[i]
|
||||
.selected_grips
|
||||
if let Some((_, g)) = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.find(|g| g.id == popup.grip_id)
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.find(|(owner, g)| **owner == popup.handle && g.id == popup.grip_id)
|
||||
{
|
||||
// "Move with Leader" drags the whole multileader; the
|
||||
// others move just the picked grip.
|
||||
|
|
@ -911,13 +912,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
} else {
|
||||
(popup.grip_id, g.is_midpoint)
|
||||
};
|
||||
self.tabs[i].active_grip = Some(GripEdit {
|
||||
handle: popup.handle,
|
||||
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||
popup.handle,
|
||||
grip_id,
|
||||
is_translate,
|
||||
origin_world: g.world,
|
||||
last_world: g.world,
|
||||
});
|
||||
g.world,
|
||||
));
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
|
@ -1006,14 +1006,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
// Grab the new arrow so it follows the cursor (click places it,
|
||||
// Esc removes it).
|
||||
if let Some(new_gid) = add_leader_gid {
|
||||
if let Some(g) = self.tabs[i].selected_grips.iter().find(|g| g.id == new_gid) {
|
||||
self.tabs[i].active_grip = Some(GripEdit {
|
||||
handle: popup.handle,
|
||||
grip_id: new_gid,
|
||||
is_translate: false,
|
||||
origin_world: g.world,
|
||||
last_world: g.world,
|
||||
});
|
||||
if let Some((_, g)) = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.find(|(owner, g)| **owner == popup.handle && g.id == new_gid)
|
||||
{
|
||||
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||
popup.handle,
|
||||
new_gid,
|
||||
false,
|
||||
g.world,
|
||||
));
|
||||
self.grip_add_provisional = Some((popup.handle, new_gid));
|
||||
}
|
||||
}
|
||||
|
|
@ -1021,18 +1025,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||
// in Absolute mode, so the arc re-fits through the cursor as
|
||||
// it moves and the next click seats it (#339).
|
||||
if matches!(item.action, GripMenuAction::ConvertToArc) {
|
||||
if let Some(g) = self.tabs[i]
|
||||
.selected_grips
|
||||
if let Some((_, g)) = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.find(|g| g.id == popup.grip_id)
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.find(|(owner, g)| **owner == popup.handle && g.id == popup.grip_id)
|
||||
{
|
||||
self.tabs[i].active_grip = Some(GripEdit {
|
||||
handle: popup.handle,
|
||||
grip_id: popup.grip_id,
|
||||
is_translate: false,
|
||||
origin_world: g.world,
|
||||
last_world: g.world,
|
||||
});
|
||||
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||
popup.handle,
|
||||
popup.grip_id,
|
||||
false,
|
||||
g.world,
|
||||
));
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
|
|
|
|||
|
|
@ -3727,6 +3727,7 @@ impl OpenCADStudio {
|
|||
// Wrap around so ◀ from the first vertex lands on the last.
|
||||
let next = (cur + delta as i64).rem_euclid(n as i64) as usize;
|
||||
self.tabs[i].properties.prop_vertex = next;
|
||||
self.tabs[i].properties.prop_vertex_indicator_active = next != cur as usize;
|
||||
self.refresh_properties();
|
||||
}
|
||||
Task::none()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ use crate::app::helpers::{
|
|||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||
use crate::modules::ModuleEvent;
|
||||
use crate::scene::model::object::GripApply;
|
||||
use crate::scene::pick::grip::{find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit};
|
||||
use crate::scene::pick::grip::{
|
||||
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripTarget,
|
||||
};
|
||||
use crate::scene::{
|
||||
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
||||
};
|
||||
|
|
@ -242,8 +244,64 @@ impl OpenCADStudio {
|
|||
}
|
||||
}
|
||||
|
||||
fn grip_edit_for_hit(
|
||||
&mut self,
|
||||
i: usize,
|
||||
handle: Handle,
|
||||
grip_id: usize,
|
||||
is_translate: bool,
|
||||
world: glam::DVec3,
|
||||
) -> GripEdit {
|
||||
if !self.tabs[i].hot_grips.contains(&(handle, grip_id)) {
|
||||
self.tabs[i].hot_grips.clear();
|
||||
return GripEdit::single(handle, grip_id, is_translate, world);
|
||||
}
|
||||
|
||||
let mut targets: Vec<GripTarget> = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.filter(|(owner, grip)| self.tabs[i].hot_grips.contains(&(*owner, grip.id)))
|
||||
.filter(|(_, grip)| grip.id != crate::app::visibility::VIS_GRIP_ID)
|
||||
.map(|(owner, grip)| GripTarget {
|
||||
handle: owner,
|
||||
grip_id: grip.id,
|
||||
is_translate: grip.is_midpoint,
|
||||
last_world: grip.world,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// A midpoint/centre grip translates its whole entity. If that entity also
|
||||
// has hot point grips, applying both would move it twice; one translate
|
||||
// target owns the entity in that case.
|
||||
let translate_handles: rustc_hash::FxHashSet<_> = targets
|
||||
.iter()
|
||||
.filter(|target| target.is_translate)
|
||||
.map(|target| target.handle)
|
||||
.collect();
|
||||
let mut used_translates = rustc_hash::FxHashSet::default();
|
||||
targets.retain(|target| {
|
||||
if translate_handles.contains(&target.handle) {
|
||||
target.is_translate && used_translates.insert(target.handle)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
if targets.is_empty() {
|
||||
return GripEdit::single(handle, grip_id, is_translate, world);
|
||||
}
|
||||
GripEdit {
|
||||
handle,
|
||||
grip_id,
|
||||
origin_world: world,
|
||||
last_world: world,
|
||||
targets,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::app) fn update_grip_hover(&mut self, i: usize, p: iced::Point) {
|
||||
const HOVER_OPEN_MS: u128 = 600;
|
||||
const HOVER_OPEN_MS: u128 = 1_000;
|
||||
const POPUP_DISMISS_PX: f32 = 80.0;
|
||||
if self.tabs[i].active_cmd.is_some()
|
||||
|| self.tabs[i].active_grip.is_some()
|
||||
|
|
@ -253,11 +311,6 @@ impl OpenCADStudio {
|
|||
self.grip_popup = None;
|
||||
return;
|
||||
}
|
||||
let Some(handle) = self.tabs[i].selected_handle else {
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
return;
|
||||
};
|
||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||
let bounds = iced::Rectangle {
|
||||
x: 0.0,
|
||||
|
|
@ -306,7 +359,12 @@ impl OpenCADStudio {
|
|||
find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds)
|
||||
};
|
||||
match hit {
|
||||
Some((grip_id, _, _)) => {
|
||||
Some((grip_index, grip_id, _, _)) => {
|
||||
let Some(&handle) = self.tabs[i].selected_grip_handles.get(grip_index) else {
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
return;
|
||||
};
|
||||
let same = self
|
||||
.grip_hover
|
||||
.as_ref()
|
||||
|
|
@ -745,23 +803,43 @@ impl OpenCADStudio {
|
|||
}
|
||||
};
|
||||
|
||||
// First move of this drag: hide the edited entity from the
|
||||
// base tessellation (one re-tess) so subsequent moves only
|
||||
// refresh a cheap overlay preview instead of re-tessellating
|
||||
// the whole model on every move.
|
||||
if self.grip_preview_handle != Some(grip.handle) {
|
||||
if let Some(prev) = self.grip_preview_handle.take() {
|
||||
self.tabs[i].scene.preview_hidden.remove(&prev);
|
||||
let mut seen_handles = rustc_hash::FxHashSet::default();
|
||||
let edited_handles: Vec<_> = grip
|
||||
.targets
|
||||
.iter()
|
||||
.map(|target| target.handle)
|
||||
.filter(|handle| seen_handles.insert(*handle))
|
||||
.collect();
|
||||
|
||||
// First move of this drag: hide every edited entity from the base
|
||||
// tessellation so subsequent moves refresh only the overlay.
|
||||
if self.grip_preview_handles != edited_handles {
|
||||
if self.grip_dirty_before.is_none() {
|
||||
self.grip_dirty_before = Some(self.tabs[i].dirty);
|
||||
}
|
||||
// Back up the original geometry so Esc can cancel the drag.
|
||||
self.grip_original = self.tabs[i].scene.document.get_entity(grip.handle).cloned();
|
||||
self.tabs[i].scene.preview_hidden.insert(grip.handle);
|
||||
// Hiding changes exactly one resident run. Publishing a full
|
||||
// delta here made the first grip move rebuild every wire.
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(grip.handle, crate::scene::ChangeKind::Modified)]);
|
||||
self.grip_preview_handle = Some(grip.handle);
|
||||
for handle in std::mem::take(&mut self.grip_preview_handles) {
|
||||
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||
}
|
||||
self.grip_originals = edited_handles
|
||||
.iter()
|
||||
.filter_map(|&handle| {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.cloned()
|
||||
.map(|entity| (handle, entity))
|
||||
})
|
||||
.collect();
|
||||
for &handle in &edited_handles {
|
||||
self.tabs[i].scene.preview_hidden.insert(handle);
|
||||
}
|
||||
let changes: Vec<_> = edited_handles
|
||||
.iter()
|
||||
.map(|&handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.grip_preview_handles = edited_handles.clone();
|
||||
// Snapshot the entity's glyph quads once so each move can
|
||||
// slide the already-shaped text rather than re-shaping it
|
||||
// (issue #316). The fast slide path only runs for a rigid
|
||||
|
|
@ -769,18 +847,26 @@ impl OpenCADStudio {
|
|||
// dimension re-tessellates) and a Square insertion grip (so
|
||||
// an MTEXT width handle, a Triangle, still re-tessellates so
|
||||
// the re-wrap is exact).
|
||||
let snap = self.tabs[i].scene.wire_models_for(&[grip.handle]);
|
||||
let snap = self.tabs[i].scene.wire_models_for(&edited_handles);
|
||||
self.grip_text_verts = snap
|
||||
.iter()
|
||||
.flat_map(|w| w.text_verts.iter().copied())
|
||||
.collect();
|
||||
let square_grip = self.tabs[i]
|
||||
.selected_grips
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.find(|g| g.id == grip.grip_id)
|
||||
.map(|g| g.shape == crate::scene::model::object::GripShape::Square)
|
||||
.copied()
|
||||
.zip(self.tabs[i].selected_grips.iter())
|
||||
.find(|(owner, grip_def)| {
|
||||
*owner == grip.handle && grip_def.id == grip.grip_id
|
||||
})
|
||||
.map(|(_, grip_def)| {
|
||||
grip_def.shape == crate::scene::model::object::GripShape::Square
|
||||
})
|
||||
.unwrap_or(false);
|
||||
self.grip_text_slide = !self.grip_text_verts.is_empty()
|
||||
self.grip_text_slide = edited_handles.len() == 1
|
||||
&& grip.targets.len() == 1
|
||||
&& !self.grip_text_verts.is_empty()
|
||||
&& snap.iter().all(|w| w.points.is_empty())
|
||||
&& square_grip;
|
||||
}
|
||||
|
|
@ -861,22 +947,30 @@ impl OpenCADStudio {
|
|||
|
||||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let apply_started = Instant::now();
|
||||
let apply = if grip.is_translate {
|
||||
GripApply::Translate(snapped - grip.last_world)
|
||||
} else {
|
||||
GripApply::Absolute(snapped)
|
||||
};
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.apply_grip(grip.handle, grip.grip_id, apply);
|
||||
if matches!(
|
||||
self.tabs[i].scene.document.get_entity(grip.handle),
|
||||
Some(acadrust::EntityType::Hatch(_))
|
||||
) {
|
||||
self.tabs[i].scene.set_preview_hatch(grip.handle);
|
||||
let delta = snapped - grip.last_world;
|
||||
let actions: Vec<_> = grip
|
||||
.targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
let apply = if target.is_translate {
|
||||
GripApply::Translate(delta)
|
||||
} else {
|
||||
GripApply::Absolute(target.last_world + delta)
|
||||
};
|
||||
(target.handle, target.grip_id, apply)
|
||||
})
|
||||
.collect();
|
||||
for (handle, grip_id, apply) in actions {
|
||||
self.tabs[i].scene.apply_grip(handle, grip_id, apply);
|
||||
}
|
||||
self.tabs[i].scene.set_preview_hatches(&edited_handles);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped;
|
||||
if let Some(active) = self.tabs[i].active_grip.as_mut() {
|
||||
active.last_world = snapped;
|
||||
for target in &mut active.targets {
|
||||
target.last_world += delta;
|
||||
}
|
||||
}
|
||||
let apply_ms = apply_started.elapsed().as_secs_f64() * 1000.0;
|
||||
let preview_started = Instant::now();
|
||||
// Overlay the moved entity (hidden from the base). Pure text
|
||||
|
|
@ -898,7 +992,7 @@ impl OpenCADStudio {
|
|||
// preview WireModels carry the entity's glyphs (gathered
|
||||
// for the preview-text buffer in the render path), so a
|
||||
// dimension / MTEXT-width drag keeps its text visible too.
|
||||
let preview = self.tabs[i].scene.wire_models_for(&[grip.handle]);
|
||||
let preview = self.tabs[i].scene.wire_models_for(&edited_handles);
|
||||
self.tabs[i].scene.set_preview_wires(preview);
|
||||
}
|
||||
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
|
||||
|
|
@ -2022,7 +2116,7 @@ impl OpenCADStudio {
|
|||
&& self.tabs[i].active_grip.is_none()
|
||||
&& !self.tabs[i].selected_grips.is_empty()
|
||||
{
|
||||
if let Some(handle) = self.tabs[i].selected_handle {
|
||||
{
|
||||
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
||||
// In-viewport grips are model-space; project them with the
|
||||
// viewport camera so they hit-test where the GPU draws
|
||||
|
|
@ -2057,7 +2151,10 @@ impl OpenCADStudio {
|
|||
let cam = self.tabs[i].scene.camera.borrow();
|
||||
find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds)
|
||||
};
|
||||
if let Some((grip_id, is_translate, world)) = grip_hit {
|
||||
if let Some((grip_index, grip_id, is_translate, world)) = grip_hit {
|
||||
let Some(&handle) = self.tabs[i].selected_grip_handles.get(grip_index) else {
|
||||
return Task::none();
|
||||
};
|
||||
// The visibility (lookup) grip opens a state
|
||||
// dropdown instead of starting a stretch drag.
|
||||
if grip_id == crate::app::visibility::VIS_GRIP_ID {
|
||||
|
|
@ -2066,13 +2163,22 @@ impl OpenCADStudio {
|
|||
self.grip_popup = None;
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].active_grip = Some(GripEdit {
|
||||
if self.shift_down {
|
||||
let key = (handle, grip_id);
|
||||
if !self.tabs[i].hot_grips.remove(&key) {
|
||||
self.tabs[i].hot_grips.insert(key);
|
||||
}
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
return Task::none();
|
||||
}
|
||||
self.tabs[i].active_grip = Some(self.grip_edit_for_hit(
|
||||
i,
|
||||
handle,
|
||||
grip_id,
|
||||
is_translate,
|
||||
origin_world: world,
|
||||
last_world: world,
|
||||
});
|
||||
world,
|
||||
));
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
return Task::none();
|
||||
|
|
@ -2148,25 +2254,35 @@ impl OpenCADStudio {
|
|||
return Task::none();
|
||||
}
|
||||
self.tabs[i].active_grip = None;
|
||||
// Commit the grip drag: keep the doc's dragged geometry,
|
||||
// un-hide the edited entity and re-tessellate the base
|
||||
// once, dropping the overlay preview.
|
||||
if let Some(h) = self.grip_preview_handle.take() {
|
||||
// Undo entry for the drag (#332): the pre-drag backup
|
||||
// and live dragged image form a one-entity Arc delta. The old
|
||||
// swap/full-document snapshot made a vertex drag O(document).
|
||||
if let Some(orig) = self.grip_original.take() {
|
||||
self.push_single_entity_history(i, "GRIP", h, std::sync::Arc::new(orig));
|
||||
// Commit the grip drag as one undoable group, then put every
|
||||
// edited entity back into the resident tessellation.
|
||||
let handles = std::mem::take(&mut self.grip_preview_handles);
|
||||
let originals = std::mem::take(&mut self.grip_originals);
|
||||
let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty);
|
||||
if !handles.is_empty() {
|
||||
if !originals.is_empty() {
|
||||
self.push_entity_group_history(
|
||||
i,
|
||||
"GRIP",
|
||||
originals
|
||||
.into_iter()
|
||||
.map(|(handle, entity)| (handle, std::sync::Arc::new(entity)))
|
||||
.collect(),
|
||||
dirty_before,
|
||||
);
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
self.grip_text_verts = Vec::new();
|
||||
self.grip_text_slide = false;
|
||||
self.tabs[i].scene.preview_hidden.remove(&h);
|
||||
for &handle in &handles {
|
||||
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||
}
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
// Only the dragged entity changed — re-tessellate just it.
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
||||
let changes: Vec<_> = handles
|
||||
.into_iter()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
}
|
||||
// Placement confirmed — keep the just-added leader.
|
||||
self.grip_add_provisional = None;
|
||||
|
|
|
|||
Loading…
Reference in a new issue