← Whiteboard results

Whiteboard

ThinkingCap Qwen3.6 27B GGUF · 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 69,947 bytes · SHA-256 b765fafe3535
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Whiteboard</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f1f3; }
#app { width: 100%; height: 100%; position: relative; }
#canvas-container { width: 100%; height: 100%; overflow: hidden; }
#whiteboard { display: block; width: 100%; height: 100%; }

/* Toolbar */
#toolbar {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  background: #ffffff;
  border-radius: 10px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.05);
  padding: 4px 6px;
  z-index: 100;
  gap: 2px;
}

.tool-btn, #toolbar button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border: none;
  background: transparent;
  border-radius: 6px;
  cursor: pointer;
  color: #5f6368;
  transition: background 0.15s, color 0.15s;
}

.tool-btn:hover, #toolbar button:hover { background: #f1f3f4; color: #202124; }
.tool-btn.active { background: #e8f0fe; color: #1a73e8; }
.tool-btn:active, #toolbar button:active { background: #dadce0; }

.divider {
  width: 1px;
  height: 24px;
  background: #e0e0e0;
  margin: 0 4px;
}

/* Properties bar */
#properties-bar {
  position: absolute;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  background: #ffffff;
  border-radius: 10px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.05);
  padding: 6px 12px;
  z-index: 100;
  gap: 16px;
}

.prop-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
}

.prop-group label {
  font-size: 10px;
  font-weight: 600;
  color: #5f6368;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.color-palette {
  display: flex;
  gap: 3px;
}

.color-swatch {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  transition: border-color 0.15s, transform 0.1s;
}

.color-swatch:hover { transform: scale(1.15); }
.color-swatch.active { border-color: #1a73e8; }

#stroke-width {
  font-size: 12px;
  padding: 2px 6px;
  border: 1px solid #dee2e6;
  border-radius: 4px;
  background: #fff;
  color: #333;
  cursor: pointer;
}

/* Zoom control */
#zoom-control {
  position: absolute;
  bottom: 12px;
  right: 12px;
  display: flex;
  align-items: center;
  background: #ffffff;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.12);
  padding: 4px 6px;
  z-index: 100;
  gap: 4px;
}

#zoom-control button {
  width: 28px;
  height: 28px;
  border: none;
  background: transparent;
  border-radius: 4px;
  cursor: pointer;
  color: #5f6368;
  font-size: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 400;
}

