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 <noreply@anthropic.com>
This commit is contained in:
parent
b3dcbe6eb9
commit
4c1c13cab6
6 changed files with 165 additions and 21 deletions
18
ROADMAP.md
18
ROADMAP.md
|
|
@ -207,8 +207,8 @@ Underlay (PDF/DWF/DGN)
|
||||||
|---------|-------|
|
|---------|-------|
|
||||||
| Endpoint / Midpoint / Center / Quadrant / Intersection | ✅ |
|
| Endpoint / Midpoint / Center / Quadrant / Intersection | ✅ |
|
||||||
| Perpendicular / Tangent / Nearest | ✅ |
|
| Perpendicular / Tangent / Nearest | ✅ |
|
||||||
| Grid snap | ⬜ |
|
| Grid snap | ✅ Zoom-adaptive spacing |
|
||||||
| Polar tracking | ⬜ |
|
| Polar tracking | ✅ Configurable angle, guide line |
|
||||||
| Object snap tracking | ⬜ |
|
| Object snap tracking | ⬜ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -239,7 +239,7 @@ Underlay (PDF/DWF/DGN)
|
||||||
| MVIEW (viewport oluşturma) | ✅ |
|
| MVIEW (viewport oluşturma) | ✅ |
|
||||||
| Per-viewport layer visibility | ✅ |
|
| Per-viewport layer visibility | ✅ |
|
||||||
| Plot ayarları (PlotSettings) | ✅ |
|
| Plot ayarları (PlotSettings) | ✅ |
|
||||||
| Çoklu named layout sekmeleri | ⬜ |
|
| Çoklu named layout sekmeleri | ✅ |
|
||||||
| VPLAYER — viewport katman override | 🔧 Altyapı var |
|
| VPLAYER — viewport katman override | 🔧 Altyapı var |
|
||||||
| Layout Manager arayüzü | ⬜ |
|
| Layout Manager arayüzü | ⬜ |
|
||||||
|
|
||||||
|
|
@ -302,15 +302,11 @@ Underlay (PDF/DWF/DGN)
|
||||||
## Öncelik Sırası (Bir Sonraki Adımlar)
|
## Öncelik Sırası (Bir Sonraki Adımlar)
|
||||||
|
|
||||||
### Yüksek Öncelik
|
### Yüksek Öncelik
|
||||||
1. **Çoklu Layout sekmeleri** arayüzü
|
1. **XREF yönetimi**
|
||||||
|
|
||||||
### Orta Öncelik
|
### Orta Öncelik
|
||||||
5. **Grid snap + Polar tracking**
|
5. **MASSPROP / DATAEXTRACTION**
|
||||||
6. **XREF yönetimi**
|
|
||||||
|
|
||||||
### Düşük Öncelik
|
### Düşük Öncelik
|
||||||
11. Grid snap + Polar tracking
|
11. Fiziksel yazıcıya yazdırma
|
||||||
12. XREF yönetimi
|
12. Boolean 3D operasyonlar
|
||||||
13. MASSPROP / DATAEXTRACTION
|
|
||||||
14. Fiziksel yazıcıya yazdırma
|
|
||||||
15. Boolean 3D operasyonlar
|
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,10 @@ pub(super) struct H7CAD {
|
||||||
pre_cmd_tangent: Option<bool>,
|
pre_cmd_tangent: Option<bool>,
|
||||||
/// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°.
|
/// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°.
|
||||||
ortho_mode: bool,
|
ortho_mode: bool,
|
||||||
/// Polar tracking (F10): constrains picks to 45° angle increments.
|
/// Polar tracking (F10): constrains picks to configurable angle increments.
|
||||||
polar_mode: bool,
|
polar_mode: bool,
|
||||||
|
/// Polar tracking angle increment in degrees (15 / 30 / 45 / 90).
|
||||||
|
polar_increment_deg: f32,
|
||||||
/// Show grid lines in the viewport (F7).
|
/// Show grid lines in the viewport (F7).
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
/// 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).
|
||||||
|
|
@ -224,8 +226,10 @@ pub enum Message {
|
||||||
ToggleGrid,
|
ToggleGrid,
|
||||||
/// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button.
|
/// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button.
|
||||||
ToggleOrtho,
|
ToggleOrtho,
|
||||||
/// Toggle polar-angle constraint (45° increments) — F10 / POLAR status-bar button.
|
/// Toggle polar-angle constraint — F10 / POLAR status-bar button.
|
||||||
TogglePolar,
|
TogglePolar,
|
||||||
|
/// Set polar tracking angle increment (right-click POLAR button).
|
||||||
|
SetPolarAngle(f32),
|
||||||
/// Toggle an individual snap mode (from popup row click).
|
/// Toggle an individual snap mode (from popup row click).
|
||||||
ToggleSnap(crate::snap::SnapType),
|
ToggleSnap(crate::snap::SnapType),
|
||||||
/// Open / close the OSNAP popup (▾ arrow click).
|
/// Open / close the OSNAP popup (▾ arrow click).
|
||||||
|
|
@ -429,6 +433,7 @@ impl H7CAD {
|
||||||
pre_cmd_tangent: None,
|
pre_cmd_tangent: None,
|
||||||
ortho_mode: false,
|
ortho_mode: false,
|
||||||
polar_mode: false,
|
polar_mode: false,
|
||||||
|
polar_increment_deg: 45.0,
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
show_ucs_icon: true,
|
show_ucs_icon: true,
|
||||||
last_point: None,
|
last_point: None,
|
||||||
|
|
|
||||||
|
|
@ -814,7 +814,7 @@ impl H7CAD {
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
snapped = ortho_constrain(snapped, base);
|
snapped = ortho_constrain(snapped, base);
|
||||||
} else if self.polar_mode {
|
} else if self.polar_mode {
|
||||||
snapped = polar_constrain(snapped, base, 45.0);
|
snapped = polar_constrain(snapped, base, self.polar_increment_deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -846,6 +846,9 @@ impl H7CAD {
|
||||||
.pick_on_target_plane(p, bounds)
|
.pick_on_target_plane(p, bounds)
|
||||||
};
|
};
|
||||||
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||||
|
// Sync grid-snap spacing to the adaptive spacing of the visible grid.
|
||||||
|
self.snapper.grid_spacing =
|
||||||
|
crate::ui::overlay::compute_grid_step(view_proj, bounds);
|
||||||
// In MSPACE, map paper-space cursor to model space so that
|
// In MSPACE, map paper-space cursor to model space so that
|
||||||
// command previews and snapping work in the correct coordinate space.
|
// command previews and snapping work in the correct coordinate space.
|
||||||
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);
|
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);
|
||||||
|
|
@ -879,14 +882,14 @@ impl H7CAD {
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
pt = ortho_constrain(pt, base);
|
pt = ortho_constrain(pt, base);
|
||||||
} else if self.polar_mode {
|
} else if self.polar_mode {
|
||||||
pt = polar_constrain(pt, base, 45.0);
|
pt = polar_constrain(pt, base, self.polar_increment_deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pt
|
pt
|
||||||
};
|
};
|
||||||
self.tabs[i].last_cursor_world = effective;
|
self.tabs[i].last_cursor_world = effective;
|
||||||
|
|
||||||
let previews = if needs_entity {
|
let mut previews = if needs_entity {
|
||||||
let hover_handle =
|
let hover_handle =
|
||||||
scene::hit_test::click_hit(p, &all_wires, view_proj, bounds)
|
scene::hit_test::click_hit(p, &all_wires, view_proj, bounds)
|
||||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||||
|
|
@ -899,6 +902,37 @@ impl H7CAD {
|
||||||
.map(|c| c.on_preview_wires(effective))
|
.map(|c| c.on_preview_wires(effective))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
// Polar tracking guide line: dotted line from last_point along
|
||||||
|
// the snapped angle direction, extending across the drawing.
|
||||||
|
if self.polar_mode && !needs_entity {
|
||||||
|
if let Some(base) = self.last_point {
|
||||||
|
let dx = effective.x - base.x;
|
||||||
|
let dy = effective.y - base.y;
|
||||||
|
if (dx * dx + dy * dy).sqrt() > 1e-4 {
|
||||||
|
let far = 1e5_f32;
|
||||||
|
let dir = glam::Vec3::new(dx, dy, 0.0).normalize();
|
||||||
|
let far_pos = base + dir * far;
|
||||||
|
let far_neg = base - dir * far;
|
||||||
|
let guide = crate::scene::WireModel {
|
||||||
|
name: "__polar_guide__".into(),
|
||||||
|
points: vec![
|
||||||
|
[far_neg.x, far_neg.y, far_neg.z],
|
||||||
|
[far_pos.x, far_pos.y, far_pos.z],
|
||||||
|
],
|
||||||
|
color: [0.2, 0.7, 0.9, 0.6],
|
||||||
|
selected: false,
|
||||||
|
aci: 0,
|
||||||
|
pattern_length: 0.8,
|
||||||
|
pattern: [0.5, -0.3, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||||
|
line_weight_px: 1.0,
|
||||||
|
snap_pts: vec![],
|
||||||
|
tangent_geoms: vec![],
|
||||||
|
key_vertices: vec![],
|
||||||
|
};
|
||||||
|
previews.push(guide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
self.tabs[i].scene.set_preview_wires(previews);
|
self.tabs[i].scene.set_preview_wires(previews);
|
||||||
} else {
|
} else {
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|
@ -1046,7 +1080,7 @@ impl H7CAD {
|
||||||
if self.ortho_mode {
|
if self.ortho_mode {
|
||||||
pt = ortho_constrain(pt, base);
|
pt = ortho_constrain(pt, base);
|
||||||
} else if self.polar_mode {
|
} else if self.polar_mode {
|
||||||
pt = polar_constrain(pt, base, 45.0);
|
pt = polar_constrain(pt, base, self.polar_increment_deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pt
|
pt
|
||||||
|
|
@ -1481,6 +1515,12 @@ impl H7CAD {
|
||||||
if self.polar_mode { self.ortho_mode = false; }
|
if self.polar_mode { self.ortho_mode = false; }
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
Message::SetPolarAngle(deg) => {
|
||||||
|
self.polar_increment_deg = deg;
|
||||||
|
self.polar_mode = true;
|
||||||
|
self.ortho_mode = false;
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
Message::ToggleSnap(t) => { self.snapper.toggle(t); Task::none() }
|
Message::ToggleSnap(t) => { self.snapper.toggle(t); Task::none() }
|
||||||
Message::ToggleSnapPopup => { self.snap_popup_open ^= true; Task::none() }
|
Message::ToggleSnapPopup => { self.snap_popup_open ^= true; Task::none() }
|
||||||
Message::CloseSnapPopup => { self.snap_popup_open = false; Task::none() }
|
Message::CloseSnapPopup => { self.snap_popup_open = false; Task::none() }
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,7 @@ impl H7CAD {
|
||||||
self.snap_popup_open,
|
self.snap_popup_open,
|
||||||
self.ortho_mode,
|
self.ortho_mode,
|
||||||
self.polar_mode,
|
self.polar_mode,
|
||||||
|
self.polar_increment_deg,
|
||||||
self.show_grid,
|
self.show_grid,
|
||||||
tab.scene.layout_names(),
|
tab.scene.layout_names(),
|
||||||
tab.scene.current_layout.clone(),
|
tab.scene.current_layout.clone(),
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,40 @@ pub struct GridParams {
|
||||||
pub plane: GridPlane,
|
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.
|
/// Parameters for the screen-space UCS icon drawn in the viewport corner.
|
||||||
pub struct UcsIconParams {
|
pub struct UcsIconParams {
|
||||||
/// View-projection matrix used to project world axis directions to screen.
|
/// View-projection matrix used to project world axis directions to screen.
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ impl StatusBar {
|
||||||
popup_open: bool,
|
popup_open: bool,
|
||||||
ortho_mode: bool,
|
ortho_mode: bool,
|
||||||
polar_mode: bool,
|
polar_mode: bool,
|
||||||
|
polar_increment_deg: f32,
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
layouts: Vec<String>,
|
layouts: Vec<String>,
|
||||||
current_layout: String,
|
current_layout: String,
|
||||||
|
|
@ -80,10 +81,7 @@ impl StatusBar {
|
||||||
toggle_pill("ORTHO", ortho_mode, Message::ToggleOrtho),
|
toggle_pill("ORTHO", ortho_mode, Message::ToggleOrtho),
|
||||||
"Orthogonal Mode\nF8"
|
"Orthogonal Mode\nF8"
|
||||||
),
|
),
|
||||||
tip(
|
polar_pill(polar_mode, polar_increment_deg),
|
||||||
toggle_pill("POLAR", polar_mode, Message::TogglePolar),
|
|
||||||
"Polar Tracking (45°)\nF10"
|
|
||||||
),
|
|
||||||
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
osnap_btn(osnap_active, snapper.snap_enabled, popup_open),
|
||||||
status_pill(space_label),
|
status_pill(space_label),
|
||||||
status_pill(scale_label),
|
status_pill(scale_label),
|
||||||
|
|
@ -192,6 +190,76 @@ fn toggle_pill(label: &'static str, active: bool, msg: Message) -> Element<'stat
|
||||||
.into()
|
.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 ────────────────────────────────────────────────────
|
// ── OSNAP split button ────────────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Left part ("⚡ OSNAP"): toggles the global snap on/off.
|
// Left part ("⚡ OSNAP"): toggles the global snap on/off.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue