(function(){
'use strict';

// ===================== DOM =====================
const $ = s => document.querySelector(s);
const viewport = $('#viewport');
const container = $('#canvasContainer');
const cMain = $('#canvasMain');
const cOverlay = $('#canvasOverlay');
const cUI = $('#canvasUI');
const ctxMain = cMain.getContext('2d');
const ctxOverlay = cOverlay.getContext('2d');
const ctxUI = cUI.getContext('2d');
const dropzone = $('#dropzone');
const fileInput = $('#fileInput');
const toolOptions = $('#toolOptions');
const textOverlay = $('#textOverlay');
const textArea = $('#textArea');
const layerPanel = $('#layerPanel');
const layerList = $('#layerList');

// ===================== STATE =====================
let img = null;
let fileName = 'image';
let hasTransparency = false;
let canvasW = 0, canvasH = 0;

// Zoom/Pan
let zoom = 1;
let panX = 0, panY = 0;
let isPanning = false;
let panStart = {x:0,y:0};
let spaceDown = false;

// Current tool
let currentTool = null;

// History (sync ImageData-based)
const MAX_HISTORY = 15;
let history = [];
let historyIndex = -1;

// Drawing state
let isDrawing = false;
let drawStart = {x:0,y:0};
let lastDraw = {x:0,y:0};

// Tool settings
let brushSize = 8;
let brushOpacity = 100;
let strokeWidth = 2;
let fillShape = false;
let fgColor = '#0054e6';

// Crop state
let cropRect = null;
let cropDragging = null;
let cropStart = null;
let cropAspect = 0;

// Internal clipboard for selection copy/paste
let internalClipboard = null; // { imageData, x, y, w, h }

// Layer move state
let layerMoving = false;
let layerMoveStart = null;
let layerMoveSnapshot = null;

// Text state
let textFont = 'sans-serif';
let textSize = 32;
let textBold = false;
let textItalic = false;

// Adjustments
let adjBrightness = 0;
let adjContrast = 0;
let adjSaturation = 0;

// Resize state
let resizeW = 0, resizeH = 0, resizeKeepRatio = true, resizeOrigW = 0, resizeOrigH = 0;

// Clone stamp state
let cloneSrc = null;
let cloneOffset = null;
let cloneSrcSet = false;

// Fill tool
let fillTolerance = 30;

// Blur / Sharpen
let blurRadius = 5;
let sharpenAmount = 50;

// Selection state
let selRect = null;
let selPath = null;       // lasso path: [{x,y}, ...]
let selDragging = false;
let selDragMode = null;
let selStart = {x:0, y:0};
let selStartRect = null;
// Marching ants animation
let marchRaf = 0;
let marchOffset = 0;

// Pending shape state
let pendingShape = null;
let pendingDragging = false;
let pendingDragStart = null;
let pendingTransformHandle = null;
let pendingTransformStart = null;

// Text layer re-editing
let editingTextLayerIndex = -1;

// Tabs
let tabs = [];
let activeTabId = 0;
let tabIdCounter = 0;

// Layer system
const MAX_LAYERS = 10;
let layers = [];
let activeLayerIndex = 0;
let layerIdCounter = 0;
let layerPanelVisible = false;
let compositeDirtyRaf = 0;

// ===================== HELPERS =====================
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

function canvasToViewport(cx, cy) {
    return { x: cx * zoom + panX, y: cy * zoom + panY };
}

function viewportToCanvas(vx, vy) {
    return { x: (vx - panX) / zoom, y: (vy - panY) / zoom };
}

function getPointerCanvas(e) {
    const rect = viewport.getBoundingClientRect();
    const vx = e.clientX - rect.left;
    const vy = e.clientY - rect.top;
    return viewportToCanvas(vx, vy);
}

function updateTransform() {
    container.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom})`;
    $('#zoomLabel').textContent = Math.round(zoom * 100) + '%';
    $('#statusZoom').textContent = Math.round(zoom * 100) + '%';
    redrawUI();
}

function redrawUI() {
    clearUI();
    drawRulers();
    drawImageBorder();
    if (selPath && selPath.length > 2) drawLassoOutline();
    else if (selRect && selRect.w > 5) drawSelectionRect(false);
    else if (cropRect) drawCropOverlay(false);
    if (pendingShape) drawPendingBBoxOnUI();
}

function fitToViewport() {
    if (!canvasW || !canvasH) return;
    const vw = viewport.clientWidth;
    const vh = viewport.clientHeight;
    const scaleX = (vw - 40) / canvasW;
    const scaleY = (vh - 40) / canvasH;
    zoom = Math.min(scaleX, scaleY, 1);
    panX = (vw - canvasW * zoom) / 2;
    panY = (vh - canvasH * zoom) / 2;
    updateTransform();
}

function setCanvasSize(w, h) {
    canvasW = w; canvasH = h;
    [cMain, cOverlay].forEach(c => { c.width = w; c.height = h; });
    container.style.width = w + 'px';
    container.style.height = h + 'px';
    selRect = null;
    selPath = null;
    stopMarchingAnts();
    pendingShape = null;
    $('#statusDims').textContent = w + ' x ' + h;
    resizeUICanvas();
}

function resizeUICanvas() {
    const dpr = window.devicePixelRatio || 1;
    const vw = viewport.clientWidth;
    const vh = viewport.clientHeight;
    cUI.width = vw * dpr;
    cUI.height = vh * dpr;
    cUI.style.width = vw + 'px';
    cUI.style.height = vh + 'px';
    ctxUI.setTransform(dpr, 0, 0, dpr, 0, 0);
}

// Convert canvas coords to screen (viewport-local) coords for UI drawing
function c2s(cx, cy) {
    return { x: cx * zoom + panX, y: cy * zoom + panY };
}

// ===================== TOOL DISPATCHER =====================
class ToolDispatcher {
    constructor() {
        this._tools = new Map();   // toolName → {onDown, onMove, onUp}
        this._pendingHandler = null;
        this._selectionHandler = null;
    }

    registerTool(name, handler) { this._tools.set(name, handler); }
    registerPending(handler) { this._pendingHandler = handler; }
    registerSelection(handler) { this._selectionHandler = handler; }

    _dispatch(type, e) {
        if (!canvasW) return false;
        // pendingShape перехватывает первым
        if (pendingShape && this._pendingHandler && this._pendingHandler[type]) {
            this._pendingHandler[type](e);
            return true;
        }
        // нет инструмента → selection
        if (!currentTool && this._selectionHandler && this._selectionHandler[type]) {
            this._selectionHandler[type](e);
            return true;
        }
        // инструмент
        const h = this._tools.get(currentTool);
        if (h && h[type]) { h[type](e); return true; }
        return false;
    }

    onDown(e) { this._dispatch('onDown', e); }
    onMove(e) { this._dispatch('onMove', e); }
    onUp(e)   { this._dispatch('onUp', e); }
}
const dispatcher = new ToolDispatcher();

// ===================== LAYER SYSTEM =====================
function createLayer(name, w, h) {
    if (layers.length >= MAX_LAYERS) return null;
    const c = document.createElement('canvas');
    c.width = w || canvasW;
    c.height = h || canvasH;
    const layer = {
        id: ++layerIdCounter,
        name: name || ('Слой ' + layerIdCounter),
        canvas: c,
        ctx: c.getContext('2d'),
        visible: true,
        opacity: 1,
        blendMode: 'source-over'
    };
    return layer;
}

function getActiveLayer() { return layers[activeLayerIndex]; }
function activeCtx() { return layers[activeLayerIndex].ctx; }
function activeCanvas() { return layers[activeLayerIndex].canvas; }

function compositeLayersToMain() {
    ctxMain.clearRect(0, 0, canvasW, canvasH);
    for (const layer of layers) {
        if (!layer.visible || layer.opacity === 0) continue;
        ctxMain.save();
        ctxMain.globalAlpha = layer.opacity;
        ctxMain.globalCompositeOperation = layer.blendMode;
        ctxMain.drawImage(layer.canvas, 0, 0);
        ctxMain.restore();
    }
}

function markCompositeDirty() {
    if (compositeDirtyRaf) return;
    compositeDirtyRaf = requestAnimationFrame(() => {
        compositeDirtyRaf = 0;
        compositeLayersToMain();
    });
}

function initLayers(w, h, image) {
    layers = [];
    layerIdCounter = 0;
    activeLayerIndex = 0;
    const layer = createLayer('Фон', w, h);
    if (image) layer.ctx.drawImage(image, 0, 0, w, h);
    layers.push(layer);
    compositeLayersToMain();
    renderLayerPanel();
}

function addLayer(name) {
    if (layers.length >= MAX_LAYERS) return;
    const layer = createLayer(name || ('Слой ' + (layerIdCounter + 1)), canvasW, canvasH);
    layers.splice(activeLayerIndex + 1, 0, layer);
    activeLayerIndex++;
    compositeLayersToMain();
    renderLayerPanel();
    pushHistory();
}

function removeLayer(index) {
    if (layers.length <= 1) return;
    layers.splice(index, 1);
    if (activeLayerIndex >= layers.length) activeLayerIndex = layers.length - 1;
    compositeLayersToMain();
    renderLayerPanel();
    pushHistory();
}

function mergeDown(index) {
    if (index <= 0 || index >= layers.length) return;
    const upper = layers[index];
    const lower = layers[index - 1];
    lower.ctx.save();
    lower.ctx.globalAlpha = upper.opacity;
    lower.ctx.globalCompositeOperation = upper.blendMode;
    lower.ctx.drawImage(upper.canvas, 0, 0);
    lower.ctx.restore();
    layers.splice(index, 1);
    if (activeLayerIndex >= index) activeLayerIndex = Math.max(0, activeLayerIndex - 1);
    compositeLayersToMain();
    renderLayerPanel();
    pushHistory();
}

function flattenLayers() {
    if (layers.length <= 1) return;
    compositeLayersToMain();
    const flat = createLayer('Фон', canvasW, canvasH);
    flat.ctx.drawImage(cMain, 0, 0);
    layers = [flat];
    activeLayerIndex = 0;
    compositeLayersToMain();
    renderLayerPanel();
    pushHistory();
}

function moveLayer(fromIndex, toIndex) {
    if (fromIndex === toIndex || toIndex < 0 || toIndex >= layers.length) return;
    const layer = layers.splice(fromIndex, 1)[0];
    layers.splice(toIndex, 0, layer);
    if (activeLayerIndex === fromIndex) {
        activeLayerIndex = toIndex;
    } else {
        if (fromIndex < activeLayerIndex && toIndex >= activeLayerIndex) activeLayerIndex--;
        else if (fromIndex > activeLayerIndex && toIndex <= activeLayerIndex) activeLayerIndex++;
    }
    compositeLayersToMain();
    renderLayerPanel();
    pushHistory();
}

function updateStatusLayer() {
    const el = $('#statusLayer');
    if (layers.length > 0) {
        el.textContent = layers[activeLayerIndex].name + ' (' + layers.length + ')';
    } else {
        el.textContent = '';
    }
}

// ===================== HISTORY (sync ImageData) =====================
// Refactoring B: replaced async toBlob/URL.createObjectURL with sync getImageData/putImageData.
// freeHistoryEntry() removed — no blob URLs to revoke.

function pushHistory() {
    while (history.length > historyIndex + 1) history.pop();
    if (history.length >= MAX_HISTORY) { history.shift(); historyIndex--; }
    const snap = {
        layers: layers.map(layer => ({
            imageData: layer.ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height),
            id: layer.id, name: layer.name,
            visible: layer.visible, opacity: layer.opacity, blendMode: layer.blendMode
        })),
        activeIndex: activeLayerIndex,
        w: canvasW, h: canvasH
    };
    history.push(snap);
    historyIndex = history.length - 1;
    updateHistoryButtons();
}

function restoreHistory(index) {
    const entry = history[index];
    if (!entry) return;
    setCanvasSize(entry.w, entry.h);
    layers = [];
    activeLayerIndex = entry.activeIndex;
    entry.layers.forEach(snap => {
        const layer = createLayer(snap.name, entry.w, entry.h);
        layer.id = snap.id;
        layer.visible = snap.visible;
        layer.opacity = snap.opacity;
        layer.blendMode = snap.blendMode;
        layer.ctx.putImageData(snap.imageData, 0, 0);
        layers.push(layer);
    });
    compositeLayersToMain();
    clearOverlay();
    redrawUI();
    renderLayerPanel();
    updateStatusLayer();
}

function undo() {
    if (historyIndex <= 0) return;
    historyIndex--;
    restoreHistory(historyIndex);
    updateHistoryButtons();
}

function redo() {
    if (historyIndex >= history.length - 1) return;
    historyIndex++;
    restoreHistory(historyIndex);
    updateHistoryButtons();
}

function updateHistoryButtons() {
    $('#btnUndo').disabled = historyIndex <= 0;
    $('#btnRedo').disabled = historyIndex >= history.length - 1;
    if (history.length > 0) {
        $('#statusHistory').textContent = (historyIndex + 1) + '/' + history.length;
    }
}

// ===================== CLEAR =====================
function clearOverlay() { ctxOverlay.clearRect(0, 0, canvasW, canvasH); }
function clearUI() { ctxUI.save(); ctxUI.setTransform(1,0,0,1,0,0); ctxUI.clearRect(0, 0, cUI.width, cUI.height); ctxUI.restore(); }

// ===================== LOAD IMAGE =====================
function _applyImageToCurrentTab(im, name, transparency) {
    img = im;
    hasTransparency = transparency;
    container.classList.toggle('has-transparency', transparency);
    fileName = name;
    const w = Math.min(im.width, 16384);
    const h = Math.min(im.height, 16384);
    setCanvasSize(w, h);
    initLayers(w, h, im);
    // Refactoring B: removed freeHistoryEntry loop — no blob URLs to revoke
    history = [];
    historyIndex = -1;
    pushHistory();
    fitToViewport();
    enableTools();
    dropzone.classList.add('hidden');
    cancelCurrentTool();
    const tab = tabs.find(t => t.id === activeTabId);
    if (tab) { tab.name = name; tab.hasImage = true; }
    renderTabBar();
    if (!layerPanelVisible) {
        layerPanelVisible = true;
        layerPanel.classList.add('visible');
        $('#btnLayers').classList.add('active');
        renderLayerPanel();
    }
}

function loadImage(file) {
    if (!file || !file.type.startsWith('image/')) return;
    const name = file.name.replace(/\.[^.]+$/, '') || 'image';
    const transparency = file.type === 'image/png' || file.type === 'image/webp';
    if (canvasW > 0) {
        saveCurrentTabState();
        const newTab = createTab(name);
        activeTabId = newTab.id;
        layers = []; history = []; historyIndex = -1; canvasW = 0; canvasH = 0;
        selRect = null;
    }
    const reader = new FileReader();
    reader.onload = e => {
        const im = new Image();
        im.onload = () => _applyImageToCurrentTab(im, name, transparency);
        im.src = e.target.result;
    };
    reader.readAsDataURL(file);
}

function loadImageFromDataURL(dataUrl) {
    const name = 'pasted';
    if (canvasW > 0) {
        saveCurrentTabState();
        const newTab = createTab(name);
        activeTabId = newTab.id;
        layers = []; history = []; historyIndex = -1; canvasW = 0; canvasH = 0;
        selRect = null;
    }
    const im = new Image();
    im.onload = () => _applyImageToCurrentTab(im, name, true);
    im.src = dataUrl;
}

function enableTools() {
    document.querySelectorAll('.tb[disabled]').forEach(b => b.disabled = false);
    document.querySelectorAll('#btnExport,#btnRotCCW,#btnRotCW,#btnFlipH,#btnFlipV,#btnResize,#btnAdjust,#btnBlur,#btnSharpen').forEach(b => b.disabled = false);
    document.querySelectorAll('#filtersGroup .tb').forEach(b => b.disabled = false);
    updateHistoryButtons();
}

function disableTools() {
    document.querySelectorAll('.tb[data-tool]').forEach(b => b.disabled = true);
    document.querySelectorAll('#btnExport,#btnRotCCW,#btnRotCW,#btnFlipH,#btnFlipV,#btnResize,#btnAdjust,#btnBlur,#btnSharpen').forEach(b => b.disabled = true);
    document.querySelectorAll('#filtersGroup .tb').forEach(b => b.disabled = true);
    updateHistoryButtons();
}

// ===================== TABS =====================
function createTab(name) {
    const id = ++tabIdCounter;
    const tab = {
        id, name: name || 'Новый',
        layers: null, activeLayerIndex: 0,
        layerIdCounter: 0,
        history: [], historyIndex: -1,
        canvasW: 0, canvasH: 0,
        zoom: 1, panX: 0, panY: 0,
        selRect: null,
        selPath: null,
        fileName: name || 'image',
        hasTransparency: false,
        hasImage: false,
    };
    tabs.push(tab);
    return tab;
}

function saveCurrentTabState() {
    const tab = tabs.find(t => t.id === activeTabId);
    if (!tab) return;
    tab.layers = layers;
    tab.activeLayerIndex = activeLayerIndex;
    tab.layerIdCounter = layerIdCounter;
    tab.history = history;
    tab.historyIndex = historyIndex;
    tab.canvasW = canvasW;
    tab.canvasH = canvasH;
    tab.zoom = zoom;
    tab.panX = panX;
    tab.panY = panY;
    tab.selRect = selRect;
    tab.selPath = selPath;
    tab.fileName = fileName;
    tab.hasTransparency = hasTransparency;
    tab.hasImage = canvasW > 0;
}

function loadTabState(tab) {
    if (pendingShape) cancelPendingShape();
    cancelCurrentTool();
    hideTextOverlay();

    activeTabId = tab.id;

    if (!tab.hasImage || !tab.layers || !tab.layers.length) {
        layers = [];
        activeLayerIndex = 0;
        layerIdCounter = 0;
        history = [];
        historyIndex = -1;
        canvasW = 0; canvasH = 0;
        zoom = 1; panX = 0; panY = 0;
        selRect = null;
        fileName = 'image';
        hasTransparency = false;
        container.classList.remove('has-transparency');
        [cMain, cOverlay].forEach(c => { c.width = 1; c.height = 1; });
        container.style.width = '1px';
        container.style.height = '1px';
        container.style.transform = 'translate(0px,0px) scale(1)';
        $('#zoomLabel').textContent = '100%';
        $('#statusZoom').textContent = '100%';
        $('#statusDims').textContent = 'Нет изображения';
        disableTools();
        dropzone.classList.remove('hidden');
        clearOverlay();
        clearUI();
        renderLayerPanel();
        renderTabBar();
        return;
    }

    layers = tab.layers;
    activeLayerIndex = tab.activeLayerIndex;
    layerIdCounter = tab.layerIdCounter || 0;
    history = tab.history;
    historyIndex = tab.historyIndex;
    canvasW = tab.canvasW;
    canvasH = tab.canvasH;
    zoom = tab.zoom;
    panX = tab.panX;
    panY = tab.panY;
    selRect = tab.selRect || null;
    selPath = tab.selPath || null;
    if (selPath && selPath.length > 2) startMarchingAnts(); else stopMarchingAnts();
    fileName = tab.fileName;
    hasTransparency = tab.hasTransparency;
    container.classList.toggle('has-transparency', !!hasTransparency);

    [cMain, cOverlay].forEach(c => { c.width = canvasW; c.height = canvasH; });
    container.style.width = canvasW + 'px';
    container.style.height = canvasH + 'px';
    $('#statusDims').textContent = canvasW + ' x ' + canvasH;

    compositeLayersToMain();
    enableTools();
    updateTransform();
    dropzone.classList.add('hidden');
    renderLayerPanel();
    renderTabBar();
}

function switchTab(tabId) {
    if (tabId === activeTabId) return;
    saveCurrentTabState();
    const tab = tabs.find(t => t.id === tabId);
    if (tab) loadTabState(tab);
}

function closeTab(tabId) {
    const idx = tabs.findIndex(t => t.id === tabId);
    if (idx === -1) return;

    if (tabs.length === 1) {
        // Last tab: reset to empty
        // Refactoring B: removed freeHistoryEntry calls — ImageData snapshots are GC'd automatically
        saveCurrentTabState();
        const tab = tabs[0];
        tab.layers = null;
        tab.history = [];
        tab.historyIndex = -1;
        tab.canvasW = 0; tab.canvasH = 0;
        tab.hasImage = false;
        tab.name = 'Новый';
        loadTabState(tab);
        return;
    }

    if (tabId !== activeTabId) {
        // Refactoring B: removed freeHistoryEntry — no blob URLs to revoke
        tabs.splice(idx, 1);
        renderTabBar();
    } else {
        // Closing active tab: switch first, then remove old
        const newIdx = idx > 0 ? idx - 1 : 1;
        const newTab = tabs[newIdx];
        // Refactoring B: removed freeHistoryEntry loop — ImageData GC'd automatically
        history = [];
        historyIndex = -1;
        layers = [];
        tabs.splice(idx, 1);
        loadTabState(newTab);
    }
}

function renderTabBar() {
    const bar = $('#tabBar');
    bar.innerHTML = '';
    tabs.forEach(tab => {
        const item = document.createElement('div');
        item.className = 'tab-item' + (tab.id === activeTabId ? ' active' : '');
        item.dataset.tabId = tab.id;

        const name = document.createElement('span');
        name.className = 'tab-name';
        name.textContent = tab.name;

        const close = document.createElement('button');
        close.className = 'tab-close';
        close.innerHTML = '&times;';
        close.title = 'Закрыть вкладку';
        close.addEventListener('click', e => { e.stopPropagation(); closeTab(tab.id); });

        item.appendChild(name);
        item.appendChild(close);
        item.addEventListener('click', () => switchTab(tab.id));
        bar.appendChild(item);
    });

    const addBtn = document.createElement('button');
    addBtn.className = 'tab-add';
    addBtn.title = 'Новая вкладка (Ctrl+T)';
    addBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>';
    addBtn.addEventListener('click', () => {
        saveCurrentTabState();
        const tab = createTab('Новый');
        activeTabId = tab.id;
        loadTabState(tab);
    });
    bar.appendChild(addBtn);

    const activeItem = bar.querySelector('.tab-item.active');
    if (activeItem) activeItem.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}

// ===================== DROP ZONE =====================
dropzone.addEventListener('click', () => fileInput.click());
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('drag-over'); });
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over'));
dropzone.addEventListener('drop', e => {
    e.preventDefault();
    dropzone.classList.remove('drag-over');
    const f = e.dataTransfer.files[0];
    if (f) loadImage(f);
});

viewport.addEventListener('dragover', e => e.preventDefault());
viewport.addEventListener('drop', e => {
    e.preventDefault();
    const f = e.dataTransfer.files[0];
    if (f) loadImage(f);
});

fileInput.addEventListener('change', e => {
    if (e.target.files[0]) loadImage(e.target.files[0]);
    e.target.value = '';
});

document.addEventListener('paste', e => {
    const items = e.clipboardData.items;
    for (const item of items) {
        if (item.type.startsWith('image/')) {
            e.preventDefault();
            loadImage(item.getAsFile());
            return;
        }
    }
});

$('#btnOpen').addEventListener('click', () => fileInput.click());

// ===================== ZOOM / PAN =====================
viewport.addEventListener('wheel', e => {
    if (!canvasW) return;
    e.preventDefault();
    const rect = viewport.getBoundingClientRect();
    const mx = e.clientX - rect.left;
    const my = e.clientY - rect.top;
    const oldZoom = zoom;
    const factor = e.deltaY < 0 ? 1.1 : 1 / 1.1;
    zoom = clamp(zoom * factor, 0.05, 32);
    panX = mx - (mx - panX) * (zoom / oldZoom);
    panY = my - (my - panY) * (zoom / oldZoom);
    updateTransform();
}, { passive: false });

$('#btnZoomIn').addEventListener('click', () => { zoomCenter(1.25); });
$('#btnZoomOut').addEventListener('click', () => { zoomCenter(1/1.25); });
$('#btnFit').addEventListener('click', fitToViewport);
$('#btnUndo').addEventListener('click', () => { if (pendingShape) cancelPendingShape(); else undo(); });
$('#btnRedo').addEventListener('click', redo);

function zoomCenter(factor) {
    if (!canvasW) return;
    const vw = viewport.clientWidth;
    const vh = viewport.clientHeight;
    const mx = vw / 2, my = vh / 2;
    const oldZoom = zoom;
    zoom = clamp(zoom * factor, 0.05, 32);
    panX = mx - (mx - panX) * (zoom / oldZoom);
    panY = my - (my - panY) * (zoom / oldZoom);
    updateTransform();
}

// Pan: capture phase — space+drag or middle mouse always wins
viewport.addEventListener('pointerdown', e => {
    if (!canvasW) return;
    if (spaceDown || e.button === 1) {
        isPanning = true;
        panStart = { x: e.clientX - panX, y: e.clientY - panY };
        viewport.setPointerCapture(e.pointerId);
        viewport.style.cursor = 'grabbing'; container.style.cursor = 'grabbing';
        e.preventDefault();
        e.stopPropagation();
        return;
    }
}, true);

// Normal pointerdown — tools, selection, auto-pan outside image
viewport.addEventListener('pointerdown', e => {
    if (!canvasW || isPanning) return;
    if (e.button === 0 && !currentTool && !pendingShape) {
        const p = getPointerCanvas(e);
        const outsideImage = p.x < 0 || p.y < 0 || p.x > canvasW || p.y > canvasH;
        if (outsideImage) {
            isPanning = true;
            panStart = { x: e.clientX - panX, y: e.clientY - panY };
            viewport.setPointerCapture(e.pointerId);
            viewport.style.cursor = 'grabbing';
            e.preventDefault();
            return;
        }
    }
    dispatcher.onDown(e);
});

viewport.addEventListener('pointermove', e => {
    if (!canvasW) return;
    if (isPanning) {
        panX = e.clientX - panStart.x;
        panY = e.clientY - panStart.y;
        updateTransform();
        return;
    }
    const p = getPointerCanvas(e);
    $('#statusCursor').textContent = Math.round(p.x) + ', ' + Math.round(p.y);
    dispatcher.onMove(e);
});

viewport.addEventListener('pointerup', e => {
    if (isPanning) {
        isPanning = false;
        viewport.releasePointerCapture(e.pointerId);
        const cur = spaceDown ? 'grab' : (currentTool ? 'crosshair' : 'default');
        viewport.style.cursor = cur;
        container.style.cursor = cur;
        return;
    }
    dispatcher.onUp(e);
});

// ===================== TOOL SELECTION =====================
function setTool(name) {
    if (name === 'select') {
        if (pendingShape) commitPendingShape();
        if (currentTool === 'select') { cancelCurrentTool(); return; }
        cancelCurrentTool();
        currentTool = 'select';
        document.querySelectorAll('#toolsGroup .tb').forEach(b => {
            b.classList.toggle('active', b.dataset.tool === 'select');
        });
        toolOptions.innerHTML = `<label>Перемещение слоя — перетаскивайте содержимое</label>`;
        toolOptions.classList.add('visible');
        viewport.style.cursor = 'move';
        container.style.cursor = 'move';
        return;
    }
    if (currentTool === name) {
        cancelCurrentTool();
        return;
    }
    if (pendingShape) commitPendingShape();
    cancelCurrentTool();
    currentTool = name;
    document.querySelectorAll('#toolsGroup .tb').forEach(b => {
        b.classList.toggle('active', b.dataset.tool === name);
    });
    $('#btnAdjust').classList.remove('active');
    showToolOptions(name);
    viewport.style.cursor = 'crosshair';
    container.style.cursor = 'crosshair';
}

function cancelCurrentTool() {
    currentTool = null;
    document.querySelectorAll('#toolsGroup .tb').forEach(b => {
        b.classList.toggle('active', b.dataset.tool === 'select');
    });
    $('#btnAdjust').classList.remove('active');
    $('#btnResize').classList.remove('active');
    $('#btnBlur').classList.remove('active');
    $('#btnSharpen').classList.remove('active');
    cMain.style.opacity = '1';
    cOverlay.style.filter = '';
    toolOptions.classList.remove('visible');
    toolOptions.innerHTML = '';
    clearOverlay();
    cropRect = null;
    selDragging = false;
    hideTextOverlay();
    adjBrightness = 0; adjContrast = 0; adjSaturation = 0;
    cOverlay.style.filter = '';
    redrawUI();
    if (selPath && selPath.length > 2) {
        showLassoOptions();
    } else if (selRect && selRect.w > 5 && selRect.h > 5) {
        showSelectionOptions();
    }
    viewport.style.cursor = 'default';
    container.style.cursor = 'default';
}

document.querySelectorAll('#toolsGroup .tb').forEach(btn => {
    btn.addEventListener('click', () => {
        if (btn.disabled) return;
        setTool(btn.dataset.tool);
    });
});

// ===================== TOOL OPTIONS =====================
// Refactoring D: helper to build a labelled range row, eliminates repeated slider HTML
function buildRangeRow(id, label, min, max, value) {
    return `<label>${label}</label><input type="range" id="${id}" min="${min}" max="${max}" value="${value}"><span id="${id}Val" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);width:30px">${value}</span>`;
}