#zoom-control button:hover { background: #f1f3f4; }
#zoom-level {
  font-size: 12px;
  color: #5f6368;
  min-width: 42px;
  text-align: center;
  user-select: none;
}
</style>
</head>
<body>
<div id="app">
  <div id="toolbar">
    <div class="tool-group">
      <button class="tool-btn active" data-tool="select" title="Select (V)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"></path><path d="M13 13l6 6"></path></svg>
      </button>
      <button class="tool-btn" data-tool="freehand" title="Pen (P)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 19l7-7 3 3-7 7-3-3z"></path><path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path><path d="M2 2l7.586 7.586"></path><circle cx="11" cy="11" r="2"></circle></svg>
      </button>
      <button class="tool-btn" data-tool="rectangle" title="Rectangle (R)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"></rect></svg>
      </button>
      <button class="tool-btn" data-tool="ellipse" title="Ellipse (O)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><ellipse cx="12" cy="12" rx="10" ry="8"></ellipse></svg>
      </button>
      <button class="tool-btn" data-tool="line" title="Line (L)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="19" x2="19" y2="5"></line></svg>
      </button>
      <button class="tool-btn" data-tool="text" title="Text (T)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7V4h16v3"></path><line x1="12" y1="4" x2="12" y2="20"></line><path d="M8 20h8"></path></svg>
      </button>
      <button class="tool-btn" data-tool="eraser" title="Eraser (E)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 20H7L3 16l9-9 8 8-4 4z"></path><path d="M6 11l8 8"></path></svg>
      </button>
    </div>
    <div class="divider"></div>
    <div class="tool-group">
      <button id="btn-undo" title="Undo (Ctrl+Z)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"></polyline><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path></svg>
      </button>
      <button id="btn-redo" title="Redo (Ctrl+Shift+Z)">
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.13-9.36L23 10"></path></svg>
      </button>
    </div>
  </div>
  <div id="properties-bar">
    <div class="prop-group">
      <label>Stroke</label>
      <div id="stroke-colors" class="color-palette"></div>
    </div>
    <div class="prop-group">
      <label>Fill</label>
      <div id="fill-colors" class="color-palette"></div>
    </div>
    <div class="prop-group">
      <label>Width</label>
      <select id="stroke-width"></select>
    </div>
  </div>
  <div id="canvas-container">
    <canvas id="whiteboard"></canvas>
  </div>
  <div id="zoom-control">
    <button id="btn-zoom-out">−</button>
    <span id="zoom-level">100%</span>
    <button id="btn-zoom-in">+</button>
    <button id="btn-reset-view" title="Reset view">⟲</button>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./interaction":"src/interaction.ts","./constants":"src/constants.ts"},"src/state.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/coords.ts":{"./types":"src/types.ts"},"src/hittest.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/render.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./hittest":"src/hittest.ts"},"src/interaction.ts":{"./types":"src/types.ts","./state":"src/state.ts","./coords":"src/coords.ts","./hittest":"src/hittest.ts","./render":"src/render.ts","./constants":"src/constants.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 — wire up the UI and start the whiteboard
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const state = __importStar(require("./state"));
const interaction = __importStar(require("./interaction"));
const constants_1 = require("./constants");
function main() {
    const canvas = document.getElementById('whiteboard');
    if (!canvas)
        return;
    // Load and init
    state.loadFromStorage();
    state.initHistory();
    // Start interaction
    interaction.init(canvas);
    interaction.updateToolIndicators();
    // --- Tool buttons ---
    document.querySelectorAll('.tool-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            const tool = btn.getAttribute('data-tool');
            if (tool) {
                state.setTool(tool);
                interaction.updateToolIndicators();
            }
        });
    });
    // --- Undo/Redo ---
    const btnUndo = document.getElementById('btn-undo');
    const btnRedo = document.getElementById('btn-redo');
    if (btnUndo)
        btnUndo.addEventListener('click', () => { state.undo(); });
    if (btnRedo)
        btnRedo.addEventListener('click', () => { state.redo(); });
    // --- Stroke colors ---
    const strokePalette = document.getElementById('stroke-colors');
    if (strokePalette) {
        for (const color of constants_1.PALETTE.strokes) {
            const swatch = document.createElement('div');
            swatch.className = 'color-swatch';
            swatch.style.background = color;
            if (color === '#f5f5f5')
                swatch.style.border = '2px solid #ccc';
            swatch.addEventListener('click', () => {
                state.setStyle({ strokeColor: color });
                updateSwatchActive(strokePalette, color);
                applyStyleToSelection();
            });
            strokePalette.appendChild(swatch);
        }
        updateSwatchActive(strokePalette, state.getStyle().strokeColor);
    }
    // --- Fill colors ---
    const fillPalette = document.getElementById('fill-colors');
    if (fillPalette) {
        for (const color of constants_1.PALETTE.fills) {
            const swatch = document.createElement('div');
            swatch.className = 'color-swatch';
            swatch.style.background = color;
            if (color === '#ffffff')
                swatch.style.border = '2px solid #ccc';
            swatch.addEventListener('click', () => {
                state.setStyle({ fillColor: color });
                updateSwatchActive(fillPalette, color);
                applyStyleToSelection();
            });
            fillPalette.appendChild(swatch);
        }
        updateSwatchActive(fillPalette, state.getStyle().fillColor);
    }
    // --- Stroke width ---
    const widthSelect = document.getElementById('stroke-width');
    if (widthSelect) {
        for (let w = constants_1.MIN_STROKE_WIDTH; w <= constants_1.MAX_STROKE_WIDTH; w += 1) {
            const opt = document.createElement('option');
            opt.value = String(w);
            opt.textContent = String(w);
            if (w === state.getStyle().strokeWidth)
                opt.selected = true;
            widthSelect.appendChild(opt);
        }
        widthSelect.addEventListener('change', () => {
            const w = parseInt(widthSelect.value, 10);
            state.setStyle({ strokeWidth: w });
            applyStyleToSelection();
        });
    }
    // --- Zoom controls ---
    const zoomLevel = document.getElementById('zoom-level');
    // Zoom render update
    const origFrame = window._wbFrame;
    function updateZoomDisplay() {
        if (zoomLevel) {
            const pct = Math.round(state.getViewport().zoom * 100);
            zoomLevel.textContent = pct + '%';
        }
    }
    setInterval(updateZoomDisplay, 500);
    updateZoomDisplay();
    const btnZoomIn = document.getElementById('btn-zoom-in');
    const btnZoomOut = document.getElementById('btn-zoom-out');
    const btnReset = document.getElementById('btn-reset-view');
    if (btnZoomIn)
        btnZoomIn.addEventListener('click', () => zoomBy(1.25));
    if (btnZoomOut)
        btnZoomOut.addEventListener('click', () => zoomBy(0.8));
    if (btnReset)
        btnReset.addEventListener('click', () => {
            state.setViewport({ offsetX: canvas.clientWidth / 2, offsetY: canvas.clientHeight / 2, zoom: 1 });
        });
    // Set initial viewport center
    state.setViewport({
        offsetX: canvas.clientWidth / 2,
        offsetY: canvas.clientHeight / 2,
        zoom: 1,
    });
}
function updateSwatchActive(container, activeColor) {
    container.querySelectorAll('.color-swatch').forEach(swatch => {
        const bg = swatch.style.background;
        swatch.classList.toggle('active', bg === activeColor);
    });
}
function applyStyleToSelection() {
    const style = state.getStyle();
    const shapes = state.getShapes();
    const ids = state.getSelectedIds();
    if (ids.length === 0)
        return;
    state.pushHistory();
    for (const s of shapes) {
        if (ids.includes(s.id)) {
            s.strokeColor = style.strokeColor;
            s.fillColor = style.fillColor;
            s.strokeWidth = style.strokeWidth;
        }
    }
    state.persist();
}
function zoomBy(factor) {
    const canvas = document.getElementById('whiteboard');
    const vp = state.getViewport();
    const cx = canvas.clientWidth / 2;
    const cy = canvas.clientHeight / 2;
    const wx = (cx - vp.offsetX) / vp.zoom;
    const wy = (cy - vp.offsetY) / vp.zoom;
    const newZoom = Math.max(0.25, Math.min(4, vp.zoom * factor));
    state.setViewport({
        offsetX: cx - wx * newZoom,
        offsetY: cy - wy * newZoom,
        zoom: newZoom,
    });
}
main();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// Tunable constants
Object.defineProperty(exports, "__esModule", { value: true });
exports.MAX_STROKE_WIDTH = exports.MIN_STROKE_WIDTH = exports.ARROW_HEAD_SIZE = exports.HANDLE_HIT_TOLERANCE = exports.HIT_TOLERANCE = exports.HANDLE_SIZE = exports.ZOOM_STEP = exports.ZOOM_MAX = exports.ZOOM_MIN = exports.PALETTE = exports.COLORS = void 0;
exports.COLORS = {
    bg: '#f8f9fa',
    grid: '#e2e5e9',
    chrome: '#ffffff',
    chromeBorder: '#dee2e6',
    toolbarShadow: 'rgba(0,0,0,0.12)',
    selected: '#4285f4',
    preview: '#9aa0a6',
    placeholder: '#adb5bd',
};
exports.PALETTE = {
    strokes: ['#1e1e1e', '#e74c3c', '#e67e22', '#27ae60', '#2980b9', '#8e44ad', '#34495e', '#f5f5f5'],
    fills: ['#ffffff', '#ffcccc', '#ffe0cc', '#ccffcc', '#cce5ff', '#f0ccff', '#ddd', '#cccccc'],
};
exports.ZOOM_MIN = 0.25;
exports.ZOOM_MAX = 4.0;
exports.ZOOM_STEP = 1.12;
exports.HANDLE_SIZE = 8;
exports.HIT_TOLERANCE = 8;
exports.HANDLE_HIT_TOLERANCE = 5;
exports.ARROW_HEAD_SIZE = 12;
exports.MIN_STROKE_WIDTH = 1;
exports.MAX_STROKE_WIDTH = 20;
};

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

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// Scene state management with undo/redo and localStorage persistence
Object.defineProperty(exports, "__esModule", { value: true });
exports.getShapes = getShapes;
exports.getSelectedIds = getSelectedIds;
exports.getTool = getTool;
exports.setTool = setTool;
exports.getViewport = getViewport;
exports.getStyle = getStyle;
exports.setViewport = setViewport;
exports.setStyle = setStyle;
exports.select = select;
exports.deselectAll = deselectAll;
exports.setSelection = setSelection;
exports.isSelected = isSelected;
exports.getSelected = getSelected;
exports.createShape = createShape;
exports.deleteSelected = deleteSelected;
exports.deleteShape = deleteShape;
exports.pushHistory = pushHistory;
exports.undo = undo;
exports.redo = redo;
exports.canUndo = canUndo;
exports.canRedo = canRedo;
exports.pushHistoryEntry = pushHistoryEntry;
exports.persist = persist;
exports.loadFromStorage = loadFromStorage;
exports.initHistory = initHistory;
const constants_1 = require("./constants");
const STORAGE_KEY = 'whiteboard-shapes';
let shapes = [];
let selectedIds = [];
let tool = 'select';
let viewport = { offsetX: 0, offsetY: 0, zoom: 1 };
let style = {
    strokeColor: constants_1.PALETTE.strokes[0],
    fillColor: constants_1.PALETTE.fills[0],
    strokeWidth: 2,
};
let history = [];
let historyIndex = -1;
let nextId = 1;
function genId() {
    return String(nextId++);
}
// --- Public API ---
function getShapes() { return shapes; }
function getSelectedIds() { return selectedIds; }
function getTool() { return tool; }
function setTool(t) { tool = t; }
function getViewport() { return { ...viewport }; }
function getStyle() { return { ...style }; }
function setViewport(vp) {
    viewport.offsetX = vp.offsetX;
    viewport.offsetY = vp.offsetY;
    viewport.zoom = Math.max(constants_1.ZOOM_MIN, Math.min(constants_1.ZOOM_MAX, vp.zoom));
}
function setStyle(s) {
    if (s.strokeColor !== undefined)
        style.strokeColor = s.strokeColor;
    if (s.fillColor !== undefined)
        style.fillColor = s.fillColor;
    if (s.strokeWidth !== undefined)
        style.strokeWidth = s.strokeWidth;
}
function select(id, add = false) {
    if (add) {
        if (!selectedIds.includes(id))
            selectedIds.push(id);
    }
    else {
        selectedIds = [id];
    }
}
function deselectAll() {
    selectedIds = [];
}
function setSelection(ids) {
    selectedIds = ids;
}
function isSelected(id) {
    return selectedIds.includes(id);
}
function getSelected() {
    return shapes.filter(s => selectedIds.includes(s.id));
}
// --- Shape operations ---
function createShape(partial) {
    const shape = {
        id: genId(),
        type: partial.type,
        strokeColor: style.strokeColor,
        fillColor: style.fillColor,
        strokeWidth: style.strokeWidth,
        opacity: 1,
        ...partial,
    };
    shapes.push(shape);
    return shape;
}
function deleteSelected() {
    if (selectedIds.length === 0)
        return;
    pushHistory();
    shapes = shapes.filter(s => !selectedIds.includes(s.id));
    selectedIds = [];
}
function deleteShape(id) {
    const exists = shapes.find(s => s.id === id);
    if (!exists)
        return;
    pushHistory();
    shapes = shapes.filter(s => s.id !== id);
    if (selectedIds.includes(id))
        selectedIds = selectedIds.filter(i => i !== id);
}
// --- Undo / Redo ---
function pushHistory() {
    history = history.slice(0, historyIndex + 1);
    history.push({
        shapesBefore: JSON.parse(JSON.stringify(shapes)),
        shapesAfter: JSON.parse(JSON.stringify(shapes)),
    });
    historyIndex++;
    if (history.length > 100) {
        history.shift();
        historyIndex--;
    }
}
function undo() {
    if (historyIndex < 0)
        return;
    history[historyIndex].shapesAfter = JSON.parse(JSON.stringify(shapes));
    shapes = JSON.parse(JSON.stringify(history[historyIndex].shapesBefore));
    historyIndex--;
    selectedIds = [];
    persist();
}
function redo() {
    if (historyIndex >= history.length - 1)
        return;
    historyIndex++;
    shapes = JSON.parse(JSON.stringify(history[historyIndex].shapesAfter));
    selectedIds = [];
    persist();
}
function canUndo() { return historyIndex >= 0; }
function canRedo() { return historyIndex < history.length - 1; }
// Push a pre-recorded entry (for multi-step ops like drag)
function pushHistoryEntry(entry) {
    history = history.slice(0, historyIndex + 1);
    history.push(entry);
    historyIndex++;
    if (history.length > 100) {
        history.shift();
        historyIndex--;
    }
}
// --- Persistence ---
function persist() {
    try {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(shapes));
    }
    catch (_) { /* quota exceeded */ }
}
function loadFromStorage() {
    try {
        const data = localStorage.getItem(STORAGE_KEY);
        if (data) {
            shapes = JSON.parse(data);
            let maxId = 0;
            for (const s of shapes) {
                const n = parseInt(s.id, 10);
                if (n > maxId)
                    maxId = n;
            }
            nextId = maxId + 1;
        }
    }
    catch (_) { /* corrupted data */ }
}
function initHistory() {
    history = [{
            shapesBefore: JSON.parse(JSON.stringify(shapes)),
            shapesAfter: JSON.parse(JSON.stringify(shapes)),
        }];
    historyIndex = 0;
}
};

