mod command; mod entities; mod io; mod linetypes; mod modules; mod patterns; mod scene; mod snap; mod ui; use acadrust::types::{Color as AcadColor, LineWeight}; use acadrust::{CadDocument, EntityType, Handle}; use command::{CadCommand, CmdResult}; use modules::ModuleEvent; use scene::dispatch; use scene::grip::{find_hit_grip, grips_to_screen, GripEdit}; use scene::object::GripApply; use snap::{SnapResult, Snapper}; use std::collections::HashSet; use std::path::PathBuf; use scene::{CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX}; use ui::{overlay, AppMenu, CommandLine, LayerPanel, PropertiesPanel, Ribbon, StatusBar}; use iced::time::Instant; use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row}; use iced::window; use iced::{mouse, Background, Border, Color, Element, Fill, Point, Subscription, Task, Theme}; const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX; const POLY_START_DELAY_MS: u128 = 150; const VARIES_LABEL: &str = "*VARIES*"; /// Parse a typed coordinate string into a world-space Vec3. /// Accepts "x,y" → Vec3(x, 0, y) (XZ drawing plane, Y-up) /// "x,y,z"→ Vec3(x, y, z) (full 3D) /// Separators: comma or semicolon; decimal point or decimal comma. fn parse_coord(text: &str) -> Option { // Split on comma or semicolon, ignore spaces. let parts: Vec = text .split(|c| c == ',' || c == ';') .map(|s| s.trim().replace(',', ".")) .filter_map(|s| s.parse().ok()) .collect(); match parts.as_slice() { [x, y] => Some(glam::Vec3::new(*x, *y, 0.0)), [x, y, z] => Some(glam::Vec3::new(*x, *y, *z)), _ => None, } } fn angle_close(a: f32, b: f32, tol: f32) -> bool { let diff = (a - b).rem_euclid(std::f32::consts::TAU); let diff = if diff > std::f32::consts::PI { diff - std::f32::consts::TAU } else { diff }; diff.abs() < tol } fn main() -> iced::Result { iced::daemon(H7CAD::boot, H7CAD::update, H7CAD::view) .subscription(H7CAD::subscription) .title(|state: &H7CAD, window_id: window::Id| { if Some(window_id) == state.layer_window { "Layer Properties Manager".to_string() } else if let Some(tab) = state.tabs.get(state.active_tab) { let dot = if tab.dirty { "● " } else { "" }; let name = tab.tab_display_name(); format!("{}H7CAD — {}", dot, name) } else { "H7CAD".to_string() } }) .theme(Theme::Dark) .run() } // ── Per-document tab state ───────────────────────────────────────────────── struct DocumentTab { scene: Scene, current_path: Option, dirty: bool, tab_title: String, properties: PropertiesPanel, layers: LayerPanel, active_cmd: Option>, last_cmd: Option, snap_result: Option, active_grip: Option, selected_grips: Vec, selected_handle: Option, wireframe: bool, visual_style: String, last_cursor_world: glam::Vec3, history: HistoryState, active_layer: String, } impl DocumentTab { fn new_drawing(n: usize) -> Self { let mut scene = Scene::new(); linetypes::populate_document(&mut scene.document); Self { scene, current_path: None, dirty: false, tab_title: format!("Drawing{}", n), properties: PropertiesPanel::empty(), layers: LayerPanel::default(), active_cmd: None, last_cmd: None, snap_result: None, active_grip: None, selected_grips: vec![], selected_handle: None, wireframe: false, visual_style: "Shaded".into(), last_cursor_world: glam::Vec3::ZERO, history: HistoryState::default(), active_layer: "0".to_string(), } } fn tab_display_name(&self) -> String { match &self.current_path { Some(p) => p .file_name() .unwrap_or_default() .to_string_lossy() .to_string(), None => self.tab_title.clone(), } } } #[derive(Clone)] struct HistorySnapshot { document: CadDocument, current_layout: String, selected: Vec, dirty: bool, label: String, } #[derive(Default)] struct HistoryState { undo_stack: Vec, redo_stack: Vec, } // ── Application state ────────────────────────────────────────────────────── struct H7CAD { start: Instant, tabs: Vec, active_tab: usize, tab_counter: usize, ribbon: Ribbon, app_menu: AppMenu, command_line: CommandLine, status_bar: StatusBar, cursor_pos: Point, vp_size: (f32, f32), snapper: Snapper, snap_popup_open: bool, /// Whether Tangent snap was enabled before a tangent-pick command started. 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_mode: bool, /// Show grid lines in the viewport (F7). show_grid: bool, /// Last point committed by a drawing command — used as ortho/polar base. last_point: Option, /// OS window Id for the floating Layer Properties Manager (None when closed). layer_window: Option, /// OS window Id of the primary application window. main_window: Option, /// In-memory clipboard: cloned entities waiting to be pasted. clipboard: Vec, /// Centroid of the clipboard entities (XZ plane, Y-up). clipboard_centroid: glam::Vec3, } #[derive(Debug, Clone)] pub enum Message { Tick(Instant), OpenFile, FileOpened(Result<(String, PathBuf, CadDocument), String>), SaveFile, SaveAs, PickedSavePath(Option), ClearScene, SetWireframe(bool), /// Switch camera projection: true = Orthographic, false = Perspective. SetProjection(bool), /// Select a ribbon module tab by index. RibbonSelectTab(usize), /// A ribbon tool button was clicked — highlights the tool and dispatches its event. RibbonToolClick { tool_id: String, event: ModuleEvent, }, // ── Application menu ────────────────────────────────────────────────── ToggleAppMenu, CloseAppMenu, /// Close the menu and immediately dispatch a CAD command. CloseAppMenuAndRun(String), AppMenuSearch(String), // ── Document tabs ────────────────────────────────────────────────────── /// Create a new empty document tab. TabNew, /// Switch to the given tab index. TabSwitch(usize), /// Close the given tab index. TabClose(usize), // ───────────────────────────────────────────────────────────────────── CommandInput(String), CommandSubmit, Command(String), ToggleLayers, /// Emitted when the main application window has been opened (daemon boot). MainWindowOpened(window::Id), /// Emitted when the layer window has been successfully opened. LayerWindowOpened(window::Id), LayerToggleVisible(usize), LayerToggleLock(usize), LayerToggleFreeze(usize), LayerNew, LayerDelete, LayerSetCurrent, LayerSelect(usize), LayerRenameStart(usize), LayerRenameEdit(String), LayerColorPickerToggle(usize), LayerColorMorePalette, LayerColorSet(u8), LayerLinetypeSet(String), LayerLineweightSet(LineWeight), LayerTransparencyEdit(usize, String), LayerRenameCommit, CursorMoved(Point), ViewportClick, ViewportMove(Point), ViewportLeftPress, ViewportLeftRelease, ViewportRightPress, ViewportRightRelease, ViewportMiddlePress, ViewportMiddleRelease, ViewportScroll(mouse::ScrollDelta), ViewportExit, ViewCubeSnap(CubeRegion), WindowResized(f32, f32), /// Enter pressed globally — finalises the active command (no text-input involvement). CommandFinalize, /// Escape pressed globally — cancels the active command. CommandEscape, /// Toggle the global snap on/off (OSNAP button body click). ToggleSnapEnabled, /// Toggle grid-snap on/off — F9 / SNAP status-bar button. ToggleGridSnap, /// Toggle grid display in the viewport — F7 / GRID status-bar button. ToggleGrid, /// Toggle orthogonal drawing constraint — F8 / ORTHO status-bar button. ToggleOrtho, /// Toggle polar-angle constraint (45° increments) — F10 / POLAR status-bar button. TogglePolar, /// Toggle an individual snap mode (from popup row click). ToggleSnap(snap::SnapType), /// Open / close the OSNAP popup (▾ arrow click). ToggleSnapPopup, /// Close the OSNAP popup (click-catcher outside the panel). CloseSnapPopup, /// Enable all snap modes. SnapSelectAll, /// Disable all snap modes. SnapClearAll, /// Toggle a ribbon dropdown open/closed. ToggleRibbonDropdown(String), /// Close any open ribbon dropdown (click-catcher outside the panel). CloseRibbonDropdown, /// User selected a specific item from a ribbon dropdown. DropdownSelectItem { dropdown_id: &'static str, cmd: &'static str, }, /// Delete key — erase all currently selected entities. DeleteSelected, Undo, Redo, UndoMany(usize), RedoMany(usize), // ── Ribbon ──────────────────────────────────────────────────────────── /// User selected a layer from the layer combobox in the ribbon. RibbonLayerChanged(String), /// User changed the active color in the Properties toolbar. RibbonColorChanged(AcadColor), /// Toggle the full ACI palette inside the ribbon color picker. RibbonColorPaletteToggle, /// User changed the active linetype in the Properties toolbar. RibbonLinetypeChanged(String), /// User changed the active lineweight in the Properties toolbar. RibbonLineweightChanged(LineWeight), // ── Properties panel ────────────────────────────────────────────────── /// User selected a layer from the layer pick_list in the Properties panel. PropLayerChanged(String), PropSelectionGroupChanged(ui::properties::SelectionGroup), /// User picked a color from the Properties color picker. PropColorChanged(AcadColor), /// User selected a lineweight from the Properties pick_list. PropLwChanged(LineWeight), /// User selected a linetype from the linetype pick_list. PropLinetypeChanged(String), /// User toggled a boolean property (e.g. Invisible). PropBoolToggle(&'static str), /// User selected a hatch pattern from the pattern pick_list in Properties. PropHatchPatternChanged(String), /// User selected a generic choice field in the Properties panel. PropGeomChoiceChanged { field: &'static str, value: String, }, /// User is typing in an editable geometry field (live buffer update). PropGeomInput { field: &'static str, value: String, }, /// User committed a geometry/common field edit (Enter pressed). PropGeomCommit(&'static str), /// Toggle the inline color picker dropdown open/closed. PropColorPickerToggle, /// Toggle the full ACI colour palette expansion. PropColorPaletteToggle, /// Switch to a named layout ("Model" or paper space layout name). LayoutSwitch(String), /// Create a new paper space layout. LayoutCreate, /// A window was closed by the OS (e.g. the user clicked the title-bar ✕). OsWindowClosed(window::Id), /// No-op — used as a fallback when a TabEvent has no host mapping. Noop, } impl H7CAD { fn new() -> Self { let first_tab = DocumentTab::new_drawing(1); let mut app = Self { start: Instant::now(), tabs: vec![first_tab], active_tab: 0, tab_counter: 1, ribbon: Ribbon::new(), app_menu: AppMenu::new(), command_line: CommandLine::new(), status_bar: StatusBar::new(), cursor_pos: Point::ORIGIN, vp_size: (1280.0, 720.0), snapper: Snapper::default(), snap_popup_open: false, pre_cmd_tangent: None, ortho_mode: false, polar_mode: false, show_grid: false, last_point: None, layer_window: None, main_window: None, clipboard: Vec::new(), clipboard_centroid: glam::Vec3::ZERO, }; app.sync_ribbon_layers(); app } /// Boot function for `iced::daemon`: returns initial state plus a task that /// opens the primary application window. fn boot() -> (Self, Task) { let state = Self::new(); let (id, open_task) = window::open(window::Settings { maximized: true, icon: window::icon::from_rgba(build_window_icon(), 32, 32).ok(), ..Default::default() }); let mut s = state; s.main_window = Some(id); let task = open_task.map(|_| Message::Noop); (s, task) } fn update(&mut self, msg: Message) -> Task { match msg { Message::Tick(t) => { self.tabs[self.active_tab].scene.update(t - self.start); Task::none() } Message::OpenFile => Task::perform(io::pick_and_open(), Message::FileOpened), Message::FileOpened(Ok((name, path, doc))) => { let entity_count = doc.entities().count(); self.command_line .push_output(&format!("Opened \"{name}\" — {entity_count} entities")); self.app_menu.push_recent(path.clone()); // Eğer aktif sekme boşsa (kaydedilmemiş, yeni çizim, entity yok) dosyayı oraya yükle. // Aksi hâlde yeni bir sekme aç. let current_is_empty = { let t = &self.tabs[self.active_tab]; t.current_path.is_none() && !t.dirty && self.tabs[self.active_tab].scene.document.entities().count() == 0 }; let i = if current_is_empty { self.active_tab } else { self.tab_counter += 1; let new_tab = DocumentTab::new_drawing(self.tab_counter); self.tabs.push(new_tab); let idx = self.tabs.len() - 1; self.active_tab = idx; idx }; self.tabs[i].current_path = Some(path); self.tabs[i].scene.document = doc; self.tabs[i].scene.populate_hatches_from_document(); self.tabs[i].scene.selected = std::collections::HashSet::new(); self.tabs[i].scene.preview_wires = vec![]; self.tabs[i].scene.current_layout = "Model".to_string(); // Merge standard linetypes not already present in the file. linetypes::populate_document(&mut self.tabs[i].scene.document); self.tabs[i].properties = PropertiesPanel::empty(); let doc_layers = self.tabs[i].scene.document.layers.clone(); self.tabs[i].layers.sync_from_doc(&doc_layers); self.sync_ribbon_layers(); self.tabs[i].scene.fit_all(); self.tabs[i].dirty = false; self.tabs[i].history = HistoryState::default(); self.refresh_selected_grips(); Task::none() } Message::FileOpened(Err(e)) => { if e != "Cancelled" { self.command_line.push_error(&format!("Open failed: {e}")); } Task::none() } Message::SaveFile => { let i = self.active_tab; if let Some(path) = &self.tabs[i].current_path { let path = path.clone(); match io::save(&self.tabs[i].scene.document, &path) { Ok(()) => { self.command_line .push_output(&format!("Saved: {}", path.display())); self.tabs[i].dirty = false; } Err(e) => self.command_line.push_error(&format!("Save failed: {e}")), } } else { return Task::perform(io::pick_save_path(), Message::PickedSavePath); } Task::none() } Message::SaveAs => Task::perform(io::pick_save_path(), Message::PickedSavePath), Message::PickedSavePath(Some(path)) => { let i = self.active_tab; match io::save(&self.tabs[i].scene.document, &path) { Ok(()) => { self.command_line .push_output(&format!("Saved: {}", path.display())); self.tabs[i].current_path = Some(path); self.tabs[i].dirty = false; } Err(e) => self.command_line.push_error(&format!("Save failed: {e}")), } Task::none() } Message::PickedSavePath(None) => Task::none(), Message::ClearScene => { let i = self.active_tab; self.push_undo_snapshot(i, "NEW"); self.tabs[i].scene.clear(); linetypes::populate_document(&mut self.tabs[i].scene.document); self.tabs[i].properties = PropertiesPanel::empty(); let doc_layers = self.tabs[i].scene.document.layers.clone(); self.tabs[i].layers.sync_from_doc(&doc_layers); self.command_line .push_output("Scene cleared. Standard linetypes loaded."); self.tabs[i].current_path = None; self.tabs[i].dirty = true; self.sync_ribbon_layers(); Task::none() } Message::SetWireframe(w) => { let i = self.active_tab; self.tabs[i].wireframe = w; self.ribbon.set_wireframe(w); self.tabs[i].visual_style = if w { "Wireframe".into() } else { "Shaded".into() }; self.command_line.push_output(if w { "Visual style: Wireframe" } else { "Visual style: Shaded" }); Task::none() } Message::SetProjection(ortho) => { use crate::scene::Projection; let proj = if ortho { Projection::Orthographic } else { Projection::Perspective }; let i = self.active_tab; self.tabs[i].scene.camera.borrow_mut().projection = proj; self.tabs[i].scene.camera_generation += 1; self.ribbon.set_ortho(ortho); self.command_line.push_output(if ortho { "Projection: Orthographic" } else { "Projection: Perspective" }); Task::none() } Message::RibbonSelectTab(idx) => { self.ribbon.select(idx); Task::none() } Message::RibbonToolClick { tool_id, event } => { self.ribbon.activate_tool(&tool_id); match event { ModuleEvent::Command(cmd) => return self.dispatch_command(&cmd), ModuleEvent::OpenFileDialog => { self.command_line .push_info("Open DWG/DXF: not yet implemented."); } ModuleEvent::ClearModels => { let i = self.active_tab; self.tabs[i].scene.clear(); self.tabs[i].properties = PropertiesPanel::empty(); self.command_line.push_output("Scene cleared."); } ModuleEvent::SetWireframe(w) => { let i = self.active_tab; self.tabs[i].wireframe = w; self.ribbon.set_wireframe(w); self.tabs[i].visual_style = if w { "Wireframe".into() } else { "Shaded".into() }; self.command_line.push_output(if w { "Visual style: Wireframe" } else { "Visual style: Shaded" }); } ModuleEvent::ToggleLayers => { return Task::done(Message::ToggleLayers); } } Task::none() } // ── Application menu ────────────────────────────────────────── Message::ToggleAppMenu => { self.app_menu.toggle(); Task::none() } Message::CloseAppMenu => { self.app_menu.close(); Task::none() } Message::CloseAppMenuAndRun(cmd) => { self.app_menu.close(); self.dispatch_command(&cmd.clone()) } Message::AppMenuSearch(s) => { self.app_menu.search = s; Task::none() } // ── Document tabs ───────────────────────────────────────────── Message::TabNew => { self.tab_counter += 1; let new_tab = DocumentTab::new_drawing(self.tab_counter); self.tabs.push(new_tab); self.active_tab = self.tabs.len() - 1; self.sync_ribbon_layers(); Task::none() } Message::TabSwitch(idx) => { if idx < self.tabs.len() { self.active_tab = idx; self.sync_ribbon_layers(); } Task::none() } Message::TabClose(idx) => { if self.tabs.len() == 1 { // Only one tab — reset it to a fresh drawing instead of closing self.tab_counter += 1; self.tabs[0] = DocumentTab::new_drawing(self.tab_counter); self.active_tab = 0; } else { self.tabs.remove(idx); if self.active_tab >= self.tabs.len() { self.active_tab = self.tabs.len() - 1; } } Task::none() } // ───────────────────────────────────────────────────────────── Message::CommandInput(s) => { self.command_line.input = s; Task::none() } Message::CommandSubmit => { let i = self.active_tab; if self.tabs[i].active_cmd.is_some() { let text = self.command_line.input.trim().to_string(); self.command_line.input.clear(); // 1. Command explicitly owns text input (e.g. TTR radius prompt). if self.tabs[i] .active_cmd .as_ref() .map(|c| c.wants_text_input()) .unwrap_or(false) { if let Some(result) = self.tabs[i] .active_cmd .as_mut() .and_then(|c| c.on_text_input(&text)) { return self.apply_cmd_result(result); } // Step accepted but no final result yet — show updated prompt and refresh preview. let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } let pt = self.tabs[i].last_cursor_world; let previews = self.tabs[i] .active_cmd .as_mut() .map(|c| c.on_preview_wires(pt)) .unwrap_or_default(); self.tabs[i].scene.set_preview_wires(previews); return self.focus_cmd_input(); } // 2. Empty input → Enter (finish / cancel). if text.is_empty() { let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); if let Some(r) = result { return self.apply_cmd_result(r); } return Task::none(); } // 3. Coordinate input "x,y" or "x,y,z" → on_point. if let Some(pt) = parse_coord(&text) { let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(pt)); if let Some(r) = result { return self.apply_cmd_result(r); } return Task::none(); } // 4. Numeric input (e.g. radius, angle, scale factor) → on_text_input. if let Some(result) = self.tabs[i] .active_cmd .as_mut() .and_then(|c| c.on_text_input(&text)) { return self.apply_cmd_result(result); } // 5. Nothing matched — show hint. self.command_line.push_error(&format!( "Expected coordinates (x,y) or a number, got: \"{text}\"" )); return self.focus_cmd_input(); } if let Some(cmd) = self.command_line.submit() { return self.dispatch_command(&cmd); } Task::none() } Message::CommandFinalize => { let i = self.active_tab; if self.tabs[i].active_cmd.is_some() { let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); if let Some(r) = result { return self.apply_cmd_result(r); } Task::none() } else if let Some(cmd) = self.tabs[i].last_cmd.clone() { self.dispatch_command(&cmd) } else { Task::none() } } Message::CommandEscape => { let i = self.active_tab; if self.tabs[i].active_cmd.is_some() { let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_escape()); if let Some(r) = result { return self.apply_cmd_result(r); } } else { // No active command → Escape deselects everything. self.tabs[i].scene.deselect_all(); self.refresh_properties(); // Also cancel any in-progress selection box. let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.box_anchor = None; sel.box_current = None; sel.box_crossing = false; } Task::none() } Message::Command(cmd) => self.dispatch_command(&cmd), Message::ToggleLayers => { if let Some(id) = self.layer_window.take() { // Window already open — close it. window::close(id) } else { // Sync layer/linetype data before opening so the panel is up to date. self.sync_ribbon_layers(); // Open a new OS window for the Layer Properties Manager. let (id, task) = window::open(window::Settings { size: iced::Size::new(900.0, 360.0), resizable: true, ..Default::default() }); self.layer_window = Some(id); task.map(|_| Message::Noop) } } Message::MainWindowOpened(id) => { self.main_window = Some(id); Task::none() } Message::LayerWindowOpened(_id) => Task::none(), Message::OsWindowClosed(id) => { if self.main_window == Some(id) { // Main window closed — exit the process entirely. return iced::exit(); } if self.layer_window == Some(id) { self.layer_window = None; } Task::none() } Message::LayerToggleVisible(idx) => { let i = self.active_tab; if idx < self.tabs[i].layers.layers.len() { self.push_undo_snapshot(i, "LAYER OFF/ON"); let l = &mut self.tabs[i].layers.layers[idx]; l.visible = !l.visible; let name = l.name.clone(); let on = l.visible; self.tabs[i].scene.toggle_layer_visibility(&name); self.command_line.push_output(&format!( "Layer \"{}\" {}", name, if on { "on" } else { "off" } )); } Task::none() } Message::LayerToggleLock(idx) => { let i = self.active_tab; if idx < self.tabs[i].layers.layers.len() { self.push_undo_snapshot(i, "LAYER LOCK/UNLOCK"); let l = &mut self.tabs[i].layers.layers[idx]; l.locked = !l.locked; let name = l.name.clone(); let locked = l.locked; self.tabs[i].scene.toggle_layer_lock(&name); self.command_line.push_output(&format!( "Layer \"{}\" {}", name, if locked { "locked" } else { "unlocked" } )); } Task::none() } Message::LayerToggleFreeze(idx) => { let i = self.active_tab; if idx < self.tabs[i].layers.layers.len() { self.push_undo_snapshot(i, "LAYER FREEZE"); let l = &mut self.tabs[i].layers.layers[idx]; l.frozen = !l.frozen; let name = l.name.clone(); let frozen = l.frozen; if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { if frozen { dl.freeze(); } else { dl.thaw(); } } self.tabs[i].dirty = true; } Task::none() } Message::LayerNew => { let i = self.active_tab; // Find unique name let mut n = 1; let new_name = loop { let candidate = format!("Layer{}", n); if !self.tabs[i].scene.document.layers.contains(&candidate) { break candidate; } n += 1; }; self.push_undo_snapshot(i, "LAYER NEW"); use acadrust::tables::layer::Layer as DocLayer; let _ = self.tabs[i].scene.document.layers.add(DocLayer::new(&new_name)); self.tabs[i].dirty = true; let doc_layers = self.tabs[i].scene.document.layers.clone(); self.tabs[i].layers.sync_from_doc(&doc_layers); // Select and start editing the new layer let new_idx = self.tabs[i].layers.layers.iter() .position(|l| l.name == new_name); if let Some(idx) = new_idx { self.tabs[i].layers.selected = Some(idx); self.tabs[i].layers.editing = Some(idx); self.tabs[i].layers.edit_buf = new_name.clone(); } self.sync_ribbon_layers(); Task::none() } Message::LayerDelete => { let i = self.active_tab; if let Some(idx) = self.tabs[i].layers.selected { let name = self.tabs[i].layers.layers.get(idx) .map(|l| l.name.clone()) .unwrap_or_default(); if name == "0" { return Task::none(); } self.push_undo_snapshot(i, "LAYER DELETE"); self.tabs[i].scene.document.layers.remove(&name); self.tabs[i].dirty = true; let doc_layers = self.tabs[i].scene.document.layers.clone(); self.tabs[i].layers.sync_from_doc(&doc_layers); self.tabs[i].layers.selected = None; self.sync_ribbon_layers(); } Task::none() } Message::LayerSetCurrent => { let i = self.active_tab; if let Some(idx) = self.tabs[i].layers.selected { if let Some(layer) = self.tabs[i].layers.layers.get(idx) { let name = layer.name.clone(); self.tabs[i].active_layer = name.clone(); self.tabs[i].layers.current_layer = name.clone(); self.ribbon.active_layer = name; } } Task::none() } Message::LayerSelect(idx) => { let i = self.active_tab; // Commit any pending rename first if self.tabs[i].layers.editing.is_some() { return Task::done(Message::LayerRenameCommit); } self.tabs[i].layers.selected = Some(idx); Task::none() } Message::LayerRenameStart(idx) => { let i = self.active_tab; self.tabs[i].layers.selected = Some(idx); if let Some(layer) = self.tabs[i].layers.layers.get(idx) { self.tabs[i].layers.edit_buf = layer.name.clone(); } self.tabs[i].layers.editing = Some(idx); Task::none() } Message::LayerRenameEdit(s) => { let i = self.active_tab; self.tabs[i].layers.edit_buf = s; Task::none() } Message::LayerRenameCommit => { let i = self.active_tab; let editing_idx = self.tabs[i].layers.editing.take(); if let Some(idx) = editing_idx { let new_name = self.tabs[i].layers.edit_buf.trim().to_string(); let old_name = self.tabs[i].layers.layers.get(idx) .map(|l| l.name.clone()) .unwrap_or_default(); if !new_name.is_empty() && new_name != old_name && !self.tabs[i].scene.document.layers.contains(&new_name) { self.push_undo_snapshot(i, "LAYER RENAME"); // acadrust Table doesn't have rename; add new + remove old if let Some(old_layer) = self.tabs[i].scene.document.layers.get(&old_name) { use acadrust::tables::layer::Layer as DocLayer; let mut nl = DocLayer::new(&new_name); nl.color = old_layer.color.clone(); nl.flags = old_layer.flags.clone(); let _ = self.tabs[i].scene.document.layers.add(nl); } self.tabs[i].scene.document.layers.remove(&old_name); // Update entities on that layer for e in self.tabs[i].scene.document.entities_mut() { if e.as_entity().layer() == old_name { e.as_entity_mut().set_layer(new_name.clone()); } } self.tabs[i].dirty = true; } let doc_layers = self.tabs[i].scene.document.layers.clone(); self.tabs[i].layers.sync_from_doc(&doc_layers); self.tabs[i].layers.edit_buf.clear(); self.sync_ribbon_layers(); } Task::none() } Message::LayerColorPickerToggle(idx) => { let i = self.active_tab; let panel = &mut self.tabs[i].layers; if panel.color_picker_row == Some(idx) { panel.color_picker_row = None; panel.color_full_palette = false; } else { panel.color_picker_row = Some(idx); panel.color_full_palette = false; panel.selected = Some(idx); } Task::none() } Message::LayerColorMorePalette => { let i = self.active_tab; self.tabs[i].layers.color_full_palette = !self.tabs[i].layers.color_full_palette; Task::none() } Message::LayerColorSet(aci) => { let i = self.active_tab; if let Some(idx) = self.tabs[i].layers.selected { if let Some(layer) = self.tabs[i].layers.layers.get(idx) { let name = layer.name.clone(); if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { dl.color = AcadColor::Index(aci); } use crate::ui::layers::iced_color_from_acad; let new_color = iced_color_from_acad(&AcadColor::Index(aci)); if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { pl.color = new_color; } self.tabs[i].dirty = true; } self.tabs[i].layers.color_picker_row = None; self.tabs[i].layers.color_full_palette = false; self.sync_ribbon_layers(); } Task::none() } Message::LayerLinetypeSet(lt) => { let i = self.active_tab; if let Some(idx) = self.tabs[i].layers.selected { if let Some(layer) = self.tabs[i].layers.layers.get(idx) { let name = layer.name.clone(); if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { dl.line_type = lt.clone(); } if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { pl.linetype = lt; } self.tabs[i].dirty = true; } } Task::none() } Message::LayerLineweightSet(lw) => { let i = self.active_tab; if let Some(idx) = self.tabs[i].layers.selected { if let Some(layer) = self.tabs[i].layers.layers.get(idx) { let name = layer.name.clone(); if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { dl.line_weight = lw; } if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { pl.lineweight = lw; } self.tabs[i].dirty = true; } } Task::none() } Message::LayerTransparencyEdit(idx, s) => { let i = self.active_tab; if let Some(pl) = self.tabs[i].layers.layers.get_mut(idx) { if let Ok(v) = s.parse::() { pl.transparency = v.clamp(0, 90); } else if s.is_empty() { pl.transparency = 0; } } Task::none() } Message::CursorMoved(p) => { let (vw, _vh) = self.tabs[self.active_tab].scene.selection.borrow().vp_size; self.cursor_pos = Point::new( vw - VIEWCUBE_PAD - VIEWCUBE_HIT_SIZE + p.x, VIEWCUBE_PAD + p.y, ); Task::none() } Message::ViewportMove(p) => { let i = self.active_tab; let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.last_move_pos = Some(p); if sel.left_down { let press = sel.left_press_pos.unwrap_or(p); let dx = p.x - press.x; let dy = p.y - press.y; let dist2 = dx * dx + dy * dy; let elapsed_ms = sel .left_press_time .map(|t| Instant::now().duration_since(t).as_millis()) .unwrap_or(u128::MAX); if !sel.left_dragging && elapsed_ms >= POLY_START_DELAY_MS && dist2 > 9.0 { sel.left_dragging = true; sel.poly_active = true; sel.poly_crossing = p.x < press.x; sel.poly_points.clear(); sel.poly_points.push(press); sel.poly_points.push(p); } else if sel.left_dragging && sel.poly_active { if sel.poly_points.last().map_or(true, |lp| { let ddx = p.x - lp.x; let ddy = p.y - lp.y; ddx * ddx + ddy * ddy > 16.0 }) { sel.poly_points.push(p); } } } else if sel.box_anchor.is_some() { sel.box_current = Some(p); if let Some(a) = sel.box_anchor { sel.box_crossing = p.x < a.x; } } if sel.right_down { if let Some(press) = sel.right_press_pos { let dx = p.x - press.x; let dy = p.y - press.y; if !sel.right_dragging && (dx * dx + dy * dy) > 9.0 { sel.right_dragging = true; sel.context_menu = None; } } if sel.right_dragging { if let Some(last) = sel.right_last_pos { let (dx, dy) = (p.x - last.x, p.y - last.y); self.tabs[i].scene.camera.borrow_mut().orbit(dx, dy); } sel.right_last_pos = Some(p); } } if sel.middle_down { if let Some(last) = sel.middle_last_pos { let (dx, dy) = (p.x - last.x, p.y - last.y); self.tabs[i].scene.camera.borrow_mut().pan(dx, dy); } sel.middle_last_pos = Some(p); } let vp_size = sel.vp_size; drop(sel); // ── Grip drag ───────────────────────────────────────────── if let Some(grip) = self.tabs[i].active_grip.clone() { let (vw, vh) = vp_size; let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh, }; let cam = self.tabs[i].scene.camera.borrow(); let raw = cam.pick_on_target_plane(p, bounds); let vp_mat = cam.view_proj(bounds); drop(cam); // Exclude the entity being dragged so its own segments // don't interfere with snap (avoids Nearest/Endpoint sticking). let edited_name = grip.handle.value().to_string(); let all_wires = self.tabs[i].scene.entity_wires(); let snap_wires: Vec<_> = all_wires .iter() .filter(|w| w.name != edited_name) .cloned() .collect(); let snap_hit = self.snapper.snap(raw, p, &snap_wires, vp_mat, bounds); let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw); self.tabs[i].snap_result = snap_hit; // Apply ortho/polar constraints relative to the grip's origin position. // Only when snap did not find a geometry hit (snap takes priority). if snap_hit.is_none() { let base = grip.origin_world; if self.ortho_mode { snapped = ortho_constrain(snapped, base); } else if self.polar_mode { snapped = polar_constrain(snapped, base, 45.0); } } let apply = if grip.is_translate { GripApply::Translate(snapped - grip.last_world) } else { GripApply::Absolute(snapped) }; self.tabs[i] .scene .apply_grip(grip.handle, grip.grip_id, apply); self.tabs[i].dirty = true; self.tabs[i].active_grip.as_mut().unwrap().last_world = snapped; self.refresh_selected_grips(); self.refresh_properties(); return Task::none(); } if self.tabs[i].active_cmd.is_some() { let (vw, vh) = vp_size; let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh, }; let cam = self.tabs[i].scene.camera.borrow(); let cursor_world = cam.pick_on_target_plane(p, bounds); let view_proj = cam.view_proj(bounds); drop(cam); let all_wires = self.tabs[i].scene.entity_wires(); let needs_tan = self.tabs[i] .active_cmd .as_ref() .map(|c| c.needs_tangent_pick()) .unwrap_or(false); self.tabs[i].snap_result = if needs_tan { self.snapper.snap_tangent_only( cursor_world, p, &all_wires, view_proj, bounds, ) } else { self.snapper .snap(cursor_world, p, &all_wires, view_proj, bounds) }; let effective = { let mut pt = self.tabs[i] .snap_result .map(|s| s.world) .unwrap_or(cursor_world); if self.tabs[i].active_cmd.is_some() { pt.z = 0.0; } if let Some(base) = self.last_point { if self.ortho_mode { pt = ortho_constrain(pt, base); } else if self.polar_mode { pt = polar_constrain(pt, base, 45.0); } } pt }; self.tabs[i].last_cursor_world = effective; let needs_entity = self.tabs[i] .active_cmd .as_ref() .map(|c| c.needs_entity_pick()) .unwrap_or(false); let 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)) .unwrap_or(acadrust::Handle::NULL); self.tabs[i] .active_cmd .as_mut() .map(|c| c.on_hover_entity(hover_handle, effective)) .unwrap_or_default() } else { self.tabs[i] .active_cmd .as_mut() .map(|c| c.on_preview_wires(effective)) .unwrap_or_default() }; self.tabs[i].scene.set_preview_wires(previews); } else { self.tabs[i].snap_result = None; } Task::none() } Message::ViewportExit => { let i = self.active_tab; let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.left_down = false; sel.left_press_pos = None; sel.left_press_time = None; sel.left_dragging = false; sel.right_down = false; sel.right_press_pos = None; sel.right_last_pos = None; sel.right_dragging = false; sel.middle_down = false; sel.middle_last_pos = None; sel.box_anchor = None; sel.box_current = None; sel.box_crossing = false; sel.poly_active = false; sel.poly_points.clear(); sel.poly_crossing = false; sel.context_menu = None; Task::none() } Message::ViewportLeftPress => { let i = self.active_tab; let (p, vp_size) = { let sel = self.tabs[i].scene.selection.borrow(); let p = match sel.last_move_pos { Some(p) => p, None => return Task::none(), }; (p, sel.vp_size) }; let (vw, vh) = vp_size; let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh, }; // Viewcube check. if vw > 1.0 && vh > 1.0 { let cam = self.tabs[i].scene.camera.borrow(); if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX) .is_some() { return Task::none(); } } // Grip hit-test: only when no command is active and one entity selected. if self.tabs[i].active_cmd.is_none() && !self.tabs[i].selected_grips.is_empty() { if let Some(handle) = self.tabs[i].selected_handle { let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let grip_hit = find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds); if let Some((grip_id, is_translate, world)) = grip_hit { self.tabs[i].active_grip = Some(GripEdit { handle, grip_id, is_translate, origin_world: world, last_world: world, }); return Task::none(); // swallow press — selection box must not start } } } let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.context_menu = None; sel.left_down = true; sel.left_press_pos = Some(p); sel.left_press_time = Some(Instant::now()); sel.left_dragging = false; Task::none() } Message::ViewportLeftRelease => { let i = self.active_tab; // Extract the state we need before any mutable borrows. let (p, is_click, is_down) = { let sel = self.tabs[i].scene.selection.borrow(); let p = match sel.last_move_pos { Some(p) => p, None => return Task::none(), }; (p, !sel.left_dragging, sel.left_down) }; // ── Grip drag end ───────────────────────────────────────── if self.tabs[i].active_grip.is_some() { self.tabs[i].active_grip = None; self.refresh_properties(); return Task::none(); } // Whether the active command is in selection-gathering mode. let is_gathering = self.tabs[i] .active_cmd .as_ref() .map(|c| c.is_selection_gathering()) .unwrap_or(false); // ── Active command: plain click = point pick ────────────── // Skip this path when gathering — let clicks flow into the // normal selection system below. if is_down && is_click && self.tabs[i].active_cmd.is_some() && !is_gathering { 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, }; // Take snap result once; preserve tangent_obj for pick routing. let snap_taken = self.tabs[i].snap_result.take(); let tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj); let world_pt = { let raw = self.tabs[i] .scene .camera .borrow() .pick_on_target_plane(p, bounds); let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let all_wires = self.tabs[i].scene.entity_wires(); let needs_tan = self.tabs[i] .active_cmd .as_ref() .map(|c| c.needs_tangent_pick()) .unwrap_or(false); let snap_hit = if needs_tan { self.snapper .snap_tangent_only(raw, p, &all_wires, vp_mat, bounds) } else { self.snapper.snap(raw, p, &all_wires, vp_mat, bounds) }; let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw); pt.z = 0.0; if let Some(base) = self.last_point { if self.ortho_mode { pt = ortho_constrain(pt, base); } else if self.polar_mode { pt = polar_constrain(pt, base, 45.0); } } pt }; let result = if self.tabs[i] .active_cmd .as_ref() .map(|c| c.needs_entity_pick()) .unwrap_or(false) { let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds); let all_wires2 = self.tabs[i].scene.entity_wires(); let hit = scene::hit_test::click_hit(p, &all_wires2, vp_mat2, bounds) .and_then(|s| Scene::handle_from_wire_name(s)); if let Some(handle) = hit { self.tabs[i] .active_cmd .as_mut() .map(|c| c.on_entity_pick(handle, world_pt)) } else { self.command_line.push_info("Nothing found at that point."); None } } else if self.tabs[i] .active_cmd .as_ref() .map(|c| c.needs_tangent_pick()) .unwrap_or(false) { let tangent_obj = tangent_obj_at_click; if let Some(obj) = tangent_obj { self.tabs[i] .active_cmd .as_mut() .map(|c| c.on_tangent_point(obj, world_pt)) } else { self.command_line.push_info("Select a tangent object."); None } } else { self.last_point = Some(world_pt); self.tabs[i] .active_cmd .as_mut() .map(|c| c.on_point(world_pt)) }; if let Some(r) = result { let task = self.apply_cmd_result(r); let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.left_down = false; sel.left_press_pos = None; sel.left_press_time = None; sel.left_dragging = false; return task; } let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.left_down = false; sel.left_press_pos = None; sel.left_press_time = None; sel.left_dragging = false; return Task::none(); } let (is_down2, is_dragging, box_anchor, box_crossing, vp_size, elapsed_ms) = { let sel = self.tabs[i].scene.selection.borrow(); let elapsed = sel .left_press_time .map(|t| Instant::now().duration_since(t).as_millis()) .unwrap_or(u128::MAX); ( sel.left_down, sel.left_dragging, sel.box_anchor, sel.box_crossing, sel.vp_size, elapsed, ) }; // True when a selection action fully completed this release event, // meaning the gathering command should fire immediately. let mut selection_just_completed = false; if is_down2 { let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vp_size.0, height: vp_size.1, }; if is_dragging { // ── Drag ended ──────────────────────────────────── if elapsed_ms < POLY_START_DELAY_MS { if let Some(a) = box_anchor { let crossing = box_crossing; let all_wires = self.tabs[i].scene.entity_wires(); let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let mut handles: Vec = scene::hit_test::box_hit( a, p, crossing, &all_wires, vp_mat, bounds, ) .into_iter() .filter_map(|s| Scene::handle_from_wire_name(s)) .collect(); handles.extend(scene::hit_test::box_hit_hatch( a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds, )); self.tabs[i].scene.deselect_all(); for h in &handles { self.tabs[i].scene.select_entity(*h, false); } self.tabs[i].scene.expand_selection_for_groups(&handles); self.refresh_properties(); selection_just_completed = true; } } else { // Long drag → lasso polygon selection. let (poly_pts, crossing) = { let sel = self.tabs[i].scene.selection.borrow(); (sel.poly_points.clone(), sel.poly_crossing) }; self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing; let all_wires = self.tabs[i].scene.entity_wires(); let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let mut handles: Vec = scene::hit_test::poly_hit( &poly_pts, crossing, &all_wires, vp_mat, bounds, ) .into_iter() .filter_map(|s| Scene::handle_from_wire_name(s)) .collect(); handles.extend(scene::hit_test::poly_hit_hatch( &poly_pts, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds, )); self.tabs[i].scene.deselect_all(); for h in &handles { self.tabs[i].scene.select_entity(*h, false); } self.tabs[i].scene.expand_selection_for_groups(&handles); self.refresh_properties(); selection_just_completed = true; } let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.poly_active = false; sel.poly_points.clear(); sel.poly_crossing = false; sel.box_anchor = None; sel.box_current = None; } else { // ── Non-drag click ──────────────────────────────── if box_anchor.is_none() { let all_wires = self.tabs[i].scene.entity_wires(); let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let hit = scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds) .and_then(|s| Scene::handle_from_wire_name(s)) .or_else(|| { scene::hit_test::click_hit_hatch( p, &self.tabs[i].scene.hatches, vp_mat, bounds, ) }); if let Some(handle) = hit { self.tabs[i].scene.select_entity(handle, true); self.tabs[i].scene.expand_selection_for_groups(&[handle]); self.refresh_properties(); // Single-click hit completes the gather; miss starts a box. selection_just_completed = true; } else { self.tabs[i].scene.deselect_all(); self.refresh_properties(); let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.box_anchor = Some(p); sel.box_current = Some(p); sel.box_crossing = false; } } else { // Second click → complete the selection box. let a = box_anchor.unwrap(); let crossing = box_crossing; let all_wires = self.tabs[i].scene.entity_wires(); let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds); let mut handles: Vec = scene::hit_test::box_hit( a, p, crossing, &all_wires, vp_mat, bounds, ) .into_iter() .filter_map(|s| Scene::handle_from_wire_name(s)) .collect(); handles.extend(scene::hit_test::box_hit_hatch( a, p, crossing, &self.tabs[i].scene.hatches, vp_mat, bounds, )); self.tabs[i].scene.deselect_all(); for h in &handles { self.tabs[i].scene.select_entity(*h, false); } self.tabs[i].scene.expand_selection_for_groups(&handles); self.refresh_properties(); let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.box_last = Some((a, p)); sel.box_last_crossing = crossing; sel.box_anchor = None; sel.box_current = None; sel.box_crossing = false; selection_just_completed = true; } } let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.left_down = false; sel.left_press_pos = None; sel.left_press_time = None; sel.left_dragging = false; } // ── Fire gathering command after selection completes ─────── if is_gathering && selection_just_completed { let handles: Vec = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { let result = cmd.on_selection_complete(handles); return self.apply_cmd_result(result); } } Task::none() } Message::ViewportRightPress => { let i = self.active_tab; let mut sel = self.tabs[i].scene.selection.borrow_mut(); let Some(p) = sel.last_move_pos else { return Task::none(); }; sel.context_menu = None; sel.right_down = true; sel.right_press_pos = Some(p); sel.right_last_pos = Some(p); sel.right_dragging = false; Task::none() } Message::ViewportRightRelease => { let i = self.active_tab; let mut sel = self.tabs[i].scene.selection.borrow_mut(); let Some(_p) = sel.last_move_pos else { return Task::none(); }; if sel.right_down { if !sel.right_dragging { sel.context_menu = sel.last_move_pos; } sel.right_down = false; sel.right_press_pos = None; sel.right_last_pos = None; sel.right_dragging = false; } Task::none() } Message::ViewportMiddlePress => { let i = self.active_tab; let now = iced::time::Instant::now(); let is_double = { let sel = self.tabs[i].scene.selection.borrow(); sel.middle_last_press_time .map(|t| now.duration_since(t).as_millis() < 300) .unwrap_or(false) }; { let mut sel = self.tabs[i].scene.selection.borrow_mut(); let Some(p) = sel.last_move_pos else { return Task::none(); }; sel.middle_down = true; sel.middle_last_pos = Some(p); sel.middle_last_press_time = Some(now); } if is_double { self.tabs[i].scene.fit_all(); self.command_line.push_output("Zoom Extents"); } Task::none() } Message::ViewportMiddleRelease => { let i = self.active_tab; let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.middle_down = false; sel.middle_last_pos = None; Task::none() } Message::ViewportScroll(delta) => { let s = match delta { mouse::ScrollDelta::Lines { y, .. } => y, mouse::ScrollDelta::Pixels { y, .. } => y * 0.01, }; let i = self.active_tab; let cursor = self.tabs[i].scene.selection.borrow().last_move_pos; 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 mut cam = self.tabs[i].scene.camera.borrow_mut(); if let Some(cursor) = cursor { cam.zoom_about_point(cursor, bounds, s); } else { cam.zoom(s); } Task::none() } Message::ViewportClick => { let i = self.active_tab; let cam = self.tabs[i].scene.camera.borrow(); let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; if let Some(region) = scene::hit_test( self.cursor_pos.x, self.cursor_pos.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX, ) { return Task::done(Message::ViewCubeSnap(region)); } Task::none() } Message::WindowResized(w, h) => { self.vp_size = ((w - 440.0).max(200.0), h); Task::none() } Message::ViewCubeSnap(region) => { let i = self.active_tab; let mut region = region; { let mut cam = self.tabs[i].scene.camera.borrow_mut(); let (target_yaw, target_pitch) = region.snap_angles(); if angle_close(cam.yaw, target_yaw, 0.01) && angle_close(cam.pitch, target_pitch, 0.01) { region = region.opposite(); } let (yaw, pitch) = region.snap_angles(); cam.snap_to_angles(yaw, pitch); } self.tabs[i].scene.camera_generation += 1; self.command_line .push_output(&format!("View: {}", region.label())); Task::none() } Message::ToggleSnapEnabled => { self.snapper.toggle_global(); Task::none() } Message::ToggleGridSnap => { self.snapper.toggle(snap::SnapType::Grid); Task::none() } Message::ToggleGrid => { self.show_grid ^= true; Task::none() } Message::ToggleOrtho => { self.ortho_mode ^= true; if self.ortho_mode { self.polar_mode = false; } Task::none() } Message::TogglePolar => { self.polar_mode ^= true; if self.polar_mode { 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() } Message::SnapSelectAll => { self.snapper.enable_all(); Task::none() } Message::SnapClearAll => { self.snapper.disable_all(); Task::none() } Message::ToggleRibbonDropdown(id) => { self.ribbon.toggle_dropdown(&id); Task::none() } Message::CloseRibbonDropdown => { self.ribbon.close_dropdown(); Task::none() } Message::DropdownSelectItem { dropdown_id, cmd } => { self.ribbon.select_dropdown_item(dropdown_id, cmd); self.ribbon.activate_tool(cmd); self.dispatch_command(cmd) } Message::DeleteSelected => { let i = self.active_tab; let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect(); if !handles.is_empty() { self.push_undo_snapshot(i, "ERASE"); self.tabs[i].scene.erase_entities(&handles); self.tabs[i].dirty = true; self.refresh_properties(); } Task::none() } // ── Properties panel messages ───────────────────────────────── Message::PropSelectionGroupChanged(group) => { self.tabs[self.active_tab].properties.selected_group = Some(group); self.refresh_properties(); Task::none() } Message::RibbonLayerChanged(layer) => { let i = self.active_tab; self.tabs[i].active_layer = layer.clone(); self.tabs[i].layers.current_layer = layer.clone(); self.ribbon.active_layer = layer; self.ribbon.close_dropdown(); Task::none() } Message::RibbonColorChanged(color) => { self.ribbon.active_color = color; self.ribbon.prop_color_palette_open = false; self.ribbon.close_dropdown(); Task::none() } Message::RibbonColorPaletteToggle => { self.ribbon.prop_color_palette_open ^= true; Task::none() } Message::RibbonLinetypeChanged(lt) => { self.ribbon.active_linetype = lt; self.ribbon.close_dropdown(); Task::none() } Message::RibbonLineweightChanged(lw) => { self.ribbon.active_lineweight = lw; self.ribbon.close_dropdown(); Task::none() } Message::PropLayerChanged(layer) => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for handle in handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { dispatch::apply_common_prop(entity, "layer", &layer); } } self.tabs[i].dirty = true; self.refresh_properties(); } Task::none() } Message::PropColorChanged(color) => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for handle in handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { dispatch::apply_color(entity, color); } } self.tabs[i].properties.color_picker_open = false; self.tabs[i].properties.color_palette_open = false; self.tabs[i].dirty = true; self.refresh_properties(); } Task::none() } Message::PropLwChanged(lw) => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for handle in handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { dispatch::apply_line_weight(entity, lw); } } self.tabs[i].dirty = true; self.refresh_properties(); } Task::none() } Message::PropLinetypeChanged(lt) => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for handle in handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { dispatch::apply_common_prop(entity, "linetype", <); } } self.tabs[i].dirty = true; self.refresh_properties(); } Task::none() } Message::PropHatchPatternChanged(name) => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { use crate::scene::hatch_patterns; if let Some(entry) = hatch_patterns::find(&name) { self.push_undo_snapshot(i, "HATCHEDIT"); for handle in handles { if let Some(acadrust::EntityType::Hatch(dxf)) = self.tabs[i].scene.document.get_entity_mut(handle) { dxf.pattern = hatch_patterns::build_dxf_pattern(entry); dxf.is_solid = matches!( entry.gpu, crate::scene::hatch_model::HatchPattern::Solid ); } if let Some(model) = self.tabs[i].scene.hatches.get_mut(&handle) { model.pattern = entry.gpu.clone(); model.name = name.clone(); } } self.tabs[i].dirty = true; self.refresh_properties(); } } Task::none() } Message::PropBoolToggle(field) => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for handle in handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { match field { "invisible" => dispatch::toggle_invisible(entity), _ => dispatch::apply_geom_prop(entity, field, "toggle"), } } } self.tabs[i].dirty = true; self.refresh_properties(); } Task::none() } Message::PropGeomChoiceChanged { field, value } => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { self.push_undo_snapshot(i, "CHPROP"); for handle in handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { dispatch::apply_geom_prop(entity, field, &value); } } self.tabs[i].dirty = true; self.refresh_properties(); } Task::none() } Message::PropGeomInput { field, value } => { self.tabs[self.active_tab] .properties .edit_buf .insert(field.to_string(), value); Task::none() } Message::PropGeomCommit(field) => { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { if let Some(val) = self.tabs[i].properties.edit_buf.remove(field) { self.push_undo_snapshot(i, "CHPROP"); for handle in handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { match field { "linetype_scale" | "transparency" => { dispatch::apply_common_prop(entity, field, &val); } _ => { dispatch::apply_geom_prop(entity, field, &val); } } } } self.tabs[i].dirty = true; self.refresh_properties(); } } Task::none() } Message::PropColorPickerToggle => { let i = self.active_tab; self.tabs[i].properties.color_picker_open = !self.tabs[i].properties.color_picker_open; if self.tabs[i].properties.color_picker_open { self.tabs[i].properties.color_palette_open = false; } Task::none() } Message::PropColorPaletteToggle => { self.tabs[self.active_tab].properties.color_palette_open = !self.tabs[self.active_tab].properties.color_palette_open; Task::none() } Message::LayoutSwitch(name) => { let i = self.active_tab; let going_to_paper = name != "Model"; self.tabs[i].scene.current_layout = name; self.tabs[i].scene.deselect_all(); self.tabs[i].scene.fit_all(); // Switch to Layout tab when entering paper space; back to Home when leaving. if going_to_paper { if let Some(idx) = self.ribbon.layout_module_index() { self.ribbon.select(idx); } } else { if self.ribbon.active_is_layout() { self.ribbon.select(0); } } Task::none() } Message::LayoutCreate => { let i = self.active_tab; let count = self.tabs[i].scene.layout_names().len(); // "Model" + existing paper let new_name = format!("Layout{}", count); self.push_undo_snapshot(i, "LAYOUT"); match self.tabs[i].scene.document.add_layout(&new_name) { Ok(_) => { self.tabs[i].scene.current_layout = new_name.clone(); self.tabs[i].scene.deselect_all(); self.tabs[i].scene.fit_all(); if let Some(idx) = self.ribbon.layout_module_index() { self.ribbon.select(idx); } self.command_line.push_output(&format!( "Layout \"{new_name}\" oluşturuldu — MVIEW ile viewport ekleyin" )); self.tabs[i].dirty = true; } Err(e) => self .command_line .push_error(&format!("Layout oluşturulamadı: {e}")), } Task::none() } Message::Undo => { self.undo_active_tab(); Task::none() } Message::Redo => { self.redo_active_tab(); Task::none() } Message::UndoMany(steps) => { self.ribbon.close_dropdown(); self.undo_steps(steps); Task::none() } Message::RedoMany(steps) => { self.ribbon.close_dropdown(); self.redo_steps(steps); Task::none() } Message::Noop => Task::none(), } } fn dispatch_command(&mut self, cmd: &str) -> Task { let i = self.active_tab; // Cancel any running command before starting a new one. if self.tabs[i].active_cmd.is_some() { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; } if let Some(path_str) = cmd.strip_prefix("OPEN_RECENT:") { let path = PathBuf::from(path_str); return Task::perform(io::open_path(path), Message::FileOpened); } match cmd { "NEW" => return Task::done(Message::ClearScene), "OPEN" => return Task::done(Message::OpenFile), "SAVE"|"QSAVE" => return Task::done(Message::SaveFile), "SAVEAS" => return Task::done(Message::SaveAs), "UNDO"|"U" => return Task::done(Message::Undo), "REDO" => return Task::done(Message::Redo), "CLEAR"|"CLR" => return Task::done(Message::ClearScene), "WIREFRAME"|"VW" => return Task::done(Message::SetWireframe(true)), "SOLID"|"VS" => return Task::done(Message::SetWireframe(false)), "ORTHO" => return Task::done(Message::SetProjection(true)), "PERSP" => return Task::done(Message::SetProjection(false)), "LAYERS"|"LA" => return Task::done(Message::ToggleLayers), // ── Layer object commands ────────────────────────────────────── // Commands that operate on the layer of selected (or picked) objects. "LAYOFF" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("LAYOFF"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let layers: std::collections::HashSet = self.tabs[i].scene .selected_entities().into_iter() .map(|(_, e)| e.common().layer.clone()).collect(); self.push_undo_snapshot(i, "LAYOFF"); for name in &layers { if name == "0" { continue; } if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { dl.turn_off(); } } self.tabs[i].dirty = true; self.sync_ribbon_layers(); self.command_line.push_info("Layer(s) turned off."); } } "LAYFRZ" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("LAYFRZ"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let layers: std::collections::HashSet = self.tabs[i].scene .selected_entities().into_iter() .map(|(_, e)| e.common().layer.clone()).collect(); self.push_undo_snapshot(i, "LAYFRZ"); for name in &layers { if name == "0" { continue; } if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { dl.freeze(); } } self.tabs[i].dirty = true; self.sync_ribbon_layers(); self.command_line.push_info("Layer(s) frozen."); } } "LAYLCK" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("LAYLCK"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let layers: std::collections::HashSet = self.tabs[i].scene .selected_entities().into_iter() .map(|(_, e)| e.common().layer.clone()).collect(); self.push_undo_snapshot(i, "LAYLCK"); for name in &layers { if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { dl.lock(); } } self.tabs[i].dirty = true; self.sync_ribbon_layers(); self.command_line.push_info("Layer(s) locked."); } } "LAYMCUR" => { let entities = self.tabs[i].scene.selected_entities(); if entities.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("LAYMCUR"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let layer = entities[0].1.common().layer.clone(); self.tabs[i].active_layer = layer.clone(); self.ribbon.active_layer = layer.clone(); self.tabs[i].layers.current_layer = layer.clone(); self.command_line.push_info(&format!("Current layer set to \"{layer}\".")); self.sync_ribbon_layers(); } } "LAYON" => { self.push_undo_snapshot(i, "LAYON"); for name in self.tabs[i].scene.document.layers.iter() .map(|l| l.name.clone()).collect::>() { if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { dl.turn_on(); } } self.tabs[i].dirty = true; self.sync_ribbon_layers(); self.command_line.push_info("All layers turned on."); } "LAYTHW" => { self.push_undo_snapshot(i, "LAYTHW"); for name in self.tabs[i].scene.document.layers.iter() .map(|l| l.name.clone()).collect::>() { if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(&name) { dl.thaw(); } } self.tabs[i].dirty = true; self.sync_ribbon_layers(); self.command_line.push_info("All layers thawed."); } "LAYULK" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("LAYULK"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let layers: std::collections::HashSet = self.tabs[i].scene .selected_entities().into_iter() .map(|(_, e)| e.common().layer.clone()).collect(); self.push_undo_snapshot(i, "LAYULK"); for name in &layers { if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) { dl.unlock(); } } self.tabs[i].dirty = true; self.sync_ribbon_layers(); self.command_line.push_info("Layer(s) unlocked."); } } "LAYMATCH"|"LAYMCH" => { use crate::command::laymatch::LayMatchCommand; let dest: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); let cmd = LayMatchCommand::new(dest); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } "MATCHPROP"|"MA" => { use crate::command::matchprop::MatchPropCommand; self.tabs[i].scene.deselect_all(); let cmd = MatchPropCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } "GROUP"|"G" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("GROUP"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let auto_name = Self::next_group_auto_name(&self.tabs[i].scene); use crate::command::group::GroupCommand; let cmd = GroupCommand::new(handles, auto_name); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } } "UNGROUP"|"UG" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); if handles.is_empty() { use crate::command::ungroup::UngroupCommand; let cmd = UngroupCommand::new(); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { self.push_undo_snapshot(i, "UNGROUP"); let count = self.tabs[i].scene.delete_groups_containing(&handles); self.tabs[i].dirty = true; if count > 0 { self.command_line.push_info(&format!("{} group(s) dissolved.", count)); } else { self.command_line.push_info("No groups found for selected objects."); } } } "COPYCLIP"|"CC" => { let handles: Vec<_> = self.tabs[i] .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("COPYCLIP"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let entities: Vec<_> = handles.iter() .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) .collect(); self.clipboard_centroid = Self::entities_centroid( &self.tabs[i].scene.wire_models_for(&handles), ); self.clipboard = entities; self.command_line.push_info( &format!("{} object(s) copied to clipboard.", self.clipboard.len()), ); } } "CUTCLIP"|"CX" => { let handles: Vec<_> = self.tabs[i] .scene.selected_entities().into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("CUTCLIP"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let entities: Vec<_> = handles.iter() .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned()) .collect(); self.clipboard_centroid = Self::entities_centroid( &self.tabs[i].scene.wire_models_for(&handles), ); let count = entities.len(); self.clipboard = entities; self.push_undo_snapshot(i, "CUTCLIP"); self.tabs[i].scene.erase_entities(&handles); self.tabs[i].scene.deselect_all(); self.tabs[i].dirty = true; self.refresh_properties(); self.command_line.push_info( &format!("{} object(s) cut to clipboard.", count), ); } } "PASTECLIP"|"PC" => { if self.clipboard.is_empty() { self.command_line.push_error("Clipboard is empty."); } else { let wires = self.tabs[i].scene.wires_for_entities(&self.clipboard); use crate::command::paste::PasteCommand; let cmd = PasteCommand::new(wires, self.clipboard_centroid); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } } "BLOCK" => { let handles: Vec<_> = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("BLOCK"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::insert::create_block::CreateBlockCommand; let cmd = CreateBlockCommand::new(handles); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } } "INSERT" => { let blocks = self.tabs[i].scene.custom_block_names(); if blocks.is_empty() { self.command_line .push_error("No user-defined blocks found in this drawing."); } else { use crate::modules::insert::insert_block::InsertBlockCommand; let cmd = InsertBlockCommand::new(blocks); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } } "LINE"|"L" => { use crate::modules::home::draw::line::LineCommand; let new_cmd = LineCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "CIRCLE"|"C" => { use crate::modules::home::draw::circle::CircleCommand; let new_cmd = CircleCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "CIRCLE_CD" => { use crate::modules::home::draw::circle::CircleCDCommand; let new_cmd = CircleCDCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "CIRCLE_2P" => { use crate::modules::home::draw::circle::Circle2PCommand; let new_cmd = Circle2PCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "CIRCLE_3P" => { use crate::modules::home::draw::circle::Circle3PCommand; let new_cmd = Circle3PCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "CIRCLE_TTR" => { use crate::modules::home::draw::circle::CircleTTRCommand; let new_cmd = CircleTTRCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); self.snapper.enabled.insert(crate::snap::SnapType::Tangent); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "CIRCLE_TTT" => { use crate::modules::home::draw::circle::CircleTTTCommand; let new_cmd = CircleTTTCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.pre_cmd_tangent = Some(self.snapper.is_on(crate::snap::SnapType::Tangent)); self.snapper.enabled.insert(crate::snap::SnapType::Tangent); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC"|"A" => { use crate::modules::home::draw::arc::ArcCommand; let new_cmd = ArcCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_3P" => { use crate::modules::home::draw::arc::Arc3PCommand; let new_cmd = Arc3PCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_SCE" => { use crate::modules::home::draw::arc::ArcSCECommand; let new_cmd = ArcSCECommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_SCA" => { use crate::modules::home::draw::arc::ArcSCACommand; let new_cmd = ArcSCACommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_SCL" => { use crate::modules::home::draw::arc::ArcSCLCommand; let new_cmd = ArcSCLCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_SEA" => { use crate::modules::home::draw::arc::ArcSEACommand; let new_cmd = ArcSEACommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_SER" => { use crate::modules::home::draw::arc::ArcSERCommand; let new_cmd = ArcSERCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_SED" => { use crate::modules::home::draw::arc::ArcSEDCommand; let new_cmd = ArcSEDCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_CSA" => { use crate::modules::home::draw::arc::ArcCSACommand; let new_cmd = ArcCSACommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_CSL" => { use crate::modules::home::draw::arc::ArcCSLCommand; let new_cmd = ArcCSLCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "RECT" => { use crate::modules::home::draw::shapes::RectCommand; let new_cmd = RectCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "RECT_ROT" => { use crate::modules::home::draw::shapes::RectRotCommand; let new_cmd = RectRotCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "RECT_CEN" => { use crate::modules::home::draw::shapes::RectCenCommand; let new_cmd = RectCenCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "POLY" => { use crate::modules::home::draw::shapes::PolyCommand; let new_cmd = PolyCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "POLY_C" => { use crate::modules::home::draw::shapes::PolyCCommand; let new_cmd = PolyCCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "POLY_E" => { use crate::modules::home::draw::shapes::PolyECommand; let new_cmd = PolyECommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "PLINE"|"PL" => { use crate::modules::home::draw::polyline::PlineCommand; let new_cmd = PlineCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "MOVE"|"M" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("MOVE"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::translate::MoveCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let new_cmd = MoveCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "COPY"|"CO" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("COPY"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::copy::CopyCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let new_cmd = CopyCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "ROTATE"|"RO" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("ROTATE"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::rotate::RotateCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let new_cmd = RotateCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "POINT"|"PO" => { use crate::modules::home::draw::point::PointCommand; let new_cmd = PointCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "HATCH"|"H" => { use crate::modules::home::draw::hatch::HatchCommand; let outlines = self.tabs[i].scene.closed_outlines(); let new_cmd = HatchCommand::new(outlines); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "GRADIENT" => { use crate::modules::home::draw::hatch::GradientCommand; let outlines = self.tabs[i].scene.closed_outlines(); let new_cmd = GradientCommand::new(outlines); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "BOUNDARY" => { use crate::modules::home::draw::hatch::BoundaryCommand; let outlines = self.tabs[i].scene.closed_outlines(); let new_cmd = BoundaryCommand::new(outlines); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ELLIPSE"|"EL" => { use crate::modules::home::draw::ellipse::EllipseCommand; let new_cmd = EllipseCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ELLIPSE_AXIS" => { use crate::modules::home::draw::ellipse::EllipseAxisCommand; let new_cmd = EllipseAxisCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ELLIPSE_ARC" => { use crate::modules::home::draw::ellipse::EllipseArcCommand; let new_cmd = EllipseArcCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "SPLINE"|"SPL" => { use crate::modules::home::draw::spline::SplineCommand; let new_cmd = SplineCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "SCALE"|"SC" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("SCALE"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::scale::ScaleCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let new_cmd = ScaleCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "MIRROR"|"MI" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("MIRROR"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::mirror::MirrorCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let new_cmd = MirrorCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "ERASE"|"E" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("ERASE"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { let n = handles.len(); self.push_undo_snapshot(i, "ERASE"); self.tabs[i].scene.erase_entities(&handles); self.tabs[i].dirty = true; self.refresh_properties(); self.command_line.push_output(&format!("{n} object(s) erased.")); } } "TEXT"|"T"|"DT" => { use crate::modules::annotate::text::TextCommand; let new_cmd = TextCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "MTEXT"|"MT" => { use crate::modules::annotate::mtext::MTextCommand; let new_cmd = MTextCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "DIMLINEAR" => { use crate::modules::annotate::linear_dim::LinearDimensionCommand; let new_cmd = LinearDimensionCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "DIMRADIUS" => { use crate::modules::annotate::radius_dim::RadiusDimensionCommand; let new_cmd = RadiusDimensionCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "DIMANGULAR" => { use crate::modules::annotate::angular_dim::AngularDimensionCommand; let new_cmd = AngularDimensionCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "LEADER"|"LE" => { use crate::modules::annotate::leader_cmd::LeaderCommand; let new_cmd = LeaderCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "MLEADER"|"MLD" => { use crate::modules::annotate::mleader_cmd::MLeaderCommand; let new_cmd = MLeaderCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ZOOM EXTENTS"|"ZOOMEXTENTS"|"ZE" => { self.tabs[i].scene.fit_all(); self.command_line.push_output("Zoom Extents"); } "STRETCH"|"SS" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("STRETCH"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::stretch::StretchCommand; let new_cmd = StretchCommand::new(handles); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "FILLET"|"F" => { use crate::modules::home::modify::fillet::FilletCommand; let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() .filter_map(|w| { let h = Scene::handle_from_wire_name(&w.name)?; self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) }).collect(); let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); let new_cmd = FilletCommand::new( crate::modules::home::defaults::get_fillet_radius(), all_entities); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARRAY"|"AR"|"ARRAYRECT" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("ARRAYRECT"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::array::ArrayRectCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let new_cmd = ArrayRectCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "ARRAYPOLAR" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("ARRAYPOLAR"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::array::ArrayPolarCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let new_cmd = ArrayPolarCommand::new(handles, wires); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "ARRAYPATH" => { let handles: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().map(|(h, _)| h).collect(); if handles.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("ARRAYPATH"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { use crate::modules::home::modify::array::ArrayPathCommand; let wires = self.tabs[i].scene.wire_models_for(&handles); let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() .filter_map(|w| { let h = Scene::handle_from_wire_name(&w.name)?; self.tabs[i].scene.document.get_entity(h).cloned() }).collect(); let new_cmd = ArrayPathCommand::new(handles, wires, all_entities); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } "CHAMFER"|"CHA" => { use crate::modules::home::modify::fillet::ChamferCommand; let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() .filter_map(|w| { let h = Scene::handle_from_wire_name(&w.name)?; self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) }).collect(); let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); let new_cmd = ChamferCommand::new( crate::modules::home::defaults::get_chamfer_dist1(), all_entities); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "EXPLODE"|"X" => { use crate::modules::home::modify::explode::explode_entity; // Collect into owned vec to release borrow before mutating let entities: Vec<_> = self.tabs[i].scene.selected_entities() .into_iter().collect(); if entities.is_empty() { use crate::command::select::SelectObjectsCommand; let cmd = SelectObjectsCommand::new("EXPLODE"); self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } else { // Pre-compute all replacements before mutating let replacements: Vec<(acadrust::Handle, Vec)> = entities .iter() .filter_map(|(h, e)| { let pieces = explode_entity(e, &self.tabs[i].scene.document); if pieces.is_empty() { None } else { Some((*h, pieces)) } }) .collect(); let exploded = replacements.len(); if exploded > 0 { self.push_undo_snapshot(i, "EXPLODE"); } for (handle, pieces) in replacements { self.tabs[i].scene.erase_entities(&[handle]); for piece in pieces { self.tabs[i].scene.add_entity(piece); } } if exploded > 0 { self.tabs[i].dirty = true; self.refresh_properties(); self.command_line.push_output(&format!("{exploded} object(s) exploded.")); } else { self.command_line.push_info("EXPLODE: no explodable objects selected."); } } } "OFFSET"|"O" => { use crate::modules::home::modify::offset::OffsetCommand; let all_entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() .filter_map(|w| { let h = Scene::handle_from_wire_name(&w.name)?; self.tabs[i].scene.document.get_entity(h).cloned() }).collect(); let new_cmd = OffsetCommand::new( crate::modules::home::defaults::get_offset_dist(), all_entities); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "TRIM"|"TR" => { use crate::modules::home::modify::trim::TrimCommand; let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() .filter_map(|w| { let h = Scene::handle_from_wire_name(&w.name)?; self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) }).collect(); let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); let new_cmd = TrimCommand::new(all_entities); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "EXTEND"|"EX" => { use crate::modules::home::modify::trim::ExtendCommand; let entities: Vec<_> = self.tabs[i].scene.entity_wires().iter() .filter_map(|w| { let h = Scene::handle_from_wire_name(&w.name)?; self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e)) }).collect(); let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect(); let new_cmd = ExtendCommand::new(all_entities); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "3DORBIT"|"3O"=> self.command_line.push_info("3D Orbit: drag with right mouse button."), "HELP"|"?" => self.command_line.push_output("Draw: LINE CIRCLE ARC PLINE POINT ELLIPSE SPLINE | Modify: MOVE COPY ROTATE SCALE MIRROR ERASE | Text: TEXT MTEXT | File: OPEN SAVE SAVEAS"), "DONATE" => { let _ = open::that("https://patreon.com/HakanSeven12"); self.command_line.push_info("Opening Patreon page..."); } // ── Layout / viewport ───────────────────────────────────────── "MVIEW"|"MV" => { if self.tabs[i].scene.current_layout == "Model" { self.command_line.push_error("MVIEW: önce bir paper space layout'una geçin."); } else { use crate::modules::layout::mview::MviewCommand; let new_cmd = MviewCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } } _ => self.command_line.push_error(&format!("Unknown command: {cmd}")), } // Focus the command line whenever a command just became active. let i = self.active_tab; if self.tabs[i].active_cmd.is_some() { self.tabs[i].last_cmd = Some(cmd.to_string()); self.focus_cmd_input() } else { Task::none() } } /// Rebuild the PropertiesPanel from the current entity selection. /// Preserves UI state (open pickers, edit buffer) across refreshes. fn refresh_properties(&mut self) { let i = self.active_tab; // Preserve transient UI state across rebuilds. let color_picker_open = self.tabs[i].properties.color_picker_open; let color_palette_open = self.tabs[i].properties.color_palette_open; let edit_buf = std::mem::take(&mut self.tabs[i].properties.edit_buf); let selected_group = self.tabs[i].properties.selected_group.clone(); let layer_names: Vec = self.tabs[i] .scene .document .layers .iter() .map(|l| l.name.clone()) .collect(); let linetype_items: Vec = self.tabs[i] .scene .document .line_types .iter() .map(|lt| { let name = if lt.name.is_empty() { "ByLayer".to_string() } else { lt.name.clone() }; let art = linetypes::extract_pattern(<.description); ui::properties::LinetypeItem { name, art } }) .collect(); let text_style_names: Vec = self.tabs[i] .scene .document .text_styles .iter() .map(|style| style.name.trim().to_string()) .filter(|name| !name.is_empty()) .collect(); let new_panel = { let selected = self.tabs[i].scene.selected_entities(); let mut panel = match selected.len() { 0 => ui::PropertiesPanel::empty(), 1 => { let (handle, entity) = selected[0]; let group_names = self.tabs[i].scene.group_names_for_entity(handle); let mut sections = dispatch::properties_sectioned(handle, entity, &text_style_names); if !group_names.is_empty() { let label = group_names.join(", "); if let Some(general) = sections.first_mut() { general.props.push(crate::scene::object::Property { label: "Group".to_string(), field: "group", value: crate::scene::object::PropValue::ReadOnly(label), }); } } let title = entity_type_label(entity); ui::PropertiesPanel { choice_combos: sections .iter() .flat_map(|section| section.props.iter()) .filter_map(|prop| match &prop.value { crate::scene::object::PropValue::Choice { options, .. } => Some(( prop.field.to_string(), iced::widget::combo_box::State::new(options.clone()), )), _ => None, }) .collect(), sections, title, layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()), hatch_pattern_combo: iced::widget::combo_box::State::new( crate::scene::hatch_patterns::names(), ), lineweight_combo: iced::widget::combo_box::State::new( ui::properties::lw_options(), ), linetype_items, ..Default::default() } } _ => { let groups = build_selection_groups(&selected); let active_group = selected_group .and_then(|group| groups.iter().find(|g| g.label == group.label).cloned()) .or_else(|| groups.first().cloned()); let filtered: Vec<(Handle, &EntityType)> = active_group .as_ref() .map(|group| { selected .iter() .filter(|(handle, _)| group.handles.contains(handle)) .copied() .collect() }) .unwrap_or_default(); let sections = aggregate_sections(&filtered, &text_style_names); ui::PropertiesPanel { choice_combos: sections .iter() .flat_map(|section| section.props.iter()) .filter_map(|prop| match &prop.value { crate::scene::object::PropValue::Choice { options, .. } => Some(( prop.field.to_string(), iced::widget::combo_box::State::new(options.clone()), )), _ => None, }) .collect(), sections, title: format!("{} objects selected", selected.len()), selection_group_combo: iced::widget::combo_box::State::new(groups.clone()), selection_groups: groups, selected_group: active_group, layer_combo: iced::widget::combo_box::State::new(layer_names.clone()), linetype_combo: iced::widget::combo_box::State::new(linetype_items.clone()), hatch_pattern_combo: iced::widget::combo_box::State::new( crate::scene::hatch_patterns::names(), ), lineweight_combo: iced::widget::combo_box::State::new( ui::properties::lw_options(), ), linetype_items, ..Default::default() } } }; // Restore UI state. panel.color_picker_open = color_picker_open; panel.color_palette_open = color_palette_open; panel.edit_buf = edit_buf; panel // `selected` is dropped here, releasing the borrow on `self.tabs[i].scene` }; self.tabs[i].properties = new_panel; self.refresh_selected_grips(); } /// Rebuild the cached selected_grips from the current entity selection. fn refresh_selected_grips(&mut self) { let i = self.active_tab; let (new_handle, new_grips) = { let selected = self.tabs[i].scene.selected_entities(); if selected.len() == 1 { let (handle, entity) = selected[0]; let grips = dispatch::grips(entity); (Some(handle), grips) } else { (None, vec![]) } // `selected` is dropped here }; self.tabs[i].selected_handle = new_handle; self.tabs[i].selected_grips = new_grips; } fn property_target_handles(&self, i: usize) -> Vec { let handles = self.tabs[i].properties.selected_handles(); if !handles.is_empty() { handles } else { self.tabs[i].selected_handle.into_iter().collect() } } fn history_label_from_active_cmd(&self, i: usize, fallback: &'static str) -> String { self.tabs[i] .active_cmd .as_ref() .map(|cmd| cmd.name().to_string()) .unwrap_or_else(|| fallback.to_string()) } fn capture_history_snapshot(&self, i: usize, label: impl Into) -> HistorySnapshot { HistorySnapshot { document: self.tabs[i].scene.document.clone(), current_layout: self.tabs[i].scene.current_layout.clone(), selected: self.tabs[i].scene.selected.iter().copied().collect(), dirty: self.tabs[i].dirty, label: label.into(), } } fn sync_ribbon_layers(&mut self) { let i = self.active_tab; let active = self.tabs[i].active_layer.clone(); // Build LayerInfo from the panel (already synced from doc). let infos: Vec = self.tabs[i] .layers .layers .iter() .map(|l| crate::ui::ribbon::LayerInfo { name: l.name.clone(), color: l.color, visible: l.visible, frozen: l.frozen, locked: l.locked, }) .collect(); let names: Vec = infos.iter().map(|l| l.name.clone()).collect(); // If the active layer no longer exists, fall back to "0". let active = if names.contains(&active) { active } else { "0".to_string() }; self.tabs[i].active_layer = active.clone(); self.tabs[i].layers.current_layer = active.clone(); self.ribbon.set_layers(infos, &active); // Sync linetypes into the layer panel (using LinetypeItem with ASCII art) let lt_items: Vec = self.tabs[i] .scene .document .line_types .iter() .map(|lt| { let name = if lt.name.eq_ignore_ascii_case("bylayer") { "ByLayer".to_string() } else { lt.name.clone() }; let art = linetypes::extract_pattern(<.description); ui::properties::LinetypeItem { name, art } }) .collect(); self.tabs[i].layers.sync_linetypes(lt_items.clone()); self.ribbon.set_available_linetypes(lt_items); } fn push_undo_snapshot(&mut self, i: usize, label: impl Into) { let snapshot = self.capture_history_snapshot(i, label); self.tabs[i].history.undo_stack.push(snapshot); self.tabs[i].history.redo_stack.clear(); } fn restore_history_snapshot(&mut self, i: usize, snapshot: HistorySnapshot) { self.tabs[i].scene.document = snapshot.document; self.tabs[i].scene.current_layout = snapshot.current_layout; self.tabs[i].scene.selected = snapshot .selected .into_iter() .filter(|h| self.tabs[i].scene.document.get_entity(*h).is_some()) .collect::>(); self.tabs[i].scene.populate_hatches_from_document(); self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.meshes.clear(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].active_grip = None; self.tabs[i].dirty = snapshot.dirty; let doc_layers = self.tabs[i].scene.document.layers.clone(); self.tabs[i].layers.sync_from_doc(&doc_layers); self.sync_ribbon_layers(); self.refresh_properties(); } fn undo_active_tab(&mut self) { self.undo_steps(1); } fn redo_active_tab(&mut self) { self.redo_steps(1); } fn undo_steps(&mut self, steps: usize) { let i = self.active_tab; let available = self.tabs[i].history.undo_stack.len(); let steps = steps.min(available); if steps == 0 { self.command_line.push_info("Nothing to undo."); return; } let mut last_label = String::new(); for _ in 0..steps { let Some(snapshot) = self.tabs[i].history.undo_stack.pop() else { break; }; let label = snapshot.label.clone(); let current = self.capture_history_snapshot(i, label.clone()); self.tabs[i].history.redo_stack.push(current); self.restore_history_snapshot(i, snapshot); last_label = label; } self.command_line.push_output(&format!("Undo: {last_label}")); } fn redo_steps(&mut self, steps: usize) { let i = self.active_tab; let available = self.tabs[i].history.redo_stack.len(); let steps = steps.min(available); if steps == 0 { self.command_line.push_info("Nothing to redo."); return; } let mut last_label = String::new(); for _ in 0..steps { let Some(snapshot) = self.tabs[i].history.redo_stack.pop() else { break; }; let label = snapshot.label.clone(); let current = self.capture_history_snapshot(i, label.clone()); self.tabs[i].history.undo_stack.push(current); self.restore_history_snapshot(i, snapshot); last_label = label; } self.command_line.push_output(&format!("Redo: {last_label}")); } /// Add an entity to the correct space: /// - Viewport entities while in paper space → add_entity_to_layout /// - Everything else → model space (add_entity) fn commit_entity(&mut self, mut entity: acadrust::EntityType) { let i = self.active_tab; let layer = &self.tabs[i].active_layer; if layer != "0" || entity.as_entity().layer().is_empty() { entity.as_entity_mut().set_layer(layer.clone()); } // Apply active color/linetype/lineweight from Properties toolbar. crate::scene::dispatch::apply_color(&mut entity, self.ribbon.active_color); crate::scene::dispatch::apply_common_prop( &mut entity, "linetype", &self.ribbon.active_linetype.clone(), ); crate::scene::dispatch::apply_line_weight(&mut entity, self.ribbon.active_lineweight); if matches!(&entity, acadrust::EntityType::Viewport(_)) && self.tabs[i].scene.current_layout != "Model" { let layout = self.tabs[i].scene.current_layout.clone(); match self.tabs[i] .scene .document .add_entity_to_layout(entity, &layout) { Ok(_) => {} Err(e) => self .command_line .push_error(&format!("Viewport eklenemedi: {e}")), } } else { self.tabs[i].scene.add_entity(entity); } } fn apply_cmd_result(&mut self, result: CmdResult) -> Task { let i = self.active_tab; match result { CmdResult::NeedPoint => { let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } } CmdResult::Preview(wire) => { self.tabs[i].scene.set_preview_wires(vec![wire]); let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } } CmdResult::InterimWire(wire) => { self.tabs[i].scene.set_interim_wire(wire); let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } } CmdResult::CommitEntity(entity) => { let label = self.history_label_from_active_cmd(i, "ENTITY"); self.push_undo_snapshot(i, label); self.commit_entity(entity); self.tabs[i].dirty = true; let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } } CmdResult::TransformSelected(handles, transform) => { let label = self.history_label_from_active_cmd(i, "MOVE"); self.push_undo_snapshot(i, label); self.tabs[i].scene.transform_entities(&handles, &transform); self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); } } self.refresh_properties(); } CmdResult::CopySelected(handles, transform) => { let label = self.history_label_from_active_cmd(i, "COPY"); self.push_undo_snapshot(i, label); let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform); self.tabs[i].dirty = true; self.tabs[i].scene.deselect_all(); for h in new_handles { self.tabs[i].scene.select_entity(h, false); } self.tabs[i].scene.clear_preview_wire(); let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } self.refresh_properties(); } CmdResult::CommitAndExit(entity) => { let label = self.history_label_from_active_cmd(i, "ENTITY"); self.push_undo_snapshot(i, label); self.commit_entity(entity); self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); } } } CmdResult::CreateBlock { handles, name, base, } => { self.push_undo_snapshot(i, "BLOCK"); match self.tabs[i].scene.create_block_from_entities(&handles, &name, base) { Ok(insert_handle) => { self.tabs[i].dirty = true; self.tabs[i].scene.deselect_all(); if !insert_handle.is_null() { self.tabs[i].scene.select_entity(insert_handle, false); } self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_output(&format!("Block \"{name}\" created.")); self.refresh_properties(); } Err(err) => { let _ = self.tabs[i].history.undo_stack.pop(); self.command_line.push_error(&err); let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } } } } CmdResult::CommitHatch(hatch) => { let label = self.history_label_from_active_cmd(i, "HATCH"); self.push_undo_snapshot(i, label); let new_handle = self.tabs[i].scene.add_hatch(hatch); // Select the newly created hatch so Properties reflects it immediately. if !new_handle.is_null() { self.tabs[i].scene.select_entity(new_handle, true); } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); } } self.refresh_properties(); } CmdResult::BatchCopy(handles, transforms) => { let label = self.history_label_from_active_cmd(i, "ARRAY"); self.push_undo_snapshot(i, label); let count = transforms.len(); for t in &transforms { self.tabs[i].scene.copy_entities(&handles, t); } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); } } self.command_line .push_output(&format!("ARRAY: {count} copies created.")); self.refresh_properties(); } CmdResult::ReplaceMany(replacements, additions) => { let label = self.history_label_from_active_cmd(i, "FILLET"); self.push_undo_snapshot(i, label); for (handle, entities) in replacements { self.tabs[i].scene.erase_entities(&[handle]); for entity in entities { self.tabs[i].scene.add_entity(entity); } } for entity in additions { self.tabs[i].scene.add_entity(entity); } self.tabs[i].dirty = true; self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.refresh_properties(); } CmdResult::ReplaceEntity(handle, new_entities) => { let label = self.history_label_from_active_cmd(i, "TRIM"); self.push_undo_snapshot(i, label); self.tabs[i].scene.erase_entities(&[handle]); let new_handles: Vec = new_entities .into_iter() .map(|e| self.tabs[i].scene.add_entity(e)) .collect(); if let Some(cmd) = &mut self.tabs[i].active_cmd { cmd.on_entity_replaced(handle, &new_handles); } self.tabs[i].dirty = true; let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); } } CmdResult::Cancel => { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); } } self.command_line.push_info("Command cancelled."); } CmdResult::Relaunch(cmd, handles) => { // Install the gathered selection then re-dispatch the original command. self.tabs[i].scene.deselect_all(); for h in &handles { self.tabs[i].scene.select_entity(*h, false); } self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); if let Some(was_on) = self.pre_cmd_tangent.take() { if !was_on { self.snapper.enabled.remove(&crate::snap::SnapType::Tangent); } } let _ = self.dispatch_command(&cmd); } CmdResult::MatchEntityLayer { dest, src } => { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); // Look up source entity's layer name. let src_layer = self.tabs[i].scene.document .get_entity(src) .map(|e| e.common().layer.clone()); if let Some(layer) = src_layer { self.push_undo_snapshot(i, "LAYMATCH"); for h in &dest { if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { e.as_entity_mut().set_layer(layer.clone()); } } self.tabs[i].dirty = true; self.command_line.push_info(&format!("Layer matched to \"{layer}\".")); self.sync_ribbon_layers(); } else { self.command_line.push_error("Source object not found."); } } CmdResult::MatchProperties { dest, src } => { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); // Read all visual properties from source entity. let props = self.tabs[i].scene.document.get_entity(src).map(|e| { let c = e.common(); ( c.layer.clone(), c.color, c.linetype.clone(), c.linetype_scale, c.line_weight, ) }); if let Some((layer, color, linetype, lt_scale, lw)) = props { self.push_undo_snapshot(i, "MATCHPROP"); for h in &dest { if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) { e.as_entity_mut().set_layer(layer.clone()); crate::scene::dispatch::apply_color(e, color); crate::scene::dispatch::apply_line_weight(e, lw); e.common_mut().linetype = linetype.clone(); e.common_mut().linetype_scale = lt_scale; } } self.tabs[i].dirty = true; self.refresh_properties(); self.command_line.push_info( &format!("Properties matched to {} object(s).", dest.len()) ); } else { self.command_line.push_error("Source object not found."); } } CmdResult::PasteClipboard { base_pt } => { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); if self.clipboard.is_empty() { self.command_line.push_error("Clipboard is empty."); } else { let delta = base_pt - self.clipboard_centroid; let translate = command::EntityTransform::Translate(delta); self.push_undo_snapshot(i, "PASTECLIP"); let count = self.clipboard.len(); let new_handles: Vec = self.clipboard.clone() .into_iter() .map(|mut entity| { crate::scene::dispatch::apply_transform(&mut entity, &translate); entity.common_mut().handle = acadrust::Handle::NULL; self.tabs[i].scene.add_entity(entity) }) .filter(|h| !h.is_null()) .collect(); self.tabs[i].scene.deselect_all(); for h in new_handles { self.tabs[i].scene.select_entity(h, false); } self.tabs[i].dirty = true; self.refresh_properties(); self.command_line.push_info(&format!("{count} object(s) pasted.")); } } CmdResult::CreateGroup { handles, name } => { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); self.push_undo_snapshot(i, "GROUP"); self.tabs[i].scene.create_group(name.clone(), handles); self.tabs[i].dirty = true; self.command_line.push_info(&format!("Group \"{}\" created.", name)); } CmdResult::DeleteGroups { handles } => { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); self.push_undo_snapshot(i, "UNGROUP"); let count = self.tabs[i].scene.delete_groups_containing(&handles); self.tabs[i].dirty = true; if count > 0 { self.command_line.push_info(&format!("{} group(s) dissolved.", count)); } else { self.command_line.push_info("No groups found for selected objects."); } } } // Focus the command-line input while a command is active; blur it when the command ends. if self.tabs[i].active_cmd.is_some() { self.focus_cmd_input() } else { self.blur_cmd_input() } } /// Compute the centroid of a set of wire models (average of all points). fn entities_centroid(wires: &[scene::WireModel]) -> glam::Vec3 { let mut sum = glam::Vec3::ZERO; let mut count = 0usize; for w in wires { for p in &w.points { sum += glam::Vec3::from(*p); count += 1; } } if count > 0 { sum / count as f32 } else { glam::Vec3::ZERO } } /// Generate the next available auto group name ("*A1", "*A2", …). fn next_group_auto_name(scene: &scene::Scene) -> String { let existing: HashSet = scene.groups().map(|g| g.name.clone()).collect(); for n in 1..=9999 { let name = format!("*A{n}"); if !existing.contains(&name) { return name; } } "*A".to_string() } fn focus_cmd_input(&self) -> Task { iced::widget::operation::focus(iced::widget::Id::new(ui::command_line::CMD_INPUT_ID)) } fn blur_cmd_input(&self) -> Task { let op = iced::advanced::widget::operation::focusable::unfocus::(); iced::advanced::widget::operate(op) } fn view(&self, window_id: window::Id) -> Element<'_, Message> { // ── Layer Properties Manager window ─────────────────────────────── if Some(window_id) == self.layer_window { let tab = &self.tabs[self.active_tab]; return tab.layers.view_window(); } let i = self.active_tab; let tab = &self.tabs[i]; let is_paper = tab.scene.current_layout != "Model"; let viewport_3d = shader(&tab.scene).width(Fill).height(Fill); let selection_overlay = { let sel = tab.scene.selection.borrow().clone(); let snap_info = tab.snap_result.map(|s| (s.screen, s.snap_type)); // Compute grip markers when a single entity is selected and no command active. let grips: Vec = if tab.active_cmd.is_none() && !tab.selected_grips.is_empty() { let (vw, vh) = tab.scene.selection.borrow().vp_size; let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh, }; let vp_mat = tab.scene.camera.borrow().view_proj(bounds); let sel_h = tab.selected_handle; grips_to_screen(&tab.selected_grips, vp_mat, bounds) .into_iter() .filter(|(_, screen, _, _)| { screen.x.is_finite() && screen.y.is_finite() && screen.x >= -bounds.width && screen.x <= bounds.width * 2.0 && screen.y >= -bounds.height && screen.y <= bounds.height * 2.0 }) .map(|(grip_id, screen, _is_midpoint, shape)| { let is_hot = tab .active_grip .as_ref() .map_or(false, |g| Some(g.handle) == sel_h && g.grip_id == grip_id); ui::overlay::GripMarker { pos: screen, shape, is_hot, } }) .collect() } else { vec![] }; let grid = if self.show_grid { let (vw, vh) = tab.scene.selection.borrow().vp_size; let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh, }; let cam = tab.scene.camera.borrow(); let plane = grid_plane_from_camera(cam.pitch, cam.yaw); Some(overlay::GridParams { view_proj: cam.view_proj(bounds), bounds, plane, }) } else { None }; overlay::selection_overlay(sel, snap_info, grips, grid) }; let nav = container(overlay::nav_toolbar()) .align_right(Fill) .align_top(Fill) .padding(iced::Padding { top: 148.0, right: 8.0, bottom: 0.0, left: 0.0, }); let info = container(overlay::info_bar( if is_paper { &tab.scene.current_layout } else { "Custom View" }, &tab.visual_style, )) .padding([4, 6]); let viewport_mouse = mouse_area(container( iced::widget::Space::new().width(Fill).height(Fill), )) .on_move(Message::ViewportMove) .on_press(Message::ViewportLeftPress) .on_release(Message::ViewportLeftRelease) .on_right_press(Message::ViewportRightPress) .on_right_release(Message::ViewportRightRelease) .on_middle_press(Message::ViewportMiddlePress) .on_middle_release(Message::ViewportMiddleRelease) .on_scroll(Message::ViewportScroll) .on_exit(Message::ViewportExit); let cube_click = mouse_area(container( iced::widget::Space::new() .width(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)) .height(iced::Length::Fixed(VIEWCUBE_HIT_SIZE)), )) .on_move(Message::CursorMoved) .on_press(Message::ViewportClick); let cube_click = container(cube_click) .align_right(Fill) .align_top(Fill) .padding(iced::Padding { top: VIEWCUBE_PAD, right: VIEWCUBE_PAD, bottom: 0.0, left: 0.0, }) .width(Fill) .height(Fill); let bg_color = if is_paper { Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0, } // slightly blue-grey for paper space } else { Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0, } // dark for model space }; let viewport_stack = stack![ container(viewport_3d) .style(move |_: &Theme| container::Style { background: Some(Background::Color(bg_color)), ..Default::default() }) .width(Fill) .height(Fill), container(info).width(Fill).height(Fill), selection_overlay, viewport_mouse, nav, cube_click, ]; let viewport_stack = viewport_stack.width(Fill).height(Fill); let center_stack = iced::widget::stack![ row![tab.properties.view(), viewport_stack] .width(Fill) .height(Fill), ] .width(Fill) .height(Fill); let center = center_stack; // Document tab bar — ribbon ile center arasında tam genişlikte let tab_bar = doc_tab_bar(&self.tabs, self.active_tab); let main_ui = container( column![ self.ribbon.view( is_paper, self.tabs[self.active_tab].history.undo_stack.len(), self.tabs[self.active_tab].history.redo_stack.len(), ), tab_bar, center, self.command_line.view(), self.status_bar.view( &self.snapper, self.snap_popup_open, self.ortho_mode, self.polar_mode, self.show_grid, tab.scene.layout_names(), tab.scene.current_layout.clone() ) ] .width(Fill) .height(Fill), ) .style(|_: &Theme| container::Style { background: Some(Background::Color(Color { r: 0.11, g: 0.11, b: 0.11, a: 1.0, })), ..Default::default() }) .width(Fill) .height(Fill); let snap_layer: Element<'_, Message> = if self.snap_popup_open { ui::snap_popup::snap_popup_overlay(&self.snapper, 4.0) } else { iced::widget::Space::new().width(0).height(0).into() }; let dropdown_layer: Element<'_, Message> = self .ribbon .dropdown_overlay( &history_dropdown_labels(&self.tabs[self.active_tab].history.undo_stack), &history_dropdown_labels(&self.tabs[self.active_tab].history.redo_stack), ) .unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into()); stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer].into() } fn subscription(&self) -> Subscription { use iced::{event, keyboard}; iced::Subscription::batch([ window::frames().map(Message::Tick), event::listen_with(|ev, status, win_id| { use iced::event::Status; match ev { iced::Event::Window(window::Event::Closed) => { Some(Message::OsWindowClosed(win_id)) } iced::Event::Window(window::Event::Resized(sz)) => { Some(Message::WindowResized(sz.width as f32, sz.height as f32)) } iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) => { let ctrl = modifiers.control(); let shift = modifiers.shift(); match key { keyboard::Key::Named(keyboard::key::Named::Enter) | keyboard::Key::Named(keyboard::key::Named::Space) if status == Status::Ignored => { Some(Message::CommandFinalize) } keyboard::Key::Named(keyboard::key::Named::Escape) => { Some(Message::CommandEscape) } keyboard::Key::Named(keyboard::key::Named::Delete) if status == Status::Ignored => { Some(Message::DeleteSelected) } keyboard::Key::Named(keyboard::key::Named::F3) => { Some(Message::ToggleSnapEnabled) } keyboard::Key::Named(keyboard::key::Named::F7) => { Some(Message::ToggleGrid) } keyboard::Key::Named(keyboard::key::Named::F8) => { Some(Message::ToggleOrtho) } keyboard::Key::Named(keyboard::key::Named::F9) => { Some(Message::ToggleGridSnap) } keyboard::Key::Named(keyboard::key::Named::F10) => { Some(Message::TogglePolar) } // ── File shortcuts ──────────────────────────── keyboard::Key::Character(c) if ctrl => match c.as_str() { "n" => Some(Message::ClearScene), "o" => Some(Message::OpenFile), "s" if !shift => Some(Message::SaveFile), "s" if shift => Some(Message::SaveAs), "z" if !shift => Some(Message::Undo), "z" if shift => Some(Message::Redo), "y" => Some(Message::Redo), "c" => Some(Message::Command("COPYCLIP".to_string())), "x" => Some(Message::Command("CUTCLIP".to_string())), "v" => Some(Message::Command("PASTECLIP".to_string())), _ => None, }, _ => None, } } _ => None, } }), ]) } } impl Default for H7CAD { fn default() -> Self { Self::new() } } fn history_dropdown_labels(stack: &[HistorySnapshot]) -> Vec { stack.iter().rev().map(|snapshot| snapshot.label.clone()).collect() } // ── Grid plane detection ─────────────────────────────────────────────────── /// Choose the grid plane whose normal is most aligned with the camera view direction. /// Switches at the 45° octant boundary — no hysteresis needed since the grid is /// a visual aid and not selection-sensitive. fn grid_plane_from_camera(pitch: f32, yaw: f32) -> ui::overlay::GridPlane { use ui::overlay::GridPlane; // Camera forward direction components (Z-up, see camera.rs): // forward.x = -cos(pitch) * sin(yaw) // forward.y = -cos(pitch) * cos(yaw) // forward.z = -sin(pitch) let fz = pitch.sin().abs(); // Z-axis alignment → XY plane let fy = (pitch.cos() * yaw.cos()).abs(); // Y-axis alignment → XZ plane let fx = (pitch.cos() * yaw.sin()).abs(); // X-axis alignment → YZ plane if fz >= fy && fz >= fx { GridPlane::Xy } else if fy >= fx { GridPlane::Xz } else { GridPlane::Yz } } // ── Drawing constraint helpers ───────────────────────────────────────────── /// Constrain `pt` to the nearest 90° direction from `base` (XY plane, Z-up). fn ortho_constrain(pt: glam::Vec3, base: glam::Vec3) -> glam::Vec3 { let dx = (pt.x - base.x).abs(); let dy = (pt.y - base.y).abs(); if dx >= dy { glam::Vec3::new(pt.x, base.y, pt.z) // horizontal } else { glam::Vec3::new(base.x, pt.y, pt.z) // vertical } } /// Constrain `pt` to the nearest polar angle multiple from `base` (XY plane, Z-up). fn polar_constrain(pt: glam::Vec3, base: glam::Vec3, step_deg: f32) -> glam::Vec3 { let dx = pt.x - base.x; let dy = pt.y - base.y; let dist = (dx * dx + dy * dy).sqrt(); if dist < 1e-6 { return pt; } let step = step_deg.to_radians(); let angle = dy.atan2(dx); let snapped = (angle / step).round() * step; glam::Vec3::new( base.x + dist * snapped.cos(), base.y + dist * snapped.sin(), pt.z, ) } // ── Document tab bar ─────────────────────────────────────────────────────── fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Element<'a, Message> { const BAR_BG: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0, }; const TAB_ACTIVE: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0, }; const TAB_HOVER: Color = Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0, }; const TAB_INACTIVE: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0, }; const ACCENT: Color = Color { r: 0.20, g: 0.55, b: 0.90, a: 1.0, }; const TEXT_ACTIVE: Color = Color::WHITE; const TEXT_INACTIVE: Color = Color { r: 0.60, g: 0.60, b: 0.60, a: 1.0, }; const CLOSE_HOVER: Color = Color { r: 0.70, g: 0.22, b: 0.22, a: 1.0, }; const BORDER_COLOR: Color = Color { r: 0.25, g: 0.25, b: 0.25, a: 1.0, }; let mut bar = Row::new().spacing(0).align_y(iced::Center); for (idx, tab) in tabs.iter().enumerate() { let is_active = idx == active_tab; let name = tab.tab_display_name(); let label = if tab.dirty { format!("● {}", name) } else { name }; let title_btn = button(text(label).size(12)) .on_press(Message::TabSwitch(idx)) .padding([5, 12]) .style(move |_: &Theme, status| button::Style { background: Some(Background::Color(match (is_active, status) { (true, _) => TAB_ACTIVE, (false, button::Status::Hovered) => TAB_HOVER, _ => TAB_INACTIVE, })), text_color: if is_active { TEXT_ACTIVE } else { TEXT_INACTIVE }, border: Border { color: if is_active { ACCENT } else { Color::TRANSPARENT }, width: if is_active { 1.0 } else { 0.0 }, radius: 0.0.into(), }, shadow: iced::Shadow::default(), snap: false, }); // Show close button only when there are multiple tabs, or always show it let close_btn = button(text("×").size(11).color(Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0, })) .on_press(Message::TabClose(idx)) .padding([3, 5]) .style(move |_: &Theme, status| button::Style { background: Some(Background::Color(match status { button::Status::Hovered => CLOSE_HOVER, _ => { if is_active { TAB_ACTIVE } else { TAB_INACTIVE } } })), border: Border { radius: 3.0.into(), ..Default::default() }, ..Default::default() }); bar = bar.push( container(row![title_btn, close_btn].spacing(0).align_y(iced::Center)).style( move |_: &Theme| container::Style { border: Border { color: if is_active { BORDER_COLOR } else { Color::TRANSPARENT }, width: if is_active { 1.0 } else { 0.0 }, radius: 0.0.into(), }, ..Default::default() }, ), ); } // "+" new tab button let new_btn = button(text("+").size(14).color(Color { r: 0.65, g: 0.65, b: 0.65, a: 1.0, })) .on_press(Message::TabNew) .padding([4, 10]) .style(|_: &Theme, status| button::Style { background: Some(Background::Color(match status { button::Status::Hovered => TAB_HOVER, _ => Color::TRANSPARENT, })), border: Border { radius: 0.0.into(), ..Default::default() }, ..Default::default() }); bar = bar.push(new_btn); bar = bar.push(iced::widget::Space::new().width(Fill)); container(bar) .style(|_: &Theme| container::Style { background: Some(Background::Color(BAR_BG)), border: Border { color: BORDER_COLOR, width: 1.0, radius: 0.0.into(), }, ..Default::default() }) .height(30) .width(Fill) .padding([0, 2]) .into() } // ── Window icon ──────────────────────────────────────────────────────────── /// Builds a 32×32 RGBA icon: red background with H7 drawn in white pixels. fn build_window_icon() -> Vec { const W: usize = 32; const SZ: usize = W * W * 4; let bg = [176u8, 48, 32, 255]; let fg = [255u8, 255, 255, 255]; let mut px = vec![0u8; SZ]; for i in 0..W * W { px[i * 4..i * 4 + 4].copy_from_slice(&bg); } // Draw a 3px-wide line from (ax,ay) to (bx,by) fn stroke(px: &mut Vec, ax: i32, ay: i32, bx: i32, by: i32, fg: [u8; 4]) { let steps = ((bx - ax).abs().max((by - ay).abs()) * 3).max(1); for s in 0..=steps { let t = s as f32 / steps as f32; let cx = ax as f32 + (bx - ax) as f32 * t; let cy = ay as f32 + (by - ay) as f32 * t; for dy in -1i32..=1 { for dx in -1i32..=1 { let ix = cx.round() as i32 + dx; let iy = cy.round() as i32 + dy; if ix >= 0 && ix < W as i32 && iy >= 0 && iy < W as i32 { let idx = (iy as usize * W + ix as usize) * 4; px[idx..idx + 4].copy_from_slice(&fg); } } } } } // H stroke(&mut px, 4, 5, 4, 26, fg); stroke(&mut px, 13, 5, 13, 26, fg); stroke(&mut px, 4, 15, 13, 15, fg); // 7 stroke(&mut px, 17, 5, 27, 5, fg); stroke(&mut px, 27, 5, 20, 26, fg); stroke(&mut px, 20, 16, 26, 16, fg); px } // ── Standalone helpers ──────────────────────────────────────────────────── fn entity_type_label(entity: &acadrust::EntityType) -> String { use acadrust::EntityType::*; match entity { Line(_) => "Line", Circle(_) => "Circle", Arc(_) => "Arc", Ellipse(_) => "Ellipse", Spline(_) => "Spline", LwPolyline(_) => "Polyline", Text(_) => "Text", MText(_) => "MText", Dimension(_) => "Dimension", Insert(_) => "Block Reference", Point(_) => "Point", Hatch(_) => "Hatch", _ => "Entity", } .to_string() } fn entity_type_key(entity: &acadrust::EntityType) -> String { match entity { acadrust::EntityType::LwPolyline(_) => "pline", acadrust::EntityType::Circle(_) => "circle", acadrust::EntityType::Line(_) => "line", acadrust::EntityType::Arc(_) => "arc", acadrust::EntityType::Ellipse(_) => "ellipse", acadrust::EntityType::Spline(_) => "spline", acadrust::EntityType::Text(_) => "text", acadrust::EntityType::MText(_) => "mtext", acadrust::EntityType::Dimension(_) => "dimension", acadrust::EntityType::Insert(_) => "insert", acadrust::EntityType::Point(_) => "point", acadrust::EntityType::Hatch(_) => "hatch", _ => "entity", } .to_string() } fn build_selection_groups( selected: &[(Handle, &EntityType)], ) -> Vec { let mut groups = vec![ui::properties::SelectionGroup { label: format!("All({})", selected.len()), handles: selected.iter().map(|(handle, _)| *handle).collect(), }]; let mut by_type: std::collections::BTreeMap> = std::collections::BTreeMap::new(); for (handle, entity) in selected { by_type.entry(entity_type_key(entity)).or_default().push(*handle); } for (kind, handles) in by_type { groups.push(ui::properties::SelectionGroup { label: format!("{}({})", title_case_word(&kind), handles.len()), handles, }); } groups } fn title_case_word(value: &str) -> String { let mut chars = value.chars(); match chars.next() { Some(first) => { let mut out = first.to_uppercase().collect::(); out.push_str(chars.as_str()); out } None => String::new(), } } fn aggregate_sections( selected: &[(Handle, &EntityType)], text_style_names: &[String], ) -> Vec { if selected.is_empty() { return vec![]; } let mut all_sections: Vec> = selected .iter() .map(|(handle, entity)| dispatch::properties_sectioned(*handle, entity, text_style_names)) .collect(); let mut result = all_sections.remove(0); for sections in all_sections { result = merge_sections(&result, §ions); } result } fn merge_sections( left: &[crate::scene::object::PropSection], right: &[crate::scene::object::PropSection], ) -> Vec { left.iter() .filter_map(|section| { let rhs = right.iter().find(|candidate| candidate.title == section.title)?; let props: Vec = section .props .iter() .filter_map(|prop| { let other = rhs.props.iter().find(|candidate| candidate.field == prop.field)?; Some(crate::scene::object::Property { label: prop.label.clone(), field: prop.field, value: merge_prop_value(&prop.value, &other.value), }) }) .collect(); if props.is_empty() { None } else { Some(crate::scene::object::PropSection { title: section.title.clone(), props, }) } }) .collect() } fn merge_prop_value( left: &crate::scene::object::PropValue, right: &crate::scene::object::PropValue, ) -> crate::scene::object::PropValue { use crate::scene::object::PropValue; if left == right { return left.clone(); } match (left, right) { (PropValue::LayerChoice(_), PropValue::LayerChoice(_)) => { PropValue::LayerChoice(VARIES_LABEL.into()) } (PropValue::ColorChoice(_), PropValue::ColorChoice(_)) | (PropValue::ColorVaries, _) | (_, PropValue::ColorVaries) => PropValue::ColorVaries, (PropValue::LwChoice(_), PropValue::LwChoice(_)) | (PropValue::LwVaries, _) | (_, PropValue::LwVaries) => PropValue::LwVaries, (PropValue::LinetypeChoice(_), PropValue::LinetypeChoice(_)) => { PropValue::LinetypeChoice(VARIES_LABEL.into()) } ( PropValue::Choice { options, .. }, PropValue::Choice { options: other_options, .. }, ) if options == other_options => PropValue::Choice { selected: VARIES_LABEL.into(), options: options.clone(), }, (PropValue::EditText(_), PropValue::EditText(_)) => { PropValue::EditText(VARIES_LABEL.into()) } (PropValue::ReadOnly(_), PropValue::ReadOnly(_)) => { PropValue::ReadOnly(VARIES_LABEL.into()) } (PropValue::HatchPatternChoice(_), PropValue::HatchPatternChoice(_)) => { PropValue::HatchPatternChoice(VARIES_LABEL.into()) } ( PropValue::BoolToggle { field, .. }, PropValue::BoolToggle { field: other_field, .. }, ) if field == other_field => PropValue::ReadOnly(VARIES_LABEL.into()), _ => left.clone(), } }