use super::{Message, OpenCADStudio};
use crate::command::{CmdResult, SelectionEntity, StepInput};
use acadrust::Handle;
use iced::Task;

impl OpenCADStudio {
    pub(super) fn reject_locked_edit(&mut self, i: usize, handle: Handle) -> bool {
        let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) else {
            return false;
        };
        self.command_line.push_info(crate::tf!(
            "Object is on locked layer \"{layer}\" — unlock the layer to edit it."
        ).as_ref());
        true
    }

    pub(super) fn refresh_area_preview(&mut self, i: usize) {
        let hatches = self.tabs[i]
            .active_cmd
            .as_ref()
            .and_then(|command| command.hatch_preview_models());
        if let Some(hatches) = hatches {
            self.tabs[i].scene.set_command_preview_hatches(hatches);
            return;
        }
        let regions = self.tabs[i]
            .active_cmd
            .as_ref()
            .and_then(|command| command.area_preview_regions());
        if let Some(regions) = regions {
            self.tabs[i].scene.set_area_preview_regions(&regions);
        }
    }

    /// Point supplied by a bare Enter before LINE/PLINE's first click. Prefer
    /// the current endpoint of the most recently created path drawable in the
    /// active space. A loaded drawing has no runtime anchor, so recover its
    /// newest line/arc/polyline endpoint. An empty space starts at the active
    /// UCS origin (0,0,0 in user coordinates).
    fn default_draw_start(&self, i: usize) -> glam::DVec3 {
        let tab = &self.tabs[i];
        let endpoint = |entity: &acadrust::EntityType| {
            let last_grip = match entity {
                acadrust::EntityType::Line(line) => {
                    return Some(glam::DVec3::new(
                        line.end.x,
                        line.end.y,
                        line.end.z,
                    ));
                }
                acadrust::EntityType::Arc(_) => Some(2),
                acadrust::EntityType::LwPolyline(polyline) => {
                    polyline.vertices.len().checked_sub(1)
                }
                acadrust::EntityType::Polyline(polyline) => {
                    polyline.vertices.len().checked_sub(1)
                }
                acadrust::EntityType::Polyline2D(polyline) => {
                    polyline.vertices.len().checked_sub(1)
                }
                acadrust::EntityType::Polyline3D(polyline) => {
                    polyline.vertices.len().checked_sub(1)
                }
                _ => None,
            }?;
            crate::scene::view::dispatch::grips(entity)
                .into_iter()
                .find(|grip| grip.id == last_grip)
                .map(|grip| grip.world)
        };

        if let Some(handle) = tab.last_draw_anchor {
            if tab.scene.entity_belongs_to_active_space(handle) {
                if let Some(point) = tab.scene.document.get_entity(handle).and_then(endpoint) {
                    return point;
                }
            }
        }

        let recovered = tab
            .scene
            .document
            .entities()
            .filter_map(|entity| {
                let handle = entity.common().handle;
                tab.scene
                    .entity_belongs_to_active_space(handle)
                    .then(|| endpoint(entity).map(|point| (handle, point)))
                    .flatten()
            })
            .max_by_key(|(handle, _)| handle.value())
            .map(|(_, point)| point);

        recovered.unwrap_or_else(|| tab.ucs_origin_world())
    }

    /// Drop cursor-relative state that was computed in the drawing space being
    /// left. This is also used by MVIEW, whose command object survives its
    /// intentional paper/model round-trip while its old-space overlays cannot.
    pub(super) fn reset_space_interaction_state(&mut self) {
        let i = self.active_tab;
        self.tabs[i].scene.clear_preview_wire();
        self.tabs[i].snap_result = None;
        self.last_point = None;
        self.snapper.from_point = None;
        self.snapper.clear_tracking();
        self.otrack_active = None;
        self.otrack_kind = None;
        self.axis_lock_dir = None;
        self.dyn_user_reshaped = false;
        self.dyn_coord_absolute = false;
        self.grip_hover = None;
        self.grip_popup = None;
        self.grip_pending = None;
        self.visibility_popup = None;
        self.hover_dwell = None;
        self.ucs_grip_drag = None;
        self.ucs_icon_selected = false;
        self.ucs_icon_hover = false;
        self.tabs[i].pan_mode = false;
        self.tabs[i].orbit_mode = false;
        self.tabs[i].zoom_dynamic_mode = false;
        let _ = self.on_viewport_exit();
    }

    /// Roll a hot grip back to its pre-drag image and remove every grip-owned
    /// overlay. Shared by Escape and drawing-space transitions.
    pub(super) fn capture_grip_history_originals(&mut self, i: usize, handles: &[Handle]) {
        if !self.grip_history_originals.is_empty() {
            return;
        }
        self.grip_history_originals = handles
            .iter()
            .filter_map(|&handle| {
                let objects = self.tabs[i].scene.solid_history_objects(handle);
                (!objects.is_empty()).then_some((handle, objects))
            })
            .collect();
    }

    pub(super) fn cancel_active_grip_edit(&mut self) -> bool {
        let i = self.active_tab;
        let had_grip = self.tabs[i].active_grip.take().is_some()
            || self.grip_add_provisional.is_some()
            || !self.grip_preview_handles.is_empty()
            || !self.grip_history_originals.is_empty();
        if !had_grip {
            return false;
        }

        // An Add-Leader arrow being placed is still provisional.
        if let Some((handle, grip_id)) = self.grip_add_provisional.take() {
            use crate::entities::traits::EntityTypeOps;
            if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
                entity.apply_grip_menu(
                    grip_id,
                    crate::scene::model::object::GripMenuAction::RemoveLeader,
                );
            }
            self.tabs[i]
                .scene
                .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
        }

        let handles = std::mem::take(&mut self.grip_preview_handles);
        let originals = std::mem::take(&mut self.grip_originals);
        let history_originals = std::mem::take(&mut self.grip_history_originals);
        let history_handles: rustc_hash::FxHashSet<_> = history_originals
            .iter()
            .map(|(handle, _)| *handle)
            .collect();
        for (_, objects) in history_originals {
            for (object_handle, object) in objects {
                self.tabs[i]
                    .scene
                    .document
                    .objects
                    .insert(object_handle, object);
            }
        }
        let mut changed_handles: rustc_hash::FxHashSet<_> = handles.iter().copied().collect();
        for (handle, original) in originals {
            changed_handles.insert(handle);
            if !history_handles.contains(&handle) {
                let current = self.tabs[i]
                    .scene
                    .document
                    .get_entity(handle)
                    .and_then(crate::entities::solid3d::point_of_reference)
                    .map(|point| [point.x, point.y, point.z]);
                let target = crate::entities::solid3d::point_of_reference(&original)
                    .map(|point| [point.x, point.y, point.z]);
                if let (Some(current), Some(target)) = (current, target) {
                    self.tabs[i].scene.translate_solid_geometry(
                        handle,
                        [
                            target[0] - current[0],
                            target[1] - current[1],
                            target[2] - current[2],
                        ],
                    );
                }
            }
            if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
                *entity = original;
            }
        }
        for handle in history_handles {
            let restored = self.tabs[i]
                .scene
                .document
                .solid_history_operation(handle)
                .cloned()
                .is_some_and(|operation| {
                    self.tabs[i]
                        .scene
                        .rebuild_solid_history(handle, operation)
                });
            if !restored {
                self.tabs[i].scene.reseed_derived_caches(handle);
            }
        }
        for &handle in &handles {
            self.tabs[i].scene.preview_hidden.remove(&handle);
        }
        let changes: Vec<_> = changed_handles
            .into_iter()
            .map(|handle| (handle, crate::scene::ChangeKind::Modified))
            .collect();
        self.tabs[i].scene.bump_entities(&changes);
        if let Some(dirty_before) = self.grip_dirty_before.take() {
            self.tabs[i].dirty = dirty_before;
        }

        self.grip_snap_wires.clear();
        self.grip_text_verts.clear();
        self.grip_text_slide = false;
        self.tabs[i].scene.clear_preview_wire();
        self.tabs[i].snap_result = None;
        self.refresh_selected_grips();
        self.refresh_properties();
        true
    }

    /// End an interactive command before its drawing coordinate context
    /// changes. This is a full interaction boundary, not only an `active_cmd`
    /// check: grip drags, suspended editor commands, snaps, tracking, dynamic
    /// input and pointer gestures all carry coordinates from the old space.
    pub(super) fn cancel_active_command_for_space_change(&mut self) -> Task<Message> {
        let i = self.active_tab;
        let mut tasks = Vec::new();
        let mut cancellation_reported = false;

        if let Some(result) = self.tabs[i]
            .active_cmd
            .as_mut()
            .map(|command| command.on_space_change())
        {
            let (result, message_pending) = match result {
                CmdResult::Cancel => (CmdResult::CancelForSpaceChange, false),
                other => (other, true),
            };
            tasks.push(self.apply_cmd_result(result));

            // `on_space_change` must be terminal. Force a plain cancellation
            // if an external/plugin command violates that contract.
            if self.tabs[i].active_cmd.is_some() {
                tasks.push(self.apply_cmd_result(CmdResult::CancelForSpaceChange));
            } else if message_pending {
                self.command_line
                    .push_info(crate::t!("Command cancelled because the active drawing space changed.").as_ref());
            }
            cancellation_reported = true;
        }

        let grip_cancelled = self.cancel_active_grip_edit();
        let suspended_cancelled = self.tabs[i].suspended_cmd.take().is_some();
        let editor_cancelled = self.text_inline.is_some() || self.mtext_editor.is_some();
        if editor_cancelled {
            self.text_inline_cancel();
            self.mtext_cancel();
        }
        if !cancellation_reported && (grip_cancelled || suspended_cancelled || editor_cancelled) {
            self.command_line
                .push_info(crate::t!("Command cancelled because the active drawing space changed.").as_ref());
        }

        self.command_line.input.clear();
        self.command_line.autocomplete_cursor = None;
        self.command_line.close_history();
        self.reset_space_interaction_state();
        if tasks.is_empty() {
            Task::none()
        } else {
            Task::batch(tasks)
        }
    }

    /// Apply LIMCHECK/PLIMCHECK to a point before an interactive command
    /// consumes it. LIMITS itself must be able to redefine a rectangle beyond
    /// the old boundary, so it is the sole bypass.
    pub(super) fn command_point_allowed(&mut self, i: usize, point: glam::DVec3) -> bool {
        let checks_limits = self.tabs[i]
            .active_cmd
            .as_ref()
            .is_some_and(|command| command.name() != "LIMITS")
            && self.tabs[i].scene.drawing_limit_check_enabled();
        if checks_limits && !self.tabs[i].scene.point_inside_drawing_limits(point) {
            self.command_line.push_error(crate::t!("Outside limits.").as_ref());
            return false;
        }
        true
    }

    /// Drive the active command's step machine with one [`StepInput`], then
    /// apply the result. This is the single entry point every input source
    /// (command line, headless, dynamic input, plugin API, viewport) funnels
    /// through, so the routing from input → `on_*` method → `apply_cmd_result`
    /// lives in exactly one place. No-op when no command is active.
    pub(super) fn feed_command(&mut self, input: StepInput) -> Task<Message> {
        // Selection keywords (P / PREVIOUS, L / LAST) consume the token
        // before it reaches the command (#426).
        if let StepInput::Text(s) = &input {
            let kw_input = s.clone();
            if let Some(task) = self.try_selection_keyword(&kw_input) {
                return task;
            }
        }
        let i = self.active_tab;
        let default_start = matches!(&input, StepInput::Enter)
            && self.tabs[i]
                .active_cmd
                .as_ref()
                .is_some_and(|command| command.enter_accepts_default_start());
        let input = if default_start {
            StepInput::Point(self.default_draw_start(i))
        } else {
            input
        };
        if let StepInput::Point(point) = &input {
            if !self.command_point_allowed(i, *point) {
                return Task::none();
            }
        }
        if default_start {
            let StepInput::Point(point) = &input else {
                unreachable!("default command start must be a point");
            };
            self.last_point = Some(*point);
            self.dyn_user_reshaped = false;
            self.dyn_coord_absolute = false;
            self.sync_dyn_fields();
            self.reset_tracking_after_point();
            self.push_ucs_to_cmd(i);
        }
        if let StepInput::SelectionComplete(handles) = &input {
            let entities = {
                let scene = &self.tabs[i].scene;
                handles
                    .iter()
                    .filter_map(|handle| {
                        scene.document.get_entity(*handle).cloned().map(|entity| {
                            let surface_area = scene
                                .meshes
                                .get(handle)
                                .or_else(|| scene.block_meshes.get(handle))
                                .map(|mesh| mesh.metrics.surface_area);
                            SelectionEntity {
                                handle: *handle,
                                entity,
                                surface_area,
                            }
                        })
                    })
                    .collect()
            };
            if let Some(command) = self.tabs[i].active_cmd.as_mut() {
                command.inject_selection_entities(entities);
            }
        }
        let ctrl = self.ctrl_down;
        let shift = self.shift_down;
        let result: Option<CmdResult> = {
            let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
                return Task::none();
            };
            cmd.set_ctrl(ctrl);
            cmd.set_shift(shift);
            match input {
                StepInput::Point(p) => Some(cmd.on_point(p)),
                StepInput::Text(s) => cmd.on_text_input(&s),
                StepInput::EntityPick(h, p) => Some(cmd.on_entity_pick(h, p)),
                StepInput::StructurePick(h, p) => Some(cmd.on_structure_pick(h, p)),
                StepInput::SelectionComplete(hs) => Some(cmd.on_selection_complete(hs)),
                StepInput::Tangent(o, p) => Some(cmd.on_tangent_point(o, p)),
                StepInput::EditorClosed(c) => Some(cmd.on_editor_closed(c)),
                StepInput::Enter => Some(cmd.on_enter()),
                StepInput::Escape => Some(cmd.on_escape()),
            }
        };
        match result {
            Some(r) => self.apply_cmd_result(r),
            None => Task::none(),
        }
    }

    /// Run one whole command-line string. A single word or an inline-argument
    /// command (`PDMODE 3`, `LAYER Walls`, `UCS Z 90` pasted as one line)
    /// dispatches as-is; for a multi-token line whose first word starts an
    /// interactive tool (`LINE 0,0 10,10`) the first word starts the tool and the
    /// remaining tokens are fed as points / option keywords, then the command is
    /// terminated as if Enter were pressed. Shared by the GUI command line and
    /// the headless automation feeder so both behave identically.
    pub(super) fn run_command_line(&mut self, cmd: &str) -> Task<Message> {
        let i = self.active_tab;
        let tokens: Vec<&str> = cmd.split_whitespace().collect();
        if tokens.len() <= 1 {
            return self.dispatch_command(cmd);
        }
        // Plugin commands parse their own inline arguments from the whole line
        // (e.g. `HC_PIPE 2B 2C 1.25 0.013`), so offer the full command to plugin
        // dispatch first. A built-in interactive tool matches only its bare name
        // (`LINE`), so the full line is not a plugin command and falls through to
        // the first-word + fed-tokens path below. (#162)
        if crate::plugin::try_dispatch(self, i, cmd) {
            let toks: Vec<String> = tokens.iter().map(|s| s.to_string()).collect();
            self.finish_active_command(&toks);
            return Task::none();
        }
        let _ = self.dispatch_command(tokens[0]);
        if self.tabs[i].active_cmd.is_none() {
            // Not an interactive tool — an inline-argument command (`PDMODE 3`).
            return self.dispatch_command(cmd);
        }
        let toks: Vec<String> = tokens.iter().map(|s| s.to_string()).collect();
        self.finish_active_command(&toks);
        Task::none()
    }

    /// Feed `tokens[1..]` to the active interactive command as points / option
    /// keywords, then terminate it as if Enter were pressed. No-op when no
    /// command is active.
    pub(super) fn finish_active_command(&mut self, tokens: &[String]) {
        let i = self.active_tab;
        if self.tabs[i].active_cmd.is_none() {
            return;
        }
        self.last_point = None;
        for tok in &tokens[1..] {
            if self.tabs[i].active_cmd.is_none() {
                break;
            }
            self.feed_active_cmd(tok);
        }
        let _ = self.feed_command(StepInput::Enter);
    }

    /// Classify one typed token into a [`StepInput`] and route it through the
    /// shared [`Self::feed_command`]. An object-pick step takes a hex handle; a
    /// coordinate is parsed (and, like the GUI command line, interpreted in the
    /// active UCS); anything else is an option keyword / value. Used by both the
    /// GUI command line and headless automation.
    /// Standard selection keywords at a "Select objects:" prompt (#426):
    /// P / PREVIOUS re-selects the set the last command worked on, and
    /// L / LAST selects the most recently created object in the current
    /// space. Returns `Some` when the token was consumed as a keyword.
    /// Handled centrally so every gathering command gets them for free.
    pub(super) fn try_selection_keyword(&mut self, text: &str) -> Option<Task<Message>> {
        let i = self.active_tab;
        let gathering = self.tabs[i]
            .active_cmd
            .as_ref()
            .map_or(false, |c| c.is_selection_gathering());
        if !gathering {
            return None;
        }
        let kw = text.trim().to_ascii_uppercase();

        // Modes that arm the next gesture rather than selecting anything now.
        // Dragging normally decides window-vs-crossing from the direction the
        // corner travels; asking for one by name has to override that, and hold
        // the override until the box is finished. (#596)
        if let "W" | "WINDOW" | "C" | "CROSSING" = kw.as_str() {
            let crossing = matches!(kw.as_str(), "C" | "CROSSING");
            {
                let mut selection = self.tabs[i].scene.selection.borrow_mut();
                selection.box_crossing = crossing;
                selection.box_crossing_locked = true;
            }
            let hint = if crossing {
                crate::t!("Crossing: specify first corner.")
            } else {
                crate::t!("Window: specify first corner.")
            };
            self.command_line.push_info(hint.as_ref());
            return Some(Task::none());
        }

        // Whether a pick adds to the set or takes away from it, for the rest of
        // this selection. Mirrors PICKADD, which the pick paths already read.
        if let "R" | "REMOVE" | "A" | "ADD" = kw.as_str() {
            let adding = matches!(kw.as_str(), "A" | "ADD");
            self.select_remove_mode = !adding;
            let hint = if adding {
                crate::t!("Add mode: picks join the selection.")
            } else {
                crate::t!("Remove mode: picks leave the selection.")
            };
            self.command_line.push_info(hint.as_ref());
            return Some(Task::none());
        }

        let add: Vec<Handle> = match kw.as_str() {
            // Every selectable object of the current space.
            "ALL" => self.tabs[i]
                .scene
                .entity_wires()
                .iter()
                .filter_map(|w| crate::scene::Scene::handle_from_wire_name(&w.name))
                .collect(),
            "P" | "PREVIOUS" => self.tabs[i]
                .prev_selection
                .iter()
                .copied()
                .filter(|&h| self.tabs[i].scene.document.get_entity(h).is_some())
                .collect(),
            // Highest handle among the selectable wires of the current space —
            // handles are handed out monotonically, so that is the most
            // recently created object.
            "L" | "LAST" => self.tabs[i]
                .scene
                .entity_wires()
                .iter()
                .filter_map(|w| crate::scene::Scene::handle_from_wire_name(&w.name))
                .max_by_key(|h| h.value())
                .into_iter()
                .collect(),
            _ => return None,
        };
        if add.is_empty() {
            self.command_line.push_info(match kw.as_str() {
                "P" | "PREVIOUS" => "No previous selection set.",
                "ALL" => "Nothing to select.",
                _ => "No last object.",
            });
            return Some(Task::none());
        }
        let count = add.len();
        for h in add {
            self.tabs[i].scene.select_entity(h, false);
        }
        self.command_line
            .push_info(crate::tf!("{count} object(s) added to selection.").as_ref());
        self.refresh_properties();
        let handles: Vec<Handle> = self.tabs[i]
            .scene
            .selected_entities()
            .into_iter()
            .map(|(h, _)| h)
            .collect();
        Some(self.feed_command(StepInput::SelectionComplete(handles)))
    }

    pub(super) fn feed_active_cmd(&mut self, token: &str) {
        let i = self.active_tab;
        // Object-pick step: the token is a handle (as returned by `query`).
        if self.tabs[i]
            .active_cmd
            .as_ref()
            .is_some_and(|c| c.needs_entity_pick())
        {
            // Option keywords take precedence over the handle reading —
            // "F"/"C"/"E" are valid hex, but during TRIM/EXTEND they are the
            // Fence/Crossing/Edge options (#336). Only an unconsumed token
            // falls through to the handle interpretation.
            let consumed = self.tabs[i]
                .active_cmd
                .as_mut()
                .and_then(|c| c.on_text_input(token));
            if let Some(r) = consumed {
                let _ = self.apply_cmd_result(r);
                return;
            }
            let accepts_points = self.tabs[i]
                .active_cmd
                .as_ref()
                .is_some_and(|command| command.entity_pick_accepts_points());
            if accepts_points {
                if let Some((coord, kind)) = super::helpers::parse_coord(token) {
                    let ucs = self.tabs[i].active_ucs.clone();
                    let wcs = match (
                        matches!(kind, super::helpers::CoordKind::Relative),
                        self.last_point,
                    ) {
                        (true, Some(base)) => {
                            base + match &ucs {
                                Some(value) => super::helpers::ucs_rotate_vec(coord, value),
                                None => coord,
                            }
                        }
                        _ => match &ucs {
                            Some(value) => super::helpers::ucs_to_wcs(coord, value),
                            None => coord,
                        },
                    };
                    if self.command_point_allowed(i, wcs) {
                        self.last_point = Some(wcs);
                        self.push_ucs_to_cmd(i);
                        let _ = self.feed_command(StepInput::Point(wcs));
                    }
                    return;
                }
            }
            if let Ok(v) = u64::from_str_radix(token.trim_start_matches("0x"), 16) {
                let handle = Handle::new(v);
                let pt = self.tabs[i]
                    .scene
                    .document
                    .get_entity(handle)
                    .map(|e| {
                        let bb = e.as_entity().bounding_box();
                        glam::Vec3::new(
                            ((bb.min.x + bb.max.x) * 0.5) as f32,
                            ((bb.min.y + bb.max.y) * 0.5) as f32,
                            0.0,
                        )
                    })
                    .unwrap_or(glam::Vec3::ZERO);
                let _ = self.feed_command(StepInput::EntityPick(handle, pt.as_dvec3()));
            }
            return;
        }
        if let Some((coord, kind)) = super::helpers::parse_coord(token) {
            // Match the GUI command line: typed coordinates are in the active
            // UCS (relative offsets are rotated by the UCS axes), so a multi-
            // token `LINE 0,0 10,10` under a rotated UCS lands correctly.
            let ucs = self.tabs[i].active_ucs.clone();
            let wcs = match (
                matches!(kind, super::helpers::CoordKind::Relative),
                self.last_point,
            ) {
                (true, Some(base)) => {
                    base + match &ucs {
                        Some(u) => super::helpers::ucs_rotate_vec(coord, u),
                        None => coord,
                    }
                }
                _ => match &ucs {
                    Some(u) => super::helpers::ucs_to_wcs(coord, u),
                    None => coord,
                },
            };
            if !self.command_point_allowed(i, wcs) {
                return;
            }
            self.last_point = Some(wcs);
            self.push_ucs_to_cmd(i);
            let _ = self.feed_command(StepInput::Point(wcs));
        } else {
            let _ = self.feed_command(StepInput::Text(token.to_string()));
        }
    }

    /// Applies one command result, then — when that result ended the active
    /// command — drops the selection. Editing tools (MOVE, COPY, ROTATE, …) and
    /// every other interactive command leave nothing selected once they finish,
    /// so a follow-up edit doesn't silently reuse the previous working set.
    ///
    /// `Relaunch`/`Dispatch` are excepted: they end the front-end command only
    /// to immediately start another and hand it a deliberate selection (the
    /// pick-first selector relaunching MOVE on the picked set works this way).
    /// Pure-selection commands (SELECTALL, QSELECT, …) run without an active
    /// command, so `was_active` is false and their selection is preserved.
    pub(super) fn apply_cmd_result(&mut self, result: CmdResult) -> Task<Message> {
        let settings = self.tabs[self.active_tab]
            .active_cmd
            .as_ref()
            .and_then(|command| command.sketch_settings());
        if let Some((sketch_type, increment, tolerance)) = settings {
            let tab = &mut self.tabs[self.active_tab];
            let changed = tab.scene.document.header.sketch_type != sketch_type
                || (tab.scene.document.header.sketch_increment - increment).abs() > f64::EPSILON
                || (tab.scene.document.header.sketch_tolerance - tolerance).abs()
                    > f64::EPSILON;
            if changed {
                crate::io::set_sketch_settings(
                    &mut tab.scene.document,
                    sketch_type,
                    increment,
                    tolerance,
                );
                tab.dirty = true;
            }
        }
        let mline_settings = self.tabs[self.active_tab]
            .active_cmd
            .as_ref()
            .and_then(|command| command.mline_settings());
        if let Some((scale, justification, style_name, style_handle)) = mline_settings {
            let tab = &mut self.tabs[self.active_tab];
            let header = &mut tab.scene.document.header;
            let style_handle = style_handle.unwrap_or(acadrust::Handle::NULL);
            let changed = (header.multiline_scale - scale).abs() > f64::EPSILON
                || header.multiline_justification != justification
                || !header.multiline_style.eq_ignore_ascii_case(&style_name)
                || header.current_multiline_style_handle != style_handle;
            if changed {
                header.multiline_scale = scale;
                header.multiline_justification = justification;
                header.multiline_style = style_name;
                header.current_multiline_style_handle = style_handle;
                tab.dirty = true;
            }
        }
        let was_active = self.tabs[self.active_tab].active_cmd.is_some();
        let preserve_selection = matches!(
            &result,
            CmdResult::Relaunch(..)
                | CmdResult::Dispatch(..)
                | CmdResult::SolidSubtract { .. }
        );
        let task = self.apply_cmd_result_inner(result);
        let i = self.active_tab;
        let preview_hidden = self.tabs[i]
            .active_cmd
            .as_ref()
            .map(|command| command.preview_hidden_handles().to_vec())
            .unwrap_or_default();
        self.tabs[i]
            .scene
            .set_command_preview_hidden(&preview_hidden);
        if was_active
            && !preserve_selection
            && self.tabs[i].active_cmd.is_none()
            && !self.tabs[i].scene.selected.is_empty()
        {
            // Remember the working set for the "Previous" selection keyword
            // (#426) before dropping it.
            self.tabs[i].prev_selection = self.tabs[i].scene.selected.iter().copied().collect();
            self.tabs[i].scene.deselect_all();
            self.refresh_properties();
        }
        // A command just ended (any terminal result) — if it was the draw
        // command an ADDSELECTED launched, revert the template-property override
        // so CLAYER / CECOLOR / … are left unchanged (#239). No-op otherwise.
        if was_active && self.tabs[i].active_cmd.is_none() {
            self.tabs[i].scene.set_hover_highlight(None);
            self.command_line.set_step_options(Vec::new());
            self.restore_add_selected_defaults();
        }
        task
    }

    fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
        let i = self.active_tab;
        match result {
            CmdResult::NeedPoint => {
                // ATTEDIT finished its entity pick: hand the chosen block off to
                // the attribute editor dialog and end the command (open_attribute
                // _editor reports "no attributes" / "select a block" as needed).
                let attedit_handle = self.tabs[i]
                    .active_cmd
                    .as_ref()
                    .and_then(|c| c.attedit_pending_handle());
                if let Some(ins_handle) = attedit_handle {
                    self.tabs[i].active_cmd = None;
                    self.command_line.set_step_options(Vec::new());
                    self.open_attribute_editor(ins_handle);
                    return Task::none();
                }
                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 opts = self.tabs[i]
                    .active_cmd
                    .as_ref()
                    .map(|c| c.options())
                    .unwrap_or_default();
                self.command_line.set_step_options(opts);
                if !self.tabs[i]
                    .active_cmd
                    .as_ref()
                    .is_some_and(|c| c.entity_pick_highlights_hover())
                {
                    self.tabs[i].scene.set_hover_highlight(None);
                }
                // The command may have advanced to a step with a different
                // dynamic-input shape (e.g. FILLET object-pick → radius entry).
                // Rebuild the fields now so the matching box appears immediately
                // and typed digits land in it rather than the command line,
                // instead of waiting for the next cursor move to resync.
                self.sync_dyn_fields();
                self.refresh_area_preview(i);
            }
            CmdResult::Preview(wire) => {
                self.tabs[i].scene.set_preview_wires(vec![wire]);
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }
            CmdResult::InterimWire(wire) => {
                self.tabs[i].scene.set_interim_wire(wire);
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }
            CmdResult::CommitEntity(entity) => {
                let source_handle = entity.common().handle;
                if !source_handle.is_null()
                    && self.tabs[i]
                        .scene
                        .document
                        .get_entity(source_handle)
                        .is_some()
                    && self.reject_locked_edit(i, source_handle)
                {
                    return Task::none();
                }
                // A line/arc drawn by a repeating command advances the ARC_CONT
                // continuation anchor, so ending one run and launching another
                // keeps continuing from the last segment (mirrors the
                // CommitAndExit arm). Non-continuable repeats (point/ray)
                // leave the anchor untouched. (#327)
                if matches!(
                    entity,
                    acadrust::EntityType::Line(_)
                        | acadrust::EntityType::Arc(_)
                        | acadrust::EntityType::LwPolyline(_)
                        | acadrust::EntityType::Polyline2D(_)
                ) {
                    self.update_cont_anchor(&entity);
                }
                let label = self.history_label_from_active_cmd(i, "ENTITY");
                // Ordinary drawables, viewports and raster images use targeted
                // entity/object deltas. Block sentinels and novel layers retain
                // the structure snapshot fallback.
                let delta_safe = self.delta_add_safe(i, &entity);
                let pending = self.begin_undo(i, label, 1, delta_safe);
                let is_associative_dimension = matches!(
                    entity,
                    acadrust::EntityType::Dimension(
                        acadrust::entities::Dimension::Linear(_)
                            | acadrust::entities::Dimension::Aligned(_)
                    )
                );
                let association_enabled =
                    self.tabs[i].scene.document.header.dimension_associativity == 2;
                let committed = self.commit_entity_handle(entity);
                if is_associative_dimension && association_enabled {
                    if let Some(handle) = committed {
                        let sources = self.tabs[i]
                            .scene
                            .infer_dimension_sources(handle);
                        self.tabs[i]
                            .scene
                            .attach_dimension_association(handle, sources.to_vec());
                    }
                }
                self.tabs[i].dirty = true;
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitEntities(entities) => {
                let locked_source = entities.iter().find_map(|entity| {
                    let handle = entity.common().handle;
                    (!handle.is_null()
                        && self.tabs[i].scene.document.get_entity(handle).is_some()
                        && self.tabs[i].scene.is_layer_locked(handle))
                    .then_some(handle)
                });
                if let Some(handle) = locked_source {
                    self.reject_locked_edit(i, handle);
                    return Task::none();
                }
                let label = self.history_label_from_active_cmd(i, "ENTITY");
                let delta_safe = entities
                    .iter()
                    .all(|entity| self.delta_add_safe(i, entity));
                let pending = self.begin_undo(i, label, entities.len(), delta_safe);
                for entity in entities {
                    self.commit_entity(entity);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitEntitiesAndExit(entities) => {
                let label = self.history_label_from_active_cmd(i, "ENTITY");
                let delta_safe = entities
                    .iter()
                    .all(|entity| self.delta_add_safe(i, entity));
                let pending = self.begin_undo(i, label, entities.len(), delta_safe);
                for entity in entities {
                    self.commit_entity(entity);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::MviewCreate {
                viewport,
                preserve_view,
            } => {
                let saved_view = preserve_view.then(|| {
                    (
                        viewport.view_target.clone(),
                        viewport.view_direction.clone(),
                        viewport.view_center.clone(),
                        viewport.view_height,
                        viewport.custom_scale,
                        viewport.lens_length,
                        viewport.twist_angle,
                        viewport.status.perspective,
                    )
                });
                let label = self.history_label_from_active_cmd(i, "MVIEW");
                let pending = self.begin_undo(i, label, 1, true);
                let handle = self.commit_entity_handle(
                    acadrust::EntityType::Viewport(viewport),
                );
                if let (Some(handle), Some(saved)) = (handle, saved_view) {
                    if let Some(acadrust::EntityType::Viewport(viewport)) =
                        self.tabs[i].scene.document.get_entity_mut(handle)
                    {
                        viewport.view_target = saved.0;
                        viewport.view_direction = saved.1;
                        viewport.view_center = saved.2;
                        viewport.view_height = saved.3;
                        viewport.custom_scale = saved.4;
                        viewport.lens_length = saved.5;
                        viewport.twist_angle = saved.6;
                        viewport.status.perspective = saved.7;
                    }
                    self.tabs[i].scene.camera_generation += 1;
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                if let Some(pending) = pending {
                    self.commit_undo_delta(i, pending);
                }
            }
            CmdResult::MviewCreateClipped {
                boundary,
                boundary_handle,
            } => {
                if boundary.is_none() {
                    let scene = &self.tabs[i].scene;
                    let valid = scene
                        .entity_belongs_to_current_layout(boundary_handle)
                        && scene
                        .document
                        .get_entity(boundary_handle)
                        .is_some_and(|entity| {
                            match entity {
                                acadrust::EntityType::Circle(_) => true,
                                acadrust::EntityType::Ellipse(ellipse) => ellipse.is_full(),
                                acadrust::EntityType::LwPolyline(polyline) => {
                                    polyline.is_closed
                                }
                                acadrust::EntityType::Polyline(polyline) => {
                                    polyline.is_closed()
                                }
                                acadrust::EntityType::Polyline2D(polyline) => {
                                    polyline.is_closed()
                                }
                                acadrust::EntityType::Polyline3D(polyline) => {
                                    polyline.flags.closed
                                }
                                _ => false,
                            }
                        });
                    if !valid {
                        self.command_line.push_error(
                            "MVIEW Object: select a closed paper-space circle, ellipse, or polyline.",
                        );
                        if let Some(prompt) =
                            self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
                        {
                            self.command_line.push_info(&prompt);
                        }
                        return Task::none();
                    }
                }

                let created_boundary = boundary.is_some();
                let touched = 2;
                let label = self.history_label_from_active_cmd(i, "MVIEW");
                let pending = self.begin_undo(i, label, touched, true);
                let clip_handle = match boundary {
                    Some(mut boundary) => {
                        // A non-rectangular viewport owns a helper boundary
                        // entity through `clip_boundary_handle`. Keep that
                        // helper in the document for DWG compatibility and
                        // stencil clipping, but do not expose it as a separate
                        // selectable polyline.
                        boundary.common_mut().invisible = true;
                        match self.commit_entity_handle(boundary) {
                            Some(handle) => handle,
                            None => {
                                self.tabs[i].active_cmd = None;
                                if let Some(pending) = pending {
                                    self.commit_undo_delta(i, pending);
                                }
                                return Task::none();
                            }
                        }
                    }
                    None => boundary_handle,
                };
                let polygon = self.tabs[i]
                    .scene
                    .clip_boundary_polygon(clip_handle, 0.0);
                let bounds: Option<(f64, f64, f64, f64)> =
                    polygon.iter().fold(None, |bounds, point| {
                        if !point[0].is_finite() || !point[1].is_finite() {
                            return bounds;
                        }
                        Some(match bounds {
                            Some((min_x, min_y, max_x, max_y)) => (
                                min_x.min(point[0] as f64),
                                min_y.min(point[1] as f64),
                                max_x.max(point[0] as f64),
                                max_y.max(point[1] as f64),
                            ),
                            None => (
                                point[0] as f64,
                                point[1] as f64,
                                point[0] as f64,
                                point[1] as f64,
                            ),
                        })
                    });
                let Some((min_x, min_y, max_x, max_y)) = bounds else {
                    self.command_line
                        .push_error(crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref());
                    self.tabs[i].active_cmd = None;
                    if let Some(pending) = pending {
                        self.commit_undo_delta(i, pending);
                    }
                    return Task::none();
                };
                if max_x - min_x < 1e-6 || max_y - min_y < 1e-6 {
                    self.command_line
                        .push_error(crate::t!("MVIEW: the clipping boundary has no usable area.").as_ref());
                    self.tabs[i].active_cmd = None;
                    if let Some(pending) = pending {
                        self.commit_undo_delta(i, pending);
                    }
                    return Task::none();
                }

                let mut viewport = acadrust::entities::Viewport::new();
                viewport.center = acadrust::types::Vector3::new(
                    (min_x + max_x) / 2.0,
                    (min_y + max_y) / 2.0,
                    0.0,
                );
                viewport.width = max_x - min_x;
                viewport.height = max_y - min_y;
                viewport.id = 2;
                viewport.clip_boundary_handle = clip_handle;
                let viewport_handle = self.commit_entity_handle(
                    acadrust::EntityType::Viewport(viewport),
                );
                if let Some(viewport_handle) = viewport_handle {
                    if !created_boundary {
                        let before = self.tabs[i]
                            .scene
                            .document
                            .get_entity(clip_handle)
                            .cloned()
                            .map(std::sync::Arc::new);
                        self.tabs[i]
                            .scene
                            .record_undo_before(clip_handle, before);
                    }
                    if let Some(boundary) =
                        self.tabs[i].scene.document.get_entity_mut(clip_handle)
                    {
                        let common = boundary.common_mut();
                        common.invisible = true;
                        if !common.reactors.contains(&viewport_handle) {
                            common.reactors.push(viewport_handle);
                        }
                    }
                    self.tabs[i].scene.bump_entities(&[(
                        clip_handle,
                        crate::scene::ChangeKind::Modified,
                    )]);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                if let Some(pending) = pending {
                    self.commit_undo_delta(i, pending);
                }
            }
            CmdResult::WipeoutFromPolyline {
                handle,
                erase_source,
            } => {
                let wipeout = {
                    let scene = &self.tabs[i].scene;
                    scene
                        .entity_belongs_to_active_space(handle)
                        .then(|| scene.document.get_entity(handle))
                        .flatten()
                        .and_then(
                            crate::modules::draw::draw::wipeout::wipeout_from_polyline,
                        )
                };
                if let Some(wipeout) = wipeout {
                    if erase_source {
                        return self.apply_cmd_result(CmdResult::ReplaceMany(
                            vec![(handle, Vec::new())],
                            vec![wipeout],
                        ));
                    }
                    return self.apply_cmd_result(CmdResult::CommitAndExit(wipeout));
                }
                self.command_line.push_error(
                    crate::t!("WIPEOUT Polyline: select a straight, closed, planar 2D polyline with at least 3 non-intersecting vertices.").as_ref(),
                );
                let command =
                    crate::modules::draw::draw::wipeout::WipeoutCommand::new_polyline();
                self.command_line.push_info(
                    &crate::command::CadCommand::prompt(&command),
                );
                self.tabs[i].active_cmd = Some(Box::new(command));
            }
            CmdResult::MviewSwitchLayout(layout) => {
                let task = self.on_layout_switch_preserving_command(layout);
                if let Some(prompt) =
                    self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
                {
                    self.command_line.push_info(&prompt);
                }
                return task;
            }
            CmdResult::MviewCancelToLayout(layout) => {
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                return self.on_layout_switch(layout);
            }
            CmdResult::TransformSelected(mut handles, transform) => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                if handles.is_empty() {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                let label = self.history_label_from_active_cmd(i, "MOVE");
                // A move/rotate/scale/mirror mutates only the selected entities
                // (and their baked dimension sub-entities) through
                // transform_entities — always delta-safe.
                let pending = self.begin_undo(i, label, handles.len(), true);
                self.tabs[i].scene.transform_entities(&handles, &transform);
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.refresh_properties();
                if let Some(p) = pending {
                    self.commit_undo_delta(i, p);
                }
            }
            CmdResult::CopySelected(mut handles, transform) => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                if handles.is_empty() {
                    return Task::none();
                }
                let label = self.history_label_from_active_cmd(i, "COPY");
                // Copying a dimension clones a *D block record, so gate delta on
                // the selection being dimension-free.
                let delta_safe = self.delta_copy_safe(i, &handles);
                let pending = self.begin_undo(i, label, handles.len(), delta_safe);
                let new_handles = self.tabs[i].scene.copy_entities(&handles, &transform);
                self.tabs[i].dirty = true;
                self.tabs[i].scene.deselect_all();
                for h in new_handles {
                    self.tabs[i].scene.select_entity(h, false);
                }
                self.tabs[i].scene.clear_preview_wire();
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
                self.refresh_properties();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitAndExit(entity) => {
                // For XATTACH: ensure the xref block definition exists before
                // committing the INSERT entity that references it.
                // Extract path early to avoid borrow conflicts.
                let xattach_path: Option<String> = {
                    let tab = &self.tabs[i];
                    if let Some(cmd) = tab.active_cmd.as_ref() {
                        if cmd.name() == "XATTACH" {
                            cmd.xattach_path()
                        } else {
                            None
                        }
                    } else {
                        None
                    }
                };
                if let Some(path) = xattach_path {
                    crate::modules::insert::xattach::prepare_xref_block(
                        &mut self.tabs[i].scene,
                        &path,
                    );
                    // Resolving the xref merged its layer / linetype tables
                    // into the document — mirror them into the Layers panel
                    // and ribbon dropdowns now, not on the next reopen (#407).
                    self.refresh_layer_panel();
                }
                let insert_block_name = match &entity {
                    acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
                    _ => None,
                };
                // Record where this draw ended so ARC_CONT can continue from it
                // (before `entity` is moved into commit_entity).
                self.update_cont_anchor(&entity);
                let label = self.history_label_from_active_cmd(i, "ENTITY");
                let delta_safe = self.delta_add_safe(i, &entity);
                let pending = self.begin_undo(i, label, 1, delta_safe);
                let is_associative_dimension = matches!(
                    entity,
                    acadrust::EntityType::Dimension(
                        acadrust::entities::Dimension::Linear(_)
                            | acadrust::entities::Dimension::Aligned(_)
                    )
                );
                let association_enabled =
                    self.tabs[i].scene.document.header.dimension_associativity == 2;
                let committed = self.commit_entity_handle(entity);
                if is_associative_dimension && association_enabled {
                    if let Some(handle) = committed {
                        let sources = self.tabs[i]
                            .scene
                            .infer_dimension_sources(handle);
                        self.tabs[i]
                            .scene
                            .attach_dimension_association(handle, sources.to_vec());
                    }
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                if let Some(name) = insert_block_name {
                    self.record_block_insert(&name);
                }
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitDimension {
                mut entity,
                association,
            } => {
                let label = self.history_label_from_active_cmd(i, "DIMENSION");
                let association_mode = self.tabs[i]
                    .scene
                    .document
                    .header
                    .dimension_associativity;
                let single_source_dimension = matches!(
                    &entity,
                    acadrust::EntityType::Dimension(
                        acadrust::entities::Dimension::Ordinate(_)
                    )
                );
                let pending = if association_mode == 0 {
                    let layer = self.tabs[i].active_layer.clone();
                    if layer != "0" || entity.as_entity().layer().is_empty() {
                        entity.as_entity_mut().set_layer(layer);
                    }
                    crate::scene::view::dispatch::apply_color(
                        &mut entity,
                        self.ribbon.active_color,
                    );
                    crate::scene::view::dispatch::apply_common_prop(
                        &mut entity,
                        "linetype",
                        &self.ribbon.active_linetype.clone(),
                    );
                    crate::scene::view::dispatch::apply_line_weight(
                        &mut entity,
                        self.ribbon.active_lineweight,
                    );
                    let celtscale = self.tabs[i]
                        .scene
                        .document
                        .header
                        .current_entity_linetype_scale;
                    if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
                        entity.common_mut().linetype_scale = celtscale;
                    }
                    crate::scene::creation_style::apply_current_creation_styles(
                        &self.tabs[i].scene.document,
                        &mut entity,
                    );
                    let pieces = crate::modules::draw::modify::explode::explode_entity(
                        &entity,
                        &self.tabs[i].scene.document,
                    );
                    let delta_safe = pieces
                        .iter()
                        .all(|piece| self.delta_add_safe(i, piece));
                    let pending = self.begin_undo(i, label, pieces.len(), delta_safe);
                    for piece in pieces {
                        self.tabs[i].scene.add_entity(piece);
                    }
                    pending
                } else {
                    let delta_safe = self.delta_add_safe(i, &entity);
                    let pending = self.begin_undo(i, label, 1, delta_safe);
                    if let Some(handle) = self.commit_entity_handle(entity) {
                        if association_mode == 2 {
                            let mut changes = vec![
                                (handle, crate::scene::ChangeKind::Modified),
                            ];
                            match association {
                                crate::command::DimensionAssociationInput::Infer(source) => {
                                    let sources: Vec<_> = source.map_or_else(
                                        || {
                                            self.tabs[i]
                                                .scene
                                                .infer_dimension_sources(handle)
                                                .into_iter()
                                                .collect()
                                        },
                                        |source| {
                                            if single_source_dimension {
                                                vec![Some(source)]
                                            } else {
                                                vec![Some(source), Some(source)]
                                            }
                                        },
                                    );
                                    self.tabs[i]
                                        .scene
                                        .attach_dimension_association(handle, sources.clone());
                                    changes.extend(sources.into_iter().flatten().map(|source| {
                                        (source, crate::scene::ChangeKind::Modified)
                                    }));
                                }
                                crate::command::DimensionAssociationInput::Explicit(sources) => {
                                    self.tabs[i]
                                        .scene
                                        .attach_dimension_association_sources(handle, sources.clone());
                                    changes.extend(sources.into_iter().flatten().map(|source| {
                                        (source.handle, crate::scene::ChangeKind::Modified)
                                    }));
                                }
                            }
                            changes.sort_by_key(|(handle, _)| handle.value());
                            changes.dedup_by_key(|(handle, _)| handle.value());
                            self.tabs[i].scene.bump_entities(&changes);
                        }
                    }
                    pending
                };
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitSolid {
                entity,
                solid,
                history,
            } => {
                let label = self.history_label_from_active_cmd(i, "SOLID");
                let pending = self.begin_undo(i, label, 1, true);
                let handle = self.add_solid_model(entity, *solid, history);
                if !handle.is_null() {
                    self.tabs[i].dirty = true;
                    if let Some(pd) = pending {
                        self.commit_undo_delta(i, pd);
                    }
                }
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
            }
            CmdResult::CommitAndEditText(entity) => {
                let annotative_mleader = matches!(
                    &entity,
                    acadrust::EntityType::MultiLeader(ml) if ml.enable_annotation_scale
                );
                let label = self.history_label_from_active_cmd(i, "ENTITY");
                let delta_safe = self.delta_add_safe(i, &entity) && !annotative_mleader;
                let pending = self.begin_undo(i, label, 1, delta_safe);
                let handle = self.commit_entity_handle(entity);

                if annotative_mleader {
                    let scale_handle = self.tabs[i].scene.creation_annotation_scale_handle();
                    if let (Some(handle), Some(scale_handle)) = (handle, scale_handle) {
                        crate::scene::annotative::create_annotation_context(
                            &mut self.tabs[i].scene.document,
                            handle,
                            scale_handle,
                        );
                        self.tabs[i]
                            .scene
                            .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
                    }
                }

                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.ribbon.deactivate_tool();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
                if let Some(h) = handle {
                    return self.begin_text_edit(h);
                }
            }
            CmdResult::CommitManyAndEditText {
                entities,
                edit_index,
            } => {
                let label = self.history_label_from_active_cmd(i, "ENTITY");
                let delta_safe = entities
                    .iter()
                    .all(|entity| self.delta_add_safe(i, entity));
                let pending = self.begin_undo(i, label, entities.len(), delta_safe);
                let mut edit_handle = None;
                let mut leader_handle = None;
                for (idx, entity) in entities.into_iter().enumerate() {
                    let is_leader = matches!(entity, acadrust::EntityType::Leader(_));
                    let h = self.commit_entity_handle(entity);
                    if idx == edit_index {
                        edit_handle = h;
                    }
                    if is_leader {
                        leader_handle = h;
                    }
                }
                // Link the leader to its annotation so the pair edits as a unit
                // (double-click on the leader resolves to the text entity).
                if let (Some(lh), Some(ah)) = (leader_handle, edit_handle) {
                    let linked = if let Some(acadrust::EntityType::Leader(l)) =
                        self.tabs[i].scene.document.get_entity_mut(lh)
                    {
                        l.annotation_handle = ah;
                        true
                    } else {
                        false
                    };

                    if linked {
                        // The LEADER may already have received its annotation context while
                        // annotation_handle was still NULL. Refresh it now that the MTEXT link
                        // is known so the context represents the finished leader.
                        self.tabs[i]
                            .scene
                            .sync_displayed_annotation_context(lh);

                        self.tabs[i].scene.bump_entities(&[(
                            lh,
                            crate::scene::ChangeKind::Modified,
                        )]);
                    }
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.ribbon.deactivate_tool();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
                if let Some(h) = edit_handle {
                    return self.begin_text_edit(h);
                }
            }
            CmdResult::CreateBlock {
                mut handles,
                name,
                base,
            } => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                if handles.is_empty() {
                    self.command_line
                        .push_info(crate::t!("No editable objects selected.").as_ref());
                    return Task::none();
                }
                self.push_undo_snapshot(i, "BLOCK");
                let ucs = self.tabs[i].ucs_xform();
                let world_to_block = ucs.to_ucs_transform_at(base);
                let block_to_world = ucs.to_wcs_transform_at(base);
                match self.tabs[i]
                    .scene
                    .create_block_from_entities(
                        &handles,
                        &name,
                        &world_to_block,
                        &block_to_world,
                    )
                {
                    Ok(insert_handle) => {
                        self.tabs[i].dirty = true;
                        self.tabs[i].scene.deselect_all();
                        if !insert_handle.is_null() {
                            self.tabs[i].scene.select_entity(insert_handle, false);
                        }
                        self.tabs[i].scene.clear_preview_wire();
                        self.tabs[i].active_cmd = None;
                        self.tabs[i].snap_result = None;
                        self.command_line
                            .push_output(crate::tf!("Block \"{name}\" created.").as_ref());
                        self.refresh_properties();
                    }
                    Err(err) => {
                        self.discard_last_undo_entry(i);
                        self.command_line.push_error(&err);
                        let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                        if let Some(p) = prompt {
                            self.command_line.push_info(&p);
                        }
                    }
                }
            }
            CmdResult::CommitHatch(hatch) => {
                let label = self.history_label_from_active_cmd(i, "HATCH");
                let pending = self.begin_undo(i, label, 1, true);
                let layer = self.tabs[i].active_layer.clone();
                let new_handle = self.tabs[i].scene.add_hatch(hatch, Some(&layer), None);
                if !new_handle.is_null() {
                    self.tabs[i].scene.select_entity(new_handle, true);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.refresh_properties();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitStyledHatch {
                hatch,
                color,
                transparency,
            } => {
                let label = self.history_label_from_active_cmd(i, "HATCH");
                let pending = self.begin_undo(i, label, 1, true);
                let layer = self.tabs[i].active_layer.clone();
                let new_handle = self.tabs[i].scene.add_hatch(
                    hatch,
                    Some(&layer),
                    Some((color, transparency)),
                );
                if !new_handle.is_null() {
                    self.tabs[i].scene.select_entity(new_handle, true);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.refresh_properties();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitHatchWithBoundaries {
                mut hatch,
                boundaries,
                entity_style,
            } => {
                let label = self.history_label_from_active_cmd(i, "HATCH");
                let pending = self.begin_undo(i, label, boundaries.len() + 1, true);
                let mut sources = Vec::with_capacity(boundaries.len());
                for boundary in boundaries {
                    let handles = self
                        .commit_entity_handle(boundary)
                        .into_iter()
                        .collect::<Vec<_>>();
                    sources.push(handles);
                }
                if let Some(paths) = hatch.boundary_paths.as_mut() {
                    for (path, handles) in std::sync::Arc::make_mut(paths).iter_mut().zip(&sources)
                    {
                        path.boundary_handles = handles.clone();
                        path.flags.set_external(!handles.is_empty());
                    }
                }
                hatch.boundary_sources = Some(std::sync::Arc::new(sources));
                let layer = self.tabs[i].active_layer.clone();
                let new_handle =
                    self.tabs[i]
                        .scene
                        .add_hatch(hatch, Some(&layer), entity_style);
                if !new_handle.is_null() {
                    self.tabs[i].scene.select_entity(new_handle, true);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.refresh_properties();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::CommitHatches {
                hatches,
                entity_style,
            } => {
                let label = self.history_label_from_active_cmd(i, "HATCH");
                let pending = self.begin_undo(i, label, hatches.len(), true);
                let layer = self.tabs[i].active_layer.clone();
                for hatch in hatches {
                    let new_handle = self.tabs[i].scene.add_hatch(
                        hatch,
                        Some(&layer),
                        entity_style.clone(),
                    );
                    if !new_handle.is_null() {
                        self.tabs[i].scene.select_entity(new_handle, true);
                    }
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.refresh_properties();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::BatchCopy(mut handles, transforms) => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                if handles.is_empty() {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                let label = self.history_label_from_active_cmd(i, "ARRAY");
                let count = transforms.len();
                // Same gate as COPY (dimension-free), sized by the total number
                // of copies the array will add.
                let delta_safe = self.delta_copy_safe(i, &handles);
                let pending = self.begin_undo(i, label.clone(), handles.len() * count, delta_safe);
                for t in &transforms {
                    self.tabs[i].scene.copy_entities(&handles, t);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                let noun = if count == 1 { "copy" } else { "copies" };
                self.command_line
                    .push_output(crate::tf!("{label}: {count} {noun} created.").as_ref());
                self.refresh_properties();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
            }
            CmdResult::ReplaceMany(replacements, additions) => {
                if let Some((handle, _)) = replacements
                    .iter()
                    .find(|(handle, _)| self.tabs[i].scene.is_layer_locked(*handle))
                {
                    self.reject_locked_edit(i, *handle);
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                let label = self.history_label_from_active_cmd(i, "FILLET");
                let was_catchment = self.tabs[i]
                    .active_cmd
                    .as_ref()
                    .is_some_and(|c| c.name() == "SS_CATCHMENT");
                self.push_undo_snapshot(i, label);
                for (handle, entities) in replacements {
                    self.tabs[i].scene.erase_entities(&[handle]);
                    for entity in entities {
                        let nh = self.tabs[i].scene.add_entity(entity);
                        // Drop a stale *D block on any replaced dimension (#181).
                        if matches!(
                            self.tabs[i].scene.document.get_entity(nh),
                            Some(acadrust::EntityType::Dimension(_))
                        ) {
                            self.tabs[i].scene.invalidate_dim_block_recorded(nh);
                        }
                    }
                }
                for entity in additions {
                    self.tabs[i].scene.add_entity(entity);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                if was_catchment {
                    self.command_line
                        .push_info(crate::t!("Catchment tagged successfully.").as_ref());
                }
                self.refresh_properties();
            }
            CmdResult::ReplaceManyContinue(replacements) => {
                if let Some((handle, _)) = replacements
                    .iter()
                    .find(|(handle, _)| self.tabs[i].scene.is_layer_locked(*handle))
                {
                    self.reject_locked_edit(i, *handle);
                    return Task::none();
                }
                let label = self.history_label_from_active_cmd(i, "TRIM");
                self.push_undo_snapshot(i, label);
                for (handle, entities) in replacements {
                    self.tabs[i].scene.erase_entities(&[handle]);
                    let new_handles: Vec<Handle> = entities
                        .into_iter()
                        .map(|entity| self.tabs[i].scene.add_entity(entity))
                        .collect();
                    if let Some(command) = self.tabs[i].active_cmd.as_mut() {
                        command.on_entity_replaced(handle, &new_handles);
                    }
                }
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].snap_result = None;
                if let Some(prompt) = self.tabs[i]
                    .active_cmd
                    .as_ref()
                    .map(|command| command.prompt())
                {
                    self.command_line.push_info(&prompt);
                }
                self.refresh_properties();
            }
            CmdResult::ReassociateCenterMark { target, source, point } => {
                if self.reject_locked_edit(i, target) {
                    return Task::none();
                }
                self.push_undo_snapshot(i, "CENTERREASSOCIATE");
                if self.tabs[i].scene.reassociate_center_mark(target, source, point) {
                    self.tabs[i].dirty = true;
                    self.command_line.push_output(
                        "CENTERREASSOCIATE: center mark associated.",
                    );
                } else {
                    self.command_line.push_error(
                        "CENTERREASSOCIATE: the selected source is not circular.",
                    );
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.refresh_properties();
            }
            CmdResult::ReplaceEntity(handle, new_entities) => {
                if self.reject_locked_edit(i, handle) {
                    return Task::none();
                }
                // Detect SPLINEDIT sentinel: a single XLine with a magic layer name.
                if new_entities.len() == 1 {
                    if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
                        let op = xl.common.layer.clone();
                        if op.starts_with("__SPLINEDIT_") {
                            let label = self.history_label_from_active_cmd(i, "SPLINEDIT");
                            self.push_undo_snapshot(i, label);
                            crate::modules::draw::modify::splinedit::apply_spline_op(
                                &mut self.tabs[i].scene.document,
                                handle,
                                &op,
                            );
                            self.tabs[i].dirty = true;
                            let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                            if let Some(p) = prompt {
                                self.command_line.push_info(&p);
                            }
                            return Task::none();
                        }
                    }
                }
                // Detect DIMBREAK sentinel.
                if new_entities.len() == 1 {
                    if let acadrust::EntityType::XLine(ref xl) = new_entities[0] {
                        let layer = xl.common.layer.clone();
                        if layer.starts_with("__DIMBREAK__")
                            || layer.starts_with("__DIMBREAK_AUTO__")
                        {
                            // DIMBREAK needs a break-gap field on the dimension
                            // model (not yet present) to store and render the gap.
                            // Report honestly rather than claiming success while
                            // changing nothing. (#181 / DIM-020)
                            self.command_line
                                .push_info(crate::t!("DIMBREAK: not yet implemented — nothing changed.").as_ref());
                            self.tabs[i].active_cmd = None;
                            self.tabs[i].snap_result = None;
                            return Task::none();
                        }
                        if layer.starts_with("__DIMSPACE__") {
                            if let Some(encoded) = layer.strip_prefix("__DIMSPACE__") {
                                apply_dimspace(&mut self.tabs[i].scene, encoded);
                            }
                            self.push_undo_snapshot(i, "DIMSPACE");
                            self.command_line.push_output(crate::t!("DIMSPACE  Spacing adjusted.").as_ref());
                            self.tabs[i].dirty = true;
                            self.tabs[i].active_cmd = None;
                            self.tabs[i].snap_result = None;
                            return Task::none();
                        }
                        if layer.starts_with("__DIMJOG__") {
                            // DIMJOGLINE needs a jog-point field on the dimension
                            // model (not yet present) to store and render the jog.
                            // Report honestly rather than faking success. (DIM-019)
                            self.command_line
                                .push_info(crate::t!("DIMJOGLINE: not yet implemented — nothing changed.").as_ref());
                            self.tabs[i].active_cmd = None;
                            self.tabs[i].snap_result = None;
                            return Task::none();
                        }
                        if layer.starts_with("__MLEADERALIGN__") {
                            if let Some(encoded) = layer.strip_prefix("__MLEADERALIGN__") {
                                apply_mleader_align(&mut self.tabs[i].scene, encoded);
                            }
                            self.push_undo_snapshot(i, "MLEADERALIGN");
                            self.command_line
                                .push_output(crate::t!("MLEADERALIGN  Leaders aligned.").as_ref());
                            self.tabs[i].dirty = true;
                            self.tabs[i].active_cmd = None;
                            self.tabs[i].snap_result = None;
                            return Task::none();
                        }
                        if layer.starts_with("__MLEADERCOLLECT__") {
                            if let Some(encoded) = layer.strip_prefix("__MLEADERCOLLECT__") {
                                apply_mleader_collect(&mut self.tabs[i].scene, encoded);
                            }
                            self.push_undo_snapshot(i, "MLEADERCOLLECT");
                            self.command_line
                                .push_output(crate::t!("MLEADERCOLLECT  Leaders collected.").as_ref());
                            self.tabs[i].dirty = true;
                            self.tabs[i].active_cmd = None;
                            self.tabs[i].snap_result = None;
                            return Task::none();
                        }
                    }
                }

                let label = self.history_label_from_active_cmd(i, "TRIM");
                self.push_undo_snapshot(i, label);
                self.tabs[i].scene.erase_entities(&[handle]);
                let new_handles: Vec<acadrust::Handle> = new_entities
                    .into_iter()
                    .map(|e| self.tabs[i].scene.add_entity(e))
                    .collect();
                // Rebuild replaced dimensions from edited data.
                for &nh in &new_handles {
                    if matches!(
                        self.tabs[i].scene.document.get_entity(nh),
                        Some(acadrust::EntityType::Dimension(_))
                    ) {
                        self.tabs[i].scene.invalidate_dim_block_recorded(nh);
                    }
                }
                if let Some(cmd) = &mut self.tabs[i].active_cmd {
                    cmd.on_entity_replaced(handle, &new_handles);
                }
                self.tabs[i].dirty = true;
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }
            CmdResult::AttreqNeeded { block_name } => {
                // Collect the full AttributeDefinitions owned by this block
                // record so each created attribute keeps its geometry (#255).
                let attdefs: Vec<acadrust::entities::AttributeDefinition> = {
                    let doc = &self.tabs[i].scene.document;
                    if let Some(br) = doc.block_records.get(&block_name) {
                        br.entity_handles
                            .iter()
                            .filter_map(|&h| {
                                if let Some(acadrust::EntityType::AttributeDefinition(ad)) =
                                    doc.get_entity(h)
                                {
                                    Some(ad.clone())
                                } else {
                                    None
                                }
                            })
                            .collect()
                    } else {
                        vec![]
                    }
                };

                if attdefs.is_empty() {
                    // No attribute definitions — commit the INSERT directly.
                    let entity = self.tabs[i]
                        .active_cmd
                        .as_mut()
                        .and_then(|c| c.attreq_take_insert());
                    if let Some(entity) = entity {
                        let insert_name = match &entity {
                            acadrust::EntityType::Insert(ins) => Some(ins.block_name.clone()),
                            _ => None,
                        };
                        let label = self.history_label_from_active_cmd(i, "INSERT");
                        let delta_safe = self.delta_add_safe(i, &entity);
                        let pending = self.begin_undo(i, label, 1, delta_safe);
                        self.commit_entity(entity);
                        self.tabs[i].dirty = true;
                        self.tabs[i].scene.clear_preview_wire();
                        self.tabs[i].active_cmd = None;
                        self.tabs[i].snap_result = None;
                        if let Some(n) = insert_name {
                            self.record_block_insert(&n);
                        }
                        self.restore_pre_cmd_tangent();
                        if let Some(pd) = pending {
                            self.commit_undo_delta(i, pd);
                        }
                    }
                } else {
                    let completed = self.tabs[i]
                        .active_cmd
                        .as_mut()
                        .and_then(|cmd| cmd.attreq_set_attdefs(attdefs));
                    if let Some(entity) = completed {
                        return self.apply_cmd_result(CmdResult::CommitAndExit(entity));
                    }
                    let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                    if let Some(p) = prompt {
                        self.command_line.push_info(&p);
                    }
                }
            }
            CmdResult::CommitLiveEntity(entity) => {
                let label = self.history_label_from_active_cmd(i, "ENTITY");
                let delta_safe = self.delta_add_safe(i, &entity);
                let pending = self.begin_undo(i, label, 1, delta_safe);
                let handle = self.commit_entity_handle(entity);
                self.tabs[i].dirty = true;
                self.tabs[i].scene.clear_preview_wire();
                if let Some(pd) = pending {
                    self.commit_undo_delta(i, pd);
                }
                if let Some(h) = handle {
                    // Keep the live document Arc unique while later vertices
                    // replace the geometry in place. The final Arc is captured
                    // by UpdateLiveEntity when the command completes.
                    self.defer_live_entity_history_after(i, h);
                    if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
                        cmd.set_live_handle(h);
                    }
                }
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }
            CmdResult::UpdateLiveEntity {
                handle,
                entity,
                finish,
            } => {
                let tracks_draw_anchor = matches!(
                    &entity,
                    acadrust::EntityType::Line(_)
                        | acadrust::EntityType::Arc(_)
                        | acadrust::EntityType::LwPolyline(_)
                        | acadrust::EntityType::Polyline(_)
                        | acadrust::EntityType::Polyline2D(_)
                        | acadrust::EntityType::Polyline3D(_)
                );
                // Replace the live entity's geometry in place, preserving its
                // handle and layer (the fresh entity from the command carries
                // defaults — a NULL handle would desync it from the document
                // map key and drop it from rendering / hit-test). No undo
                // snapshot — the create already pushed one, so the whole object
                // reverts as a unit.
                if let Some(old) = self.tabs[i].scene.document.get_entity_mut(handle) {
                    let old_handle = old.as_entity().handle();
                    let layer = old.as_entity().layer().to_string();
                    let mut new = entity;
                    new.as_entity_mut().set_handle(old_handle);
                    new.as_entity_mut().set_layer(layer);
                    *old = new;
                    self.tabs[i]
                        .scene
                        .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
                    self.tabs[i].dirty = true;
                }
                if tracks_draw_anchor {
                    self.tabs[i].last_draw_anchor = Some(handle);
                }
                if finish {
                    self.finish_live_entity_history(i, handle);
                    self.tabs[i].scene.clear_preview_wire();
                    self.tabs[i].active_cmd = None;
                    self.tabs[i].snap_result = None;
                    self.restore_pre_cmd_tangent();
                } else {
                    let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                    if let Some(p) = prompt {
                        self.command_line.push_info(&p);
                    }
                }
            }
            CmdResult::FinalizeLiveEntity(handle) => {
                // The live geometry already matches the command's committed
                // vertices. Close the deferred history image and UI state
                // without publishing a redundant Modified delta.
                self.finish_live_entity_history(i, handle);
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
            }
            CmdResult::RemoveLiveEntity(handle) => {
                // The command backed off below a valid entity (PLINE Undo at
                // one remaining vertex): take the live entity out of the
                // document, drop its provisional history entry and keep
                // prompting. A later second point creates one fresh entry.
                self.tabs[i].scene.erase_entities(&[handle]);
                if self.tabs[i]
                    .last_draw_anchor
                    .is_some_and(|anchor_handle| anchor_handle == handle)
                {
                    self.tabs[i].last_draw_anchor = None;
                }
                self.discard_last_undo_entry(i);
                self.tabs[i].dirty = true;
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }
            cancel @ (CmdResult::Cancel | CmdResult::CancelForSpaceChange) => {
                let space_changed = matches!(cancel, CmdResult::CancelForSpaceChange);
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.restore_pre_cmd_tangent();
                self.command_line.push_info(if space_changed {
                    "Command cancelled because the active drawing space changed."
                } else {
                    "Command cancelled."
                });
            }
            CmdResult::SelectByPath {
                path,
                closed,
                crossing,
            } => {
                // The command picked the path; the hit test lives here, where
                // the camera and the drawing's geometry are. Everything after
                // matches what a lasso does, Remove included.
                let canvas = self.tabs[i].scene.selection.borrow().vp_size;
                let bounds = iced::Rectangle {
                    x: 0.0,
                    y: 0.0,
                    width: canvas.0,
                    height: canvas.1,
                };
                let edit_cam = self.tabs[i]
                    .scene
                    .viewport_edit_frame(canvas)
                    .map(|(cam, _)| cam);
                let (view_rot, eye, all_wires) = self.pick_view(i, &edit_cam, bounds);
                let screen: Vec<iced::Point> = path
                    .iter()
                    .map(|p| {
                        crate::scene::pick::hit_test::world_to_screen(
                            glam::DVec3::new(p[0], p[1], 0.0),
                            view_rot,
                            eye,
                            bounds,
                        )
                    })
                    .collect();
                let handles = self.tabs[i].scene.path_hit_handles(
                    &screen,
                    crossing,
                    !closed,
                    all_wires,
                    view_rot,
                    eye,
                    bounds,
                    |point| self.cursor_model_point(i, &edit_cam, point, bounds),
                );
                if self.select_remove_mode {
                    for h in &handles {
                        self.tabs[i].scene.deselect_entity(*h);
                    }
                } else {
                    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 selected: Vec<Handle> = self.tabs[i]
                    .scene
                    .selected_entities()
                    .into_iter()
                    .map(|(h, _)| h)
                    .collect();
                let count = handles.len();
                self.command_line
                    .push_info(crate::tf!("{count} object(s) found.").as_ref());
                return self.feed_command(StepInput::SelectionComplete(selected));
            }
            CmdResult::Relaunch(cmd, handles) => {
                self.tabs[i].scene.deselect_all();
                for h in &handles {
                    self.tabs[i].scene.select_entity(*h, false);
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                let _ = self.dispatch_command(&cmd);
            }
            CmdResult::Dispatch(cmd) => {
                // End this interactive front-end, then run the assembled command
                // through the normal dispatcher. Selection is left untouched.
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                let _ = self.dispatch_command(&cmd);
            }
            CmdResult::MatchEntityLayer { dest, src } => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                let src_layer = self.tabs[i]
                    .scene
                    .document
                    .get_entity(src)
                    .map(|e| e.common().layer.clone());
                let dest: Vec<_> = dest
                    .into_iter()
                    .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
                    .collect();
                if dest.is_empty() {
                    self.command_line
                        .push_info(crate::t!("No editable objects selected.").as_ref());
                } else if let Some(layer) = src_layer {
                    self.push_undo_snapshot(i, "LAYMATCH");
                    for h in &dest {
                        if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
                            e.as_entity_mut().set_layer(layer.clone());
                        }
                    }
                    // New layer changes the baked by-layer colour/linetype/
                    // lineweight — re-tessellate the moved entities so they
                    // repaint immediately (issue #231 class).
                    self.invalidate_property_targets(i, &dest);
                    self.tabs[i].dirty = true;
                    self.command_line
                        .push_info(crate::tf!("Layer matched to \"{layer}\".").as_ref());
                    self.sync_ribbon_layers();
                } else {
                    self.command_line.push_error(crate::t!("Source object not found.").as_ref());
                }
            }
            CmdResult::MatchProperties { mut dest, src } => {
                dest.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                if dest.is_empty() {
                    return Task::none();
                }
                // The command stays active after each apply so more targets
                // can keep being picked; Enter / Esc ends it (#362).
                let src_clone = self.tabs[i].scene.document.get_entity(src).cloned();
                let src_common = src_clone.as_ref().map(|e| e.common().clone());
                let thickness = src_clone
                    .as_ref()
                    .and_then(crate::scene::view::dispatch::entity_thickness);
                // This application record is the hatch background colour.
                // Keep the outer Option to distinguish "source is not a
                // hatch" from "source hatch has no background".
                let hatch_background_xdata: Option<Option<Vec<acadrust::xdata::XDataValue>>> =
                    match src_clone.as_ref() {
                        Some(acadrust::EntityType::Hatch(h)) => Some(
                            h.common
                                .extended_data
                                .get_record("HATCHBACKGROUNDCOLOR")
                                .map(|r| r.values.clone()),
                        ),
                        _ => None,
                    };
                // Dimension-style overrides ride the ACAD record, identified
                // by a leading DSTYLE string. Matching replicates that payload
                // (or clears the destination when the source has none).
                let dstyle_xdata: Option<Vec<(i16, acadrust::xdata::XDataValue)>> = src_clone
                    .as_ref()
                    .filter(|e| {
                        matches!(
                            e,
                            acadrust::EntityType::Dimension(_) | acadrust::EntityType::Leader(_)
                        )
                    })
                    .map(|e| crate::entities::dim_override::pairs(&e.common().extended_data));

                if let Some(common) = src_common {
                    self.push_undo_snapshot(i, "MATCHPROP");
                    for h in &dest {
                        let mut is_dim = false;
                        let mut is_hatch = false;
                        if let Some(e) = self.tabs[i].scene.document.get_entity_mut(*h) {
                            e.as_entity_mut().set_layer(common.layer.clone());
                            crate::scene::view::dispatch::apply_color(e, common.color);
                            crate::scene::view::dispatch::apply_line_weight(e, common.line_weight);
                            {
                                let dst_common = e.common_mut();
                                dst_common.linetype = common.linetype.clone();
                                dst_common.linetype_handle = common.linetype_handle;
                                dst_common.linetype_scale = common.linetype_scale;
                                dst_common.transparency = common.transparency.clone();
                                dst_common.color_book_handle = common.color_book_handle;
                                dst_common.full_visual_style_handle =
                                    common.full_visual_style_handle;
                                dst_common.face_visual_style_handle =
                                    common.face_visual_style_handle;
                                dst_common.edge_visual_style_handle =
                                    common.edge_visual_style_handle;
                                dst_common.material_flags = common.material_flags;
                                dst_common.material_handle = common.material_handle;
                                dst_common.shadow_flags = common.shadow_flags;
                                dst_common.plotstyle_flags = common.plotstyle_flags;
                                dst_common.plotstyle_handle = common.plotstyle_handle;
                            }
                            if let Some(value) = thickness {
                                crate::scene::view::dispatch::set_entity_thickness(e, value);
                            }
                            if let Some(se) = &src_clone {
                                is_dim = matches!(e, acadrust::EntityType::Dimension(_));
                                is_hatch = matches!(e, acadrust::EntityType::Hatch(_));
                                match_special_props(se, e);
                            }
                        }
                        if is_hatch {
                            if let Some(values) = &hatch_background_xdata {
                                crate::scene::view::dispatch::set_entity_xdata(
                                    &mut self.tabs[i].scene.document,
                                    *h,
                                    "HATCHBACKGROUNDCOLOR",
                                    values.clone(),
                                );
                            }
                        }
                        // Dim-style overrides follow the style for dimension /
                        // leader destinations — through set_entity_xdata so no
                        // stale raw record survives.
                        if dstyle_xdata.is_some()
                            || matches!(
                                self.tabs[i].scene.document.get_entity(*h),
                                Some(
                                    acadrust::EntityType::Dimension(_)
                                        | acadrust::EntityType::Leader(_)
                                )
                            )
                        {
                            if matches!(
                                self.tabs[i].scene.document.get_entity(*h),
                                Some(
                                    acadrust::EntityType::Dimension(_)
                                        | acadrust::EntityType::Leader(_)
                                )
                            ) && matches!(
                                src_clone,
                                Some(
                                    acadrust::EntityType::Dimension(_)
                                        | acadrust::EntityType::Leader(_)
                                )
                            ) {
                                crate::entities::dim_override::replace(
                                    &mut self.tabs[i].scene.document,
                                    *h,
                                    dstyle_xdata.clone().unwrap_or_default(),
                                );
                            }
                        }
                        // A restyled dimension renders from its baked *D block —
                        // drop the stale block so the new style shows (#398).
                        if is_dim {
                            self.tabs[i].scene.invalidate_dim_block_recorded(*h);
                        }
                        // Hatch fills render from a prebuilt model (#415).
                        self.tabs[i].scene.refresh_fill_model(*h);
                    }
                    self.tabs[i].dirty = true;
                    // Color / linetype / lineweight are baked into the cached
                    // wires at tessellation time; re-tessellate only the matched
                    // objects instead of rebuilding a large drawing.
                    let changes: Vec<_> = dest
                        .iter()
                        .copied()
                        .map(|handle| (handle, crate::scene::ChangeKind::Modified))
                        .collect();
                    self.tabs[i].scene.bump_entities(&changes);
                    self.refresh_properties();
                    self.command_line
                        .push_info(crate::tf!("Properties matched to {} object(s).", dest.len()).as_ref());
                    // Clear the consumed target selection and keep prompting.
                    self.tabs[i].scene.deselect_all();
                    if let Some(cmd) = &self.tabs[i].active_cmd {
                        self.command_line.push_info(&cmd.prompt());
                    }
                } else {
                    self.command_line.push_error(crate::t!("Source object not found.").as_ref());
                    self.tabs[i].active_cmd = None;
                    self.tabs[i].snap_result = None;
                    self.tabs[i].scene.clear_preview_wire();
                }
            }
            CmdResult::PasteClipboard { base_pt } => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                if self.clipboard.is_empty() {
                    self.command_line.push_error(crate::t!("Clipboard is empty.").as_ref());
                } else {
                    let delta = base_pt - self.clipboard_base;
                    let translate = crate::command::EntityTransform::Translate(delta);
                    self.push_undo_snapshot(i, "PASTECLIP");
                    let count = self.clipboard.len();
                    let by_index = self.finalize_paste(i, Some(translate));
                    self.tabs[i].scene.deselect_all();
                    for h in by_index.iter().copied().filter(|h| !h.is_null()) {
                        self.tabs[i].scene.select_entity(h, false);
                    }
                    self.tabs[i].dirty = true;
                    // Surface any layers the paste brought in (cross-drawing)
                    // in the layer manager and the layer dropdown.
                    self.refresh_layer_panel();
                    self.refresh_properties();
                    self.command_line
                        .push_info(crate::tf!("{count} object(s) pasted.").as_ref());
                }
            }
            CmdResult::CreateGroup { mut handles, name } => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                if handles.is_empty() {
                    return Task::none();
                }
                let undo = self.begin_group_undo(i, "GROUP");
                self.tabs[i].scene.create_group(name.clone(), handles);
                self.tabs[i].dirty = true;
                self.commit_group_undo(i, undo);
                self.command_line
                    .push_info(crate::tf!("Group \"{}\" created.", name).as_ref());
            }
            CmdResult::DeleteGroups { mut handles } => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                if handles.is_empty() {
                    return Task::none();
                }
                let undo = self.begin_group_undo(i, "UNGROUP");
                let count = self.tabs[i].scene.delete_groups_containing(&handles);
                self.tabs[i].dirty = true;
                self.commit_group_undo(i, undo);
                if count > 0 {
                    self.command_line
                        .push_info(crate::tf!("{} group(s) dissolved.", count).as_ref());
                } else {
                    self.command_line
                        .push_info(crate::t!("No groups found for selected objects.").as_ref());
                }
            }
            CmdResult::VpLayerUpdate {
                vp_handle,
                freeze,
                thaw,
            } => {
                // Resolve layer names → handles, then update frozen_layers on the viewport(s).
                // vp_handle == Handle::NULL means "apply to all viewports in current layout".
                let freeze_handles: Vec<Handle> = freeze
                    .iter()
                    .filter_map(|name| {
                        self.tabs[i]
                            .scene
                            .document
                            .layers
                            .iter()
                            .find(|l| l.name.eq_ignore_ascii_case(name))
                            .map(|l| l.handle)
                    })
                    .collect();
                let thaw_handles: Vec<Handle> = thaw
                    .iter()
                    .filter_map(|name| {
                        self.tabs[i]
                            .scene
                            .document
                            .layers
                            .iter()
                            .find(|l| l.name.eq_ignore_ascii_case(name))
                            .map(|l| l.handle)
                    })
                    .collect();

                let mut frozen_count = 0usize;
                let mut thawed_count = 0usize;

                // Collect target viewport handles
                let target_handles: Vec<Handle> = if vp_handle == acadrust::Handle::NULL {
                    // All viewports in current layout block
                    let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
                    self.tabs[i]
                        .scene
                        .document
                        .entities()
                        .filter(|e| {
                            e.common().owner_handle == block_handle
                                && matches!(e, acadrust::EntityType::Viewport(_))
                        })
                        .map(|e| e.common().handle)
                        .collect()
                } else {
                    vec![vp_handle]
                };

                for &target_handle in &target_handles {
                    if let Some(acadrust::EntityType::Viewport(vp)) =
                        self.tabs[i].scene.document.get_entity_mut(target_handle)
                    {
                        for h in &freeze_handles {
                            if !vp.frozen_layers.contains(h) {
                                vp.frozen_layers.push(*h);
                                frozen_count += 1;
                            }
                        }
                        for h in &thaw_handles {
                            let before = vp.frozen_layers.len();
                            vp.frozen_layers.retain(|fh| fh != h);
                            if vp.frozen_layers.len() < before {
                                thawed_count += 1;
                            }
                        }
                    }
                }

                if frozen_count > 0 || thawed_count > 0 {
                    self.push_undo_snapshot(i, "VPLAYER");
                    self.tabs[i].dirty = true;
                    if frozen_count > 0 {
                        self.command_line.push_info(crate::tf!(
                            "VPLAYER: {frozen_count} layer(s) frozen in viewport."
                        ).as_ref());
                    }
                    if thawed_count > 0 {
                        self.command_line.push_info(crate::tf!(
                            "VPLAYER: {thawed_count} layer(s) thawed in viewport."
                        ).as_ref());
                    }
                    // Sync layer panel so VP freeze columns update immediately.
                    let doc_layers = self.tabs[i].scene.document.layers.clone();
                    let vp_info = self.tabs[i].scene.viewport_list();
                    self.tabs[i]
                        .layers
                        .sync_with_viewports(&doc_layers, vp_info);
                }

                // Show updated prompt (command stays active for more operations).
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }

            CmdResult::ZoomToWindow { p1, p2 } => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.tabs[i].scene.remember_current_view();
                self.tabs[i]
                    .scene
                    .zoom_to_window(p1.as_vec3(), p2.as_vec3());
                self.command_line.push_output(crate::t!("Zoom Window").as_ref());
            }
            CmdResult::Measurement(msg) => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                self.command_line.push_output(&msg);
            }
            CmdResult::ReportMeasurement(msg) => {
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.refresh_area_preview(i);
                self.command_line.push_output(&msg);
                if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
                    self.command_line.push_info(&prompt);
                }
            }
            CmdResult::ReportMeasurementAndDeselect(msg) => {
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.deselect_all();
                self.tabs[i].scene.clear_preview_wire();
                self.refresh_area_preview(i);
                self.refresh_properties();
                self.command_line.push_output(&msg);
                if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
                    self.command_line.push_info(&prompt);
                }
            }
            CmdResult::DeselectAndContinue => {
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.deselect_all();
                self.tabs[i].scene.clear_preview_wire();
                self.refresh_area_preview(i);
                self.refresh_properties();
                if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
                    self.command_line.push_info(&prompt);
                }
            }
            CmdResult::AlignSelected {
                mut handles,
                src1,
                dst1,
                angle_rad,
                scale,
            } => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                if handles.is_empty() {
                    self.tabs[i].active_cmd = None;
                    self.tabs[i].snap_result = None;
                    self.tabs[i].scene.clear_preview_wire();
                    self.restore_pre_cmd_tangent();
                } else {
                    let label = self.history_label_from_active_cmd(i, "ALIGN");
                    // A chain of transform_entities on the same handles — the
                    // recording keeps each entity's first (pre-align) image, so
                    // it's delta-safe like a single transform.
                    let pending = self.begin_undo(i, label, handles.len(), true);
                    // Step 1: translate so src1 is at origin
                    self.tabs[i].scene.transform_entities(
                        &handles,
                        &crate::command::EntityTransform::Translate(-src1),
                    );
                    // Step 2: uniform scale (only when != 1)
                    if (scale - 1.0).abs() > 1e-4 {
                        self.tabs[i].scene.transform_entities(
                            &handles,
                            &crate::command::EntityTransform::Scale {
                                center: glam::DVec3::ZERO,
                                factor: scale,
                            },
                        );
                    }
                    // Step 3: rotate in the XY plane by angle_rad
                    if angle_rad.abs() > 1e-4 {
                        self.tabs[i].scene.transform_entities(
                            &handles,
                            &crate::command::EntityTransform::Rotate {
                                center: glam::DVec3::ZERO,
                                axis: glam::DVec3::Z,
                                angle_rad,
                            },
                        );
                    }
                    // Step 4: translate to dst1
                    self.tabs[i].scene.transform_entities(
                        &handles,
                        &crate::command::EntityTransform::Translate(dst1),
                    );
                    self.tabs[i].dirty = true;
                    self.tabs[i].scene.deselect_all();
                    for h in &handles {
                        self.tabs[i].scene.select_entity(*h, false);
                    }
                    self.tabs[i].scene.clear_preview_wire();
                    self.tabs[i].active_cmd = None;
                    self.tabs[i].snap_result = None;
                    self.restore_pre_cmd_tangent();
                    self.command_line.push_output(crate::t!("ALIGN: applied.").as_ref());
                    self.refresh_properties();
                    if let Some(pd) = pending {
                        self.commit_undo_delta(i, pd);
                    }
                }
            }
            CmdResult::LengthenEntity {
                handle,
                pick_pt,
                mode,
            } => {
                if self.reject_locked_edit(i, handle) {
                    return Task::none();
                }
                use crate::modules::draw::modify::lengthen::lengthen_entity;
                let result = self.tabs[i]
                    .scene
                    .document
                    .get_entity(handle)
                    .and_then(|e| lengthen_entity(e, pick_pt.as_vec3(), &mode));
                match result {
                    Some(new_entity) => {
                        let label = self.history_label_from_active_cmd(i, "LENGTHEN");
                        self.push_undo_snapshot(i, label);
                        self.tabs[i].scene.erase_entities(&[handle]);
                        self.tabs[i].scene.add_entity(new_entity);
                        self.tabs[i].dirty = true;
                        self.command_line.push_output(crate::t!("LENGTHEN: applied.").as_ref());
                        self.refresh_properties();
                    }
                    None => {
                        self.command_line
                            .push_error(crate::t!("LENGTHEN: entity type not supported.").as_ref());
                    }
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }
            CmdResult::DivideEntity { handle, n } => {
                use crate::modules::draw::inquiry::divide::divide_entity;
                let pts = self.tabs[i]
                    .scene
                    .document
                    .get_entity(handle)
                    .map(|e| divide_entity(e, n))
                    .unwrap_or_default();
                let count = pts.len();
                if count > 0 {
                    self.push_undo_snapshot(i, "DIVIDE");
                    for p in pts {
                        self.tabs[i].scene.add_entity(p);
                    }
                    self.tabs[i].dirty = true;
                    self.command_line
                        .push_output(crate::tf!("DIVIDE: {count} point(s) placed.").as_ref());
                } else {
                    self.command_line
                        .push_error(crate::t!("DIVIDE: entity type not supported or N < 2.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }
            CmdResult::MeasureEntity {
                handle,
                segment_length,
            } => {
                use crate::modules::draw::inquiry::divide::measure_entity;
                let pts = self.tabs[i]
                    .scene
                    .document
                    .get_entity(handle)
                    .map(|e| measure_entity(e, segment_length))
                    .unwrap_or_default();
                let count = pts.len();
                if count > 0 {
                    self.push_undo_snapshot(i, "MEASURE");
                    for p in pts {
                        self.tabs[i].scene.add_entity(p);
                    }
                    self.tabs[i].dirty = true;
                    self.command_line
                        .push_output(crate::tf!("MEASURE: {count} point(s) placed.").as_ref());
                } else {
                    self.command_line
                        .push_error(crate::t!("MEASURE: entity type not supported or distance too large.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }
            CmdResult::PeditOp { handle, op } => {
                if self.reject_locked_edit(i, handle) {
                    return Task::none();
                }
                use crate::modules::draw::modify::pedit::{
                    apply_pedit, convert_to_polyline, PeditOp,
                };
                match &op {
                    // The convert replaces the entity (new handle).
                    PeditOp::ConvertToPolyline => {
                        let converted = self.tabs[i]
                            .scene
                            .document
                            .get_entity(handle)
                            .and_then(convert_to_polyline);
                        match converted {
                            Some(pl) => {
                                return self
                                    .apply_cmd_result(CmdResult::ReplaceEntity(handle, vec![pl]));
                            }
                            None => self
                                .command_line
                                .push_error(crate::t!("PEDIT: cannot convert this entity.").as_ref()),
                        }
                    }
                    _ => {
                        // Snapshot BEFORE the mutation — an after-the-fact
                        // snapshot records the changed state and undo no-ops.
                        self.push_undo_snapshot(i, "PEDIT");
                        let changed = self.tabs[i]
                            .scene
                            .document
                            .get_entity_mut(handle)
                            .map(|e| apply_pedit(e, &op))
                            .unwrap_or(false);
                        if changed {
                            self.tabs[i].dirty = true;
                            // Repaint immediately (wide-band fill included).
                            self.tabs[i].scene.refresh_fill_model(handle);
                            self.tabs[i]
                                .scene
                                .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]);
                            self.command_line.push_output(crate::t!("PEDIT: applied.").as_ref());
                            self.refresh_properties();
                        } else {
                            self.discard_last_undo_entry(i);
                            self.command_line
                                .push_error(crate::t!("PEDIT: operation not applicable to this entity.").as_ref());
                        }
                    }
                }
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }
            CmdResult::JoinEntities(handles) => {
                if let Some(handle) = handles
                    .iter()
                    .find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
                {
                    self.reject_locked_edit(i, *handle);
                    return Task::none();
                }
                use crate::modules::draw::modify::join::join_entities;
                let pairs: Vec<_> = handles
                    .iter()
                    .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).map(|e| (h, e)))
                    .collect();
                match join_entities(&pairs) {
                    Some((to_remove, merged)) => {
                        let label = self.history_label_from_active_cmd(i, "JOIN");
                        self.push_undo_snapshot(i, label);
                        self.tabs[i].scene.erase_entities(&to_remove);
                        let count_in = to_remove.len();
                        let count_out = merged.len();
                        for e in merged {
                            self.tabs[i].scene.add_entity(e);
                        }
                        self.tabs[i].dirty = true;
                        self.tabs[i].scene.clear_preview_wire();
                        self.tabs[i].active_cmd = None;
                        self.tabs[i].snap_result = None;
                        self.restore_pre_cmd_tangent();
                        self.command_line.push_output(crate::tf!(
                            "JOIN: {count_in} object(s) joined into {count_out}."
                        ).as_ref());
                        self.refresh_properties();
                    }
                    None => {
                        self.tabs[i].active_cmd = None;
                        self.tabs[i].snap_result = None;
                        self.tabs[i].scene.clear_preview_wire();
                        self.restore_pre_cmd_tangent();
                        self.command_line.push_error(
                            "JOIN: objects don't form a single connected chain, or contain an unsupported type / tilted arc.",
                        );
                    }
                }
            }
            CmdResult::BreakEntity { handle, p1, p2 } => {
                if self.reject_locked_edit(i, handle) {
                    return Task::none();
                }
                use crate::modules::draw::modify::break_cmd::break_entity;
                let replacement = self.tabs[i]
                    .scene
                    .document
                    .get_entity(handle)
                    .and_then(|e| break_entity(e, p1, p2));
                match replacement {
                    Some(frags) => {
                        let label = self.history_label_from_active_cmd(i, "BREAK");
                        self.push_undo_snapshot(i, label);
                        self.tabs[i].scene.erase_entities(&[handle]);
                        let count = frags.len();
                        for e in frags {
                            self.tabs[i].scene.add_entity(e);
                        }
                        self.tabs[i].dirty = true;
                        self.tabs[i].scene.clear_preview_wire();
                        self.tabs[i].active_cmd = None;
                        self.tabs[i].snap_result = None;
                        self.restore_pre_cmd_tangent();
                        self.command_line
                            .push_output(crate::tf!("BREAK: {} fragment(s).", count).as_ref());
                        self.refresh_properties();
                    }
                    None => {
                        self.tabs[i].active_cmd = None;
                        self.tabs[i].snap_result = None;
                        self.tabs[i].scene.clear_preview_wire();
                        self.restore_pre_cmd_tangent();
                        self.command_line
                            .push_error(crate::t!("BREAK: entity type not supported.").as_ref());
                    }
                }
            }
            CmdResult::SetPlotWindow { p1, p2 } => {
                let layout_name = self.tabs[i].scene.current_layout.clone();
                if layout_name == "Model" {
                    // Model space: remember the window (world X/Y) for the plot dialog.
                    let x0 = p1.x.min(p2.x);
                    let y0 = p1.y.min(p2.y);
                    let x1 = p1.x.max(p2.x);
                    let y1 = p1.y.max(p2.y);
                    self.plot_window = Some((x0, y0, x1, y1));
                    self.command_line
                        .push_output(crate::tf!("Plot window: {x0:.2},{y0:.2} to {x1:.2},{y1:.2}").as_ref());
                    // Pick window closed the plot dialog so the viewport could
                    // receive the two clicks — bring the dialog back with the
                    // window now active.
                    self.plot_dialog.area = "Window".to_string();
                    self.active_modal = Some(super::ModalKind::Plot);
                } else {
                    // PLOTWINDOW always describes the plotted layout. In MSPACE
                    // the command points are model coordinates, so map them back
                    // through the active floating viewport first.
                    let p1 = self.tabs[i].scene.model_to_paper(p1);
                    let p2 = self.tabs[i].scene.model_to_paper(p2);
                    let x1 = p1.x.min(p2.x);
                    let y1 = p1.y.min(p2.y);
                    let x2 = p1.x.max(p2.x);
                    let y2 = p1.y.max(p2.y);
                    self.plot_window = Some((x1, y1, x2, y2));
                    self.command_line.push_output(crate::tf!(
                        "Plot window: {x1:.2},{y1:.2} to {x2:.2},{y2:.2}"
                    ).as_ref());
                    self.plot_dialog.area = "Window".to_string();
                    self.active_modal = Some(super::ModalKind::Plot);
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }
            CmdResult::QuickPrint(handles) => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                return self.on_quick_print_handles(handles);
            }
            CmdResult::StretchWindow {
                mut handles,
                windows,
            } => {
                // Accumulate every entity touched by any crossing window. Keep STRETCH
                // in its selection stage; Enter is what advances to the base point.
                {
                    let scene = &self.tabs[i].scene;

                    for (win_min, win_max) in &windows {
                        handles.extend(
                            scene
                                .interaction_handles_in_world_aabb([
                                    win_min.x,
                                    win_min.y,
                                    win_max.x,
                                    win_max.y,
                                ])
                                .into_iter()
                                .filter(|&handle| !scene.is_layer_locked(handle)),
                        );
                    }
                }

                handles.sort_unstable_by_key(|handle| handle.value());
                handles.dedup();

                if handles.is_empty() {
                    self.command_line.push_output(
                        crate::t!("STRETCH: nothing crosses the window.").as_ref(),
                    );
                }

                use crate::command::CadCommand;
                use crate::modules::draw::modify::stretch::StretchCommand;

                let wires = self.tabs[i].scene.wire_models_for(&handles);

                let cmd = StretchCommand::with_windows(
                    handles,
                    wires,
                    windows,
                );

                self.command_line.push_info(&CadCommand::prompt(&cmd));
                self.tabs[i].active_cmd = Some(Box::new(cmd));
            }
            CmdResult::StretchEntities {
                mut handles,
                windows,
                delta,
            } => {
                handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
                if handles.is_empty() {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                let structural = handles.iter().any(|handle| {
                    matches!(
                        self.tabs[i].scene.document.get_entity(*handle),
                        Some(acadrust::EntityType::Dimension(_))
                    )
                });
                let pending = self.begin_undo(i, "STRETCH", handles.len(), !structural);
                let mut count = 0usize;
                let mut changed_handles = Vec::new();

                // Helper: is DXF point (x, y) inside the world-space window?
                // Drawing plane is world XY (= DXF XY).
                let in_win = |x: f64, y: f64| -> bool {
                    windows.iter().any(|(win_min, win_max)| {
                        x >= win_min.x
                            && x <= win_max.x
                            && y >= win_min.y
                            && y <= win_max.y
                    })
                };

                let dx = delta.x as f64;
                let dy = delta.y as f64; // drawing plane is world XY
                let dz = delta.z as f64;

                // Dimensions whose points moved — their baked *D block is
                // stale afterwards and must be dropped (see #398 / #372).
                let mut stretched_dims: Vec<acadrust::Handle> = Vec::new();
                for handle in &handles {
                    let before = self.tabs[i].scene.document.get_entity_arc(*handle);
                    let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) else {
                        continue;
                    };
                    let mut stretched = false;
                    match entity {
                        acadrust::EntityType::Line(l) => {
                            let s_in = in_win(l.start.x, l.start.y);
                            let e_in = in_win(l.end.x, l.end.y);
                            if s_in {
                                l.start.x += dx;
                                l.start.y += dy;
                                l.start.z += dz;
                                stretched = true;
                            }
                            if e_in {
                                l.end.x += dx;
                                l.end.y += dy;
                                l.end.z += dz;
                                stretched = true;
                            }
                        }
                        acadrust::EntityType::LwPolyline(p) => {
                            let Some(mut world) =
                                crate::entities::curve::lwpolyline_world_xy(p)
                            else {
                                continue;
                            };
                            for v in &mut world.vertices {
                                if in_win(v.location.x, v.location.y) {
                                    v.location.x += dx;
                                    v.location.y += dy;
                                    stretched = true;
                                }
                            }
                            if stretched {
                                *p = world;
                            }
                        }
                        acadrust::EntityType::Polyline2D(p) => {
                            for v in &mut p.vertices {
                                if in_win(v.location.x, v.location.y) {
                                    v.location.x += dx;
                                    v.location.y += dy;
                                    stretched = true;
                                }
                            }
                        }
                        acadrust::EntityType::Polyline(p) => {
                            for v in &mut p.vertices {
                                if in_win(v.location.x, v.location.z) {
                                    v.location.x += dx;
                                    v.location.z += dy;
                                    stretched = true;
                                }
                            }
                        }
                        acadrust::EntityType::Arc(a) => {
                            if in_win(a.center.x, a.center.y) {
                                a.center.x += dx;
                                a.center.y += dy;
                                a.center.z += dz;
                                stretched = true;
                            }
                        }
                        acadrust::EntityType::Circle(c) => {
                            if in_win(c.center.x, c.center.y) {
                                c.center.x += dx;
                                c.center.y += dy;
                                c.center.z += dz;
                                stretched = true;
                            }
                        }
                        acadrust::EntityType::Ellipse(e) => {
                            if in_win(e.center.x, e.center.y) {
                                e.center.x += dx;
                                e.center.y += dy;
                                e.center.z += dz;
                                stretched = true;
                            }
                        }
                        acadrust::EntityType::Insert(ins) => {
                            if in_win(ins.insert_point.x, ins.insert_point.y) {
                                ins.insert_point.x += dx;
                                ins.insert_point.y += dy;
                                ins.insert_point.z += dz;
                                stretched = true;
                            }
                        }
                        acadrust::EntityType::Text(t) => {
                            if in_win(t.insertion_point.x, t.insertion_point.y) {
                                t.insertion_point.x += dx;
                                t.insertion_point.y += dy;
                                t.insertion_point.z += dz;
                                stretched = true;
                            }
                        }
                        acadrust::EntityType::MText(t) => {
                            if in_win(t.insertion_point.x, t.insertion_point.y) {
                                t.insertion_point.x += dx;
                                t.insertion_point.y += dy;
                                t.insertion_point.z += dz;
                                stretched = true;
                            }
                        }
                        acadrust::EntityType::Viewport(vp) => {
                            stretched = windows.iter().any(|(win_min, win_max)| {
                                crate::entities::viewport::stretch(
                                    vp,
                                    *win_min,
                                    *win_max,
                                    delta,
                                )
                            });
                        }
                        acadrust::EntityType::Dimension(dim) => {
                            use acadrust::entities::Dimension;
                            // Move every definition point that falls inside the
                            // window — the same points the grips expose — then
                            // refresh the stored measurement so the value tracks
                            // the stretched geometry.
                            let mv = |p: &mut acadrust::types::Vector3| {
                                if in_win(p.x, p.y) {
                                    p.x += dx;
                                    p.y += dy;
                                    p.z += dz;
                                    true
                                } else {
                                    false
                                }
                            };
                            match dim {
                                Dimension::Linear(d) => {
                                    stretched |= mv(&mut d.first_point);
                                    stretched |= mv(&mut d.second_point);
                                    stretched |= mv(&mut d.definition_point);
                                }
                                Dimension::Aligned(d) => {
                                    stretched |= mv(&mut d.first_point);
                                    stretched |= mv(&mut d.second_point);
                                    stretched |= mv(&mut d.definition_point);
                                }
                                Dimension::Radius(d) => {
                                    stretched |= mv(&mut d.angle_vertex);
                                    stretched |= mv(&mut d.definition_point);
                                }
                                Dimension::Diameter(d) => {
                                    stretched |= mv(&mut d.angle_vertex);
                                    stretched |= mv(&mut d.definition_point);
                                }
                                Dimension::Angular2Ln(d) => {
                                    stretched |= mv(&mut d.angle_vertex);
                                    stretched |= mv(&mut d.first_point);
                                    stretched |= mv(&mut d.second_point);
                                    stretched |= mv(&mut d.definition_point);
                                }
                                Dimension::Angular3Pt(d) => {
                                    stretched |= mv(&mut d.angle_vertex);
                                    stretched |= mv(&mut d.first_point);
                                    stretched |= mv(&mut d.second_point);
                                    stretched |= mv(&mut d.definition_point);
                                }
                                Dimension::Ordinate(d) => {
                                    stretched |= mv(&mut d.feature_location);
                                    stretched |= mv(&mut d.leader_endpoint);
                                }
                                Dimension::Arc(d) => {
                                    stretched |= mv(&mut d.definition_point);
                                    stretched |= mv(&mut d.first_extension_point);
                                    stretched |= mv(&mut d.second_extension_point);
                                    stretched |= mv(&mut d.center_point);
                                    if d.has_leader {
                                        stretched |= mv(&mut d.first_leader_point);
                                        stretched |= mv(&mut d.second_leader_point);
                                    }
                                }
                                Dimension::LargeRadial(d) => {
                                    stretched |= mv(&mut d.definition_point);
                                    stretched |= mv(&mut d.chord_point);
                                    stretched |= mv(&mut d.override_center);
                                    stretched |= mv(&mut d.jog_point);
                                }
                            }
                            // Pinned text follows too; the zero sentinel means
                            // "auto placement" and must not be captured by a
                            // window that happens to cover the origin.
                            let t = dim.base().text_middle_point;
                            if t.x * t.x + t.y * t.y + t.z * t.z > 1e-16 {
                                let mut t = t;
                                if mv(&mut t) {
                                    dim.base_mut().text_middle_point = t;
                                    stretched = true;
                                }
                            }
                            if stretched {
                                dim.base_mut().actual_measurement = dim.measurement();
                                stretched_dims.push(*handle);
                            }
                        }
                        _ => {
                            // Generic: move entire entity (treat as block-level)
                            stretched = false; // skip generic types
                        }
                    }
                    if stretched {
                        if let Some(before) = before {
                            self.tabs[i].scene.record_undo_before(*handle, Some(before));
                        }
                        self.tabs[i].scene.mark_entity_dirty(*handle);
                        changed_handles.push(*handle);
                        count += 1;
                    }
                }
                // A stretched dimension renders through its baked *D block when
                // one exists (file roundtrip) — drop it so tessellation falls
                // back to the live points and the next save re-bakes. (#372)
                for h in stretched_dims {
                    self.tabs[i].scene.invalidate_dim_block_recorded(h);
                }

                // Geometry was edited in place via get_entity_mut, which the
                // scene's tessellation cache doesn't observe. Re-tessellate the
                // moved entities so the viewport reflects the stretch right away
                // instead of only on the next unrelated redraw. See #95.
                if count > 0 {
                    let changes: Vec<_> = changed_handles
                        .iter()
                        .map(|&handle| (handle, crate::scene::ChangeKind::Modified))
                        .collect();
                    self.tabs[i].scene.bump_entities(&changes);
                }
                self.tabs[i].dirty = true;
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                self.command_line
                    .push_output(crate::tf!("STRETCH: {count} entity(ies) stretched.").as_ref());
                self.refresh_properties();
                if let Some(pending) = pending {
                    self.commit_undo_delta(i, pending);
                }
            }
            // ── EXTRUDE ────────────────────────────────────────────────────
            CmdResult::ExtrudeEntity {
                handle,
                height,
                color: _,
            } => {
                if self.reject_locked_edit(i, handle) {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                use crate::modules::insert::solid3d_cmds::empty_solid3d;
                use crate::scene::model::sweep_model;

                let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
                if let Some(entity) = entity_opt {
                    if let Some(solid) = sweep_model::extruded(&entity, height) {
                        let history = crate::scene::model::solid_history::brep_op(&solid);
                        let pending = self.begin_undo(i, "EXTRUDE", 1, true);
                        let created = self.add_solid_model(empty_solid3d(), solid, history);
                        if !created.is_null() {
                            self.tabs[i].dirty = true;
                            self.command_line
                                .push_output(crate::t!("EXTRUDE: solid created.").as_ref());
                            if let Some(pd) = pending {
                                self.commit_undo_delta(i, pd);
                            }
                        }
                    } else {
                        self.command_line.push_error(crate::t!("EXTRUDE: could not build profile. Select a closed 2D entity (Circle, LwPolyline, etc.).").as_ref());
                    }
                } else {
                    self.command_line.push_error(crate::t!("EXTRUDE: entity not found.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }

            CmdResult::PresspullEntity {
                handle,
                pick,
                distance,
                drag,
                color,
            } => {
                if matches!(
                    self.tabs[i].scene.document.get_entity(handle),
                    Some(acadrust::EntityType::Solid3D(_))
                ) {
                    let task = self.solid_face_presspull(handle, pick, distance, drag);
                    self.tabs[i].active_cmd = None;
                    self.tabs[i].snap_result = None;
                    self.tabs[i].scene.clear_preview_wire();
                    self.restore_pre_cmd_tangent();
                    return task;
                }
                if self.reject_locked_edit(i, handle) {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                use crate::modules::insert::solid3d_cmds::empty_solid3d;
                use crate::scene::model::{solid_history, sweep_model};

                let result = self.tabs[i].scene.document.get_entity(handle).and_then(|entity| {
                    let distance = match drag {
                        Some(point) => sweep_model::projected_drag(entity, pick, point)?,
                        None => distance,
                    };
                    sweep_model::extruded(entity, distance)
                });
                if let Some(solid) = result {
                    let pending = self.begin_undo(i, "PRESSPULL", 1, true);
                    let history = solid_history::brep_op(&solid);
                    let created = self.add_solid_model(empty_solid3d(), solid, history);
                    let _ = color;
                    if !created.is_null() {
                        self.tabs[i].dirty = true;
                        self.command_line
                            .push_output(crate::t!("PRESSPULL: solid created.").as_ref());
                        if let Some(delta) = pending {
                            self.commit_undo_delta(i, delta);
                        }
                    }
                } else {
                    self.command_line.push_error(
                        crate::t!("PRESSPULL: select a closed profile or planar solid face.")
                            .as_ref(),
                    );
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }

            // ── REVOLVE ────────────────────────────────────────────────────
            CmdResult::RevolveEntity {
                handle,
                axis_start,
                axis_end,
                angle_deg,
                color: _,
            } => {
                if self.reject_locked_edit(i, handle) {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                use crate::modules::insert::solid3d_cmds::empty_solid3d;
                use crate::scene::model::sweep_model;

                let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
                if let Some(entity) = entity_opt {
                    let result = sweep_model::revolved(
                        &entity,
                        [
                            axis_start.x as f64,
                            axis_start.y as f64,
                            axis_start.z as f64,
                        ],
                        [axis_end.x as f64, axis_end.y as f64, axis_end.z as f64],
                        (angle_deg as f64).to_radians(),
                    );
                    if let Some(solid) = result {
                        let history = crate::scene::model::solid_history::brep_op(&solid);
                        let pending = self.begin_undo(i, "REVOLVE", 1, true);
                        let created = self.add_solid_model(empty_solid3d(), solid, history);
                        if !created.is_null() {
                            self.tabs[i].dirty = true;
                            self.command_line.push_output(
                                crate::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref(),
                            );
                            if let Some(pd) = pending {
                                self.commit_undo_delta(i, pd);
                            }
                        }
                    } else {
                        self.command_line
                            .push_error(crate::t!("REVOLVE: could not revolve profile.").as_ref());
                    }
                } else {
                    self.command_line
                        .push_error(crate::t!("REVOLVE: entity not found.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }
            // ── SWEEP ─────────────────────────────────────────────────────
            CmdResult::SweepEntity {
                profile_handle,
                path_handle,
                color: _,
            } => {
                if self.reject_locked_edit(i, profile_handle)
                    || self.reject_locked_edit(i, path_handle)
                {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                use crate::modules::insert::solid3d_cmds::empty_solid3d;
                use crate::scene::model::{solid_history, sweep_model};

                let profile_ent = self.tabs[i]
                    .scene
                    .document
                    .get_entity(profile_handle)
                    .cloned();
                let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned();
                let result = profile_ent
                    .zip(path_ent)
                    .and_then(|(profile, path)| sweep_model::swept(&profile, &path));

                if let Some(solid) = result {
                    let pending = self.begin_undo(i, "SWEEP", 1, true);
                    let history = solid_history::brep_op(&solid);
                    let created = self.add_solid_model(empty_solid3d(), solid, history);
                    if !created.is_null() {
                        self.tabs[i].dirty = true;
                        self.command_line
                            .push_output(crate::t!("SWEEP: solid created.").as_ref());
                        if let Some(pd) = pending {
                            self.commit_undo_delta(i, pd);
                        }
                    }
                } else {
                    self.command_line.push_error(crate::t!("SWEEP: could not sweep the profile along the path.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }

            // ── LOFT ──────────────────────────────────────────────────────
            CmdResult::LoftEntities { handles, color: _ } => {
                if let Some(handle) = handles
                    .iter()
                    .find(|handle| self.tabs[i].scene.is_layer_locked(**handle))
                    .copied()
                {
                    self.reject_locked_edit(i, handle);
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                use crate::modules::insert::solid3d_cmds::empty_solid3d;
                use crate::scene::model::{solid_history, sweep_model};

                let profiles: Vec<acadrust::EntityType> = handles
                    .iter()
                    .filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle).cloned())
                    .collect();
                if let Some(solid) = sweep_model::lofted(&profiles) {
                    let pending = self.begin_undo(i, "LOFT", 1, true);
                    let history = solid_history::brep_op(&solid);
                    let created = self.add_solid_model(empty_solid3d(), solid, history);
                    if !created.is_null() {
                        self.tabs[i].dirty = true;
                        self.command_line
                            .push_output(crate::t!("LOFT: solid created.").as_ref());
                        if let Some(pd) = pending {
                            self.commit_undo_delta(i, pd);
                        }
                    }
                } else {
                    self.command_line.push_error(crate::t!("LOFT: select at least two closed profiles.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }

            CmdResult::SolidEdgeBlend {
                handle,
                pick,
                value,
                fillet,
            } => {
                let task = self.solid_edge_blend(handle, pick, value, fillet);
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                return task;
            }

            CmdResult::SolidSubtract { bases, cutters } => {
                let task = self.solid_subtract(&bases, &cutters);
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                return task;
            }

            CmdResult::HatcheditApply {
                handle,
                name,
                scale,
                angle,
                operation,
            } => {
                if self.reject_locked_edit(i, handle) {
                    return Task::none();
                }
                if !matches!(
                    self.tabs[i].scene.document.get_entity(handle),
                    Some(acadrust::EntityType::Hatch(_))
                ) {
                    self.command_line
                        .push_error(crate::t!("HATCHEDIT: hatch entity not found.").as_ref());
                } else {
                    use crate::command::HatchEditOperation;
                    if matches!(
                        &operation,
                        HatchEditOperation::DrawOrderFront | HatchEditOperation::DrawOrderBack
                    ) {
                        let command = if matches!(&operation, HatchEditOperation::DrawOrderFront) {
                            "DRAWORDER FRONT"
                        } else {
                            "DRAWORDER BACK"
                        };
                        self.tabs[i].scene.deselect_all();
                        self.tabs[i].scene.select_entity(handle, false);
                        self.tabs[i].active_cmd = None;
                        return self.dispatch_view(command, i).unwrap_or_else(Task::none);
                    }
                    self.push_undo_snapshot(i, "HATCHEDIT");
                    match operation {
                        HatchEditOperation::Update {
                            origin,
                            disassociate,
                            style,
                            annotative,
                        } => {
                            if let Some(acadrust::EntityType::Hatch(hatch)) =
                                self.tabs[i].scene.document.get_entity_mut(handle)
                            {
                                if !name.is_empty() && name != hatch.pattern.name {
                                    if let Some(entry) =
                                        crate::scene::model::hatch_patterns::find(&name)
                                    {
                                        let old_origin = hatch
                                            .pattern
                                            .lines
                                            .first()
                                            .map(|line| line.base_point);
                                        let mut pattern = crate::scene::model::hatch_patterns::build_dxf_pattern(entry);
                                        crate::entities::hatch::scale_pattern_geometry(
                                            &mut pattern,
                                            scale.max(1.0e-6) as f64,
                                        );
                                        crate::entities::hatch::rotate_pattern_geometry(
                                            &mut pattern,
                                            (angle as f64).to_radians(),
                                        );
                                        if let (Some(old), Some(new)) = (
                                            old_origin,
                                            pattern.lines.first().map(|line| line.base_point),
                                        ) {
                                            crate::entities::hatch::translate_pattern_geometry(
                                                &mut pattern,
                                                old.x - new.x,
                                                old.y - new.y,
                                            );
                                        }
                                        hatch.pattern = pattern;
                                        hatch.is_solid = matches!(
                                            entry.gpu,
                                            crate::scene::model::hatch_model::HatchPattern::Solid
                                        );
                                        hatch.pattern_type =
                                            acadrust::entities::HatchPatternType::Predefined;
                                        hatch.gradient_color.enabled = false;
                                    }
                                } else {
                                    let requested_scale = scale.max(1.0e-6) as f64;
                                    if hatch.pattern_scale > 1.0e-12 {
                                        let factor = requested_scale / hatch.pattern_scale;
                                        crate::entities::hatch::scale_pattern_geometry(
                                            &mut hatch.pattern,
                                            factor,
                                        );
                                    }
                                    let requested_angle = (angle as f64).to_radians();
                                    let delta = requested_angle - hatch.pattern_angle;
                                    crate::entities::hatch::rotate_pattern_geometry(
                                        &mut hatch.pattern,
                                        delta,
                                    );
                                }
                                hatch.pattern_scale = scale.max(1.0e-6) as f64;
                                hatch.pattern_angle = (angle as f64).to_radians();
                                if let Some((x, y)) = origin {
                                    if let Some(current) =
                                        hatch.pattern.lines.first().map(|line| line.base_point)
                                    {
                                        crate::entities::hatch::translate_pattern_geometry(
                                            &mut hatch.pattern,
                                            x - current.x,
                                            y - current.y,
                                        );
                                    }
                                }
                                if disassociate {
                                    for path in &mut hatch.paths {
                                        path.boundary_handles.clear();
                                        path.flags.set_external(false);
                                    }
                                    hatch.is_associative = false;
                                }
                                if let Some(style) = style {
                                    hatch.style = style;
                                }
                            }
                            if let Some(value) = annotative {
                                crate::scene::annotative::set_entity_annotative(
                                    &mut self.tabs[i].scene.document,
                                    handle,
                                    value,
                                );
                                if value {
                                    if let Some(scale_handle) =
                                        self.tabs[i].scene.creation_annotation_scale_handle()
                                    {
                                        crate::scene::annotative::create_annotation_context(
                                            &mut self.tabs[i].scene.document,
                                            handle,
                                            scale_handle,
                                        );
                                    }
                                }
                            }
                            self.tabs[i].scene.bump_entities(&[(
                                handle,
                                crate::scene::ChangeKind::Modified,
                            )]);
                        }
                        HatchEditOperation::AddBoundaries(handles) => {
                            self.tabs[i]
                                .scene
                                .edit_hatch_boundary_handles(handle, &handles, true);
                        }
                        HatchEditOperation::RemoveBoundaries(handles) => {
                            self.tabs[i]
                                .scene
                                .edit_hatch_boundary_handles(handle, &handles, false);
                        }
                        HatchEditOperation::RecreateBoundary => {
                            let source = self.tabs[i].scene.document.get_entity(handle).cloned();
                            if let Some(acadrust::EntityType::Hatch(source)) = source {
                                let storage = crate::entities::curve::ocs_plane(
                                    source.normal,
                                    source.elevation,
                                );
                                let plane = crate::command::WorkingPlane::new(
                                    glam::DVec3::from_array(storage.origin),
                                    glam::DVec3::from_array(storage.x_axis),
                                    glam::DVec3::from_array(storage.y_axis),
                                );
                                let rings = crate::scene::hatch_boundary_rings(&source);
                                let entities = crate::scene::boundary_entities(&rings, plane);
                                let mut handles = Vec::new();
                                for entity in entities {
                                    if let Some(boundary) = self.commit_entity_handle(entity) {
                                        handles.push(boundary);
                                    }
                                }
                                if let Some(acadrust::EntityType::Hatch(hatch)) =
                                    self.tabs[i].scene.document.get_entity_mut(handle)
                                {
                                    for (path, boundary) in
                                        hatch.paths.iter_mut().zip(handles.iter().copied())
                                    {
                                        path.boundary_handles = vec![boundary];
                                        path.flags.set_external(true);
                                    }
                                    hatch.is_associative = !handles.is_empty();
                                }
                                self.tabs[i].scene.bump_entities(&[(
                                    handle,
                                    crate::scene::ChangeKind::Modified,
                                )]);
                            }
                        }
                        HatchEditOperation::Separate => {
                            let source = self.tabs[i].scene.document.get_entity(handle).cloned();
                            if let Some(acadrust::EntityType::Hatch(hatch)) = source {
                                let groups = crate::scene::separated_hatch_path_groups(&hatch);
                                if groups.len() > 1 {
                                    for paths in groups {
                                        let mut separated = hatch.clone();
                                        separated.common.handle = acadrust::Handle::NULL;
                                        separated.paths = paths;
                                        separated.is_associative = separated
                                            .paths
                                            .iter()
                                            .any(|path| !path.boundary_handles.is_empty());
                                        self.tabs[i]
                                            .scene
                                            .add_entity(acadrust::EntityType::Hatch(separated));
                                    }
                                    self.tabs[i].scene.erase_entities(&[handle]);
                                }
                            }
                        }
                        HatchEditOperation::DrawOrderFront
                        | HatchEditOperation::DrawOrderBack => unreachable!(),
                    }
                    self.tabs[i].dirty = true;
                    self.command_line
                        .push_output(crate::t!("HATCHEDIT: hatch updated.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }
            CmdResult::OpenMTextEditor {
                pos,
                handle,
                initial,
                height,
            } => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.open_mtext_editor(pos, handle, &initial, height);
            }
            CmdResult::SuspendForMTextInput {
                pos,
                initial,
                height,
            } => {
                self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                self.command_mtext_input = true;
                self.pending_command_editor_text = None;
                self.open_mtext_editor(pos, None, &initial, height);
            }
            CmdResult::OpenTextEditor {
                pos,
                handle,
                initial,
                height,
            } => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.open_text_inline(
                    pos,
                    handle,
                    &initial,
                    height,
                    super::text_inline::TextEntityField::Text,
                    None,
                );
            }
            CmdResult::SuspendForTextInput { pos, entity } => {
                self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                let height = entity.height;
                self.open_text_inline(
                    pos,
                    None,
                    "",
                    height,
                    super::text_inline::TextEntityField::Text,
                    Some(entity),
                );
            }
            CmdResult::EditTextEntity { handle } => {
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                self.ribbon.deactivate_tool();
                return self.begin_text_edit(handle);
            }
            CmdResult::SuspendForTextEdit { handle } => {
                let is_editable =
                    crate::app::text_inline::can_edit_text(handle, &self.tabs[i].scene.document);
                if !is_editable {
                    self.command_line
                        .push_error(crate::t!("TEXTEDIT: selected entity is not text.").as_ref());
                    let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                    if let Some(p) = prompt {
                        self.command_line.push_info(&p);
                    }
                    return Task::none();
                }
                let cmd = self.tabs[i].active_cmd.take();
                self.tabs[i].suspended_cmd = cmd;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
                self.ribbon.deactivate_tool();
                return self.begin_text_edit(handle);
            }
            CmdResult::UndoDocument => {
                let active = self.tabs[i].active_cmd.take();
                self.undo_active_tab();
                self.tabs[i].active_cmd = active;
                let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
                if let Some(p) = prompt {
                    self.command_line.push_info(&p);
                }
            }
            CmdResult::SetTexteditMode(val) => {
                self.texteditmode = val;
                let display_val = if val { 1 } else { 0 };
                self.command_line
                    .push_output(crate::tf!("TEXTEDITMODE set to {display_val}").as_ref());
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
            }
            CmdResult::DdeditEntity { handle, new_text } => {
                if self.reject_locked_edit(i, handle) {
                    self.tabs[i].active_cmd = None;
                    return Task::none();
                }
                self.push_undo_snapshot(i, "DDEDIT");
                let mut updated = false;
                let mut is_dim = false;
                if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) {
                    match entity {
                        acadrust::EntityType::Text(t) => {
                            t.value = new_text;
                            updated = true;
                        }
                        acadrust::EntityType::MText(t) => {
                            t.value = new_text;
                            updated = true;
                        }
                        acadrust::EntityType::AttributeDefinition(a) => {
                            a.default_value = new_text;
                            updated = true;
                        }
                        acadrust::EntityType::AttributeEntity(a) => {
                            a.set_value(new_text);
                            updated = true;
                        }
                        acadrust::EntityType::Dimension(d) => {
                            // Empty string resets to auto-measured value; otherwise set override.
                            let base = d.base_mut();
                            base.text = new_text;
                            updated = true;
                            is_dim = true;
                        }
                        _ => {}
                    }
                }
                if is_dim {
                    // The edited override changed the dimension text; drop its
                    // stale *D block so save re-bakes it. (#181)
                    self.tabs[i].scene.invalidate_dim_block_recorded(handle);
                }
                if updated {
                    self.tabs[i].dirty = true;
                    self.command_line.push_output(crate::t!("DDEDIT: text updated.").as_ref());
                } else {
                    self.discard_last_undo_entry(i);
                    self.command_line
                        .push_error(crate::t!("DDEDIT: entity type not supported.").as_ref());
                }
                self.tabs[i].active_cmd = None;
                self.tabs[i].snap_result = None;
                self.tabs[i].scene.clear_preview_wire();
                self.restore_pre_cmd_tangent();
            }
        }
        // When no command is running the ribbon tool button still has to
        // visually deactivate. Keyboard focus is assigned below to whichever
        // editor currently owns typed input.
        if self.tabs[i].active_cmd.is_none() {
            self.ribbon.deactivate_tool();
        }
        // The rich text canvas owns keyboard editing itself. Leaving the
        // hidden command input focused would make it consume Left/Right before
        // the editor can handle them.
        if self.mtext_editor.is_some() {
            return self.unfocus_widgets();
        }
        // The in-place TEXT editor needs keyboard focus on its own field.
        if self.text_inline.is_some() {
            return iced::widget::operation::focus(iced::widget::Id::new(
                super::view::TEXT_INLINE_ID,
            ));
        }
        self.focus_cmd_input()
    }

    /// Restore the tangent-snap / ortho state that was in effect before the command started.
    /// Recreate clipboard-dependency records (layer / linetype / text + dim
    /// style) in tab `i`'s document for any the copied entities reference but
    /// this drawing doesn't already have. Each recreated record gets a fresh
    /// handle from the target document so it can't collide with an existing
    /// one. No-op for same-document pastes (the records already exist). (#129)
    pub(super) fn merge_dependencies(
        &mut self,
        i: usize,
        deps: &crate::app::ClipboardDeps,
    ) {
        use acadrust::TableEntry;
        if deps.is_empty() {
            return;
        }
        let doc = &mut self.tabs[i].scene.document;
        for rec in &deps.layers {
            if !doc.layers.contains(rec.name()) {
                let mut r = rec.clone();
                r.set_handle(doc.allocate_handle());
                let _ = doc.layers.add(r);
            }
        }
        for rec in &deps.linetypes {
            if !doc.line_types.contains(rec.name()) {
                let mut r = rec.clone();
                r.set_handle(doc.allocate_handle());
                let _ = doc.line_types.add(r);
            }
        }
        for rec in &deps.text_styles {
            if !doc.text_styles.contains(rec.name()) {
                let mut r = rec.clone();
                r.set_handle(doc.allocate_handle());
                let _ = doc.text_styles.add(r);
            }
        }
        for rec in &deps.dim_styles {
            if !doc.dim_styles.contains(rec.name()) {
                let mut r = rec.clone();
                r.set_handle(doc.allocate_handle());
                let _ = doc.dim_styles.add(r);
            }
        }
    }

    pub(super) fn merge_clipboard_deps(&mut self, i: usize) {
        let deps = self.clipboard_deps.clone();
        self.merge_dependencies(i, &deps);
    }

    /// Recreate any block definition the pasted INSERTs reference but tab
    /// `i`'s document lacks (cross-drawing paste), so the block reference
    /// renders its geometry instead of nothing. No-op for same-document
    /// pastes. (#135)
    pub(super) fn merge_clipboard_blocks(&mut self, i: usize) {
        if self.clipboard_deps.blocks.is_empty() {
            return;
        }
        let blocks = self.clipboard_deps.blocks.clone();
        for def in blocks {
            if self.tabs[i]
                .scene
                .document
                .block_records
                .get(&def.name)
                .is_some()
            {
                continue;
            }
            self.tabs[i]
                .scene
                .define_block_raw(&def.name, def.base_point, def.entities);
        }
    }

    /// Shared paste finalize for every paste path (PASTECLIP, PASTEORIG):
    /// recreate the clipboard's dependency records and block definitions, add
    /// each entity with fresh handles (optionally transformed), recreate each
    /// entity's xdictionary graph (XCLIP filters etc.), and tessellate pasted
    /// solids. Returns the new handles, index-aligned with the clipboard
    /// (NULL where an add failed). Keeping this in one place means a new
    /// cross-drawing concern is wired once, not re-implemented per command.
    pub(super) fn finalize_paste(
        &mut self,
        i: usize,
        translate: Option<crate::command::EntityTransform>,
    ) -> Vec<Handle> {
        self.merge_clipboard_deps(i);
        self.merge_clipboard_blocks(i);
        let by_index: Vec<Handle> = self
            .clipboard
            .clone()
            .into_iter()
            .map(|mut entity| {
                if let Some(t) = &translate {
                    crate::scene::view::dispatch::apply_transform(&mut entity, t);
                }
                // A dimension draws from its baked `*D` block (baked in WCS), so
                // give the paste its own transformed copy of that block and
                // re-point it — otherwise the pasted dimension renders at the
                // source location instead of the paste point. The block was
                // snapshotted into the clipboard at copy time, so this works
                // cross-drawing too. Mirrors the in-drawing copy. (#290, #161)
                if let acadrust::EntityType::Dimension(d) = &entity {
                    let bn = d.base().block_name.clone();
                    if !bn.trim().is_empty() {
                        let subs = self
                            .clipboard_deps
                            .dim_blocks
                            .iter()
                            .find(|b| b.name.eq_ignore_ascii_case(&bn))
                            .map(|def| def.entities.clone());
                        if let Some(subs) = subs {
                            let bt = translate.clone().unwrap_or(
                                crate::command::EntityTransform::Translate(glam::DVec3::ZERO),
                            );
                            if let Some(new_bn) =
                                self.tabs[i].scene.define_transformed_block(&subs, &bt)
                            {
                                if let acadrust::EntityType::Dimension(d) = &mut entity {
                                    d.base_mut().block_name = new_bn;
                                }
                            }
                        }
                    }
                }
                self.tabs[i].scene.add_entity_clone(entity)
            })
            .collect();
        let annotation_delta = match translate {
            Some(crate::command::EntityTransform::Translate(delta)) => delta,
            _ => glam::DVec3::ZERO,
        };
        self.merge_clipboard_ext_objects(i, &by_index, annotation_delta);
                // Source handles stored in the clipboard map one-to-one to the freshly
        // pasted handles. Use that map to reconnect LEADER -> copied annotation.
        let mut handle_map = rustc_hash::FxHashMap::default();

        for (source, &copied) in self.clipboard.iter().zip(by_index.iter()) {
            if !copied.is_null() {
                handle_map.insert(source.common().handle, copied);
            }
        }

        let leader_links: Vec<(Handle, Handle)> = self
            .clipboard
            .iter()
            .filter_map(|source| {
                let acadrust::EntityType::Leader(leader) = source else {
                    return None;
                };

                let copied_leader = handle_map.get(&source.common().handle).copied()?;
                let copied_annotation = handle_map
                    .get(&leader.annotation_handle)
                    .copied()
                    .unwrap_or(Handle::NULL);

                Some((copied_leader, copied_annotation))
            })
            .collect();

        for (leader_handle, annotation_handle) in leader_links {
            if let Some(acadrust::EntityType::Leader(leader)) =
                self.tabs[i].scene.document.get_entity_mut(leader_handle)
            {
                leader.annotation_handle = annotation_handle;
            }

            let _ = self.tabs[i]
                .scene
                .sync_displayed_annotation_context(leader_handle);
        }
        // Recreate any group whose whole membership was copied, so a pasted
        // group stays grouped — cross-drawing too, since the groups were
        // snapshotted into the clipboard at copy time. `by_index` is aligned
        // with `self.clipboard`, so the source handle of each pasted entity maps
        // its clipboard clone to its new handle. Same shared `recreate_groups`
        // the in-drawing COPY path uses. (#440)
        if !self.clipboard_deps.groups.is_empty() {
            let groups = self.clipboard_deps.groups.clone();
            self.tabs[i].scene.recreate_groups(groups, &handle_map);
        }
        // Incremental: `add_entity` already tessellated every pasted top-level
        // solid, and existing document solids are still cached — so only newly
        // introduced block-definition solids need building. The full rebuild
        // would clear and re-tessellate the entire document (every solid in the
        // drawing) on each paste, which is what made a large paste stall.
        self.tabs[i].scene.populate_missing_meshes_from_document();
        by_index
    }

    /// Recreate the extension-dictionary object graph (XCLIP spatial filters,
    /// attached XRecords, …) captured for each copied entity, cloning every
    /// object into this document with fresh handles, remapping all internal
    /// references, and re-pointing the pasted entity's `xdictionary_handle` at
    /// the new root. `by_index` is the paste's new entity handles, aligned with
    /// the clipboard order (NULL where the add failed). No-op without captures.
    pub(super) fn merge_clipboard_ext_objects(
        &mut self,
        i: usize,
        by_index: &[Handle],
        annotation_delta: glam::DVec3,
    ) {
        if self.clipboard_deps.ext_objects.is_empty() {
            return;
        }
        let captures = self.clipboard_deps.ext_objects.clone();
        let doc = &mut self.tabs[i].scene.document;
        for cap in &captures {
            let Some(&new_entity) = by_index.get(cap.entity_index) else {
                continue;
            };
            if new_entity.is_null() {
                continue;
            }
            if let Some(new_root) = recreate_ext_subtree(doc, cap, Some(new_entity)) {
                if let Some(e) = doc.get_entity_mut(new_entity) {
                    e.common_mut().xdictionary_handle = Some(new_root);
                }
                crate::scene::annotative::translate_annotation_contexts(
                    doc,
                    new_entity,
                    annotation_delta,
                );
            }
        }
        // The wires were tessellated before the filters existed; refresh only
        // the freshly-pasted entities whose clip object graph was attached.
        let changes: Vec<_> = by_index
            .iter()
            .copied()
            .filter(|handle| !handle.is_null())
            .map(|handle| (handle, crate::scene::ChangeKind::Modified))
            .collect();
        self.tabs[i].scene.bump_entities(&changes);
    }

    /// Recreate the captured xdictionary subtrees in this document (fresh
    /// handles, remapped references) WITHOUT an added host entity, returning
    /// `entity_index → new xdictionary root`. Used by PASTEBLOCK, which folds
    /// the clipboard into a new block definition: the caller stamps each new
    /// root onto the matching entity's `xdictionary_handle` before defining the
    /// block, so the block's nested insert keeps its XCLIP filter.
    pub(super) fn recreate_clipboard_ext_roots(
        &mut self,
        i: usize,
    ) -> std::collections::HashMap<usize, Handle> {
        let mut out = std::collections::HashMap::new();
        if self.clipboard_deps.ext_objects.is_empty() {
            return out;
        }
        let captures = self.clipboard_deps.ext_objects.clone();
        let doc = &mut self.tabs[i].scene.document;
        for cap in &captures {
            if let Some(new_root) = recreate_ext_subtree(doc, cap, None) {
                out.insert(cap.entity_index, new_root);
            }
        }
        out
    }

    fn restore_pre_cmd_tangent(&mut self) {
        if let Some(was_on) = self.pre_cmd_tangent.take() {
            if !was_on {
                self.snapper.enabled.remove(&crate::snap::SnapType::Tangent);
            }
        }
        if self.rect_suppressed_ortho {
            self.rect_suppressed_ortho = false;
            self.ortho_mode = true;
            self.polar_mode = false;
        }
    }
}

/// Clone one captured xdictionary subtree into `doc` with fresh handles,
/// remapping every internal reference (and the owning entity, when known),
/// returning the new root handle. `allocate_handle` advances the document's
/// handle counter — `next_handle()` only peeks, so reusing it would hand every
/// object the same handle and collapse the dictionary chain.
fn recreate_ext_subtree(
    doc: &mut acadrust::CadDocument,
    cap: &crate::app::ClipExtObjects,
    entity_handle: Option<Handle>,
) -> Option<Handle> {
    use std::collections::HashMap;
    let mut remap: HashMap<Handle, Handle> = HashMap::new();
    if let Some(eh) = entity_handle {
        remap.insert(cap.src_entity_handle, eh);
    }
    for (old, scale) in &cap.annotation_scales {
        let target = crate::scene::annotative::ensure_scale_object(doc, scale);
        remap.insert(*old, target);
    }
    for (old, _) in &cap.objects {
        remap.insert(*old, doc.allocate_handle());
    }
    for (old, obj) in &cap.objects {
        let mut obj = obj.clone();
        let new_h = remap[old];
        remap_object(&mut obj, new_h, &remap);
        doc.objects.insert(new_h, obj);
    }
    remap.get(&cap.root).copied()
}

/// Replace references to a clipboard entity inside one recreated extension
/// dictionary graph after its final block-owned handle becomes known.
pub(crate) fn remap_ext_subtree_reference(
    doc: &mut acadrust::CadDocument,
    root: Handle,
    source_entity: Handle,
    target_entity: Handle,
) {
    use acadrust::objects::ObjectType;
    use rustc_hash::FxHashSet;
    use std::collections::HashMap;

    let remap = HashMap::from([(source_entity, target_entity)]);
    let mut seen = FxHashSet::default();
    let mut pending = vec![root];
    while let Some(handle) = pending.pop() {
        if handle.is_null() || !seen.insert(handle) {
            continue;
        }
        let children = match doc.objects.get(&handle) {
            Some(ObjectType::Dictionary(dictionary)) => {
                let mut children: Vec<_> =
                    dictionary.entries.iter().map(|(_, child)| *child).collect();
                if let Some(extension) = dictionary.xdictionary_handle {
                    children.push(extension);
                }
                children
            }
            Some(ObjectType::DictionaryWithDefault(dictionary)) => {
                let mut children: Vec<_> =
                    dictionary.entries.iter().map(|(_, child)| *child).collect();
                children.push(dictionary.default_handle);
                children
            }
            _ => Vec::new(),
        };
        pending.extend(children);
        if let Some(mut object) = doc.objects.remove(&handle) {
            remap_object(&mut object, handle, &remap);
            doc.objects.insert(handle, object);
        }
    }
}

/// Rewrite a cloned extension-dictionary object onto fresh handles: set its own
/// handle to `new_handle` and remap its owner and any handle references it holds
/// through `remap` (a handle still in the source space stays unchanged, which is
/// correct for cross-references that point outside the captured subtree).
fn remap_object(
    obj: &mut acadrust::objects::ObjectType,
    new_handle: acadrust::Handle,
    remap: &std::collections::HashMap<acadrust::Handle, acadrust::Handle>,
) {
    use acadrust::objects::ObjectType;
    let map = |h: acadrust::Handle| remap.get(&h).copied().unwrap_or(h);
    match obj {
        ObjectType::Dictionary(d) => {
            d.handle = new_handle;
            d.owner = map(d.owner);
            for (_, h) in d.entries.iter_mut() {
                *h = map(*h);
            }
            if let Some(x) = d.xdictionary_handle.as_mut() {
                *x = map(*x);
            }
            for r in d.reactors.iter_mut() {
                *r = map(*r);
            }
        }
        ObjectType::DictionaryWithDefault(d) => {
            d.handle = new_handle;
            d.owner = map(d.owner);
            for (_, h) in d.entries.iter_mut() {
                *h = map(*h);
            }
            d.default_handle = map(d.default_handle);
        }
        ObjectType::DictionaryVariable(v) => {
            v.handle = new_handle;
            v.owner_handle = map(v.owner_handle);
        }
        ObjectType::SpatialFilter(s) => {
            s.handle = new_handle;
            s.owner = map(s.owner);
        }
        ObjectType::XRecord(x) => {
            x.handle = new_handle;
            x.owner = map(x.owner);
            for entry in &mut x.entries {
                if let acadrust::objects::XRecordValue::Handle(handle) = &mut entry.value {
                    *handle = map(*handle);
                }
            }
        }
        ObjectType::Group(g) => {
            g.handle = new_handle;
            g.owner = map(g.owner);
            for h in g.entities.iter_mut() {
                *h = map(*h);
            }
        }
        ObjectType::ObjectContextData(context) => {
            context.handle = new_handle;
            context.owner_handle = map(context.owner_handle);
            for reactor in &mut context.reactors {
                *reactor = map(*reactor);
            }
            if let Some(dictionary) = &mut context.xdictionary_handle {
                *dictionary = map(*dictionary);
            }
            context.scale = map(context.scale);
            match &mut context.kind {
                acadrust::objects::ObjectContextKind::Dim(dimension) => {
                    dimension.block = map(dimension.block);
                }
                acadrust::objects::ObjectContextKind::HatchView(hatch) => {
                    hatch.view = map(hatch.view);
                }
                acadrust::objects::ObjectContextKind::MTextAttribute(attribute) => {
                    if let Some(embedded) = &mut attribute.context {
                        embedded.owner_handle = map(embedded.owner_handle);
                        for reactor in &mut embedded.reactors {
                            *reactor = map(*reactor);
                        }
                        if let Some(dictionary) = &mut embedded.xdictionary_handle {
                            *dictionary = map(*dictionary);
                        }
                        embedded.scale = map(embedded.scale);
                    }
                }
                acadrust::objects::ObjectContextKind::MLeader(mleader) => {
                    if let Some(handle) = &mut mleader.text_style_handle {
                        *handle = map(*handle);
                    }
                    if let Some(handle) = &mut mleader.block_content_handle {
                        *handle = map(*handle);
                    }
                    if let Some(handle) = &mut mleader.scale_handle {
                        *handle = map(*handle);
                    }
                    for root in &mut mleader.leader_roots {
                        for line in &mut root.lines {
                            if let Some(handle) = &mut line.line_type_handle {
                                *handle = map(*handle);
                            }
                            if let Some(handle) = &mut line.arrowhead_handle {
                                *handle = map(*handle);
                            }
                        }
                    }
                }
                _ => {}
            }
        }
        // Other leaf object kinds don't appear in an entity xdictionary; if one
        // does, it's inserted with the fresh handle below via the caller's key,
        // but its internal owner is left as-is (best effort).
        _ => {}
    }
}

// ── DIMSPACE helper ───────────────────────────────────────────────────────────

/// Parse `base_val,h1;h2;...;hN,spacing` and adjust parallel dimension positions.
fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
    // Format: "<base_handle>,<h1>;<h2>;...;<hN>,<spacing>"
    let parts: Vec<&str> = encoded.splitn(3, ',').collect();
    if parts.len() < 3 {
        return;
    }
    let base_val: u64 = parts[0].parse().unwrap_or(0);
    let other_vals: Vec<u64> = parts[1]
        .split(';')
        .filter_map(|s| s.parse::<u64>().ok())
        .collect();
    let spacing: f64 = parts[2].parse().unwrap_or(0.0);

    use acadrust::entities::Dimension;
    let base_h = acadrust::Handle::from(base_val);
    // Base dim: the perpendicular direction (from its rotation / axis) and the
    // dim line's perpendicular coordinate. Spacing steps each parallel dim along
    // this perp IN THE DRAWING PLANE — offsetting Z had no effect on the dim
    // line, which is computed from def·perp with perp.z = 0. (#181 / DIM-021)
    let (perp, base_coord) = match scene.document.get_entity(base_h) {
        Some(acadrust::EntityType::Dimension(Dimension::Linear(d))) => {
            let (s, c) = d.rotation.sin_cos();
            let perp = (-s, c);
            let dp = d.definition_point;
            (perp, dp.x * perp.0 + dp.y * perp.1)
        }
        Some(acadrust::EntityType::Dimension(Dimension::Aligned(d))) => {
            let dx = d.second_point.x - d.first_point.x;
            let dy = d.second_point.y - d.first_point.y;
            let len = (dx * dx + dy * dy).sqrt().max(1e-12);
            let perp = (-dy / len, dx / len);
            let dp = d.definition_point;
            (perp, dp.x * perp.0 + dp.y * perp.1)
        }
        _ => return,
    };

    let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing };
    let mut changes = Vec::new();
    for (idx, &hv) in other_vals.iter().enumerate() {
        let h = acadrust::Handle::from(hv);
        let target = base_coord + effective_spacing * (idx + 1) as f64;
        let mut changed = false;
        if let Some(acadrust::EntityType::Dimension(dim)) = scene.document.get_entity_mut(h) {
            // Slide this dim's definition point along perp so its perpendicular
            // coordinate equals `target`; update both the struct field (render)
            // and base (save).
            let slide = |p: &mut acadrust::types::Vector3| {
                let cur = p.x * perp.0 + p.y * perp.1;
                let delta = target - cur;
                p.x += perp.0 * delta;
                p.y += perp.1 * delta;
            };
            match dim {
                Dimension::Linear(d) => {
                    slide(&mut d.definition_point);
                    d.base.definition_point = d.definition_point;
                    changed = true;
                }
                Dimension::Aligned(d) => {
                    slide(&mut d.definition_point);
                    d.base.definition_point = d.definition_point;
                    changed = true;
                }
                _ => {}
            }
        }
        if changed {
            // The dimension line moved, so its baked *D block is stale — drop it
            // so the next save re-bakes it. (#181)
            scene.invalidate_dim_block_recorded(h);
            changes.push((h, crate::scene::ChangeKind::Modified));
        }
    }
    if !changes.is_empty() {
        scene.bump_entities(&changes);
    }
}

// ── MLEADERALIGN helper ───────────────────────────────────────────────────────

/// Parse `h1,h2,...;fx,fz;tx,tz` and align multileader content points along the direction.
fn apply_mleader_align(scene: &mut crate::scene::Scene, encoded: &str) {
    // Format: "<h1>,<h2>,...;<fx>,<fz>;<tx>,<tz>"
    let parts: Vec<&str> = encoded.splitn(3, ';').collect();
    if parts.len() < 3 {
        return;
    }
    let handles: Vec<acadrust::Handle> = parts[0]
        .split(',')
        .filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
        .collect();
    let from_parts: Vec<f64> = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
    let to_parts: Vec<f64> = parts[2].split(',').filter_map(|s| s.parse().ok()).collect();
    if from_parts.len() < 2 || to_parts.len() < 2 || handles.is_empty() {
        return;
    }

    let fx = from_parts[0];
    let fz = from_parts[1];
    let tx = to_parts[0];
    let tz = to_parts[1];
    let dx = tx - fx;
    let dz = tz - fz;
    let len = (dx * dx + dz * dz).sqrt();
    if len < 1e-9 {
        return;
    }

    // Project each multileader's content point onto the alignment line, then
    // snap it to the line (preserve perpendicular offset from line is discarded;
    // align along direction through `from`).
    for h in handles {
        if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(h) {
            let cp = &mut ml.context.content_base_point;
            // Project onto line from_pt + t * dir: keep t component, set perpendicular = 0
            let rel_x = cp.x - fx;
            let rel_z = cp.z - fz;
            let t = (rel_x * (dx / len) + rel_z * (dz / len)) / len;
            let t = t.clamp(0.0, 1.0);
            cp.x = fx + t * dx;
            cp.z = fz + t * dz;
        }
    }
}

// ── MLEADERCOLLECT helper ─────────────────────────────────────────────────────

/// Parse `h1,h2,...;px,pz` — merge all selected multileaders into the first one at position.
fn apply_mleader_collect(scene: &mut crate::scene::Scene, encoded: &str) {
    let parts: Vec<&str> = encoded.splitn(2, ';').collect();
    if parts.len() < 2 {
        return;
    }
    let handles: Vec<acadrust::Handle> = parts[0]
        .split(',')
        .filter_map(|s| s.parse::<u64>().ok().map(acadrust::Handle::from))
        .collect();
    let pos_parts: Vec<f64> = parts[1].split(',').filter_map(|s| s.parse().ok()).collect();
    if handles.len() < 2 || pos_parts.len() < 2 {
        return;
    }

    let px = pos_parts[0];
    let pz = pos_parts[1];

    // Collect all leader roots from the secondary multileaders.
    let mut extra_roots: Vec<acadrust::entities::LeaderRoot> = Vec::new();
    for &h in &handles[1..] {
        if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity(h) {
            extra_roots.extend(ml.context.leader_roots.iter().cloned());
        }
    }

    // Add collected roots to the first multileader and move its content point.
    if let Some(acadrust::EntityType::MultiLeader(ml)) = scene.document.get_entity_mut(handles[0]) {
        ml.context.content_base_point.x = px;
        ml.context.content_base_point.z = pz;
        for root in extra_roots {
            ml.context.leader_roots.push(root);
        }
    }

    // Erase the secondary multileaders.
    scene.erase_entities(&handles[1..]);
}

/// MATCHPROP special properties (#281): copy every STYLE-affecting field from
/// `src` to `dst` when the destination supports it — never content (text
/// strings, block names) or placement (positions, rotations of the object
/// itself). Text formatting crosses TEXT ↔ MTEXT; the dimension style crosses
/// Dimension / Leader / Tolerance.
fn match_special_props(src: &acadrust::EntityType, dst: &mut acadrust::EntityType) {
    use acadrust::EntityType as E;

    // Text-ish source formatting.
    let text_fmt = match src {
        E::Text(t) => Some((
            t.style.clone(),
            t.height,
            Some(t.width_factor),
            Some(t.oblique_angle),
        )),
        E::MText(m) => Some((m.style.clone(), m.height, None, None)),
        E::AttributeDefinition(a) => Some((
            a.text_style.clone(),
            a.height,
            Some(a.width_factor),
            Some(a.oblique_angle),
        )),
        E::AttributeEntity(a) => Some((
            a.text_style.clone(),
            a.height,
            Some(a.width_factor),
            Some(a.oblique_angle),
        )),
        _ => None,
    };
    if let Some((style, height, wf, ob)) = text_fmt {
        match dst {
            E::Text(t) => {
                t.style = style.clone();
                t.height = height;
                if let Some(value) = wf {
                    t.width_factor = value;
                }
                if let Some(value) = ob {
                    t.oblique_angle = value;
                }
            }
            E::MText(m) => {
                m.style = style.clone();
                m.height = height;
            }
            E::AttributeDefinition(a) => {
                a.text_style = style.clone();
                a.height = height;
                if let Some(value) = wf {
                    a.width_factor = value;
                }
                if let Some(value) = ob {
                    a.oblique_angle = value;
                }
            }
            E::AttributeEntity(a) => {
                a.text_style = style;
                a.height = height;
                if let Some(value) = wf {
                    a.width_factor = value;
                }
                if let Some(value) = ob {
                    a.oblique_angle = value;
                }
            }
            _ => {}
        }
    }
    // MText-only extras (line spacing, background fill).
    if let (E::MText(sm), E::MText(dm)) = (src, dst as &mut E) {
        dm.drawing_direction = sm.drawing_direction;
        dm.line_spacing_factor = sm.line_spacing_factor;
        dm.line_spacing_style = sm.line_spacing_style;
        dm.background_fill_flags = sm.background_fill_flags;
        dm.background_scale = sm.background_scale;
        dm.background_color = sm.background_color;
        dm.background_transparency = sm.background_transparency;
    }

    // Dimension style name crosses the three dim-styled families.
    let dim_style = match src {
        E::Dimension(d) => Some(d.base().style_name.clone()),
        E::Leader(l) => Some(l.dimension_style.clone()),
        E::Tolerance(t) => Some(t.dimension_style_name.clone()),
        _ => None,
    };
    if let Some(ds) = dim_style {
        match dst {
            E::Dimension(d) => d.base_mut().style_name = ds,
            E::Leader(l) => l.dimension_style = ds,
            E::Tolerance(t) => t.dimension_style_name = ds,
            _ => {}
        }
    }

    // Hatch pattern / gradient — everything but the boundary.
    if let (E::Hatch(sh), E::Hatch(dh)) = (src, dst as &mut E) {
        dh.pattern = sh.pattern.clone();
        dh.pattern_type = sh.pattern_type;
        dh.pattern_angle = sh.pattern_angle;
        dh.pattern_scale = sh.pattern_scale;
        dh.is_solid = sh.is_solid;
        dh.is_double = sh.is_double;
        dh.style = sh.style;
        dh.gradient_color = sh.gradient_color.clone();
    }

    // Polyline display style crosses lightweight and legacy 2D polylines.
    // Per-vertex/tapered widths are resampled over the destination vertices;
    // they must not be flattened into the source's constant-width field.
    if let Some(style) = PolylineMatchStyle::from_entity(src) {
        style.apply_to(dst);
    }

    if let (E::Leader(sl), E::Leader(dl)) = (src, dst as &mut E) {
        dl.arrow_enabled = sl.arrow_enabled;
        dl.path_type = sl.path_type;
        dl.hookline_direction = sl.hookline_direction;
        dl.hookline_enabled = sl.hookline_enabled;
        dl.override_color = sl.override_color;
        dl.dimension_gap = sl.dimension_gap;
        dl.arrowhead_type = sl.arrowhead_type;
        dl.arrow_size = sl.arrow_size;
        dl.byblock_color = sl.byblock_color;
    }
    if let (E::Tolerance(st), E::Tolerance(dt)) = (src, dst as &mut E) {
        dt.dimension_style_handle = st.dimension_style_handle;
        dt.text_height = st.text_height;
        dt.dimension_gap = st.dimension_gap;
    }

    // Paper-space geometry and view position stay; viewport display/plot
    // styling and effective scale follow the source.
    if let (E::Viewport(sv), E::Viewport(dv)) = (src, dst as &mut E) {
        let scale = crate::scene::vp_effective_scale(sv.custom_scale, sv.view_height, sv.height);
        dv.custom_scale = scale;
        if scale.abs() > 1e-9 {
            dv.view_height = dv.height / scale;
        }
        dv.status.locked = sv.status.locked;
        dv.status.hide_plot = sv.status.hide_plot;
        dv.render_mode = sv.render_mode;
        dv.style_sheet = sv.style_sheet.clone();
        dv.shade_plot_mode = sv.shade_plot_mode;
        dv.background_handle = sv.background_handle;
        dv.shade_plot_handle = sv.shade_plot_handle;
        dv.visual_style_handle = sv.visual_style_handle;
        dv.default_lighting = sv.default_lighting;
        dv.default_lighting_type = sv.default_lighting_type;
        dv.brightness = sv.brightness;
        dv.contrast = sv.contrast;
        dv.ambient_color = sv.ambient_color;
    }

    if let (E::Table(st), E::Table(dt)) = (src, dst as &mut E) {
        dt.table_style_handle = st.table_style_handle;
        dt.base_style = st.base_style.clone();
        dt.override_flag = st.override_flag;
        dt.override_border_color = st.override_border_color;
        dt.override_border_line_weight = st.override_border_line_weight;
        dt.override_border_visibility = st.override_border_visibility;
        dt.legacy_style_override = st.legacy_style_override.clone();
        dt.legacy_border_colors = st.legacy_border_colors.clone();
        dt.legacy_border_line_weights = st.legacy_border_line_weights.clone();
        dt.legacy_border_visibility = st.legacy_border_visibility.clone();
    }

    if let (E::MLine(sm), E::MLine(dm)) = (src, dst as &mut E) {
        dm.style_handle = sm.style_handle;
        dm.style_name = sm.style_name.clone();
        dm.style_element_count = sm.style_element_count;
        dm.justification = sm.justification;
        dm.scale_factor = sm.scale_factor;
        // Stored segment offsets bake the old style into each vertex. Empty
        // data deliberately selects the renderer's style-derived fallback.
        for vertex in &mut dm.vertices {
            vertex.segments.clear();
        }
    }

    // MultiLeader style + every style-affecting override; content and
    // geometry (leader points, text, block handle) stay.
    if let (E::MultiLeader(sm), E::MultiLeader(dm)) = (src, dst as &mut E) {
        dm.style_handle = sm.style_handle;
        dm.path_type = sm.path_type;
        dm.line_color = sm.line_color;
        dm.line_type_handle = sm.line_type_handle;
        dm.line_weight = sm.line_weight;
        dm.enable_landing = sm.enable_landing;
        dm.enable_dogleg = sm.enable_dogleg;
        dm.dogleg_length = sm.dogleg_length;
        dm.arrowhead_handle = sm.arrowhead_handle;
        dm.arrowhead_size = sm.arrowhead_size;
        dm.text_style_handle = sm.text_style_handle;
        dm.text_color = sm.text_color;
        dm.text_frame = sm.text_frame;
        dm.text_height = sm.text_height;
        dm.text_left_attachment = sm.text_left_attachment;
        dm.text_right_attachment = sm.text_right_attachment;
        dm.text_top_attachment = sm.text_top_attachment;
        dm.text_bottom_attachment = sm.text_bottom_attachment;
        dm.text_attachment_direction = sm.text_attachment_direction;
        dm.text_attachment_point = sm.text_attachment_point;
        dm.text_alignment = sm.text_alignment;
        dm.text_angle_type = sm.text_angle_type;
        dm.text_direction_negative = sm.text_direction_negative;
        dm.text_align_in_ipe = sm.text_align_in_ipe;
        dm.block_content_color = sm.block_content_color;
        dm.block_connection_type = sm.block_connection_type;
        dm.block_scale = sm.block_scale;
        dm.scale_factor = sm.scale_factor;
        dm.property_override_flags = sm.property_override_flags;
        dm.enable_annotation_scale = sm.enable_annotation_scale;
        dm.extend_leader_to_text = sm.extend_leader_to_text;
        dm.arrowhead_overrides = sm.arrowhead_overrides.clone();

        let sc = &sm.context;
        let dc = &mut dm.context;
        dc.scale_factor = sc.scale_factor;
        dc.text_height = sc.text_height;
        dc.text_width = sc.text_width;
        dc.text_boundary_height = sc.text_boundary_height;
        dc.line_spacing_factor = sc.line_spacing_factor;
        dc.line_spacing_style = sc.line_spacing_style;
        dc.text_color = sc.text_color;
        dc.text_attachment_point = sc.text_attachment_point;
        dc.text_flow_direction = sc.text_flow_direction;
        dc.text_alignment = sc.text_alignment;
        dc.text_left_attachment = sc.text_left_attachment;
        dc.text_right_attachment = sc.text_right_attachment;
        dc.text_top_attachment = sc.text_top_attachment;
        dc.text_bottom_attachment = sc.text_bottom_attachment;
        dc.text_height_automatic = sc.text_height_automatic;
        dc.word_break = sc.word_break;
        dc.text_style_handle = sc.text_style_handle;
        dc.block_content_scale = sc.block_content_scale;
        dc.block_content_color = sc.block_content_color;
        dc.block_connection_type = sc.block_connection_type;
        dc.column_type = sc.column_type;
        dc.column_width = sc.column_width;
        dc.column_gutter = sc.column_gutter;
        dc.column_flow_reversed = sc.column_flow_reversed;
        dc.column_sizes = sc.column_sizes.clone();
        dc.background_fill_enabled = sc.background_fill_enabled;
        dc.background_mask_fill_on = sc.background_mask_fill_on;
        dc.background_fill_color = sc.background_fill_color;
        dc.background_scale_factor = sc.background_scale_factor;
        dc.background_transparency = sc.background_transparency;
        dc.arrowhead_size = sc.arrowhead_size;
        dc.landing_gap = sc.landing_gap;
        dc.scale_handle = sc.scale_handle;
    }

    // External-reference identity, placement and clip geometry stay. Only
    // display controls/appearance are matched.
    if let (E::RasterImage(si), E::RasterImage(di)) = (src, dst as &mut E) {
        di.flags = si.flags;
        di.clipping_enabled = si.clipping_enabled;
        di.brightness = si.brightness;
        di.contrast = si.contrast;
        di.fade = si.fade;
        di.clip_boundary.clip_mode = si.clip_boundary.clip_mode;
    }
    if let (E::Wipeout(sw), E::Wipeout(dw)) = (src, dst as &mut E) {
        dw.flags = sw.flags;
        dw.clipping_enabled = sw.clipping_enabled;
        dw.brightness = sw.brightness;
        dw.contrast = sw.contrast;
        dw.fade = sw.fade;
        dw.clip_mode = sw.clip_mode;
    }
    if let (E::Underlay(su), E::Underlay(du)) = (src, dst as &mut E) {
        du.flags = su.flags;
        du.contrast = su.contrast;
        du.fade = su.fade;
        du.clip_inverted = su.clip_inverted;
    }
}

struct PolylineMatchStyle {
    plinegen: bool,
    widths: Vec<(f64, f64)>,
}

impl PolylineMatchStyle {
    fn from_entity(entity: &acadrust::EntityType) -> Option<Self> {
        use acadrust::EntityType as E;

        match entity {
            E::LwPolyline(poly) => {
                let widths = if poly.vertices.is_empty() {
                    vec![(poly.constant_width, poly.constant_width)]
                } else {
                    poly.vertices
                        .iter()
                        .map(|v| {
                            (
                                width_or_default(v.start_width, poly.constant_width),
                                width_or_default(v.end_width, poly.constant_width),
                            )
                        })
                        .collect()
                };
                Some(Self {
                    plinegen: poly.plinegen,
                    widths,
                })
            }
            E::Polyline2D(poly) => {
                let widths = if poly.vertices.is_empty() {
                    vec![(poly.start_width, poly.end_width)]
                } else {
                    poly.vertices
                        .iter()
                        .map(|v| {
                            (
                                width_or_default(v.start_width, poly.start_width),
                                width_or_default(v.end_width, poly.end_width),
                            )
                        })
                        .collect()
                };
                Some(Self {
                    plinegen: poly.flags.bits()
                        & acadrust::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits()
                        != 0,
                    widths,
                })
            }
            _ => None,
        }
    }

    fn apply_to(&self, entity: &mut acadrust::EntityType) {
        use acadrust::EntityType as E;

        match entity {
            E::LwPolyline(poly) => {
                poly.plinegen = self.plinegen;
                let sampled = resample_widths(&self.widths, poly.vertices.len());
                let first = sampled.first().copied().unwrap_or((0.0, 0.0));
                let can_use_constant = widths_are_same(&sampled) && nearly_equal(first.0, first.1);
                poly.constant_width = if can_use_constant { first.0 } else { 0.0 };
                for (vertex, (start, end)) in poly.vertices.iter_mut().zip(sampled) {
                    if can_use_constant {
                        vertex.start_width = 0.0;
                        vertex.end_width = 0.0;
                    } else {
                        vertex.start_width = start;
                        vertex.end_width = end;
                    }
                }
            }
            E::Polyline2D(poly) => {
                let mut bits = poly.flags.bits();
                let flag = acadrust::entities::PolylineFlags::LINETYPE_CONTINUOUS.bits();
                if self.plinegen {
                    bits |= flag;
                } else {
                    bits &= !flag;
                }
                poly.flags = acadrust::entities::PolylineFlags::from_bits(bits);
                let sampled = resample_widths(&self.widths, poly.vertices.len());
                let first = sampled.first().copied().unwrap_or((0.0, 0.0));
                let can_use_defaults = widths_are_same(&sampled);
                if can_use_defaults {
                    poly.start_width = first.0;
                    poly.end_width = first.1;
                } else {
                    poly.start_width = 0.0;
                    poly.end_width = 0.0;
                }
                for (vertex, (start, end)) in poly.vertices.iter_mut().zip(sampled) {
                    if can_use_defaults {
                        vertex.start_width = 0.0;
                        vertex.end_width = 0.0;
                    } else {
                        vertex.start_width = start;
                        vertex.end_width = end;
                    }
                }
            }
            _ => {}
        }
    }
}

fn width_or_default(value: f64, default: f64) -> f64 {
    if value.abs() <= 1e-12 {
        default
    } else {
        value
    }
}

fn nearly_equal(a: f64, b: f64) -> bool {
    (a - b).abs() <= 1e-9
}

fn widths_are_same(widths: &[(f64, f64)]) -> bool {
    let Some(first) = widths.first() else {
        return true;
    };
    widths
        .iter()
        .all(|value| nearly_equal(value.0, first.0) && nearly_equal(value.1, first.1))
}

fn resample_widths(source: &[(f64, f64)], count: usize) -> Vec<(f64, f64)> {
    if count == 0 || source.is_empty() {
        return Vec::new();
    }
    if count == 1 || source.len() == 1 {
        return vec![source[0]; count];
    }
    (0..count)
        .map(|index| {
            let source_index = index.saturating_mul(source.len() - 1) / count.saturating_sub(1);
            source[source_index]
        })
        .collect()
}
