WME Guide Lines

Réguas e linhas guia para o WME. Arraste da régua para criar; botão esquerdo move, botão direito + arrastar gira. Limite de 5 linhas guia. Botão para excluir todas linhas guia.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         WME Guide Lines
// @namespace    https://greasyfork.org/
// @version      1.0.1
// @description  Réguas e linhas guia para o WME. Arraste da régua para criar; botão esquerdo move, botão direito + arrastar gira. Limite de 5 linhas guia. Botão para excluir todas linhas guia.
// @match        https://www.waze.com/*editor*
// @match        https://beta.waze.com/*editor*
// @exclude      https://www.waze.com/*user/*editor/*
// @grant        none
// @require      https://update.greasyfork.org/scripts/450160/1704233/WME-Bootstrap.js
// ==/UserScript==

/* global W, $ */
/* jshint esversion: 11 */

(function () {
    'use strict';

    // ─────────────────────────────────────────────────────────────────────────
    // CONFIG
    // ─────────────────────────────────────────────────────────────────────────
    const CFG = {
        MAX_GUIDES:  5,
        RULER_W:     18,      // px — ruler thickness
        HIT_RADIUS:  8,       // px — mouse hit tolerance
        COLOR_IDLE:  '#00bfff',
        COLOR_HOVER: '#ff6b35',
        ALPHA:       0.80,
        LINE_W:      1.5,
        DASH:        [10, 6],
        TICK_SM:     4,
        TICK_LG:     10,
    };

    // ─────────────────────────────────────────────────────────────────────────
    // WME LAYOUT — selectors tried in order
    // ─────────────────────────────────────────────────────────────────────────
    // Header: bar that contains search + save button
    const HEADER_SEL = [
        '#app-head',
        '#topbar',
        '.app-header',
        'header.toolbar',
        'nav.toolbar',
        '.toolbar-container',
        '#toolbar',
    ];
    // Sidebar: left panel (collapses/expands)
    const SIDEBAR_SEL = [
        '#sidebar',
        '.sidebar',
        '#edit-panel',
        '.edit-panel',
        'aside',
        '#panel-container',
    ];

    function findEl(selectors) {
        for (const sel of selectors) {
            const el = document.querySelector(sel);
            if (el && el.offsetWidth > 0) return el;
        }
        return null;
    }

    // Returns { top, left } — the offset where the map area starts
    function getMapOffset() {
        // Try to read directly from #WazeMap (the actual OL map container)
        const wazeMap = document.getElementById('WazeMap');
        if (wazeMap) {
            const r = wazeMap.getBoundingClientRect();
            return { top: r.top, left: r.left };
        }
        // Fallback: read header height + sidebar width
        const header  = findEl(HEADER_SEL);
        const sidebar = findEl(SIDEBAR_SEL);
        return {
            top:  header  ? header.getBoundingClientRect().bottom : 60,
            left: sidebar ? sidebar.getBoundingClientRect().right  : 0,
        };
    }

    // ─────────────────────────────────────────────────────────────────────────
    // STATE
    // ─────────────────────────────────────────────────────────────────────────
    const guides = [];   // { id, type:'H'|'V', pos:number, angleDeg:number }
    let nextId = 1;
    let hidden = false;
    let layout = { top: 60, left: 0 };   // cached map offset

    const drag = {
        active:       false,
        mode:         null,     // 'create-H'|'create-V'|'move'|'rotate'
        guideId:      null,
        startCX:      0,
        startCY:      0,
        startPos:     0,
        startAngle:   0,
    };

    // ─────────────────────────────────────────────────────────────────────────
    // DOM REFS
    // ─────────────────────────────────────────────────────────────────────────
    let hRuler, vRuler, lineCanvas, lCtx, angleLabel;

    // ─────────────────────────────────────────────────────────────────────────
    // SETUP
    // ─────────────────────────────────────────────────────────────────────────
    function setup() {
        injectCSS();
        buildDOM();
        updateLayout();

        // Observe sidebar & header for size changes
        const ro = new ResizeObserver(updateLayout);
        const header  = findEl(HEADER_SEL);
        const sidebar = findEl(SIDEBAR_SEL);
        if (header)  ro.observe(header);
        if (sidebar) ro.observe(sidebar);
        window.addEventListener('resize', updateLayout);

        // Also poll in case sidebar animates (toggle arrow)
        setInterval(updateLayout, 400);

        console.log('[WME-GL] Guide Lines v3 carregado.');
    }

    // ─────────────────────────────────────────────────────────────────────────
    // CSS
    // ─────────────────────────────────────────────────────────────────────────
    function injectCSS() {
        if (document.getElementById('wmegl-style')) return;
        const s = document.createElement('style');
        s.id = 'wmegl-style';
        // All elements are fixed. Positions are set dynamically via updateLayout().
        s.textContent = `
            #wmegl-hruler {
                position: fixed;
                height: ${CFG.RULER_W}px;
                background: rgba(27, 31, 46, 0.4);
                border-bottom: 1px solid #2e3a55;
                z-index: 99980;
                cursor: s-resize;
                pointer-events: all;
                box-sizing: border-box;
                user-select: none;
            }
            #wmegl-vruler {
                position: fixed;
                width: ${CFG.RULER_W}px;
                background: rgba(27, 31, 46, 0.4);
                border-right: 1px solid #2e3a55;
                z-index: 99980;
                cursor: e-resize;
                pointer-events: all;
                box-sizing: border-box;
                user-select: none;
            }
            #wmegl-corner {
                position: fixed;
                width: ${CFG.RULER_W}px;
                height: ${CFG.RULER_W}px;
                background: #141824;
                border-right: 1px solid #2e3a55;
                border-bottom: 1px solid #2e3a55;
                z-index: 99982;
                pointer-events: none;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            #wmegl-canvas {
                position: fixed;
                pointer-events: none;
                z-index: 99979;
            }
            .wmegl-hit {
                position: fixed;
                z-index: 99981;
                pointer-events: all;
                cursor: move;
            }
            #wmegl-ghost {
                position: fixed;
                pointer-events: none;
                z-index: 99983;
                opacity: 0.55;
                display: none;
                background: ${CFG.COLOR_IDLE};
            }
            #wmegl-angle-label {
                position: fixed;
                pointer-events: none;
                z-index: 99985;
                display: none;
                background: rgba(14,18,28,0.92);
                border: 1px solid #2e3a55;
                border-radius: 4px;
                padding: 3px 8px;
                font: 11px/1.4 monospace;
                color: #7eb8f7;
            }
            #wmegl-clear-btn {
                position: fixed;
                z-index: 99984;
                background: rgba(20,24,36,0.92);
                border: 1px solid #2e3a55;
                border-radius: 5px;
                padding: 4px 10px;
                font: 11px/1.5 'Segoe UI', system-ui, sans-serif;
                color: #b0bbce;
                cursor: pointer;
                pointer-events: all;
                white-space: nowrap;
                box-shadow: 0 2px 8px rgba(0,0,0,0.5);
                transition: background 0.12s, color 0.12s, border-color 0.12s;
            }
            #wmegl-clear-btn:hover {
                background: #2e1a1a;
                color: #f87171;
                border-color: #f87171;
            }
        `;
        document.head.appendChild(s);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // BUILD DOM
    // ─────────────────────────────────────────────────────────────────────────
    function buildDOM() {
        // Horizontal ruler (top edge of map area)
        hRuler = document.createElement('canvas');
        hRuler.id = 'wmegl-hruler';
        hRuler.title = 'Arraste para baixo para criar uma linha guia horizontal';
        document.body.appendChild(hRuler);

        // Vertical ruler (left edge of map area)
        vRuler = document.createElement('canvas');
        vRuler.id = 'wmegl-vruler';
        vRuler.title = 'Arraste para a direita para criar uma linha guia vertical';
        document.body.appendChild(vRuler);

        // Corner square (intersection of the two rulers)
        const corner = document.createElement('div');
        corner.id = 'wmegl-corner';
        corner.title = 'WME Guide Lines';
        corner.innerHTML = `<svg width="12" height="12" viewBox="0 0 12 12">
            <line x1="1" y1="6" x2="11" y2="6" stroke="#3a4060" stroke-width="1.5"/>
            <line x1="6" y1="1" x2="6" y2="11" stroke="#3a4060" stroke-width="1.5"/>
        </svg>`;
        document.body.appendChild(corner);

        // Guide lines canvas (covers the entire map area)
        lineCanvas = document.createElement('canvas');
        lineCanvas.id = 'wmegl-canvas';
        document.body.appendChild(lineCanvas);
        lCtx = lineCanvas.getContext('2d');

        // Ghost line while dragging from ruler
        const ghost = document.createElement('div');
        ghost.id = 'wmegl-ghost';
        document.body.appendChild(ghost);

        // Angle label while rotating
        angleLabel = document.createElement('div');
        angleLabel.id = 'wmegl-angle-label';
        document.body.appendChild(angleLabel);

        // Clear button (top-left of map area, just after the corner)
        const clearBtn = document.createElement('button');
        clearBtn.id = 'wmegl-clear-btn';
        clearBtn.textContent = 'Limpar Linhas Guia';
        clearBtn.title = 'Remove todas as linhas guia';
        clearBtn.addEventListener('click', clearAll);
        document.body.appendChild(clearBtn);

        // Ruler mouse events
        hRuler.addEventListener('mousedown', e => onRulerDown(e, 'H'));
        vRuler.addEventListener('mousedown', e => onRulerDown(e, 'V'));

        // Global events
        document.addEventListener('mousemove', onMouseMove);
        document.addEventListener('mouseup',   onMouseUp);
        document.addEventListener('contextmenu', e => {
            if (drag.mode === 'rotate') e.preventDefault();
        });
    }

    // ─────────────────────────────────────────────────────────────────────────
    // LAYOUT — recompute positions whenever the WME panels change
    // ─────────────────────────────────────────────────────────────────────────
    function updateLayout() {
        const off = getMapOffset();
        // Snap to pixel to avoid sub-pixel gaps
        layout.top  = Math.round(off.top);
        layout.left = Math.round(off.left);

        const R  = CFG.RULER_W;
        const vw = window.innerWidth;
        const vh = window.innerHeight;

        // Horizontal ruler: sits at the top of the map area, spanning its width
        Object.assign(hRuler.style, {
            top:  layout.top + 'px',
            left: (layout.left + R) + 'px',
            width: (vw - layout.left - R) + 'px',
        });
        hRuler.width  = Math.max(1, vw - layout.left - R);
        hRuler.height = R;

        // Vertical ruler: sits at the left edge of the map area
        Object.assign(vRuler.style, {
            top:  (layout.top + R) + 'px',
            left: layout.left + 'px',
            height: (vh - layout.top - R) + 'px',
        });
        vRuler.width  = R;
        vRuler.height = Math.max(1, vh - layout.top - R);

        // Corner square: intersection of the two rulers
        const corner = document.getElementById('wmegl-corner');
        if (corner) {
            corner.style.top  = layout.top + 'px';
            corner.style.left = layout.left + 'px';
        }

        // Canvas: covers the full map area (below both rulers)
        Object.assign(lineCanvas.style, {
            top:  (layout.top + R) + 'px',
            left: (layout.left + R) + 'px',
            width:  (vw - layout.left - R) + 'px',
            height: (vh - layout.top - R) + 'px',
        });
        lineCanvas.width  = Math.max(1, vw - layout.left - R);
        lineCanvas.height = Math.max(1, vh - layout.top - R);

        // Clear button: just to the right of the corner, vertically centered in ruler
        const clearBtn = document.getElementById('wmegl-clear-btn');
        if (clearBtn) {
            clearBtn.style.top  = (layout.top + R + 8) + 'px';
            clearBtn.style.left = (layout.left + R + 8) + 'px';
        }

        redraw();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // COORDINATE HELPERS
    // Because the canvas starts at (layout.left + R, layout.top + R),
    // we map client coords into canvas space as:
    //   canvasX = clientX - layout.left - R
    //   canvasY = clientY - layout.top  - R
    // Guide pos values are stored in canvas space.
    // ─────────────────────────────────────────────────────────────────────────
    const R = CFG.RULER_W;

    function clientToCanvas(cx, cy) {
        return {
            x: cx - layout.left - R,
            y: cy - layout.top  - R,
        };
    }

    function canvasToClient(cx, cy) {
        return {
            x: cx + layout.left + R,
            y: cy + layout.top  + R,
        };
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DRAW
    // ─────────────────────────────────────────────────────────────────────────
    function redraw(hoverGuide) {
        drawLines(hoverGuide);
        drawHRuler();
        drawVRuler();
        rebuildHitZones();
    }

    function drawLines(hoverGuide) {
        const W = lineCanvas.width, H = lineCanvas.height;
        lCtx.clearRect(0, 0, W, H);
        if (hidden) return;
        guides.forEach(g => drawOneLine(g, g === hoverGuide));
    }

    function drawOneLine(g, highlight) {
        const W = lineCanvas.width, H = lineCanvas.height;
        const ep = endpoints(g, W, H);
        lCtx.save();
        lCtx.beginPath();
        lCtx.moveTo(ep.x1, ep.y1);
        lCtx.lineTo(ep.x2, ep.y2);
        lCtx.setLineDash(CFG.DASH);
        lCtx.strokeStyle = highlight ? CFG.COLOR_HOVER : CFG.COLOR_IDLE;
        lCtx.globalAlpha = CFG.ALPHA;
        lCtx.lineWidth   = highlight ? CFG.LINE_W + 1.5 : CFG.LINE_W;
        lCtx.stroke();
        lCtx.restore();
    }

    // Two far endpoints of an infinite guide line within the canvas
    function endpoints(g, W, H) {
        const BIG = Math.max(W, H) * 4;
        const rad = (g.angleDeg % 180) * Math.PI / 180;
        const dx  = Math.cos(rad);
        const dy  = Math.sin(rad);
        // Anchor: for H-guide the anchor y = g.pos; for V-guide the anchor x = g.pos
        const ax = g.type === 'V' ? g.pos : W / 2;
        const ay = g.type === 'H' ? g.pos : H / 2;
        return {
            x1: ax - dx * BIG, y1: ay - dy * BIG,
            x2: ax + dx * BIG, y2: ay + dy * BIG,
        };
    }

    function distToGuide(g, cx, cy) {
        const W = lineCanvas.width, H = lineCanvas.height;
        const ep = endpoints(g, W, H);
        const dx = ep.x2 - ep.x1, dy = ep.y2 - ep.y1;
        const len = Math.sqrt(dx * dx + dy * dy) || 1;
        return Math.abs((cy - ep.y1) * dx - (cx - ep.x1) * dy) / len;
    }

    function findGuideAt(cx, cy) {
        for (let i = guides.length - 1; i >= 0; i--) {
            if (distToGuide(guides[i], cx, cy) <= CFG.HIT_RADIUS) return guides[i];
        }
        return null;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // DRAW RULERS
    // ─────────────────────────────────────────────────────────────────────────
    function drawHRuler() {
        if (!hRuler.width) return;
        const W = hRuler.width, H = hRuler.height;
        const ctx = hRuler.getContext('2d');
        ctx.clearRect(0, 0, W, H);
        ctx.fillStyle = 'rgba(27, 31, 46, 0.4)';
        ctx.fillRect(0, 0, W, H);

        ctx.strokeStyle = '#2e3a55';
        ctx.fillStyle   = '#4a5a7a';
        ctx.font        = '7px monospace';
        ctx.lineWidth   = 1;
        for (let x = 0; x < W; x += 50) {
            const isMaj = (x % 100 === 0);
            ctx.beginPath();
            ctx.moveTo(x, H);
            ctx.lineTo(x, H - (isMaj ? CFG.TICK_LG : CFG.TICK_SM));
            ctx.stroke();
            if (isMaj && x > 0) ctx.fillText(x, x + 2, H - 2);
        }

        // Hint text in the middle
        ctx.fillStyle = '#2e3a55';
        ctx.font = '9px sans-serif';
        const hint = '↓ arraste para criar guia horizontal';
        ctx.fillText(hint, W / 2 - ctx.measureText(hint).width / 2, H - 2);

        // Markers for existing guides
        if (!hidden) {
            guides.forEach(g => {
                // For H-guides: mark their Y position on the H-ruler? No, H-guides are horizontal.
                // For V-guides: mark their X position on the H-ruler.
                if (g.type === 'V') {
                    const x = g.pos; // already in canvas space
                    ctx.fillStyle = CFG.COLOR_IDLE;
                    ctx.globalAlpha = 0.9;
                    ctx.beginPath();
                    ctx.moveTo(x, H);
                    ctx.lineTo(x - 4, H - 7);
                    ctx.lineTo(x + 4, H - 7);
                    ctx.closePath();
                    ctx.fill();
                    ctx.globalAlpha = 1;
                }
            });
        }
    }

    function drawVRuler() {
        if (!vRuler.height) return;
        const W = vRuler.width, H = vRuler.height;
        const ctx = vRuler.getContext('2d');
        ctx.clearRect(0, 0, W, H);
        ctx.fillStyle = 'rgba(27, 31, 46, 0.4)';
        ctx.fillRect(0, 0, W, H);

        ctx.strokeStyle = '#2e3a55';
        ctx.fillStyle   = '#4a5a7a';
        ctx.font        = '7px monospace';
        ctx.lineWidth   = 1;
        for (let y = 0; y < H; y += 50) {
            const isMaj = (y % 100 === 0);
            ctx.beginPath();
            ctx.moveTo(W, y);
            ctx.lineTo(W - (isMaj ? CFG.TICK_LG : CFG.TICK_SM), y);
            ctx.stroke();
            if (isMaj && y > 0) {
                ctx.save();
                ctx.translate(W - 2, y - 2);
                ctx.rotate(-Math.PI / 2);
                ctx.fillText(y, 0, 0);
                ctx.restore();
            }
        }

        // Hint
        ctx.save();
        ctx.fillStyle = '#2e3a55';
        ctx.font = '9px sans-serif';
        ctx.translate(W - 2, H / 2);
        ctx.rotate(-Math.PI / 2);
        const hint = '→ arraste para criar guia vertical';
        ctx.fillText(hint, -ctx.measureText(hint).width / 2, 0);
        ctx.restore();

        // Markers for H-guides on v-ruler
        if (!hidden) {
            guides.forEach(g => {
                if (g.type === 'H') {
                    const y = g.pos;
                    ctx.fillStyle = CFG.COLOR_IDLE;
                    ctx.globalAlpha = 0.9;
                    ctx.beginPath();
                    ctx.moveTo(W, y);
                    ctx.lineTo(W - 7, y - 4);
                    ctx.lineTo(W - 7, y + 4);
                    ctx.closePath();
                    ctx.fill();
                    ctx.globalAlpha = 1;
                }
            });
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // HIT ZONES (invisible divs over each line for mouse interaction)
    // ─────────────────────────────────────────────────────────────────────────
    function rebuildHitZones() {
        document.querySelectorAll('.wmegl-hit').forEach(el => el.remove());
        if (hidden) return;

        const W = lineCanvas.width, H = lineCanvas.height;
        const HIT = CFG.HIT_RADIUS * 2;

        guides.forEach(g => {
            const ep  = endpoints(g, W, H);
            // Convert canvas endpoints back to client coords
            const cl1 = canvasToClient(ep.x1, ep.y1);
            const cl2 = canvasToClient(ep.x2, ep.y2);

            const hit  = document.createElement('div');
            hit.className   = 'wmegl-hit';
            hit.dataset.id  = g.id;

            const mx  = (cl1.x + cl2.x) / 2;
            const my  = (cl1.y + cl2.y) / 2;
            const len = Math.hypot(cl2.x - cl1.x, cl2.y - cl1.y);
            const ang = Math.atan2(cl2.y - cl1.y, cl2.x - cl1.x) * 180 / Math.PI;

            Object.assign(hit.style, {
                left:            (mx - len / 2) + 'px',
                top:             (my - HIT / 2) + 'px',
                width:           len + 'px',
                height:          HIT + 'px',
                transformOrigin: `${len / 2}px ${HIT / 2}px`,
                transform:       `rotate(${ang}deg)`,
            });

            hit.addEventListener('mousedown', e => {
                const guide = guides.find(g => g.id === +hit.dataset.id);
                if (!guide) return;
                e.preventDefault();
                e.stopPropagation();
                if (e.button === 0) {
                    drag.active    = true;
                    drag.mode      = 'move';
                    drag.guideId   = guide.id;
                    drag.startCX   = e.clientX;
                    drag.startCY   = e.clientY;
                    drag.startPos  = guide.pos;
                } else if (e.button === 2) {
                    drag.active      = true;
                    drag.mode        = 'rotate';
                    drag.guideId     = guide.id;
                    drag.startCX     = e.clientX;
                    drag.startCY     = e.clientY;
                    drag.startAngle  = guide.angleDeg;
                }
            });

            hit.addEventListener('dblclick', e => {
                e.preventDefault();
                e.stopPropagation();
                removeGuide(+hit.dataset.id);
                redraw();
            });

            document.body.appendChild(hit);
        });
    }

    // ─────────────────────────────────────────────────────────────────────────
    // RULER DRAG — create guide
    // ─────────────────────────────────────────────────────────────────────────
    function onRulerDown(e, type) {
        if (e.button !== 0) return;
        if (guides.length >= CFG.MAX_GUIDES) return;
        e.preventDefault();
        e.stopPropagation();
        drag.active = true;
        drag.mode   = type === 'H' ? 'create-H' : 'create-V';
        drag.startCX = e.clientX;
        drag.startCY = e.clientY;
        showGhost(type, e.clientX, e.clientY);
    }

    // ─────────────────────────────────────────────────────────────────────────
    // MOUSEMOVE
    // ─────────────────────────────────────────────────────────────────────────
    function onMouseMove(e) {
        if (!drag.active) {
            // Hover highlight — only inside canvas area
            const { x, y } = clientToCanvas(e.clientX, e.clientY);
            if (x >= 0 && y >= 0 && x <= lineCanvas.width && y <= lineCanvas.height) {
                const g = findGuideAt(x, y);
                redraw(g || null);
            }
            return;
        }

        const dx = e.clientX - drag.startCX;
        const dy = e.clientY - drag.startCY;

        if (drag.mode === 'create-H') {
            moveGhost('H', e.clientX, e.clientY);
        } else if (drag.mode === 'create-V') {
            moveGhost('V', e.clientX, e.clientY);
        } else if (drag.mode === 'move') {
            const g = guides.find(g => g.id === drag.guideId);
            if (g) {
                g.pos = drag.startPos + (g.type === 'H' ? dy : dx);
                redraw();
            }
        } else if (drag.mode === 'rotate') {
            const g = guides.find(g => g.id === drag.guideId);
            if (g) {
                g.angleDeg = ((drag.startAngle + dx) % 180 + 180) % 180;
                redraw();
                angleLabel.textContent  = `${Math.round(g.angleDeg)}°`;
                angleLabel.style.left   = (e.clientX + 14) + 'px';
                angleLabel.style.top    = (e.clientY - 10) + 'px';
                angleLabel.style.display = 'block';
            }
        }
    }

    // ─────────────────────────────────────────────────────────────────────────
    // MOUSEUP
    // ─────────────────────────────────────────────────────────────────────────
    function onMouseUp(e) {
        if (!drag.active) return;

        if (drag.mode === 'create-H' || drag.mode === 'create-V') {
            hideGhost();
            const type = drag.mode === 'create-H' ? 'H' : 'V';
            const { x, y } = clientToCanvas(e.clientX, e.clientY);
            // Only create if dropped inside the map canvas area
            if (x >= 0 && y >= 0 && x <= lineCanvas.width && y <= lineCanvas.height) {
                addGuide(type, type === 'H' ? y : x);
                redraw();
            }
        } else if (drag.mode === 'rotate') {
            angleLabel.style.display = 'none';
            redraw();
        } else if (drag.mode === 'move') {
            redraw();
        }

        drag.active  = false;
        drag.mode    = null;
        drag.guideId = null;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // GHOST LINE
    // ─────────────────────────────────────────────────────────────────────────
    function showGhost(type, cx, cy) {
        const ghost = document.getElementById('wmegl-ghost');
        ghost.style.display = 'block';
        if (type === 'H') {
            Object.assign(ghost.style, {
                left: '0', right: '0', top: cy + 'px', bottom: 'auto',
                width: '100vw', height: '2px',
                transform: 'translateY(-50%)',
            });
        } else {
            Object.assign(ghost.style, {
                top: '0', bottom: '0', left: cx + 'px', right: 'auto',
                width: '2px', height: '100vh',
                transform: 'translateX(-50%)',
            });
        }
    }

    function moveGhost(type, cx, cy) {
        const ghost = document.getElementById('wmegl-ghost');
        if (ghost.style.display === 'none') return;
        if (type === 'H') ghost.style.top  = cy + 'px';
        else              ghost.style.left = cx + 'px';
    }

    function hideGhost() {
        document.getElementById('wmegl-ghost').style.display = 'none';
    }

    // ─────────────────────────────────────────────────────────────────────────
    // GUIDE CRUD
    // ─────────────────────────────────────────────────────────────────────────
    function addGuide(type, pos) {
        guides.push({ id: nextId++, type, pos, angleDeg: type === 'H' ? 0 : 90 });
    }

    function removeGuide(id) {
        const i = guides.findIndex(g => g.id === id);
        if (i !== -1) guides.splice(i, 1);
    }

    function clearAll() {
        guides.length = 0;
        document.querySelectorAll('.wmegl-hit').forEach(el => el.remove());
        redraw();
    }

    // ─────────────────────────────────────────────────────────────────────────
    // INIT
    // ─────────────────────────────────────────────────────────────────────────
    function init() {
        if (document.getElementById('wmegl-style')) return;
        // Wait until the WME map container is rendered
        const wait = () => {
            const wazeMap = document.getElementById('WazeMap') || findEl(HEADER_SEL);
            if (wazeMap && wazeMap.clientWidth > 0) setup();
            else setTimeout(wait, 600);
        };
        wait();
    }

    $(document).on('bootstrap.wme', init);

})();