feat: Grid snap adaptive spacing + Polar tracking improvements

Grid snap (F9):
  • snap spacing now tracks the visible grid zoom level — computed via
    compute_grid_step() (same adaptive pow-of-5 logic as draw_grid),
    updated on every ViewportMove so zooming in/out keeps snap aligned
    with the grid lines on screen

Polar tracking (F10):
  • configurable angle increment stored in polar_increment_deg (default 45°)
  • right-click the POLAR pill to cycle through 15 → 30 → 45 → 90°
  • pill label shows current angle ("POLAR 45°")
  • visual guide line: while drawing with polar on, a dashed cyan line is
    drawn from last_point along the snapped direction across the viewport
  • SetPolarAngle message + handler; all polar_constrain() calls use the
    stored increment instead of the hardcoded 45°

ROADMAP updated: Grid snap and Polar tracking marked ✅.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-07 19:30:25 +03:00
1 parent b3dcbe6eb9
commit 4c1c13cab6
6 files changed
+165 -21

No files matched your search

+34
View File
@@ -146,6 +146,40 @@ pub struct GridParams {
pub plane: GridPlane,
}
/// Compute the adaptive grid step size (world units) that the grid renderer
/// would use for a given view-projection matrix and viewport bounds.
///
/// Returns the smallest power-of-5 multiple of 1.0 that places grid lines at
/// least `MIN_GRID_PX` pixels apart. This matches exactly what `draw_grid`
/// renders, so callers can sync snap spacing to the visible grid.
pub fn compute_grid_step(vp: Mat4, bounds: iced::Rectangle) -> f32 {
use glam::Vec3;
let w2s = |world: Vec3| {
let ndc = vp.project_point3(world);
glam::Vec2::new(
(ndc.x + 1.0) * 0.5 * bounds.width,
(1.0 - ndc.y) * 0.5 * bounds.height,
)
};
let o = w2s(Vec3::ZERO);
let a1 = w2s(Vec3::X);
let a2 = w2s(Vec3::Y);
let d1 = (a1 - o).length();
let d2 = (a2 - o).length();
let px_per_unit = d1.max(d2);
if px_per_unit < 1e-6 {
return 1.0;
}
let mut s = 1.0_f32;
while s * px_per_unit < MIN_GRID_PX {
s *= 5.0;
if s > 1e9 {
return 1.0;
}
}
s
}
/// Parameters for the screen-space UCS icon drawn in the viewport corner.
pub struct UcsIconParams {
/// View-projection matrix used to project world axis directions to screen.
+72 -4
View File
@@ -26,6 +26,7 @@ impl StatusBar {
popup_open: bool,
ortho_mode: bool,
polar_mode: bool,
polar_increment_deg: f32,
show_grid: bool,
layouts: Vec<String>,
current_layout: String,
@@ -80,10 +81,7 @@ impl StatusBar {
toggle_pill("ORTHO", ortho_mode, Message::ToggleOrtho),
"Orthogonal Mode\nF8"
),
tip(
toggle_pill("POLAR", polar_mode, Message::TogglePolar),
"Polar Tracking (45°)\nF10"
),
polar_pill(polar_mode, polar_increment_deg),
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
status_pill(space_label),
status_pill(scale_label),
@@ -192,6 +190,76 @@ fn toggle_pill(label: &'static str, active: bool, msg: Message) -> Element<'stat
.into()
}
// ── Polar tracking pill ───────────────────────────────────────────────────
//
// Left-click toggles polar on/off.
// Right-click cycles through common angle increments: 15 → 30 → 45 → 90 → 15 …
fn polar_pill(active: bool, increment_deg: f32) -> Element<'static, Message> {
let label = format!("POLAR {:.0}°", increment_deg);
let tooltip_text = format!(
"Polar Tracking ({}°)\nF10 — left-click on/off\nRight-click to change angle",
increment_deg as u32
);
let bg_color = move |hovered: bool| {
match (active, hovered) {
(true, true) => SNAP_ON_HOVER,
(true, false) => SNAP_ON_BG,
(false, true) => SNAP_OFF_HOVER,
(false, false) => SNAP_OFF_BG,
}
};
// Cycle to the next common angle on right-click.
let next_angle = match increment_deg as u32 {
15 => 30.0_f32,
30 => 45.0,
45 => 90.0,
_ => 15.0,
};
let inner = container(
text(label).size(10).color(if active { OSNAP_ON_TEXT } else { OSNAP_OFF_TEXT })
)
.style(move |_: &Theme| container::Style {
background: Some(Background::Color(bg_color(false))),
border: Border {
color: if active { SNAP_BORDER_ON } else { BORDER_COLOR },
width: 1.0,
radius: 2.0.into(),
},
..Default::default()
})
.padding([2, 6]);
let pill = mouse_area(inner)
.on_press(Message::TogglePolar)
.on_right_press(Message::SetPolarAngle(next_angle));
tooltip(
pill,
container(text(tooltip_text).size(11).color(Color::WHITE))
.style(|_: &Theme| container::Style {
background: Some(Background::Color(Color {
r: 0.13,
g: 0.13,
b: 0.13,
a: 0.95,
})),
border: Border {
color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 },
width: 1.0,
radius: 3.0.into(),
},
..Default::default()
})
.padding([4, 8]),
TipPos::Top,
)
.into()
}
// ── OSNAP split button ────────────────────────────────────────────────────
//
// Left part ("⚡ OSNAP"): toggles the global snap on/off.