← Whiteboard results

Whiteboard

Qwen 3.6 27b · typescript

Initial view of Whiteboard

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 82,134 bytes · SHA-256 d7d9f2fca40c
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Whiteboard — Collaborative Canvas</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: 'Inter', 'Segoe UI', system-ui, -apple-system, sans-serif; background: #f0f1f3; }
#app { position: relative; width: 100%; height: 100%; overflow: hidden; }
#canvas { display: block; width: 100%; height: 100%; cursor: crosshair; }

/* Cursor styles per tool */
#canvas.tool-select { cursor: default; }
#canvas.tool-pen { cursor: crosshair; }
#canvas.tool-rectangle, #canvas.tool-ellipse, #canvas.tool-line, #canvas.tool-arrow { cursor: crosshair; }
#canvas.tool-text { cursor: text; }
#canvas.tool-eraser { cursor: none; }
#canvas.panning { cursor: grabbing !important; }

/* Eraser cursor indicator */
#eraser-cursor {
  position: absolute; pointer-events: none; z-index: 50;
  width: 20px; height: 20px; border-radius: 50%;
  border: 2px solid #fa5252; background: rgba(250, 82, 82, 0.1);
  transform: translate(-50%, -50%);
  display: none;
}
/* ── css: toolbar ── */
#toolbar {
  position: absolute; top: 12px; left: 50%; transform: translateX(-50%);
  display: flex; gap: 2px; padding: 6px; z-index: 100;
  background: #ffffff; border-radius: 12px;
  border: 1px solid #dee2e6;
  box-shadow: 0 2px 12px rgba(0,0,0,0.10);
}
#toolbar button {
  width: 38px; height: 38px; border: none; border-radius: 8px;
  background: transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  font-size: 18px; color: #495057; transition: all 0.15s;
  position: relative;
}
#toolbar button:hover { background: #f1f3f5; }
#toolbar button.active { background: #e7f5ff; color: #228be6; border: 1.5px solid #4dabf7; }
#toolbar button .tooltip {
  display: none; position: absolute; bottom: -30px; left: 50%; transform: translateX(-50%);
  background: #212529; color: #fff; font-size: 11px; padding: 3px 8px; border-radius: 4px;
  white-space: nowrap; pointer-events: none;
}
#toolbar button:hover .tooltip { display: block; }
#toolbar button .shortcut {
  position: absolute; top: 2px; right: 4px; font-size: 9px; color: #adb5bd; font-weight: 600;
}
/* ── css: propbar ── */
#propbar {
  position: absolute; top: 70px; left: 50%; transform: translateX(-50%);
  display: flex; align-items: center; gap: 10px; padding: 6px 14px; z-index: 100;
  background: #ffffff; border-radius: 10px;
  border: 1px solid #dee2e6;
  box-shadow: 0 2px 10px rgba(0,0,0,0.08);
  opacity: 0; pointer-events: none; transition: opacity 0.2s;
}
#propbar.visible { opacity: 1; pointer-events: auto; }
#propbar label { font-size: 11px; color: #868e96; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.color-swatch {
  width: 24px; height: 24px; border-radius: 6px; border: 2px solid transparent;
  cursor: pointer; transition: all 0.15s; display: inline-block;
}
.color-swatch:hover { transform: scale(1.15); }
.color-swatch.active { border-color: #4dabf7; box-shadow: 0 0 0 1px #4dabf7; }
.color-row { display: flex; gap: 4px; align-items: center; }
.width-btn {
  width: 28px; height: 28px; border: 1px solid #dee2e6; border-radius: 6px;
  background: #fff; cursor: pointer; font-size: 12px; color: #495057;
  display: flex; align-items: center; justify-content: center; transition: all 0.15s;
}
.width-btn:hover { background: #f1f3f5; }
.width-btn.active { background: #e7f5ff; border-color: #4dabf7; color: #228be6; }
.divider { width: 1px; height: 24px; background: #dee2e6; }
/* ── css: zoom-history ── */
#zoom-indicator {
  position: absolute; bottom: 16px; right: 16px; z-index: 100;
  display: flex; align-items: center; gap: 4px; padding: 4px 6px;
  background: #ffffff; border-radius: 8px;
  border: 1px solid #dee2e6;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
#zoom-indicator button {
  width: 30px; height: 30px; border: none; border-radius: 6px;
  background: transparent; cursor: pointer; font-size: 16px; color: #495057;
  display: flex; align-items: center; justify-content: center; transition: all 0.15s;
}
#zoom-indicator button:hover { background: #f1f3f5; }
#zoom-level { font-size: 13px; color: #495057; min-width: 50px; text-align: center; font-weight: 500; user-select: none; }

#history-bar {
  position: absolute; bottom: 16px; left: 16px; z-index: 100;
  display: flex; align-items: center; gap: 4px; padding: 4px 6px;
  background: #ffffff; border-radius: 8px;
  border: 1px solid #dee2e6;
  box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
#history-bar button {
  width: 30px; height: 30px; border: none; border-radius: 6px;
  background: transparent; cursor: pointer; font-size: 16px; color: #495057;
  display: flex; align-items: center; justify-content: center; transition: all 0.15s;
}
#history-bar button:hover { background: #f1f3f5; }
#history-bar button:disabled { opacity: 0.3; cursor: default; }
#history-bar button:disabled:hover { background: transparent; }
</style>
</head>
<body>
<div id="app">
  <div id="toolbar"></div>
  <div id="propbar"></div>
  <div id="zoom-indicator">
    <button id="zoom-out" title="Zoom Out">−</button>
    <span id="zoom-level">100%</span>
    <button id="zoom-in" title="Zoom In">+</button>
    <button id="zoom-reset" title="Reset View">⌂</button>
  </div>
  <div id="history-bar">
    <button id="undo-btn" title="Undo (Ctrl+Z)">↶</button>
    <button id="redo-btn" title="Redo (Ctrl+Shift+Z)">↷</button>
  </div>
  <canvas id="canvas"></canvas>
  <textarea id="text-input" style="display:none; position:absolute; border:2px solid #4dabf7; border-radius:4px; padding:4px 8px; outline:none; resize:none; font-family:inherit; background:#fff; box-shadow:0 2px 8px rgba(0,0,0,0.15);"></textarea>
<div id="eraser-cursor"></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./render":"src/render.ts","./input":"src/input.ts","./ui":"src/ui.ts","./persistence":"src/persistence.ts","./history":"src/history.ts"},"src/state.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/viewport.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/scene.ts":{"./types":"src/types.ts","./state":"src/state.ts","./constants":"src/constants.ts"},"src/hitTest.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/history.ts":{"./types":"src/types.ts","./scene":"src/scene.ts","./state":"src/state.ts"},"src/render.ts":{"./types":"src/types.ts","./viewport":"src/viewport.ts","./scene":"src/scene.ts","./state":"src/state.ts","./constants":"src/constants.ts"},"src/persistence.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./scene":"src/scene.ts","./state":"src/state.ts","./history":"src/history.ts"},"src/tools.ts":{"./types":"src/types.ts","./state":"src/state.ts","./scene":"src/scene.ts","./hitTest":"src/hitTest.ts","./history":"src/history.ts","./constants":"src/constants.ts","./persistence":"src/persistence.ts"},"src/input.ts":{"./types":"src/types.ts","./state":"src/state.ts","./viewport":"src/viewport.ts","./scene":"src/scene.ts","./hitTest":"src/hitTest.ts","./tools":"src/tools.ts","./history":"src/history.ts","./constants":"src/constants.ts","./persistence":"src/persistence.ts"},"src/ui.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./state":"src/state.ts","./history":"src/history.ts","./viewport":"src/viewport.ts","./tools":"src/tools.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ── Entry Point ───────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const state_1 = require("./state");
const render_1 = require("./render");
const input_1 = require("./input");
const ui_1 = require("./ui");
const persistence_1 = require("./persistence");
const history_1 = require("./history");
function start() {
    try {
        // 1. Initialize state
        (0, state_1.initState)();
        // 2. Try to load from storage
        const loaded = (0, persistence_1.loadFromStorage)();
        if (!loaded)
            (0, history_1.clearHistory)();
        // 3. Build UI
        (0, ui_1.buildUI)();
        // 4. Get DOM elements
        const canvas = document.getElementById('canvas');
        const textInput = document.getElementById('text-input');
        if (!canvas || !textInput) {
            console.error('Missing required DOM elements');
            return;
        }
        // 5. Initialize canvas
        (0, render_1.initRender)(canvas);
        (0, render_1.resizeCanvas)(window.innerWidth, window.innerHeight);
        // 6. Initialize input handling
        (0, input_1.initInput)(canvas, textInput);
        // 7. Set up render callback
        (0, input_1.setRenderCallback)(() => {
            (0, render_1.render)();
            (0, ui_1.updatePropBarVisibility)();
        });
        // 8. Set up toolbar callback
        (0, input_1.setToolbarCallback)(() => {
            (0, ui_1.updateToolbar)();
        });
        // 9. Set up zoom display callback
        (0, input_1.setZoomDisplayCallback)(() => {
            const el = document.getElementById('zoom-level');
            if (el)
                el.textContent = Math.round((0, state_1.getState)().viewport.zoom * 100) + '%';
        });
        // 10. Set up history callback
        (0, input_1.setHistoryCallback)(() => {
            (0, ui_1.updateHistoryButtons)();
        });
        // 11. Set render trigger for UI module
        (0, ui_1.setRenderTrigger)(() => {
            (0, render_1.render)();
            (0, ui_1.updatePropBarVisibility)();
        });
        // 12. Initial render
        (0, ui_1.updateToolbar)();
        (0, ui_1.updateHistoryButtons)();
        (0, render_1.render)();
        // 13. Handle resize
        window.addEventListener('resize', () => {
            (0, render_1.resizeCanvas)(window.innerWidth, window.innerHeight);
            (0, render_1.render)();
        });
        // 14. Periodic save
        setInterval(() => (0, persistence_1.saveToStorage)(), 5000);
        // 15. Save on page unload
        window.addEventListener('beforeunload', () => (0, persistence_1.saveToStorage)());
    }
    catch (e) {
        console.error('Whiteboard init error:', e);
    }
}
// Self-invoke entry
start();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ── Tunable Constants ──────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.TEXT_INPUT_DELAY = exports.STORAGE_KEY = exports.MIN_SHAPE_SIZE = exports.DEFAULT_FONT_SIZE = exports.DEFAULT_STROKE_WIDTH = exports.DEFAULT_FILL_COLOR = exports.DEFAULT_STROKE_COLOR = exports.NUDGE_STEP_SHIFT = exports.NUDGE_STEP = exports.GRID_SPACING = exports.HANDLE_SIZE = exports.HIT_TOLERANCE = exports.ZOOM_STEP = exports.ZOOM_MAX = exports.ZOOM_MIN = exports.FILL_PALETTE = exports.PALETTE = exports.COLORS = void 0;
exports.COLORS = {
    canvas: '#f8f9fa',
    chrome: '#ffffff',
    chromeBorder: '#dee2e6',
    chromeShadow: 'rgba(0,0,0,0.12)',
    gridDot: '#d0d5dc',
    gridLine: '#e2e5ea',
    selection: '#4dabf7',
    selectionFill: 'rgba(77,171,247,0.08)',
    handle: '#ffffff',
    handleStroke: '#4dabf7',
    preview: 'rgba(77,171,247,0.5)',
    textPlaceholder: '#adb5bd',
    toolbarBg: '#ffffff',
    toolbarBorder: '#dee2e6',
    toolbarShadow: 'rgba(0,0,0,0.10)',
    toolActiveBg: '#e7f5ff',
    toolActiveBorder: '#4dabf7',
    propBg: '#ffffff',
    propBorder: '#dee2e6',
    propShadow: 'rgba(0,0,0,0.08)',
    zoomBg: '#ffffff',
    zoomBorder: '#dee2e6',
    zoomShadow: 'rgba(0,0,0,0.08)',
    text: '#212529',
    textMuted: '#868e96',
    danger: '#fa5252',
    success: '#40c057',
};
exports.PALETTE = [
    '#212529', '#495057', '#868e96',
    '#fa5252', '#e64980', '#be4bdb',
    '#7950f2', '#4c6ef5', '#228be6',
    '#15aabf', '#12b886', '#40c057',
    '#fab005', '#fd7e14',
];
exports.FILL_PALETTE = [
    'transparent',
    '#fff3bf', '#d3f9d8', '#d0ebff', '#e5dbff',
    '#ffe3e3', '#fce4ec', '#ffe8cc',
];
exports.ZOOM_MIN = 0.25;
exports.ZOOM_MAX = 4.0;
exports.ZOOM_STEP = 1.1;
exports.HIT_TOLERANCE = 6; // px for stroke hit-testing
exports.HANDLE_SIZE = 8; // px (screen space)
exports.GRID_SPACING = 24; // px (world space)
exports.NUDGE_STEP = 1; // px (world)
exports.NUDGE_STEP_SHIFT = 10; // px (world)
exports.DEFAULT_STROKE_COLOR = '#212529';
exports.DEFAULT_FILL_COLOR = 'transparent';
exports.DEFAULT_STROKE_WIDTH = 2;
exports.DEFAULT_FONT_SIZE = 20;
exports.MIN_SHAPE_SIZE = 2;
exports.STORAGE_KEY = 'whiteboard-state-v1';
exports.TEXT_INPUT_DELAY = 150; // ms debounce for text commits
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ── Type Definitions ──────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ── Global State ──────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initState = initState;
exports.getState = getState;
exports.setState = setState;
exports.setViewport = setViewport;
exports.setCurrentTool = setCurrentTool;
exports.setSelectedIds = setSelectedIds;
exports.getDefaultStyle = getDefaultStyle;
exports.setDefaultStyle = setDefaultStyle;
exports.nextId = nextId;
const constants_1 = require("./constants");
let state;
function createInitialState() {
    return {
        shapes: [],
        viewport: { x: 0, y: 0, zoom: 1 },
        currentTool: 'select',
        selectedIds: [],
        defaultStyle: {
            strokeColor: constants_1.DEFAULT_STROKE_COLOR,
            fillColor: constants_1.DEFAULT_FILL_COLOR,
            strokeWidth: constants_1.DEFAULT_STROKE_WIDTH,
            fontSize: constants_1.DEFAULT_FONT_SIZE,
        },
        isPanning: false,
        dragState: null,
        spaceDown: false,
    };
}
function initState() {
    state = createInitialState();
    return state;
}
function getState() {
    return state;
}
function setState(updater) {
    updater(state);
}
function setViewport(vp) {
    state.viewport = { ...vp };
}
function setCurrentTool(tool) {
    state.currentTool = tool;
    if (tool !== 'select') {
        state.selectedIds = [];
    }
}
function setSelectedIds(ids) {
    state.selectedIds = ids;
}
function getDefaultStyle() {
    return { ...state.defaultStyle };
}
function setDefaultStyle(style) {
    state.defaultStyle = { ...state.defaultStyle, ...style };
}
// Generate a unique ID
let _idCounter = 0;
function nextId() {
    return 's' + (++_idCounter).toString(36) + Date.now().toString(36);
}
};

// ── module: src/viewport.ts ──
__mods["src/viewport.ts"] = function (exports, require, module) {
"use strict";
// ── Viewport Math ─────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.screenToWorld = screenToWorld;
exports.worldToScreen = worldToScreen;
exports.zoomAtScreen = zoomAtScreen;
exports.resetView = resetView;
exports.panWorld = panWorld;
exports.panScreen = panScreen;
const constants_1 = require("./constants");
/** Convert screen (canvas pixel) → world coordinates */
function screenToWorld(sx, sy, vp) {
    return [sx / vp.zoom - vp.x, sy / vp.zoom - vp.y];
}
/** Convert world → screen (canvas pixel) coordinates */
function worldToScreen(wx, wy, vp) {
    return [(wx + vp.x) * vp.zoom, (wy + vp.y) * vp.zoom];
}
/** Zoom toward a screen point, clamping the zoom level */
function zoomAtScreen(sx, sy, vp, factor) {
    const worldBefore = screenToWorld(sx, sy, vp);
    let newZoom = vp.zoom * factor;
    newZoom = Math.max(constants_1.ZOOM_MIN, Math.min(constants_1.ZOOM_MAX, newZoom));
    // Keep the world point under the cursor
    const newX = sx / newZoom - worldBefore[0];
    const newY = sy / newZoom - worldBefore[1];
    return { x: newX, y: newY, zoom: newZoom };
}
/** Reset view to origin at zoom 1 */
function resetView() {
    return { x: 0, y: 0, zoom: 1 };
}
/** Pan the viewport by delta in world coordinates */
function panWorld(vp, dx, dy) {
    return { x: vp.x - dx, y: vp.y - dy, zoom: vp.zoom };
}
/** Pan the viewport by delta in screen coordinates */
function panScreen(vp, dx, dy) {
    return { x: vp.x - dx / vp.zoom, y: vp.y - dy / vp.zoom, zoom: vp.zoom };
}
};

// ── module: src/scene.ts ──
__mods["src/scene.ts"] = function (exports, require, module) {
"use strict";
// ── Scene Management ──────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.getShapes = getShapes;
exports.addShape = addShape;
exports.removeShape = removeShape;
exports.removeShapes = removeShapes;
exports.getShape = getShape;
exports.updateShape = updateShape;
exports.createRectangle = createRectangle;
exports.createEllipse = createEllipse;
exports.createLine = createLine;
exports.createFreehand = createFreehand;
exports.createText = createText;
exports.getShapeBounds = getShapeBounds;
exports.getSelectionBounds = getSelectionBounds;
const state_1 = require("./state");
const constants_1 = require("./constants");
function getShapes() {
    return (0, state_1.getState)().shapes;
}
function addShape(shape) {
    (0, state_1.setState)(s => s.shapes.push(shape));
}
function removeShape(id) {
    (0, state_1.setState)(s => { s.shapes = s.shapes.filter(sh => sh.id !== id); });
}
function removeShapes(ids) {
    (0, state_1.setState)(s => { s.shapes = s.shapes.filter(sh => !ids.includes(sh.id)); });
}
function getShape(id) {
    return (0, state_1.getState)().shapes.find(sh => sh.id === id);
}
function updateShape(id, updater) {
    (0, state_1.setState)(s => {
        const idx = s.shapes.findIndex(sh => sh.id === id);
        if (idx >= 0)
            s.shapes[idx] = updater(s.shapes[idx]);
    });
}
function createRectangle(x, y, w, h) {
    const s = (0, state_1.getState)().defaultStyle;
    return {
        id: (0, state_1.nextId)(), type: 'rectangle',
        x, y, width: Math.abs(w), height: Math.abs(h),
        strokeColor: s.strokeColor, fillColor: s.fillColor, strokeWidth: s.strokeWidth,
    };
}
function createEllipse(x, y, w, h) {
    const s = (0, state_1.getState)().defaultStyle;
    return {
        id: (0, state_1.nextId)(), type: 'ellipse',
        x, y, width: Math.abs(w), height: Math.abs(h),
        strokeColor: s.strokeColor, fillColor: s.fillColor, strokeWidth: s.strokeWidth,
    };
}
function createLine(x1, y1, x2, y2, arrow = false) {
    const s = (0, state_1.getState)().defaultStyle;
    return {
        id: (0, state_1.nextId)(), type: arrow ? 'arrow' : 'line',
        x1, y1, x2, y2,
        strokeColor: s.strokeColor, strokeWidth: s.strokeWidth,
    };
}
function createFreehand(points) {
    const s = (0, state_1.getState)().defaultStyle;
    return {
        id: (0, state_1.nextId)(), type: 'freehand',
        points, strokeColor: s.strokeColor, strokeWidth: s.strokeWidth,
    };
}
function createText(x, y) {
    const s = (0, state_1.getState)().defaultStyle;
    return {
        id: (0, state_1.nextId)(), type: 'text',
        x, y, text: '', fontSize: s.fontSize ?? constants_1.DEFAULT_FONT_SIZE,
        strokeColor: s.strokeColor, fillColor: s.fillColor,
        width: 200, height: 40,
    };
}
function getShapeBounds(shape) {
    switch (shape.type) {
        case 'rectangle':
        case 'ellipse':
            return { x: shape.x, y: shape.y, w: shape.width, h: shape.height };
        case 'line':
        case 'arrow': {
            const minX = Math.min(shape.x1, shape.x2);
            const minY = Math.min(shape.y1, shape.y2);
            return { x: minX, y: minY, w: Math.abs(shape.x2 - shape.x1), h: Math.abs(shape.y2 - shape.y1) };
        }
        case 'freehand': {
            if (!shape.points.length)
                return { x: 0, y: 0, w: 0, h: 0 };
            let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
            for (const [px, py] of shape.points) {
                if (px < minX)
                    minX = px;
                if (py < minY)
                    minY = py;
                if (px > maxX)
                    maxX = px;
                if (py > maxY)
                    maxY = py;
            }
            return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
        }
        case 'text':
            return { x: shape.x, y: shape.y, w: shape.width, h: shape.height };
    }
}
function getSelectionBounds(ids) {
    const shapes = ids.map(id => getShape(id)).filter(Boolean);
    if (!shapes.length)
        return null;
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const sh of shapes) {
        const b = getShapeBounds(sh);
        if (b.x < minX)
            minX = b.x;
        if (b.y < minY)
            minY = b.y;
        if (b.x + b.w > maxX)
            maxX = b.x + b.w;
        if (b.y + b.h > maxY)
            maxY = b.y + b.h;
    }
    return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
}
};