function showToolOptions(tool) {
    toolOptions.innerHTML = '';
    toolOptions.classList.add('visible');

    if (tool === 'brush' || tool === 'eraser') {
        toolOptions.innerHTML = `
            ${buildRangeRow('optSize', 'Размер', 1, 100, brushSize)}
            <div class="opt-sep"></div>
            ${buildRangeRow('optOpacity', 'Прозр.', 1, 100, brushOpacity)}
        `;
        $('#optSize').oninput = e => { brushSize = +e.target.value; $('#optSizeVal').textContent = brushSize; };
        $('#optOpacity').oninput = e => { brushOpacity = +e.target.value; $('#optOpacityVal').textContent = brushOpacity + '%'; };
    }
    else if (tool === 'line' || tool === 'arrow') {
        toolOptions.innerHTML = `
            ${buildRangeRow('optStroke', 'Толщина', 1, 20, strokeWidth)}
        `;
        $('#optStroke').oninput = e => { strokeWidth = +e.target.value; $('#optStrokeVal').textContent = strokeWidth; };
    }
    else if (tool === 'rect' || tool === 'circle') {
        toolOptions.innerHTML = `
            ${buildRangeRow('optStroke', 'Толщина', 1, 20, strokeWidth)}
            <div class="opt-sep"></div>
            <button class="opt-toggle ${fillShape?'on':''}" id="optFill">Заливка</button>
        `;
        $('#optStroke').oninput = e => { strokeWidth = +e.target.value; $('#optStrokeVal').textContent = strokeWidth; };
        $('#optFill').onclick = () => { fillShape = !fillShape; $('#optFill').classList.toggle('on', fillShape); };
    }
    else if (tool === 'crop') {
        toolOptions.innerHTML = `
            <label>Пропорции</label>
            <select id="optCropRatio">
                <option value="0">Свободно</option>
                <option value="${1/1}">1:1</option>
                <option value="${4/3}">4:3</option>
                <option value="${16/9}">16:9</option>
                <option value="${3/2}">3:2</option>
                <option value="${2/3}">2:3</option>
                <option value="${9/16}">9:16</option>
            </select>
            <div class="opt-sep"></div>
            <button class="opt-btn primary" id="optCropApply">Применить</button>
            <button class="opt-btn" id="optCropCancel">Отмена</button>
        `;
        cropAspect = 0;
        initCrop();
        $('#optCropRatio').onchange = e => {
            cropAspect = +e.target.value;
            if (cropAspect > 0 && cropRect) {
                cropRect.h = cropRect.w / cropAspect;
                drawCropOverlay();
            }
        };
        $('#optCropApply').onclick = applyCrop;
        $('#optCropCancel').onclick = () => cancelCurrentTool();
    }
    else if (tool === 'text') {
        toolOptions.innerHTML = `
            <label>Шрифт</label>
            <select id="optFont">
                <option value="sans-serif">Sans</option>
                <option value="serif">Serif</option>
                <option value="monospace">Mono</option>
                <option value="Manrope,sans-serif">Manrope</option>
                <option value="'JetBrains Mono',monospace">JetBrains</option>
            </select>
            <label>Размер</label><input type="number" id="optTextSize" min="8" max="200" value="${textSize}">
            <div class="opt-sep"></div>
            <button class="opt-toggle ${textBold?'on':''}" id="optBold" style="font-weight:700">B</button>
            <button class="opt-toggle ${textItalic?'on':''}" id="optItalic" style="font-style:italic">I</button>
        `;
        const fontSel = $('#optFont');
        fontSel.value = textFont;
        fontSel.onchange = e => textFont = e.target.value;
        $('#optTextSize').onchange = e => textSize = clamp(+e.target.value, 8, 200);
        $('#optBold').onclick = () => { textBold = !textBold; $('#optBold').classList.toggle('on', textBold); };
        $('#optItalic').onclick = () => { textItalic = !textItalic; $('#optItalic').classList.toggle('on', textItalic); };
    }
    else if (tool === 'eyedropper') {
        toolOptions.innerHTML = `<label>Нажмите на холст чтобы выбрать цвет</label>`;
    }
    else if (tool === 'fill') {
        toolOptions.innerHTML = `
            ${buildRangeRow('optTolerance', 'Допуск', 0, 255, fillTolerance)}
        `;
        $('#optTolerance').oninput = e => { fillTolerance = +e.target.value; $('#optToleranceVal').textContent = fillTolerance; };
    }
    else if (tool === 'clone') {
        toolOptions.innerHTML = `
            ${buildRangeRow('optSize', 'Размер', 1, 100, brushSize)}
            <div class="opt-sep"></div>
            ${buildRangeRow('optOpacity', 'Прозр.', 1, 100, brushOpacity)}
            <div class="opt-sep"></div>
            <label id="cloneHint" style="color:var(--accent)">Alt+клик — выбрать источник</label>
        `;
        $('#optSize').oninput = e => { brushSize = +e.target.value; $('#optSizeVal').textContent = brushSize; };
        $('#optOpacity').oninput = e => { brushOpacity = +e.target.value; $('#optOpacityVal').textContent = brushOpacity + '%'; };
    }
    else {
        toolOptions.classList.remove('visible');
    }
}

// ===================== ADJUST =====================
$('#btnAdjust').addEventListener('click', () => {
    if ($('#btnAdjust').classList.contains('active')) {
        cancelCurrentTool();
        return;
    }
    cancelCurrentTool();
    currentTool = 'adjust';
    $('#btnAdjust').classList.add('active');
    toolOptions.classList.add('visible');
    toolOptions.innerHTML = `
        <label>Яркость</label><input type="range" id="optBright" min="-100" max="100" value="0"><span id="optBrightVal" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);width:30px">0</span>
        <div class="opt-sep"></div>
        <label>Контраст</label><input type="range" id="optContrast" min="-100" max="100" value="0"><span id="optContrastVal" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);width:30px">0</span>
        <div class="opt-sep"></div>
        <label>Насыщ.</label><input type="range" id="optSatur" min="-100" max="100" value="0"><span id="optSaturVal" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);width:30px">0</span>
        <div class="opt-sep"></div>
        <button class="opt-btn primary" id="optAdjApply">Применить</button>
        <button class="opt-btn" id="optAdjCancel">Отмена</button>
    `;
    const update = () => {
        const b = 1 + adjBrightness / 100;
        const c = 1 + adjContrast / 100;
        const s = 1 + adjSaturation / 100;
        cOverlay.style.filter = `brightness(${b}) contrast(${c}) saturate(${s})`;
        ctxOverlay.clearRect(0, 0, canvasW, canvasH);
        ctxOverlay.drawImage(cMain, 0, 0);
        cMain.style.opacity = '0';
    };
    $('#optBright').oninput = e => { adjBrightness = +e.target.value; $('#optBrightVal').textContent = adjBrightness; update(); };
    $('#optContrast').oninput = e => { adjContrast = +e.target.value; $('#optContrastVal').textContent = adjContrast; update(); };
    $('#optSatur').oninput = e => { adjSaturation = +e.target.value; $('#optSaturVal').textContent = adjSaturation; update(); };
    $('#optAdjApply').onclick = applyAdjustments;
    $('#optAdjCancel').onclick = () => { cMain.style.opacity = '1'; cOverlay.style.filter = ''; clearOverlay(); cancelCurrentTool(); };
});

