feat: Dynamic Input overlay (DYNMODE / F12)
- overlay.rs: dynamic_input_overlay() canvas widget draws a semi-transparent
coordinate tooltip near the cursor; shows X/Y in absolute mode or
dist/angle relative to last_point when a command base point exists
- statusbar: DYN toggle pill with F12 tooltip (calls ToggleDynInput)
- app/mod.rs: dyn_input: bool field (default true), ToggleDynInput message
- view.rs: builds DYN label and pushes overlay into viewport_stack when
dyn_input=true and a command is active
- document.rs: last_cursor_screen: iced::Point — updated on every ViewportMove
- ROADMAP.md: Dynamic Input overlay marked ✅
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f80d694818
commit
51e6c4b359
7 changed files with 127 additions and 2 deletions
|
|
@ -197,7 +197,7 @@ Underlay (PDF/DWF/DGN)
|
|||
| Named UCS kaydetme | ⬜ |
|
||||
| VPORTS (viewport bölme) | ⬜ |
|
||||
| Nesne snap izleme (Object Snap Tracking) | ⬜ |
|
||||
| Dynamic Input overlay | ⬜ |
|
||||
| Dynamic Input overlay | ✅ F12 toggle, absolute XY + relative dist/angle |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use acadrust::{CadDocument, Handle};
|
|||
use acadrust::tables::Ucs;
|
||||
use crate::linetypes;
|
||||
use std::path::PathBuf;
|
||||
use iced;
|
||||
|
||||
// ── Per-document tab state ─────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ pub(super) struct DocumentTab {
|
|||
pub(super) wireframe: bool,
|
||||
pub(super) visual_style: String,
|
||||
pub(super) last_cursor_world: glam::Vec3,
|
||||
pub(super) last_cursor_screen: iced::Point,
|
||||
pub(super) history: HistoryState,
|
||||
pub(super) active_layer: String,
|
||||
/// Currently active UCS. `None` means WCS (identity transform).
|
||||
|
|
@ -57,6 +59,7 @@ impl DocumentTab {
|
|||
wireframe: false,
|
||||
visual_style: "Shaded".into(),
|
||||
last_cursor_world: glam::Vec3::ZERO,
|
||||
last_cursor_screen: iced::Point::ORIGIN,
|
||||
history: HistoryState::default(),
|
||||
active_layer: "0".to_string(),
|
||||
active_ucs: None,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ pub(super) struct H7CAD {
|
|||
polar_increment_deg: f32,
|
||||
/// Show grid lines in the viewport (F7).
|
||||
show_grid: bool,
|
||||
/// Dynamic input overlay (F12): show coordinate tooltip near cursor.
|
||||
dyn_input: bool,
|
||||
/// Show the UCS icon in the bottom-left corner of model space (UCSICON).
|
||||
show_ucs_icon: bool,
|
||||
/// Last point committed by a drawing command — used as ortho/polar base.
|
||||
|
|
@ -230,6 +232,8 @@ pub enum Message {
|
|||
TogglePolar,
|
||||
/// Set polar tracking angle increment (right-click POLAR button).
|
||||
SetPolarAngle(f32),
|
||||
/// Toggle dynamic input overlay (F12).
|
||||
ToggleDynInput,
|
||||
/// Toggle an individual snap mode (from popup row click).
|
||||
ToggleSnap(crate::snap::SnapType),
|
||||
/// Open / close the OSNAP popup (▾ arrow click).
|
||||
|
|
@ -450,6 +454,7 @@ impl H7CAD {
|
|||
polar_mode: false,
|
||||
polar_increment_deg: 45.0,
|
||||
show_grid: false,
|
||||
dyn_input: true,
|
||||
show_ucs_icon: true,
|
||||
last_point: None,
|
||||
layer_window: None,
|
||||
|
|
|
|||
|
|
@ -1031,6 +1031,7 @@ impl H7CAD {
|
|||
pt
|
||||
};
|
||||
self.tabs[i].last_cursor_world = effective;
|
||||
self.tabs[i].last_cursor_screen = p;
|
||||
|
||||
let mut previews = if needs_entity {
|
||||
let hover_handle =
|
||||
|
|
@ -1658,6 +1659,7 @@ impl H7CAD {
|
|||
if self.polar_mode { self.ortho_mode = false; }
|
||||
Task::none()
|
||||
}
|
||||
Message::ToggleDynInput => { self.dyn_input ^= true; Task::none() }
|
||||
Message::SetPolarAngle(deg) => {
|
||||
self.polar_increment_deg = deg;
|
||||
self.polar_mode = true;
|
||||
|
|
|
|||
|
|
@ -135,7 +135,26 @@ impl H7CAD {
|
|||
.unwrap_or(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0 })
|
||||
};
|
||||
|
||||
let viewport_stack = stack![
|
||||
// Dynamic input overlay — shown when a command is active and DYN is on.
|
||||
let dyn_input_overlay: Option<Element<'_, Message>> =
|
||||
if self.dyn_input && tab.active_cmd.is_some() {
|
||||
let w = tab.last_cursor_world;
|
||||
let label = if let Some(base) = self.last_point {
|
||||
// Show relative distance + angle when we have a base point.
|
||||
let dx = (w.x - base.x) as f64;
|
||||
let dy = (w.z - base.z) as f64;
|
||||
let dist = (dx * dx + dy * dy).sqrt();
|
||||
let ang = dy.atan2(dx).to_degrees();
|
||||
format!("d={:.3} <{:.1}°", dist, ang)
|
||||
} else {
|
||||
format!("X:{:.3} Y:{:.3}", w.x, w.z)
|
||||
};
|
||||
Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, label))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut viewport_stack = stack![
|
||||
container(viewport_3d)
|
||||
.style(move |_: &Theme| container::Style {
|
||||
background: Some(Background::Color(bg_color)),
|
||||
|
|
@ -151,6 +170,9 @@ impl H7CAD {
|
|||
]
|
||||
.width(Fill)
|
||||
.height(Fill);
|
||||
if let Some(dyn_ol) = dyn_input_overlay {
|
||||
viewport_stack = viewport_stack.push(dyn_ol);
|
||||
}
|
||||
|
||||
let center_stack = iced::widget::stack![
|
||||
row![tab.properties.view(), viewport_stack]
|
||||
|
|
@ -179,6 +201,7 @@ impl H7CAD {
|
|||
self.polar_mode,
|
||||
self.polar_increment_deg,
|
||||
self.show_grid,
|
||||
self.dyn_input,
|
||||
tab.scene.layout_names(),
|
||||
tab.scene.current_layout.clone(),
|
||||
self.layout_rename_state.as_ref(),
|
||||
|
|
@ -342,6 +365,9 @@ impl H7CAD {
|
|||
keyboard::Key::Named(keyboard::key::Named::F10) => {
|
||||
Some(Message::TogglePolar)
|
||||
}
|
||||
keyboard::Key::Named(keyboard::key::Named::F12) => {
|
||||
Some(Message::ToggleDynInput)
|
||||
}
|
||||
keyboard::Key::Character(c) if ctrl => match c.as_str() {
|
||||
"n" => Some(Message::ClearScene),
|
||||
"o" => Some(Message::OpenFile),
|
||||
|
|
|
|||
|
|
@ -849,3 +849,87 @@ fn draw_ucs_icon(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle) {
|
|||
let circle = canvas::Path::circle(icon_origin, 3.0);
|
||||
frame.fill(&circle, Color { r: 0.9, g: 0.9, b: 0.9, a: 0.9 });
|
||||
}
|
||||
|
||||
// ── Dynamic Input overlay ─────────────────────────────────────────────────
|
||||
|
||||
/// Draw a small coordinate / distance-angle tooltip near the cursor.
|
||||
///
|
||||
/// `cursor_screen` — cursor position in viewport pixels.
|
||||
/// `label` — text to display (e.g. "X: 12.34 Y: 56.78").
|
||||
pub fn dynamic_input_overlay<'a>(
|
||||
cursor_screen: Point,
|
||||
label: String,
|
||||
) -> Element<'a, Message> {
|
||||
canvas(DynInputCanvas { cursor_screen, label })
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
struct DynInputCanvas {
|
||||
cursor_screen: Point,
|
||||
label: String,
|
||||
}
|
||||
|
||||
impl canvas::Program<Message> for DynInputCanvas {
|
||||
type State = ();
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_state: &(),
|
||||
_bounds: iced::Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> mouse::Interaction {
|
||||
mouse::Interaction::None
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_state: &(),
|
||||
renderer: &iced::Renderer,
|
||||
_theme: &Theme,
|
||||
bounds: iced::Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut frame = canvas::Frame::new(renderer, bounds.size());
|
||||
|
||||
// Offset the box 14 px right and 20 px below the cursor.
|
||||
const OFFSET_X: f32 = 14.0;
|
||||
const OFFSET_Y: f32 = 20.0;
|
||||
const PAD: f32 = 4.0;
|
||||
const FONT_SIZE: f32 = 11.0;
|
||||
const BOX_W: f32 = 160.0;
|
||||
const BOX_H: f32 = FONT_SIZE + PAD * 2.0;
|
||||
|
||||
let mut bx = self.cursor_screen.x + OFFSET_X;
|
||||
let mut by = self.cursor_screen.y + OFFSET_Y;
|
||||
|
||||
// Keep box inside viewport.
|
||||
if bx + BOX_W > bounds.width { bx = self.cursor_screen.x - BOX_W - 4.0; }
|
||||
if by + BOX_H > bounds.height { by = self.cursor_screen.y - BOX_H - 4.0; }
|
||||
|
||||
let bg = canvas::Path::rectangle(
|
||||
Point { x: bx, y: by },
|
||||
Size { width: BOX_W, height: BOX_H },
|
||||
);
|
||||
frame.fill(
|
||||
&bg,
|
||||
Color { r: 0.05, g: 0.05, b: 0.12, a: 0.85 },
|
||||
);
|
||||
frame.stroke(
|
||||
&bg,
|
||||
canvas::Stroke::default()
|
||||
.with_color(Color { r: 0.35, g: 0.55, b: 0.90, a: 0.9 })
|
||||
.with_width(1.0),
|
||||
);
|
||||
frame.fill_text(canvas::Text {
|
||||
content: self.label.clone(),
|
||||
position: Point { x: bx + PAD, y: by + PAD },
|
||||
color: Color { r: 0.90, g: 0.90, b: 0.90, a: 1.0 },
|
||||
size: iced::Pixels(FONT_SIZE),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ impl StatusBar {
|
|||
polar_mode: bool,
|
||||
polar_increment_deg: f32,
|
||||
show_grid: bool,
|
||||
dyn_input: bool,
|
||||
layouts: Vec<String>,
|
||||
current_layout: String,
|
||||
// If `Some((original, edit_value))`, the named tab shows a text input.
|
||||
|
|
@ -82,6 +83,10 @@ impl StatusBar {
|
|||
"Orthogonal Mode\nF8"
|
||||
),
|
||||
polar_pill(polar_mode, polar_increment_deg),
|
||||
tip(
|
||||
toggle_pill("DYN", dyn_input, Message::ToggleDynInput),
|
||||
"Dynamic Input\nF12"
|
||||
),
|
||||
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
||||
status_pill(space_label),
|
||||
status_pill(scale_label),
|
||||
|
|
|
|||
Loading…
Reference in a new issue