// ── module: src/coords.ts ──
__mods["src/coords.ts"] = function (exports, require, module) {
"use strict";
// Coordinate transforms between screen and world
Object.defineProperty(exports, "__esModule", { value: true });
exports.screenToWorld = screenToWorld;
exports.worldToScreen = worldToScreen;
exports.zoomAtPoint = zoomAtPoint;
function screenToWorld(px, py, vp) {
    return {
        x: (px - vp.offsetX) / vp.zoom,
        y: (py - vp.offsetY) / vp.zoom,
    };
}
function worldToScreen(wx, wy, vp) {
    return {
        x: wx * vp.zoom + vp.offsetX,
        y: wy * vp.zoom + vp.offsetY,
    };
}
function zoomAtPoint(newZoom, screenX, screenY, vp) {
    const world = screenToWorld(screenX, screenY, vp);
    return {
        offsetX: screenX - world.x * newZoom,
        offsetY: screenY - world.y * newZoom,
        zoom: newZoom,
    };
}
};

// ── module: src/hittest.ts ──
__mods["src/hittest.ts"] = function (exports, require, module) {
"use strict";
// Hit testing — all in world coordinates
Object.defineProperty(exports, "__esModule", { value: true });
exports.hitTest = hitTest;
exports.getBounds = getBounds;
exports.getCombinedBounds = getCombinedBounds;
exports.hitHandle = hitHandle;
const constants_1 = require("./constants");
// Check if a point is near a line segment (within tolerance)
function distPointToSegment(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));
    return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
}
// Check if point is inside a rectangle
function hitRectangle(shape, wx, wy) {
    if (shape.x === undefined || shape.y === undefined || shape.width === undefined || shape.height === undefined)
        return false;
    const halfW = shape.width / 2, halfH = shape.height / 2;
    const left = shape.x - halfW, right = shape.x + halfW;
    const top = shape.y - halfH, bottom = shape.y + halfH;
    // Check interior
    if (wx >= left && wx <= right && wy >= top && wy <= bottom)
        return true;
    // Check stroke border
    const tol = (shape.strokeWidth || 0) / 2 + constants_1.HIT_TOLERANCE;
    return (wx >= left - tol && wx <= right + tol && wy >= top - tol && wy <= bottom + tol &&
        (wx < left || wx > right || wy < top || wy > bottom));
}
// Check if point is inside an ellipse
function hitEllipse(shape, wx, wy) {
    if (shape.cx === undefined || shape.cy === undefined || shape.rx === undefined || shape.ry === undefined)
        return false;
    const dx = (wx - shape.cx) / (shape.rx + (shape.strokeWidth || 0) / 2 + constants_1.HIT_TOLERANCE / 2);
    const dy = (wy - shape.cy) / (shape.ry + (shape.strokeWidth || 0) / 2 + constants_1.HIT_TOLERANCE / 2);
    return dx * dx + dy * dy <= 1;
}
// Check if point is near a polyline
function hitFreehand(shape, wx, wy) {
    if (!shape.points || shape.points.length < 2)
        return false;
    const tol = (shape.strokeWidth || 0) / 2 + constants_1.HIT_TOLERANCE;
    for (let i = 0; i < shape.points.length - 1; i++) {
        if (distPointToSegment(wx, wy, shape.points[i].x, shape.points[i].y, shape.points[i + 1].x, shape.points[i + 1].y) <= tol) {
            return true;
        }
    }
    // Also check distance to each point
    for (const p of shape.points) {
        if (Math.hypot(wx - p.x, wy - p.y) <= tol)
            return true;
    }
    return false;
}
// Check if point is near a line/arrow
function hitLine(shape, wx, wy) {
    if (shape.x1 === undefined || shape.y1 === undefined || shape.x2 === undefined || shape.y2 === undefined)
        return false;
    const tol = (shape.strokeWidth || 0) / 2 + constants_1.HIT_TOLERANCE;
    return distPointToSegment(wx, wy, shape.x1, shape.y1, shape.x2, shape.y2) <= tol;
}
// Check if point is near text
function hitText(shape, wx, wy, ctx) {
    if (shape.x === undefined || shape.y === undefined)
        return false;
    const fontSize = shape.fontSize || 16;
    ctx.font = `${fontSize}px sans-serif`;
    const lines = (shape.text || '').split('\n');
    const lineHeight = fontSize * 1.2;
    let maxWidth = 0;
    for (const line of lines) {
        const w = ctx.measureText(line).width;
        if (w > maxWidth)
            maxWidth = w;
    }
    const totalH = lines.length * lineHeight;
    const padding = 4;
    const left = shape.x - padding;
    const top = shape.y - fontSize - padding;
    return wx >= left && wx <= left + maxWidth + padding * 2 && wy >= top && wy <= top + totalH + padding * 2;
}
function hitTest(shape, wx, wy, ctx) {
    switch (shape.type) {
        case 'rectangle': return hitRectangle(shape, wx, wy);
        case 'ellipse': return hitEllipse(shape, wx, wy);
        case 'freehand': return hitFreehand(shape, wx, wy);
        case 'line':
        case 'arrow': return hitLine(shape, wx, wy);
        case 'text': return hitText(shape, wx, wy, ctx);
        default: return false;
    }
}
// Get bounding box in world coordinates
function getBounds(shape) {
    switch (shape.type) {
        case 'rectangle':
            if (shape.x !== undefined && shape.y !== undefined && shape.width !== undefined && shape.height !== undefined) {
                return { x: shape.x - shape.width / 2, y: shape.y - shape.height / 2, width: shape.width, height: shape.height };
            }
            break;
        case 'ellipse':
            if (shape.cx !== undefined && shape.cy !== undefined && shape.rx !== undefined && shape.ry !== undefined) {
                return { x: shape.cx - shape.rx, y: shape.cy - shape.ry, width: shape.rx * 2, height: shape.ry * 2 };
            }
            break;
        case 'freehand':
            if (shape.points && shape.points.length > 0) {
                let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
                for (const p of shape.points) {
                    if (p.x < minX)
                        minX = p.x;
                    if (p.y < minY)
                        minY = p.y;
                    if (p.x > maxX)
                        maxX = p.x;
                    if (p.y > maxY)
                        maxY = p.y;
                }
                return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
            }
            break;
        case 'line':
        case 'arrow':
            if (shape.x1 !== undefined && shape.y1 !== undefined && shape.x2 !== undefined && shape.y2 !== undefined) {
                return {
                    x: Math.min(shape.x1, shape.x2),
                    y: Math.min(shape.y1, shape.y2),
                    width: Math.abs(shape.x2 - shape.x1),
                    height: Math.abs(shape.y2 - shape.y1),
                };
            }
            break;
        case 'text':
            if (shape.x !== undefined && shape.y !== undefined) {
                return { x: shape.x, y: shape.y - (shape.fontSize || 16), width: 100, height: shape.fontSize || 16 };
            }
            break;
    }
    return { x: 0, y: 0, width: 0, height: 0 };
}
// Combined bounding box for multiple shapes
function getCombinedBounds(shapes) {
    if (shapes.length === 0)
        return { x: 0, y: 0, width: 0, height: 0 };
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const s of shapes) {
        const b = getBounds(s);
        if (b.x < minX)
            minX = b.x;
        if (b.y < minY)
            minY = b.y;
        if (b.x + b.width > maxX)
            maxX = b.x + b.width;
        if (b.y + b.height > maxY)
            maxY = b.y + b.height;
    }
    return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}