function applyAdjustments() {
    if (adjBrightness === 0 && adjContrast === 0 && adjSaturation === 0) {
        cMain.style.opacity = '1'; cOverlay.style.filter = ''; clearOverlay();
        cancelCurrentTool();
        return;
    }
    const _ctx = activeCtx();
    const { sx, sy, sw, sh } = getSelectionBBox();
    const imageData = _ctx.getImageData(sx, sy, sw, sh);
    const d = imageData.data;
    const br = adjBrightness / 100;
    const co = adjContrast / 100;
    const sa = adjSaturation / 100;
    const contrastFactor = (1 + co) * (1 + co);

    for (let i = 0; i < d.length; i += 4) {
        let r = d[i], g = d[i+1], b = d[i+2];
        r += 255 * br; g += 255 * br; b += 255 * br;
        r = ((r / 255 - 0.5) * contrastFactor + 0.5) * 255;
        g = ((g / 255 - 0.5) * contrastFactor + 0.5) * 255;
        b = ((b / 255 - 0.5) * contrastFactor + 0.5) * 255;
        const gray = 0.2126 * r + 0.7152 * g + 0.0722 * b;
        r = gray + (r - gray) * (1 + sa);
        g = gray + (g - gray) * (1 + sa);
        b = gray + (b - gray) * (1 + sa);
        d[i] = clamp(Math.round(r), 0, 255);
        d[i+1] = clamp(Math.round(g), 0, 255);
        d[i+2] = clamp(Math.round(b), 0, 255);
    }
    putFilteredRegion(_ctx, imageData, sx, sy, sw, sh);
    cMain.style.opacity = '1';
    cOverlay.style.filter = '';
    clearOverlay();
    compositeLayersToMain();
    pushHistory();
    cancelCurrentTool();
}

// ===================== BLUR =====================
$('#btnBlur').addEventListener('click', () => {
    if ($('#btnBlur').classList.contains('active')) { cancelCurrentTool(); return; }
    cancelCurrentTool();
    currentTool = 'blur';
    $('#btnBlur').classList.add('active');
    toolOptions.classList.add('visible');
    toolOptions.innerHTML = `
        <label>Радиус</label><input type="range" id="optBlurR" min="1" max="20" value="${blurRadius}"><span id="optBlurRVal" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);width:24px">${blurRadius}</span>
        <div class="opt-sep"></div>
        <button class="opt-btn primary" id="optBlurApply">Применить</button>
        <button class="opt-btn" id="optBlurCancel">Отмена</button>
    `;
    const updateBlur = () => {
        ctxOverlay.clearRect(0, 0, canvasW, canvasH);
        ctxOverlay.drawImage(cMain, 0, 0);
        cOverlay.style.filter = `blur(${blurRadius}px)`;
        cMain.style.opacity = '0';
    };
    updateBlur();
    $('#optBlurR').oninput = e => { blurRadius = +e.target.value; $('#optBlurRVal').textContent = blurRadius; updateBlur(); };
    $('#optBlurApply').onclick = applyBlur;
    $('#optBlurCancel').onclick = () => { cMain.style.opacity = '1'; cOverlay.style.filter = ''; clearOverlay(); cancelCurrentTool(); };
});

function applyBlur() {
    if (blurRadius === 0) { cMain.style.opacity = '1'; cOverlay.style.filter = ''; clearOverlay(); cancelCurrentTool(); return; }
    const _c = activeCanvas();
    const hasSel = hasActiveSelection();
    if (hasSel) {
        const { sx, sy, sw, sh } = getSelectionBBox();
        const tmp = document.createElement('canvas');
        tmp.width = sw; tmp.height = sh;
        const tctx = tmp.getContext('2d');
        tctx.filter = `blur(${blurRadius}px)`;
        tctx.drawImage(_c, sx, sy, sw, sh, 0, 0, sw, sh);
        const ac = activeCtx();
        ac.save();
        ac.beginPath();
        if (selPath && selPath.length > 2) {
            selPath.forEach((p, i) => i === 0 ? ac.moveTo(p.x, p.y) : ac.lineTo(p.x, p.y));
            ac.closePath();
        } else { ac.rect(sx, sy, sw, sh); }
        ac.clip();
        ac.clearRect(sx, sy, sw, sh);
        ac.drawImage(tmp, sx, sy);
        ac.restore();
    } else {
        const tmp = document.createElement('canvas');
        tmp.width = canvasW; tmp.height = canvasH;
        const tctx = tmp.getContext('2d');
        tctx.filter = `blur(${blurRadius}px)`;
        tctx.drawImage(_c, 0, 0);
        activeCtx().clearRect(0, 0, canvasW, canvasH);
        activeCtx().drawImage(tmp, 0, 0);
    }
    cMain.style.opacity = '1';
    cOverlay.style.filter = '';
    clearOverlay();
    compositeLayersToMain();
    pushHistory();
    cancelCurrentTool();
}

// ===================== SHARPEN =====================
$('#btnSharpen').addEventListener('click', () => {
    if ($('#btnSharpen').classList.contains('active')) { cancelCurrentTool(); return; }
    cancelCurrentTool();
    currentTool = 'sharpen';
    $('#btnSharpen').classList.add('active');
    toolOptions.classList.add('visible');
    toolOptions.innerHTML = `
        <label>Сила</label><input type="range" id="optSharpAmt" min="1" max="100" value="${sharpenAmount}"><span id="optSharpAmtVal" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);width:24px">${sharpenAmount}</span>
        <div class="opt-sep"></div>
        <button class="opt-btn primary" id="optSharpApply">Применить</button>
        <button class="opt-btn" id="optSharpCancel">Отмена</button>
    `;
    previewSharpen();
    $('#optSharpAmt').oninput = e => { sharpenAmount = +e.target.value; $('#optSharpAmtVal').textContent = sharpenAmount; previewSharpen(); };
    $('#optSharpApply').onclick = applySharpen;
    $('#optSharpCancel').onclick = () => { cMain.style.opacity = '1'; clearOverlay(); cancelCurrentTool(); };
});

function previewSharpen() {
    ctxOverlay.clearRect(0, 0, canvasW, canvasH);
    const tmp = document.createElement('canvas');
    tmp.width = canvasW; tmp.height = canvasH;
    const tctx = tmp.getContext('2d');
    tctx.drawImage(cMain, 0, 0);
    sharpenCanvas(tctx, canvasW, canvasH, sharpenAmount / 100);
    ctxOverlay.drawImage(tmp, 0, 0);
    cMain.style.opacity = '0';
}

function sharpenCanvas(ctx, w, h, amount) {
    const src = ctx.getImageData(0, 0, w, h);
    const dst = ctx.createImageData(w, h);
    const s = src.data, d = dst.data;
    const mix = amount;
    for (let y = 1; y < h - 1; y++) {
        for (let x = 1; x < w - 1; x++) {
            const i = (y * w + x) * 4;
            for (let c = 0; c < 3; c++) {
                const val = s[i+c] * (1 + 4*mix)
                    - s[((y-1)*w+x)*4+c] * mix
                    - s[((y+1)*w+x)*4+c] * mix
                    - s[(y*w+x-1)*4+c] * mix
                    - s[(y*w+x+1)*4+c] * mix;
                d[i+c] = clamp(Math.round(val), 0, 255);
            }
            d[i+3] = s[i+3];
        }
    }
    for (let x = 0; x < w; x++) { for (let c = 0; c < 4; c++) { d[x*4+c] = s[x*4+c]; d[((h-1)*w+x)*4+c] = s[((h-1)*w+x)*4+c]; } }
    for (let y = 0; y < h; y++) { for (let c = 0; c < 4; c++) { d[(y*w)*4+c] = s[(y*w)*4+c]; d[(y*w+w-1)*4+c] = s[(y*w+w-1)*4+c]; } }
    ctx.putImageData(dst, 0, 0);
}

function applySharpen() {
    const hasSel = hasActiveSelection();
    if (hasSel) {
        const { sx, sy, sw, sh } = getSelectionBBox();
        const tmp = document.createElement('canvas');
        tmp.width = sw; tmp.height = sh;
        const tctx = tmp.getContext('2d');
        tctx.drawImage(activeCanvas(), sx, sy, sw, sh, 0, 0, sw, sh);
        sharpenCanvas(tctx, sw, sh, sharpenAmount / 100);
        const ac = activeCtx();
        ac.save();
        ac.beginPath();
        if (selPath && selPath.length > 2) {
            selPath.forEach((p, i) => i === 0 ? ac.moveTo(p.x, p.y) : ac.lineTo(p.x, p.y));
            ac.closePath();
        } else { ac.rect(sx, sy, sw, sh); }
        ac.clip();
        ac.clearRect(sx, sy, sw, sh);
        ac.drawImage(tmp, sx, sy);
        ac.restore();
    } else {
        sharpenCanvas(activeCtx(), canvasW, canvasH, sharpenAmount / 100);
    }
    cMain.style.opacity = '1';
    clearOverlay();
    compositeLayersToMain();
    pushHistory();
    cancelCurrentTool();
}

// ===================== FILTERS =====================
function applyFilter(fn) {
    const _ctx = activeCtx();
    const { sx, sy, sw, sh } = getSelectionBBox();
    const imageData = _ctx.getImageData(sx, sy, sw, sh);
    fn(imageData.data);
    putFilteredRegion(_ctx, imageData, sx, sy, sw, sh);
    compositeLayersToMain();
    pushHistory();
}

$('#btnGrayscale').addEventListener('click', () => {
    applyFilter(d => {
        for (let i = 0; i < d.length; i += 4) {
            const g = Math.round(0.2126 * d[i] + 0.7152 * d[i+1] + 0.0722 * d[i+2]);
            d[i] = d[i+1] = d[i+2] = g;
        }
    });
});

$('#btnSepia').addEventListener('click', () => {
    applyFilter(d => {
        for (let i = 0; i < d.length; i += 4) {
            const r = d[i], g = d[i+1], b = d[i+2];
            d[i]   = clamp(Math.round(r * 0.393 + g * 0.769 + b * 0.189), 0, 255);
            d[i+1] = clamp(Math.round(r * 0.349 + g * 0.686 + b * 0.168), 0, 255);
            d[i+2] = clamp(Math.round(r * 0.272 + g * 0.534 + b * 0.131), 0, 255);
        }
    });
});

$('#btnInvert').addEventListener('click', () => {
    applyFilter(d => {
        for (let i = 0; i < d.length; i += 4) {
            d[i] = 255 - d[i]; d[i+1] = 255 - d[i+1]; d[i+2] = 255 - d[i+2];
        }
    });
});

$('#btnAutoLevels').addEventListener('click', () => {
    applyFilter(d => {
        let minR = 255, maxR = 0, minG = 255, maxG = 0, minB = 255, maxB = 0;
        for (let i = 0; i < d.length; i += 4) {
            if (d[i+3] < 10) continue;
            minR = Math.min(minR, d[i]);   maxR = Math.max(maxR, d[i]);
            minG = Math.min(minG, d[i+1]); maxG = Math.max(maxG, d[i+1]);
            minB = Math.min(minB, d[i+2]); maxB = Math.max(maxB, d[i+2]);
        }
        const rng = (v, min, max) => max > min ? clamp(Math.round((v - min) / (max - min) * 255), 0, 255) : v;
        for (let i = 0; i < d.length; i += 4) {
            d[i]   = rng(d[i],   minR, maxR);
            d[i+1] = rng(d[i+1], minG, maxG);
            d[i+2] = rng(d[i+2], minB, maxB);
        }
    });
});

// ===================== BUCKET FILL / CLONE STAMP =====================
// Handled via dispatcher.registerTool('fill',...) and dispatcher.registerTool('clone',...) above.

// ===================== RESIZE =====================
$('#btnResize').addEventListener('click', () => {
    if ($('#btnResize').classList.contains('active')) {
        cancelCurrentTool();
        return;
    }
    cancelCurrentTool();
    currentTool = 'resize';
    $('#btnResize').classList.add('active');
    resizeW = canvasW; resizeH = canvasH;
    resizeOrigW = canvasW; resizeOrigH = canvasH;
    resizeKeepRatio = true;
    toolOptions.classList.add('visible');
    toolOptions.innerHTML = `
        <label>Ш</label><input type="number" id="optResW" min="1" max="16384" value="${resizeW}" style="width:100px;">
        <button class="opt-toggle on" id="optResLink" title="Keep aspect ratio">&#x1f517;</button>
        <label>В</label><input type="number" id="optResH" min="1" max="16384" value="${resizeH}" style="width:100px;">
        <div class="opt-sep"></div>
        <button class="opt-btn primary" id="optResApply">Применить</button>
        <button class="opt-btn" id="optResCancel">Отмена</button>
    `;
    $('#optResLink').onclick = () => {
        resizeKeepRatio = !resizeKeepRatio;
        $('#optResLink').classList.toggle('on', resizeKeepRatio);
    };
    $('#optResW').onchange = e => {
        resizeW = clamp(+e.target.value, 1, 16384);
        if (resizeKeepRatio) {
            resizeH = Math.round(resizeW * resizeOrigH / resizeOrigW);
            $('#optResH').value = resizeH;
        }
    };
    $('#optResH').onchange = e => {
        resizeH = clamp(+e.target.value, 1, 16384);
        if (resizeKeepRatio) {
            resizeW = Math.round(resizeH * resizeOrigW / resizeOrigH);
            $('#optResW').value = resizeW;
        }
    };
    $('#optResApply').onclick = applyResize;
    $('#optResCancel').onclick = () => cancelCurrentTool();
});

function applyResize() {
    if (resizeW === canvasW && resizeH === canvasH) { cancelCurrentTool(); return; }
    layers.forEach(layer => {
        const tmp = document.createElement('canvas');
        tmp.width = resizeW; tmp.height = resizeH;
        const tc = tmp.getContext('2d');
        tc.imageSmoothingEnabled = true;
        tc.imageSmoothingQuality = 'high';
        tc.drawImage(layer.canvas, 0, 0, resizeW, resizeH);
        layer.canvas = tmp;
        layer.ctx = tc;
    });
    setCanvasSize(resizeW, resizeH);
    compositeLayersToMain();
    pushHistory();
    fitToViewport();
    cancelCurrentTool();
    renderLayerPanel();
}

// ===================== ROTATE / FLIP =====================
$('#btnRotCW').addEventListener('click', () => rotate90(1));
$('#btnRotCCW').addEventListener('click', () => rotate90(-1));
$('#btnFlipH').addEventListener('click', () => flip('h'));
$('#btnFlipV').addEventListener('click', () => flip('v'));

function rotate90(dir) {
    const oldW = canvasW, oldH = canvasH;
    layers.forEach(layer => {
        const tmp = document.createElement('canvas');
        tmp.width = oldH; tmp.height = oldW;
        const tc = tmp.getContext('2d');
        tc.save();
        if (dir === 1) { tc.translate(oldH, 0); tc.rotate(Math.PI / 2); }
        else { tc.translate(0, oldW); tc.rotate(-Math.PI / 2); }
        tc.drawImage(layer.canvas, 0, 0);
        tc.restore();
        layer.canvas = tmp;
        layer.ctx = tc;
    });
    setCanvasSize(oldH, oldW);
    compositeLayersToMain();
    pushHistory();
    fitToViewport();
    renderLayerPanel();
}

function flip(dir) {
    layers.forEach(layer => {
        const tmp = document.createElement('canvas');
        tmp.width = canvasW; tmp.height = canvasH;
        const tc = tmp.getContext('2d');
        tc.save();
        if (dir === 'h') { tc.translate(canvasW, 0); tc.scale(-1, 1); }
        else { tc.translate(0, canvasH); tc.scale(1, -1); }
        tc.drawImage(layer.canvas, 0, 0);
        tc.restore();
        layer.canvas = tmp;
        layer.ctx = tc;
    });
    compositeLayersToMain();
    pushHistory();
    renderLayerPanel();
}

// ===================== CROP =====================
function initCrop() {
    const margin = Math.min(canvasW, canvasH) * 0.1;
    cropRect = {
        x: margin,
        y: margin,
        w: canvasW - margin * 2,
        h: canvasH - margin * 2
    };
    drawCropOverlay();
}

function drawCropOverlay(full) {
    if (!cropRect) return;
    if (full !== false) { clearUI(); drawRulers(); drawImageBorder(); }
    const ctx = ctxUI;
    const vw = viewport.clientWidth;
    const vh = viewport.clientHeight;
    const tl = c2s(cropRect.x, cropRect.y);
    const br = c2s(cropRect.x + cropRect.w, cropRect.y + cropRect.h);
    const sx = tl.x, sy = tl.y, sw = br.x - tl.x, sh = br.y - tl.y;
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(0, 0, vw, sy);
    ctx.fillRect(0, sy, sx, sh);
    ctx.fillRect(sx + sw, sy, vw - sx - sw, sh);
    ctx.fillRect(0, sy + sh, vw, vh - sy - sh);
    ctx.strokeStyle = '#0054e6';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([6, 4]);
    ctx.strokeRect(sx, sy, sw, sh);
    ctx.setLineDash([]);
    ctx.strokeStyle = 'rgba(255,255,255,0.2)';
    ctx.lineWidth = 0.5;
    for (let i = 1; i < 3; i++) {
        ctx.beginPath();
        ctx.moveTo(sx + sw * i / 3, sy);
        ctx.lineTo(sx + sw * i / 3, sy + sh);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(sx, sy + sh * i / 3);
        ctx.lineTo(sx + sw, sy + sh * i / 3);
        ctx.stroke();
    }
    const hs = 5;
    ctx.fillStyle = '#0054e6';
    const handles = getCropHandles();
    for (const hp of Object.values(handles)) {
        const sp = c2s(hp.x, hp.y);
        ctx.fillRect(sp.x - hs, sp.y - hs, hs * 2, hs * 2);
    }
}