// ── module: src/hitTest.ts ──
__mods["src/hitTest.ts"] = function (exports, require, module) {
"use strict";
// ── Hit Testing ───────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.hitTestShape = hitTestShape;
exports.findHitShape = findHitShape;
exports.findShapesInRect = findShapesInRect;
exports.hitTestHandle = hitTestHandle;
const constants_1 = require("./constants");
/**
 * Distance from point (px, py) to line segment (x1,y1)-(x2,y2).
 */
function pointToSegmentDist(px, py, x1, y1, x2, y2) {
    const dx = x2 - x1, dy = y2 - y1;
    const lenSq = dx * dx + dy * dy;
    if (lenSq === 0)
        return Math.hypot(px - x1, py - y1);
    let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
    t = Math.max(0, Math.min(1, t));
    const projX = x1 + t * dx, projY = y1 + t * dy;
    return Math.hypot(px - projX, py - projY);
}
/**
 * Check if point is inside a rectangle (including border with tolerance).
 */
function hitTestRectangle(wx, wy, shape) {
    const hw = shape.strokeWidth / 2 + constants_1.HIT_TOLERANCE;
    // Check border
    if (wx >= shape.x - hw && wx <= shape.x + shape.width + hw &&
        wy >= shape.y - hw && wy <= shape.y + shape.height + hw) {
        // Inside the expanded rect
        // Check if within the actual shape area (filled or border)
        const inside = wx >= shape.x && wx <= shape.x + shape.width &&
            wy >= shape.y && wy <= shape.y + shape.height;
        if (inside)
            return true;
        // Check near border
        const nearLeft = Math.abs(wx - shape.x) <= hw;
        const nearRight = Math.abs(wx - (shape.x + shape.width)) <= hw;
        const nearTop = Math.abs(wy - shape.y) <= hw;
        const nearBottom = Math.abs(wy - (shape.y + shape.height)) <= hw;
        if (nearLeft || nearRight || nearTop || nearBottom)
            return true;
    }
    return false;
}
/**
 * Check if point is inside an ellipse (including border with tolerance).
 * Uses the ellipse equation: ((x-h)/a)^2 + ((y-k)/b)^2 <= 1
 */
function hitTestEllipse(wx, wy, shape) {
    const cx = shape.x + shape.width / 2;
    const cy = shape.y + shape.height / 2;
    const rx = Math.abs(shape.width) / 2;
    const ry = Math.abs(shape.height) / 2;
    if (rx === 0 || ry === 0)
        return false;
    // Normalized distance from center
    const dx = (wx - cx) / (rx + constants_1.HIT_TOLERANCE);
    const dy = (wy - cy) / (ry + constants_1.HIT_TOLERANCE);
    return dx * dx + dy * dy <= 1;
}
/**
 * Check if point is near a line/arrow (within stroke width + tolerance).
 */
function hitTestLine(wx, wy, shape) {
    const tolerance = shape.strokeWidth / 2 + constants_1.HIT_TOLERANCE;
    return pointToSegmentDist(wx, wy, shape.x1, shape.y1, shape.x2, shape.y2) <= tolerance;
}
/**
 * Check if point is near a freehand stroke (within stroke width + tolerance).
 * Tests against each segment.
 */
function hitTestFreehand(wx, wy, shape) {
    const tolerance = shape.strokeWidth / 2 + constants_1.HIT_TOLERANCE;
    for (let i = 0; i < shape.points.length - 1; i++) {
        const [x1, y1] = shape.points[i];
        const [x2, y2] = shape.points[i + 1];
        if (pointToSegmentDist(wx, wy, x1, y1, x2, y2) <= tolerance)
            return true;
    }
    // Also check endpoints
    if (shape.points.length > 0) {
        const [lx, ly] = shape.points[shape.points.length - 1];
        if (Math.hypot(wx - lx, wy - ly) <= tolerance)
            return true;
        const [fx, fy] = shape.points[0];
        if (Math.hypot(wx - fx, wy - fy) <= tolerance)
            return true;
    }
    return false;
}
/**
 * Check if point is inside a text shape.
 */
function hitTestText(wx, wy, shape) {
    return wx >= shape.x && wx <= shape.x + shape.width &&
        wy >= shape.y && wy <= shape.y + shape.height;
}
/**
 * Test a single shape for a hit at world coordinates.
 */
function hitTestShape(wx, wy, shape) {
    switch (shape.type) {
        case 'rectangle': return hitTestRectangle(wx, wy, shape);
        case 'ellipse': return hitTestEllipse(wx, wy, shape);
        case 'line':
        case 'arrow': return hitTestLine(wx, wy, shape);
        case 'freehand': return hitTestFreehand(wx, wy, shape);
        case 'text': return hitTestText(wx, wy, shape);
    }
}
/**
 * Find the topmost shape hit at world coordinates.
 * Tests shapes in reverse order (topmost rendered last).
 */
function findHitShape(shapes, wx, wy) {
    for (let i = shapes.length - 1; i >= 0; i--) {
        if (hitTestShape(wx, wy, shapes[i]))
            return shapes[i];
    }
    return null;
}
/**
 * Find all shapes whose bounding boxes intersect a rectangle.
 */
function findShapesInRect(shapes, rx, ry, rw, rh) {
    const ids = [];
    const left = Math.min(rx, rx + rw);
    const top = Math.min(ry, ry + rh);
    const right = Math.max(rx, rx + rw);
    const bottom = Math.max(ry, ry + rh);
    for (const sh of shapes) {
        const b = getBounds(sh);
        if (b.x < right && b.x + b.w > left && b.y < bottom && b.y + b.h > top) {
            ids.push(sh.id);
        }
    }
    return ids;
}
function getBounds(sh) {
    switch (sh.type) {
        case 'rectangle':
        case 'ellipse':
            return { x: sh.x, y: sh.y, w: sh.width, h: sh.height };
        case 'line':
        case 'arrow':
            return {
                x: Math.min(sh.x1, sh.x2), y: Math.min(sh.y1, sh.y2),
                w: Math.abs(sh.x2 - sh.x1), h: Math.abs(sh.y2 - sh.y1),
            };
        case 'freehand': {
            if (!sh.points.length)
                return { x: 0, y: 0, w: 0, h: 0 };
            let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
            for (const [px, py] of sh.points) {
                if (px < minX)
                    minX = px;
                if (py < minY)
                    minY = py;
                if (px > maxX)
                    maxX = px;
                if (py > maxY)
                    maxY = py;
            }
            return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
        }
        case 'text':
            return { x: sh.x, y: sh.y, w: sh.width, h: sh.height };
    }
}
/**
 * Check if a world point is within a resize handle.
 * Handles are positioned at the 4 corners of the selection bounding box.
 */
function hitTestHandle(wx, wy, bx, by, bw, bh, handleSize) {
    // 4 corners: 0=TL, 1=TR, 2=BR, 3=BL
    const handles = [
        [bx, by],
        [bx + bw, by],
        [bx + bw, by + bh],
        [bx, by + bh],
    ];
    const half = handleSize / 2;
    for (let i = 0; i < handles.length; i++) {
        const [hx, hy] = handles[i];
        if (wx >= hx - half && wx <= hx + half && wy >= hy - half && wy <= hy + half)
            return i;
    }
    return null;
}
};