// Check if a world point is near a handle position
function hitHandle(handleWorldPos, testWorldPos, zoom) {
    const threshold = (constants_1.HANDLE_SIZE + constants_1.HANDLE_SIZE) / (2 * zoom);
    return Math.abs(handleWorldPos.x - testWorldPos.x) <= threshold &&
        Math.abs(handleWorldPos.y - testWorldPos.y) <= threshold;
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// Canvas rendering — all shapes, grid, selection, handles
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
const constants_1 = require("./constants");
const hittest_1 = require("./hittest");
function drawGrid(ctx, w, h, vp) {
    if (vp.zoom < 0.3)
        return; // too zoomed out
    const alpha = Math.min(1, (vp.zoom - 0.3) * 2);
    ctx.save();
    ctx.globalAlpha = alpha;
    ctx.fillStyle = constants_1.COLORS.grid;
    const dotSpacing = 24;
    // World-space positions visible in viewport
    const topLeft = { x: -vp.offsetX / vp.zoom, y: -vp.offsetY / vp.zoom };
    const botRight = {
        x: (w - vp.offsetX) / vp.zoom,
        y: (h - vp.offsetY) / vp.zoom,
    };
    const startX = Math.floor(topLeft.x / dotSpacing) * dotSpacing;
    const startY = Math.floor(topLeft.y / dotSpacing) * dotSpacing;
    for (let wx = startX; wx < botRight.x; wx += dotSpacing) {
        for (let wy = startY; wy < botRight.y; wy += dotSpacing) {
            const sx = wx * vp.zoom + vp.offsetX;
            const sy = wy * vp.zoom + vp.offsetY;
            ctx.beginPath();
            ctx.arc(sx, sy, 1.2, 0, Math.PI * 2);
            ctx.fill();
        }
    }
    ctx.restore();
}
function drawFreehand(ctx, shape) {
    if (!shape.points || shape.points.length < 2)
        return;
    ctx.save();
    ctx.globalAlpha = shape.opacity;
    ctx.strokeStyle = shape.strokeColor;
    ctx.lineWidth = shape.strokeWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.beginPath();
    ctx.moveTo(shape.points[0].x, shape.points[0].y);
    // Simple smoothing: use quadratic curves between midpoint pairs
    if (shape.points.length === 2) {
        ctx.lineTo(shape.points[1].x, shape.points[1].y);
    }
    else {
        for (let i = 1; i < shape.points.length - 1; i++) {
            const mx = (shape.points[i].x + shape.points[i + 1].x) / 2;
            const my = (shape.points[i].y + shape.points[i + 1].y) / 2;
            ctx.quadraticCurveTo(shape.points[i].x, shape.points[i].y, mx, my);
        }
        const last = shape.points[shape.points.length - 1];
        ctx.lineTo(last.x, last.y);
    }
    ctx.stroke();
    ctx.restore();
}
function drawRectangle(ctx, shape) {
    if (shape.x === undefined || shape.y === undefined || shape.width === undefined || shape.height === undefined)
        return;
    ctx.save();
    ctx.globalAlpha = shape.opacity;
    const x = shape.x - shape.width / 2;
    const y = shape.y - shape.height / 2;
    if (shape.fillColor && shape.fillColor !== '#ffffff') {
        ctx.fillStyle = shape.fillColor;
        ctx.fillRect(x, y, shape.width, shape.height);
    }
    ctx.strokeStyle = shape.strokeColor;
    ctx.lineWidth = shape.strokeWidth;
    ctx.strokeRect(x, y, shape.width, shape.height);
    ctx.restore();
}
function drawEllipse(ctx, shape) {
    if (shape.cx === undefined || shape.cy === undefined || shape.rx === undefined || shape.ry === undefined)
        return;
    ctx.save();
    ctx.globalAlpha = shape.opacity;
    ctx.beginPath();
    ctx.ellipse(shape.cx, shape.cy, shape.rx, shape.ry, 0, 0, Math.PI * 2);
    if (shape.fillColor && shape.fillColor !== '#ffffff') {
        ctx.fillStyle = shape.fillColor;
        ctx.fill();
    }
    ctx.strokeStyle = shape.strokeColor;
    ctx.lineWidth = shape.strokeWidth;
    ctx.stroke();
    ctx.restore();
}
function drawLine(ctx, shape) {
    if (shape.x1 === undefined || shape.y1 === undefined || shape.x2 === undefined || shape.y2 === undefined)
        return;
    ctx.save();
    ctx.globalAlpha = shape.opacity;
    ctx.strokeStyle = shape.strokeColor;
    ctx.lineWidth = shape.strokeWidth;
    ctx.lineCap = 'round';
    ctx.beginPath();
    ctx.moveTo(shape.x1, shape.y1);
    ctx.lineTo(shape.x2, shape.y2);
    ctx.stroke();
    // Arrow head
    if (shape.type === 'arrow') {
        const angle = Math.atan2(shape.y2 - shape.y1, shape.x2 - shape.x1);
        const headLen = constants_1.ARROW_HEAD_SIZE;
        ctx.fillStyle = shape.strokeColor;
        ctx.beginPath();
        ctx.moveTo(shape.x2, shape.y2);
        ctx.lineTo(shape.x2 - headLen * Math.cos(angle - Math.PI / 6), shape.y2 - headLen * Math.sin(angle - Math.PI / 6));
        ctx.lineTo(shape.x2 - headLen * Math.cos(angle + Math.PI / 6), shape.y2 - headLen * Math.sin(angle + Math.PI / 6));
        ctx.closePath();
        ctx.fill();
    }
    ctx.restore();
}
function drawText(ctx, shape) {
    if (shape.x === undefined || shape.y === undefined)
        return;
    ctx.save();
    ctx.globalAlpha = shape.opacity;
    const fontSize = shape.fontSize || 16;
    ctx.font = `${fontSize}px sans-serif`;
    ctx.fillStyle = shape.strokeColor;
    const lines = (shape.text || '').split('\n');
    const lineHeight = fontSize * 1.2;
    ctx.textBaseline = 'top';
    for (let i = 0; i < lines.length; i++) {
        ctx.fillText(lines[i], shape.x, shape.y + i * lineHeight);
    }
    ctx.restore();
}
function drawShape(ctx, shape) {
    switch (shape.type) {
        case 'freehand':
            drawFreehand(ctx, shape);
            break;
        case 'rectangle':
            drawRectangle(ctx, shape);
            break;
        case 'ellipse':
            drawEllipse(ctx, shape);
            break;
        case 'line':
        case 'arrow':
            drawLine(ctx, shape);
            break;
        case 'text':
            drawText(ctx, shape);
            break;
    }
}
function drawSelectionBox(ctx, bounds) {
    ctx.save();
    ctx.strokeStyle = constants_1.COLORS.selected;
    ctx.lineWidth = 1.5;
    ctx.setLineDash([5, 3]);
    ctx.strokeRect(bounds.x, bounds.y, bounds.width, bounds.height);
    ctx.setLineDash([]);
    ctx.restore();
}
function drawHandles(ctx, bounds) {
    const handles = [
        { x: bounds.x, y: bounds.y }, // top-left
        { x: bounds.x + bounds.width, y: bounds.y }, // top-right
        { x: bounds.x, y: bounds.y + bounds.height }, // bottom-left
        { x: bounds.x + bounds.width, y: bounds.y + bounds.height }, // bottom-right
    ];
    ctx.save();
    ctx.fillStyle = '#fff';
    ctx.strokeStyle = constants_1.COLORS.selected;
    ctx.lineWidth = 2;
    for (const h of handles) {
        ctx.fillRect(h.x - constants_1.HANDLE_SIZE / 2, h.y - constants_1.HANDLE_SIZE / 2, constants_1.HANDLE_SIZE, constants_1.HANDLE_SIZE);
        ctx.strokeRect(h.x - constants_1.HANDLE_SIZE / 2, h.y - constants_1.HANDLE_SIZE / 2, constants_1.HANDLE_SIZE, constants_1.HANDLE_SIZE);
    }
    ctx.restore();
}
function drawRubberBand(ctx, x1, y1, x2, y2) {
    ctx.save();
    ctx.strokeStyle = constants_1.COLORS.selected;
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 3]);
    ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
    ctx.fillStyle = 'rgba(66,133,244,0.08)';
    ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
    ctx.setLineDash([]);
    ctx.restore();
}
// --- Main render ---
function render(ctx, shapes, vp, selectedIds, rubberBand, w, h) {
    ctx.save();
    ctx.clearRect(0, 0, w, h);
    // Background
    ctx.fillStyle = constants_1.COLORS.bg;
    ctx.fillRect(0, 0, w, h);
    // Transform to world
    ctx.translate(vp.offsetX, vp.offsetY);
    ctx.scale(vp.zoom, vp.zoom);
    // Grid in world
    drawGrid(ctx, w, h, vp);
    // All shapes
    for (const shape of shapes) {
        drawShape(ctx, shape);
    }
    // Selection indicators (in world coords)
    if (selectedIds.length > 0) {
        const selected = shapes.filter(s => selectedIds.includes(s.id));
        if (selected.length > 0) {
            const bounds = (0, hittest_1.getCombinedBounds)(selected);
            drawSelectionBox(ctx, bounds);
            drawHandles(ctx, bounds);
        }
    }
    ctx.restore();
    // Rubber band (in screen coords, not transformed)
    if (rubberBand) {
        const x1 = Math.min(rubberBand.x1, rubberBand.x2);
        const y1 = Math.min(rubberBand.y1, rubberBand.y2);
        const x2 = Math.max(rubberBand.x1, rubberBand.x2);
        const y2 = Math.max(rubberBand.y1, rubberBand.y2);
        drawRubberBand(ctx, x1, y1, x2, y2);
    }
}
};

