use super::{H7CAD, Message, POLY_START_DELAY_MS};
use super::helpers::{parse_coord, angle_close, ortho_constrain, polar_constrain, ucs_to_wcs, ucs_z_axis};
use crate::scene::{self, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
use crate::scene::grip::{find_hit_grip, GripEdit};
use crate::scene::object::GripApply;
use crate::modules::ModuleEvent;
use crate::ui::PropertiesPanel;
use acadrust::types::Color as AcadColor;
use acadrust::{EntityType as AcadEntityType, Handle};
use iced::time::Instant;
use iced::window;
use iced::{mouse, Task};

const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;

impl H7CAD {
    pub fn update(&mut self, msg: Message) -> Task<Message> {
        match msg {
            Message::Tick(t) => {
                self.tabs[self.active_tab].scene.update(t - self.start);
                Task::none()
            }

            Message::OpenFile => Task::perform(crate::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());

                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 = super::document::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.populate_images_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();
                crate::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 = super::document::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::ImagePick => {
                Task::perform(crate::io::pick_image_file(), Message::ImagePickResult)
            }

            Message::ImagePickResult(Ok((path, pw, ph))) => {
                use crate::command::CadCommand;
                use crate::modules::home::draw::raster_image::ImageCommand;
                let path_str = path.to_string_lossy().into_owned();
                let short = std::path::Path::new(&path_str)
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or(&path_str)
                    .to_string();
                self.command_line
                    .push_output(&format!("IMAGE  \"{short}\": {pw}×{ph} px"));
                let cmd = ImageCommand::new(path_str, pw, ph);
                let i = self.active_tab;
                self.command_line.push_info(&cmd.prompt());
                self.tabs[i].active_cmd = Some(Box::new(cmd));
                Task::none()
            }

            Message::ImagePickResult(Err(e)) => {
                if e != "Cancelled" {
                    self.command_line.push_error(&format!("IMAGE: {e}"));
                }
                Task::none()
            }

            Message::SaveFile => {
                let i = self.active_tab;
                if let Some(path) = &self.tabs[i].current_path {
                    let path = path.clone();
                    match crate::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(crate::io::pick_save_path(), Message::PickedSavePath);
                }
                Task::none()
            }

            Message::SaveAs => Task::perform(crate::io::pick_save_path(), Message::PickedSavePath),

            Message::PickedSavePath(Some(path)) => {
                let i = self.active_tab;
                match crate::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();
                crate::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 = super::document::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 {
                    self.tab_counter += 1;
                    self.tabs[0] = super::document::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();

                    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);
                        }
                        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();
                    }

                    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();
                    }

                    if let Some(ucs_pt) = parse_coord(&text) {
                        // Typed coordinates are in active UCS space; convert to WCS.
                        let wcs_pt = if let Some(ref ucs) = self.tabs[i].active_ucs {
                            ucs_to_wcs(ucs_pt, ucs)
                        } else {
                            ucs_pt
                        };
                        let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_point(wcs_pt));
                        if let Some(r) = result {
                            return self.apply_cmd_result(r);
                        }
                        return Task::none();
                    }

                    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);
                    }

                    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 => {
                // Cancel layout rename / context menu first, then fall through.
                if self.layout_rename_state.take().is_some() || self.layout_context_menu.take().is_some() {
                    return Task::none();
                }
                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 if self.tabs[i].scene.active_viewport.is_some() {
                    // ESC while in MSPACE → exit back to paper space.
                    return Task::done(Message::ExitViewport);
                } else {
                    self.tabs[i].scene.deselect_all();
                    self.refresh_properties();
                    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::close(id)
                } else {
                    self.sync_ribbon_layers();
                    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::OsWindowClosed(id) => {
                if self.main_window == Some(id) {
                    return iced::exit();
                }
                if self.layer_window == Some(id) {
                    self.layer_window = None;
                }
                Task::none()
            }

            // ── Layer panel messages ───────────────────────────────────────
            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;
                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);
                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;
                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");
                        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);
                        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::<i32>() {
                        pl.transparency = v.clamp(0, 90);
                    } else if s.is_empty() {
                        pl.transparency = 0;
                    }
                }
                Task::none()
            }

            // ── Cursor / viewport messages ─────────────────────────────────
            Message::CursorMoved(p) => {
                let (vw, _vh) = self.tabs[self.active_tab].scene.selection.borrow().vp_size;
                self.cursor_pos = iced::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);
                    }
                }

                let (mid_down, mid_last, vp_size) =
                    (sel.middle_down, sel.middle_last_pos, sel.vp_size);
                if mid_down {
                    if let Some(last) = mid_last {
                        let (dx, dy) = (p.x - last.x, p.y - last.y);
                        let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vp_size.0, height: vp_size.1 };
                        // Drop `sel` before calling mutable scene methods.
                        drop(sel);
                        if self.tabs[i].scene.active_viewport.is_some() {
                            self.tabs[i].scene.pan_active_viewport(dx, dy, bounds);
                        } else {
                            self.tabs[i].scene.camera.borrow_mut().pan(dx, dy);
                        }
                        self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
                        return Task::none();
                    }
                    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_paper = cam.pick_on_target_plane(p, bounds);
                    let vp_mat = cam.view_proj(bounds);
                    drop(cam);
                    let raw = self.tabs[i].scene.paper_to_model(raw_paper);

                    let edited_name = grip.handle.value().to_string();
                    let all_wires = self.tabs[i].scene.hit_test_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;

                    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 cursor_paper = if let Some(ref ucs) = self.tabs[i].active_ucs {
                        let origin = glam::Vec3::new(
                            ucs.origin.x as f32, ucs.origin.y as f32, ucs.origin.z as f32,
                        );
                        let normal = ucs_z_axis(ucs);
                        self.tabs[i].scene.camera.borrow()
                            .pick_on_plane(p, bounds, normal, origin)
                    } else {
                        self.tabs[i].scene.camera.borrow()
                            .pick_on_target_plane(p, bounds)
                    };
                    let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
                    // In MSPACE, map paper-space cursor to model space so that
                    // command previews and snapping work in the correct coordinate space.
                    let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);

                    let all_wires = self.tabs[i].scene.hit_test_wires();
                    let needs_entity = self.tabs[i]
                        .active_cmd.as_ref().map(|c| c.needs_entity_pick()).unwrap_or(false);
                    let is_gathering = self.tabs[i]
                        .active_cmd.as_ref().map(|c| c.is_selection_gathering()).unwrap_or(false);
                    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_entity || is_gathering {
                        None
                    } else 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 = {
                        // snap.world is paper-space for viewport-projected wires; convert
                        // to model-space so previews use consistent coordinates.
                        let mut pt = self.tabs[i].snap_result
                            .map(|s| self.tabs[i].scene.paper_to_model(s.world))
                            .unwrap_or(cursor_world);
                        // Clamp to world XY only when no UCS is active; with a UCS the
                        // point already lies on the UCS XY plane.
                        if self.tabs[i].active_cmd.is_some() && self.tabs[i].active_ucs.is_none() {
                            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 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 };

                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();
                    }
                }

                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();
                        }
                    }
                }

                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;
                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)
                };

                if self.tabs[i].active_grip.is_some() {
                    self.tabs[i].active_grip = None;
                    self.refresh_properties();
                    return Task::none();
                }

                let is_gathering = self.tabs[i]
                    .active_cmd.as_ref().map(|c| c.is_selection_gathering()).unwrap_or(false);

                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 };

                    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 = {
                        // Project screen point onto the active UCS XY plane (or world XY when
                        // no UCS is active).
                        let raw_paper = if let Some(ref ucs) = self.tabs[i].active_ucs {
                            let origin = glam::Vec3::new(
                                ucs.origin.x as f32, ucs.origin.y as f32, ucs.origin.z as f32,
                            );
                            let normal = ucs_z_axis(ucs);
                            self.tabs[i].scene.camera.borrow()
                                .pick_on_plane(p, bounds, normal, origin)
                        } else {
                            self.tabs[i].scene.camera.borrow()
                                .pick_on_target_plane(p, bounds)
                        };
                        // Convert paper-space → model-space when inside a viewport.
                        let raw = self.tabs[i].scene.paper_to_model(raw_paper);
                        let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
                        let all_wires = self.tabs[i].scene.hit_test_wires();
                        let needs_tan = self.tabs[i].active_cmd.as_ref()
                            .map(|c| c.needs_tangent_pick()).unwrap_or(false);
                        let needs_entity_click = self.tabs[i].active_cmd.as_ref()
                            .map(|c| c.needs_entity_pick()).unwrap_or(false);
                        let snap_hit = if needs_entity_click {
                            None
                        } else 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)
                        };
                        // snap.world is in paper-space (projected wire coords in MSPACE);
                        // convert to model-space so commands receive consistent coordinates.
                        let mut pt = snap_hit
                            .map(|s| self.tabs[i].scene.paper_to_model(s.world))
                            .unwrap_or(raw);
                        // When no UCS is active clamp to world XY; with a UCS the point is
                        // already constrained to that plane by the ray–plane intersection.
                        if self.tabs[i].active_ucs.is_none() {
                            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.hit_test_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)
                    {
                        if let Some(obj) = tangent_obj_at_click {
                            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)
                };

                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 {
                        if elapsed_ms < POLY_START_DELAY_MS {
                            if let Some(a) = box_anchor {
                                let crossing = box_crossing;
                                let all_wires = self.tabs[i].scene.hit_test_wires();
                                let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
                                let mut handles: Vec<Handle> = 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 {
                            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.hit_test_wires();
                            let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
                            let mut handles: Vec<Handle> = 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 {
                        if box_anchor.is_none() {
                            let all_wires = self.tabs[i].scene.hit_test_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();
                                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 {
                            let a = box_anchor.unwrap();
                            let crossing = box_crossing;
                            let all_wires = self.tabs[i].scene.hit_test_wires();
                            let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
                            let mut handles: Vec<Handle> = 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;
                }

                if is_gathering && selection_just_completed {
                    let handles: Vec<Handle> = 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);
                    }
                }

                // ── Double-click: enter/exit MSPACE ───────────────────────
                // Only when no command is running, no drag, and we're in paper space.
                if is_click
                    && is_down   // ensures there was a matching left-press
                    && self.tabs[i].active_cmd.is_none()
                    && self.tabs[i].scene.current_layout != "Model"
                {
                    let now = Instant::now();
                    let is_double = self
                        .last_vp_click_time
                        .map(|t| {
                            let dt = now.duration_since(t).as_millis();
                            let last = self.last_vp_click_pos.unwrap_or(p);
                            let d = (p.x - last.x).hypot(p.y - last.y);
                            dt < 400 && d < 8.0
                        })
                        .unwrap_or(false);

                    self.last_vp_click_time = Some(now);
                    self.last_vp_click_pos = Some(p);

                    if is_double {
                        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 };

                        // 1) Try direct wire hit — works when the border is clicked.
                        let hit_vp: Option<acadrust::Handle> = {
                            let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
                            let all_wires = self.tabs[i].scene.hit_test_wires();
                            scene::hit_test::click_hit(p, &all_wires, vp_mat, bounds)
                                .and_then(|s| Scene::handle_from_wire_name(s))
                                .and_then(|h| {
                                    if let Some(AcadEntityType::Viewport(vp)) =
                                        self.tabs[i].scene.document.get_entity(h)
                                    {
                                        if vp.id > 1 { Some(h) } else { None }
                                    } else {
                                        None
                                    }
                                })
                        };

                        // 2) Geometric fallback: check if the cursor is inside any
                        //    viewport's bounding rectangle in paper space.  This handles
                        //    double-clicks on model-entity content wires or empty areas.
                        let hit_vp = hit_vp.or_else(|| {
                            let paper_pt = self.tabs[i]
                                .scene
                                .camera
                                .borrow()
                                .pick_on_target_plane(p, bounds);
                            self.tabs[i]
                                .scene
                                .viewport_at_paper_point(paper_pt.x, paper_pt.y)
                        });

                        if let Some(handle) = hit_vp {
                            return Task::done(Message::EnterViewport(handle));
                        } else if self.tabs[i].scene.active_viewport.is_some() {
                            // Double-clicked outside all viewports while in MSPACE → exit.
                            return Task::done(Message::ExitViewport);
                        }
                    }
                }

                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 = 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 };
                if self.tabs[i].scene.active_viewport.is_some() {
                    // In MSPACE: zoom the active viewport's model-space view,
                    // keeping the model point under the cursor stationary.
                    let cursor_paper = cursor.map(|cp| {
                        let pt = self.tabs[i].scene.camera.borrow()
                            .pick_on_target_plane(cp, bounds);
                        glam::Vec2::new(pt.x, pt.y)
                    });
                    self.tabs[i].scene.zoom_active_viewport(s, cursor_paper);
                } else {
                    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()
            }

            // ── Snap / mode toggles ───────────────────────────────────────
            Message::ToggleSnapEnabled => { self.snapper.toggle_global(); Task::none() }
            Message::ToggleGridSnap => { self.snapper.toggle(crate::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() }

            // ── Ribbon dropdowns ──────────────────────────────────────────
            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) {
                            crate::scene::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) {
                            crate::scene::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) {
                            crate::scene::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) {
                            crate::scene::dispatch::apply_common_prop(entity, "linetype", &lt);
                        }
                    }
                    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" => crate::scene::dispatch::toggle_invisible(entity),
                                _ => crate::scene::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");
                    if field == "vp_ucs_name" {
                        // Resolve UCS name → cloned data, then mutate viewports.
                        let ucs_data = self.tabs[i].scene.document.ucss
                            .iter()
                            .find(|u| u.name == value)
                            .cloned();
                        if let Some(ucs) = ucs_data {
                            for handle in &handles {
                                if let Some(acadrust::EntityType::Viewport(vp)) =
                                    self.tabs[i].scene.document.get_entity_mut(*handle)
                                {
                                    vp.ucs_handle = ucs.handle;
                                    vp.ucs_origin = ucs.origin.clone();
                                    vp.ucs_x_axis = ucs.x_axis.clone();
                                    vp.ucs_y_axis = ucs.y_axis.clone();
                                }
                            }
                        }
                    } else if field == "vp_named_view" {
                        // Assign a named view to viewport(s): copy camera parameters.
                        let view_data = self.tabs[i].scene.document.views
                            .iter()
                            .find(|v| v.name == value)
                            .cloned();
                        if let Some(view) = view_data {
                            for handle in &handles {
                                if let Some(acadrust::EntityType::Viewport(vp)) =
                                    self.tabs[i].scene.document.get_entity_mut(*handle)
                                {
                                    vp.view_target = view.target.clone();
                                    vp.view_direction = view.direction.clone();
                                    if view.height > 0.0 {
                                        vp.view_height = view.height;
                                    }
                                }
                            }
                            self.tabs[i].scene.camera_generation += 1;
                        }
                    } else {
                        for handle in handles {
                            if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
                                crate::scene::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");
                        if field == "frozen_layers" {
                            // Resolve layer names → handles, then apply to viewports.
                            let layer_handles: Vec<acadrust::Handle> = val
                                .split(',')
                                .map(|s| s.trim())
                                .filter(|s| !s.is_empty())
                                .filter_map(|name| {
                                    self.tabs[i].scene.document.layers.iter()
                                        .find(|l| l.name.eq_ignore_ascii_case(name))
                                        .map(|l| l.handle)
                                })
                                .collect();
                            for handle in handles {
                                if let Some(acadrust::EntityType::Viewport(vp)) =
                                    self.tabs[i].scene.document.get_entity_mut(handle)
                                {
                                    vp.frozen_layers = layer_handles.clone();
                                }
                            }
                        } else {
                            for handle in handles {
                                if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
                                    match field {
                                        "linetype_scale" | "transparency" => {
                                            crate::scene::dispatch::apply_common_prop(entity, field, &val);
                                        }
                                        _ => {
                                            crate::scene::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";
                // Cancel any pending rename/context-menu and active viewport when switching.
                self.layout_rename_state = None;
                self.layout_context_menu = None;
                self.tabs[i].scene.active_viewport = None;
                self.tabs[i].scene.current_layout = name;
                self.tabs[i].scene.deselect_all();
                self.tabs[i].scene.fit_all();
                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;
                // Find a unique name (e.g. Layout2, Layout3, ...).
                let existing = self.tabs[i].scene.layout_names();
                let mut idx = existing.len();
                let new_name = loop {
                    let candidate = format!("Layout{}", idx);
                    if !existing.contains(&candidate) {
                        break candidate;
                    }
                    idx += 1;
                };
                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::LayoutDelete(name) => {
                let i = self.active_tab;
                self.push_undo_snapshot(i, "LAYOUT DEL");
                if self.tabs[i].scene.delete_layout(&name) {
                    self.layout_context_menu = None;
                    self.layout_rename_state = None;
                    // If we fell back to Model space, update ribbon.
                    if self.tabs[i].scene.current_layout == "Model"
                        && self.ribbon.active_is_layout()
                    {
                        self.ribbon.select(0);
                    }
                    self.command_line.push_output(&format!("Layout \"{name}\" silindi"));
                    self.tabs[i].dirty = true;
                }
                Task::none()
            }

            Message::LayoutRenameStart(name) => {
                if name != "Model" {
                    self.layout_rename_state = Some((name.clone(), name));
                    self.layout_context_menu = None;
                }
                Task::none()
            }

            Message::LayoutRenameEdit(val) => {
                if let Some((orig, _)) = &self.layout_rename_state {
                    let orig = orig.clone();
                    self.layout_rename_state = Some((orig, val));
                }
                Task::none()
            }

            Message::LayoutRenameCommit => {
                if let Some((orig, new_name)) = self.layout_rename_state.take() {
                    let new_name = new_name.trim().to_string();
                    if !new_name.is_empty() && new_name != orig {
                        let i = self.active_tab;
                        let exists = self.tabs[i]
                            .scene
                            .layout_names()
                            .iter()
                            .any(|n| *n == new_name);
                        if exists {
                            self.command_line.push_error(&format!(
                                "\"{}\" adı zaten kullanımda",
                                new_name
                            ));
                        } else {
                            self.push_undo_snapshot(i, "LAYOUT RENAME");
                            self.tabs[i].scene.rename_layout(&orig, &new_name);
                            if self.tabs[i].scene.current_layout == orig {
                                self.tabs[i].scene.current_layout = new_name.clone();
                            }
                            self.tabs[i].dirty = true;
                            self.command_line
                                .push_output(&format!("Layout \"{orig}\" → \"{new_name}\""));
                        }
                    }
                }
                Task::none()
            }

            Message::LayoutRenameCancel => {
                self.layout_rename_state = None;
                Task::none()
            }

            Message::LayoutContextMenu(name) => {
                if name != "Model" {
                    self.layout_context_menu = Some(name);
                }
                Task::none()
            }

            Message::LayoutContextMenuClose => {
                self.layout_context_menu = None;
                Task::none()
            }

            Message::EnterViewport(handle) => {
                let i = self.active_tab;
                // Clear paper-space selection before entering model space.
                self.tabs[i].scene.deselect_all();
                self.tabs[i].scene.active_viewport = Some(handle);
                self.refresh_properties();
                self.command_line.push_output("MSPACE");
                Task::none()
            }

            Message::ExitViewport => {
                let i = self.active_tab;
                // Clear model-space selection before returning to paper space.
                self.tabs[i].scene.deselect_all();
                self.refresh_properties();
                self.tabs[i].scene.active_viewport = None;
                self.command_line.push_output("PSPACE");
                Task::none()
            }

            Message::MspaceCommand => {
                let i = self.active_tab;
                if self.tabs[i].scene.current_layout == "Model" {
                    self.command_line.push_error("MS is only available in paper space layouts.");
                    return Task::none();
                }
                if self.tabs[i].scene.active_viewport.is_some() {
                    // Already in MSPACE — nothing to do.
                    return Task::none();
                }
                match self.tabs[i].scene.first_user_viewport() {
                    Some(handle) => Task::done(Message::EnterViewport(handle)),
                    None => {
                        self.command_line.push_error("No viewport found in this layout.");
                        Task::none()
                    }
                }
            }

            Message::PspaceCommand => {
                Task::done(Message::ExitViewport)
            }

            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(),

            // ── Page Setup ────────────────────────────────────────────────
            Message::PageSetupOpen => {
                let i = self.active_tab;
                // Populate edit buffers from current paper limits.
                let (w, h) = if let Some(((_, _), (x1, y1))) = self.tabs[i].scene.paper_limits() {
                    (x1, y1)
                } else {
                    (297.0, 210.0) // A4 default
                };
                self.page_setup_w = format!("{w:.1}");
                self.page_setup_h = format!("{h:.1}");
                self.page_setup_open = true;
                Task::none()
            }
            Message::PageSetupClose => {
                self.page_setup_open = false;
                Task::none()
            }
            Message::PageSetupWidthEdit(s) => {
                self.page_setup_w = s;
                Task::none()
            }
            Message::PageSetupHeightEdit(s) => {
                self.page_setup_h = s;
                Task::none()
            }
            Message::PageSetupPreset(name) => {
                // Paper size presets defined in view.rs — mirror them here.
                let sizes: &[(&str, f64, f64)] = &[
                    ("A4 Portrait",      210.0, 297.0),
                    ("A4 Landscape",     297.0, 210.0),
                    ("A3 Portrait",      297.0, 420.0),
                    ("A3 Landscape",     420.0, 297.0),
                    ("A2 Portrait",      420.0, 594.0),
                    ("A2 Landscape",     594.0, 420.0),
                    ("A1 Portrait",      594.0, 841.0),
                    ("A1 Landscape",     841.0, 594.0),
                    ("A0 Portrait",      841.0, 1189.0),
                    ("A0 Landscape",    1189.0,  841.0),
                    ("Letter Portrait",  215.9, 279.4),
                    ("Letter Landscape", 279.4, 215.9),
                ];
                if let Some(&(_, w, h)) = sizes.iter().find(|(n, _, _)| *n == name) {
                    self.page_setup_w = format!("{w:.1}");
                    self.page_setup_h = format!("{h:.1}");
                }
                Task::none()
            }
            Message::PageSetupPlotArea(s) => {
                self.page_setup_plot_area = s;
                Task::none()
            }
            Message::PageSetupCenterToggle => {
                self.page_setup_center = !self.page_setup_center;
                Task::none()
            }
            Message::PageSetupOffsetXEdit(s) => {
                self.page_setup_offset_x = s;
                Task::none()
            }
            Message::PageSetupOffsetYEdit(s) => {
                self.page_setup_offset_y = s;
                Task::none()
            }
            Message::PageSetupRotation(s) => {
                self.page_setup_rotation = s;
                Task::none()
            }
            Message::PageSetupScale(s) => {
                self.page_setup_scale = s;
                Task::none()
            }
            Message::PageSetupCommit => {
                let i = self.active_tab;
                let layout_name = self.tabs[i].scene.current_layout.clone();
                if layout_name != "Model" {
                    let w: f64 = self.page_setup_w.parse::<f64>().unwrap_or(297.0).max(1.0);
                    let h: f64 = self.page_setup_h.parse::<f64>().unwrap_or(210.0).max(1.0);
                    let plot_area = self.page_setup_plot_area.clone();
                    let center    = self.page_setup_center;
                    let offset_x  = self.page_setup_offset_x.parse::<f64>().unwrap_or(0.0);
                    let offset_y  = self.page_setup_offset_y.parse::<f64>().unwrap_or(0.0);
                    let rotation: i16 = self.page_setup_rotation.parse().unwrap_or(0);
                    let scale_str = self.page_setup_scale.clone();

                    // Update the Layout object's limits.
                    for obj in self.tabs[i].scene.document.objects.values_mut() {
                        if let acadrust::objects::ObjectType::Layout(l) = obj {
                            if l.name == layout_name {
                                l.min_limits = (0.0, 0.0);
                                l.max_limits = (w, h);
                                l.min_extents = (0.0, 0.0, 0.0);
                                l.max_extents = (w, h, 0.0);
                                break;
                            }
                        }
                    }

                    // Find or create the PlotSettings object for this layout.
                    use acadrust::objects::{ObjectType, PlotSettings, PlotType, PlotRotation, PlotPaperUnits};
                    let plot_handle = self.tabs[i].scene.document.objects.iter()
                        .find_map(|(h, obj)| {
                            if let ObjectType::PlotSettings(ps) = obj {
                                if ps.page_name == layout_name { Some(*h) } else { None }
                            } else { None }
                        });

                    let ps_entry = if let Some(h) = plot_handle {
                        self.tabs[i].scene.document.objects.get_mut(&h)
                    } else {
                        // Create a new PlotSettings object and insert it.
                        let mut ps = PlotSettings::new(layout_name.clone());
                        ps.handle = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
                        let h = ps.handle;
                        self.tabs[i].scene.document.objects.insert(h, ObjectType::PlotSettings(ps));
                        self.tabs[i].scene.document.objects.get_mut(&h)
                    };

                    if let Some(ObjectType::PlotSettings(ps)) = ps_entry {
                        ps.paper_width  = w;
                        ps.paper_height = h;
                        ps.paper_units  = PlotPaperUnits::Millimeters;
                        ps.plot_type    = if plot_area == "Extents" {
                            PlotType::Extents
                        } else {
                            PlotType::Layout
                        };
                        ps.flags.plot_centered = center;
                        ps.origin_x = offset_x;
                        ps.origin_y = offset_y;
                        ps.rotation = match rotation {
                            90  => PlotRotation::Degrees90,
                            180 => PlotRotation::Degrees180,
                            270 => PlotRotation::Degrees270,
                            _   => PlotRotation::None,
                        };
                        // Apply plot scale.
                        use acadrust::objects::ScaledType;
                        let (num, den) = parse_plot_scale(&scale_str);
                        if scale_str == "Fit" {
                            ps.set_scale_to_fit();
                        } else {
                            ps.scale_type = ScaledType::CustomScale;
                            ps.scale_numerator = num;
                            ps.scale_denominator = den;
                        }
                    }

                    self.tabs[i].dirty = true;
                    self.command_line.push_info(&format!(
                        "Page setup: {w:.1}×{h:.1} mm  area={plot_area}  \
                         center={center}  rot={rotation}°"
                    ));
                }
                self.page_setup_open = false;
                Task::none()
            }

            // ── Plot / Export ─────────────────────────────────────────────
            Message::PlotExport => {
                let i = self.active_tab;
                let stem = self.tabs[i]
                    .current_path
                    .as_deref()
                    .and_then(|p: &std::path::Path| p.file_stem())
                    .map(|s: &std::ffi::OsStr| s.to_string_lossy().into_owned())
                    .unwrap_or_else(|| "drawing".into());
                Task::perform(
                    crate::io::pdf_export::pick_pdf_path_owned(stem),
                    Message::PlotExportPath,
                )
            }
            Message::PlotExportPath(None) => Task::none(),
            Message::PlotExportPath(Some(path)) => {
                let i = self.active_tab;
                let scene = &self.tabs[i].scene;
                let layout_name = scene.current_layout.clone();
                let wires = scene.entity_wires();

                // Read PlotSettings for current layout (if available).
                use acadrust::objects::{ObjectType, PlotType};
                let ps_snap = scene.document.objects.values().find_map(|obj| {
                    if let ObjectType::PlotSettings(ps) = obj {
                        if ps.page_name == layout_name { Some(ps.clone()) } else { None }
                    } else { None }
                });

                // Determine paper size and drawing offset.
                let (paper_w, paper_h, mut draw_ox, mut draw_oy, rotation_deg) =
                    if let Some(((x0, y0), (x1, y1))) = scene.paper_limits() {
                        let (pw, ph) = (x1 - x0, y1 - y0);

                        // If PlotSettings says Extents, use model space extents instead.
                        let use_extents = ps_snap.as_ref()
                            .map(|ps| matches!(ps.plot_type, PlotType::Extents))
                            .unwrap_or(false);

                        let (ox, oy) = if use_extents {
                            if let Some((mn, _mx)) = scene.model_space_extents() {
                                (-mn.x as f64, -mn.y as f64)
                            } else {
                                (-x0, -y0)
                            }
                        } else {
                            (-x0, -y0)
                        };

                        let rot = ps_snap.as_ref()
                            .map(|ps| ps.rotation.to_degrees() as i32)
                            .unwrap_or(0);

                        (pw, ph, ox, oy, rot)
                    } else {
                        // Model space: fit with 5% margin.
                        let margin = 1.05_f64;
                        if let Some((mn, mx)) = scene.model_space_extents() {
                            let w = ((mx.x - mn.x) as f64 * margin).max(1.0);
                            let h = ((mx.y - mn.y) as f64 * margin).max(1.0);
                            let pad_x = (w - (mx.x - mn.x) as f64) * 0.5;
                            let pad_y = (h - (mx.y - mn.y) as f64) * 0.5;
                            (w, h, -(mn.x as f64) + pad_x, -(mn.y as f64) + pad_y, 0)
                        } else {
                            (297.0, 210.0, 0.0, 0.0, 0)
                        }
                    };

                // Apply PlotSettings offset and centering.
                if let Some(ref ps) = ps_snap {
                    if ps.flags.plot_centered {
                        // Centering: compute wire extents and re-centre.
                        let all_x: Vec<f32> = wires.iter()
                            .flat_map(|w| w.points.iter().map(|p| p[0]))
                            .filter(|v| !v.is_nan())
                            .collect();
                        let all_y: Vec<f32> = wires.iter()
                            .flat_map(|w| w.points.iter().map(|p| p[1]))
                            .filter(|v| !v.is_nan())
                            .collect();
                        if let (Some(&min_x), Some(&max_x), Some(&min_y), Some(&max_y)) = (
                            all_x.iter().copied().reduce(f32::min).as_ref(),
                            all_x.iter().copied().reduce(f32::max).as_ref(),
                            all_y.iter().copied().reduce(f32::min).as_ref(),
                            all_y.iter().copied().reduce(f32::max).as_ref(),
                        ) {
                            let cx = (min_x + max_x) as f64 / 2.0;
                            let cy = (min_y + max_y) as f64 / 2.0;
                            draw_ox += paper_w / 2.0 - cx;
                            draw_oy += paper_h / 2.0 - cy;
                        }
                    } else {
                        draw_ox += ps.origin_x;
                        draw_oy += ps.origin_y;
                    }
                }

                // For rotation: swap paper dimensions and note angle for export.
                let (eff_w, eff_h) = match rotation_deg {
                    90 | 270 => (paper_h, paper_w),
                    _        => (paper_w, paper_h),
                };

                match crate::io::pdf_export::export_pdf(
                    &wires, eff_w, eff_h,
                    draw_ox as f32, draw_oy as f32,
                    rotation_deg, &path,
                    self.active_plot_style.as_ref(),
                ) {
                    Ok(()) => self.command_line.push_info(&format!(
                        "Exported: {}",
                        path.file_name().unwrap_or_default().to_string_lossy()
                    )),
                    Err(e) => self.command_line.push_error(&format!("Export failed: {e}")),
                }
                Task::none()
            }

            // ── Plot Style Table ──────────────────────────────────────────────
            Message::PlotStyleLoad => {
                Task::perform(
                    crate::io::pick_plot_style(),
                    Message::PlotStyleLoaded,
                )
            }
            Message::PlotStyleLoaded(Some(table)) => {
                self.command_line.push_output(&format!(
                    "Plot style '{}' loaded ({} color entries).",
                    table.name,
                    table.aci_entries.iter().filter(|e| e.color.is_some()).count()
                ));
                self.active_plot_style = Some(table);
                Task::none()
            }
            Message::PlotStyleLoaded(None) => Task::none(),
            Message::PlotStyleClear => {
                self.active_plot_style = None;
                self.command_line.push_output("Plot style table cleared.");
                Task::none()
            }

            // ── TextStyle Font Browser ────────────────────────────────────────
            Message::TextStyleDialogOpen => {
                let i = self.active_tab;
                let cur = self.tabs[i].scene.document.header.current_text_style_name.clone();
                let exists = self.tabs[i].scene.document.text_styles.get(&cur).is_some();
                self.textstyle_selected = if exists {
                    cur
                } else {
                    self.tabs[i].scene.document.text_styles
                        .iter().next().map(|s| s.name.clone())
                        .unwrap_or_else(|| "Standard".to_string())
                };
                self.load_textstyle_bufs(i);
                self.textstyle_open = true;
                Task::none()
            }
            Message::TextStyleDialogClose => {
                self.textstyle_open = false;
                Task::none()
            }
            Message::TextStyleDialogSelect(name) => {
                let i = self.active_tab;
                self.textstyle_selected = name;
                self.load_textstyle_bufs(i);
                Task::none()
            }
            Message::TextStyleDialogSetCurrent => {
                let i = self.active_tab;
                let name = self.textstyle_selected.clone();
                if self.tabs[i].scene.document.text_styles.get(&name).is_some() {
                    self.push_undo_snapshot(i, "STYLE SET");
                    self.tabs[i].scene.document.header.current_text_style_name = name.clone();
                    self.tabs[i].dirty = true;
                    self.command_line.push_output(&format!("Current text style: {}", name));
                }
                Task::none()
            }
            Message::TextStyleDialogNew => {
                let i = self.active_tab;
                let doc = &self.tabs[i].scene.document;
                let mut n = 1u32;
                let new_name = loop {
                    let candidate = format!("Style{}", n);
                    if !doc.text_styles.contains(&candidate) { break candidate; }
                    n += 1;
                };
                self.push_undo_snapshot(i, "STYLE NEW");
                let style = acadrust::tables::TextStyle::new(&new_name);
                let _ = self.tabs[i].scene.document.text_styles.add(style);
                self.textstyle_selected = new_name.clone();
                self.textstyle_font = String::new();
                self.textstyle_width = "1.0".to_string();
                self.textstyle_oblique = "0.0".to_string();
                self.tabs[i].dirty = true;
                Task::none()
            }
            Message::TextStyleDialogDelete => {
                let i = self.active_tab;
                let name = self.textstyle_selected.clone();
                if name.eq_ignore_ascii_case("Standard") {
                    self.command_line.push_error("Cannot delete the Standard text style.");
                    return Task::none();
                }
                self.push_undo_snapshot(i, "STYLE DEL");
                self.tabs[i].scene.document.text_styles.remove(&name);
                self.textstyle_selected = self.tabs[i].scene.document.text_styles
                    .iter().next().map(|s| s.name.clone())
                    .unwrap_or_else(|| "Standard".to_string());
                self.load_textstyle_bufs(i);
                self.tabs[i].dirty = true;
                Task::none()
            }
            Message::TextStyleEdit { field, value } => {
                match field {
                    "font" => self.textstyle_font = value,
                    "width" => self.textstyle_width = value,
                    "oblique" => self.textstyle_oblique = value,
                    _ => {}
                }
                Task::none()
            }
            Message::TextStyleApply => {
                let i = self.active_tab;
                let name = self.textstyle_selected.clone();
                if self.tabs[i].scene.document.text_styles.get(&name).is_some() {
                    self.push_undo_snapshot(i, "STYLE EDIT");
                    let font = self.textstyle_font.clone();
                    let width_str = self.textstyle_width.clone();
                    let oblique_str = self.textstyle_oblique.clone();
                    if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) {
                        s.font_file = font;
                        if let Ok(w) = width_str.trim().parse::<f64>() {
                            s.width_factor = w;
                        }
                        if let Ok(a) = oblique_str.trim().parse::<f64>() {
                            s.oblique_angle = a.to_radians();
                        }
                    }
                    self.tabs[i].dirty = true;
                }
                Task::none()
            }
            Message::TextStyleFontPick(font_file) => {
                let i = self.active_tab;
                self.textstyle_font = font_file.clone();
                let name = self.textstyle_selected.clone();
                if self.tabs[i].scene.document.text_styles.get(&name).is_some() {
                    self.push_undo_snapshot(i, "STYLE FONT");
                    if let Some(s) = self.tabs[i].scene.document.text_styles.get_mut(&name) {
                        s.font_file = font_file;
                    }
                    self.tabs[i].dirty = true;
                }
                Task::none()
            }

            // ── TableStyle Dialog ─────────────────────────────────────────────
            Message::TableStyleDialogOpen => {
                use acadrust::objects::ObjectType;
                let i = self.active_tab;
                self.tablestyle_selected = self.tabs[i].scene.document.objects.values()
                    .find_map(|o| if let ObjectType::TableStyle(s) = o { Some(s.name.clone()) } else { None })
                    .unwrap_or_else(|| "Standard".to_string());
                self.tablestyle_open = true;
                Task::none()
            }
            Message::TableStyleDialogClose => {
                self.tablestyle_open = false;
                Task::none()
            }
            Message::TableStyleDialogSelect(name) => {
                self.tablestyle_selected = name;
                Task::none()
            }

            Message::TableStyleDialogNew => {
                use acadrust::objects::ObjectType;
                let i = self.active_tab;
                let doc = &self.tabs[i].scene.document;
                let mut n = 1u32;
                let new_name = loop {
                    let candidate = format!("TS{}", n);
                    let taken = doc.objects.values().any(|o| {
                        matches!(o, ObjectType::TableStyle(s) if s.name.eq_ignore_ascii_case(&candidate))
                    });
                    if !taken { break candidate; }
                    n += 1;
                };
                self.push_undo_snapshot(i, "TABLESTYLE NEW");
                let mut style = acadrust::objects::TableStyle::standard();
                style.name = new_name.clone();
                let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
                style.handle = nh;
                self.tabs[i].scene.document.objects.insert(nh, ObjectType::TableStyle(style));
                self.tablestyle_selected = new_name;
                self.tabs[i].dirty = true;
                Task::none()
            }
            Message::TableStyleDialogDelete => {
                use acadrust::objects::ObjectType;
                let i = self.active_tab;
                let name = self.tablestyle_selected.clone();
                if name.eq_ignore_ascii_case("Standard") {
                    self.command_line.push_error("Cannot delete the Standard style.");
                    return Task::none();
                }
                let handle = self.tabs[i].scene.document.objects.iter()
                    .find_map(|(&h, o)| {
                        if let ObjectType::TableStyle(s) = o {
                            if s.name == name { Some(h) } else { None }
                        } else { None }
                    });
                if let Some(h) = handle {
                    self.push_undo_snapshot(i, "TABLESTYLE DEL");
                    self.tabs[i].scene.document.objects.remove(&h);
                    self.tablestyle_selected = self.tabs[i].scene.document.objects.values()
                        .find_map(|o| if let ObjectType::TableStyle(s) = o { Some(s.name.clone()) } else { None })
                        .unwrap_or_else(|| "Standard".to_string());
                    self.tabs[i].dirty = true;
                }
                Task::none()
            }

            // ── MLineStyle Dialog ─────────────────────────────────────────────
            Message::MlStyleDialogOpen => {
                use acadrust::objects::ObjectType;
                let i = self.active_tab;
                let cur = self.tabs[i].scene.document.header.multiline_style.clone();
                let exists = self.tabs[i].scene.document.objects.values().any(|o| {
                    matches!(o, ObjectType::MLineStyle(s) if s.name == cur)
                });
                self.mlstyle_selected = if exists {
                    cur
                } else {
                    self.tabs[i].scene.document.objects.values()
                        .find_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s.name.clone()) } else { None })
                        .unwrap_or_else(|| "Standard".to_string())
                };
                self.mlstyle_open = true;
                Task::none()
            }
            Message::MlStyleDialogClose => {
                self.mlstyle_open = false;
                Task::none()
            }
            Message::MlStyleDialogSelect(name) => {
                self.mlstyle_selected = name;
                Task::none()
            }
            Message::MlStyleDialogSetCurrent => {
                use acadrust::objects::ObjectType;
                let i = self.active_tab;
                let name = self.mlstyle_selected.clone();
                let exists = self.tabs[i].scene.document.objects.values().any(|o| {
                    matches!(o, ObjectType::MLineStyle(s) if s.name == name)
                });
                if exists {
                    self.push_undo_snapshot(i, "MLSTYLE SET");
                    self.tabs[i].scene.document.header.multiline_style = name.clone();
                    self.tabs[i].dirty = true;
                    self.command_line.push_output(&format!("Current multiline style: {}", name));
                }
                Task::none()
            }
            Message::MlStyleDialogNew => {
                use acadrust::objects::ObjectType;
                let i = self.active_tab;
                // Generate a unique name.
                let doc = &self.tabs[i].scene.document;
                let mut n = 1u32;
                let base = "MLS";
                let new_name = loop {
                    let candidate = format!("{}{}", base, n);
                    let taken = doc.objects.values().any(|o| {
                        matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&candidate))
                    });
                    if !taken { break candidate; }
                    n += 1;
                };
                self.push_undo_snapshot(i, "MLSTYLE NEW");
                let mut style = acadrust::objects::MLineStyle::standard();
                style.name = new_name.clone();
                let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
                style.handle = nh;
                self.tabs[i].scene.document.objects.insert(nh, ObjectType::MLineStyle(style));
                self.mlstyle_selected = new_name;
                self.tabs[i].dirty = true;
                Task::none()
            }
            Message::MlStyleDialogDelete => {
                use acadrust::objects::ObjectType;
                let i = self.active_tab;
                let name = self.mlstyle_selected.clone();
                if name.eq_ignore_ascii_case("Standard") {
                    self.command_line.push_error("Cannot delete the Standard style.");
                    return Task::none();
                }
                let handle = self.tabs[i].scene.document.objects.iter()
                    .find_map(|(&h, o)| {
                        if let ObjectType::MLineStyle(s) = o {
                            if s.name == name { Some(h) } else { None }
                        } else { None }
                    });
                if let Some(h) = handle {
                    self.push_undo_snapshot(i, "MLSTYLE DEL");
                    self.tabs[i].scene.document.objects.remove(&h);
                    // Select first remaining style.
                    self.mlstyle_selected = self.tabs[i].scene.document.objects.values()
                        .find_map(|o| if let ObjectType::MLineStyle(s) = o { Some(s.name.clone()) } else { None })
                        .unwrap_or_else(|| "Standard".to_string());
                    self.tabs[i].dirty = true;
                }
                Task::none()
            }

            // ── DimStyle Dialog ───────────────────────────────────────────────
            Message::DimStyleDialogOpen => {
                let i = self.active_tab;
                // Pick the document's current dim style or "Standard".
                let cur = self.tabs[i].scene.document.header.current_dimstyle_name.clone();
                let selected = if self.tabs[i].scene.document.dim_styles.get(&cur).is_some() {
                    cur
                } else {
                    self.tabs[i].scene.document.dim_styles
                        .iter().next().map(|s| s.name.clone())
                        .unwrap_or_else(|| "Standard".to_string())
                };
                self.dimstyle_selected = selected.clone();
                self.dimstyle_open = true;
                self.load_dimstyle_bufs(i);
                Task::none()
            }
            Message::DimStyleDialogClose => {
                self.dimstyle_open = false;
                Task::none()
            }
            Message::DimStyleDialogApply => {
                let i = self.active_tab;
                self.apply_dimstyle_bufs(i);
                Task::none()
            }
            Message::DimStyleDialogSelect(name) => {
                let i = self.active_tab;
                self.dimstyle_selected = name;
                self.load_dimstyle_bufs(i);
                Task::none()
            }
            Message::DimStyleDialogTab(tab) => {
                self.dimstyle_tab = tab;
                Task::none()
            }
            Message::DimStyleDialogNew => {
                // Delegate to the DIMSTYLE NEW command via command line prompt.
                self.command_line.push_info("Enter new DimStyle name:");
                self.dimstyle_open = false;
                Task::none()
            }
            Message::DimStyleDialogSetCurrent => {
                let i = self.active_tab;
                self.push_undo_snapshot(i, "DIMSTYLE SETCURRENT");
                self.tabs[i].scene.document.header.current_dimstyle_name =
                    self.dimstyle_selected.clone();
                self.tabs[i].dirty = true;
                self.command_line.push_output(&format!(
                    "Current dim style set to '{}'.", self.dimstyle_selected
                ));
                Task::none()
            }
            Message::DimStyleDialogDelete => {
                let i = self.active_tab;
                let name = self.dimstyle_selected.clone();
                if name == "Standard" {
                    self.command_line.push_error("Cannot delete the Standard dim style.");
                } else if self.tabs[i].scene.document.dim_styles.remove(&name).is_some() {
                    self.tabs[i].dirty = true;
                    // Select first remaining style.
                    self.dimstyle_selected = self.tabs[i].scene.document.dim_styles
                        .iter().next().map(|s| s.name.clone())
                        .unwrap_or_else(|| "Standard".to_string());
                    self.load_dimstyle_bufs(i);
                    self.command_line.push_output(&format!("DimStyle '{}' deleted.", name));
                }
                Task::none()
            }
            Message::DsEdit(field, val) => {
                self.apply_ds_edit(field, val);
                Task::none()
            }
            Message::DsToggle(field) => {
                self.apply_ds_toggle(field);
                Task::none()
            }
        }
    }
}