// ── module: src/history.ts ──
__mods["src/history.ts"] = function (exports, require, module) {
"use strict";
// ── Undo / Redo History ───────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.getHistoryState = getHistoryState;
exports.recordCreate = recordCreate;
exports.recordDelete = recordDelete;
exports.beginOp = beginOp;
exports.commitOp = commitOp;
exports.undo = undo;
exports.redo = redo;
exports.clearHistory = clearHistory;
const scene_1 = require("./scene");
let history = [];
let historyIndex = -1;
const MAX_HISTORY = 200;
function getHistoryState() {
    return { length: history.length, index: historyIndex };
}
function pushEntry(entry) {
    if (historyIndex < history.length - 1) {
        history = history.slice(0, historyIndex + 1);
    }
    history.push(entry);
    if (history.length > MAX_HISTORY)
        history.shift();
    historyIndex = history.length - 1;
}
function snapshot(ids) {
    const m = new Map();
    for (const id of ids)
        m.set(id, (0, scene_1.getShape)(id));
    return m;
}
/** Record a create. */
function recordCreate(shape) {
    const before = new Map();
    before.set(shape.id, undefined);
    pushEntry({ label: 'Create ' + shape.type, action: 'create', before, after: snapshot([shape.id]) });
}
/** Record a delete. */
function recordDelete(ids) {
    const before = snapshot(ids);
    for (const id of ids)
        (0, scene_1.removeShape)(id);
    pushEntry({ label: 'Delete', action: 'delete', before, after: snapshot(ids) });
}
/**
 * Record a multi-step operation (move, resize, nudge, style).
 * Call beginOp(ids, label) before the operation, then commitOp() after.
 */
let _pendingOp = null;
function beginOp(ids, label, action) {
    _pendingOp = { ids, label, action, before: snapshot(ids) };
}
function commitOp() {
    if (!_pendingOp)
        return;
    const { ids, label, action, before } = _pendingOp;
    pushEntry({ label, action, before, after: snapshot(ids) });
    _pendingOp = null;
}
/** Undo. */
function undo() {
    if (historyIndex < 0)
        return false;
    const entry = history[historyIndex];
    for (const [id, shape] of entry.before) {
        if (shape === undefined)
            (0, scene_1.removeShape)(id);
        else {
            const existing = (0, scene_1.getShape)(id);
            if (existing)
                (0, scene_1.updateShape)(id, () => shape);
            else
                (0, scene_1.addShape)(shape);
        }
    }
    historyIndex--;
    return true;
}
/** Redo. */
function redo() {
    if (historyIndex >= history.length - 1)
        return false;
    historyIndex++;
    const entry = history[historyIndex];
    for (const [id, shape] of entry.after) {
        if (shape === undefined)
            (0, scene_1.removeShape)(id);
        else {
            const existing = (0, scene_1.getShape)(id);
            if (existing)
                (0, scene_1.updateShape)(id, () => shape);
            else
                (0, scene_1.addShape)(shape);
        }
    }
    return true;
}
function clearHistory() {
    history = [];
    historyIndex = -1;
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ── Canvas Rendering ──────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRender = initRender;
exports.resizeCanvas = resizeCanvas;
exports.render = render;
const viewport_1 = require("./viewport");
const scene_1 = require("./scene");
const state_1 = require("./state");
const constants_1 = require("./constants");
let canvas;
let ctx;
function initRender(c) {
    canvas = c;
    ctx = c.getContext('2d');
}
function resizeCanvas(w, h) {
    canvas.width = w * devicePixelRatio;
    canvas.height = h * devicePixelRatio;
    canvas.style.width = w + 'px';
    canvas.style.height = h + 'px';
    ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
}
function render() {
    const s = (0, state_1.getState)();
    const vp = s.viewport;
    const w = canvas.width / devicePixelRatio;
    const h = canvas.height / devicePixelRatio;
    ctx.clearRect(0, 0, w, h);
    // Background
    ctx.fillStyle = constants_1.COLORS.canvas;
    ctx.fillRect(0, 0, w, h);
    // Grid
    drawGrid(vp, w, h);
    // Apply viewport transform with high-quality rendering
    ctx.save();
    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = 'high';
    ctx.translate(vp.x * vp.zoom, vp.y * vp.zoom);
    ctx.scale(vp.zoom, vp.zoom);
    // Draw shapes
    for (const shape of (0, scene_1.getShapes)()) {
        drawShape(shape, s.selectedIds.includes(shape.id));
    }
    ctx.restore();
    // Draw selection overlay (handles in screen space)
    drawSelectionOverlay(s.selectedIds, vp);
    // Draw drag preview
    if (s.dragState) {
        drawDragPreview(s.dragState, vp);
    }
    // Draw rubberband rect
    if (s.dragState && s.dragState.type === 'rubberband' && s.dragState.rubberbandRect) {
        drawRubberband(s.dragState.rubberbandRect, vp);
    }
}
function drawGrid(vp, w, h) {
    const gridSize = constants_1.GRID_SPACING * vp.zoom;
    if (gridSize < 3)
        return; // Too small
    const startX = ((-vp.x * vp.zoom) % gridSize + gridSize) % gridSize;
    const startY = ((-vp.y * vp.zoom) % gridSize + gridSize) % gridSize;
    const dotSize = Math.max(0.5, Math.min(2, gridSize * 0.06));
    const alpha = Math.max(0.15, Math.min(0.5, 0.15 + gridSize * 0.003));
    ctx.fillStyle = `rgba(0,0,0,${alpha})`;
    for (let x = startX; x < w; x += gridSize) {
        for (let y = startY; y < h; y += gridSize) {
            ctx.beginPath();
            ctx.arc(x, y, dotSize, 0, Math.PI * 2);
            ctx.fill();
        }
    }
}
function drawShape(shape, selected) {
    const sel = selected;
    switch (shape.type) {
        case 'rectangle':
            drawRectangle(shape, sel);
            break;
        case 'ellipse':
            drawEllipse(shape, sel);
            break;
        case 'line':
            drawLine(shape, sel);
            break;
        case 'arrow':
            drawArrow(shape, sel);
            break;
        case 'freehand':
            drawFreehand(shape, sel);
            break;
        case 'text':
            drawText(shape, sel);
            break;
    }
}
function drawRectangle(shape, selected) {
    const s = shape;
    ctx.save();
    if (s.fillColor && s.fillColor !== 'transparent') {
        ctx.fillStyle = s.fillColor;
        ctx.fillRect(s.x, s.y, s.width, s.height);
    }
    ctx.strokeStyle = s.strokeColor;
    ctx.lineWidth = s.strokeWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.strokeRect(s.x, s.y, s.width, s.height);
    if (selected) {
        ctx.strokeStyle = constants_1.COLORS.selection;
        ctx.lineWidth = 1.5;
        ctx.setLineDash([5, 3]);
        ctx.strokeRect(s.x - 2, s.y - 2, s.width + 4, s.height + 4);
        ctx.setLineDash([]);
    }
    ctx.restore();
}
function drawEllipse(shape, selected) {
    const s = shape;
    ctx.save();
    const cx = s.x + s.width / 2;
    const cy = s.y + s.height / 2;
    const rx = Math.abs(s.width) / 2;
    const ry = Math.abs(s.height) / 2;
    ctx.beginPath();
    ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
    if (s.fillColor && s.fillColor !== 'transparent') {
        ctx.fillStyle = s.fillColor;
        ctx.fill();
    }
    ctx.strokeStyle = s.strokeColor;
    ctx.lineWidth = s.strokeWidth;
    ctx.lineCap = 'round';
    ctx.stroke();
    if (selected) {
        ctx.strokeStyle = constants_1.COLORS.selection;
        ctx.lineWidth = 1.5;
        ctx.setLineDash([5, 3]);
        ctx.stroke();
        ctx.setLineDash([]);
    }
    ctx.restore();
}
function drawLine(shape, selected) {
    const s = shape;
    ctx.save();
    ctx.strokeStyle = s.strokeColor;
    ctx.lineWidth = s.strokeWidth;
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(s.x1, s.y1);
    ctx.lineTo(s.x2, s.y2);
    ctx.stroke();
    ctx.restore();
}
function drawArrow(shape, selected) {
    const s = shape;
    ctx.save();
    ctx.strokeStyle = s.strokeColor;
    ctx.fillStyle = s.strokeColor;
    ctx.lineWidth = s.strokeWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    // Line
    ctx.beginPath();
    ctx.moveTo(s.x1, s.y1);
    ctx.lineTo(s.x2, s.y2);
    ctx.stroke();
    // Arrowhead
    const angle = Math.atan2(s.y2 - s.y1, s.x2 - s.x1);
    const headLen = Math.max(12, s.strokeWidth * 4);
    const headAngle = Math.PI / 7;
    ctx.beginPath();
    ctx.moveTo(s.x2, s.y2);
    ctx.lineTo(s.x2 - headLen * Math.cos(angle - headAngle), s.y2 - headLen * Math.sin(angle - headAngle));
    ctx.lineTo(s.x2 - headLen * Math.cos(angle + headAngle), s.y2 - headLen * Math.sin(angle + headAngle));
    ctx.closePath();
    ctx.fill();
    ctx.restore();
}
function drawFreehand(shape, selected) {
    const s = shape;
    if (s.points.length < 2)
        return;
    ctx.save();
    ctx.strokeStyle = s.strokeColor;
    ctx.lineWidth = s.strokeWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    // Smooth the path using quadratic curves
    ctx.beginPath();
    ctx.moveTo(s.points[0][0], s.points[0][1]);
    if (s.points.length === 2) {
        ctx.lineTo(s.points[1][0], s.points[1][1]);
    }
    else {
        // Quadratic bezier through midpoints
        for (let i = 1; i < s.points.length - 1; i++) {
            const mx = (s.points[i][0] + s.points[i + 1][0]) / 2;
            const my = (s.points[i][1] + s.points[i + 1][1]) / 2;
            ctx.quadraticCurveTo(s.points[i][0], s.points[i][1], mx, my);
        }
        // Last point
        const last = s.points[s.points.length - 1];
        ctx.lineTo(last[0], last[1]);
    }
    ctx.stroke();
    ctx.restore();
}
function drawText(shape, selected) {
    const s = shape;
    ctx.save();
    ctx.font = `${s.fontSize}px "Inter", "Segoe UI", system-ui, sans-serif`;
    ctx.fillStyle = s.strokeColor;
    ctx.textBaseline = 'top';
    // Word wrap
    const maxWidth = s.width;
    const lines = wrapText(ctx, s.text || 'Type here...', maxWidth);
    const lineHeight = s.fontSize * 1.3;
    for (let i = 0; i < lines.length; i++) {
        ctx.fillText(lines[i], s.x + 4, s.y + 4 + i * lineHeight);
    }
    if (selected) {
        ctx.strokeStyle = constants_1.COLORS.selection;
        ctx.lineWidth = 1.5;
        ctx.setLineDash([5, 3]);
        ctx.strokeRect(s.x - 2, s.y - 2, s.width + 4, s.height + 4);
        ctx.setLineDash([]);
    }
    ctx.restore();
}
function wrapText(c, text, maxWidth) {
    if (!text)
        return [''];
    const words = text.split(' ');
    const lines = [];
    let currentLine = '';
    for (const word of words) {
        const testLine = currentLine ? currentLine + ' ' + word : word;
        const metrics = c.measureText(testLine);
        if (metrics.width > maxWidth && currentLine) {
            lines.push(currentLine);
            currentLine = word;
        }
        else {
            currentLine = testLine;
        }
    }
    if (currentLine)
        lines.push(currentLine);
    return lines.length ? lines : [''];
}
function drawSelectionOverlay(ids, vp) {
    if (!ids.length)
        return;
    const bounds = (0, scene_1.getSelectionBounds)(ids);
    if (!bounds)
        return;
    const [sx, sy] = (0, viewport_1.worldToScreen)(bounds.x, bounds.y, vp);
    const [sw, sh] = [bounds.w * vp.zoom, bounds.h * vp.zoom];
    const hs = constants_1.HANDLE_SIZE;
    // Selection box
    ctx.strokeStyle = constants_1.COLORS.selection;
    ctx.lineWidth = 1.5;
    ctx.setLineDash([5, 3]);
    ctx.strokeRect(sx - 2, sy - 2, sw + 4, sh + 4);
    ctx.setLineDash([]);
    // Handles
    const handles = [
        [sx, sy],
        [sx + sw, sy],
        [sx + sw, sy + sh],
        [sx, sy + sh],
    ];
    for (const [hx, hy] of handles) {
        ctx.fillStyle = constants_1.COLORS.handle;
        ctx.strokeStyle = constants_1.COLORS.handleStroke;
        ctx.lineWidth = 1.5;
        ctx.fillRect(hx - hs / 2, hy - hs / 2, hs, hs);
        ctx.strokeRect(hx - hs / 2, hy - hs / 2, hs, hs);
    }
}
function drawDragPreview(drag, vp) {
    ctx.save();
    ctx.translate(vp.x * vp.zoom, vp.y * vp.zoom);
    ctx.scale(vp.zoom, vp.zoom);
    ctx.strokeStyle = constants_1.COLORS.preview;
    ctx.lineWidth = 1.5;
    ctx.setLineDash([4, 3]);
    const dx = drag.startWorldX;
    const dy = drag.startWorldY;
    const endWorld = (0, viewport_1.screenToWorld)(drag._currentScreenX ?? drag.startX, drag._currentScreenY ?? drag.startY, vp);
    if (drag.type === 'create') {
        const w = endWorld[0] - dx;
        const h = endWorld[1] - dy;
        ctx.strokeRect(dx, dy, w, h);
    }
    else if (drag.type === 'draw' && drag.drawPoints && drag.drawPoints.length > 1) {
        ctx.strokeStyle = (0, state_1.getState)().defaultStyle.strokeColor;
        ctx.lineWidth = (0, state_1.getState)().defaultStyle.strokeWidth;
        ctx.setLineDash([]);
        ctx.lineCap = 'round';
        ctx.beginPath();
        ctx.moveTo(drag.drawPoints[0][0], drag.drawPoints[0][1]);
        for (let i = 1; i < drag.drawPoints.length; i++) {
            ctx.lineTo(drag.drawPoints[i][0], drag.drawPoints[i][1]);
        }
        ctx.stroke();
    }
    ctx.restore();
}
function drawRubberband(rect, vp) {
    const [sx, sy] = (0, viewport_1.worldToScreen)(rect.x, rect.y, vp);
    const sw = rect.w * vp.zoom;
    const sh = rect.h * vp.zoom;
    ctx.strokeStyle = constants_1.COLORS.selection;
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 3]);
    ctx.strokeRect(sx, sy, sw, sh);
    ctx.setLineDash([]);
    ctx.fillStyle = constants_1.COLORS.selectionFill;
    ctx.fillRect(sx, sy, sw, sh);
}
};

// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
// ── localStorage Persistence ──────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveToStorage = saveToStorage;
exports.loadFromStorage = loadFromStorage;
exports.clearStorage = clearStorage;
const constants_1 = require("./constants");
const scene_1 = require("./scene");
const state_1 = require("./state");
const history_1 = require("./history");
function saveToStorage() {
    try {
        const shapes = (0, scene_1.getShapes)();
        const data = JSON.stringify(shapes);
        localStorage.setItem(constants_1.STORAGE_KEY, data);
    }
    catch {
        // Storage full or unavailable — silently ignore
    }
}
function loadFromStorage() {
    try {
        const data = localStorage.getItem(constants_1.STORAGE_KEY);
        if (!data)
            return false;
        const shapes = JSON.parse(data);
        if (!Array.isArray(shapes))
            return false;
        // Clear and restore
        (0, state_1.setState)(s => { s.shapes = []; });
        for (const shape of shapes) {
            (0, scene_1.addShape)(shape);
        }
        return true;
    }
    catch {
        return false;
    }
}
function clearStorage() {
    localStorage.removeItem(constants_1.STORAGE_KEY);
    (0, state_1.setState)(s => { s.shapes = []; });
    (0, history_1.clearHistory)();
}
};

// ── module: src/tools.ts ──
__mods["src/tools.ts"] = function (exports, require, module) {
"use strict";
// ── Tool Logic ────────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.selectShapeAt = selectShapeAt;
exports.deselectAll = deselectAll;
exports.deleteSelection = deleteSelection;
exports.nudgeSelection = nudgeSelection;
exports.resizeSelected = resizeSelected;
exports.finishCreateShape = finishCreateShape;
exports.finishFreehand = finishFreehand;
exports.finishText = finishText;
exports.eraseAt = eraseAt;
exports.applyStyleToSelection = applyStyleToSelection;
const state_1 = require("./state");
const scene_1 = require("./scene");
const hitTest_1 = require("./hitTest");
const history_1 = require("./history");
const constants_1 = require("./constants");
const persistence_1 = require("./persistence");
// ─── Selection ────────────────────────────────────────────────────────────────
function selectShapeAt(wx, wy, addMode) {
    const hit = (0, hitTest_1.findHitShape)((0, scene_1.getShapes)(), wx, wy);
    if (hit) {
        (0, state_1.setState)(s => {
            if (addMode) {
                if (!s.selectedIds.includes(hit.id))
                    s.selectedIds.push(hit.id);
            }
            else
                s.selectedIds = [hit.id];
        });
        syncStyleFromSelection();
    }
    else if (!addMode) {
        (0, state_1.setSelectedIds)([]);
    }
}
function deselectAll() { (0, state_1.setSelectedIds)([]); }
function deleteSelection() {
    const s = (0, state_1.getState)();
    if (!s.selectedIds.length)
        return;
    (0, history_1.recordDelete)([...s.selectedIds]);
    (0, state_1.setSelectedIds)([]);
    (0, persistence_1.saveToStorage)();
}
function nudgeSelection(dx, dy) {
    const s = (0, state_1.getState)();
    if (!s.selectedIds.length)
        return;
    const ids = [...s.selectedIds];
    (0, history_1.beginOp)(ids, 'Nudge', 'move');
    for (const id of ids) {
        const sh = (0, scene_1.getShape)(id);
        if (!sh)
            continue;
        (0, scene_1.updateShape)(id, (shape) => {
            const n = { ...shape };
            switch (n.type) {
                case 'rectangle':
                case 'ellipse':
                case 'text':
                    n.x += dx;
                    n.y += dy;
                    break;
                case 'line':
                case 'arrow':
                    n.x1 += dx;
                    n.y1 += dy;
                    n.x2 += dx;
                    n.y2 += dy;
                    break;
                case 'freehand':
                    n.points = n.points.map((p) => [p[0] + dx, p[1] + dy]);
                    break;
            }
            return n;
        });
    }
    (0, history_1.commitOp)();
    (0, persistence_1.saveToStorage)();
}
function syncStyleFromSelection() {
    const s = (0, state_1.getState)();
    if (s.selectedIds.length === 1) {
        const sh = (0, scene_1.getShape)(s.selectedIds[0]);
        if (sh) {
            const style = (0, state_1.getDefaultStyle)();
            if ('strokeColor' in sh)
                style.strokeColor = sh.strokeColor;
            if ('fillColor' in sh)
                style.fillColor = sh.fillColor;
            if ('strokeWidth' in sh)
                style.strokeWidth = sh.strokeWidth;
            (0, state_1.setDefaultStyle)(style);
        }
    }
}
// ─── Resize ───────────────────────────────────────────────────────────────────
function resizeSelected(handle, toWorldX, toWorldY, shiftKey) {
    const s = (0, state_1.getState)();
    if (!s.selectedIds.length)
        return;
    const bounds = (0, scene_1.getSelectionBounds)(s.selectedIds);
    if (!bounds)
        return;
    const ids = [...s.selectedIds];
    let newW = bounds.w, newH = bounds.h, newX = bounds.x, newY = bounds.y;
    switch (handle) {
        case 0:
            newW = bounds.x + bounds.w - toWorldX;
            newH = bounds.y + bounds.h - toWorldY;
            newX = toWorldX;
            newY = toWorldY;
            break;
        case 1:
            newW = toWorldX - bounds.x;
            newH = bounds.y + bounds.h - toWorldY;
            newY = toWorldY;
            break;
        case 2:
            newW = toWorldX - bounds.x;
            newH = toWorldY - bounds.y;
            break;
        case 3:
            newW = bounds.x + bounds.w - toWorldX;
            newH = toWorldY - bounds.y;
            newX = toWorldX;
            break;
    }
    if (shiftKey) {
        const aspect = bounds.w / Math.max(bounds.h, 0.001);
        if (Math.abs(newW) > Math.abs(newH))
            newH = newW / aspect;
        else
            newW = newH * aspect;
    }
    const dx = newX - bounds.x, dy = newY - bounds.y, dw = newW - bounds.w, dh = newH - bounds.h;
    for (const id of ids) {
        const sh = (0, scene_1.getShape)(id);
        if (!sh)
            continue;
        const b = (0, scene_1.getShapeBounds)(sh);
        const scaleW = b.w > 0 ? dw / b.w : 0, scaleH = b.h > 0 ? dh / b.h : 0;
        (0, scene_1.updateShape)(id, (shape) => {
            const n = { ...shape };
            switch (n.type) {
                case 'rectangle':
                case 'ellipse':
                    n.x += dx;
                    n.y += dy;
                    n.width += dw;
                    n.height += dh;
                    break;
                case 'line':
                case 'arrow':
                    n.x1 += dx;
                    n.y1 += dy;
                    n.x2 += dx + dw;
                    n.y2 += dy + dh;
                    break;
                case 'freehand':
                    n.points = n.points.map((p) => [p[0] + dx + (p[0] - b.x) * scaleW, p[1] + dy + (p[1] - b.y) * scaleH]);
                    break;
                case 'text':
                    n.x += dx;
                    n.y += dy;
                    n.width = Math.max(50, n.width + dw);
                    n.height = Math.max(30, n.height + dh);
                    break;
            }
            return n;
        });
    }
}
// ─── Shape Creation ───────────────────────────────────────────────────────────
function finishCreateShape(tool, endX, endY, startX, startY) {
    const dx = endX - startX, dy = endY - startY;
    if (Math.abs(dx) < constants_1.MIN_SHAPE_SIZE && Math.abs(dy) < constants_1.MIN_SHAPE_SIZE)
        return;
    let shape;
    switch (tool) {
        case 'rectangle':
            shape = (0, scene_1.createRectangle)(startX, startY, dx, dy);
            break;
        case 'ellipse':
            shape = (0, scene_1.createEllipse)(startX, startY, dx, dy);
            break;
        case 'line':
            shape = (0, scene_1.createLine)(startX, startY, endX, endY, false);
            break;
        case 'arrow':
            shape = (0, scene_1.createLine)(startX, startY, endX, endY, true);
            break;
        default: return;
    }
    (0, scene_1.addShape)(shape);
    (0, history_1.recordCreate)(shape);
    (0, state_1.setSelectedIds)([shape.id]);
    (0, persistence_1.saveToStorage)();
}
function finishFreehand(points) {
    if (points.length < 2)
        return;
    const shape = (0, scene_1.createFreehand)(points);
    (0, scene_1.addShape)(shape);
    (0, history_1.recordCreate)(shape);
    (0, state_1.setSelectedIds)([shape.id]);
    (0, persistence_1.saveToStorage)();
}
function finishText(text, x, y) {
    if (!text.trim())
        return;
    const s = (0, state_1.getDefaultStyle)();
    const shape = (0, scene_1.createText)(x, y);
    shape.text = text;
    const c = document.createElement('canvas');
    const ctx = c.getContext('2d');
    ctx.font = `${s.fontSize ?? 20}px "Inter", "Segoe UI", system-ui, sans-serif`;
    const lines = text.split('\n');
    let maxW = 0;
    for (const line of lines) {
        const m = ctx.measureText(line);
        if (m.width > maxW)
            maxW = m.width;
    }
    shape.width = Math.max(100, maxW + 20);
    shape.height = Math.max(30, lines.length * (s.fontSize ?? 20) * 1.3 + 8);
    (0, scene_1.addShape)(shape);
    (0, history_1.recordCreate)(shape);
    (0, state_1.setSelectedIds)([shape.id]);
    (0, persistence_1.saveToStorage)();
}
// ─── Eraser ───────────────────────────────────────────────────────────────────
function eraseAt(wx, wy) {
    const hit = (0, hitTest_1.findHitShape)((0, scene_1.getShapes)(), wx, wy);
    if (hit) {
        (0, history_1.recordDelete)([hit.id]);
        (0, state_1.setState)(s => { s.selectedIds = s.selectedIds.filter(id => id !== hit.id); });
        (0, persistence_1.saveToStorage)();
    }
}
// ─── Style ────────────────────────────────────────────────────────────────────
function applyStyleToSelection(props) {
    const s = (0, state_1.getState)();
    if (!s.selectedIds.length) {
        (0, state_1.setDefaultStyle)(props);
        return;
    }
    const ids = [...s.selectedIds];
    (0, history_1.beginOp)(ids, 'Restyle', 'modify');
    for (const id of ids) {
        const sh = (0, scene_1.getShape)(id);
        if (!sh)
            continue;
        (0, scene_1.updateShape)(id, (shape) => {
            const n = { ...shape };
            if (props.strokeColor !== undefined && 'strokeColor' in n)
                n.strokeColor = props.strokeColor;
            if (props.fillColor !== undefined && 'fillColor' in n)
                n.fillColor = props.fillColor;
            if (props.strokeWidth !== undefined && 'strokeWidth' in n)
                n.strokeWidth = props.strokeWidth;
            return n;
        });
    }
    (0, history_1.commitOp)();
    (0, persistence_1.saveToStorage)();
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ── Input Handling ────────────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.setRenderCallback = setRenderCallback;
exports.setToolbarCallback = setToolbarCallback;
exports.setZoomDisplayCallback = setZoomDisplayCallback;
exports.setHistoryCallback = setHistoryCallback;
exports.resetView = resetView;
const state_1 = require("./state");
const viewport_1 = require("./viewport");
const scene_1 = require("./scene");
const hitTest_1 = require("./hitTest");
const tools_1 = require("./tools");
const history_1 = require("./history");
const constants_1 = require("./constants");
const persistence_1 = require("./persistence");
let canvas = null;
let textOverlay = null;
function initInput(c, ta) {
    canvas = c;
    textOverlay = ta;
    setupMouseEvents();
    setupKeyboardEvents();
}
function getCanvas() { return canvas; }
function getMouseWorldPos(e) {
    const r = getCanvas().getBoundingClientRect();
    return (0, viewport_1.screenToWorld)(e.clientX - r.left, e.clientY - r.top, (0, state_1.getState)().viewport);
}
function getMouseScreenPos(e) {
    const r = getCanvas().getBoundingClientRect();
    return [e.clientX - r.left, e.clientY - r.top];
}
function setupMouseEvents() {
    const c = getCanvas();
    c.addEventListener('mousedown', onMouseDown);
    c.addEventListener('mousemove', onMouseMove);
    c.addEventListener('mouseup', onMouseUp);
    c.addEventListener('mouseleave', onMouseLeave);
    c.addEventListener('wheel', onWheel, { passive: false });
    c.addEventListener('contextmenu', e => e.preventDefault());
}
function onMouseDown(e) {
    const s = (0, state_1.getState)();
    const [wx, wy] = getMouseWorldPos(e);
    const [sx, sy] = getMouseScreenPos(e);
    if (e.button === 1 || (e.button === 0 && s.spaceDown)) {
        e.preventDefault();
        (0, state_1.setState)(st => { st.isPanning = true; st.dragState = { type: 'pan', startX: e.clientX, startY: e.clientY, startWorldX: 0, startWorldY: 0 }; });
        return;
    }
    if (e.button !== 0)
        return;
    switch (s.currentTool) {
        case 'select':
            handleSelectDown(wx, wy, sx, sy, e);
            break;
        case 'pen':
            handlePenDown(wx, wy, sx, sy);
            break;
        case 'rectangle':
        case 'ellipse':
        case 'line':
        case 'arrow':
            handleCreateDown(s.currentTool, wx, wy, sx, sy);
            break;
        case 'text':
            handleTextDown(wx, wy, sx, sy);
            break;
        case 'eraser':
            handleEraserDown(wx, wy, sx, sy);
            break;
    }
}
function onMouseMove(e) {
    const s = (0, state_1.getState)();
    const [wx, wy] = getMouseWorldPos(e);
    const [sx, sy] = getMouseScreenPos(e);
    updateEraserCursor(sx, sy);
    if (!s.dragState)
        return;
    switch (s.dragState.type) {
        case 'pan':
            handlePanMove(e);
            break;
        case 'move':
            handleMoveMove(wx, wy);
            break;
        case 'resize':
            handleResizeMove(wx, wy, e);
            break;
        case 'create':
            handleCreateMove(sx, sy);
            break;
        case 'draw':
            handleDrawMove(wx, wy, sx, sy);
            break;
        case 'rubberband':
            handleRubberbandMove(wx, wy, sx, sy);
            break;
        case 'erase':
            handleEraserMove(wx, wy);
            break;
    }
}
function onMouseUp(e) {
    const s = (0, state_1.getState)();
    if (!s.dragState)
        return;
    const [wx, wy] = getMouseWorldPos(e);
    switch (s.dragState.type) {
        case 'pan':
            handlePanUp();
            break;
        case 'move':
            handleMoveUp();
            break;
        case 'resize':
            handleResizeUp();
            break;
        case 'create':
            handleCreateUp(s.currentTool, wx, wy);
            break;
        case 'draw':
            handleDrawUp();
            break;
        case 'rubberband':
            handleRubberbandUp();
            break;
        case 'erase':
            handleEraserUp();
            break;
    }
    (0, state_1.setState)(st => { st.dragState = null; });
}
function onMouseLeave(e) {
    const s = (0, state_1.getState)();
    if (s.dragState && s.dragState.type === 'pan')
        handlePanUp();
    (0, state_1.setState)(st => { st.dragState = null; });
}
function onWheel(e) {
    e.preventDefault();
    const [sx, sy] = getMouseScreenPos(e);
    const factor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
    (0, state_1.setViewport)((0, viewport_1.zoomAtScreen)(sx, sy, (0, state_1.getState)().viewport, factor));
    onRender?.();
    updateZoomDisplay?.();
}
// ─── Pan ──────────────────────────────────────────────────────────────────────
function handlePanMove(e) {
    const s = (0, state_1.getState)();
    (0, state_1.setViewport)((0, viewport_1.panScreen)(s.viewport, e.clientX - s.dragState.startX, e.clientY - s.dragState.startY));
    onRender?.();
}
function handlePanUp() { (0, state_1.setState)(s => { s.isPanning = false; }); }
// ─── Select Tool ──────────────────────────────────────────────────────────────
function handleSelectDown(wx, wy, sx, sy, e) {
    const s = (0, state_1.getState)();
    // Check resize handles
    if (s.selectedIds.length) {
        const bounds = (0, scene_1.getSelectionBounds)(s.selectedIds);
        if (bounds) {
            const bsx = (bounds.x + s.viewport.x) * s.viewport.zoom;
            const bsy = (bounds.y + s.viewport.y) * s.viewport.zoom;
            const bsw = bounds.w * s.viewport.zoom, bsh = bounds.h * s.viewport.zoom;
            const handle = (0, hitTest_1.hitTestHandle)(sx, sy, bsx, bsy, bsw, bsh, constants_1.HANDLE_SIZE);
            if (handle !== null) {
                const ids = [...s.selectedIds];
                (0, history_1.beginOp)(ids, 'Resize', 'resize');
                (0, state_1.setState)(st => {
                    st.dragState = {
                        type: 'resize', startX: sx, startY: sy,
                        startWorldX: bounds.x, startWorldY: bounds.y,
                        resizeHandle: handle, movedIds: ids,
                        movedFrom: new Map(ids.map(id => [id, (0, scene_1.getShape)(id)])),
                    };
                });
                return;
            }
        }
    }
    // Check shape click
    const hit = (0, hitTest_1.findHitShape)((0, scene_1.getShapes)(), wx, wy);
    if (hit) {
        if (e.shiftKey)
            (0, tools_1.selectShapeAt)(wx, wy, true);
        else
            (0, state_1.setSelectedIds)([hit.id]);
        const ids = [...(0, state_1.getState)().selectedIds];
        (0, history_1.beginOp)(ids, 'Move', 'move');
        (0, state_1.setState)(st => {
            st.dragState = {
                type: 'move', startX: sx, startY: sy,
                startWorldX: wx, startWorldY: wy,
                movedIds: ids,
                movedFrom: new Map(ids.map(id => [id, (0, scene_1.getShape)(id)])),
            };
        });
    }
    else {
        if (!e.shiftKey)
            (0, state_1.setSelectedIds)([]);
        (0, state_1.setState)(st => {
            st.dragState = {
                type: 'rubberband', startX: sx, startY: sy,
                startWorldX: wx, startWorldY: wy,
                rubberbandRect: { x: wx, y: wy, w: 0, h: 0 },
            };
        });
    }
}
function handleMoveMove(wx, wy) {
    const drag = (0, state_1.getState)().dragState;
    const dx = wx - drag.startWorldX, dy = wy - drag.startWorldY;
    for (const id of drag.movedIds) {
        const orig = drag.movedFrom.get(id);
        if (!orig)
            continue;
        (0, scene_1.updateShape)(id, (shape) => {
            const n = { ...shape };
            switch (orig.type) {
                case 'rectangle':
                case 'ellipse':
                case 'text':
                    n.x = orig.x + dx;
                    n.y = orig.y + dy;
                    break;
                case 'line':
                case 'arrow':
                    n.x1 = orig.x1 + dx;
                    n.y1 = orig.y1 + dy;
                    n.x2 = orig.x2 + dx;
                    n.y2 = orig.y2 + dy;
                    break;
                case 'freehand':
                    n.points = orig.points.map((p) => [p[0] + dx, p[1] + dy]);
                    break;
            }
            return n;
        });
    }
    onRender?.();
}
function handleMoveUp() { (0, history_1.commitOp)(); (0, persistence_1.saveToStorage)(); }
function handleResizeMove(wx, wy, e) {
    const drag = (0, state_1.getState)().dragState;
    (0, tools_1.resizeSelected)(drag.resizeHandle, wx, wy, e.shiftKey);
    onRender?.();
}
function handleResizeUp() { (0, history_1.commitOp)(); (0, persistence_1.saveToStorage)(); }
function handleRubberbandMove(wx, wy, sx, sy) {
    const drag = (0, state_1.getState)().dragState;
    (0, state_1.setState)(st => {
        st.dragState.rubberbandRect = { x: drag.startWorldX, y: drag.startWorldY, w: wx - drag.startWorldX, h: wy - drag.startWorldY };
        st.dragState._currentScreenX = sx;
        st.dragState._currentScreenY = sy;
    });
    onRender?.();
}
function handleRubberbandUp() {
    const drag = (0, state_1.getState)().dragState;
    if (!drag.rubberbandRect)
        return;
    (0, state_1.setSelectedIds)((0, hitTest_1.findShapesInRect)((0, scene_1.getShapes)(), drag.rubberbandRect.x, drag.rubberbandRect.y, drag.rubberbandRect.w, drag.rubberbandRect.h));
    onRender?.();
}
// ─── Create Tool ──────────────────────────────────────────────────────────────
function handleCreateDown(tool, wx, wy, sx, sy) {
    (0, state_1.setState)(st => { st.dragState = { type: 'create', startX: sx, startY: sy, startWorldX: wx, startWorldY: wy }; });
}
function handleCreateMove(sx, sy) {
    (0, state_1.setState)(st => { if (st.dragState) {
        st.dragState._currentScreenX = sx;
        st.dragState._currentScreenY = sy;
    } });
    onRender?.();
}
function handleCreateUp(tool, wx, wy) {
    const drag = (0, state_1.getState)().dragState;
    (0, tools_1.finishCreateShape)(tool, wx, wy, drag.startWorldX, drag.startWorldY);
    onRender?.();
}
// ─── Pen Tool ─────────────────────────────────────────────────────────────────
function handlePenDown(wx, wy, sx, sy) {
    (0, tools_1.deselectAll)();
    (0, state_1.setState)(st => { st.dragState = { type: 'draw', startX: sx, startY: sy, startWorldX: wx, startWorldY: wy, drawPoints: [[wx, wy]] }; });
}
function handleDrawMove(wx, wy, sx, sy) {
    const drag = (0, state_1.getState)().dragState;
    drag.drawPoints.push([wx, wy]);
    drag._currentScreenX = sx;
    drag._currentScreenY = sy;
    onRender?.();
}
function handleDrawUp() {
    const drag = (0, state_1.getState)().dragState;
    if (drag.drawPoints && drag.drawPoints.length >= 2)
        (0, tools_1.finishFreehand)(drag.drawPoints);
    onRender?.();
}
// ─── Text Tool ────────────────────────────────────────────────────────────────
function handleTextDown(wx, wy, sx, sy) {
    const ta = textOverlay;
    const vp = (0, state_1.getState)().viewport;
    ta.style.left = (wx + vp.x) * vp.zoom + 'px';
    ta.style.top = (wy + vp.y) * vp.zoom + 'px';
    ta.style.width = '200px';
    ta.style.fontSize = ((0, state_1.getDefaultStyle)().fontSize ?? 20) + 'px';
    ta.style.color = (0, state_1.getDefaultStyle)().strokeColor;
    ta.style.display = 'block';
    ta.value = '';
    ta.placeholder = 'Type here...';
    ta.focus();
    ta.onblur = () => {
        if (ta.value.trim())
            (0, tools_1.finishText)(ta.value, wx, wy);
        ta.style.display = 'none';
        ta.value = '';
        onRender?.();
    };
    ta.onkeydown = (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            ta.blur();
        }
        if (e.key === 'Escape') {
            ta.value = '';
            ta.blur();
        }
    };
}
// ─── Eraser Tool ──────────────────────────────────────────────────────────────
function handleEraserDown(wx, wy, sx, sy) {
    (0, tools_1.eraseAt)(wx, wy);
    (0, state_1.setState)(st => { st.dragState = { type: 'erase', startX: sx, startY: sy, startWorldX: wx, startWorldY: wy }; });
    onRender?.();
}
function handleEraserMove(wx, wy) { (0, tools_1.eraseAt)(wx, wy); onRender?.(); }
function handleEraserUp() { onRender?.(); }
// Eraser cursor indicator
function updateEraserCursor(sx, sy) {
    const el = document.getElementById('eraser-cursor');
    if (!el)
        return;
    const s = (0, state_1.getState)();
    if (s.currentTool === 'eraser') {
        el.style.display = 'block';
        el.style.left = sx + 'px';
        el.style.top = sy + 'px';
    }
    else {
        el.style.display = 'none';
    }
}
// ─── Keyboard Events ──────────────────────────────────────────────────────────
function setupKeyboardEvents() {
    document.addEventListener('keydown', onKeyDown);
    document.addEventListener('keyup', onKeyUp);
}
function onKeyDown(e) {
    const s = (0, state_1.getState)();
    if (e.code === 'Space' && !e.repeat) {
        e.preventDefault();
        (0, state_1.setState)(st => { st.spaceDown = true; });
        return;
    }
    if (textOverlay && textOverlay.style.display !== 'none' && textOverlay === document.activeElement)
        return;
    if (!e.ctrlKey && !e.metaKey) {
        switch (e.key.toLowerCase()) {
            case 'v':
                (0, state_1.setCurrentTool)('select');
                updateToolbar?.();
                break;
            case 'p':
                (0, state_1.setCurrentTool)('pen');
                updateToolbar?.();
                break;
            case 'r':
                (0, state_1.setCurrentTool)('rectangle');
                updateToolbar?.();
                break;
            case 'o':
                (0, state_1.setCurrentTool)('ellipse');
                updateToolbar?.();
                break;
            case 'l':
                (0, state_1.setCurrentTool)('line');
                updateToolbar?.();
                break;
            case 'a':
                (0, state_1.setCurrentTool)('arrow');
                updateToolbar?.();
                break;
            case 't':
                (0, state_1.setCurrentTool)('text');
                updateToolbar?.();
                break;
            case 'e':
                (0, state_1.setCurrentTool)('eraser');
                updateToolbar?.();
                break;
        }
    }
    if ((e.key === 'Delete' || e.key === 'Backspace') && s.selectedIds.length) {
        e.preventDefault();
        (0, tools_1.deleteSelection)();
        onRender?.();
    }
    if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z') {
        e.preventDefault();
        if (e.shiftKey)
            (0, history_1.redo)();
        else
            (0, history_1.undo)();
        onRender?.();
        updateHistoryButtons?.();
    }
    if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) && s.selectedIds.length) {
        e.preventDefault();
        const step = e.shiftKey ? constants_1.NUDGE_STEP_SHIFT : constants_1.NUDGE_STEP;
        let dx = 0, dy = 0;
        if (e.key === 'ArrowUp')
            dy = -step;
        if (e.key === 'ArrowDown')
            dy = step;
        if (e.key === 'ArrowLeft')
            dx = -step;
        if (e.key === 'ArrowRight')
            dx = step;
        (0, tools_1.nudgeSelection)(dx, dy);
        onRender?.();
    }
}
function onKeyUp(e) {
    if (e.code === 'Space')
        (0, state_1.setState)(st => { st.spaceDown = false; });
}
// ─── Callbacks ────────────────────────────────────────────────────────────────
let onRender = null;
function setRenderCallback(cb) { onRender = cb; }
let updateToolbar = null;
function setToolbarCallback(cb) { updateToolbar = cb; }
let updateZoomDisplay = null;
function setZoomDisplayCallback(cb) { updateZoomDisplay = cb; }
let updateHistoryButtons = null;
function setHistoryCallback(cb) { updateHistoryButtons = cb; }
function resetView() {
    (0, state_1.setViewport)((0, viewport_1.resetView)());
    onRender?.();
    updateZoomDisplay?.();
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ── UI Construction & Updates ─────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.setRenderTrigger = setRenderTrigger;
exports.buildUI = buildUI;
exports.updateToolbar = updateToolbar;
exports.updateHistoryButtons = updateHistoryButtons;
exports.updatePropBarVisibility = updatePropBarVisibility;
const constants_1 = require("./constants");
const state_1 = require("./state");
const history_1 = require("./history");
const viewport_1 = require("./viewport");
const tools_1 = require("./tools");
const TOOLS = [
    { id: 'select', icon: '◇', label: 'Select', shortcut: 'V' },
    { id: 'pen', icon: '✎', label: 'Pen', shortcut: 'P' },
    { id: 'rectangle', icon: '▭', label: 'Rectangle', shortcut: 'R' },
    { id: 'ellipse', icon: '◯', label: 'Ellipse', shortcut: 'O' },
    { id: 'line', icon: '╱', label: 'Line', shortcut: 'L' },
    { id: 'arrow', icon: '→', label: 'Arrow', shortcut: 'A' },
    { id: 'text', icon: 'T', label: 'Text', shortcut: 'T' },
    { id: 'eraser', icon: '◻', label: 'Eraser', shortcut: 'E' },
];
// Render trigger (set by main)
let _triggerRender = null;
function setRenderTrigger(cb) { _triggerRender = cb; }
function triggerRender() { _triggerRender?.(); }
function buildUI() {
    buildToolbar();
    buildPropBar();
    setupZoomButtons();
    setupHistoryButtons();
}
function buildToolbar() {
    const toolbar = document.getElementById('toolbar');
    toolbar.innerHTML = '';
    for (const tool of TOOLS) {
        const btn = document.createElement('button');
        btn.id = 'tool-' + tool.id;
        btn.title = `${tool.label} (${tool.shortcut})`;
        btn.innerHTML = `<span class="shortcut">${tool.shortcut}</span>${tool.icon}`;
        btn.addEventListener('click', () => {
            (0, state_1.setCurrentTool)(tool.id);
            updateToolbar();
        });
        toolbar.appendChild(btn);
    }
}
function updateToolbar() {
    const s = (0, state_1.getState)();
    for (const tool of TOOLS) {
        const btn = document.getElementById('tool-' + tool.id);
        if (btn) {
            btn.classList.toggle('active', s.currentTool === tool.id);
        }
    }
    // Update canvas cursor
    const canvas = document.getElementById('canvas');
    if (canvas) {
        canvas.className = 'tool-' + s.currentTool;
    }
}
function buildPropBar() {
    const propbar = document.getElementById('propbar');
    propbar.innerHTML = '';
    // Stroke color label
    const strokeLabel = document.createElement('label');
    strokeLabel.textContent = 'Stroke';
    propbar.appendChild(strokeLabel);
    // Stroke color swatches
    const strokeRow = document.createElement('div');
    strokeRow.className = 'color-row';
    strokeRow.id = 'stroke-colors';
    for (const color of constants_1.PALETTE) {
        const swatch = document.createElement('div');
        swatch.className = 'color-swatch';
        swatch.style.background = color;
        swatch.addEventListener('click', () => {
            (0, tools_1.applyStyleToSelection)({ strokeColor: color });
            (0, state_1.setDefaultStyle)({ strokeColor: color });
            triggerRender();
        });
        strokeRow.appendChild(swatch);
    }
    propbar.appendChild(strokeRow);
    // Divider
    const div1 = document.createElement('div');
    div1.className = 'divider';
    propbar.appendChild(div1);
    // Fill color label
    const fillLabel = document.createElement('label');
    fillLabel.textContent = 'Fill';
    propbar.appendChild(fillLabel);
    // Fill color swatches
    const fillRow = document.createElement('div');
    fillRow.className = 'color-row';
    fillRow.id = 'fill-colors';
    for (const color of constants_1.FILL_PALETTE) {
        const swatch = document.createElement('div');
        swatch.className = 'color-swatch';
        if (color === 'transparent') {
            swatch.style.background = 'linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%)';
            swatch.style.border = '2px solid #dee2e6';
        }
        else {
            swatch.style.background = color;
            swatch.style.border = '2px solid transparent';
        }
        swatch.addEventListener('click', () => {
            (0, tools_1.applyStyleToSelection)({ fillColor: color });
            (0, state_1.setDefaultStyle)({ fillColor: color });
            triggerRender();
        });
        fillRow.appendChild(swatch);
    }
    propbar.appendChild(fillRow);
    // Divider
    const div2 = document.createElement('div');
    div2.className = 'divider';
    propbar.appendChild(div2);
    // Stroke width buttons
    const widthLabel = document.createElement('label');
    widthLabel.textContent = 'Width';
    propbar.appendChild(widthLabel);
    const widthRow = document.createElement('div');
    widthRow.className = 'color-row';
    widthRow.id = 'stroke-widths';
    for (const w of [1, 2, 3, 5, 8]) {
        const btn = document.createElement('button');
        btn.className = 'width-btn';
        btn.innerHTML = `<span style="width:${Math.min(w * 3, 16)}px;height:${w}px;background:#495057;display:block;border-radius:1px;"></span>`;
        btn.addEventListener('click', () => {
            (0, tools_1.applyStyleToSelection)({ strokeWidth: w });
            (0, state_1.setDefaultStyle)({ strokeWidth: w });
            triggerRender();
        });
        widthRow.appendChild(btn);
    }
    propbar.appendChild(widthRow);
}
function setupZoomButtons() {
    const zoomIn = document.getElementById('zoom-in');
    const zoomOut = document.getElementById('zoom-out');
    const zoomReset = document.getElementById('zoom-reset');
    if (zoomIn) {
        zoomIn.addEventListener('click', () => {
            const canvas = document.getElementById('canvas');
            const cx = canvas.clientWidth / 2;
            const cy = canvas.clientHeight / 2;
            const newVp = (0, viewport_1.zoomAtScreen)(cx, cy, (0, state_1.getState)().viewport, 1.25);
            (0, state_1.setViewport)(newVp);
            triggerRender();
            updateZoomLevel();
        });
    }
    if (zoomOut) {
        zoomOut.addEventListener('click', () => {
            const canvas = document.getElementById('canvas');
            const cx = canvas.clientWidth / 2;
            const cy = canvas.clientHeight / 2;
            const newVp = (0, viewport_1.zoomAtScreen)(cx, cy, (0, state_1.getState)().viewport, 1 / 1.25);
            (0, state_1.setViewport)(newVp);
            triggerRender();
            updateZoomLevel();
        });
    }
    if (zoomReset) {
        zoomReset.addEventListener('click', () => {
            (0, state_1.setViewport)((0, viewport_1.resetView)());
            triggerRender();
            updateZoomLevel();
        });
    }
}
function updateZoomLevel() {
    const el = document.getElementById('zoom-level');
    if (el)
        el.textContent = Math.round((0, state_1.getState)().viewport.zoom * 100) + '%';
}
function setupHistoryButtons() {
    const undoBtn = document.getElementById('undo-btn');
    const redoBtn = document.getElementById('redo-btn');
    if (undoBtn) {
        undoBtn.addEventListener('click', () => {
            (0, history_1.undo)();
            triggerRender();
            updateHistoryButtons();
        });
    }
    if (redoBtn) {
        redoBtn.addEventListener('click', () => {
            (0, history_1.redo)();
            triggerRender();
            updateHistoryButtons();
        });
    }
}
function updateHistoryButtons() {
    const { length, index } = (0, history_1.getHistoryState)();
    const undoBtn = document.getElementById('undo-btn');
    const redoBtn = document.getElementById('redo-btn');
    if (undoBtn)
        undoBtn.disabled = index < 0;
    if (redoBtn)
        redoBtn.disabled = index >= length - 1;
}
// Show/hide prop bar based on selection
function updatePropBarVisibility() {
    const propbar = document.getElementById('propbar');
    if (!propbar)
        return;
    const hasSelection = (0, state_1.getState)().selectedIds.length > 0;
    propbar.classList.toggle('visible', hasSelection);
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"84b88095-bc8d-4f15-ac13-97c0b16e9855","tokensIn":4204090,"tokensOut":68711,"tokensTotal":4272801,"cost":0,"turns":72,"toolCalls":85,"failedToolCalls":5,"timestamp":"2026-08-01T17:05:28.498Z"} -->