← Whiteboard results

Whiteboard

thinkingcap qwen3.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 70,172 bytes · SHA-256 05eb5219f8d0
<!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: 'Inter', system-ui, -apple-system, sans-serif; background: #e8eaed; }

#board-canvas {
  display: block;
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  cursor: crosshair;
}

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

.tool-btn {
  width: 36px; height: 36px;
  display: flex; align-items: center; justify-content: center;
  border: none; background: transparent;
  border-radius: 8px; cursor: pointer;
  font-size: 16px; color: #495057;
  transition: background 0.1s, color 0.1s;
}

.tool-btn:hover { background: #f1f3f5; }
.tool-btn.active { background: #e7f5ff; color: #1971c2; font-weight: 600; }

.toolbar-sep {
  width: 1px; height: 24px;
  background: #dee2e6; margin: 0 4px;
}

/* ─── Properties Bar ─────────────────────────────────────────────── */
.floating-props {
  position: fixed;
  bottom: 12px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 8px 12px;
  background: #ffffff;
  border-radius: 12px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.06);
  z-index: 100;
}

.props-label {
  font-size: 11px; color: #868e96; margin-right: 4px; text-transform: uppercase; letter-spacing: 0.5px;
}

.color-swatch {
  width: 24px; height: 24px;
  border-radius: 6px; border: 2px solid transparent;
  cursor: pointer; transition: transform 0.1s, border-color 0.1s;
}

.color-swatch:hover { transform: scale(1.15); }
.color-swatch.active { border-color: #4dabf7; }
.transparent-swatch { background-color: #fff !important; }

.width-btn {
  width: 28px; height: 28px;
  display: flex; align-items: center; justify-content: center;
  border: none; border-bottom-style: solid; border-bottom-color: #495057;
  background: transparent; cursor: pointer;
  border-radius: 6px; transition: background 0.1s;
}

.width-btn:hover { background: #f1f3f5; }
.width-btn.active { background: #e7f5ff; }

/* ─── Zoom Indicator ─────────────────────────────────────────────── */
.zoom-badge {
  position: fixed;
  bottom: 12px; right: 16px;
  padding: 4px 10px;
  background: #ffffff;
  border-radius: 8px;
  font-size: 13px; color: #495057;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  z-index: 100;
  user-select: none;
}

/* ─── Text Editor Overlay ────────────────────────────────────────── */
.text-editor {
  position: absolute;
  z-index: 200;
  border: 2px solid #4dabf7;
  border-radius: 4px;
  padding: 4px 8px;
  outline: none;
  resize: both;
  overflow: auto;
  background: rgba(255,255,255,0.95);
  min-width: 100px;
}

/* ─── Cursor styles per tool ─────────────────────────────────────── */
#board-canvas.tool-select { cursor: default; }
#board-canvas.tool-pen { cursor: crosshair; }
#board-canvas.tool-rectangle, #board-canvas.tool-ellipse, #board-canvas.tool-line { cursor: crosshair; }
#board-canvas.tool-text { cursor: text; }
#board-canvas.tool-eraser { cursor: cell; }
</style>
</head>
<body>
<div id="toolbar" class="floating-toolbar"></div>
<div id="props-bar" class="floating-props"></div>
<div id="zoom-indicator" class="zoom-badge">100%</div>
<canvas id="board-canvas"></canvas>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./state":"src/state.ts","./viewport":"src/viewport.ts","./rendering":"src/rendering.ts","./input":"src/input.ts","./ui":"src/ui.ts","./types":"src/types.ts","./constants":"src/constants.ts"},"src/state.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/viewport.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/hitTesting.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/rendering.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./viewport":"src/viewport.ts"},"src/history.ts":{"./types":"src/types.ts"},"src/input.ts":{"./types":"src/types.ts","./state":"src/state.ts","./viewport":"src/viewport.ts","./hitTesting":"src/hitTesting.ts","./history":"src/history.ts","./constants":"src/constants.ts"},"src/ui.ts":{"./types":"src/types.ts","./state":"src/state.ts","./input":"src/input.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 Point ──────────────────────────────────────────────────────
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 render = __importStar(require("./rendering"));
const input = __importStar(require("./input"));
const ui = __importStar(require("./ui"));
const constants_1 = require("./constants");
// ─── Canvas Setup ──────────────────────────────────────────────────────
const canvas = document.getElementById('board-canvas');
const ctx = canvas.getContext('2d', { alpha: false });
function resizeCanvas() {
    const dpr = window.devicePixelRatio || 1;
    canvas.width = window.innerWidth * dpr;
    canvas.height = window.innerHeight * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// ─── Init State & UI ──────────────────────────────────────────────────
state.init();
ui.initUI();
input.setCanvas(canvas);
// ─── Render Loop ──────────────────────────────────────────────────────
function renderFrame() {
    const w = window.innerWidth;
    const h = window.innerHeight;
    const shapes = state.getShapes();
    const v = state.getViewport();
    const selIds = state.getSelectedIds();
    // Main scene
    render.render(ctx, shapes, v, selIds, w, h);
    // Pen stroke preview
    const penPts = input.getCurrentStrokePoints();
    if (penPts.length > 1) {
        drawPenPreview(ctx, penPts, state.getDefaultStyle());
    }
    // Shape creation preview
    const creatingType = input.getCreatingShapeType();
    if (creatingType && input.isDragActive()) {
        const startWp = input.getShapeStartWorld();
        const endWp = input.getShapeEndWorld();
        if (startWp && endWp) {
            drawShapePreview(ctx, creatingType, startWp, endWp);
        }
    }
    // Rubber band selection rect
    const rbRect = input.getRubberRect();
    if (rbRect) {
        ctx.strokeStyle = constants_1.COLORS.rubberBandStroke;
        ctx.lineWidth = 1;
        ctx.setLineDash([6, 4]);
        ctx.fillStyle = constants_1.COLORS.rubberBandFill;
        ctx.fillRect(rbRect.x, rbRect.y, rbRect.w, rbRect.h);
        ctx.strokeRect(rbRect.x, rbRect.y, rbRect.w, rbRect.h);
        ctx.setLineDash([]);
    }
    // Cursor style per tool
    canvas.className = 'tool-' + state.getTool();
    requestAnimationFrame(renderFrame);
}
// ─── Preview Drawing Helpers ──────────────────────────────────────────
function drawPenPreview(ctx, points, style) {
    if (points.length < 2)
        return;
    ctx.save();
    const v = state.getViewport();
    ctx.translate(v.offsetX, v.offsetY);
    ctx.scale(v.zoom, v.zoom);
    ctx.strokeStyle = style.strokeColor;
    ctx.lineWidth = style.strokeWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    ctx.beginPath();
    ctx.moveTo(points[0].x, points[0].y);
    for (let i = 1; i < points.length - 1; i++) {
        const mx = (points[i].x + points[i + 1].x) / 2;
        const my = (points[i].y + points[i + 1].y) / 2;
        ctx.quadraticCurveTo(points[i].x, points[i].y, mx, my);
    }
    ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y);
    ctx.stroke();
    ctx.restore();
}
function drawShapePreview(ctx, type, startWp, endWp) {
    ctx.save();
    const v = state.getViewport();
    ctx.translate(v.offsetX, v.offsetY);
    ctx.scale(v.zoom, v.zoom);
    const style = state.getDefaultStyle();
    ctx.strokeStyle = style.strokeColor;
    ctx.lineWidth = style.strokeWidth;
    ctx.lineCap = 'round';
    if (style.fillColor && style.fillColor !== 'transparent') {
        ctx.fillStyle = style.fillColor;
    }
    switch (type) {
        case 'rectangle':
            ctx.beginPath();
            ctx.rect(startWp.x, startWp.y, endWp.x - startWp.x, endWp.y - startWp.y);
            if (style.fillColor && style.fillColor !== 'transparent')
                ctx.fill();
            ctx.stroke();
            break;
        case 'ellipse': {
            const cx = (startWp.x + endWp.x) / 2;
            const cy = (startWp.y + endWp.y) / 2;
            const rx = Math.abs(endWp.x - startWp.x) / 2;
            const ry = Math.abs(endWp.y - startWp.y) / 2;
            ctx.beginPath();
            ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
            if (style.fillColor && style.fillColor !== 'transparent')
                ctx.fill();
            ctx.stroke();
            break;
        }
        case 'line':
            ctx.beginPath();
            ctx.moveTo(startWp.x, startWp.y);
            ctx.lineTo(endWp.x, endWp.y);
            ctx.stroke();
            break;
    }
    ctx.restore();
}
// ─── Start Render Loop ────────────────────────────────────────────────
requestAnimationFrame(renderFrame);
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ─── Tunable Constants ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.SHIFT_NUDGE_AMOUNT = exports.NUDGE_AMOUNT = exports.GRID_SPACING = exports.HIT_TOLERANCE = exports.HANDLE_SIZE = exports.ZOOM_STEP = exports.ZOOM_MAX = exports.ZOOM_MIN = exports.STROKE_WIDTHS = exports.FILL_PALETTE = exports.PALETTE = exports.COLORS = void 0;
exports.COLORS = {
    bg: '#f8f9fa',
    gridDot: '#e0e0e0',
    chrome: '#ffffff',
    chromeBorder: '#dee2e6',
    toolbarShadow: 'rgba(0,0,0,0.12)',
    selectedStroke: '#4dabf7',
    handleFill: '#ffffff',
    handleStroke: '#4dabf7',
    rubberBandStroke: '#4dabf7',
    rubberBandFill: 'rgba(77,171,247,0.1)',
};
exports.PALETTE = [
    '#1e1e1e', '#ee5659', '#bb3e03', '#f6c40e', '#2b8a3e',
    '#0c73bd', '#6a0dad', '#8c6293', '#000000', '#ffffff',
];
exports.FILL_PALETTE = [
    'transparent', '#fee2e2', '#fef3c7', '#d1fae5', '#dbeafe',
    '#ede9fe', '#f3e8ff', '#fce7f3', '#ffffff', '#1e1e1e',
];
exports.STROKE_WIDTHS = [1, 2, 4, 6];
exports.ZOOM_MIN = 0.25;
exports.ZOOM_MAX = 4.0;
exports.ZOOM_STEP = 1.1;
exports.HANDLE_SIZE = 8; // in world coords (scales with zoom)
exports.HIT_TOLERANCE = 6; // px for stroke hit testing
exports.GRID_SPACING = 24;
exports.NUDGE_AMOUNT = 1;
exports.SHIFT_NUDGE_AMOUNT = 10;
};

// ── 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";
// ─── Application State & Persistence ──────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.init = init;
exports.getState = getState;
exports.setShapes = setShapes;
exports.getShapes = getShapes;
exports.addShape = addShape;
exports.removeShape = removeShape;
exports.updateShape = updateShape;
exports.setViewport = setViewport;
exports.getViewport = getViewport;
exports.setTool = setTool;
exports.getTool = getTool;
exports.setSelectedIds = setSelectedIds;
exports.getSelectedIds = getSelectedIds;
exports.toggleSelection = toggleSelection;
exports.setDefaultStyle = setDefaultStyle;
exports.getDefaultStyle = getDefaultStyle;
const constants_1 = require("./constants");
const STORAGE_KEY = 'whiteboard-board-data';
let state;
function createInitialState() {
    return {
        shapes: [],
        viewport: { offsetX: 0, offsetY: 0, zoom: 1 },
        tool: 'select',
        selectedIds: new Set(),
        defaultStyle: { strokeColor: '#1e1e1e', fillColor: 'transparent', strokeWidth: constants_1.STROKE_WIDTHS[2] },
    };
}
function init() {
    state = loadFromStorage() ?? createInitialState();
    // Ensure selectedIds is a Set (can't survive JSON serialization)
    if (!(state.selectedIds instanceof Set)) {
        state.selectedIds = new Set(state.selectedIds);
    }
    return state;
}
function getState() { return state; }
function setShapes(shapes) { state.shapes = shapes; saveToStorage(); }
function getShapes() { return state.shapes; }
function addShape(shape) { state.shapes.push(shape); saveToStorage(); }
function removeShape(id) { state.shapes = state.shapes.filter(s => s.id !== id); saveToStorage(); }
function updateShape(updates) {
    const idx = state.shapes.findIndex(s => s.id === updates.id);
    if (idx >= 0)
        Object.assign(state.shapes[idx], updates);
    saveToStorage();
}
function setViewport(vp) { state.viewport = vp; }
function getViewport() { return state.viewport; }
function setTool(tool) { state.tool = tool; }
function getTool() { return state.tool; }
function setSelectedIds(ids) { state.selectedIds = ids; }
function getSelectedIds() { return state.selectedIds; }
function toggleSelection(id, addMode) {
    if (addMode && state.selectedIds.has(id)) {
        state.selectedIds.delete(id);
    }
    else {
        state.selectedIds.add(id);
    }
}
function setDefaultStyle(style) { state.defaultStyle = style; saveToStorage(); }
function getDefaultStyle() { return state.defaultStyle; }
// ─── Persistence ──────────────────────────────────────────────────────
function serialize() {
    const data = {
        shapes: state.shapes,
        viewport: state.viewport,
        defaultStyle: state.defaultStyle,
    };
    try {
        return JSON.stringify(data);
    }
    catch {
        return '';
    }
}
function saveToStorage() {
    try {
        localStorage.setItem(STORAGE_KEY, serialize());
    }
    catch { }
}
function loadFromStorage() {
    try {
        const raw = localStorage.getItem(STORAGE_KEY);
        if (!raw)
            return null;
        const data = JSON.parse(raw);
        return {
            shapes: data.shapes ?? [],
            viewport: data.viewport ?? { offsetX: 0, offsetY: 0, zoom: 1 },
            tool: 'select',
            selectedIds: new Set(),
            defaultStyle: data.defaultStyle ?? { strokeColor: '#1e1e1e', fillColor: 'transparent', strokeWidth: 4 },
        };
    }
    catch {
        return null;
    }
}
};

// ── module: src/viewport.ts ──
__mods["src/viewport.ts"] = function (exports, require, module) {
"use strict";
// ─── Viewport Transform (world ↔ screen) ──────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.worldToScreen = worldToScreen;
exports.screenToWorld = screenToWorld;
exports.zoomAt = zoomAt;
exports.clampZoom = clampZoom;
const constants_1 = require("./constants");
function worldToScreen(wp, vp) {
    return { x: wp.x * vp.zoom + vp.offsetX, y: wp.y * vp.zoom + vp.offsetY };
}
function screenToWorld(sp, vp) {
    return { x: (sp.x - vp.offsetX) / vp.zoom, y: (sp.y - vp.offsetY) / vp.zoom };
}
// Zoom toward a specific screen point — the world point under that cursor stays fixed.
function zoomAt(vp, sp, factor) {
    const newZoom = Math.max(constants_1.ZOOM_MIN, Math.min(constants_1.ZOOM_MAX, vp.zoom * factor));
    // The world point under sp must stay under sp after the zoom change.
    const worldPt = screenToWorld(sp, vp);
    return {
        offsetX: sp.x - worldPt.x * newZoom,
        offsetY: sp.y - worldPt.y * newZoom,
        zoom: newZoom,
    };
}
function clampZoom(vp) {
    const z = Math.max(constants_1.ZOOM_MIN, Math.min(constants_1.ZOOM_MAX, vp.zoom));
    return { ...vp, zoom: z };
}
};

// ── module: src/hitTesting.ts ──
__mods["src/hitTesting.ts"] = function (exports, require, module) {
"use strict";
// ─── Hit Testing (world coordinates) ──────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.hitTest = hitTest;
exports.boundingBox = boundingBox;
const constants_1 = require("./constants");
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));
}
function pointInRect(px, py, x, y, w, h) {
    const left = Math.min(x, x + w), right = Math.max(x, x + w);
    const top = Math.min(y, y + h), bottom = Math.max(y, y + h);
    return px >= left && px <= right && py >= top && py <= bottom;
}
function pointInEllipse(px, py, cx, cy, rx, ry) {
    if (rx === 0 || ry === 0)
        return false;
    return ((px - cx) ** 2 / rx ** 2 + (py - cy) ** 2 / ry ** 2) <= 1;
}
function pointNearPenStroke(points, px, py) {
    for (let i = 1; i < points.length; i++) {
        if (distPointToSegment(px, py, points[i - 1].x, points[i - 1].y, points[i].x, points[i].y) <= constants_1.HIT_TOLERANCE) {
            return true;
        }
    }
    return false;
}
function pointNearLine(x1, y1, x2, y2, px, py) {
    return distPointToSegment(px, py, x1, y1, x2, y2) <= constants_1.HIT_TOLERANCE;
}
function pointInTextRect(x, y, text, fontSize, px, py) {
    // Approximate text bounding box
    const w = text.length * fontSize * 0.6 + 8;
    const h = fontSize * 1.3;
    return px >= x && px <= x + w && py >= y - h && py <= y;
}
function hitTest(shape, wp) {
    const { x: px, y: py } = wp;
    switch (shape.type) {
        case 'rectangle':
            return pointInRect(px, py, shape.x, shape.y, shape.w, shape.h);
        case 'ellipse':
            return pointInEllipse(px, py, shape.cx, shape.cy, Math.abs(shape.rx), Math.abs(shape.ry));
        case 'line': {
            const tol = Math.max(constants_1.HIT_TOLERANCE, shape.strokeWidth / 2 + 2);
            return distPointToSegment(px, py, shape.x1, shape.y1, shape.x2, shape.y2) <= tol;
        }
        case 'pen':
            return pointNearPenStroke(shape.points, px, py);
        case 'text':
            return pointInTextRect(shape.x, shape.y, shape.text, shape.fontSize, px, py);
    }
}
// Compute bounding box in world coords for a set of shapes.
function boundingBox(shapes) {
    if (!shapes.length)
        return null;
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const s of shapes) {
        switch (s.type) {
            case 'rectangle': {
                const l = Math.min(s.x, s.x + s.w), r = Math.max(s.x, s.x + s.w);
                const t = Math.min(s.y, s.y + s.h), b = Math.max(s.y, s.y + s.h);
                minX = Math.min(minX, l);
                minY = Math.min(minY, t);
                maxX = Math.max(maxX, r);
                maxY = Math.max(maxY, b);
                break;
            }
            case 'ellipse': {
                minX = Math.min(minX, s.cx - s.rx);
                minY = Math.min(minY, s.cy - s.ry);
                maxX = Math.max(maxX, s.cx + s.rx);
                maxY = Math.max(maxY, s.cy + s.ry);
                break;
            }
            case 'line': {
                minX = Math.min(minX, s.x1, s.x2);
                minY = Math.min(minY, s.y1, s.y2);
                maxX = Math.max(maxX, s.x1, s.x2);
                maxY = Math.max(maxY, s.y1, s.y2);
                break;
            }
            case 'pen': {
                for (const p of s.points) {
                    minX = Math.min(minX, p.x);
                    minY = Math.min(minY, p.y);
                    maxX = Math.max(maxX, p.x);
                    maxY = Math.max(maxY, p.y);
                }
                break;
            }
            case 'text': {
                const w = s.text.length * s.fontSize * 0.6 + 8;
                const h = s.fontSize * 1.3;
                minX = Math.min(minX, s.x);
                minY = Math.min(minY, s.y - h);
                maxX = Math.max(maxX, s.x + w);
                maxY = Math.max(maxY, s.y);
                break;
            }
        }
    }
    return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
}
};