// ── DimStyle dialog helpers ─────────────────────────────────────────────────

impl H7CAD {
    /// Populate all edit buffers from the currently selected dim style.
    fn load_dimstyle_bufs(&mut self, tab: usize) {
        let doc = &self.tabs[tab].scene.document;
        let Some(ds) = doc.dim_styles.get(&self.dimstyle_selected) else { return };
        self.ds_dimdle  = format!("{}", ds.dimdle);
        self.ds_dimdli  = format!("{}", ds.dimdli);
        self.ds_dimgap  = format!("{}", ds.dimgap);
        self.ds_dimexe  = format!("{}", ds.dimexe);
        self.ds_dimexo  = format!("{}", ds.dimexo);
        self.ds_dimsd1  = ds.dimsd1; self.ds_dimsd2 = ds.dimsd2;
        self.ds_dimse1  = ds.dimse1; self.ds_dimse2 = ds.dimse2;
        self.ds_dimasz  = format!("{}", ds.dimasz);
        self.ds_dimcen  = format!("{}", ds.dimcen);
        self.ds_dimtsz  = format!("{}", ds.dimtsz);
        self.ds_dimtxt  = format!("{}", ds.dimtxt);
        self.ds_dimtxsty = ds.dimtxsty.clone();
        self.ds_dimtad  = format!("{}", ds.dimtad);
        self.ds_dimtih  = ds.dimtih; self.ds_dimtoh = ds.dimtoh;
        self.ds_dimscale = format!("{}", ds.dimscale);
        self.ds_dimlfac  = format!("{}", ds.dimlfac);
        self.ds_dimlunit = format!("{}", ds.dimlunit);
        self.ds_dimdec   = format!("{}", ds.dimdec);
        self.ds_dimpost  = ds.dimpost.clone();
        self.ds_dimtol   = ds.dimtol; self.ds_dimlim = ds.dimlim;
        self.ds_dimtp    = format!("{}", ds.dimtp);
        self.ds_dimtm    = format!("{}", ds.dimtm);
        self.ds_dimtdec  = format!("{}", ds.dimtdec);
        self.ds_dimtfac  = format!("{}", ds.dimtfac);
    }

    /// Write edit buffers back into the selected dim style document entry.
    fn apply_dimstyle_bufs(&mut self, tab: usize) {
        self.push_undo_snapshot(tab, "DIMSTYLE EDIT");
        let doc = &mut self.tabs[tab].scene.document;
        let Some(ds) = doc.dim_styles.get_mut(&self.dimstyle_selected) else { return };
        macro_rules! set_f64 { ($field:ident, $buf:expr) => {
            if let Ok(v) = $buf.trim().parse::<f64>() { ds.$field = v; }
        }}
        macro_rules! set_i16 { ($field:ident, $buf:expr) => {
            if let Ok(v) = $buf.trim().parse::<i16>() { ds.$field = v; }
        }}
        set_f64!(dimdle, self.ds_dimdle);   set_f64!(dimdli, self.ds_dimdli);
        set_f64!(dimgap, self.ds_dimgap);   set_f64!(dimexe, self.ds_dimexe);
        set_f64!(dimexo, self.ds_dimexo);   set_f64!(dimasz, self.ds_dimasz);
        set_f64!(dimcen, self.ds_dimcen);   set_f64!(dimtsz, self.ds_dimtsz);
        set_f64!(dimtxt, self.ds_dimtxt);   set_f64!(dimscale, self.ds_dimscale);
        set_f64!(dimlfac, self.ds_dimlfac); set_f64!(dimtp, self.ds_dimtp);
        set_f64!(dimtm, self.ds_dimtm);     set_f64!(dimtfac, self.ds_dimtfac);
        set_i16!(dimtad, self.ds_dimtad);   set_i16!(dimlunit, self.ds_dimlunit);
        set_i16!(dimdec, self.ds_dimdec);   set_i16!(dimtdec, self.ds_dimtdec);
        ds.dimsd1 = self.ds_dimsd1; ds.dimsd2 = self.ds_dimsd2;
        ds.dimse1 = self.ds_dimse1; ds.dimse2 = self.ds_dimse2;
        ds.dimtih = self.ds_dimtih; ds.dimtoh = self.ds_dimtoh;
        ds.dimtol = self.ds_dimtol; ds.dimlim = self.ds_dimlim;
        ds.dimpost = self.ds_dimpost.clone();
        ds.dimtxsty = self.ds_dimtxsty.clone();
        self.tabs[tab].dirty = true;
        self.command_line.push_output(&format!(
            "DimStyle '{}' updated.", self.dimstyle_selected
        ));
    }

