parent
a418710cbd
commit
a66940a3f0
13 changed files with 368 additions and 170 deletions
|
|
@ -35,7 +35,7 @@ impl OpenCADStudio {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let had_grip = self.tabs[i].active_grip.take().is_some()
|
let had_grip = self.tabs[i].active_grip.take().is_some()
|
||||||
|| self.grip_add_provisional.is_some()
|
|| self.grip_add_provisional.is_some()
|
||||||
|| self.grip_preview_handle.is_some();
|
|| !self.grip_preview_handles.is_empty();
|
||||||
if !had_grip {
|
if !had_grip {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -54,18 +54,23 @@ impl OpenCADStudio {
|
||||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(handle) = self.grip_preview_handle.take() {
|
let handles = std::mem::take(&mut self.grip_preview_handles);
|
||||||
if let Some(original) = self.grip_original.take() {
|
let originals = std::mem::take(&mut self.grip_originals);
|
||||||
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
for (handle, original) in originals {
|
||||||
*entity = original;
|
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
||||||
}
|
*entity = original;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
for &handle in &handles {
|
||||||
self.tabs[i].scene.preview_hidden.remove(&handle);
|
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||||
self.tabs[i]
|
}
|
||||||
.scene
|
let changes: Vec<_> = handles
|
||||||
.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
|
.into_iter()
|
||||||
} else {
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
self.grip_original = None;
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
|
if let Some(dirty_before) = self.grip_dirty_before.take() {
|
||||||
|
self.tabs[i].dirty = dirty_before;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.grip_text_verts.clear();
|
self.grip_text_verts.clear();
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,10 @@ pub(super) struct DocumentTab {
|
||||||
pub(super) snap_result: Option<SnapResult>,
|
pub(super) snap_result: Option<SnapResult>,
|
||||||
pub(super) active_grip: Option<GripEdit>,
|
pub(super) active_grip: Option<GripEdit>,
|
||||||
pub(super) selected_grips: Vec<GripDef>,
|
pub(super) selected_grips: Vec<GripDef>,
|
||||||
|
/// Entity handle for each entry in `selected_grips`.
|
||||||
|
pub(super) selected_grip_handles: Vec<Handle>,
|
||||||
|
/// Shift-selected grips, keyed by entity and object-local grip id.
|
||||||
|
pub(super) hot_grips: rustc_hash::FxHashSet<(Handle, usize)>,
|
||||||
pub(super) selected_handle: Option<Handle>,
|
pub(super) selected_handle: Option<Handle>,
|
||||||
/// Dynamic-block visibility grip for the current single selection.
|
/// Dynamic-block visibility grip for the current single selection.
|
||||||
pub(super) visibility_grip: Option<super::visibility::VisibilityGrip>,
|
pub(super) visibility_grip: Option<super::visibility::VisibilityGrip>,
|
||||||
|
|
@ -430,6 +434,8 @@ impl DocumentTab {
|
||||||
snap_result: None,
|
snap_result: None,
|
||||||
active_grip: None,
|
active_grip: None,
|
||||||
selected_grips: vec![],
|
selected_grips: vec![],
|
||||||
|
selected_grip_handles: vec![],
|
||||||
|
hot_grips: rustc_hash::FxHashSet::default(),
|
||||||
selected_handle: None,
|
selected_handle: None,
|
||||||
visibility_grip: None,
|
visibility_grip: None,
|
||||||
wireframe: false,
|
wireframe: false,
|
||||||
|
|
|
||||||
|
|
@ -184,21 +184,27 @@ impl OpenCADStudio {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn push_single_entity_history(
|
pub(super) fn push_entity_group_history(
|
||||||
&mut self,
|
&mut self,
|
||||||
i: usize,
|
i: usize,
|
||||||
label: impl Into<String>,
|
label: impl Into<String>,
|
||||||
handle: Handle,
|
before: Vec<(Handle, Arc<EntityType>)>,
|
||||||
before: Arc<EntityType>,
|
dirty_before: bool,
|
||||||
) {
|
) {
|
||||||
self.finish_pending_history(i);
|
self.finish_pending_history(i);
|
||||||
let Some(after) = self.tabs[i].scene.document.get_entity_arc(handle) else {
|
let entities: Vec<_> = before
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(handle, original)| {
|
||||||
|
let after = self.tabs[i].scene.document.get_entity_arc(handle)?;
|
||||||
|
Some((handle, Some(original), Some(after)))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if entities.is_empty() {
|
||||||
return;
|
return;
|
||||||
};
|
}
|
||||||
let selected: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
|
let selected: Vec<Handle> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||||
let dirty_before = self.tabs[i].dirty;
|
|
||||||
let delta = DeltaSnapshot {
|
let delta = DeltaSnapshot {
|
||||||
entities: vec![(handle, Some(before), Some(after))],
|
entities,
|
||||||
current_layout_before: self.tabs[i].scene.current_layout.clone(),
|
current_layout_before: self.tabs[i].scene.current_layout.clone(),
|
||||||
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
current_layout_after: self.tabs[i].scene.current_layout.clone(),
|
||||||
selected_before: selected.clone(),
|
selected_before: selected.clone(),
|
||||||
|
|
|
||||||
|
|
@ -429,23 +429,20 @@ pub(super) struct OpenCADStudio {
|
||||||
/// being placed (follows the cursor). `(entity handle, new-arrow grip id)`.
|
/// being placed (follows the cursor). `(entity handle, new-arrow grip id)`.
|
||||||
/// Esc before the placement click removes it again.
|
/// Esc before the placement click removes it again.
|
||||||
grip_add_provisional: Option<(acadrust::Handle, usize)>,
|
grip_add_provisional: Option<(acadrust::Handle, usize)>,
|
||||||
/// Handle hidden from the base tessellation during an in-progress grip
|
/// Handles hidden from the base tessellation during an in-progress grip
|
||||||
/// drag. While dragging, the edited entity is excluded from the cached
|
/// drag. The edited entities are shown in the overlay until commit.
|
||||||
/// wire set and shown as a cheap overlay preview instead, so each move
|
grip_preview_handles: Vec<acadrust::Handle>,
|
||||||
/// updates only the overlay rather than re-tessellating the whole model.
|
|
||||||
/// Committed (un-hidden + one re-tess) when the drag ends. `None` = idle.
|
|
||||||
grip_preview_handle: Option<acadrust::Handle>,
|
|
||||||
/// Pending rollover hit-test. Each idle cursor move stashes
|
/// Pending rollover hit-test. Each idle cursor move stashes
|
||||||
/// `(last_move_at, point, tab)` here and clears the live highlight;
|
/// `(last_move_at, point, tab)` here and clears the live highlight;
|
||||||
/// `HoverDwellTick` runs the pick once the cursor has been still for
|
/// `HoverDwellTick` runs the pick once the cursor has been still for
|
||||||
/// `HOVER_DWELL_MS`. Skipping the pick mid-stroke avoids the per-frame
|
/// `HOVER_DWELL_MS`. Skipping the pick mid-stroke avoids the per-frame
|
||||||
/// O(N) wire+hatch+mesh sweep that froze the cursor on large drawings.
|
/// O(N) wire+hatch+mesh sweep that froze the cursor on large drawings.
|
||||||
hover_dwell: Option<HoverDwell>,
|
hover_dwell: Option<HoverDwell>,
|
||||||
/// Snapshot of the edited entity taken at the start of a grip drag, used to
|
/// Snapshots of edited entities taken at the start of a grip drag. The drag
|
||||||
/// restore it if the user presses Esc to cancel the drag. The drag mutates
|
/// mutates the document live, so Escape restores this group atomically.
|
||||||
/// the document live (so grips / properties track), so cancel reverts from
|
grip_originals: Vec<(acadrust::Handle, acadrust::EntityType)>,
|
||||||
/// this backup. Dropped (kept) on a normal commit.
|
/// Document dirty state before the live grip mutation began.
|
||||||
grip_original: Option<acadrust::EntityType>,
|
grip_dirty_before: Option<bool>,
|
||||||
/// Drag-start snapshot of the dragged entity's SDF glyph quads. A whole-
|
/// Drag-start snapshot of the dragged entity's SDF glyph quads. A whole-
|
||||||
/// entity text move slides these each frame (translating the already-shaped
|
/// entity text move slides these each frame (translating the already-shaped
|
||||||
/// glyphs) instead of re-tessellating the run every cursor move (issue #316).
|
/// glyphs) instead of re-tessellating the run every cursor move (issue #316).
|
||||||
|
|
@ -2700,9 +2697,10 @@ impl OpenCADStudio {
|
||||||
grip_pending: None,
|
grip_pending: None,
|
||||||
visibility_popup: None,
|
visibility_popup: None,
|
||||||
grip_add_provisional: None,
|
grip_add_provisional: None,
|
||||||
grip_preview_handle: None,
|
grip_preview_handles: Vec::new(),
|
||||||
hover_dwell: None,
|
hover_dwell: None,
|
||||||
grip_original: None,
|
grip_originals: Vec::new(),
|
||||||
|
grip_dirty_before: None,
|
||||||
grip_text_verts: Vec::new(),
|
grip_text_verts: Vec::new(),
|
||||||
grip_text_slide: false,
|
grip_text_slide: false,
|
||||||
qselect: None,
|
qselect: None,
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,11 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
let prop_vertex_indicator_active = if cur_handles == prev_handles {
|
||||||
|
self.tabs[i].properties.prop_vertex_indicator_active
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
crate::scene::view::dispatch::set_prop_current_vertex(prop_vertex);
|
crate::scene::view::dispatch::set_prop_current_vertex(prop_vertex);
|
||||||
|
|
||||||
let new_panel = {
|
let new_panel = {
|
||||||
|
|
@ -1540,6 +1545,7 @@ impl OpenCADStudio {
|
||||||
panel.expanded_groups = expanded_groups;
|
panel.expanded_groups = expanded_groups;
|
||||||
panel.source_handles = new_handles;
|
panel.source_handles = new_handles;
|
||||||
panel.prop_vertex = prop_vertex;
|
panel.prop_vertex = prop_vertex;
|
||||||
|
panel.prop_vertex_indicator_active = prop_vertex_indicator_active;
|
||||||
panel
|
panel
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1666,33 +1672,40 @@ impl OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
[0.0_f64; 3]
|
[0.0_f64; 3]
|
||||||
};
|
};
|
||||||
let (new_handle, new_grips) = {
|
let (new_handle, new_grips, new_grip_handles) = {
|
||||||
let selected = self.tabs[i].scene.selected_entities();
|
let selected = self.tabs[i].scene.selected_entities();
|
||||||
if selected.len() == 1 {
|
let single_handle = (selected.len() == 1).then(|| selected[0].0);
|
||||||
let (handle, entity) = selected[0];
|
let mut grips = Vec::new();
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
for (handle, entity) in selected {
|
||||||
let contextual = crate::scene::annotative::entity_for_active_context(
|
let contextual = crate::scene::annotative::entity_for_active_context(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
entity,
|
entity,
|
||||||
);
|
);
|
||||||
let grips = dispatch::grips(contextual.as_ref())
|
for mut grip in dispatch::grips(contextual.as_ref()) {
|
||||||
.into_iter()
|
// Subtract in f64: at UTM magnitudes an f32 cast before
|
||||||
.map(|mut g| {
|
// the offset costs ~1 unit and draws the grip off the wire.
|
||||||
// Subtract in f64: at UTM magnitudes an f32 cast before
|
grip.world.x -= wo[0];
|
||||||
// the offset costs ~1 unit and draws the grip off the
|
grip.world.y -= wo[1];
|
||||||
// wire.
|
grip.world.z -= wo[2];
|
||||||
g.world.x -= wo[0];
|
handles.push(handle);
|
||||||
g.world.y -= wo[1];
|
grips.push(grip);
|
||||||
g.world.z -= wo[2];
|
}
|
||||||
g
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
(Some(handle), grips)
|
|
||||||
} else {
|
|
||||||
(None, vec![])
|
|
||||||
}
|
}
|
||||||
|
(single_handle, grips, handles)
|
||||||
};
|
};
|
||||||
self.tabs[i].selected_handle = new_handle;
|
self.tabs[i].selected_handle = new_handle;
|
||||||
self.tabs[i].selected_grips = new_grips;
|
self.tabs[i].selected_grips = new_grips;
|
||||||
|
self.tabs[i].selected_grip_handles = new_grip_handles;
|
||||||
|
let available: rustc_hash::FxHashSet<_> = self.tabs[i]
|
||||||
|
.selected_grip_handles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.zip(self.tabs[i].selected_grips.iter().map(|grip| grip.id))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i]
|
||||||
|
.hot_grips
|
||||||
|
.retain(|key| available.contains(key));
|
||||||
// Append the dynamic-block visibility (lookup) grip, if the lone
|
// Append the dynamic-block visibility (lookup) grip, if the lone
|
||||||
// selection is a visibility-parametric block reference.
|
// selection is a visibility-parametric block reference.
|
||||||
self.refresh_visibility_grip(wo);
|
self.refresh_visibility_grip(wo);
|
||||||
|
|
|
||||||
|
|
@ -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
|
// click places it (click-move-click) — same as picking the
|
||||||
// grip directly in the viewport. Without this the menu just
|
// grip directly in the viewport. Without this the menu just
|
||||||
// closed and the grip never became hot (issue #48).
|
// closed and the grip never became hot (issue #48).
|
||||||
if let Some(g) = self.tabs[i]
|
if let Some((_, g)) = self.tabs[i]
|
||||||
.selected_grips
|
.selected_grip_handles
|
||||||
.iter()
|
.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
|
// "Move with Leader" drags the whole multileader; the
|
||||||
// others move just the picked grip.
|
// others move just the picked grip.
|
||||||
|
|
@ -911,13 +912,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
} else {
|
} else {
|
||||||
(popup.grip_id, g.is_midpoint)
|
(popup.grip_id, g.is_midpoint)
|
||||||
};
|
};
|
||||||
self.tabs[i].active_grip = Some(GripEdit {
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||||
handle: popup.handle,
|
popup.handle,
|
||||||
grip_id,
|
grip_id,
|
||||||
is_translate,
|
is_translate,
|
||||||
origin_world: g.world,
|
g.world,
|
||||||
last_world: g.world,
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return Task::none();
|
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,
|
// Grab the new arrow so it follows the cursor (click places it,
|
||||||
// Esc removes it).
|
// Esc removes it).
|
||||||
if let Some(new_gid) = add_leader_gid {
|
if let Some(new_gid) = add_leader_gid {
|
||||||
if let Some(g) = self.tabs[i].selected_grips.iter().find(|g| g.id == new_gid) {
|
if let Some((_, g)) = self.tabs[i]
|
||||||
self.tabs[i].active_grip = Some(GripEdit {
|
.selected_grip_handles
|
||||||
handle: popup.handle,
|
.iter()
|
||||||
grip_id: new_gid,
|
.zip(self.tabs[i].selected_grips.iter())
|
||||||
is_translate: false,
|
.find(|(owner, g)| **owner == popup.handle && g.id == new_gid)
|
||||||
origin_world: g.world,
|
{
|
||||||
last_world: g.world,
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||||
});
|
popup.handle,
|
||||||
|
new_gid,
|
||||||
|
false,
|
||||||
|
g.world,
|
||||||
|
));
|
||||||
self.grip_add_provisional = Some((popup.handle, new_gid));
|
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
|
// in Absolute mode, so the arc re-fits through the cursor as
|
||||||
// it moves and the next click seats it (#339).
|
// it moves and the next click seats it (#339).
|
||||||
if matches!(item.action, GripMenuAction::ConvertToArc) {
|
if matches!(item.action, GripMenuAction::ConvertToArc) {
|
||||||
if let Some(g) = self.tabs[i]
|
if let Some((_, g)) = self.tabs[i]
|
||||||
.selected_grips
|
.selected_grip_handles
|
||||||
.iter()
|
.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 {
|
self.tabs[i].active_grip = Some(GripEdit::single(
|
||||||
handle: popup.handle,
|
popup.handle,
|
||||||
grip_id: popup.grip_id,
|
popup.grip_id,
|
||||||
is_translate: false,
|
false,
|
||||||
origin_world: g.world,
|
g.world,
|
||||||
last_world: g.world,
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
|
||||||
|
|
@ -3727,6 +3727,7 @@ impl OpenCADStudio {
|
||||||
// Wrap around so ◀ from the first vertex lands on the last.
|
// Wrap around so ◀ from the first vertex lands on the last.
|
||||||
let next = (cur + delta as i64).rem_euclid(n as i64) as usize;
|
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 = next;
|
||||||
|
self.tabs[i].properties.prop_vertex_indicator_active = next != cur as usize;
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,9 @@ use crate::app::helpers::{
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
use crate::modules::ModuleEvent;
|
use crate::modules::ModuleEvent;
|
||||||
use crate::scene::model::object::GripApply;
|
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::{
|
use crate::scene::{
|
||||||
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
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) {
|
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;
|
const POPUP_DISMISS_PX: f32 = 80.0;
|
||||||
if self.tabs[i].active_cmd.is_some()
|
if self.tabs[i].active_cmd.is_some()
|
||||||
|| self.tabs[i].active_grip.is_some()
|
|| self.tabs[i].active_grip.is_some()
|
||||||
|
|
@ -253,11 +311,6 @@ impl OpenCADStudio {
|
||||||
self.grip_popup = None;
|
self.grip_popup = None;
|
||||||
return;
|
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 (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||||
let bounds = iced::Rectangle {
|
let bounds = iced::Rectangle {
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
|
|
@ -306,7 +359,12 @@ impl OpenCADStudio {
|
||||||
find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds)
|
find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds)
|
||||||
};
|
};
|
||||||
match hit {
|
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
|
let same = self
|
||||||
.grip_hover
|
.grip_hover
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -745,23 +803,43 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// First move of this drag: hide the edited entity from the
|
let mut seen_handles = rustc_hash::FxHashSet::default();
|
||||||
// base tessellation (one re-tess) so subsequent moves only
|
let edited_handles: Vec<_> = grip
|
||||||
// refresh a cheap overlay preview instead of re-tessellating
|
.targets
|
||||||
// the whole model on every move.
|
.iter()
|
||||||
if self.grip_preview_handle != Some(grip.handle) {
|
.map(|target| target.handle)
|
||||||
if let Some(prev) = self.grip_preview_handle.take() {
|
.filter(|handle| seen_handles.insert(*handle))
|
||||||
self.tabs[i].scene.preview_hidden.remove(&prev);
|
.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.
|
for handle in std::mem::take(&mut self.grip_preview_handles) {
|
||||||
self.grip_original = self.tabs[i].scene.document.get_entity(grip.handle).cloned();
|
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||||
self.tabs[i].scene.preview_hidden.insert(grip.handle);
|
}
|
||||||
// Hiding changes exactly one resident run. Publishing a full
|
self.grip_originals = edited_handles
|
||||||
// delta here made the first grip move rebuild every wire.
|
.iter()
|
||||||
self.tabs[i]
|
.filter_map(|&handle| {
|
||||||
.scene
|
self.tabs[i]
|
||||||
.bump_entities(&[(grip.handle, crate::scene::ChangeKind::Modified)]);
|
.scene
|
||||||
self.grip_preview_handle = Some(grip.handle);
|
.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
|
// Snapshot the entity's glyph quads once so each move can
|
||||||
// slide the already-shaped text rather than re-shaping it
|
// slide the already-shaped text rather than re-shaping it
|
||||||
// (issue #316). The fast slide path only runs for a rigid
|
// (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
|
// dimension re-tessellates) and a Square insertion grip (so
|
||||||
// an MTEXT width handle, a Triangle, still re-tessellates so
|
// an MTEXT width handle, a Triangle, still re-tessellates so
|
||||||
// the re-wrap is exact).
|
// 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
|
self.grip_text_verts = snap
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|w| w.text_verts.iter().copied())
|
.flat_map(|w| w.text_verts.iter().copied())
|
||||||
.collect();
|
.collect();
|
||||||
let square_grip = self.tabs[i]
|
let square_grip = self.tabs[i]
|
||||||
.selected_grips
|
.selected_grip_handles
|
||||||
.iter()
|
.iter()
|
||||||
.find(|g| g.id == grip.grip_id)
|
.copied()
|
||||||
.map(|g| g.shape == crate::scene::model::object::GripShape::Square)
|
.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);
|
.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())
|
&& snap.iter().all(|w| w.points.is_empty())
|
||||||
&& square_grip;
|
&& square_grip;
|
||||||
}
|
}
|
||||||
|
|
@ -861,22 +947,30 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
let snap_ms = snap_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
let apply_started = Instant::now();
|
let apply_started = Instant::now();
|
||||||
let apply = if grip.is_translate {
|
let delta = snapped - grip.last_world;
|
||||||
GripApply::Translate(snapped - grip.last_world)
|
let actions: Vec<_> = grip
|
||||||
} else {
|
.targets
|
||||||
GripApply::Absolute(snapped)
|
.iter()
|
||||||
};
|
.map(|target| {
|
||||||
self.tabs[i]
|
let apply = if target.is_translate {
|
||||||
.scene
|
GripApply::Translate(delta)
|
||||||
.apply_grip(grip.handle, grip.grip_id, apply);
|
} else {
|
||||||
if matches!(
|
GripApply::Absolute(target.last_world + delta)
|
||||||
self.tabs[i].scene.document.get_entity(grip.handle),
|
};
|
||||||
Some(acadrust::EntityType::Hatch(_))
|
(target.handle, target.grip_id, apply)
|
||||||
) {
|
})
|
||||||
self.tabs[i].scene.set_preview_hatch(grip.handle);
|
.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].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 apply_ms = apply_started.elapsed().as_secs_f64() * 1000.0;
|
||||||
let preview_started = Instant::now();
|
let preview_started = Instant::now();
|
||||||
// Overlay the moved entity (hidden from the base). Pure text
|
// Overlay the moved entity (hidden from the base). Pure text
|
||||||
|
|
@ -898,7 +992,7 @@ impl OpenCADStudio {
|
||||||
// preview WireModels carry the entity's glyphs (gathered
|
// preview WireModels carry the entity's glyphs (gathered
|
||||||
// for the preview-text buffer in the render path), so a
|
// for the preview-text buffer in the render path), so a
|
||||||
// dimension / MTEXT-width drag keeps its text visible too.
|
// 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);
|
self.tabs[i].scene.set_preview_wires(preview);
|
||||||
}
|
}
|
||||||
let preview_ms = preview_started.elapsed().as_secs_f64() * 1000.0;
|
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].active_grip.is_none()
|
||||||
&& !self.tabs[i].selected_grips.is_empty()
|
&& !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";
|
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
||||||
// In-viewport grips are model-space; project them with the
|
// In-viewport grips are model-space; project them with the
|
||||||
// viewport camera so they hit-test where the GPU draws
|
// viewport camera so they hit-test where the GPU draws
|
||||||
|
|
@ -2057,7 +2151,10 @@ impl OpenCADStudio {
|
||||||
let cam = self.tabs[i].scene.camera.borrow();
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds)
|
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
|
// The visibility (lookup) grip opens a state
|
||||||
// dropdown instead of starting a stretch drag.
|
// dropdown instead of starting a stretch drag.
|
||||||
if grip_id == crate::app::visibility::VIS_GRIP_ID {
|
if grip_id == crate::app::visibility::VIS_GRIP_ID {
|
||||||
|
|
@ -2066,13 +2163,22 @@ impl OpenCADStudio {
|
||||||
self.grip_popup = None;
|
self.grip_popup = None;
|
||||||
return Task::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,
|
handle,
|
||||||
grip_id,
|
grip_id,
|
||||||
is_translate,
|
is_translate,
|
||||||
origin_world: world,
|
world,
|
||||||
last_world: world,
|
));
|
||||||
});
|
|
||||||
self.grip_hover = None;
|
self.grip_hover = None;
|
||||||
self.grip_popup = None;
|
self.grip_popup = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
|
|
@ -2148,25 +2254,35 @@ impl OpenCADStudio {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
self.tabs[i].active_grip = None;
|
self.tabs[i].active_grip = None;
|
||||||
// Commit the grip drag: keep the doc's dragged geometry,
|
// Commit the grip drag as one undoable group, then put every
|
||||||
// un-hide the edited entity and re-tessellate the base
|
// edited entity back into the resident tessellation.
|
||||||
// once, dropping the overlay preview.
|
let handles = std::mem::take(&mut self.grip_preview_handles);
|
||||||
if let Some(h) = self.grip_preview_handle.take() {
|
let originals = std::mem::take(&mut self.grip_originals);
|
||||||
// Undo entry for the drag (#332): the pre-drag backup
|
let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty);
|
||||||
// and live dragged image form a one-entity Arc delta. The old
|
if !handles.is_empty() {
|
||||||
// swap/full-document snapshot made a vertex drag O(document).
|
if !originals.is_empty() {
|
||||||
if let Some(orig) = self.grip_original.take() {
|
self.push_entity_group_history(
|
||||||
self.push_single_entity_history(i, "GRIP", h, std::sync::Arc::new(orig));
|
i,
|
||||||
|
"GRIP",
|
||||||
|
originals
|
||||||
|
.into_iter()
|
||||||
|
.map(|(handle, entity)| (handle, std::sync::Arc::new(entity)))
|
||||||
|
.collect(),
|
||||||
|
dirty_before,
|
||||||
|
);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
}
|
}
|
||||||
self.grip_text_verts = Vec::new();
|
self.grip_text_verts = Vec::new();
|
||||||
self.grip_text_slide = false;
|
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();
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
// Only the dragged entity changed — re-tessellate just it.
|
let changes: Vec<_> = handles
|
||||||
self.tabs[i]
|
.into_iter()
|
||||||
.scene
|
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||||
.bump_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
}
|
}
|
||||||
// Placement confirmed — keep the just-added leader.
|
// Placement confirmed — keep the just-added leader.
|
||||||
self.grip_add_provisional = None;
|
self.grip_add_provisional = None;
|
||||||
|
|
|
||||||
|
|
@ -315,14 +315,19 @@ impl OpenCADStudio {
|
||||||
// mark that grip hot so the navigated vertex is visible in
|
// mark that grip hot so the navigated vertex is visible in
|
||||||
// the drawing. Only for a single selected polyline, whose
|
// the drawing. Only for a single selected polyline, whose
|
||||||
// vertex grips are ids 0..n. (Properties vertex stepper)
|
// vertex grips are ids 0..n. (Properties vertex stepper)
|
||||||
let current_vertex_grip: Option<usize> = sel_h.and_then(|h| {
|
let current_vertex_grip: Option<usize> = tab
|
||||||
|
.properties
|
||||||
|
.prop_vertex_indicator_active
|
||||||
|
.then(|| sel_h)
|
||||||
|
.flatten()
|
||||||
|
.and_then(|h| {
|
||||||
matches!(
|
matches!(
|
||||||
tab.scene.document.get_entity(h),
|
tab.scene.document.get_entity(h),
|
||||||
Some(acadrust::EntityType::LwPolyline(_))
|
Some(acadrust::EntityType::LwPolyline(_))
|
||||||
| Some(acadrust::EntityType::Polyline2D(_))
|
| Some(acadrust::EntityType::Polyline2D(_))
|
||||||
)
|
)
|
||||||
.then_some(tab.properties.prop_vertex)
|
.then_some(tab.properties.prop_vertex)
|
||||||
});
|
});
|
||||||
// In-viewport grips are model-space; project them with the
|
// In-viewport grips are model-space; project them with the
|
||||||
// viewport camera so they sit on the wire the GPU draws.
|
// viewport camera so they sit on the wire the GPU draws.
|
||||||
// Paper entities use the 2-D paper transform; the model tab
|
// Paper entities use the 2-D paper transform; the model tab
|
||||||
|
|
@ -349,7 +354,8 @@ impl OpenCADStudio {
|
||||||
};
|
};
|
||||||
screen_grips
|
screen_grips
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(_, screen, _, _, _)| {
|
.enumerate()
|
||||||
|
.filter(|(_, (_, screen, _, _, _))| {
|
||||||
screen.x.is_finite()
|
screen.x.is_finite()
|
||||||
&& screen.y.is_finite()
|
&& screen.y.is_finite()
|
||||||
&& screen.x >= -bounds.width
|
&& screen.x >= -bounds.width
|
||||||
|
|
@ -357,12 +363,18 @@ impl OpenCADStudio {
|
||||||
&& screen.y >= -bounds.height
|
&& screen.y >= -bounds.height
|
||||||
&& screen.y <= bounds.height * 2.0
|
&& screen.y <= bounds.height * 2.0
|
||||||
})
|
})
|
||||||
.map(|(grip_id, screen, _is_midpoint, shape, dir)| {
|
.map(|(index, (grip_id, screen, _is_midpoint, shape, dir))| {
|
||||||
let is_hot = tab
|
let owner = tab.selected_grip_handles.get(index).copied();
|
||||||
.active_grip
|
let is_hot = owner.is_some_and(|handle| {
|
||||||
.as_ref()
|
tab.hot_grips.contains(&(handle, grip_id))
|
||||||
.map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id)
|
|| tab.active_grip.as_ref().is_some_and(|edit| {
|
||||||
|| Some(grip_id) == current_vertex_grip;
|
edit.targets.iter().any(|target| {
|
||||||
|
target.handle == handle && target.grip_id == grip_id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|| (Some(handle) == sel_h
|
||||||
|
&& Some(grip_id) == current_vertex_grip)
|
||||||
|
});
|
||||||
crate::ui::overlay::GripMarker {
|
crate::ui::overlay::GripMarker {
|
||||||
pos: screen,
|
pos: screen,
|
||||||
shape,
|
shape,
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@ impl OpenCADStudio {
|
||||||
shape: GripShape::Triangle,
|
shape: GripShape::Triangle,
|
||||||
dir: None,
|
dir: None,
|
||||||
});
|
});
|
||||||
|
self.tabs[i].selected_grip_handles.push(handle);
|
||||||
self.tabs[i].visibility_grip = Some(VisibilityGrip {
|
self.tabs[i].visibility_grip = Some(VisibilityGrip {
|
||||||
insert_handle: handle,
|
insert_handle: handle,
|
||||||
state_names,
|
state_names,
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,42 @@ pub struct GripEdit {
|
||||||
pub handle: Handle,
|
pub handle: Handle,
|
||||||
/// Index into the entity's grip list.
|
/// Index into the entity's grip list.
|
||||||
pub grip_id: usize,
|
pub grip_id: usize,
|
||||||
/// `true` → midpoint / translate grip; `false` → endpoint / absolute grip.
|
|
||||||
pub is_translate: bool,
|
|
||||||
/// World-space position of the grip when the drag started (ortho/polar base).
|
/// World-space position of the grip when the drag started (ortho/polar base).
|
||||||
pub origin_world: DVec3,
|
pub origin_world: DVec3,
|
||||||
/// Last world-space cursor position (needed for incremental delta on translate drags).
|
/// Last world-space cursor position (needed for incremental delta on translate drags).
|
||||||
pub last_world: DVec3,
|
pub last_world: DVec3,
|
||||||
|
/// Every hot grip moved by this edit. A normal grip edit contains one target.
|
||||||
|
pub targets: Vec<GripTarget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GripTarget {
|
||||||
|
pub handle: Handle,
|
||||||
|
pub grip_id: usize,
|
||||||
|
pub is_translate: bool,
|
||||||
|
pub last_world: DVec3,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GripEdit {
|
||||||
|
pub fn single(
|
||||||
|
handle: Handle,
|
||||||
|
grip_id: usize,
|
||||||
|
is_translate: bool,
|
||||||
|
world: DVec3,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
handle,
|
||||||
|
grip_id,
|
||||||
|
origin_world: world,
|
||||||
|
last_world: world,
|
||||||
|
targets: vec![GripTarget {
|
||||||
|
handle,
|
||||||
|
grip_id,
|
||||||
|
is_translate,
|
||||||
|
last_world: world,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Screen-space helpers ───────────────────────────────────────────────────
|
// ── Screen-space helpers ───────────────────────────────────────────────────
|
||||||
|
|
@ -83,11 +113,11 @@ pub fn find_hit_grip_paper(
|
||||||
half_w: f32,
|
half_w: f32,
|
||||||
half_h: f32,
|
half_h: f32,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Option<(usize, bool, DVec3)> {
|
) -> Option<(usize, usize, bool, DVec3)> {
|
||||||
let mut best_dist = GRIP_THRESHOLD_PX;
|
let mut best_dist = GRIP_THRESHOLD_PX;
|
||||||
let mut best: Option<(usize, bool, DVec3)> = None;
|
let mut best: Option<(usize, usize, bool, DVec3)> = None;
|
||||||
|
|
||||||
for g in grips {
|
for (index, g) in grips.iter().enumerate() {
|
||||||
let screen = Point::new(
|
let screen = Point::new(
|
||||||
(g.world.x as f32 - tx + half_w) / (2.0 * half_w) * bounds.width,
|
(g.world.x as f32 - tx + half_w) / (2.0 * half_w) * bounds.width,
|
||||||
(ty + half_h - g.world.y as f32) / (2.0 * half_h) * bounds.height,
|
(ty + half_h - g.world.y as f32) / (2.0 * half_h) * bounds.height,
|
||||||
|
|
@ -97,7 +127,7 @@ pub fn find_hit_grip_paper(
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
if d < best_dist {
|
if d < best_dist {
|
||||||
best_dist = d;
|
best_dist = d;
|
||||||
best = Some((g.id, g.is_midpoint, g.world));
|
best = Some((index, g.id, g.is_midpoint, g.world));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
best
|
best
|
||||||
|
|
@ -110,11 +140,11 @@ pub fn find_hit_grip(
|
||||||
grips: &[GripDef],
|
grips: &[GripDef],
|
||||||
camera: &crate::scene::view::camera::Camera,
|
camera: &crate::scene::view::camera::Camera,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Option<(usize, bool, DVec3)> {
|
) -> Option<(usize, usize, bool, DVec3)> {
|
||||||
let mut best_dist = GRIP_THRESHOLD_PX;
|
let mut best_dist = GRIP_THRESHOLD_PX;
|
||||||
let mut best: Option<(usize, bool, DVec3)> = None;
|
let mut best: Option<(usize, usize, bool, DVec3)> = None;
|
||||||
|
|
||||||
for g in grips {
|
for (index, g) in grips.iter().enumerate() {
|
||||||
let Some(screen) = camera.project(g.world, bounds) else {
|
let Some(screen) = camera.project(g.world, bounds) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
@ -124,7 +154,7 @@ pub fn find_hit_grip(
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
if d < best_dist {
|
if d < best_dist {
|
||||||
best_dist = d;
|
best_dist = d;
|
||||||
best = Some((g.id, g.is_midpoint, g.world));
|
best = Some((index, g.id, g.is_midpoint, g.world));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
best
|
best
|
||||||
|
|
@ -176,11 +206,11 @@ pub fn find_hit_grip_rte(
|
||||||
view_rot: Mat4,
|
view_rot: Mat4,
|
||||||
eye: DVec3,
|
eye: DVec3,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Option<(usize, bool, DVec3)> {
|
) -> Option<(usize, usize, bool, DVec3)> {
|
||||||
let mut best_dist = GRIP_THRESHOLD_PX;
|
let mut best_dist = GRIP_THRESHOLD_PX;
|
||||||
let mut best: Option<(usize, bool, DVec3)> = None;
|
let mut best: Option<(usize, usize, bool, DVec3)> = None;
|
||||||
|
|
||||||
for g in grips {
|
for (index, g) in grips.iter().enumerate() {
|
||||||
let Some(screen) = project_rte(g.world, view_rot, eye, bounds) else {
|
let Some(screen) = project_rte(g.world, view_rot, eye, bounds) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
@ -189,7 +219,7 @@ pub fn find_hit_grip_rte(
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
if d < best_dist {
|
if d < best_dist {
|
||||||
best_dist = d;
|
best_dist = d;
|
||||||
best = Some((g.id, g.is_midpoint, g.world));
|
best = Some((index, g.id, g.is_midpoint, g.world));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
best
|
best
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,20 @@ impl Scene {
|
||||||
self.preview_wires = wires;
|
self.preview_wires = wires;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Publish the current cached hatch as a one-entity live fill overlay.
|
/// Publish all edited hatches as one live fill overlay.
|
||||||
/// The edited hatch is hidden from the resident set during a grip drag;
|
pub fn set_preview_hatches(&mut self, handles: &[Handle]) {
|
||||||
/// this keeps its pattern visible without rebuilding the full hatch batch.
|
let mut models = Vec::new();
|
||||||
pub fn set_preview_hatch(&mut self, handle: Handle) {
|
for &handle in handles {
|
||||||
|
self.append_preview_hatch(handle, &mut models);
|
||||||
|
}
|
||||||
|
self.preview_hatches = std::sync::Arc::new(models);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_preview_hatch(&self, handle: Handle, models: &mut Vec<HatchModel>) {
|
||||||
let Some(mut model) = self.hatches.get(&handle).cloned() else {
|
let Some(mut model) = self.hatches.get(&handle).cloned() else {
|
||||||
self.preview_hatches = std::sync::Arc::new(Vec::new());
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(entity) = self.document.get_entity(handle) else {
|
let Some(entity) = self.document.get_entity(handle) else {
|
||||||
self.preview_hatches = std::sync::Arc::new(Vec::new());
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -41,7 +45,6 @@ impl Scene {
|
||||||
.get(&handle.value())
|
.get(&handle.value())
|
||||||
.map_or(0.0, |depth| depth[0]);
|
.map_or(0.0, |depth| depth[0]);
|
||||||
|
|
||||||
let mut models = Vec::with_capacity(2);
|
|
||||||
if let EntityType::Hatch(hatch) = entity {
|
if let EntityType::Hatch(hatch) = entity {
|
||||||
if let Some(background) = crate::entities::hatch::background_color(hatch) {
|
if let Some(background) = crate::entities::hatch::background_color(hatch) {
|
||||||
let mut backdrop = model.clone();
|
let mut backdrop = model.clone();
|
||||||
|
|
@ -63,7 +66,6 @@ impl Scene {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
models.push(model);
|
models.push(model);
|
||||||
self.preview_hatches = std::sync::Arc::new(models);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_preview_text(&mut self, verts: Vec<crate::scene::pipeline::text_gpu::TextVertex>) {
|
pub fn set_preview_text(&mut self, verts: Vec<crate::scene::pipeline::text_gpu::TextVertex>) {
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,9 @@ pub struct PropertiesPanel {
|
||||||
/// Which vertex a multi-vertex entity (polyline) is focused on — driven by
|
/// Which vertex a multi-vertex entity (polyline) is focused on — driven by
|
||||||
/// the Current Vertex ◀ / ▶ stepper. Reset to 0 when the selection changes.
|
/// the Current Vertex ◀ / ▶ stepper. Reset to 0 when the selection changes.
|
||||||
pub prop_vertex: usize,
|
pub prop_vertex: usize,
|
||||||
|
/// Draw the Current Vertex indicator only after the user changes the
|
||||||
|
/// stepper for the current selection.
|
||||||
|
pub prop_vertex_indicator_active: bool,
|
||||||
/// Coordinate groups ("Position", "Scale", …) the user expanded into their
|
/// Coordinate groups ("Position", "Scale", …) the user expanded into their
|
||||||
/// component X/Y/Z rows. Collapsed by default; keyed `section:base` and
|
/// component X/Y/Z rows. Collapsed by default; keyed `section:base` and
|
||||||
/// carried across panel rebuilds so the state survives edits and selection
|
/// carried across panel rebuilds so the state survives edits and selection
|
||||||
|
|
@ -319,6 +322,7 @@ impl Default for PropertiesPanel {
|
||||||
bg_color_picker_open: false,
|
bg_color_picker_open: false,
|
||||||
open_color_field: None,
|
open_color_field: None,
|
||||||
prop_vertex: 0,
|
prop_vertex: 0,
|
||||||
|
prop_vertex_indicator_active: false,
|
||||||
expanded_groups: HashSet::default(),
|
expanded_groups: HashSet::default(),
|
||||||
edit_choice_open: false,
|
edit_choice_open: false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue