diff --git a/ROADMAP.md b/ROADMAP.md index 8a6102736..9a1904ecc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -207,8 +207,8 @@ Underlay (PDF/DWF/DGN) |---------|-------| | Endpoint / Midpoint / Center / Quadrant / Intersection | ✅ | | Perpendicular / Tangent / Nearest | ✅ | -| Grid snap | ⬜ | -| Polar tracking | ⬜ | +| Grid snap | ✅ Zoom-adaptive spacing | +| Polar tracking | ✅ Configurable angle, guide line | | Object snap tracking | ⬜ | --- @@ -239,7 +239,7 @@ Underlay (PDF/DWF/DGN) | MVIEW (viewport oluşturma) | ✅ | | Per-viewport layer visibility | ✅ | | Plot ayarları (PlotSettings) | ✅ | -| Çoklu named layout sekmeleri | ⬜ | +| Çoklu named layout sekmeleri | ✅ | | VPLAYER — viewport katman override | 🔧 Altyapı var | | Layout Manager arayüzü | ⬜ | @@ -302,15 +302,11 @@ Underlay (PDF/DWF/DGN) ## Öncelik Sırası (Bir Sonraki Adımlar) ### Yüksek Öncelik -1. **Çoklu Layout sekmeleri** arayüzü +1. **XREF yönetimi** ### Orta Öncelik -5. **Grid snap + Polar tracking** -6. **XREF yönetimi** +5. **MASSPROP / DATAEXTRACTION** ### Düşük Öncelik -11. Grid snap + Polar tracking -12. XREF yönetimi -13. MASSPROP / DATAEXTRACTION -14. Fiziksel yazıcıya yazdırma -15. Boolean 3D operasyonlar +11. Fiziksel yazıcıya yazdırma +12. Boolean 3D operasyonlar diff --git a/src/app/mod.rs b/src/app/mod.rs index d7a3039f9..1fceb032f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -43,8 +43,10 @@ pub(super) struct H7CAD { pre_cmd_tangent: Option, /// Orthogonal drawing constraint (F8): constrains picks to 0°/90°/180°/270°. 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 tracking angle increment in degrees (15 / 30 / 45 / 90). + polar_increment_deg: f32, /// Show grid lines in the viewport (F7). show_grid: bool, /// Show the UCS icon in the bottom-left corner of model space (UCSICON). @@ -224,8 +226,10 @@ pub enum Message { ToggleGrid, /// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button. ToggleOrtho, - /// Toggle polar-angle constraint (45° increments) — F10 / POLAR status-bar button. + /// Toggle polar-angle constraint — F10 / POLAR status-bar button. TogglePolar, + /// Set polar tracking angle increment (right-click POLAR button). + SetPolarAngle(f32), /// Toggle an individual snap mode (from popup row click). ToggleSnap(crate::snap::SnapType), /// Open / close the OSNAP popup (▾ arrow click). @@ -429,6 +433,7 @@ impl H7CAD { pre_cmd_tangent: None, ortho_mode: false, polar_mode: false, + polar_increment_deg: 45.0, show_grid: false, show_ucs_icon: true, last_point: None, diff --git a/src/app/update.rs b/src/app/update.rs index da2e5e19b..21272b7f5 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -814,7 +814,7 @@ impl H7CAD { if self.ortho_mode { snapped = ortho_constrain(snapped, base); } 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) }; 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 // command previews and snapping work in the correct coordinate space. let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper); @@ -879,14 +882,14 @@ impl H7CAD { if self.ortho_mode { pt = ortho_constrain(pt, base); } else if self.polar_mode { - pt = polar_constrain(pt, base, 45.0); + pt = polar_constrain(pt, base, self.polar_increment_deg); } } pt }; self.tabs[i].last_cursor_world = effective; - let previews = if needs_entity { + let mut previews = if needs_entity { let hover_handle = scene::hit_test::click_hit(p, &all_wires, view_proj, bounds) .and_then(|s| Scene::handle_from_wire_name(s)) @@ -899,6 +902,37 @@ impl H7CAD { .map(|c| c.on_preview_wires(effective)) .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); } else { self.tabs[i].snap_result = None; @@ -1046,7 +1080,7 @@ impl H7CAD { if self.ortho_mode { pt = ortho_constrain(pt, base); } else if self.polar_mode { - pt = polar_constrain(pt, base, 45.0); + pt = polar_constrain(pt, base, self.polar_increment_deg); } } pt @@ -1481,6 +1515,12 @@ impl H7CAD { if self.polar_mode { self.ortho_mode = false; } 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::ToggleSnapPopup => { self.snap_popup_open ^= true; Task::none() } Message::CloseSnapPopup => { self.snap_popup_open = false; Task::none() } diff --git a/src/app/view.rs b/src/app/view.rs index e2ef42a15..b237381b1 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -177,6 +177,7 @@ impl H7CAD { self.snap_popup_open, self.ortho_mode, self.polar_mode, + self.polar_increment_deg, self.show_grid, tab.scene.layout_names(), tab.scene.current_layout.clone(), diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index f822c2598..2c900532c 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -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. diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index adcb1d81c..eb90635d6 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -26,6 +26,7 @@ impl StatusBar { popup_open: bool, ortho_mode: bool, polar_mode: bool, + polar_increment_deg: f32, show_grid: bool, layouts: Vec, 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.