// ── module: src/interaction.ts ──
__mods["src/interaction.ts"] = function (exports, require, module) {
"use strict";
// Interaction handler — all mouse/keyboard logic, tool behaviors
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.init = init;
exports.updateToolIndicators = updateToolIndicators;
exports.finalizeCreate = finalizeCreate;
const state = __importStar(require("./state"));
const coords_1 = require("./coords");
const hittest_1 = require("./hittest");
const render_1 = require("./render");
const constants_1 = require("./constants");
let canvas;
let ctx;
let dpr = 1;
// Interaction state
let isPanning = false;
let isDrawing = false;
let isShapeDrag = false;
let isResizing = false;
let isRubberBand = false;
let isErasing = false;
let spaceDown = false;
let lastMouseScreen = { x: 0, y: 0 };
let lastMouseWorld = { x: 0, y: 0 };
let drawPoints = [];
let shapeStart = { x: 0, y: 0 };
let rubberBandStart = null;
let resizeHandle = -1;
let shiftActive = false;
// History snapshots for drag/resize
let dragOrigShapes = [];
let dragStartWorld = { x: 0, y: 0 };
let resizeOrigBounds = { x: 0, y: 0, width: 0, height: 0 };
let resizeStartWorld = { x: 0, y: 0 };
let eraseEntry = null;
// Text editing
let textEditing = false;
function init(el) {
    canvas = el;
    ctx = canvas.getContext('2d');
    dpr = window.devicePixelRatio || 1;
    resize();
    window.addEventListener('resize', resize);
    canvas.addEventListener('mousedown', onMouseDown);
    canvas.addEventListener('mousemove', onMouseMove);
    canvas.addEventListener('mouseup', onMouseUp);
    canvas.addEventListener('mouseleave', onMouseUp);
    canvas.addEventListener('wheel', onWheel, { passive: false });
    canvas.addEventListener('contextmenu', e => e.preventDefault());
    window.addEventListener('keydown', onKeyDown);
    window.addEventListener('keyup', onKeyUp);
    // Start render loop
    requestAnimationFrame(frame);
}
function resize() {
    dpr = window.devicePixelRatio || 1;
    canvas.width = canvas.clientWidth * dpr;
    canvas.height = canvas.clientHeight * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
// --- Render loop ---
function frame() {
    const w = canvas.clientWidth;
    const h = canvas.clientHeight;
    const vp = state.getViewport();
    const shapes = state.getShapes();
    const selectedIds = state.getSelectedIds();
    let rubberBand = null;
    if (isRubberBand && rubberBandStart) {
        rubberBand = {
            x1: rubberBandStart.x, y1: rubberBandStart.y,
            x2: lastMouseScreen.x, y2: lastMouseScreen.y,
        };
    }
    // Draw preview shape by temporarily adding it
    const preview = getPreviewShape();
    const renderShapes = preview ? [...shapes, preview] : shapes;
    (0, render_1.render)(ctx, renderShapes, vp, selectedIds, rubberBand, w, h);
    // Draw freehand preview
    if (isDrawing && drawPoints.length > 0) {
        ctx.save();
        ctx.translate(vp.offsetX, vp.offsetY);
        ctx.scale(vp.zoom, vp.zoom);
        ctx.strokeStyle = state.getStyle().strokeColor;
        ctx.lineWidth = state.getStyle().strokeWidth;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';
        ctx.globalAlpha = 0.7;
        ctx.beginPath();
        ctx.moveTo(drawPoints[0].x, drawPoints[0].y);
        for (let i = 1; i < drawPoints.length - 1; i++) {
            const mx = (drawPoints[i].x + drawPoints[i + 1].x) / 2;
            const my = (drawPoints[i].y + drawPoints[i + 1].y) / 2;
            ctx.quadraticCurveTo(drawPoints[i].x, drawPoints[i].y, mx, my);
        }
        const last = drawPoints[drawPoints.length - 1];
        if (drawPoints.length > 2) {
            const prev = drawPoints[drawPoints.length - 2];
            const mx = (prev.x + last.x) / 2;
            const my = (prev.y + last.y) / 2;
            ctx.quadraticCurveTo(prev.x, prev.y, mx, my);
        }
        ctx.lineTo(last.x, last.y);
        ctx.stroke();
        ctx.restore();
    }
    requestAnimationFrame(frame);
}
// --- Mouse handlers ---
function onMouseDown(e) {
    const rect = canvas.getBoundingClientRect();
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    const vp = state.getViewport();
    const wx = (sx - vp.offsetX) / vp.zoom;
    const wy = (sy - vp.offsetY) / vp.zoom;
    shiftActive = e.shiftKey;
    lastMouseScreen = { x: sx, y: sy };
    lastMouseWorld = { x: wx, y: wy };
    const tool = state.getTool();
    // Middle mouse or Space+drag = pan
    if (e.button === 1 || (e.button === 0 && spaceDown)) {
        isPanning = true;
        canvas.style.cursor = 'grabbing';
        e.preventDefault();
        return;
    }
    if (e.button !== 0)
        return;
    switch (tool) {
        case 'select':
            handleSelectDown(sx, sy, wx, wy);
            break;
        case 'eraser':
            handleEraserDown(wx, wy);
            break;
        case 'text':
            handleTextDown(wx, wy);
            break;
        default:
            handleCreateDown(sx, sy, wx, wy);
            break;
    }
}
function onMouseMove(e) {
    const rect = canvas.getBoundingClientRect();
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    const vp = state.getViewport();
    const wx = (sx - vp.offsetX) / vp.zoom;
    const wy = (sy - vp.offsetY) / vp.zoom;
    lastMouseScreen = { x: sx, y: sy };
    lastMouseWorld = { x: wx, y: wy };
    if (isPanning) {
        // lastMouseScreen still has the previous frame value at this point
        const dx = sx - lastMouseScreen.x;
        const dy = sy - lastMouseScreen.y;
        state.setViewport({ ...vp, offsetX: vp.offsetX + dx, offsetY: vp.offsetY + dy });
        lastMouseScreen = { x: sx, y: sy };
        return;
    }
    if (isDrawing) {
        // Only add point if far enough from last point (thinning)
        const last = drawPoints[drawPoints.length - 1];
        if (Math.hypot(wx - last.x, wy - last.y) > 1 / Math.max(vp.zoom, 0.5)) {
            drawPoints.push({ x: wx, y: wy });
        }
        return;
    }
    if (isShapeDrag) {
        handleMoveDrag(wx, wy);
        return;
    }
    if (isResizing) {
        handleResizeDrag(wx, wy);
        return;
    }
    if (isErasing) {
        eraseAt(wx, wy);
        return;
    }
}
function onMouseUp(e) {
    const rect = canvas.getBoundingClientRect();
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    const vp = state.getViewport();
    const wx = (sx - vp.offsetX) / vp.zoom;
    const wy = (sy - vp.offsetY) / vp.zoom;
    lastMouseScreen = { x: sx, y: sy };
    lastMouseWorld = { x: wx, y: wy };
    if (isPanning) {
        isPanning = false;
        canvas.style.cursor = '';
        return;
    }
    if (isDrawing) {
        finishDrawing();
        return;
    }
    if (isShapeDrag) {
        state.pushHistoryEntry({
            shapesBefore: JSON.parse(JSON.stringify(dragOrigShapes)),
            shapesAfter: JSON.parse(JSON.stringify(state.getShapes())),
        });
        state.persist();
        isShapeDrag = false;
        return;
    }
    if (isResizing) {
        state.pushHistoryEntry({
            shapesBefore: JSON.parse(JSON.stringify(dragOrigShapes)),
            shapesAfter: JSON.parse(JSON.stringify(state.getShapes())),
        });
        state.persist();
        isResizing = false;
        resizeHandle = -1;
        return;
    }
    if (isRubberBand) {
        finishRubberBand();
        isRubberBand = false;
        rubberBandStart = null;
        return;
    }
    if (isErasing) {
        if (eraseEntry) {
            eraseEntry.shapesAfter = JSON.parse(JSON.stringify(state.getShapes()));
            state.pushHistoryEntry(eraseEntry);
            state.persist();
            eraseEntry = null;
        }
        isErasing = false;
        return;
    }
    // Check if we did a shape creation drag (not drawing, not selecting, not erasing)
    const tool = state.getTool();
    if (tool === 'rectangle' || tool === 'ellipse' || tool === 'line') {
        finalizeCreate();
        state.setTool('select');
        updateToolIndicators();
        updateCursor();
    }
}
function onWheel(e) {
    e.preventDefault();
    const vp = state.getViewport();
    const rect = canvas.getBoundingClientRect();
    const sx = e.clientX - rect.left;
    const sy = e.clientY - rect.top;
    const factor = e.deltaY < 0 ? constants_1.ZOOM_STEP : 1 / constants_1.ZOOM_STEP;
    const newZoom = Math.max(constants_1.ZOOM_MIN, Math.min(constants_1.ZOOM_MAX, vp.zoom * factor));
    // Zoom toward cursor
    const worldX = (sx - vp.offsetX) / vp.zoom;
    const worldY = (sy - vp.offsetY) / vp.zoom;
    state.setViewport({
        offsetX: sx - worldX * newZoom,
        offsetY: sy - worldY * newZoom,
        zoom: newZoom,
    });
}
// --- Keyboard ---
function onKeyDown(e) {
    if (textEditing)
        return;
    if (e.code === 'Space' && !e.repeat) {
        spaceDown = true;
        if (!isPanning)
            canvas.style.cursor = 'grab';
        e.preventDefault();
    }
    if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
        e.preventDefault();
        state.undo();
        updateUndoRedoButtons();
    }
    if ((e.key === 'z' && (e.ctrlKey || e.metaKey) && e.shiftKey) ||
        (e.key === 'y' && (e.ctrlKey || e.metaKey))) {
        e.preventDefault();
        state.redo();
        updateUndoRedoButtons();
    }
    if ((e.key === 'Delete' || e.key === 'Backspace') && state.getSelectedIds().length > 0) {
        e.preventDefault();
        state.deleteSelected();
        state.persist();
    }
    if (!e.ctrlKey && !e.metaKey && state.getSelectedIds().length === 0) {
        switch (e.key.toLowerCase()) {
            case 'v':
                state.setTool('select');
                break;
            case 'p':
                state.setTool('freehand');
                break;
            case 'r':
                state.setTool('rectangle');
                break;
            case 'o':
                state.setTool('ellipse');
                break;
            case 'l':
                state.setTool('line');
                break;
            case 't':
                state.setTool('text');
                break;
            case 'e':
                state.setTool('eraser');
                break;
        }
        updateToolIndicators();
        updateCursor();
    }
    // Arrow key nudging
    if (state.getSelectedIds().length > 0 && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
        e.preventDefault();
        const nudge = e.shiftKey ? 10 : 1;
        let dx = 0, dy = 0;
        if (e.key === 'ArrowUp')
            dy = -nudge;
        if (e.key === 'ArrowDown')
            dy = nudge;
        if (e.key === 'ArrowLeft')
            dx = -nudge;
        if (e.key === 'ArrowRight')
            dx = nudge;
        nudgeSelection(dx, dy);
    }
}
function onKeyUp(e) {
    if (e.code === 'Space') {
        spaceDown = false;
        canvas.style.cursor = '';
        updateCursor();
    }
}
function nudgeSelection(dx, dy) {
    const shapes = state.getShapes();
    const ids = state.getSelectedIds();
    if (ids.length === 0)
        return;
    const entry = {
        shapesBefore: JSON.parse(JSON.stringify(shapes)),
        shapesAfter: JSON.parse(JSON.stringify(shapes)),
    };
    for (const s of shapes) {
        if (!ids.includes(s.id))
            continue;
        nudgeShape(s, dx, dy);
    }
    entry.shapesAfter = JSON.parse(JSON.stringify(shapes));
    state.pushHistoryEntry(entry);
    state.persist();
}
function nudgeShape(s, dx, dy) {
    switch (s.type) {
        case 'rectangle':
            if (s.x !== undefined && s.y !== undefined) {
                s.x += dx;
                s.y += dy;
            }
            break;
        case 'ellipse':
            if (s.cx !== undefined && s.cy !== undefined) {
                s.cx += dx;
                s.cy += dy;
            }
            break;
        case 'freehand':
            if (s.points) {
                for (const p of s.points) {
                    p.x += dx;
                    p.y += dy;
                }
            }
            break;
        case 'line':
        case 'arrow':
            if (s.x1 !== undefined && s.y1 !== undefined && s.x2 !== undefined && s.y2 !== undefined) {
                s.x1 += dx;
                s.y1 += dy;
                s.x2 += dx;
                s.y2 += dy;
            }
            break;
        case 'text':
            if (s.x !== undefined && s.y !== undefined) {
                s.x += dx;
                s.y += dy;
            }
            break;
    }
}
// --- Select tool ---
function handleSelectDown(sx, sy, wx, wy) {
    const shapes = state.getShapes();
    const vp = state.getViewport();
    // Check handles first
    const selectedIds = state.getSelectedIds();
    if (selectedIds.length > 0) {
        const selected = shapes.filter(s => selectedIds.includes(s.id));
        const bounds = (0, hittest_1.getCombinedBounds)(selected);
        const handles = [
            { x: bounds.x, y: bounds.y },
            { x: bounds.x + bounds.width, y: bounds.y },
            { x: bounds.x, y: bounds.y + bounds.height },
            { x: bounds.x + bounds.width, y: bounds.y + bounds.height },
        ];
        for (let i = 0; i < handles.length; i++) {
            if ((0, hittest_1.hitHandle)(handles[i], { x: wx, y: wy }, vp.zoom)) {
                isResizing = true;
                resizeHandle = i;
                resizeOrigBounds = JSON.parse(JSON.stringify(bounds));
                resizeStartWorld = { x: wx, y: wy };
                dragOrigShapes = JSON.parse(JSON.stringify(shapes));
                return;
            }
        }
    }
    // Hit test shapes (top first)
    for (let i = shapes.length - 1; i >= 0; i--) {
        if ((0, hittest_1.hitTest)(shapes[i], wx, wy, ctx)) {
            isShapeDrag = true;
            if (shiftActive) {
                state.select(shapes[i].id, true);
            }
            else {
                state.select(shapes[i].id, false);
            }
            dragStartWorld = { x: wx, y: wy };
            dragOrigShapes = JSON.parse(JSON.stringify(shapes));
            return;
        }
    }
    // Empty space — rubber band or deselect
    isRubberBand = true;
    rubberBandStart = { x: sx, y: sy };
    if (!shiftActive) {
        state.deselectAll();
    }
}
function handleMoveDrag(wx, wy) {
    const dwx = wx - dragStartWorld.x;
    const dwy = wy - dragStartWorld.y;
    const shapes = state.getShapes();
    const ids = state.getSelectedIds();
    for (let i = 0; i < dragOrigShapes.length; i++) {
        if (!ids.includes(dragOrigShapes[i].id))
            continue;
        const current = shapes.find(s => s.id === dragOrigShapes[i].id);
        if (!current)
            continue;
        const orig = dragOrigShapes[i];
        moveShapeTo(current, orig, dwx, dwy);
    }
}
function moveShapeTo(current, orig, dx, dy) {
    switch (current.type) {
        case 'rectangle':
            if (current.x !== undefined && current.y !== undefined) {
                current.x = orig.x + dx;
                current.y = orig.y + dy;
            }
            break;
        case 'ellipse':
            if (current.cx !== undefined && current.cy !== undefined) {
                current.cx = orig.cx + dx;
                current.cy = orig.cy + dy;
            }
            break;
        case 'freehand':
            if (current.points && orig.points) {
                for (let i = 0; i < orig.points.length; i++) {
                    current.points[i].x = orig.points[i].x + dx;
                    current.points[i].y = orig.points[i].y + dy;
                }
            }
            break;
        case 'line':
        case 'arrow':
            if (current.x1 !== undefined && current.y1 !== undefined && current.x2 !== undefined && current.y2 !== undefined) {
                current.x1 = orig.x1 + dx;
                current.y1 = orig.y1 + dy;
                current.x2 = orig.x2 + dx;
                current.y2 = orig.y2 + dy;
            }
            break;
        case 'text':
            if (current.x !== undefined && current.y !== undefined) {
                current.x = orig.x + dx;
                current.y = orig.y + dy;
            }
            break;
    }
}
function handleResizeDrag(wx, wy) {
    const shapes = state.getShapes();
    const ids = state.getSelectedIds();
    let newX = resizeOrigBounds.x;
    let newY = resizeOrigBounds.y;
    let newW = resizeOrigBounds.width;
    let newH = resizeOrigBounds.height;
    const dx = wx - resizeStartWorld.x;
    const dy = wy - resizeStartWorld.y;
    switch (resizeHandle) {
        case 0: // TL
            newX = resizeOrigBounds.x + dx;
            newY = resizeOrigBounds.y + dy;
            newW = resizeOrigBounds.width - dx;
            newH = resizeOrigBounds.height - dy;
            break;
        case 1: // TR
            newY = resizeOrigBounds.y + dy;
            newW = resizeOrigBounds.width + dx;
            newH = resizeOrigBounds.height - dy;
            break;
        case 2: // BL
            newX = resizeOrigBounds.x + dx;
            newW = resizeOrigBounds.width - dx;
            newH = resizeOrigBounds.height + dy;
            break;
        case 3: // BR
            newW = resizeOrigBounds.width + dx;
            newH = resizeOrigBounds.height + dy;
            break;
    }
    const scaleX = resizeOrigBounds.width !== 0 ? newW / resizeOrigBounds.width : 1;
    const scaleY = resizeOrigBounds.height !== 0 ? newH / resizeOrigBounds.height : 1;
    for (const orig of dragOrigShapes) {
        if (!ids.includes(orig.id))
            continue;
        const current = shapes.find(s => s.id === orig.id);
        if (!current)
            continue;
        resizeShape(current, orig, resizeOrigBounds, scaleX, scaleY);
    }
}
function resizeShape(current, orig, bounds, sx, sy) {
    switch (current.type) {
        case 'rectangle': {
            if (orig.x !== undefined && orig.width !== undefined && orig.height !== undefined) {
                const cx = orig.x - bounds.x;
                const cy = orig.y - bounds.y;
                current.width = orig.width * sx;
                current.height = orig.height * sy;
                current.x = bounds.x + cx;
                current.y = bounds.y + cy;
            }
            break;
        }
        case 'ellipse': {
            if (orig.cx !== undefined && orig.rx !== undefined && orig.ry !== undefined) {
                const cx = orig.cx - bounds.x;
                const cy = orig.cy - bounds.y;
                current.rx = orig.rx * sx;
                current.ry = orig.ry * sy;
                current.cx = bounds.x + cx;
                current.cy = bounds.y + cy;
            }
            break;
        }
        case 'freehand': {
            if (current.points && orig.points) {
                for (let i = 0; i < orig.points.length; i++) {
                    const lx = orig.points[i].x - bounds.x;
                    const ly = orig.points[i].y - bounds.y;
                    current.points[i].x = bounds.x + lx * sx;
                    current.points[i].y = bounds.y + ly * sy;
                }
            }
            break;
        }
        case 'line':
        case 'arrow': {
            if (orig.x1 !== undefined && orig.y1 !== undefined && orig.x2 !== undefined && orig.y2 !== undefined) {
                const lx1 = orig.x1 - bounds.x;
                const ly1 = orig.y1 - bounds.y;
                const lx2 = orig.x2 - bounds.x;
                const ly2 = orig.y2 - bounds.y;
                current.x1 = bounds.x + lx1 * sx;
                current.y1 = bounds.y + ly1 * sy;
                current.x2 = bounds.x + lx2 * sx;
                current.y2 = bounds.y + ly2 * sy;
            }
            break;
        }
        case 'text': {
            if (orig.x !== undefined && orig.y !== undefined) {
                const lx = orig.x - bounds.x;
                const ly = orig.y - bounds.y;
                current.x = bounds.x + lx * sx;
                current.y = bounds.y + ly * sy;
            }
            break;
        }
    }
}
function finishRubberBand() {
    if (!rubberBandStart)
        return;
    const shapes = state.getShapes();
    const x1 = Math.min(rubberBandStart.x, lastMouseScreen.x);
    const y1 = Math.min(rubberBandStart.y, lastMouseScreen.y);
    const x2 = Math.max(rubberBandStart.x, lastMouseScreen.x);
    const y2 = Math.max(rubberBandStart.y, lastMouseScreen.y);
    const vp = state.getViewport();
    const w1 = (x1 - vp.offsetX) / vp.zoom;
    const t1 = (y1 - vp.offsetY) / vp.zoom;
    const w2 = (x2 - vp.offsetX) / vp.zoom;
    const t2 = (y2 - vp.offsetY) / vp.zoom;
    const ids = [];
    for (const s of shapes) {
        const b = (0, hittest_1.getCombinedBounds)([s]);
        const cx = b.x + b.width / 2;
        const cy = b.y + b.height / 2;
        if (cx >= w1 && cx <= w2 && cy >= t1 && cy <= t2) {
            ids.push(s.id);
        }
    }
    state.setSelection(ids);
}
// --- Create tools ---
function handleCreateDown(sx, sy, wx, wy) {
    const tool = state.getTool();
    if (tool === 'freehand') {
        isDrawing = true;
        drawPoints = [{ x: wx, y: wy }];
    }
    else {
        shapeStart = { x: wx, y: wy };
    }
}
function finishDrawing() {
    if (drawPoints.length < 2) {
        isDrawing = false;
        drawPoints = [];
        return;
    }
    const shapes = state.getShapes();
    const shape = state.createShape({ type: 'freehand', points: [...drawPoints] });
    state.select(shape.id, false);
    state.pushHistoryEntry({
        shapesBefore: JSON.parse(JSON.stringify(shapes)),
        shapesAfter: JSON.parse(JSON.stringify(state.getShapes())),
    });
    state.persist();
    isDrawing = false;
    drawPoints = [];
}
function getPreviewShape() {
    const tool = state.getTool();
    if (tool === 'select' || tool === 'eraser' || tool === 'text')
        return null;
    if (tool === 'freehand')
        return null;
    if (isDrawing || isShapeDrag || isResizing)
        return null;
    const sx = lastMouseWorld.x;
    const sy = lastMouseWorld.y;
    const sx0 = shapeStart.x;
    const sy0 = shapeStart.y;
    const style = state.getStyle();
    const partial = {
        strokeColor: style.strokeColor,
        fillColor: style.fillColor,
        strokeWidth: style.strokeWidth,
        opacity: 0.5,
    };
    if (tool === 'rectangle') {
        return {
            ...partial,
            id: '__preview__',
            type: 'rectangle',
            x: (sx0 + sx) / 2,
            y: (sy0 + sy) / 2,
            width: sx - sx0,
            height: sy - sy0,
        };
    }
    else if (tool === 'ellipse') {
        return {
            ...partial,
            id: '__preview__',
            type: 'ellipse',
            cx: (sx0 + sx) / 2,
            cy: (sy0 + sy) / 2,
            rx: Math.abs(sx - sx0) / 2,
            ry: Math.abs(sy - sy0) / 2,
        };
    }
    else if (tool === 'line') {
        return {
            ...partial,
            id: '__preview__',
            type: 'line',
            x1: sx0, y1: sy0, x2: sx, y2: sy,
        };
    }
    return null;
}
// Finalize shape creation on mouse up
const origOnMouseUp = onMouseUp;
// --- Eraser ---
function handleEraserDown(wx, wy) {
    isErasing = true;
    eraseEntry = null;
    eraseAt(wx, wy);
}
function eraseAt(wx, wy) {
    const shapes = state.getShapes();
    const vp = state.getViewport();
    // Eraser has wider hit tolerance scaled by zoom
    const eraserRadius = 12 / vp.zoom;
    for (let i = shapes.length - 1; i >= 0; i--) {
        // Simple distance check to shape center + extended bounds
        const b = (0, hittest_1.getCombinedBounds)([shapes[i]]);
        const cx = b.x + b.width / 2;
        const cy = b.y + b.height / 2;
        const maxDim = Math.max(b.width, b.height) / 2 + eraserRadius;
        if (Math.abs(wx - cx) > maxDim && Math.abs(wy - cy) > maxDim)
            continue;
        if ((0, hittest_1.hitTest)(shapes[i], wx, wy, ctx)) {
            if (!eraseEntry) {
                eraseEntry = {
                    shapesBefore: JSON.parse(JSON.stringify(state.getShapes())),
                    shapesAfter: [],
                };
            }
            state.deleteShape(shapes[i].id);
            break;
        }
    }
}
// --- Text tool ---
function handleTextDown(wx, wy) {
    state.setTool('select');
    updateToolIndicators();
    updateCursor();
    const style = state.getStyle();
    const shapes = state.getShapes();
    const shape = state.createShape({
        type: 'text',
        x: wx, y: wy,
        text: '',
        fontSize: 16,
        strokeColor: style.strokeColor,
        fillColor: '#ffffff',
        strokeWidth: 0,
    });
    showTextInput(shape);
    // Commit creation to history when text is done
    shape._commitFn = () => {
        state.pushHistoryEntry({
            shapesBefore: [],
            shapesAfter: [shape],
        });
        state.persist();
    };
}
function showTextInput(shape) {
    textEditing = true;
    const vp = state.getViewport();
    const sp = (0, coords_1.worldToScreen)(shape.x || 0, shape.y || 0, vp);
    const textarea = document.createElement('textarea');
    textarea.value = shape.text || '';
    Object.assign(textarea.style, {
        position: 'fixed',
        left: sp.x + 'px',
        top: sp.y + 'px',
        fontSize: (shape.fontSize || 16) + 'px',
        color: shape.strokeColor,
        border: `2px solid ${constants_1.COLORS.selected}`,
        borderRadius: '4px',
        padding: '4px 8px',
        minWidth: '120px',
        background: '#fff',
        outline: 'none',
        zIndex: '1000',
        fontFamily: 'sans-serif',
        lineHeight: '1.2',
        resize: 'none',
    });
    document.body.appendChild(textarea);
    textarea.focus();
    textarea.select();
    const commit = () => {
        if (document.body.contains(textarea)) {
            document.body.removeChild(textarea);
        }
        textEditing = false;
        if (!textarea.value.trim()) {
            state.deleteShape(shape.id);
        }
        else {
            shape.text = textarea.value;
            if (typeof shape._commitFn === 'function') {
                shape._commitFn();
            }
        }
    };
    textarea.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            commit();
            e.preventDefault();
            e.stopPropagation();
        }
    });
    textarea.addEventListener('blur', commit);
    textarea.addEventListener('input', () => {
        shape.text = textarea.value;
    });
}
// --- UI helpers ---
function updateCursor() {
    if (spaceDown) {
        canvas.style.cursor = 'grab';
        return;
    }
    const tool = state.getTool();
    switch (tool) {
        case 'select':
            canvas.style.cursor = 'default';
            break;
        case 'freehand':
            canvas.style.cursor = 'crosshair';
            break;
        case 'rectangle':
        case 'ellipse':
        case 'line':
            canvas.style.cursor = 'crosshair';
            break;
        case 'text':
            canvas.style.cursor = 'text';
            break;
        case 'eraser':
            canvas.style.cursor = 'pointer';
            break;
    }
}
function updateToolIndicators() {
    const tool = state.getTool();
    document.querySelectorAll('.tool-btn').forEach(btn => {
        const t = btn.getAttribute('data-tool');
        btn.classList.toggle('active', t === tool);
    });
}
function updateUndoRedoButtons() {
    const undoBtn = document.getElementById('btn-undo');
    const redoBtn = document.getElementById('btn-redo');
    if (undoBtn)
        undoBtn.disabled = !state.canUndo();
    if (redoBtn)
        redoBtn.disabled = !state.canRedo();
}
// Expose shape creation finalization — called from render preview
function finalizeCreate() {
    const tool = state.getTool();
    if (tool === 'select' || tool === 'freehand' || tool === 'eraser' || tool === 'text')
        return;
    const sx = lastMouseWorld.x;
    const sy = lastMouseWorld.y;
    const sx0 = shapeStart.x;
    const sy0 = shapeStart.y;
    if (tool === 'rectangle') {
        const shapes = state.getShapes();
        const shape = state.createShape({
            type: 'rectangle',
            x: (sx0 + sx) / 2,
            y: (sy0 + sy) / 2,
            width: sx - sx0,
            height: sy - sy0,
        });
        state.select(shape.id, false);
        state.pushHistoryEntry({
            shapesBefore: JSON.parse(JSON.stringify(shapes)),
            shapesAfter: JSON.parse(JSON.stringify(state.getShapes())),
        });
        state.persist();
    }
    else if (tool === 'ellipse') {
        const shapes = state.getShapes();
        const shape = state.createShape({
            type: 'ellipse',
            cx: (sx0 + sx) / 2,
            cy: (sy0 + sy) / 2,
            rx: Math.abs(sx - sx0) / 2,
            ry: Math.abs(sy - sy0) / 2,
        });
        state.select(shape.id, false);
        state.pushHistoryEntry({
            shapesBefore: JSON.parse(JSON.stringify(shapes)),
            shapesAfter: JSON.parse(JSON.stringify(state.getShapes())),
        });
        state.persist();
    }
    else if (tool === 'line') {
        const shapes = state.getShapes();
        const shape = state.createShape({
            type: 'line',
            x1: sx0, y1: sy0, x2: sx, y2: sy,
        });
        state.select(shape.id, false);
        state.pushHistoryEntry({
            shapesBefore: JSON.parse(JSON.stringify(shapes)),
            shapesAfter: JSON.parse(JSON.stringify(state.getShapes())),
        });
        state.persist();
    }
}
// We need finalizeCreate to run on mouse up for create tools
// Let me patch onMouseUp — actually it already falls through, I need to add the call
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ThinkingCap-Qwen3.6-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"e01825c1-5f6b-44f6-8e67-cccfa49bf9fd","tokensIn":1590074,"tokensOut":42274,"tokensTotal":1632348,"cost":0,"turns":40,"toolCalls":44,"failedToolCalls":1,"timestamp":"2026-08-03T21:58:37.223Z"} -->