function getCropHandles() {
    if (!cropRect) return {};
    const {x, y, w, h} = cropRect;
    return {
        tl: {x, y}, tm: {x: x+w/2, y}, tr: {x: x+w, y},
        ml: {x, y: y+h/2}, mr: {x: x+w, y: y+h/2},
        bl: {x, y: y+h}, bm: {x: x+w/2, y: y+h}, br: {x: x+w, y: y+h}
    };
}

function hitCropHandle(cx, cy) {
    const handles = getCropHandles();
    const threshold = 8 / zoom;
    for (const [name, hp] of Object.entries(handles)) {
        if (Math.abs(cx - hp.x) < threshold && Math.abs(cy - hp.y) < threshold) return name;
    }
    if (cropRect && cx >= cropRect.x && cx <= cropRect.x + cropRect.w && cy >= cropRect.y && cy <= cropRect.y + cropRect.h) {
        return 'move';
    }
    return null;
}

function applyCrop() {
    if (!cropRect) return;
    const rx = Math.round(cropRect.x);
    const ry = Math.round(cropRect.y);
    const rw = Math.max(1, Math.round(cropRect.w));
    const rh = Math.max(1, Math.round(cropRect.h));
    layers.forEach(layer => {
        const tmp = document.createElement('canvas');
        tmp.width = rw;
        tmp.height = rh;
        const tc = tmp.getContext('2d');
        // Draw old layer content shifted: source at (rx,ry) maps to (0,0)
        tc.drawImage(layer.canvas, -rx, -ry);
        layer.canvas.width = rw;
        layer.canvas.height = rh;
        layer.ctx.clearRect(0, 0, rw, rh);
        layer.ctx.drawImage(tmp, 0, 0);
    });
    setCanvasSize(rw, rh);
    compositeLayersToMain();
    cropRect = null;
    pushHistory();
    fitToViewport();
    cancelCurrentTool();
    renderLayerPanel();
}

// ===================== TOOL HANDLERS (registered into dispatcher below) =====================
// Brush handler
dispatcher.registerTool('brush', {
    onDown(e) {
        if (!canvasW) return;
        const p = getPointerCanvas(e);
        isDrawing = true;
        drawStart = { x: p.x, y: p.y };
        lastDraw = { x: p.x, y: p.y };
        viewport.setPointerCapture(e.pointerId);
        applySelectionClip(ctxOverlay);
        ctxOverlay.lineCap = 'round';
        ctxOverlay.lineJoin = 'round';
        ctxOverlay.lineWidth = brushSize;
        ctxOverlay.globalCompositeOperation = 'source-over';
        ctxOverlay.strokeStyle = fgColor;
        ctxOverlay.globalAlpha = brushOpacity / 100;
        ctxOverlay.beginPath();
        ctxOverlay.moveTo(p.x, p.y);
        ctxOverlay.lineTo(p.x + 0.1, p.y + 0.1);
        ctxOverlay.stroke();
    },
    onMove(e) {
        if (!isDrawing) return;
        const p = getPointerCanvas(e);
        ctxOverlay.beginPath();
        ctxOverlay.moveTo(lastDraw.x, lastDraw.y);
        ctxOverlay.lineTo(p.x, p.y);
        ctxOverlay.stroke();
        lastDraw = { x: p.x, y: p.y };
    },
    onUp(e) {
        if (!isDrawing) return;
        isDrawing = false;
        if (selRect && selRect.w > 5 && selRect.h > 5) ctxOverlay.restore();
        activeCtx().save();
        if (selRect && selRect.w > 5 && selRect.h > 5) {
            activeCtx().beginPath();
            activeCtx().rect(selRect.x, selRect.y, selRect.w, selRect.h);
            activeCtx().clip();
        }
        activeCtx().globalAlpha = 1;
        activeCtx().drawImage(cOverlay, 0, 0);
        activeCtx().restore();
        ctxOverlay.globalCompositeOperation = 'source-over';
        ctxOverlay.globalAlpha = 1;
        clearOverlay();
        compositeLayersToMain();
        pushHistory();
    }
});

// Eraser handler — draws directly on activeCtx() with destination-out
dispatcher.registerTool('eraser', (function() {
    let eraserDrawing = false;
    let eraserLastPoint = null;
    return {
        onDown(e) {
            if (!canvasW) return;
            const p = getPointerCanvas(e);
            eraserDrawing = true;
            eraserLastPoint = { x: p.x, y: p.y };
            viewport.setPointerCapture(e.pointerId);
            isDrawing = true;
            activeCtx().save();
            if (selRect && selRect.w > 5 && selRect.h > 5) {
                activeCtx().beginPath();
                activeCtx().rect(selRect.x, selRect.y, selRect.w, selRect.h);
                activeCtx().clip();
            }
            activeCtx().globalCompositeOperation = 'destination-out';
            activeCtx().lineCap = 'round';
            activeCtx().lineJoin = 'round';
            activeCtx().lineWidth = brushSize;
            activeCtx().globalAlpha = brushOpacity / 100;
            activeCtx().strokeStyle = 'rgba(0,0,0,1)';
            activeCtx().beginPath();
            activeCtx().moveTo(p.x, p.y);
            activeCtx().lineTo(p.x + 0.1, p.y + 0.1);
            activeCtx().stroke();
            compositeLayersToMain();
        },
        onMove(e) {
            if (!eraserDrawing) return;
            const p = getPointerCanvas(e);
            activeCtx().beginPath();
            activeCtx().moveTo(eraserLastPoint.x, eraserLastPoint.y);
            activeCtx().lineTo(p.x, p.y);
            activeCtx().stroke();
            compositeLayersToMain();
            eraserLastPoint = { x: p.x, y: p.y };
            redrawUI();
            const sp = c2s(p.x, p.y);
            ctxUI.save();
            ctxUI.strokeStyle = '#fff';
            ctxUI.lineWidth = 1;
            ctxUI.beginPath();
            ctxUI.arc(sp.x, sp.y, brushSize / 2 * zoom, 0, Math.PI * 2);
            ctxUI.stroke();
            ctxUI.restore();
        },
        onUp(e) {
            if (!eraserDrawing) return;
            eraserDrawing = false;
            eraserLastPoint = null;
            isDrawing = false;
            activeCtx().restore();
            compositeLayersToMain();
            pushHistory();
        }
    };
})());

const CROP_HANDLE_CURSORS = {
    tl:'nwse-resize', tr:'nesw-resize', bl:'nesw-resize', br:'nwse-resize',
    tm:'ns-resize', bm:'ns-resize', ml:'ew-resize', mr:'ew-resize',
    move:'move'
};

// Crop handler
dispatcher.registerTool('crop', {
    onDown(e) {
        if (!canvasW) return;
        const p = getPointerCanvas(e);
        const handle = hitCropHandle(p.x, p.y);
        if (handle) {
            cropDragging = handle;
            cropStart = { x: p.x, y: p.y, rect: {...cropRect} };
            viewport.setPointerCapture(e.pointerId);
            viewport.style.cursor = CROP_HANDLE_CURSORS[handle] || 'crosshair';
        }
    },
    onMove(e) {
        if (!cropDragging || !cropStart) {
            // Hover: update cursor based on handle under pointer
            const p = getPointerCanvas(e);
            const hit = hitCropHandle(p.x, p.y);
            viewport.style.cursor = hit ? (CROP_HANDLE_CURSORS[hit] || 'crosshair') : 'crosshair';
            return;
        }
        const p = getPointerCanvas(e);
        const dx = p.x - cropStart.x;
        const dy = p.y - cropStart.y;
        const r = cropStart.rect;
        if (cropDragging === 'move') {
            cropRect.x = r.x + dx;
            cropRect.y = r.y + dy;
        } else {
            let nx = r.x, ny = r.y, nw = r.w, nh = r.h;
            if (cropDragging.includes('l')) { nx = r.x + dx; nw = r.w - dx; }
            if (cropDragging.includes('r')) { nw = r.w + dx; }
            if (cropDragging.includes('t')) { ny = r.y + dy; nh = r.h - dy; }
            if (cropDragging.includes('b')) { nh = r.h + dy; }
            if (nw < 10) { nw = 10; if (cropDragging.includes('l')) nx = r.x + r.w - 10; }
            if (nh < 10) { nh = 10; if (cropDragging.includes('t')) ny = r.y + r.h - 10; }
            if (cropAspect > 0) {
                if (cropDragging.includes('l') || cropDragging.includes('r')) { nh = nw / cropAspect; }
                else { nw = nh * cropAspect; }
            }
            cropRect = { x: nx, y: ny, w: nw, h: nh };
        }
        drawCropOverlay();
    },
    onUp(e) {
        if (cropDragging) {
            cropDragging = null;
            cropStart = null;
            viewport.style.cursor = 'crosshair';
        }
    }
});

// Select (move layer content) handler
dispatcher.registerTool('select', {
    onDown(e) {
        if (!canvasW) return;
        const p = getPointerCanvas(e);
        layerMoving = true;
        layerMoveStart = { x: p.x, y: p.y };
        const layer = layers[activeLayerIndex];
        layerMoveSnapshot = layer.ctx.getImageData(0, 0, canvasW, canvasH);
        viewport.setPointerCapture(e.pointerId);
    },
    onMove(e) {
        if (!layerMoving || !layerMoveStart || !layerMoveSnapshot) return;
        const p = getPointerCanvas(e);
        const dx = Math.round(p.x - layerMoveStart.x);
        const dy = Math.round(p.y - layerMoveStart.y);
        const layer = layers[activeLayerIndex];
        layer.ctx.clearRect(0, 0, canvasW, canvasH);
        layer.ctx.putImageData(layerMoveSnapshot, dx, dy);
        compositeLayersToMain();
    },
    onUp(e) {
        if (layerMoving) {
            layerMoving = false;
            layerMoveStart = null;
            layerMoveSnapshot = null;
            pushHistory();
        }
    }
});

// Eyedropper handler
dispatcher.registerTool('eyedropper', {
    onDown(e) {
        if (!canvasW) return;
        const p = getPointerCanvas(e);
        if (p.x >= 0 && p.x < canvasW && p.y >= 0 && p.y < canvasH) {
            const pixel = ctxMain.getImageData(Math.round(p.x), Math.round(p.y), 1, 1).data;
            const hex = '#' + [pixel[0],pixel[1],pixel[2]].map(c => c.toString(16).padStart(2,'0')).join('');
            fgColor = hex;
            $('#colorPicker').value = hex;
            updateColorSwatch();
        }
    },
    onMove(e) {},
    onUp(e) {}
});

// Text handler
dispatcher.registerTool('text', {
    onDown(e) {
        if (!canvasW) return;
        const p = getPointerCanvas(e);
        const hadText = ((textArea.innerText || '').trim()).length > 0;
        commitText();
        if (!hadText) {
            const textLayerIdx = findTextLayerAt(p.x, p.y);
            if (textLayerIdx >= 0) {
                const td = layers[textLayerIdx].textData;
                textFont = td.font; textSize = td.size;
                textBold = td.bold; textItalic = td.italic;
                fgColor = td.color;
                $('#colorPicker').value = fgColor;
                updateColorSwatch();
                editingTextLayerIndex = textLayerIdx;
                layers[textLayerIdx].ctx.clearRect(0, 0, canvasW, canvasH);
                compositeLayersToMain();
                showTextOverlay(td.x + td.ox, td.y + td.oy);
                textArea.textContent = td.content;
                textArea.dataset.cx = td.x + td.ox;
                textArea.dataset.cy = td.y + td.oy;
                textArea.dataset.rotation = td.rotation || 0;
                textArea.dataset.scaleX = td.scaleX || 1;
                textArea.dataset.scaleY = td.scaleY || 1;
                setTimeout(() => { textArea.focus(); selectAllTextEditor(); }, 50);
            } else {
                showTextOverlay(p.x, p.y);
            }
        } else {
            hideTextOverlay();
        }
    },
    onMove(e) {},
    onUp(e) {}
});

// Shape handler factory (line, rect, circle, arrow)
function _makeShapeHandler(toolName) {
    return {
        onDown(e) {
            if (!canvasW) return;
            const p = getPointerCanvas(e);
            isDrawing = true;
            drawStart = { x: p.x, y: p.y };
            lastDraw = { x: p.x, y: p.y };
            viewport.setPointerCapture(e.pointerId);
        },
        onMove(e) {
            if (!isDrawing) return;
            const p = getPointerCanvas(e);
            clearOverlay();
            ctxOverlay.save();
            if (selRect && selRect.w > 5 && selRect.h > 5) {
                ctxOverlay.beginPath();
                ctxOverlay.rect(selRect.x, selRect.y, selRect.w, selRect.h);
                ctxOverlay.clip();
            }
            ctxOverlay.strokeStyle = fgColor;
            ctxOverlay.fillStyle = fgColor;
            ctxOverlay.lineWidth = strokeWidth;
            ctxOverlay.lineCap = 'round';
            ctxOverlay.lineJoin = 'round';
            ctxOverlay.globalAlpha = 1;
            let ex = p.x, ey = p.y;
            const shift = e.shiftKey;
            if (toolName === 'line') {
                if (shift) { const sn = snapAngle(drawStart.x, drawStart.y, ex, ey); ex = sn.x; ey = sn.y; }
                ctxOverlay.beginPath();
                ctxOverlay.moveTo(drawStart.x, drawStart.y);
                ctxOverlay.lineTo(ex, ey);
                ctxOverlay.stroke();
            } else if (toolName === 'arrow') {
                if (shift) { const sn = snapAngle(drawStart.x, drawStart.y, ex, ey); ex = sn.x; ey = sn.y; }
                drawArrow(ctxOverlay, drawStart.x, drawStart.y, ex, ey, strokeWidth);
            } else if (toolName === 'rect') {
                let rw = ex - drawStart.x, rh = ey - drawStart.y;
                if (shift) { const s = Math.max(Math.abs(rw), Math.abs(rh)); rw = s * Math.sign(rw); rh = s * Math.sign(rh); }
                if (fillShape) ctxOverlay.fillRect(drawStart.x, drawStart.y, rw, rh);
                ctxOverlay.strokeRect(drawStart.x, drawStart.y, rw, rh);
            } else if (toolName === 'circle') {
                let rx = Math.abs(ex - drawStart.x) / 2;
                let ry = Math.abs(ey - drawStart.y) / 2;
                if (shift) { rx = ry = Math.max(rx, ry); }
                const cx = (drawStart.x + (shift ? drawStart.x + rx * 2 * Math.sign(ex - drawStart.x) : ex)) / 2;
                const cy = (drawStart.y + (shift ? drawStart.y + ry * 2 * Math.sign(ey - drawStart.y) : ey)) / 2;
                ctxOverlay.beginPath();
                ctxOverlay.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
                if (fillShape) ctxOverlay.fill();
                ctxOverlay.stroke();
            }
            ctxOverlay.restore();
        },
        onUp(e) {
            if (!isDrawing) return;
            isDrawing = false;
            const p = getPointerCanvas(e);
            let ex = p.x, ey = p.y;
            const shift = e.shiftKey;
            const ps = { type: toolName, ox: 0, oy: 0, params: { color: fgColor, strokeWidth: strokeWidth } };
            if (toolName === 'line') {
                if (shift) { const sn = snapAngle(drawStart.x, drawStart.y, ex, ey); ex = sn.x; ey = sn.y; }
                ps.params.x1 = drawStart.x; ps.params.y1 = drawStart.y;
                ps.params.x2 = ex; ps.params.y2 = ey;
            } else if (toolName === 'arrow') {
                if (shift) { const sn = snapAngle(drawStart.x, drawStart.y, ex, ey); ex = sn.x; ey = sn.y; }
                ps.params.x1 = drawStart.x; ps.params.y1 = drawStart.y;
                ps.params.x2 = ex; ps.params.y2 = ey;
            } else if (toolName === 'rect') {
                let rw = ex - drawStart.x, rh = ey - drawStart.y;
                if (shift) { const s = Math.max(Math.abs(rw), Math.abs(rh)); rw = s * Math.sign(rw); rh = s * Math.sign(rh); }
                ps.params.x = drawStart.x; ps.params.y = drawStart.y;
                ps.params.w = rw; ps.params.h = rh;
                ps.params.fill = fillShape;
            } else if (toolName === 'circle') {
                let rx = Math.abs(ex - drawStart.x) / 2;
                let ry = Math.abs(ey - drawStart.y) / 2;
                if (shift) { rx = ry = Math.max(rx, ry); }
                const cx2 = (drawStart.x + (shift ? drawStart.x + rx * 2 * Math.sign(ex - drawStart.x) : ex)) / 2;
                const cy2 = (drawStart.y + (shift ? drawStart.y + ry * 2 * Math.sign(ey - drawStart.y) : ey)) / 2;
                ps.params.cx = cx2; ps.params.cy = cy2;
                ps.params.rx = rx; ps.params.ry = ry;
                ps.params.fill = fillShape;
            }
            pendingShape = ps;
            renderPendingOnOverlay();
            showPendingOptions();
        }
    };
}
['line','rect','circle','arrow'].forEach(t => dispatcher.registerTool(t, _makeShapeHandler(t)));

// Fill handler
dispatcher.registerTool('fill', {
    onDown(e) {
        if (!canvasW) return;
        const p = getPointerCanvas(e);
        const px = Math.round(p.x), py = Math.round(p.y);
        if (px < 0 || py < 0 || px >= canvasW || py >= canvasH) return;
        floodFill(px, py, fgColor, fillTolerance);
        pushHistory();
    },
    onMove(e) {},
    onUp(e) {}
});

