feat(grip): multi-functional hover popup menu
Phase 2 of the grip overhaul: when the cursor dwells on a selected entity's grip the overlay opens a popup menu with entity-specific options (Stretch / Add Vertex / Remove Vertex / Convert to Arc-Line / …). Picking an item dispatches `apply_grip_menu`; the default `Stretch` entry is a no-op that lets the user click the grip to start the normal drag. - `GripMenuItem` + `GripMenuAction` (full vocabulary covering stretch, add/remove vertex, convert to arc/line, reverse arrows, text-position variants, hatch parameters, tangent direction). - `Grippable` trait gains `grip_menu` + `apply_grip_menu` with trait-default `Stretch`-only impls so every existing entity gets the popup for free. - `EntityTypeOps` dispatches both methods across the same entity list as the existing grip / property machinery. - `OpenCADStudio` carries `grip_hover` (dwell tracker) + `grip_popup` (open menu). `update_grip_hover` runs from `ViewportMove`: hit-tests grips, refreshes the dwell timer, and opens the popup after 600 ms. Cursor drift past 80 px or a grip click dismisses it. - `view.rs` paints the popup as an absolute-positioned column of buttons at the grip's screen anchor; each button publishes `Message::GripMenuPick(idx)`. - `LwPolyline` overrides `Grippable` manually (skips the standard macro) so its vertex grip exposes Add/Remove Vertex and its segment-midpoint grip exposes Add Vertex + Convert to Arc/Line. `apply_grip_menu` performs the edits: vertex insertion at the chord midpoint inheriting the previous bulge, vertex removal, and bulge toggle for the arc/line conversion.
This commit is contained in:
parent
5755c2e130
commit
6c1462932d
6 changed files with 443 additions and 2 deletions
|
|
@ -23,6 +23,26 @@ pub struct TileDrag {
|
||||||
pub orient: TileEdgeOrient,
|
pub orient: TileEdgeOrient,
|
||||||
pub last_applied: f32,
|
pub last_applied: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Cursor dwelling over a grip. When `started.elapsed() >=` the popup
|
||||||
|
/// threshold the multi-functional menu opens (`grip_popup`).
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GripHover {
|
||||||
|
pub handle: acadrust::Handle,
|
||||||
|
pub grip_id: usize,
|
||||||
|
pub screen: iced::Point,
|
||||||
|
pub started: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open multi-functional-grip popup state.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GripPopup {
|
||||||
|
pub handle: acadrust::Handle,
|
||||||
|
pub grip_id: usize,
|
||||||
|
pub anchor: iced::Point,
|
||||||
|
pub items: Vec<crate::scene::object::GripMenuItem>,
|
||||||
|
pub selected: usize,
|
||||||
|
}
|
||||||
use crate::snap::Snapper;
|
use crate::snap::Snapper;
|
||||||
use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar};
|
use crate::ui::{AppMenu, CommandLine, Ribbon, StatusBar};
|
||||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
|
|
@ -94,6 +114,14 @@ pub(super) struct OpenCADStudio {
|
||||||
/// reverting to the command-default when `has_base` flips. Cleared
|
/// reverting to the command-default when `has_base` flips. Cleared
|
||||||
/// on point commit / command start. See #35.
|
/// on point commit / command start. See #35.
|
||||||
dyn_user_reshaped: bool,
|
dyn_user_reshaped: bool,
|
||||||
|
/// Grip the cursor is currently dwelling on. Set when the cursor
|
||||||
|
/// stops within `GRIP_THRESHOLD_PX` of a grip; cleared when it
|
||||||
|
/// drifts away. The instant lets `ViewportMove` detect when the
|
||||||
|
/// dwell crosses the popup-open threshold.
|
||||||
|
grip_hover: Option<GripHover>,
|
||||||
|
/// Open multi-functional grip popup. Persists across mouse moves
|
||||||
|
/// until dismissed (click outside, ESC, cursor leaves the grip).
|
||||||
|
grip_popup: Option<GripPopup>,
|
||||||
/// Show the UCS icon in the bottom-left corner of model space (UCSICON).
|
/// Show the UCS icon in the bottom-left corner of model space (UCSICON).
|
||||||
show_ucs_icon: bool,
|
show_ucs_icon: bool,
|
||||||
/// Whether the ViewCube 3D gizmo is visible in model space (NAVVCUBE).
|
/// Whether the ViewCube 3D gizmo is visible in model space (NAVVCUBE).
|
||||||
|
|
@ -422,6 +450,9 @@ pub enum Message {
|
||||||
ViewportScroll(mouse::ScrollDelta),
|
ViewportScroll(mouse::ScrollDelta),
|
||||||
ViewportExit,
|
ViewportExit,
|
||||||
ViewCubeSnap(CubeRegion),
|
ViewCubeSnap(CubeRegion),
|
||||||
|
/// User picked an item in the multi-functional grip popup menu —
|
||||||
|
/// the index is into `grip_popup.items`.
|
||||||
|
GripMenuPick(usize),
|
||||||
WindowResized(f32, f32),
|
WindowResized(f32, f32),
|
||||||
/// Enter pressed globally — finalises the active command (no text-input involvement).
|
/// Enter pressed globally — finalises the active command (no text-input involvement).
|
||||||
CommandFinalize,
|
CommandFinalize,
|
||||||
|
|
@ -770,6 +801,8 @@ impl OpenCADStudio {
|
||||||
awaiting_vports: false,
|
awaiting_vports: false,
|
||||||
tile_drag: None,
|
tile_drag: None,
|
||||||
dyn_user_reshaped: false,
|
dyn_user_reshaped: false,
|
||||||
|
grip_hover: None,
|
||||||
|
grip_popup: None,
|
||||||
show_ucs_icon: true,
|
show_ucs_icon: true,
|
||||||
show_viewcube: true,
|
show_viewcube: true,
|
||||||
show_properties: true,
|
show_properties: true,
|
||||||
|
|
|
||||||
|
|
@ -1766,6 +1766,11 @@ impl OpenCADStudio {
|
||||||
VIEWCUBE_PX,
|
VIEWCUBE_PX,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Multi-functional grip hover: detect cursor sitting on a
|
||||||
|
// selected entity's grip and, after a dwell, open the
|
||||||
|
// popup menu. See scene::object::GripMenuItem.
|
||||||
|
self.update_grip_hover(i, p);
|
||||||
|
|
||||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||||
sel.last_move_pos = Some(p);
|
sel.last_move_pos = Some(p);
|
||||||
|
|
||||||
|
|
@ -2269,6 +2274,8 @@ impl OpenCADStudio {
|
||||||
origin_world: world,
|
origin_world: world,
|
||||||
last_world: world,
|
last_world: world,
|
||||||
});
|
});
|
||||||
|
self.grip_hover = None;
|
||||||
|
self.grip_popup = None;
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3060,6 +3067,38 @@ impl OpenCADStudio {
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Message::GripMenuPick(idx) => {
|
||||||
|
let i = self.active_tab;
|
||||||
|
let Some(popup) = self.grip_popup.take() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
self.grip_hover = None;
|
||||||
|
let Some(item) = popup.items.get(idx).cloned() else {
|
||||||
|
return Task::none();
|
||||||
|
};
|
||||||
|
use crate::entities::traits::EntityTypeOps;
|
||||||
|
use crate::scene::object::GripMenuAction;
|
||||||
|
if matches!(item.action, GripMenuAction::Stretch) {
|
||||||
|
// Default action: nothing — user can click the grip
|
||||||
|
// to start the normal drag.
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.push_undo_snapshot(i, item.label);
|
||||||
|
if let Some(entity) = self
|
||||||
|
.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity_mut(popup.handle)
|
||||||
|
{
|
||||||
|
entity.apply_grip_menu(popup.grip_id, item.action);
|
||||||
|
}
|
||||||
|
self.tabs[i].scene.bump_geometry();
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.refresh_selected_grips();
|
||||||
|
self.refresh_properties();
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
|
||||||
// ── Snap / mode toggles ───────────────────────────────────────
|
// ── Snap / mode toggles ───────────────────────────────────────
|
||||||
Message::ToggleSnapEnabled => {
|
Message::ToggleSnapEnabled => {
|
||||||
self.snapper.toggle_global();
|
self.snapper.toggle_global();
|
||||||
|
|
@ -5469,6 +5508,108 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Track cursor dwell over a selected entity's grip. Sets
|
||||||
|
/// `grip_hover` while the cursor sits within `GRIP_THRESHOLD_PX` of
|
||||||
|
/// a grip and opens `grip_popup` once the dwell exceeds the
|
||||||
|
/// threshold. Cursor drift clears both.
|
||||||
|
fn update_grip_hover(&mut self, i: usize, p: iced::Point) {
|
||||||
|
const HOVER_OPEN_MS: u128 = 600;
|
||||||
|
const POPUP_DISMISS_PX: f32 = 80.0;
|
||||||
|
if self.tabs[i].active_cmd.is_some()
|
||||||
|
|| self.tabs[i].active_grip.is_some()
|
||||||
|
|| self.tabs[i].selected_grips.is_empty()
|
||||||
|
{
|
||||||
|
self.grip_hover = None;
|
||||||
|
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,
|
||||||
|
y: 0.0,
|
||||||
|
width: vw,
|
||||||
|
height: vh,
|
||||||
|
};
|
||||||
|
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
||||||
|
let hit = if is_paper {
|
||||||
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
|
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
|
||||||
|
let half_h = cam.ortho_size();
|
||||||
|
let half_w = half_h * aspect;
|
||||||
|
let tx = cam.target.x;
|
||||||
|
let ty = cam.target.y;
|
||||||
|
drop(cam);
|
||||||
|
find_hit_grip_paper(
|
||||||
|
p,
|
||||||
|
&self.tabs[i].selected_grips,
|
||||||
|
tx,
|
||||||
|
ty,
|
||||||
|
half_w,
|
||||||
|
half_h,
|
||||||
|
bounds,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||||
|
find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds)
|
||||||
|
};
|
||||||
|
match hit {
|
||||||
|
Some((grip_id, _, _)) => {
|
||||||
|
let same = self
|
||||||
|
.grip_hover
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |h| h.handle == handle && h.grip_id == grip_id);
|
||||||
|
if !same {
|
||||||
|
self.grip_hover = Some(super::GripHover {
|
||||||
|
handle,
|
||||||
|
grip_id,
|
||||||
|
screen: p,
|
||||||
|
started: std::time::Instant::now(),
|
||||||
|
});
|
||||||
|
self.grip_popup = None;
|
||||||
|
} else if let Some(h) = self.grip_hover.as_mut() {
|
||||||
|
h.screen = p;
|
||||||
|
}
|
||||||
|
// Open popup once dwell crosses the threshold.
|
||||||
|
if self.grip_popup.is_none()
|
||||||
|
&& self
|
||||||
|
.grip_hover
|
||||||
|
.as_ref()
|
||||||
|
.map_or(false, |h| h.started.elapsed().as_millis() >= HOVER_OPEN_MS)
|
||||||
|
{
|
||||||
|
let entity_opt = self.tabs[i].scene.document.get_entity(handle);
|
||||||
|
if let Some(e) = entity_opt {
|
||||||
|
use crate::entities::traits::EntityTypeOps;
|
||||||
|
let items = e.grip_menu(grip_id);
|
||||||
|
if !items.is_empty() {
|
||||||
|
self.grip_popup = Some(super::GripPopup {
|
||||||
|
handle,
|
||||||
|
grip_id,
|
||||||
|
anchor: p,
|
||||||
|
items,
|
||||||
|
selected: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.grip_hover = None;
|
||||||
|
if let Some(popup) = &self.grip_popup {
|
||||||
|
let dx = p.x - popup.anchor.x;
|
||||||
|
let dy = p.y - popup.anchor.y;
|
||||||
|
if (dx * dx + dy * dy).sqrt() > POPUP_DISMISS_PX {
|
||||||
|
self.grip_popup = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Re-run the active command's preview hook against the current
|
/// Re-run the active command's preview hook against the current
|
||||||
/// cursor world position. Keyboard-driven point commits (typed
|
/// cursor world position. Keyboard-driven point commits (typed
|
||||||
/// coordinates in the command line or dynamic input) don't fire a
|
/// coordinates in the command line or dynamic input) don't fire a
|
||||||
|
|
|
||||||
|
|
@ -684,6 +684,69 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Multi-functional grip popup (Phase 2). Floating column of
|
||||||
|
// menu items at the grip's screen position, with the selected
|
||||||
|
// item highlighted. Each item dispatches `GripMenuPick`.
|
||||||
|
if let Some(popup) = self.grip_popup.as_ref() {
|
||||||
|
if !tab.is_start {
|
||||||
|
let anchor_x = (popup.anchor.x + 12.0).max(0.0);
|
||||||
|
let anchor_y = (popup.anchor.y + 12.0).max(0.0);
|
||||||
|
let mut col = column![].spacing(0);
|
||||||
|
for (idx, item) in popup.items.iter().enumerate() {
|
||||||
|
let is_sel = idx == popup.selected;
|
||||||
|
let label = item.label;
|
||||||
|
let btn = button(text(label).size(12).color(Color::WHITE))
|
||||||
|
.on_press(Message::GripMenuPick(idx))
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(move |_: &Theme, status| iced::widget::button::Style {
|
||||||
|
background: Some(Background::Color(match (is_sel, status) {
|
||||||
|
(true, _) => Color {
|
||||||
|
r: 0.20,
|
||||||
|
g: 0.45,
|
||||||
|
b: 0.95,
|
||||||
|
a: 0.95,
|
||||||
|
},
|
||||||
|
(_, iced::widget::button::Status::Hovered) => Color {
|
||||||
|
r: 0.20,
|
||||||
|
g: 0.20,
|
||||||
|
b: 0.20,
|
||||||
|
a: 0.95,
|
||||||
|
},
|
||||||
|
_ => Color {
|
||||||
|
r: 0.10,
|
||||||
|
g: 0.10,
|
||||||
|
b: 0.10,
|
||||||
|
a: 0.92,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
border: Border {
|
||||||
|
color: Color {
|
||||||
|
r: 0.35,
|
||||||
|
g: 0.35,
|
||||||
|
b: 0.35,
|
||||||
|
a: 1.0,
|
||||||
|
},
|
||||||
|
width: 1.0,
|
||||||
|
radius: 0.0.into(),
|
||||||
|
},
|
||||||
|
text_color: Color::WHITE,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
col = col.push(iced::widget::opaque(btn));
|
||||||
|
}
|
||||||
|
let popup_layer = iced::widget::column![
|
||||||
|
Space::new().height(iced::Length::Fixed(anchor_y)),
|
||||||
|
iced::widget::row![
|
||||||
|
Space::new().width(iced::Length::Fixed(anchor_x)),
|
||||||
|
col,
|
||||||
|
],
|
||||||
|
]
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill);
|
||||||
|
viewport_stack = viewport_stack.push(popup_layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Properties / layers panels carry no useful state on the Start tab.
|
// Properties / layers panels carry no useful state on the Start tab.
|
||||||
// Replace the properties panel with a Recent Documents list there.
|
// Replace the properties panel with a Recent Documents list there.
|
||||||
let properties_el: Element<'_, Message> = if tab.is_start {
|
let properties_el: Element<'_, Message> = if tab.is_start {
|
||||||
|
|
|
||||||
|
|
@ -440,7 +440,121 @@ impl TruckConvertible for LwPolyline {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
crate::impl_entity_basics!(LwPolyline);
|
impl crate::entities::traits::Grippable for LwPolyline {
|
||||||
|
fn grips(&self) -> Vec<crate::scene::object::GripDef> {
|
||||||
|
grips(self)
|
||||||
|
}
|
||||||
|
fn apply_grip(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
apply: crate::scene::object::GripApply,
|
||||||
|
) {
|
||||||
|
apply_grip(self, grip_id, apply);
|
||||||
|
}
|
||||||
|
fn grip_menu(
|
||||||
|
&self,
|
||||||
|
grip_id: usize,
|
||||||
|
) -> Vec<crate::scene::object::GripMenuItem> {
|
||||||
|
use crate::scene::object::{GripMenuAction, GripMenuItem};
|
||||||
|
let n = self.vertices.len();
|
||||||
|
if grip_id < n {
|
||||||
|
// Vertex grip.
|
||||||
|
return vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
GripMenuItem { label: "Remove Vertex", action: GripMenuAction::RemoveVertex },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// Segment midpoint grip.
|
||||||
|
let seg = grip_id - n;
|
||||||
|
let is_arc = self
|
||||||
|
.vertices
|
||||||
|
.get(seg)
|
||||||
|
.map_or(false, |v| v.bulge.abs() >= 1e-9);
|
||||||
|
let convert = if is_arc {
|
||||||
|
GripMenuItem { label: "Convert to Line", action: GripMenuAction::ConvertToLine }
|
||||||
|
} else {
|
||||||
|
GripMenuItem { label: "Convert to Arc", action: GripMenuAction::ConvertToArc }
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch },
|
||||||
|
GripMenuItem { label: "Add Vertex", action: GripMenuAction::AddVertex },
|
||||||
|
convert,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
fn apply_grip_menu(
|
||||||
|
&mut self,
|
||||||
|
grip_id: usize,
|
||||||
|
action: crate::scene::object::GripMenuAction,
|
||||||
|
) {
|
||||||
|
use crate::scene::object::GripMenuAction as A;
|
||||||
|
let n = self.vertices.len();
|
||||||
|
match action {
|
||||||
|
A::Stretch => {}
|
||||||
|
A::AddVertex => {
|
||||||
|
// Insert a new vertex at the chord midpoint between the
|
||||||
|
// hovered vertex (or segment) and its neighbour. The new
|
||||||
|
// vertex inherits the previous vertex's bulge so an
|
||||||
|
// existing arc is split into two arcs of the same chord.
|
||||||
|
let (i0, i1) = if grip_id < n {
|
||||||
|
let i0 = grip_id;
|
||||||
|
let i1 = (grip_id + 1) % n;
|
||||||
|
(i0, i1)
|
||||||
|
} else {
|
||||||
|
let seg = grip_id - n;
|
||||||
|
(seg, (seg + 1) % n)
|
||||||
|
};
|
||||||
|
if i1 == 0 && !self.is_closed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v0 = &self.vertices[i0];
|
||||||
|
let v1 = &self.vertices[i1];
|
||||||
|
let mx = (v0.location.x + v1.location.x) * 0.5;
|
||||||
|
let my = (v0.location.y + v1.location.y) * 0.5;
|
||||||
|
let inherited = v0.clone();
|
||||||
|
let mut new_v = inherited.clone();
|
||||||
|
new_v.location.x = mx;
|
||||||
|
new_v.location.y = my;
|
||||||
|
let insert_at = (i0 + 1).min(self.vertices.len());
|
||||||
|
self.vertices.insert(insert_at, new_v);
|
||||||
|
}
|
||||||
|
A::RemoveVertex if grip_id < n && self.vertices.len() > 2 => {
|
||||||
|
self.vertices.remove(grip_id);
|
||||||
|
}
|
||||||
|
A::ConvertToArc if grip_id >= n => {
|
||||||
|
if let Some(v) = self.vertices.get_mut(grip_id - n) {
|
||||||
|
if v.bulge.abs() < 1e-9 {
|
||||||
|
v.bulge = 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
A::ConvertToLine if grip_id >= n => {
|
||||||
|
if let Some(v) = self.vertices.get_mut(grip_id - n) {
|
||||||
|
v.bulge = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::entities::traits::PropertyEditable for LwPolyline {
|
||||||
|
fn geometry_properties(
|
||||||
|
&self,
|
||||||
|
_text_style_names: &[String],
|
||||||
|
) -> crate::scene::object::PropSection {
|
||||||
|
properties(self)
|
||||||
|
}
|
||||||
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||||||
|
apply_geom_prop(self, field, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::entities::traits::Transformable for LwPolyline {
|
||||||
|
fn apply_transform(&mut self, t: &crate::command::EntityTransform) {
|
||||||
|
apply_transform(self, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> Vec<Vec<[f32; 2]>> {
|
pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> Vec<Vec<[f32; 2]>> {
|
||||||
let hw_const = (pl.constant_width / 2.0) as f32;
|
let hw_const = (pl.constant_width / 2.0) as f32;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ use acadrust::{CadDocument, EntityType};
|
||||||
|
|
||||||
use crate::command::EntityTransform;
|
use crate::command::EntityTransform;
|
||||||
use crate::scene::acad_to_truck::TruckEntity;
|
use crate::scene::acad_to_truck::TruckEntity;
|
||||||
use crate::scene::object::{GripApply, GripDef, PropSection};
|
use crate::scene::object::{
|
||||||
|
GripApply, GripDef, GripMenuAction, GripMenuItem, PropSection,
|
||||||
|
};
|
||||||
use crate::scene::tess_util::FallbackGeometry;
|
use crate::scene::tess_util::FallbackGeometry;
|
||||||
|
|
||||||
pub trait TruckConvertible {
|
pub trait TruckConvertible {
|
||||||
|
|
@ -20,6 +22,19 @@ pub trait FallbackTess {
|
||||||
pub trait Grippable {
|
pub trait Grippable {
|
||||||
fn grips(&self) -> Vec<GripDef>;
|
fn grips(&self) -> Vec<GripDef>;
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
||||||
|
/// Items shown in the hover popup that opens when the cursor
|
||||||
|
/// dwells on a grip. Default is a single `Stretch` entry — i.e.
|
||||||
|
/// the popup just confirms the default drag behaviour. Override
|
||||||
|
/// per entity to add Add Vertex / Convert to Arc / Reverse Arrows /
|
||||||
|
/// etc.
|
||||||
|
fn grip_menu(&self, _grip_id: usize) -> Vec<GripMenuItem> {
|
||||||
|
vec![GripMenuItem { label: "Stretch", action: GripMenuAction::Stretch }]
|
||||||
|
}
|
||||||
|
/// React to a popup-menu commit. Default no-op — `Stretch` is the
|
||||||
|
/// "do nothing extra" path, the normal drag still happens on click.
|
||||||
|
/// Entities override this to mutate themselves when the user picks
|
||||||
|
/// Add Vertex / Remove Vertex / Convert / Reverse / …
|
||||||
|
fn apply_grip_menu(&mut self, _grip_id: usize, _action: GripMenuAction) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PropertyEditable {
|
pub trait PropertyEditable {
|
||||||
|
|
@ -58,9 +73,11 @@ pub trait TextContent {
|
||||||
pub trait EntityTypeOps {
|
pub trait EntityTypeOps {
|
||||||
fn to_truck_entity(&self, document: &CadDocument) -> Option<TruckEntity>;
|
fn to_truck_entity(&self, document: &CadDocument) -> Option<TruckEntity>;
|
||||||
fn grips(&self) -> Vec<GripDef>;
|
fn grips(&self) -> Vec<GripDef>;
|
||||||
|
fn grip_menu(&self, grip_id: usize) -> Vec<GripMenuItem>;
|
||||||
fn geometry_properties(&self, text_style_names: &[String]) -> Option<PropSection>;
|
fn geometry_properties(&self, text_style_names: &[String]) -> Option<PropSection>;
|
||||||
fn apply_geom_prop(&mut self, field: &str, value: &str);
|
fn apply_geom_prop(&mut self, field: &str, value: &str);
|
||||||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply);
|
||||||
|
fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction);
|
||||||
fn apply_transform(&mut self, t: &EntityTransform);
|
fn apply_transform(&mut self, t: &EntityTransform);
|
||||||
fn mass_props(&self) -> Option<MassProps>;
|
fn mass_props(&self) -> Option<MassProps>;
|
||||||
fn text_content(&self) -> Option<String>;
|
fn text_content(&self) -> Option<String>;
|
||||||
|
|
@ -238,6 +255,38 @@ impl EntityTypeOps for EntityType {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn grip_menu(&self, grip_id: usize) -> Vec<GripMenuItem> {
|
||||||
|
dispatch!(self,
|
||||||
|
|e| Grippable::grip_menu(e, grip_id),
|
||||||
|
[
|
||||||
|
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||||
|
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||||
|
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||||
|
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||||
|
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||||
|
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||||
|
Underlay, Shape, Ole2Frame,
|
||||||
|
],
|
||||||
|
_ => vec![],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_grip_menu(&mut self, grip_id: usize, action: GripMenuAction) {
|
||||||
|
dispatch!(self,
|
||||||
|
|e| Grippable::apply_grip_menu(e, grip_id, action),
|
||||||
|
[
|
||||||
|
Line, Circle, Arc, Ellipse, LwPolyline, Polyline, Polyline2D,
|
||||||
|
Polyline3D, Ray, XLine, RasterImage, Wipeout,
|
||||||
|
AttributeDefinition, AttributeEntity, MLine, Tolerance,
|
||||||
|
Solid, Solid3D, Region, Body, Face3D, PolygonMesh,
|
||||||
|
PolyfaceMesh, Mesh, Table, Point, Spline, Text, MText,
|
||||||
|
Viewport, Insert, Leader, MultiLeader, Dimension, Hatch,
|
||||||
|
Underlay, Shape, Ole2Frame,
|
||||||
|
],
|
||||||
|
_ => {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_transform(&mut self, t: &EntityTransform) {
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
||||||
dispatch!(self,
|
dispatch!(self,
|
||||||
|e| Transformable::apply_transform(e, t),
|
|e| Transformable::apply_transform(e, t),
|
||||||
|
|
|
||||||
|
|
@ -96,3 +96,44 @@ pub enum GripApply {
|
||||||
/// Translate the whole object by this delta vector.
|
/// Translate the whole object by this delta vector.
|
||||||
Translate(Vec3),
|
Translate(Vec3),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One entry in the hover-popup menu that opens when the cursor dwells
|
||||||
|
/// on a grip. The `label` is the user-visible string; `action` is the
|
||||||
|
/// operation the entity will perform when the item is committed.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct GripMenuItem {
|
||||||
|
pub label: &'static str,
|
||||||
|
pub action: GripMenuAction,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All operations a grip popup menu can dispatch. Entity-specific code
|
||||||
|
/// in `apply_grip_menu` decodes these into edits. `Stretch` is the
|
||||||
|
/// default no-op-equivalent — picking it just starts the regular
|
||||||
|
/// stretch drag, identical to clicking the grip with no popup open.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum GripMenuAction {
|
||||||
|
Stretch,
|
||||||
|
Lengthen,
|
||||||
|
Radius,
|
||||||
|
ArcLength,
|
||||||
|
AddVertex,
|
||||||
|
RemoveVertex,
|
||||||
|
ConvertToArc,
|
||||||
|
ConvertToLine,
|
||||||
|
StretchVertex,
|
||||||
|
AddLeader,
|
||||||
|
RemoveLeader,
|
||||||
|
ReverseArrows,
|
||||||
|
MoveWithDimLine,
|
||||||
|
MoveWithLeader,
|
||||||
|
MoveIndependent,
|
||||||
|
ResetText,
|
||||||
|
RotateText,
|
||||||
|
AboveDimLine,
|
||||||
|
Center,
|
||||||
|
OriginPoint,
|
||||||
|
HatchAngle,
|
||||||
|
HatchScale,
|
||||||
|
HatchPattern,
|
||||||
|
TangentDirection,
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue