Greasy Fork is available in English.

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.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);

})();