// Clone handler
dispatcher.registerTool('clone', (function() {
    let cloneDrawing = false;
    return {
        onDown(e) {
            if (!canvasW) return;
            const p = getPointerCanvas(e);
            if (e.altKey) {
                cloneSrc = { x: p.x, y: p.y };
                cloneOffset = null;
                cloneSrcSet = true;
                if ($('#cloneHint')) $('#cloneHint').textContent = `Источник: ${Math.round(p.x)}, ${Math.round(p.y)}`;
                redrawUI();
                return;
            }
            if (!cloneSrc) return;
            if (!cloneOffset) {
                cloneOffset = { dx: cloneSrc.x - p.x, dy: cloneSrc.y - p.y };
            }
            cloneDrawing = true;
            viewport.setPointerCapture(e.pointerId);
            _clonePaint(p.x, p.y);
        },
        onMove(e) {
            const p = getPointerCanvas(e);
            if (cloneDrawing) {
                _clonePaint(p.x, p.y);
                redrawUI();
                _drawCloneCrosshair(p.x + cloneOffset.dx, p.y + cloneOffset.dy);
                return;
            }
            if (!canvasW) return;
            redrawUI();
            if (cloneSrc) {
                const off = cloneOffset || { dx: cloneSrc.x - p.x, dy: cloneSrc.y - p.y };
                _drawCloneCrosshair(p.x + off.dx, p.y + off.dy);
            }
            const sp = c2s(p.x, p.y);
            ctxUI.save();
            ctxUI.strokeStyle = '#fff';
            ctxUI.lineWidth = 1;
            ctxUI.beginPath();
            ctxUI.arc(sp.x, sp.y, brushSize / 2 * zoom, 0, Math.PI * 2);
            ctxUI.stroke();
            ctxUI.restore();
        },
        onUp(e) {
            if (cloneDrawing) { cloneDrawing = false; pushHistory(); }
        }
    };
})());

dispatcher.registerTool('lasso', (function() {
    let lassoPoints = [], lassoDrawing = false;
    return {
        onDown(e) {
            if (!canvasW || e.button !== 0) return;
            lassoPoints = [];
            lassoDrawing = true;
            selPath = null;
            selRect = null;
            stopMarchingAnts();
            const p = getPointerCanvas(e);
            lassoPoints.push(p);
            viewport.setPointerCapture(e.pointerId);
            clearUI();
            toolOptions.classList.remove('visible');
            toolOptions.innerHTML = '';
        },
        onMove(e) {
            if (!lassoDrawing) return;
            const p = getPointerCanvas(e);
            const last = lassoPoints[lassoPoints.length - 1];
            if (Math.hypot(p.x - last.x, p.y - last.y) > 2) {
                lassoPoints.push(p);
                drawLassoInProgress(lassoPoints);
            }
        },
        onUp(e) {
            if (!lassoDrawing) return;
            lassoDrawing = false;
            if (lassoPoints.length < 3) { lassoPoints = []; return; }
            selPath = lassoPoints.slice();
            lassoPoints = [];
            redrawUI();
            showLassoOptions();
        }
    };
})());

function _clonePaint(destX, destY) {
    const srcX = destX + cloneOffset.dx;
    const srcY = destY + cloneOffset.dy;
    const r = brushSize / 2;
    const sx = Math.round(srcX - r), sy = Math.round(srcY - r);
    const size = Math.round(brushSize);
    if (sx < 0 || sy < 0 || sx + size > canvasW || sy + size > canvasH) return;
    const srcData = ctxMain.getImageData(sx, sy, size, size);
    const tmp = document.createElement('canvas');
    tmp.width = size; tmp.height = size;
    const tctx = tmp.getContext('2d');
    tctx.beginPath();
    tctx.arc(size/2, size/2, size/2, 0, Math.PI * 2);
    tctx.clip();
    tctx.putImageData(srcData, 0, 0);
    activeCtx().save();
    if (selRect && selRect.w > 5 && selRect.h > 5) {
        activeCtx().beginPath();
        activeCtx().rect(selRect.x, selRect.y, selRect.w, selRect.h);
        activeCtx().clip();
    }
    activeCtx().globalAlpha = brushOpacity / 100;
    activeCtx().drawImage(tmp, Math.round(destX - r), Math.round(destY - r));
    activeCtx().restore();
    compositeLayersToMain();
}

function _drawCloneCrosshair(cx, cy) {
    const sp = c2s(cx, cy);
    const ctx = ctxUI;
    const len = 10;
    ctx.save();
    ctx.strokeStyle = '#ff5252';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(sp.x - len, sp.y); ctx.lineTo(sp.x + len, sp.y);
    ctx.moveTo(sp.x, sp.y - len); ctx.lineTo(sp.x, sp.y + len);
    ctx.stroke();
    ctx.beginPath();
    ctx.arc(sp.x, sp.y, brushSize / 2 * zoom, 0, Math.PI * 2);
    ctx.stroke();
    ctx.restore();
}

// ===================== SELECTION + PENDING registered into dispatcher =====================

// ===================== SELECTION HELPERS =====================
function getSelHandles() {
    if (!selRect) return {};
    const {x, y, w, h} = selRect;
    return {
        tl:{x,y}, tm:{x:x+w/2,y}, tr:{x:x+w,y},
        ml:{x,y:y+h/2}, mr:{x:x+w,y:y+h/2},
        bl:{x,y:y+h}, bm:{x:x+w/2,y:y+h}, br:{x:x+w,y:y+h}
    };
}

function hitSelHandle(cx, cy) {
    if (!selRect) return null;
    const handles = getSelHandles();
    const threshold = 8 / zoom;
    for (const [name, hp] of Object.entries(handles)) {
        if (Math.abs(cx - hp.x) < threshold && Math.abs(cy - hp.y) < threshold) return name;
    }
    if (cx >= selRect.x && cx <= selRect.x + selRect.w && cy >= selRect.y && cy <= selRect.y + selRect.h) return 'move';
    return null;
}

const SEL_HANDLE_CURSORS = {
    tl:'nwse-resize', tr:'nesw-resize', bl:'nesw-resize', br:'nwse-resize',
    tm:'ns-resize', bm:'ns-resize', ml:'ew-resize', mr:'ew-resize',
    move:'grab'
};

dispatcher.registerSelection({
    onDown(e) {
        if (!canvasW || e.button !== 0) return;
        const p = getPointerCanvas(e);
        if (selRect && selRect.w > 5 && selRect.h > 5) {
            const hit = hitSelHandle(p.x, p.y);
            if (hit) {
                selDragging = true;
                selDragMode = hit;
                selStart = {x: p.x, y: p.y};
                selStartRect = {...selRect};
                viewport.setPointerCapture(e.pointerId);
                return;
            }
        }
        selDragging = true;
        selDragMode = 'new';
        selStart = {x: p.x, y: p.y};
        selRect = {x: p.x, y: p.y, w: 0, h: 0};
        selStartRect = null;
        viewport.setPointerCapture(e.pointerId);
        clearUI();
        toolOptions.classList.remove('visible');
        toolOptions.innerHTML = '';
    },
    onMove(e) {
        const p = getPointerCanvas(e);
        if (canvasW && selRect && selRect.w > 5 && !selDragging) {
            const hit = hitSelHandle(p.x, p.y);
            viewport.style.cursor = hit ? (SEL_HANDLE_CURSORS[hit] || 'default') : 'default';
        }
        if (!selDragging) return;
        viewport.style.cursor = selDragMode === 'move' ? 'grabbing' : (SEL_HANDLE_CURSORS[selDragMode] || 'crosshair');
        if (selDragMode === 'new') {
            const x1 = Math.min(selStart.x, p.x);
            const y1 = Math.min(selStart.y, p.y);
            const x2 = Math.max(selStart.x, p.x);
            const y2 = Math.max(selStart.y, p.y);
            selRect = {x: x1, y: y1, w: x2 - x1, h: y2 - y1};
        } else if (selDragMode === 'move') {
            const dx = p.x - selStart.x;
            const dy = p.y - selStart.y;
            selRect.x = clamp(selStartRect.x + dx, 0, canvasW - selStartRect.w);
            selRect.y = clamp(selStartRect.y + dy, 0, canvasH - selStartRect.h);
        } else {
            const dx = p.x - selStart.x;
            const dy = p.y - selStart.y;
            const r = selStartRect;
            let nx = r.x, ny = r.y, nw = r.w, nh = r.h;
            if (selDragMode.includes('l')) { nx = r.x + dx; nw = r.w - dx; }
            if (selDragMode.includes('r')) { nw = r.w + dx; }
            if (selDragMode !== 'ml' && selDragMode !== 'mr' && selDragMode.includes('t')) { ny = r.y + dy; nh = r.h - dy; }
            if (selDragMode !== 'tm' && selDragMode !== 'bm' && selDragMode.includes('b')) { nh = r.h + dy; }
            if (nw < 0) { nx = nx + nw; nw = -nw; }
            if (nh < 0) { ny = ny + nh; nh = -nh; }
            if (nx < 0) { nw += nx; nx = 0; }
            if (ny < 0) { nh += ny; ny = 0; }
            if (nx + nw > canvasW) nw = canvasW - nx;
            if (ny + nh > canvasH) nh = canvasH - ny;
            selRect = {x: nx, y: ny, w: Math.max(nw, 2), h: Math.max(nh, 2)};
        }
        drawSelectionRect();
    },
    onUp(e) {
        if (!selDragging) return;
        selDragging = false;
        selDragMode = null;
        selStartRect = null;
        if (selRect && selRect.w > 5 && selRect.h > 5) {
            selRect.x = clamp(selRect.x, 0, canvasW);
            selRect.y = clamp(selRect.y, 0, canvasH);
            selRect.w = clamp(selRect.w, 0, canvasW - selRect.x);
            selRect.h = clamp(selRect.h, 0, canvasH - selRect.y);
            drawSelectionRect();
            showSelectionOptions();
        } else {
            selRect = null;
            clearUI();
            toolOptions.classList.remove('visible');
            toolOptions.innerHTML = '';
        }
    }
});

function drawSelectionRect(full) {
    if (!selRect) return;
    if (full !== false) { clearUI(); drawRulers(); drawImageBorder(); }
    const ctx = ctxUI;
    const vw = viewport.clientWidth;
    const vh = viewport.clientHeight;
    const tl = c2s(selRect.x, selRect.y);
    const br = c2s(selRect.x + selRect.w, selRect.y + selRect.h);
    const sx = tl.x, sy = tl.y, sw = br.x - tl.x, sh = br.y - tl.y;
    ctx.fillStyle = 'rgba(0,0,0,0.4)';
    ctx.fillRect(0, 0, vw, sy);
    ctx.fillRect(0, sy, sx, sh);
    ctx.fillRect(sx + sw, sy, vw - sx - sw, sh);
    ctx.fillRect(0, sy + sh, vw, vh - sy - sh);
    ctx.strokeStyle = '#0054e6';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([6, 4]);
    ctx.strokeRect(sx, sy, sw, sh);
    ctx.setLineDash([]);
    const hs = 4;
    ctx.fillStyle = '#0054e6';
    ctx.strokeStyle = '#000';
    ctx.lineWidth = 1;
    for (const hp of Object.values(getSelHandles())) {
        const sp = c2s(hp.x, hp.y);
        ctx.fillRect(sp.x - hs, sp.y - hs, hs * 2, hs * 2);
        ctx.strokeRect(sp.x - hs, sp.y - hs, hs * 2, hs * 2);
    }
    const lw = Math.round(selRect.w);
    const lh = Math.round(selRect.h);
    ctx.font = '12px JetBrains Mono, monospace';
    ctx.fillStyle = '#0054e6';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'bottom';
    ctx.fillText(lw + ' \u00d7 ' + lh, sx + sw/2, sy - 6);
}

function showSelectionOptions() {
    toolOptions.innerHTML = `
        <label>${Math.round(selRect.w)} x ${Math.round(selRect.h)}</label>
        <div class="opt-sep"></div>
        <button class="opt-btn primary" id="optSelCrop">Обрезать</button>
        <button class="opt-btn" id="optSelCancel">Отмена</button>
    `;
    toolOptions.classList.add('visible');
    $('#optSelCrop').onclick = applySelectionCrop;
    $('#optSelCancel').onclick = clearSelection;
}

function applySelectionCrop() {
    if (!selRect) return;
    const rx = Math.round(clamp(selRect.x, 0, canvasW));
    const ry = Math.round(clamp(selRect.y, 0, canvasH));
    const rw = Math.round(clamp(selRect.w, 1, canvasW - rx));
    const rh = Math.round(clamp(selRect.h, 1, canvasH - ry));
    layers.forEach(layer => {
        const data = layer.ctx.getImageData(rx, ry, rw, rh);
        layer.canvas.width = rw;
        layer.canvas.height = rh;
        layer.ctx.putImageData(data, 0, 0);
    });
    setCanvasSize(rw, rh);
    compositeLayersToMain();
    selRect = null;
    clearUI();
    toolOptions.classList.remove('visible');
    toolOptions.innerHTML = '';
    pushHistory();
    fitToViewport();
    renderLayerPanel();
}

function clearSelection() {
    selRect = null;
    selPath = null;
    selDragMode = null;
    selStartRect = null;
    stopMarchingAnts();
    redrawUI();
    toolOptions.classList.remove('visible');
    toolOptions.innerHTML = '';
}

function applySelectionClip(ctx) {
    if (selPath && selPath.length > 2) {
        ctx.save();
        ctx.beginPath();
        selPath.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
        ctx.closePath();
        ctx.clip();
        return true;
    }
    if (!selRect || selRect.w <= 5 || selRect.h <= 5) return false;
    ctx.save();
    ctx.beginPath();
    ctx.rect(selRect.x, selRect.y, selRect.w, selRect.h);
    ctx.clip();
    return true;
}

// ===================== SELECTION HELPERS =====================

function hasActiveSelection() {
    return (selPath && selPath.length > 2) || (selRect && selRect.w > 5 && selRect.h > 5);
}

function getSelectionBBox() {
    if (selPath && selPath.length > 2) {
        const xs = selPath.map(p => p.x), ys = selPath.map(p => p.y);
        const sx = Math.max(0, Math.floor(Math.min(...xs)));
        const sy = Math.max(0, Math.floor(Math.min(...ys)));
        const sw = Math.min(canvasW - sx, Math.ceil(Math.max(...xs)) - sx);
        const sh = Math.min(canvasH - sy, Math.ceil(Math.max(...ys)) - sy);
        return { sx, sy, sw: Math.max(1, sw), sh: Math.max(1, sh) };
    }
    if (selRect && selRect.w > 5 && selRect.h > 5) {
        const sx = Math.round(clamp(selRect.x, 0, canvasW));
        const sy = Math.round(clamp(selRect.y, 0, canvasH));
        const sw = Math.round(clamp(selRect.w, 1, canvasW - sx));
        const sh = Math.round(clamp(selRect.h, 1, canvasH - sy));
        return { sx, sy, sw, sh };
    }
    return { sx: 0, sy: 0, sw: canvasW, sh: canvasH };
}

// Composite filtered temp canvas back to ctx, clipped to active selection.
// Use instead of ctx.putImageData when selection is lasso (putImageData ignores clip).
function putFilteredRegion(ctx, imageData, sx, sy, sw, sh) {
    if (selPath && selPath.length > 2) {
        const tmp = document.createElement('canvas');
        tmp.width = sw; tmp.height = sh;
        tmp.getContext('2d').putImageData(imageData, 0, 0);
        ctx.save();
        ctx.beginPath();
        selPath.forEach((p, i) => i === 0 ? ctx.moveTo(p.x, p.y) : ctx.lineTo(p.x, p.y));
        ctx.closePath();
        ctx.clip();
        ctx.clearRect(sx, sy, sw, sh);
        ctx.drawImage(tmp, sx, sy);
        ctx.restore();
    } else {
        ctx.putImageData(imageData, sx, sy);
    }
}

// Point-in-polygon (ray casting) for lasso masking
function pointInPolygon(x, y, pts) {
    let inside = false;
    for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
        const xi = pts[i].x, yi = pts[i].y, xj = pts[j].x, yj = pts[j].y;
        if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
    }
    return inside;
}

// ===================== LASSO UI =====================

function drawLassoInProgress(pts) {
    clearUI(); drawRulers(); drawImageBorder();
    const ctx = ctxUI;
    ctx.save();
    ctx.strokeStyle = '#0054e6';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([6, 4]);
    ctx.beginPath();
    pts.forEach((p, i) => { const s = c2s(p.x, p.y); i === 0 ? ctx.moveTo(s.x, s.y) : ctx.lineTo(s.x, s.y); });
    const s0 = c2s(pts[0].x, pts[0].y);
    ctx.lineTo(s0.x, s0.y);
    ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();
}

function drawLassoOutline() {
    if (!selPath || selPath.length < 3) return;
    const ctx = ctxUI;
    ctx.save();
    const buildPath = () => {
        ctx.beginPath();
        selPath.forEach((p, i) => { const s = c2s(p.x, p.y); i === 0 ? ctx.moveTo(s.x, s.y) : ctx.lineTo(s.x, s.y); });
        ctx.closePath();
    };
    // White base stroke
    ctx.strokeStyle = 'rgba(255,255,255,0.7)';
    ctx.lineWidth = 1.5;
    ctx.setLineDash([6, 4]);
    ctx.lineDashOffset = -(marchOffset + 6);
    buildPath(); ctx.stroke();
    // Blue top stroke
    ctx.strokeStyle = '#0054e6';
    ctx.lineDashOffset = -marchOffset;
    buildPath(); ctx.stroke();
    ctx.setLineDash([]);
    ctx.restore();
}

function startMarchingAnts() {
    if (marchRaf) return;
    function step() {
        if (!selPath || selPath.length < 3) { marchRaf = 0; return; }
        marchOffset = (marchOffset + 0.4) % 20;
        clearUI(); drawRulers(); drawImageBorder();
        drawLassoOutline();
        marchRaf = requestAnimationFrame(step);
    }
    marchRaf = requestAnimationFrame(step);
}

function stopMarchingAnts() {
    if (marchRaf) { cancelAnimationFrame(marchRaf); marchRaf = 0; }
}

function showLassoOptions() {
    const { sw, sh } = getSelectionBBox();
    toolOptions.innerHTML = `
        <label>${sw} × ${sh}</label>
        <div class="opt-sep"></div>
        <button class="opt-btn primary" id="optLassoCrop">Обрезать</button>
        <button class="opt-btn" id="optLassoCancel">Отмена</button>
    `;
    toolOptions.classList.add('visible');
    $('#optLassoCrop').onclick = applyLassoCrop;
    $('#optLassoCancel').onclick = clearSelection;
    startMarchingAnts();
}

function applyLassoCrop() {
    if (!selPath || selPath.length < 3) return;
    const { sx, sy, sw, sh } = getSelectionBBox();
    layers.forEach(layer => {
        const tmp = document.createElement('canvas');
        tmp.width = sw; tmp.height = sh;
        const tctx = tmp.getContext('2d');
        tctx.save();
        tctx.beginPath();
        selPath.forEach((p, i) => i === 0 ? tctx.moveTo(p.x - sx, p.y - sy) : tctx.lineTo(p.x - sx, p.y - sy));
        tctx.closePath();
        tctx.clip();
        tctx.drawImage(layer.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
        tctx.restore();
        layer.canvas = tmp;
        layer.ctx = tctx;
    });
    selPath = null;
    stopMarchingAnts();
    setCanvasSize(sw, sh);
    compositeLayersToMain();
    clearUI();
    toolOptions.classList.remove('visible');
    toolOptions.innerHTML = '';
    pushHistory();
    fitToViewport();
    renderLayerPanel();
}

// fill handler already registered above via dispatcher.registerTool('fill', ...)

function floodFill(startX, startY, hexColor, tolerance) {
    const imageData = activeCtx().getImageData(0, 0, canvasW, canvasH);
    const d = imageData.data;
    const w = canvasW, h = canvasH;
    const fr = parseInt(hexColor.slice(1,3), 16);
    const fg = parseInt(hexColor.slice(3,5), 16);
    const fb = parseInt(hexColor.slice(5,7), 16);
    const idx = (startY * w + startX) * 4;
    const tr = d[idx], tg = d[idx+1], tb = d[idx+2], ta = d[idx+3];
    if (tr === fr && tg === fg && tb === fb && ta === 255) return;
    const tol2 = tolerance * tolerance;
    const visited = new Uint8Array(w * h);
    const { sx: selMinX, sy: selMinY, sw: _sw, sh: _sh } = getSelectionBBox();
    const selMaxX = selMinX + _sw, selMaxY = selMinY + _sh;
    const useLasso = selPath && selPath.length > 2;
    const stack = [startX, startY];
    function match(i) {
        const dr = d[i] - tr, dg = d[i+1] - tg, db = d[i+2] - tb, da = d[i+3] - ta;
        return (dr*dr + dg*dg + db*db + da*da) <= tol2 * 4;
    }
    while (stack.length > 0) {
        const y = stack.pop(), x = stack.pop();
        const pi = y * w + x;
        if (x < selMinX || x >= selMaxX || y < selMinY || y >= selMaxY || visited[pi]) continue;
        if (useLasso && !pointInPolygon(x, y, selPath)) continue;
        const i4 = pi * 4;
        if (!match(i4)) continue;
        visited[pi] = 1;
        d[i4] = fr; d[i4+1] = fg; d[i4+2] = fb; d[i4+3] = 255;
        stack.push(x+1, y, x-1, y, x, y+1, x, y-1);
    }
    activeCtx().putImageData(imageData, 0, 0);
    compositeLayersToMain();
}

// clone handler already registered above via dispatcher.registerTool('clone', ...)

// ===================== ARROW DRAWING =====================
function drawArrow(ctx, x1, y1, x2, y2, width) {
    const angle = Math.atan2(y2 - y1, x2 - x1);
    const headLen = Math.max(width * 4, 12);

    ctx.beginPath();
    ctx.moveTo(x1, y1);
    ctx.lineTo(x2, y2);
    ctx.stroke();

    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - headLen * Math.cos(angle - Math.PI / 6), y2 - headLen * Math.sin(angle - Math.PI / 6));
    ctx.lineTo(x2 - headLen * Math.cos(angle + Math.PI / 6), y2 - headLen * Math.sin(angle + Math.PI / 6));
    ctx.closePath();
    ctx.fill();
}

function snapAngle(x1, y1, x2, y2) {
    const angle = Math.atan2(y2 - y1, x2 - x1);
    const snapped = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4);
    const dist = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
    return { x: x1 + dist * Math.cos(snapped), y: y1 + dist * Math.sin(snapped) };
}

// ===================== BRUSH CURSOR =====================
viewport.addEventListener('pointermove', e => {
    if (!canvasW) return;
    if ((currentTool === 'brush' || currentTool === 'eraser') && !isPanning) {
        const p = getPointerCanvas(e);
        if (!isDrawing || currentTool === 'brush') {
            redrawUI();
            const sp = c2s(p.x, p.y);
            ctxUI.save();
            ctxUI.strokeStyle = currentTool === 'eraser' ? '#ff5252' : fgColor;
            ctxUI.lineWidth = 1;
            ctxUI.beginPath();
            ctxUI.arc(sp.x, sp.y, brushSize / 2 * zoom, 0, Math.PI * 2);
            ctxUI.stroke();
            ctxUI.restore();
        }
    }
});

// ===================== PENDING SHAPE (MOVEABLE + TRANSFORMABLE) =====================

function getPendingBBox(ps) {
    if (!ps) return null;
    const ox = ps.ox || 0, oy = ps.oy || 0;
    const sw = ps.params.strokeWidth || 2;
    const pad = sw / 2 + 4;
    if (ps.type === 'line' || ps.type === 'arrow') {
        const p = ps.params;
        const headPad = ps.type === 'arrow' ? Math.max(sw * 4, 12) : 0;
        const xMin = Math.min(p.x1, p.x2) - pad - headPad;
        const yMin = Math.min(p.y1, p.y2) - pad - headPad;
        const xMax = Math.max(p.x1, p.x2) + pad + headPad;
        const yMax = Math.max(p.y1, p.y2) + pad + headPad;
        return { x: xMin + ox, y: yMin + oy, w: xMax - xMin, h: yMax - yMin };
    }
    if (ps.type === 'rect') {
        const p = ps.params;
        const x = Math.min(p.x, p.x + p.w);
        const y = Math.min(p.y, p.y + p.h);
        return { x: x + ox - pad, y: y + oy - pad, w: Math.abs(p.w) + 2 * pad, h: Math.abs(p.h) + 2 * pad };
    }
    if (ps.type === 'circle') {
        const p = ps.params;
        return { x: p.cx - p.rx + ox - pad, y: p.cy - p.ry + oy - pad, w: 2 * p.rx + 2 * pad, h: 2 * p.ry + 2 * pad };
    }
    if (ps.type === 'text') {
        const p = ps.params;
        ctxOverlay.save();
        const fontStyle = (p.italic ? 'italic ' : '') + (p.bold ? '700 ' : '400 ');
        ctxOverlay.font = fontStyle + p.size + 'px ' + p.font;
        const lines = p.text.split('\n');
        let maxW = 0;
        lines.forEach(line => { maxW = Math.max(maxW, ctxOverlay.measureText(line).width); });
        ctxOverlay.restore();
        const scaleX = ps.scaleX || 1, scaleY = ps.scaleY || 1;
        return { x: p.x + ox - 2, y: p.y + oy - 2, w: (maxW + 4) * scaleX, h: (lines.length * p.lineHeight + 4) * scaleY };
    }
    return null;
}

function getPendingPivot(ps) {
    const bbox = getPendingBBox(ps);
    if (!bbox) return { x: ps.ox || 0, y: ps.oy || 0 };
    return { x: bbox.x + bbox.w / 2, y: bbox.y + bbox.h / 2 };
}

function transformPoint(x, y, pivotX, pivotY, rotation, scaleX, scaleY) {
    const cos = Math.cos(rotation || 0), sin = Math.sin(rotation || 0);
    const dx = (x - pivotX) * (scaleX || 1);
    const dy = (y - pivotY) * (scaleY || 1);
    return { x: pivotX + dx * cos - dy * sin, y: pivotY + dx * sin + dy * cos };
}

function getPendingHandles(ps) {
    const bbox = getPendingBBox(ps);
    if (!bbox) return [];
    const pivot = getPendingPivot(ps);
    const rot = ps.rotation || 0;
    const L = bbox.x, R = bbox.x + bbox.w, T = bbox.y, B = bbox.y + bbox.h;
    const tp = (x, y) => transformPoint(x, y, pivot.x, pivot.y, rot, 1, 1);
    const handles = [
        { ...tp(L, T), id: 'tl' }, { ...tp(R, T), id: 'tr' },
        { ...tp(L, B), id: 'bl' }, { ...tp(R, B), id: 'br' },
    ];
    const topMid = tp((L + R) / 2, T);
    const rotDist = 30 / zoom;
    handles.push({
        x: topMid.x + Math.cos(rot - Math.PI / 2) * rotDist,
        y: topMid.y + Math.sin(rot - Math.PI / 2) * rotDist,
        id: 'rot'
    });
    return handles;
}

function hitPendingHandle(cx, cy) {
    if (!pendingShape) return null;
    const handles = getPendingHandles(pendingShape);
    const hitR = 12 / zoom;
    for (const h of handles) {
        if (Math.hypot(cx - h.x, cy - h.y) <= hitR) return h.id;
    }
    return null;
}

function hitPendingShape(cx, cy) {
    if (!pendingShape) return false;
    const bbox = getPendingBBox(pendingShape);
    if (!bbox) return false;
    const rot = pendingShape.rotation || 0;
    if (rot === 0) return cx >= bbox.x && cx <= bbox.x + bbox.w && cy >= bbox.y && cy <= bbox.y + bbox.h;
    const pivotX = bbox.x + bbox.w / 2, pivotY = bbox.y + bbox.h / 2;
    const cos = Math.cos(-rot), sin = Math.sin(-rot);
    const dx = cx - pivotX, dy = cy - pivotY;
    const nx = pivotX + dx * cos - dy * sin, ny = pivotY + dx * sin + dy * cos;
    return nx >= bbox.x && nx <= bbox.x + bbox.w && ny >= bbox.y && ny <= bbox.y + bbox.h;
}

function drawPendingShapeOnCtx(ctx, ps) {
    if (!ps) return;
    const ox = ps.ox || 0, oy = ps.oy || 0;
    const rot = ps.rotation || 0;
    const scaleX = ps.scaleX || 1, scaleY = ps.scaleY || 1;
    ctx.save();
    if (rot !== 0) {
        const bbox = getPendingBBox(ps);
        if (bbox) {
            const pvX = bbox.x + bbox.w / 2, pvY = bbox.y + bbox.h / 2;
            ctx.translate(pvX, pvY);
            ctx.rotate(rot);
            ctx.translate(-pvX, -pvY);
        }
    }
    ctx.strokeStyle = ps.params.color;
    ctx.fillStyle = ps.params.color;
    ctx.lineWidth = ps.params.strokeWidth || 2;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    if (ps.type === 'line') {
        ctx.beginPath();
        ctx.moveTo(ps.params.x1 + ox, ps.params.y1 + oy);
        ctx.lineTo(ps.params.x2 + ox, ps.params.y2 + oy);
        ctx.stroke();
    } else if (ps.type === 'arrow') {
        drawArrow(ctx, ps.params.x1 + ox, ps.params.y1 + oy, ps.params.x2 + ox, ps.params.y2 + oy, ps.params.strokeWidth);
    } else if (ps.type === 'rect') {
        const p = ps.params;
        if (p.fill) ctx.fillRect(p.x + ox, p.y + oy, p.w, p.h);
        ctx.strokeRect(p.x + ox, p.y + oy, p.w, p.h);
    } else if (ps.type === 'circle') {
        const p = ps.params;
        ctx.beginPath();
        ctx.ellipse(p.cx + ox, p.cy + oy, p.rx, p.ry, 0, 0, Math.PI * 2);
        if (p.fill) ctx.fill();
        ctx.stroke();
    } else if (ps.type === 'text') {
        const p = ps.params;
        const fontStyle = (p.italic ? 'italic ' : '') + (p.bold ? '700 ' : '400 ');
        ctx.font = fontStyle + p.size + 'px ' + p.font;
        ctx.textBaseline = 'top';
        const lines = p.text.split('\n');
        if (scaleX !== 1 || scaleY !== 1) {
            ctx.translate(p.x + ox, p.y + oy);
            ctx.scale(scaleX, scaleY);
            ctx.translate(-(p.x + ox), -(p.y + oy));
        }
        lines.forEach((line, i) => {
            ctx.fillText(line, p.x + ox, p.y + oy + i * p.lineHeight);
        });
    }
    ctx.restore();
}

function renderPendingOnOverlay() {
    clearOverlay();
    if (!pendingShape) return;
    drawPendingShapeOnCtx(ctxOverlay, pendingShape);
    redrawUI();
}

function drawPendingBBoxOnUI() {
    if (!pendingShape) return;
    const bbox = getPendingBBox(pendingShape);
    if (!bbox) return;
    const rot = pendingShape.rotation || 0;
    const pivot = getPendingPivot(pendingShape);
    const ctx = ctxUI;
    ctx.save();

    const L = bbox.x, R = bbox.x + bbox.w, T = bbox.y, B = bbox.y + bbox.h;
    const tp = (x, y) => { const t = transformPoint(x, y, pivot.x, pivot.y, rot, 1, 1); return c2s(t.x, t.y); };
    const tl = tp(L, T), tr = tp(R, T), bl = tp(L, B), br = tp(R, B);

    ctx.beginPath();
    ctx.moveTo(tl.x, tl.y); ctx.lineTo(tr.x, tr.y);
    ctx.lineTo(br.x, br.y); ctx.lineTo(bl.x, bl.y);
    ctx.closePath();
    ctx.fillStyle = 'rgba(0,84,230,0.07)';
    ctx.fill();
    ctx.strokeStyle = '#0054e6';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 3]);
    ctx.stroke();
    ctx.setLineDash([]);

    const handles = getPendingHandles(pendingShape);
    const topMidCanvas = transformPoint((L + R) / 2, T, pivot.x, pivot.y, rot, 1, 1);
    const topMidScreen = c2s(topMidCanvas.x, topMidCanvas.y);

    handles.forEach(h => {
        const s = c2s(h.x, h.y);
        ctx.beginPath();
        if (h.id === 'rot') {
            ctx.moveTo(topMidScreen.x, topMidScreen.y);
            ctx.lineTo(s.x, s.y);
            ctx.strokeStyle = '#0054e6';
            ctx.lineWidth = 1;
            ctx.stroke();
            ctx.beginPath();
            ctx.arc(s.x, s.y, 6, 0, Math.PI * 2);
            ctx.fillStyle = '#fff';
            ctx.fill();
            ctx.strokeStyle = '#0054e6';
            ctx.lineWidth = 1.5;
            ctx.stroke();
        } else {
            ctx.rect(s.x - 5, s.y - 5, 10, 10);
            ctx.fillStyle = '#0054e6';
            ctx.fill();
            ctx.strokeStyle = '#000';
            ctx.lineWidth = 1;
            ctx.stroke();
        }
    });
    ctx.restore();
}

function commitPendingShape() {
    if (!pendingShape) return;

    if (pendingShape.type === 'text') {
        let textLayer;
        if (editingTextLayerIndex >= 0 && editingTextLayerIndex < layers.length) {
            textLayer = layers[editingTextLayerIndex];
            textLayer.ctx.clearRect(0, 0, canvasW, canvasH);
            editingTextLayerIndex = -1;
        } else {
            textLayer = createLayer('Текст', canvasW, canvasH);
            layers.splice(activeLayerIndex + 1, 0, textLayer);
            activeLayerIndex++;
        }
        textLayer.textData = {
            content: pendingShape.params.text,
            x: pendingShape.params.x, y: pendingShape.params.y,
            font: pendingShape.params.font, size: pendingShape.params.size,
            bold: pendingShape.params.bold, italic: pendingShape.params.italic,
            color: pendingShape.params.color, lineHeight: pendingShape.params.lineHeight,
            ox: pendingShape.ox || 0, oy: pendingShape.oy || 0,
            rotation: pendingShape.rotation || 0,
            scaleX: pendingShape.scaleX || 1, scaleY: pendingShape.scaleY || 1,
        };
        textLayer.ctx.save();
        if (selRect && selRect.w > 5 && selRect.h > 5) {
            textLayer.ctx.beginPath();
            textLayer.ctx.rect(selRect.x, selRect.y, selRect.w, selRect.h);
            textLayer.ctx.clip();
        }
        drawPendingShapeOnCtx(textLayer.ctx, pendingShape);
        textLayer.ctx.restore();
    } else {
        const _ctx = activeCtx();
        _ctx.save();
        if (selRect && selRect.w > 5 && selRect.h > 5) {
            _ctx.beginPath();
            _ctx.rect(selRect.x, selRect.y, selRect.w, selRect.h);
            _ctx.clip();
        }
        drawPendingShapeOnCtx(_ctx, pendingShape);
        _ctx.restore();
    }

    clearOverlay();
    pendingShape = null;
    pendingDragging = false;
    pendingDragStart = null;
    pendingTransformHandle = null;
    pendingTransformStart = null;
    compositeLayersToMain();
    pushHistory();
    redrawUI();
    renderLayerPanel();
    toolOptions.classList.remove('visible');
    toolOptions.innerHTML = '';
    if (currentTool) showToolOptions(currentTool);
    else if (selRect && selRect.w > 5 && selRect.h > 5) showSelectionOptions();
}

function cancelPendingShape() {
    if (!pendingShape) return;
    if (editingTextLayerIndex >= 0 && editingTextLayerIndex < layers.length) {
        const td = layers[editingTextLayerIndex].textData;
        if (td) {
            const restorePs = {
                type: 'text', ox: td.ox, oy: td.oy, rotation: td.rotation, scaleX: td.scaleX, scaleY: td.scaleY,
                params: { x: td.x, y: td.y, text: td.content, color: td.color, font: td.font, size: td.size, bold: td.bold, italic: td.italic, lineHeight: td.lineHeight }
            };
            layers[editingTextLayerIndex].ctx.clearRect(0, 0, canvasW, canvasH);
            drawPendingShapeOnCtx(layers[editingTextLayerIndex].ctx, restorePs);
            compositeLayersToMain();
        }
        editingTextLayerIndex = -1;
    }
    pendingShape = null;
    pendingDragging = false;
    pendingDragStart = null;
    pendingTransformHandle = null;
    pendingTransformStart = null;
    clearOverlay();
    redrawUI();
    toolOptions.classList.remove('visible');
    toolOptions.innerHTML = '';
    if (currentTool) showToolOptions(currentTool);
    else if (selRect && selRect.w > 5 && selRect.h > 5) showSelectionOptions();
}

function findTextLayerAt(cx, cy) {
    for (let i = layers.length - 1; i >= 0; i--) {
        const layer = layers[i];
        if (!layer.textData || !layer.visible) continue;
        const td = layer.textData;
        const tempPs = {
            type: 'text', ox: td.ox, oy: td.oy, rotation: td.rotation, scaleX: td.scaleX, scaleY: td.scaleY,
            params: { x: td.x, y: td.y, text: td.content, color: td.color, font: td.font, size: td.size, bold: td.bold, italic: td.italic, lineHeight: td.lineHeight }
        };
        const bbox = getPendingBBox(tempPs);
        if (!bbox) continue;
        const rot = td.rotation || 0;
        let hit;
        if (rot === 0) {
            hit = cx >= bbox.x && cx <= bbox.x + bbox.w && cy >= bbox.y && cy <= bbox.y + bbox.h;
        } else {
            const pvX = bbox.x + bbox.w / 2, pvY = bbox.y + bbox.h / 2;
            const cos = Math.cos(-rot), sin = Math.sin(-rot);
            const dx = cx - pvX, dy = cy - pvY;
            const nx = pvX + dx * cos - dy * sin, ny = pvY + dx * sin + dy * cos;
            hit = nx >= bbox.x && nx <= bbox.x + bbox.w && ny >= bbox.y && ny <= bbox.y + bbox.h;
        }
        if (hit) return i;
    }
    return -1;
}

function showPendingOptions() {
    toolOptions.innerHTML = `
        <label style="color:var(--accent)">Перемещение / Трансформация</label>
        <div class="opt-sep"></div>
        <button class="opt-btn primary" id="optPendApply">Применить (Enter)</button>
        <button class="opt-btn" id="optPendCancel">Отмена (Esc)</button>
    `;
    toolOptions.classList.add('visible');
    $('#optPendApply').onclick = commitPendingShape;
    $('#optPendCancel').onclick = cancelPendingShape;
}

// Register pending shape handler — intercepts first in dispatcher._dispatch
dispatcher.registerPending({
    onDown(e) {
        if (e.button !== 0) return;
        const p = getPointerCanvas(e);
        const handle = hitPendingHandle(p.x, p.y);
        if (handle) {
            const pivot = getPendingPivot(pendingShape);
            pendingTransformHandle = handle;
            pendingTransformStart = {
                pivotX: pivot.x, pivotY: pivot.y,
                ox: pendingShape.ox, oy: pendingShape.oy,
                scaleX: pendingShape.scaleX || 1, scaleY: pendingShape.scaleY || 1,
                rotation: pendingShape.rotation || 0,
                handles: getPendingHandles(pendingShape),
                rotAngle: Math.atan2(p.y - pivot.y, p.x - pivot.x),
            };
            try { viewport.setPointerCapture(e.pointerId); } catch(_) {}
            viewport.style.cursor = handle === 'rot' ? 'grab' : 'se-resize';
            container.style.cursor = handle === 'rot' ? 'grab' : 'se-resize';
            return;
        }
        if (hitPendingShape(p.x, p.y)) {
            pendingDragging = true;
            pendingDragStart = { x: p.x, y: p.y, ox: pendingShape.ox, oy: pendingShape.oy };
            try { viewport.setPointerCapture(e.pointerId); } catch(_) {}
            viewport.style.cursor = 'move';
            container.style.cursor = 'move';
            return;
        }
        // Clicked outside pending shape — commit and re-dispatch
        commitPendingShape();
        dispatcher.onDown(e);
    },
    onMove(e) {
        if (pendingTransformHandle && pendingShape) {
            const p = getPointerCanvas(e);
            if (pendingTransformHandle === 'rot') {
                const currentAngle = Math.atan2(p.y - pendingTransformStart.pivotY, p.x - pendingTransformStart.pivotX);
                pendingShape.rotation = pendingTransformStart.rotation + (currentAngle - pendingTransformStart.rotAngle);
            } else {
                const origH = pendingTransformStart.handles.find(h => h.id === pendingTransformHandle);
                const origDist = Math.hypot(origH.x - pendingTransformStart.pivotX, origH.y - pendingTransformStart.pivotY);
                const newDist = Math.hypot(p.x - pendingTransformStart.pivotX, p.y - pendingTransformStart.pivotY);
                const factor = origDist > 1 ? newDist / origDist : 1;
                pendingShape.scaleX = Math.max(0.05, pendingTransformStart.scaleX * factor);
                pendingShape.scaleY = Math.max(0.05, pendingTransformStart.scaleY * factor);
            }
            renderPendingOnOverlay();
            return;
        }
        if (pendingDragging && pendingShape) {
            const p = getPointerCanvas(e);
            pendingShape.ox = pendingDragStart.ox + (p.x - pendingDragStart.x);
            pendingShape.oy = pendingDragStart.oy + (p.y - pendingDragStart.y);
            renderPendingOnOverlay();
            return;
        }
        if (!isDrawing) {
            const p = getPointerCanvas(e);
            const handle = hitPendingHandle(p.x, p.y);
            const cur = handle ? (handle === 'rot' ? 'grab' : 'se-resize') : (hitPendingShape(p.x, p.y) ? 'move' : (currentTool ? 'crosshair' : 'default'));
            viewport.style.cursor = cur;
            container.style.cursor = cur;
        }
    },
    onUp(e) {
        if (pendingTransformHandle) {
            pendingTransformHandle = null;
            pendingTransformStart = null;
            viewport.style.cursor = 'move';
            container.style.cursor = 'move';
            return;
        }
        if (pendingDragging) {
            pendingDragging = false;
            pendingDragStart = null;
            viewport.style.cursor = 'move';
            container.style.cursor = 'move';
            return;
        }
    }
});

// ===================== TEXT TOOL =====================
function showTextOverlay(cx, cy) {
    const vp = canvasToViewport(cx, cy);
    textOverlay.style.display = 'block';
    textOverlay.style.left = vp.x + 'px';
    textOverlay.style.top = vp.y + 'px';
    textOverlay.style.transform = `scale(${zoom})`;
    const sz = Math.max(8, textSize);
    textArea.style.fontSize = sz + 'px';
    textArea.style.fontFamily = textFont === 'sans-serif' ? 'Manrope, sans-serif' : textFont === 'monospace' ? "'JetBrains Mono', monospace" : textFont;
    textArea.style.fontWeight = textBold ? '700' : '400';
    textArea.style.fontStyle = textItalic ? 'italic' : 'normal';
    textArea.style.color = fgColor;
    textArea.style.caretColor = fgColor;
    textArea.style.minWidth = Math.round(textSize * 4) + 'px';
    textArea.textContent = '';
    textArea.dataset.cx = cx;
    textArea.dataset.cy = cy;
    setTimeout(() => { textArea.focus(); }, 50);
}

function hideTextOverlay() {
    textOverlay.style.display = 'none';
    textArea.textContent = '';
}

function selectAllTextEditor() {
    const range = document.createRange();
    range.selectNodeContents(textArea);
    const sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
}

function commitText() {
    const text = (textArea.innerText || '').trim().replace(/\n$/, '');
    if (!text) { hideTextOverlay(); return; }

    const cx = +textArea.dataset.cx;
    const cy = +textArea.dataset.cy;

    const rotation = parseFloat(textArea.dataset.rotation) || 0;
    const scaleX = parseFloat(textArea.dataset.scaleX) || 1;
    const scaleY = parseFloat(textArea.dataset.scaleY) || 1;
    pendingShape = {
        type: 'text', ox: 0, oy: 0, rotation, scaleX, scaleY,
        params: {
            x: cx, y: cy, text: text,
            color: fgColor, font: textFont, size: textSize,
            bold: textBold, italic: textItalic,
            lineHeight: textSize * 1.3
        }
    };
    textArea.dataset.rotation = '0';
    textArea.dataset.scaleX = '1';
    textArea.dataset.scaleY = '1';
    hideTextOverlay();
    renderPendingOnOverlay();
    showPendingOptions();
}

textArea.addEventListener('keydown', e => {
    if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        e.stopPropagation();
        const text = (textArea.innerText || '').trim().replace(/\n$/, '');
        if (text) {
            commitText();
        } else {
            hideTextOverlay();
            if (pendingShape) commitPendingShape();
        }
        return;
    }
    if (e.key === 'Escape') {
        hideTextOverlay();
        if (pendingShape) cancelPendingShape();
        return;
    }
    e.stopPropagation();
});

textOverlay.addEventListener('pointerdown', e => { e.stopPropagation(); });
textOverlay.addEventListener('click', e => { e.stopPropagation(); });

// ===================== COLOR PICKER =====================
const colorPicker = $('#colorPicker');
const colorSwatch = $('#colorSwatch');

function updateColorSwatch() {
    colorSwatch.style.background = fgColor;
}
updateColorSwatch();

colorPicker.addEventListener('input', e => {
    fgColor = e.target.value;
    updateColorSwatch();
});

// ===================== EXPORT =====================
$('#btnExport').addEventListener('click', showExportModal);
$('#exportCancel').addEventListener('click', hideExportModal);
$('#exportConfirm').addEventListener('click', doExport);
$('#exportFormat').addEventListener('change', updateExportUI);
$('#exportQuality').addEventListener('input', e => {
    $('#exportQualityVal').textContent = e.target.value;
});

function showExportModal() {
    if (!canvasW) return;
    $('#exportName').value = fileName;
    updateExportUI();
    $('#exportModal').classList.add('visible');
}

function hideExportModal() {
    $('#exportModal').classList.remove('visible');
}

function updateExportUI() {
    const fmt = $('#exportFormat').value;
    const showQuality = fmt !== 'png';
    $('#qualityRow').style.display = showQuality ? 'flex' : 'none';
    const warn = fmt === 'jpeg' && hasTransparency;
    $('#exportWarning').classList.toggle('visible', warn);
}

function doExport() {
    const fmt = $('#exportFormat').value;
    const quality = +$('#exportQuality').value / 100;
    const name = $('#exportName').value || 'image';
    const ext = fmt === 'jpeg' ? 'jpg' : fmt;
    const mime = fmt === 'jpeg' ? 'image/jpeg' : fmt === 'webp' ? 'image/webp' : 'image/png';

    let exportCanvas = cMain;
    if (fmt === 'jpeg') {
        exportCanvas = document.createElement('canvas');
        exportCanvas.width = canvasW;
        exportCanvas.height = canvasH;
        const ectx = exportCanvas.getContext('2d');
        ectx.fillStyle = '#ffffff';
        ectx.fillRect(0, 0, canvasW, canvasH);
        ectx.drawImage(cMain, 0, 0);
    }

    exportCanvas.toBlob(blob => {
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = name + '.' + ext;
        a.click();
        URL.revokeObjectURL(url);
        hideExportModal();
    }, mime, quality);
}

$('#exportModal').addEventListener('click', e => {
    if (e.target === $('#exportModal')) hideExportModal();
});

// ===================== KEYBOARD SHORTCUTS =====================
document.addEventListener('keydown', e => {
    if (e.code === 'Space' && e.target.tagName !== 'TEXTAREA') {
        e.preventDefault();
        spaceDown = true;
        if (canvasW) { viewport.style.cursor = 'grab'; container.style.cursor = 'grab'; }
        return;
    }

    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;

    const ctrl = e.ctrlKey || e.metaKey;

    if (ctrl && e.code === 'KeyO') { e.preventDefault(); fileInput.click(); return; }
    if (ctrl && e.code === 'KeyT') { e.preventDefault(); saveCurrentTabState(); const _nt = createTab('Новый'); activeTabId = _nt.id; layers = []; history = []; historyIndex = -1; canvasW = 0; canvasH = 0; selRect = null; loadTabState(_nt); return; }
    if (ctrl && e.code === 'KeyW') { e.preventDefault(); closeTab(activeTabId); return; }
    if (ctrl && e.code === 'KeyS') { e.preventDefault(); showExportModal(); return; }
    if (ctrl && e.code === 'KeyZ' && !e.shiftKey) { e.preventDefault(); if (pendingShape) { cancelPendingShape(); } else { undo(); } return; }
    if (ctrl && e.code === 'KeyZ' && e.shiftKey) { e.preventDefault(); redo(); return; }
    if (ctrl && e.code === 'KeyC' && !e.shiftKey) {
        e.preventDefault();
        if (!canvasW) return;
        // Copy selection (or whole image) from active layer to internal clipboard
        const src = layers[activeLayerIndex];
        if (selRect && selRect.w > 5 && selRect.h > 5) {
            const sx = Math.round(clamp(selRect.x, 0, canvasW));
            const sy = Math.round(clamp(selRect.y, 0, canvasH));
            const sw = Math.round(clamp(selRect.w, 1, canvasW - sx));
            const sh = Math.round(clamp(selRect.h, 1, canvasH - sy));
            const data = src.ctx.getImageData(sx, sy, sw, sh);
            internalClipboard = { imageData: data, x: sx, y: sy, w: sw, h: sh };
        } else if (selPath && selPath.length > 2) {
            // Lasso: use bounding box
            let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
            selPath.forEach(p => { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); });
            const sx = Math.round(clamp(minX, 0, canvasW));
            const sy = Math.round(clamp(minY, 0, canvasH));
            const sw = Math.round(clamp(maxX - minX, 1, canvasW - sx));
            const sh = Math.round(clamp(maxY - minY, 1, canvasH - sy));
            // Create masked copy
            const tmp = document.createElement('canvas'); tmp.width = sw; tmp.height = sh;
            const tc = tmp.getContext('2d');
            tc.beginPath();
            selPath.forEach((p, i) => i === 0 ? tc.moveTo(p.x - sx, p.y - sy) : tc.lineTo(p.x - sx, p.y - sy));
            tc.closePath(); tc.clip();
            tc.drawImage(src.canvas, -sx, -sy);
            internalClipboard = { imageData: tc.getImageData(0, 0, sw, sh), x: sx, y: sy, w: sw, h: sh };
        } else {
            // No selection: copy whole layer
            const data = src.ctx.getImageData(0, 0, canvasW, canvasH);
            internalClipboard = { imageData: data, x: 0, y: 0, w: canvasW, h: canvasH };
        }
        return;
    }
    if (ctrl && e.code === 'KeyX' && !e.shiftKey) {
        e.preventDefault();
        if (!canvasW) return;
        const src = layers[activeLayerIndex];
        if (selRect && selRect.w > 5 && selRect.h > 5) {
            const sx = Math.round(clamp(selRect.x, 0, canvasW));
            const sy = Math.round(clamp(selRect.y, 0, canvasH));
            const sw = Math.round(clamp(selRect.w, 1, canvasW - sx));
            const sh = Math.round(clamp(selRect.h, 1, canvasH - sy));
            internalClipboard = { imageData: src.ctx.getImageData(sx, sy, sw, sh), x: sx, y: sy, w: sw, h: sh };
            src.ctx.clearRect(sx, sy, sw, sh);
        } else if (selPath && selPath.length > 2) {
            let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
            selPath.forEach(p => { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); });
            const sx = Math.round(clamp(minX, 0, canvasW));
            const sy = Math.round(clamp(minY, 0, canvasH));
            const sw = Math.round(clamp(maxX - minX, 1, canvasW - sx));
            const sh = Math.round(clamp(maxY - minY, 1, canvasH - sy));
            const tmp = document.createElement('canvas'); tmp.width = sw; tmp.height = sh;
            const tc = tmp.getContext('2d');
            tc.beginPath();
            selPath.forEach((p, i) => i === 0 ? tc.moveTo(p.x - sx, p.y - sy) : tc.lineTo(p.x - sx, p.y - sy));
            tc.closePath(); tc.clip();
            tc.drawImage(src.canvas, -sx, -sy);
            internalClipboard = { imageData: tc.getImageData(0, 0, sw, sh), x: sx, y: sy, w: sw, h: sh };
            // Erase lasso area from layer
            src.ctx.save();
            src.ctx.beginPath();
            selPath.forEach((p, i) => i === 0 ? src.ctx.moveTo(p.x, p.y) : src.ctx.lineTo(p.x, p.y));
            src.ctx.closePath();
            src.ctx.clip();
            src.ctx.clearRect(sx, sy, sw, sh);
            src.ctx.restore();
        } else {
            internalClipboard = { imageData: src.ctx.getImageData(0, 0, canvasW, canvasH), x: 0, y: 0, w: canvasW, h: canvasH };
            src.ctx.clearRect(0, 0, canvasW, canvasH);
        }
        compositeLayersToMain();
        pushHistory();
        clearSelection();
        return;
    }
    if (ctrl && e.code === 'KeyV' && !e.shiftKey) {
        if (internalClipboard) {
            e.preventDefault();
            // Paste to new layer
            const layer = createLayer('Вставка', canvasW, canvasH);
            if (!layer) return;
            layer.ctx.putImageData(internalClipboard.imageData, internalClipboard.x, internalClipboard.y);
            layers.splice(activeLayerIndex + 1, 0, layer);
            activeLayerIndex++;
            compositeLayersToMain();
            renderLayerPanel();
            pushHistory();
            // Switch to select tool so user can move the pasted content
            setTool('select');
            return;
        }
        // If no internal clipboard, let default paste handler (from clipboard API) work
        return;
    }
    if (ctrl && e.key === '0') { e.preventDefault(); fitToViewport(); return; }
    if (ctrl && e.key === '1') { e.preventDefault(); zoom = 1; panX = (viewport.clientWidth - canvasW) / 2; panY = (viewport.clientHeight - canvasH) / 2; updateTransform(); return; }

    if (e.key === 'Escape') { if (pendingShape) { cancelPendingShape(); } else if (currentTool) { cancelCurrentTool(); } clearSelection(); return; }
    if (e.key === 'Enter') {
        if (pendingShape) { e.preventDefault(); commitPendingShape(); return; }
        if (selRect && !currentTool) { e.preventDefault(); applySelectionCrop(); return; }
        if (currentTool === 'crop' && cropRect) { e.preventDefault(); applyCrop(); return; }
    }

    if (!canvasW) return;

    const toolMap = { v:'select', b:'brush', e:'eraser', t:'text', c:'crop', l:'line', u:'rect', o:'circle', a:'arrow', i:'eyedropper', g:'fill', s:'clone', w:'lasso' };
    if (toolMap[e.key.toLowerCase()] && !ctrl) {
        setTool(toolMap[e.key.toLowerCase()]);
        return;
    }

    if (e.code === 'BracketLeft' || e.code === 'BracketRight') {
        const delta = e.code === 'BracketLeft' ? -2 : 2;
        if (['brush', 'eraser', 'clone'].includes(currentTool)) {
            brushSize = Math.max(1, Math.min(100, brushSize + delta));
            if ($('#optSize')) { $('#optSize').value = brushSize; }
            if ($('#optSizeVal')) { $('#optSizeVal').textContent = brushSize; }
        } else if (['line', 'rect', 'circle', 'arrow'].includes(currentTool)) {
            strokeWidth = Math.max(1, Math.min(20, strokeWidth + (delta > 0 ? 1 : -1)));
            if ($('#optStroke')) { $('#optStroke').value = strokeWidth; }
            if ($('#optStrokeVal')) { $('#optStrokeVal').textContent = strokeWidth; }
        }
        return;
    }
});