// ── module: src/rendering.ts ──
__mods["src/rendering.ts"] = function (exports, require, module) {
"use strict";
// ─── Canvas Rendering ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
const constants_1 = require("./constants");
const viewport_1 = require("./viewport");
function render(ctx, shapes, vp, selectedIds, width, height) {
    // ── Background ────────────────────────────────────────────────────
    ctx.fillStyle = constants_1.COLORS.bg;
    ctx.fillRect(0, 0, width, height);
    // ── Grid dots ─────────────────────────────────────────────────────
    drawGrid(ctx, vp, width, height);
    // ── Shapes ────────────────────────────────────────────────────────
    for (const shape of shapes) {
        drawShape(ctx, shape, vp, selectedIds.has(shape.id));
    }
    // ── Selection overlay ─────────────────────────────────────────────
    const selected = shapes.filter(s => selectedIds.has(s.id));
    if (selected.length > 0) {
        drawSelectionOverlay(ctx, selected, vp);
    }
}
// ─── Grid ──────────────────────────────────────────────────────────────
function drawGrid(ctx, vp, w, h) {
    if (vp.zoom < 0.3)
        return; // skip grid at very low zooms
    const spacing = constants_1.GRID_SPACING * vp.zoom;
    if (spacing < 6)
        return; // too dense
    const startX = vp.offsetX % spacing;
    const startY = vp.offsetY % spacing;
    ctx.fillStyle = constants_1.COLORS.gridDot;
    for (let x = startX; x < w; x += spacing) {
        for (let y = startY; y < h; y += spacing) {
            ctx.beginPath();
            ctx.arc(x, y, Math.max(0.5, vp.zoom * 1), 0, Math.PI * 2);
            ctx.fill();
        }
    }
}
// ─── Shape Drawing ─────────────────────────────────────────────────────
function drawShape(ctx, shape, vp, selected) {
    const z = vp.zoom;
    ctx.save();
    ctx.translate(vp.offsetX, vp.offsetY);
    ctx.scale(z, z);
    ctx.strokeStyle = shape.strokeColor;
    ctx.lineWidth = shape.strokeWidth / z * z; // keep consistent in world coords... actually just use strokeWidth
    ctx.lineWidth = shape.strokeWidth;
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';
    switch (shape.type) {
        case 'rectangle':
            drawRectangle(ctx, shape);
            break;
        case 'ellipse':
            drawEllipse(ctx, shape);
            break;
        case 'line':
            drawLine(ctx, shape);
            break;
        case 'pen':
            drawPenStroke(ctx, shape);
            break;
        case 'text':
            drawText(ctx, shape);
            break;
    }
    ctx.restore();
}
function drawRectangle(ctx, s) {
    const rect = s.x !== undefined ? s : null;
    if (!rect)
        return;
    if (rect.fillColor && rect.fillColor !== 'transparent') {
        ctx.fillStyle = rect.fillColor;
        ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
    }
    ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
}
function drawEllipse(ctx, s) {
    const e = s;
    if (!e.cx && !e.cy && !e.rx && !e.ry)
        return; // degenerate
    ctx.beginPath();
    ctx.ellipse(e.cx, e.cy, Math.abs(e.rx), Math.abs(e.ry), 0, 0, Math.PI * 2);
    if (e.fillColor && e.fillColor !== 'transparent') {
        ctx.fillStyle = e.fillColor;
        ctx.fill();
    }
    ctx.stroke();
}
function drawLine(ctx, s) {
    const l = s;
    ctx.beginPath();
    ctx.moveTo(l.x1, l.y1);
    ctx.lineTo(l.x2, l.y2);
    ctx.stroke();
    if (l.arrowHead) {
        drawArrowHead(ctx, l.x1, l.y1, l.x2, l.y2, l.strokeWidth);
    }
}
function drawArrowHead(ctx, x1, y1, x2, y2, sw) {
    const angle = Math.atan2(y2 - y1, x2 - x1);
    const headLen = Math.max(8 + sw * 3, 16);
    ctx.beginPath();
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - headLen * Math.cos(angle - Math.PI / 6), y2 - headLen * Math.sin(angle - Math.PI / 6));
    ctx.moveTo(x2, y2);
    ctx.lineTo(x2 - headLen * Math.cos(angle + Math.PI / 6), y2 - headLen * Math.sin(angle + Math.PI / 6));
    ctx.stroke();
}
function drawPenStroke(ctx, s) {
    const pen = s;
    if (pen.points.length < 2)
        return;
    ctx.beginPath();
    ctx.moveTo(pen.points[0].x, pen.points[0].y);
    // Smooth the stroke using quadratic curves through midpoints
    for (let i = 1; i < pen.points.length - 1; i++) {
        const mx = (pen.points[i].x + pen.points[i + 1].x) / 2;
        const my = (pen.points[i].y + pen.points[i + 1].y) / 2;
        ctx.quadraticCurveTo(pen.points[i].x, pen.points[i].y, mx, my);
    }
    // Last segment
    const last = pen.points[pen.points.length - 1];
    ctx.lineTo(last.x, last.y);
    ctx.stroke();
}
function drawText(ctx, s) {
    const t = s;
    ctx.font = `${t.fontSize}px "Inter", system-ui, sans-serif`;
    if (t.fillColor && t.fillColor !== 'transparent') {
        ctx.fillStyle = t.fillColor;
        const lines = wrapText(t.text, 40);
        for (let i = 0; i < lines.length; i++) {
            ctx.fillText(lines[i], t.x, t.y - (lines.length - 1 - i) * t.fontSize * 1.2);
        }
    }
    else {
        ctx.fillStyle = s.strokeColor;
        const lines = wrapText(t.text, 40);
        for (let i = 0; i < lines.length; i++) {
            ctx.fillText(lines[i], t.x, t.y - (lines.length - 1 - i) * t.fontSize * 1.2);
        }
    }
}
function wrapText(text, maxChars) {
    if (text.length <= maxChars)
        return [text];
    const lines = [];
    let current = '';
    for (const ch of text) {
        if (current.length >= maxChars) {
            lines.push(current);
            current = '';
        }
        current += ch;
    }
    if (current)
        lines.push(current);
    return lines;
}
// ─── Selection Overlay ────────────────────────────────────────────────
function drawSelectionOverlay(ctx, shapes, vp) {
    const box = computeScreenBounds(shapes, vp);
    if (!box)
        return;
    ctx.strokeStyle = constants_1.COLORS.selectedStroke;
    ctx.lineWidth = 1.5;
    ctx.setLineDash([6, 4]);
    ctx.strokeRect(box.x - 4, box.y - 4, box.w + 8, box.h + 8);
    ctx.setLineDash([]);
    // Corner handles
    const hs = constants_1.HANDLE_SIZE * vp.zoom;
    const corners = [
        { x: box.x - 4, y: box.y - 4 },
        { x: box.x + box.w + 4, y: box.y - 4 },
        { x: box.x - 4, y: box.y + box.h + 4 },
        { x: box.x + box.w + 4, y: box.y + box.h + 4 },
    ];
    for (const c of corners) {
        ctx.fillStyle = constants_1.COLORS.handleFill;
        ctx.strokeStyle = constants_1.COLORS.handleStroke;
        ctx.lineWidth = 1.5;
        ctx.fillRect(c.x - hs / 2, c.y - hs / 2, hs, hs);
        ctx.strokeRect(c.x - hs / 2, c.y - hs / 2, hs, hs);
    }
}
function computeScreenBounds(shapes, vp) {
    if (!shapes.length)
        return null;
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    for (const s of shapes) {
        switch (s.type) {
            case 'rectangle': {
                const l = Math.min(s.x, s.x + s.w), r = Math.max(s.x, s.x + s.w);
                const t = Math.min(s.y, s.y + s.h), b = Math.max(s.y, s.y + s.h);
                minX = Math.min(minX, l);
                minY = Math.min(minY, t);
                maxX = Math.max(maxX, r);
                maxY = Math.max(maxY, b);
                break;
            }
            case 'ellipse': {
                minX = Math.min(minX, s.cx - Math.abs(s.rx));
                minY = Math.min(minY, s.cy - Math.abs(s.ry));
                maxX = Math.max(maxX, s.cx + Math.abs(s.rx));
                maxY = Math.max(maxY, s.cy + Math.abs(s.ry));
                break;
            }
            case 'line': {
                minX = Math.min(minX, s.x1, s.x2);
                minY = Math.min(minY, s.y1, s.y2);
                maxX = Math.max(maxX, s.x1, s.x2);
                maxY = Math.max(maxY, s.y1, s.y2);
                break;
            }
            case 'pen': {
                for (const p of s.points) {
                    minX = Math.min(minX, p.x);
                    minY = Math.min(minY, p.y);
                    maxX = Math.max(maxX, p.x);
                    maxY = Math.max(maxY, p.y);
                }
                break;
            }
            case 'text': {
                const w = s.text.length * s.fontSize * 0.6 + 8;
                const h = s.fontSize * 1.3;
                minX = Math.min(minX, s.x);
                minY = Math.min(minY, s.y - h);
                maxX = Math.max(maxX, s.x + w);
                maxY = Math.max(maxY, s.y);
                break;
            }
        }
    }
    const tl = (0, viewport_1.worldToScreen)({ x: minX, y: minY }, vp);
    const br = (0, viewport_1.worldToScreen)({ x: maxX, y: maxY }, vp);
    return { x: tl.x, y: tl.y, w: br.x - tl.x, h: br.y - tl.y };
}
};