    /// Update a single string buffer field.
    fn apply_ds_edit(&mut self, field: super::DsField, val: String) {
        use super::DsField::*;
        match field {
            Dimdle => self.ds_dimdle = val,   Dimdli => self.ds_dimdli = val,
            Dimgap => self.ds_dimgap = val,   Dimexe => self.ds_dimexe = val,
            Dimexo => self.ds_dimexo = val,   Dimasz => self.ds_dimasz = val,
            Dimcen => self.ds_dimcen = val,   Dimtsz => self.ds_dimtsz = val,
            Dimtxt => self.ds_dimtxt = val,   Dimtxsty => self.ds_dimtxsty = val,
            Dimtad => self.ds_dimtad = val,   Dimscale => self.ds_dimscale = val,
            Dimlfac => self.ds_dimlfac = val, Dimlunit => self.ds_dimlunit = val,
            Dimdec => self.ds_dimdec = val,   Dimpost => self.ds_dimpost = val,
            Dimtp => self.ds_dimtp = val,     Dimtm => self.ds_dimtm = val,
            Dimtdec => self.ds_dimtdec = val, Dimtfac => self.ds_dimtfac = val,
            // Bool fields — no-op for string edit
            _ => {}
        }
    }

    /// Toggle a boolean buffer field.
    fn apply_ds_toggle(&mut self, field: super::DsField) {
        use super::DsField::*;
        match field {
            Dimsd1 => self.ds_dimsd1 = !self.ds_dimsd1,
            Dimsd2 => self.ds_dimsd2 = !self.ds_dimsd2,
            Dimse1 => self.ds_dimse1 = !self.ds_dimse1,
            Dimse2 => self.ds_dimse2 = !self.ds_dimse2,
            Dimtih => self.ds_dimtih = !self.ds_dimtih,
            Dimtoh => self.ds_dimtoh = !self.ds_dimtoh,
            Dimtol => self.ds_dimtol = !self.ds_dimtol,
            Dimlim => self.ds_dimlim = !self.ds_dimlim,
            _ => {}
        }
    }

    /// Populate edit buffers from the currently selected text style.
    fn load_textstyle_bufs(&mut self, tab: usize) {
        let doc = &self.tabs[tab].scene.document;
        if let Some(s) = doc.text_styles.get(&self.textstyle_selected) {
            self.textstyle_font = s.font_file.clone();
            self.textstyle_width = format!("{:.4}", s.width_factor);
            self.textstyle_oblique = format!("{:.2}", s.oblique_angle.to_degrees());
        }
    }
}

/// Parse a scale string like "1:50" or "2:1" into (numerator, denominator).
/// Returns (1.0, 1.0) for "Fit" or unknown formats.
fn parse_plot_scale(s: &str) -> (f64, f64) {
    if s == "Fit" { return (1.0, 1.0); }
    if let Some((a, b)) = s.split_once(':') {
        let num: f64 = a.trim().parse().unwrap_or(1.0);
        let den: f64 = b.trim().parse().unwrap_or(1.0);
        if den > 0.0 { return (num, den); }
    }
    (1.0, 1.0)
}