document.addEventListener('keyup', e => {
    if (e.code === 'Space') {
        spaceDown = false;
        const cur = currentTool ? 'crosshair' : 'default';
        viewport.style.cursor = cur;
        container.style.cursor = cur;
    }
});

// ===================== IMAGE BORDER =====================
function drawImageBorder() {
    if (!canvasW) return;
    const ctx = ctxUI;
    const tl = c2s(0, 0);
    const br = c2s(canvasW, canvasH);
    ctx.save();
    ctx.strokeStyle = 'rgba(255,255,255,0.15)';
    ctx.lineWidth = 1;
    ctx.strokeRect(tl.x, tl.y, br.x - tl.x, br.y - tl.y);
    ctx.restore();
}

// ===================== PIXEL RULERS =====================
function drawRulers() {
    if (!canvasW) return;
    const ctx = ctxUI;
    const vw = viewport.clientWidth;
    const vh = viewport.clientHeight;
    const rulerH = 20;
    ctx.save();
    ctx.fillStyle = 'rgba(9,16,32,0.85)';
    ctx.fillRect(0, 0, vw, rulerH);
    ctx.fillRect(0, rulerH, rulerH, vh - rulerH);
    let step = 100;
    if (zoom >= 4) step = 10;
    else if (zoom >= 1.5) step = 25;
    else if (zoom >= 0.5) step = 50;
    else if (zoom < 0.2) step = 200;
    ctx.font = '9px JetBrains Mono, monospace';
    ctx.fillStyle = '#666';
    ctx.strokeStyle = '#444';
    ctx.lineWidth = 0.5;
    const startX = Math.floor(-panX / zoom / step) * step;
    const endX = Math.ceil((vw - panX) / zoom / step) * step;
    for (let cx = startX; cx <= endX; cx += step) {
        const sx = cx * zoom + panX;
        if (sx < rulerH || sx > vw) continue;
        ctx.beginPath(); ctx.moveTo(sx, rulerH - 6); ctx.lineTo(sx, rulerH); ctx.stroke();
        ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
        ctx.fillText(cx, sx, rulerH - 7);
    }
    const minor = step / 5;
    if (minor * zoom >= 4) {
        const ms = Math.floor(-panX / zoom / minor) * minor;
        const me = Math.ceil((vw - panX) / zoom / minor) * minor;
        for (let cx = ms; cx <= me; cx += minor) {
            const sx = cx * zoom + panX;
            if (sx < rulerH || sx > vw) continue;
            ctx.beginPath(); ctx.moveTo(sx, rulerH - 3); ctx.lineTo(sx, rulerH); ctx.stroke();
        }
    }
    const startY = Math.floor(-panY / zoom / step) * step;
    const endY = Math.ceil((vh - panY) / zoom / step) * step;
    for (let cy = startY; cy <= endY; cy += step) {
        const sy = cy * zoom + panY;
        if (sy < rulerH || sy > vh) continue;
        ctx.beginPath(); ctx.moveTo(rulerH - 6, sy); ctx.lineTo(rulerH, sy); ctx.stroke();
        ctx.save(); ctx.translate(rulerH - 7, sy); ctx.rotate(-Math.PI/2);
        ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
        ctx.fillText(cy, 0, 0);
        ctx.restore();
    }
    if (minor * zoom >= 4) {
        const ms2 = Math.floor(-panY / zoom / minor) * minor;
        const me2 = Math.ceil((vh - panY) / zoom / minor) * minor;
        for (let cy = ms2; cy <= me2; cy += minor) {
            const sy = cy * zoom + panY;
            if (sy < rulerH || sy > vh) continue;
            ctx.beginPath(); ctx.moveTo(rulerH - 3, sy); ctx.lineTo(rulerH, sy); ctx.stroke();
        }
    }
    ctx.fillStyle = 'rgba(9,16,32,0.85)';
    ctx.fillRect(0, 0, rulerH, rulerH);
    ctx.restore();
}

// ===================== WINDOW RESIZE =====================
window.addEventListener('resize', () => {
    resizeUICanvas();
    if (canvasW) fitToViewport();
});

// ===================== CONTEXT MENU =====================
const ctxMenu = $('#ctxMenu');

function showCtxMenu(x, y) {
    const hasImage = canvasW > 0;
    const hasSel = selRect && selRect.w > 5 && selRect.h > 5;
    ctxMenu.querySelector('[data-action="undo"]').classList.toggle('disabled', historyIndex <= 0);
    ctxMenu.querySelector('[data-action="redo"]').classList.toggle('disabled', historyIndex >= history.length - 1);
    ctxMenu.querySelector('[data-action="crop-sel"]').classList.toggle('disabled', !hasSel);
    ctxMenu.querySelector('[data-action="resize"]').classList.toggle('disabled', !hasImage);
    ctxMenu.querySelector('[data-action="rot-cw"]').classList.toggle('disabled', !hasImage);
    ctxMenu.querySelector('[data-action="rot-ccw"]').classList.toggle('disabled', !hasImage);
    ctxMenu.querySelector('[data-action="flip-h"]').classList.toggle('disabled', !hasImage);
    ctxMenu.querySelector('[data-action="flip-v"]').classList.toggle('disabled', !hasImage);
    ctxMenu.querySelector('[data-action="copy"]').classList.toggle('disabled', !hasImage);
    ctxMenu.querySelector('[data-action="export"]').classList.toggle('disabled', !hasImage);

    ctxMenu.style.left = '0'; ctxMenu.style.top = '0';
    ctxMenu.classList.add('visible');
    const mw = ctxMenu.offsetWidth, mh = ctxMenu.offsetHeight;
    const vw = window.innerWidth, vh = window.innerHeight;
    ctxMenu.style.left = Math.min(x, vw - mw - 8) + 'px';
    ctxMenu.style.top = Math.min(y, vh - mh - 8) + 'px';
}

function hideCtxMenu() {
    ctxMenu.classList.remove('visible');
}

viewport.addEventListener('contextmenu', e => {
    e.preventDefault();
    showCtxMenu(e.clientX, e.clientY);
});

document.addEventListener('pointerdown', e => {
    if (!ctxMenu.contains(e.target)) hideCtxMenu();
});
document.addEventListener('keydown', e => {
    if (ctxMenu.classList.contains('visible') && e.key === 'Escape') { hideCtxMenu(); e.stopPropagation(); }
}, true);

ctxMenu.addEventListener('click', e => {
    const item = e.target.closest('.ctx-item');
    if (!item || item.classList.contains('disabled')) return;
    const action = item.dataset.action;
    hideCtxMenu();

    switch (action) {
        case 'undo': undo(); break;
        case 'redo': redo(); break;
        case 'crop-sel': if (selRect) applySelectionCrop(); break;
        case 'resize': $('#btnResize').click(); break;
        case 'rot-cw': rotate90(1); break;
        case 'rot-ccw': rotate90(-1); break;
        case 'flip-h': flip('h'); break;
        case 'flip-v': flip('v'); break;
        case 'fit': fitToViewport(); break;
        case 'zoom100':
            zoom = 1;
            panX = (viewport.clientWidth - canvasW) / 2;
            panY = (viewport.clientHeight - canvasH) / 2;
            updateTransform();
            break;
        case 'copy':
            if (canvasW) cMain.toBlob(b => {
                navigator.clipboard.write([new ClipboardItem({'image/png': b})]).catch(()=>{});
            });
            break;
        case 'paste':
            navigator.clipboard.read().then(items => {
                for (const item of items) {
                    for (const type of item.types) {
                        if (type.startsWith('image/')) {
                            item.getType(type).then(blob => {
                                const f = new File([blob], 'pasted.png', {type});
                                loadImage(f);
                            });
                            return;
                        }
                    }
                }
            }).catch(()=>{});
            break;
        case 'export': showExportModal(); break;
    }
});

// ===================== LAYER PANEL =====================
$('#btnLayers').addEventListener('click', () => {
    layerPanelVisible = !layerPanelVisible;
    layerPanel.classList.toggle('visible', layerPanelVisible);
    $('#btnLayers').classList.toggle('active', layerPanelVisible);
    if (layerPanelVisible) renderLayerPanel();
});

function renderLayerPanel() {
    if (!layerPanelVisible && !layerPanel.classList.contains('visible')) { updateStatusLayer(); return; }
    layerList.innerHTML = '';
    for (let i = layers.length - 1; i >= 0; i--) {
        const layer = layers[i];
        const item = document.createElement('div');
        item.className = 'layer-item' + (i === activeLayerIndex ? ' active' : '');
        item.dataset.index = i;
        item.draggable = true;

        const vis = document.createElement('button');
        vis.className = 'layer-vis' + (layer.visible ? '' : ' off');
        vis.innerHTML = layer.visible ? '&#x1f441;' : '&#x25CB;';
        vis.title = 'Видимость';
        vis.addEventListener('click', e => {
            e.stopPropagation();
            layer.visible = !layer.visible;
            compositeLayersToMain();
            renderLayerPanel();
            pushHistory();
        });

        const thumbWrap = document.createElement('div');
        thumbWrap.className = 'layer-thumb';
        const thumbCanvas = document.createElement('canvas');
        thumbCanvas.width = 32; thumbCanvas.height = 32;
        const tc = thumbCanvas.getContext('2d');
        if (layer.canvas.width && layer.canvas.height) {
            const scale = Math.min(32 / layer.canvas.width, 32 / layer.canvas.height);
            const w = layer.canvas.width * scale;
            const h = layer.canvas.height * scale;
            tc.drawImage(layer.canvas, (32 - w) / 2, (32 - h) / 2, w, h);
        }
        thumbWrap.appendChild(thumbCanvas);

        const info = document.createElement('div');
        info.className = 'layer-info';
        const nameEl = document.createElement('div');
        nameEl.className = 'layer-name';
        nameEl.textContent = (layer.textData ? 'T ' : '') + layer.name;
        if (layer.textData) nameEl.style.color = 'var(--accent)';
        const meta = document.createElement('div');
        meta.className = 'layer-meta';
        meta.textContent = Math.round(layer.opacity * 100) + '%' + (layer.blendMode !== 'source-over' ? ' ' + layer.blendMode : '');
        info.appendChild(nameEl);
        info.appendChild(meta);

        item.appendChild(vis);
        item.appendChild(thumbWrap);
        item.appendChild(info);

        item.addEventListener('click', () => {
            activeLayerIndex = i;
            renderLayerPanel();
        });

        item.addEventListener('dblclick', e => {
            e.stopPropagation();
            const input = document.createElement('input');
            input.className = 'layer-name-input';
            input.value = layer.name;
            nameEl.replaceWith(input);
            input.focus();
            input.select();
            const finish = () => {
                layer.name = input.value.trim() || layer.name;
                renderLayerPanel();
            };
            input.addEventListener('blur', finish);
            input.addEventListener('keydown', ev => {
                if (ev.key === 'Enter') { ev.preventDefault(); input.blur(); }
                if (ev.key === 'Escape') { input.value = layer.name; input.blur(); }
                ev.stopPropagation();
            });
        });

        item.addEventListener('dragstart', e => {
            e.dataTransfer.setData('text/plain', i.toString());
            e.dataTransfer.effectAllowed = 'move';
            item.style.opacity = '0.5';
        });
        item.addEventListener('dragend', () => { item.style.opacity = '1'; });
        item.addEventListener('dragover', e => {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            const rect = item.getBoundingClientRect();
            const mid = rect.top + rect.height / 2;
            item.classList.remove('drag-over-top', 'drag-over-bottom');
            item.classList.add(e.clientY < mid ? 'drag-over-top' : 'drag-over-bottom');
        });
        item.addEventListener('dragleave', () => {
            item.classList.remove('drag-over-top', 'drag-over-bottom');
        });
        item.addEventListener('drop', e => {
            e.preventDefault();
            item.classList.remove('drag-over-top', 'drag-over-bottom');
            const fromIdx = parseInt(e.dataTransfer.getData('text/plain'));
            const rect = item.getBoundingClientRect();
            const mid = rect.top + rect.height / 2;
            let toIdx = i;
            if (e.clientY < mid && fromIdx < i) toIdx = i;
            else if (e.clientY >= mid && fromIdx > i) toIdx = i;
            else if (e.clientY < mid && fromIdx > i) toIdx = i + 1;
            else if (e.clientY >= mid && fromIdx < i) toIdx = i - 1;
            if (fromIdx !== toIdx) moveLayer(fromIdx, toIdx);
        });

        layerList.appendChild(item);
    }

    if (layers.length > 0) {
        const active = layers[activeLayerIndex];
        $('#lpBlend').value = active.blendMode;
        $('#lpOpacity').value = Math.round(active.opacity * 100);
        $('#lpOpacityVal').textContent = Math.round(active.opacity * 100) + '%';
    }

    $('#lpDel').disabled = layers.length <= 1;
    $('#lpMerge').disabled = activeLayerIndex <= 0;
    $('#lpAdd').disabled = layers.length >= MAX_LAYERS;

    updateStatusLayer();
}

// Layer panel buttons
$('#lpAdd').addEventListener('click', () => addLayer());
$('#lpDel').addEventListener('click', () => removeLayer(activeLayerIndex));
$('#lpMerge').addEventListener('click', () => mergeDown(activeLayerIndex));
$('#lpFlatten').addEventListener('click', () => flattenLayers());

$('#lpBlend').addEventListener('change', e => {
    if (layers.length === 0) return;
    layers[activeLayerIndex].blendMode = e.target.value;
    compositeLayersToMain();
    renderLayerPanel();
    pushHistory();
});

$('#lpOpacity').addEventListener('input', e => {
    if (layers.length === 0) return;
    layers[activeLayerIndex].opacity = +e.target.value / 100;
    $('#lpOpacityVal').textContent = e.target.value + '%';
    compositeLayersToMain();
});
$('#lpOpacity').addEventListener('change', () => {
    renderLayerPanel();
    pushHistory();
});

layerPanel.addEventListener('pointerdown', e => e.stopPropagation());
layerPanel.addEventListener('keydown', e => e.stopPropagation());

// ===================== LAYER SHORTCUTS =====================
document.addEventListener('keydown', e => {
    if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
    const ctrl = e.ctrlKey || e.metaKey;
    if (ctrl && e.shiftKey && e.code === 'KeyN') {
        e.preventDefault();
        if (canvasW) addLayer();
        return;
    }
    if (ctrl && !e.shiftKey && e.code === 'KeyE') {
        e.preventDefault();
        if (canvasW) mergeDown(activeLayerIndex);
        return;
    }
    if (ctrl && e.shiftKey && e.code === 'KeyE') {
        e.preventDefault();
        if (canvasW) flattenLayers();
        return;
    }
}, true);

// ===================== SETTINGS PERSISTENCE =====================
const SETTINGS_KEY = 'editor_settings_v1';

function saveSettings() {
    try {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify({
            brushSize, brushOpacity, strokeWidth, fillShape,
            fgColor, textFont, textSize, textBold, textItalic,
            fillTolerance, blurRadius, sharpenAmount
        }));
    } catch(_) {}
}

function loadSettings() {
    try {
        const s = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}');
        if (s.brushSize)    brushSize    = s.brushSize;
        if (s.brushOpacity) brushOpacity = s.brushOpacity;
        if (s.strokeWidth)  strokeWidth  = s.strokeWidth;
        if (typeof s.fillShape === 'boolean') fillShape = s.fillShape;
        if (s.fgColor)      { fgColor = s.fgColor; $('#colorPicker').value = fgColor; updateColorSwatch(); }
        if (s.textFont)     textFont  = s.textFont;
        if (s.textSize)     textSize  = s.textSize;
        if (typeof s.textBold === 'boolean')   textBold   = s.textBold;
        if (typeof s.textItalic === 'boolean') textItalic = s.textItalic;
        if (s.fillTolerance !== undefined) fillTolerance = s.fillTolerance;
        if (s.blurRadius    !== undefined) blurRadius    = s.blurRadius;
        if (s.sharpenAmount !== undefined) sharpenAmount = s.sharpenAmount;
    } catch(_) {}
}

let _settingsSaveTimer = 0;
function scheduleSaveSettings() {
    clearTimeout(_settingsSaveTimer);
    _settingsSaveTimer = setTimeout(saveSettings, 500);
}

colorPicker.addEventListener('input', scheduleSaveSettings);

const _settingsObserver = new MutationObserver(() => {
    ['optSize','optOpacity','optStroke','optTolerance','optBlurR','optSharpAmt'].forEach(id => {
        const el = document.getElementById(id);
        if (el && !el.dataset.settingsHooked) {
            el.dataset.settingsHooked = '1';
            el.addEventListener('change', scheduleSaveSettings);
        }
    });
});
_settingsObserver.observe(toolOptions, { childList: true });

loadSettings();

// ===================== INIT TABS =====================
(function initTabs() {
    const first = createTab('Новый');
    activeTabId = first.id;
    renderTabBar();
})();

})();