// ── module: src/history.ts ──
__mods["src/history.ts"] = function (exports, require, module) {
"use strict";
// ─── Undo / Redo History ──────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.canUndo = canUndo;
exports.canRedo = canRedo;
exports.beginEdit = beginEdit;
exports.commitEdit = commitEdit;
exports.undo = undo;
exports.redo = redo;
exports.clearHistory = clearHistory;
const MAX_HISTORY = 100;
let undoStack = [];
let redoStack = [];
function canUndo() { return undoStack.length > 0; }
function canRedo() { return redoStack.length > 0; }
// Begin recording a change. Returns the shapes that existed before (for snapshot).
function beginEdit(allShapes) {
    const before = new Map();
    for (const s of allShapes)
        before.set(s.id, structuredClone(s));
    return before;
}
// Commit the edit. `after` is the current full shape list.
function commitEdit(before, after) {
    const shapesAfter = new Map();
    for (const s of after)
        shapesAfter.set(s.id, structuredClone(s));
    undoStack.push({ action: 'modify', shapesBefore: before, shapesAfter });
    if (undoStack.length > MAX_HISTORY)
        undoStack.shift();
    redoStack = []; // discard redo on new edit
}
function undo(allShapes) {
    const entry = undoStack.pop();
    if (!entry)
        return allShapes;
    // Save current state for redo
    const shapesNow = new Map();
    for (const s of allShapes)
        shapesNow.set(s.id, structuredClone(s));
    redoStack.push({ action: 'modify', shapesBefore: entry.shapesAfter, shapesAfter: shapesNow });
    return applyState(entry.shapesBefore);
}
function redo(allShapes) {
    const entry = redoStack.pop();
    if (!entry)
        return allShapes;
    // Save current state for undo
    const shapesNow = new Map();
    for (const s of allShapes)
        shapesNow.set(s.id, structuredClone(s));
    undoStack.push({ action: 'modify', shapesBefore: entry.shapesBefore, shapesAfter: shapesNow });
    return applyState(entry.shapesAfter);
}
function applyState(shapeMap) {
    const result = [];
    // Preserve insertion order from the map (which was built in shape order)
    for (const [, s] of shapeMap)
        result.push(s);
    return result;
}
function clearHistory() {
    undoStack = [];
    redoStack = [];
}
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ─── Input Handling (mouse, keyboard) ──────────────────────────────────
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.setCanvas = setCanvas;
exports.startTextEditing = startTextEditing;
exports.commitTextEdit = commitTextEdit;
exports.cancelTextEdit = cancelTextEdit;
exports.getTextEditingId = getTextEditingId;
exports.on = on;
exports.getRubberRect = getRubberRect;
exports.getCurrentStrokePoints = getCurrentStrokePoints;
exports.isDragActive = isDragActive;
exports.getCreatingShapeType = getCreatingShapeType;
exports.getShapeStartWorld = getShapeStartWorld;
exports.getShapeEndWorld = getShapeEndWorld;
exports.setShapeEndWorld = setShapeEndWorld;
exports.doUndo = doUndo;
exports.doRedo = doRedo;
const state = __importStar(require("./state"));
const vp = __importStar(require("./viewport"));
const hit = __importStar(require("./hitTesting"));
const history = __importStar(require("./history"));
const constants_1 = require("./constants");
// ─── Interaction State ────────────────────────────────────────────────
let isDragging = false;
let dragStartScreen = null;
let dragStartWorld = null;
let currentStrokePoints = [];
let textEditingId = null;
// Shape creation preview state
let creatingShapeType = null;
let dragMode = 'none';
let resizeHandleIdx = -1;
let editSnapshot = null;
// Rubber band rect (screen coords)
let rubberRect = null;
function setCanvas(canvas) {
    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);
}
// ─── Mouse Events ──────────────────────────────────────────────────────
function onMouseDown(e) {
    const rect = e.target.getBoundingClientRect();
    const sp = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    const wp = vp.screenToWorld(sp, state.getViewport());
    const tool = state.getTool();
    if (e.button === 1 || (e.button === 0 && e.altKey)) {
        startPan(sp);
        return;
    }
    if (e.button !== 0)
        return;
    switch (tool) {
        case 'select':
            handleSelectDown(sp, wp, e.shiftKey);
            break;
        case 'pen':
            startDrawing(wp);
            break;
        case 'rectangle':
        case 'ellipse':
        case 'line':
            startShapeCreation(tool, wp);
            break;
        case 'text':
            placeText(wp);
            break;
        case 'eraser':
            startErasing(wp);
            break;
    }
}
function onMouseMove(e) {
    const rect = e.target.getBoundingClientRect();
    const sp = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    const wp = vp.screenToWorld(sp, state.getViewport());
    if (!isDragging)
        return;
    switch (dragMode) {
        case 'pan':
            doPan(sp);
            break;
        case 'move':
            doMove(wp);
            break;
        case 'resize':
            doResize(wp, e.shiftKey);
            break;
        case 'rubberband':
            rubberRect = computeRubberRect(sp);
            break;
        case 'draw':
            continueDrawing(wp);
            break;
        case 'erase':
            eraseAt(wp);
            break;
    }
}
function onMouseUp(e) {
    if (!isDragging)
        return;
    const rect = e.target.getBoundingClientRect();
    const sp = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    switch (dragMode) {
        case 'move':
            finishMove();
            break;
        case 'resize':
            finishResize();
            break;
        case 'rubberband':
            finishRubberBand(sp);
            rubberRect = null;
            break;
        case 'draw':
            finishDrawing();
            break;
    }
    isDragging = false;
    dragMode = 'none';
}
function onWheel(e) {
    e.preventDefault();
    const rect = e.target.getBoundingClientRect();
    const sp = { x: e.clientX - rect.left, y: e.clientY - rect.top };
    const factor = e.deltaY < 0 ? 1.08 : 1 / 1.08;
    state.setViewport(vp.zoomAt(state.getViewport(), sp, factor));
}
// ─── Pan ──────────────────────────────────────────────────────────────
function startPan(sp) {
    isDragging = true;
    dragMode = 'pan';
    dragStartScreen = sp;
}
function doPan(sp) {
    if (!dragStartScreen)
        return;
    const dx = sp.x - dragStartScreen.x, dy = sp.y - dragStartScreen.y;
    const v = state.getViewport();
    state.setViewport({ offsetX: v.offsetX + dx, offsetY: v.offsetY + dy, zoom: v.zoom });
    dragStartScreen = sp;
}
// ─── Select Tool ──────────────────────────────────────────────────────
function handleSelectDown(sp, wp, shiftKey) {
    const shapes = state.getShapes();
    const selectedIds = state.getSelectedIds();
    // Check resize handles (single selection only)
    if (selectedIds.size === 1) {
        const selShape = shapes.find(s => s.id === [...selectedIds][0]);
        if (selShape) {
            const hIdx = hitResizeHandle(sp, selShape);
            if (hIdx >= 0) {
                startResize(hIdx, wp);
                return;
            }
        }
    }
    // Hit test shapes top-to-bottom
    let hitShape = null;
    for (let i = shapes.length - 1; i >= 0; i--) {
        if (hit.hitTest(shapes[i], wp)) {
            hitShape = shapes[i];
            break;
        }
    }
    if (hitShape) {
        if (shiftKey)
            state.toggleSelection(hitShape.id, true);
        else if (!selectedIds.has(hitShape.id))
            setSelected([hitShape.id]);
        startMove(wp);
    }
    else {
        if (shiftKey)
            return;
        setSelected([]);
        dragMode = 'rubberband';
        isDragging = true;
        dragStartScreen = sp;
    }
}
function setSelected(ids) { state.setSelectedIds(new Set(ids)); }
// ─── Move ──────────────────────────────────────────────────────────────
function startMove(wp) {
    isDragging = true;
    dragMode = 'move';
    dragStartWorld = wp;
    editSnapshot = history.beginEdit(state.getShapes());
}
function doMove(wp) {
    if (!dragStartWorld || !editSnapshot)
        return;
    const dx = wp.x - dragStartWorld.x, dy = wp.y - dragStartWorld.y;
    for (const s of state.getShapes()) {
        if (state.getSelectedIds().has(s.id))
            moveShape(s, dx, dy);
    }
}
function finishMove() {
    history.commitEdit(editSnapshot, state.getShapes());
    editSnapshot = null;
}
// ─── Resize ────────────────────────────────────────────────────────────
function startResize(handleIdx, wp) {
    isDragging = true;
    dragMode = 'resize';
    resizeHandleIdx = handleIdx;
    dragStartWorld = wp;
    editSnapshot = history.beginEdit(state.getShapes());
}
function doResize(wp, shiftKey) {
    if (!dragStartWorld || !editSnapshot)
        return;
    const shapes = state.getShapes();
    const selIds = state.getSelectedIds();
    if (selIds.size !== 1)
        return;
    const shape = shapes.find(s => s.id === [...selIds][0]);
    if (!shape)
        return;
    resizeShape(shape, resizeHandleIdx, wp.x - dragStartWorld.x, wp.y - dragStartWorld.y, shiftKey);
}
function finishResize() {
    history.commitEdit(editSnapshot, state.getShapes());
    editSnapshot = null;
    resizeHandleIdx = -1;
}
// ─── Rubber Band Selection ─────────────────────────────────────────────
function computeRubberRect(sp) {
    if (!dragStartScreen)
        return null;
    return {
        x: Math.min(dragStartScreen.x, sp.x), y: Math.min(dragStartScreen.y, sp.y),
        w: Math.abs(sp.x - dragStartScreen.x), h: Math.abs(sp.y - dragStartScreen.y),
    };
}
function finishRubberBand(sp) {
    if (!dragStartScreen)
        return;
    const dx = sp.x - dragStartScreen.x, dy = sp.y - dragStartScreen.y;
    if (Math.abs(dx) < 3 && Math.abs(dy) < 3)
        return;
    const rect = computeRubberRect(sp);
    if (!rect || rect.w < 2 || rect.h < 2)
        return;
    const v = state.getViewport();
    const tl = vp.screenToWorld({ x: rect.x, y: rect.y }, v);
    const br = vp.screenToWorld({ x: rect.x + rect.w, y: rect.y + rect.h }, v);
    const selected = [];
    for (const s of state.getShapes()) {
        if (shapeIntersectsRect(s, tl.x, tl.y, br.x - tl.x, br.y - tl.y))
            selected.push(s.id);
    }
    setSelected(selected);
}
function shapeIntersectsRect(shape, rx, ry, rw, rh) {
    const bb = hit.boundingBox([shape]);
    if (!bb)
        return false;
    return !(bb.x + bb.w < rx || bb.x > rx + rw || bb.y + bb.h < ry || bb.y > ry + rh);
}
// ─── Drawing (Pen Tool) ────────────────────────────────────────────────
function startDrawing(wp) {
    isDragging = true;
    dragMode = 'draw';
    creatingShapeType = null;
    currentStrokePoints = [wp];
}
function continueDrawing(wp) {
    if (creatingShapeType) {
        shapeEndWorld = wp;
        return;
    }
    currentStrokePoints.push({ ...wp });
}
function finishDrawing() {
    if (creatingShapeType && dragStartWorld && shapeEndWorld) {
        finishShapeCreation();
    }
    else if (currentStrokePoints.length > 1) {
        const style = state.getDefaultStyle();
        const stroke = {
            id: genId(), type: 'pen', points: [...currentStrokePoints],
            strokeColor: style.strokeColor, fillColor: style.fillColor, strokeWidth: style.strokeWidth,
        };
        const before = history.beginEdit(state.getShapes());
        state.addShape(stroke);
        history.commitEdit(before, state.getShapes());
    }
    currentStrokePoints = [];
}
function finishShapeCreation() {
    if (!creatingShapeType || !dragStartWorld || !shapeEndWorld)
        return;
    const style = state.getDefaultStyle();
    const s = dragStartWorld, e = shapeEndWorld;
    let shape = null;
    switch (creatingShapeType) {
        case 'rectangle':
            if (Math.abs(e.x - s.x) > 2 || Math.abs(e.y - s.y) > 2) {
                shape = { id: genId(), type: 'rectangle', x: s.x, y: s.y, w: e.x - s.x, h: e.y - s.y, strokeColor: style.strokeColor, fillColor: style.fillColor, strokeWidth: style.strokeWidth };
            }
            break;
        case 'ellipse': {
            const rx = Math.abs(e.x - s.x) / 2, ry = Math.abs(e.y - s.y) / 2;
            if (rx > 1 || ry > 1) {
                shape = { id: genId(), type: 'ellipse', cx: (s.x + e.x) / 2, cy: (s.y + e.y) / 2, rx, ry, strokeColor: style.strokeColor, fillColor: style.fillColor, strokeWidth: style.strokeWidth };
            }
            break;
        }
        case 'line': {
            const dist = Math.hypot(e.x - s.x, e.y - s.y);
            if (dist > 2) {
                shape = { id: genId(), type: 'line', x1: s.x, y1: s.y, x2: e.x, y2: e.y, arrowHead: false, strokeColor: style.strokeColor, fillColor: style.fillColor, strokeWidth: style.strokeWidth };
            }
            break;
        }
    }
    if (shape) {
        const before = history.beginEdit(state.getShapes());
        state.addShape(shape);
        history.commitEdit(before, state.getShapes());
    }
    creatingShapeType = null;
    shapeEndWorld = null;
}
// ─── Shape Creation ────────────────────────────────────────────────────
let shapeEndWorld = null;
function startShapeCreation(type, wp) {
    isDragging = true;
    dragMode = 'draw';
    creatingShapeType = type;
    dragStartWorld = wp;
    currentStrokePoints = [];
}
// ─── Eraser ────────────────────────────────────────────────────────────
function startErasing(wp) {
    isDragging = true;
    dragMode = 'erase';
    editSnapshot = history.beginEdit(state.getShapes());
    eraseAt(wp);
}
function eraseAt(wp) {
    const shapes = state.getShapes();
    for (let i = shapes.length - 1; i >= 0; i--) {
        if (hit.hitTest(shapes[i], wp)) {
            history.commitEdit(editSnapshot, shapes.filter((_, idx) => idx !== i));
            editSnapshot = new Map();
            for (const s of state.getShapes())
                editSnapshot.set(s.id, structuredClone(s));
            break;
        }
    }
}
// ─── Text Tool ────────────────────────────────────────────────────────
function placeText(wp) {
    const style = state.getDefaultStyle();
    const textShape = {
        id: genId(), type: 'text', x: wp.x, y: wp.y, text: '',
        strokeColor: style.strokeColor, fillColor: style.fillColor, strokeWidth: style.strokeWidth, fontSize: 20,
    };
    state.addShape(textShape);
    const before = history.beginEdit(state.getShapes());
    history.commitEdit(before, state.getShapes());
    startTextEditing(textShape.id);
}
function startTextEditing(id) { textEditingId = id; emitEvent('text-edit-start', { id }); }
function commitTextEdit(id, text) {
    const before = history.beginEdit(state.getShapes());
    state.updateShape({ id, text });
    history.commitEdit(before, state.getShapes());
    textEditingId = null;
    emitEvent('text-edit-end', {});
}
function cancelTextEdit() {
    if (textEditingId) {
        const before = history.beginEdit(state.getShapes());
        state.removeShape(textEditingId);
        history.commitEdit(before, state.getShapes());
        textEditingId = null;
        emitEvent('text-edit-end', {});
    }
}
function getTextEditingId() { return textEditingId; }
// ─── Keyboard ──────────────────────────────────────────────────────────
function onKeyDown(e) {
    if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
        e.preventDefault();
        doUndo();
        return;
    }
    if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
        e.preventDefault();
        doRedo();
        return;
    }
    if ((e.key === 'Delete' || e.key === 'Backspace') && !textEditingId) {
        const sel = state.getSelectedIds();
        if (sel.size > 0) {
            e.preventDefault();
            const before = history.beginEdit(state.getShapes());
            for (const id of sel)
                state.removeShape(id);
            history.commitEdit(before, state.getShapes());
            setSelected([]);
        }
        return;
    }
    if (!e.ctrlKey && !e.metaKey && !textEditingId) {
        switch (e.key.toLowerCase()) {
            case 'v':
                state.setTool('select');
                emitEvent('tool-change', {});
                break;
            case 'p':
                state.setTool('pen');
                emitEvent('tool-change', {});
                break;
            case 'r':
                state.setTool('rectangle');
                emitEvent('tool-change', {});
                break;
            case 'o':
                state.setTool('ellipse');
                emitEvent('tool-change', {});
                break;
            case 'l':
                state.setTool('line');
                emitEvent('tool-change', {});
                break;
            case 't':
                state.setTool('text');
                emitEvent('tool-change', {});
                break;
            case 'e':
                state.setTool('eraser');
                emitEvent('tool-change', {});
                break;
        }
    }
    if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key) && !textEditingId) {
        const sel = state.getSelectedIds();
        if (sel.size > 0) {
            e.preventDefault();
            const amt = e.shiftKey ? constants_1.SHIFT_NUDGE_AMOUNT : constants_1.NUDGE_AMOUNT;
            let dx = 0, dy = 0;
            switch (e.key) {
                case 'ArrowUp':
                    dy = -amt;
                    break;
                case 'ArrowDown':
                    dy = amt;
                    break;
                case 'ArrowLeft':
                    dx = -amt;
                    break;
                case 'ArrowRight':
                    dx = amt;
                    break;
            }
            const before = history.beginEdit(state.getShapes());
            for (const s of state.getShapes()) {
                if (sel.has(s.id))
                    moveShape(s, dx, dy);
            }
            history.commitEdit(before, state.getShapes());
        }
    }
}
// ─── Helpers ──────────────────────────────────────────────────────────
function moveShape(shape, dx, dy) {
    switch (shape.type) {
        case 'rectangle':
            shape.x += dx;
            shape.y += dy;
            break;
        case 'ellipse':
            shape.cx += dx;
            shape.cy += dy;
            break;
        case 'line':
            shape.x1 += dx;
            shape.y1 += dy;
            shape.x2 += dx;
            shape.y2 += dy;
            break;
        case 'pen':
            for (const p of shape.points) {
                p.x += dx;
                p.y += dy;
            }
            break;
        case 'text':
            shape.x += dx;
            shape.y += dy;
            break;
    }
}
function resizeShape(shape, handle, dx, dy, aspect) {
    switch (shape.type) {
        case 'rectangle':
            resizeRect(shape, handle, dx, dy, aspect);
            break;
        case 'ellipse':
            resizeEllipse(shape, handle, dx, dy, aspect);
            break;
        case 'line':
            resizeLine(shape, handle, dx, dy);
            break;
    }
}
function resizeRect(s, h, dx, dy, asp) {
    const orig = { w: s.w, h: s.h };
    switch (h) {
        case 0:
            s.x += dx;
            s.y += dy;
            s.w -= dx;
            s.h -= dy;
            break;
        case 1:
            s.y += dy;
            s.w += dx;
            s.h -= dy;
            break;
        case 2:
            s.x += dx;
            s.w -= dx;
            s.h += dy;
            break;
        case 3:
            s.w += dx;
            s.h += dy;
            break;
    }
    if (asp && orig.w) {
        const r = Math.abs(orig.w / orig.h);
        if (Math.abs(s.w) > Math.abs(s.h) * r)
            s.h = s.w / r * Math.sign(s.h || 1);
        else
            s.w = s.h * r * Math.sign(s.w || 1);
    }
}
function resizeEllipse(s, h, dx, dy, asp) {
    switch (h) {
        case 0:
            s.rx -= dx;
            s.ry -= dy;
            break;
        case 1:
            s.rx += dx;
            s.ry -= dy;
            break;
        case 2:
            s.rx -= dx;
            s.ry += dy;
            break;
        case 3:
            s.rx += dx;
            s.ry += dy;
            break;
    }
    if (asp && Math.abs(s.rx)) {
        const r = Math.abs(s.rx / s.ry);
        s.ry = s.rx / r * Math.sign(s.ry || 1);
    }
}
function resizeLine(s, h, dx, dy) {
    if (h === 0 || h === 2) {
        s.x1 += dx;
        s.y1 += dy;
    }
    else {
        s.x2 += dx;
        s.y2 += dy;
    }
}
function hitResizeHandle(sp, shape) {
    const box = hit.boundingBox([shape]);
    if (!box)
        return -1;
    const v = state.getViewport();
    const tl = vp.worldToScreen({ x: box.x, y: box.y }, v);
    const br = vp.worldToScreen({ x: box.x + box.w, y: box.y + box.h }, v);
    const hs = constants_1.HANDLE_SIZE * v.zoom;
    const pad = 4 * v.zoom;
    const corners = [
        { x: tl.x - pad, y: tl.y - pad }, { x: br.x + pad, y: tl.y - pad },
        { x: tl.x - pad, y: br.y + pad }, { x: br.x + pad, y: br.y + pad },
    ];
    for (let i = 0; i < corners.length; i++) {
        if (Math.abs(sp.x - corners[i].x) <= hs / 2 && Math.abs(sp.y - corners[i].y) <= hs / 2)
            return i;
    }
    return -1;
}
function genId() { return 's' + Math.random().toString(36).slice(2, 10); }
// ─── Event System ──────────────────────────────────────────────────────
const listeners = new Map();
function on(event, fn) {
    if (!listeners.has(event))
        listeners.set(event, []);
    listeners.get(event).push(fn);
}
function emitEvent(event, data) {
    const fns = listeners.get(event);
    if (fns)
        for (const fn of fns)
            fn(data);
}
// ─── Exports for rendering integration ────────────────────────────────
function getRubberRect() { return rubberRect; }
function getCurrentStrokePoints() { return currentStrokePoints; }
function isDragActive() { return isDragging; }
function getCreatingShapeType() { return creatingShapeType; }
function getShapeStartWorld() { return dragStartWorld; }
function getShapeEndWorld() { return shapeEndWorld; }
function setShapeEndWorld(p) { shapeEndWorld = p; }
// ─── Undo/Redo (direct, no double-commit) ──────────────────────────────
function doUndo() {
    const newShapes = history.undo(state.getShapes());
    state.setShapes(newShapes);
}
function doRedo() {
    const newShapes = history.redo(state.getShapes());
    state.setShapes(newShapes);
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ─── UI: Toolbar, Properties Bar, Zoom Indicator ──────────────────────
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.initUI = initUI;
exports.updateToolButtons = updateToolButtons;
exports.updateZoomDisplay = updateZoomDisplay;
const state = __importStar(require("./state"));
const input = __importStar(require("./input"));
const constants_1 = require("./constants");
let toolbarEl;
let propsBarEl;
let zoomIndicatorEl;
let textEditorEl = null;
const TOOL_DEFS = [
    { id: 'select', label: 'V', icon: '◇', tip: 'Select (V)' },
    { id: 'pen', label: 'P', icon: '✎', tip: 'Pen (P)' },
    { id: 'rectangle', label: 'R', icon: '▭', tip: 'Rectangle (R)' },
    { id: 'ellipse', label: 'O', icon: '◯', tip: 'Ellipse (O)' },
    { id: 'line', label: 'L', icon: '╱', tip: 'Line/Arrow (L)' },
    { id: 'text', label: 'T', icon: 'T', tip: 'Text (T)' },
    { id: 'eraser', label: 'E', icon: '✕', tip: 'Eraser (E)' },
];
function initUI() {
    toolbarEl = document.getElementById('toolbar');
    propsBarEl = document.getElementById('props-bar');
    zoomIndicatorEl = document.getElementById('zoom-indicator');
    buildToolbar();
    buildPropsBar();
    updateZoomDisplay();
    updateToolButtons();
    // Listen for tool changes from keyboard shortcuts
    input.on('tool-change', () => { updateToolButtons(); });
    input.on('text-edit-start', (data) => showTextEditor(data.id));
    input.on('text-edit-end', () => hideTextEditor());
}
// ─── Toolbar ──────────────────────────────────────────────────────────
function buildToolbar() {
    toolbarEl.innerHTML = '';
    // Undo / Redo buttons
    const undoBtn = makeToolButton('↶', 'Undo (Ctrl+Z)', () => input.doUndo());
    const redoBtn = makeToolButton('↷', 'Redo (Ctrl+Shift+Z)', () => input.doRedo());
    toolbarEl.appendChild(undoBtn);
    toolbarEl.appendChild(redoBtn);
    // Separator
    toolbarEl.appendChild(makeSeparator());
    // Tool buttons
    for (const def of TOOL_DEFS) {
        const btn = makeToolButton(def.icon, def.tip, () => {
            state.setTool(def.id);
            updateToolButtons();
        });
        btn.dataset.tool = def.id;
        toolbarEl.appendChild(btn);
    }
    // Separator
    toolbarEl.appendChild(makeSeparator());
    // Reset view button
    const resetBtn = makeToolButton('⊞', 'Reset View', () => {
        state.setViewport({ offsetX: 0, offsetY: 0, zoom: 1 });
    });
    toolbarEl.appendChild(resetBtn);
}
function makeToolButton(icon, title, onClick) {
    const btn = document.createElement('button');
    btn.className = 'tool-btn';
    btn.textContent = icon;
    btn.title = title;
    btn.addEventListener('click', onClick);
    return btn;
}
function makeSeparator() {
    const sep = document.createElement('div');
    sep.className = 'toolbar-sep';
    return sep;
}
function updateToolButtons() {
    const tool = state.getTool();
    toolbarEl.querySelectorAll('[data-tool]').forEach(btn => {
        btn.classList.toggle('active', btn.dataset.tool === tool);
    });
}
// ─── Properties Bar ────────────────────────────────────────────────────
function buildPropsBar() {
    propsBarEl.innerHTML = '';
    // Stroke color
    const strokeLabel = document.createElement('span');
    strokeLabel.className = 'props-label';
    strokeLabel.textContent = 'Stroke';
    propsBarEl.appendChild(strokeLabel);
    for (const c of constants_1.PALETTE) {
        const swatch = makeColorSwatch(c, () => {
            updateStyle('strokeColor', c);
        });
        propsBarEl.appendChild(swatch);
    }
    // Fill color
    const fillLabel = document.createElement('span');
    fillLabel.className = 'props-label';
    fillLabel.textContent = 'Fill';
    propsBarEl.appendChild(fillLabel);
    for (const c of constants_1.FILL_PALETTE) {
        const swatch = makeColorSwatch(c, () => {
            updateStyle('fillColor', c);
        });
        if (c === 'transparent') {
            swatch.classList.add('transparent-swatch');
        }
        propsBarEl.appendChild(swatch);
    }
    // Stroke width
    const widthLabel = document.createElement('span');
    widthLabel.className = 'props-label';
    widthLabel.textContent = 'Width';
    propsBarEl.appendChild(widthLabel);
    for (const w of constants_1.STROKE_WIDTHS) {
        const btn = makeWidthButton(w, () => {
            updateStyle('strokeWidth', w);
        });
        propsBarEl.appendChild(btn);
    }
    updatePropsHighlight();
}
function makeColorSwatch(color, onClick) {
    const btn = document.createElement('button');
    btn.className = 'color-swatch';
    btn.style.backgroundColor = color;
    if (color === 'transparent') {
        // Draw X through transparent swatch
        btn.style.backgroundImage = 'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%)';
        btn.style.backgroundSize = '8px 8px';
        btn.style.backgroundPosition = '0 0, 8px 8px';
    }
    btn.addEventListener('click', onClick);
    return btn;
}
function makeWidthButton(width, onClick) {
    const btn = document.createElement('button');
    btn.className = 'width-btn';
    btn.style.borderBottomWidth = width + 'px';
    btn.addEventListener('click', onClick);
    return btn;
}
function updateStyle(prop, value) {
    const style = state.getDefaultStyle();
    style[prop] = value;
    state.setDefaultStyle(style);
    // Also apply to selected shapes
    for (const s of state.getShapes()) {
        if (state.getSelectedIds().has(s.id)) {
            state.updateShape({ id: s.id, [prop]: value });
        }
    }
    updatePropsHighlight();
}
function updatePropsHighlight() {
    const style = state.getDefaultStyle();
    propsBarEl.querySelectorAll('.color-swatch').forEach(btn => {
        btn.classList.remove('active');
    });
    propsBarEl.querySelectorAll('.width-btn').forEach(btn => {
        btn.classList.remove('active');
    });
}
// ─── Zoom Indicator ────────────────────────────────────────────────────
function updateZoomDisplay() {
    const zoom = state.getViewport().zoom;
    zoomIndicatorEl.textContent = Math.round(zoom * 100) + '%';
}
// ─── Text Editor Overlay ──────────────────────────────────────────────
function showTextEditor(shapeId) {
    if (textEditorEl)
        return; // already showing
    const shape = state.getShapes().find(s => s.id === shapeId);
    if (!shape || shape.type !== 'text')
        return;
    textEditorEl = document.createElement('textarea');
    textEditorEl.className = 'text-editor';
    textEditorEl.value = shape.text;
    textEditorEl.rows = Math.max(1, (shape.text?.length || 0) / 40 + 1);
    const v = state.getViewport();
    const sx = shape.x * v.zoom + v.offsetX;
    const sy = shape.y * v.zoom + v.offsetY;
    textEditorEl.style.left = sx + 'px';
    textEditorEl.style.top = (sy - 24) + 'px';
    textEditorEl.style.fontSize = (shape.fontSize || 20) * v.zoom + 'px';
    textEditorEl.style.color = shape.strokeColor;
    document.body.appendChild(textEditorEl);
    textEditorEl.focus();
    const commit = () => {
        input.commitTextEdit(shapeId, textEditorEl.value);
    };
    textEditorEl.addEventListener('blur', commit);
    textEditorEl.addEventListener('keydown', (e) => {
        if (e.key === 'Escape') {
            e.preventDefault();
            input.cancelTextEdit();
        }
    });
}
function hideTextEditor() {
    if (textEditorEl) {
        textEditorEl.remove();
        textEditorEl = null;
    }
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"typescript","sessionId":"99648545-fd14-4253-9b99-469793600ece","tokensIn":1351993,"tokensOut":35266,"tokensTotal":1387259,"cost":0,"turns":43,"toolCalls":42,"failedToolCalls":4,"timestamp":"2026-07-31T20:31:36.399Z"} -->