@fieldnotes/core 0.38.3 → 0.38.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1308,17 +1308,174 @@ var ShortcutMap = class {
1308
1308
  }
1309
1309
  };
1310
1310
 
1311
- // src/canvas/input-handler.ts
1312
- var ZOOM_SENSITIVITY = 1e-3;
1311
+ // src/canvas/keyboard-handler.ts
1313
1312
  var ZOOM_STEP = 1.2;
1314
- var MIDDLE_BUTTON = 1;
1315
- var LONG_PRESS_MS = 500;
1316
1313
  var NUDGE_DELTAS = {
1317
1314
  "nudge-left": [-1, 0],
1318
1315
  "nudge-right": [1, 0],
1319
1316
  "nudge-up": [0, -1],
1320
1317
  "nudge-down": [0, 1]
1321
1318
  };
1319
+ var KeyboardHandler = class {
1320
+ constructor(deps) {
1321
+ this.deps = deps;
1322
+ this.shortcutMap = new ShortcutMap(deps.shortcuts?.bindings);
1323
+ window.addEventListener("keydown", this.onKeyDown, { signal: deps.abortSignal });
1324
+ window.addEventListener("keyup", this.onKeyUp, { signal: deps.abortSignal });
1325
+ }
1326
+ shortcutMap;
1327
+ get shortcuts() {
1328
+ return this.shortcutMap;
1329
+ }
1330
+ viewportCenter() {
1331
+ const rect = this.deps.element.getBoundingClientRect();
1332
+ return { x: rect.width / 2, y: rect.height / 2 };
1333
+ }
1334
+ zoomByFactor(factor) {
1335
+ this.deps.camera.zoomAt(this.deps.camera.zoom * factor, this.viewportCenter());
1336
+ }
1337
+ zoomToLevel(level) {
1338
+ this.deps.camera.zoomAt(level, this.viewportCenter());
1339
+ }
1340
+ onKeyDown = (e) => {
1341
+ const target = e.target;
1342
+ if (target?.isContentEditable) return;
1343
+ const tag = target?.tagName;
1344
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1345
+ if (!this.isInScope()) return;
1346
+ if (e.key === " ") {
1347
+ this.deps.setSpaceHeld(true);
1348
+ }
1349
+ const action = this.shortcutMap.match(e);
1350
+ if (action !== null) {
1351
+ this.runAction(action, e);
1352
+ }
1353
+ };
1354
+ onKeyUp = (e) => {
1355
+ if (e.key === " ") {
1356
+ this.deps.setSpaceHeld(false);
1357
+ if (this.deps.getActivePointerCount() === 0) {
1358
+ const lastPointerEvent = this.deps.getLastPointerEvent();
1359
+ if (lastPointerEvent) {
1360
+ this.deps.dispatchToolHover(lastPointerEvent);
1361
+ } else {
1362
+ this.deps.getToolContext()?.setCursor?.("default");
1363
+ }
1364
+ }
1365
+ }
1366
+ };
1367
+ runAction(action, e) {
1368
+ switch (action) {
1369
+ case "delete":
1370
+ e?.preventDefault();
1371
+ this.deps.actions.deleteSelected();
1372
+ return;
1373
+ case "deselect":
1374
+ this.deps.actions.deselect();
1375
+ return;
1376
+ case "undo":
1377
+ e?.preventDefault();
1378
+ this.deps.actions.undo();
1379
+ return;
1380
+ case "redo":
1381
+ e?.preventDefault();
1382
+ this.deps.actions.redo();
1383
+ return;
1384
+ case "select-all":
1385
+ e?.preventDefault();
1386
+ this.deps.actions.selectAll();
1387
+ return;
1388
+ case "copy":
1389
+ e?.preventDefault();
1390
+ this.deps.actions.copy();
1391
+ return;
1392
+ case "paste":
1393
+ e?.preventDefault();
1394
+ this.deps.actions.paste();
1395
+ return;
1396
+ case "duplicate":
1397
+ e?.preventDefault();
1398
+ this.deps.actions.duplicate();
1399
+ return;
1400
+ case "z-forward":
1401
+ e?.preventDefault();
1402
+ this.deps.actions.zOrder("forward");
1403
+ return;
1404
+ case "z-backward":
1405
+ e?.preventDefault();
1406
+ this.deps.actions.zOrder("backward");
1407
+ return;
1408
+ case "z-front":
1409
+ e?.preventDefault();
1410
+ this.deps.actions.zOrder("front");
1411
+ return;
1412
+ case "z-back":
1413
+ e?.preventDefault();
1414
+ this.deps.actions.zOrder("back");
1415
+ return;
1416
+ case "zoom-fit":
1417
+ e?.preventDefault();
1418
+ this.deps.actions.zoomToFit();
1419
+ return;
1420
+ case "group":
1421
+ e?.preventDefault();
1422
+ this.deps.actions.group();
1423
+ return;
1424
+ case "ungroup":
1425
+ e?.preventDefault();
1426
+ this.deps.actions.ungroup();
1427
+ return;
1428
+ case "cut":
1429
+ e?.preventDefault();
1430
+ this.deps.actions.cut();
1431
+ return;
1432
+ case "toggle-lock":
1433
+ e?.preventDefault();
1434
+ this.deps.actions.toggleLock();
1435
+ return;
1436
+ case "zoom-in":
1437
+ e?.preventDefault();
1438
+ this.zoomByFactor(ZOOM_STEP);
1439
+ return;
1440
+ case "zoom-out":
1441
+ e?.preventDefault();
1442
+ this.zoomByFactor(1 / ZOOM_STEP);
1443
+ return;
1444
+ case "zoom-reset":
1445
+ e?.preventDefault();
1446
+ this.zoomToLevel(1);
1447
+ return;
1448
+ case "nudge-left":
1449
+ case "nudge-right":
1450
+ case "nudge-up":
1451
+ case "nudge-down": {
1452
+ const delta = NUDGE_DELTAS[action];
1453
+ if (delta && this.deps.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
1454
+ e?.preventDefault();
1455
+ }
1456
+ return;
1457
+ }
1458
+ default:
1459
+ if (action.startsWith("tool:")) {
1460
+ if (this.deps.getIsToolActive()) return;
1461
+ e?.preventDefault();
1462
+ this.deps.getToolContext()?.switchTool?.(action.slice("tool:".length));
1463
+ return;
1464
+ }
1465
+ console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1466
+ }
1467
+ }
1468
+ isInScope() {
1469
+ if (this.deps.scope === "window") return true;
1470
+ const active = document.activeElement;
1471
+ return active === this.deps.element || this.deps.element.contains(active);
1472
+ }
1473
+ };
1474
+
1475
+ // src/canvas/input-handler.ts
1476
+ var ZOOM_SENSITIVITY = 1e-3;
1477
+ var MIDDLE_BUTTON = 1;
1478
+ var LONG_PRESS_MS = 500;
1322
1479
  var InputHandler = class {
1323
1480
  constructor(element, camera, options = {}) {
1324
1481
  this.element = element;
@@ -1340,8 +1497,23 @@ var InputHandler = class {
1340
1497
  getLastPointerWorld: () => this.lastPointerWorld()
1341
1498
  });
1342
1499
  this.openContextMenu = options.openContextMenu;
1343
- this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1344
1500
  this.scope = options.shortcuts?.scope ?? "focus";
1501
+ this.keyboard = new KeyboardHandler({
1502
+ element: this.element,
1503
+ camera: this.camera,
1504
+ actions: this.actions,
1505
+ scope: this.scope,
1506
+ shortcuts: options.shortcuts,
1507
+ abortSignal: this.abortController.signal,
1508
+ getToolContext: () => this.toolContext,
1509
+ getIsToolActive: () => this.isToolActive,
1510
+ getLastPointerEvent: () => this.lastPointerEvent,
1511
+ setSpaceHeld: (v) => {
1512
+ this.spaceHeld = v;
1513
+ },
1514
+ getActivePointerCount: () => this.activePointers.size,
1515
+ dispatchToolHover: (e) => this.dispatchToolHover(e)
1516
+ });
1345
1517
  this.element.style.touchAction = "none";
1346
1518
  if (this.scope === "focus") {
1347
1519
  this.element.tabIndex = 0;
@@ -1367,7 +1539,7 @@ var InputHandler = class {
1367
1539
  longPressStart = null;
1368
1540
  abortController = new AbortController();
1369
1541
  actions;
1370
- shortcutMap;
1542
+ keyboard;
1371
1543
  scope;
1372
1544
  openContextMenu;
1373
1545
  setToolManager(toolManager, toolContext) {
@@ -1378,7 +1550,7 @@ var InputHandler = class {
1378
1550
  this.actions.flushPendingNudge();
1379
1551
  }
1380
1552
  get shortcuts() {
1381
- return this.shortcutMap;
1553
+ return this.keyboard.shortcuts;
1382
1554
  }
1383
1555
  destroy() {
1384
1556
  this.actions.dispose();
@@ -1401,18 +1573,6 @@ var InputHandler = class {
1401
1573
  this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
1402
1574
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
1403
1575
  this.element.addEventListener("contextmenu", this.onContextMenu, opts);
1404
- window.addEventListener("keydown", this.onKeyDown, opts);
1405
- window.addEventListener("keyup", this.onKeyUp, opts);
1406
- }
1407
- viewportCenter() {
1408
- const rect = this.element.getBoundingClientRect();
1409
- return { x: rect.width / 2, y: rect.height / 2 };
1410
- }
1411
- zoomByFactor(factor) {
1412
- this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
1413
- }
1414
- zoomToLevel(level) {
1415
- this.camera.zoomAt(level, this.viewportCenter());
1416
1576
  }
1417
1577
  onWheel = (e) => {
1418
1578
  e.preventDefault();
@@ -1508,132 +1668,8 @@ var InputHandler = class {
1508
1668
  this.deferredDown = null;
1509
1669
  }
1510
1670
  };
1511
- onKeyDown = (e) => {
1512
- const target = e.target;
1513
- if (target?.isContentEditable) return;
1514
- const tag = target?.tagName;
1515
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1516
- if (!this.isInScope()) return;
1517
- if (e.key === " ") {
1518
- this.spaceHeld = true;
1519
- }
1520
- const action = this.shortcutMap.match(e);
1521
- if (action !== null) {
1522
- this.runAction(action, e);
1523
- }
1524
- };
1525
- onKeyUp = (e) => {
1526
- if (e.key === " ") {
1527
- this.spaceHeld = false;
1528
- if (this.activePointers.size === 0) {
1529
- if (this.lastPointerEvent) {
1530
- this.dispatchToolHover(this.lastPointerEvent);
1531
- } else {
1532
- this.toolContext?.setCursor?.("default");
1533
- }
1534
- }
1535
- }
1536
- };
1537
1671
  runAction(action, e) {
1538
- switch (action) {
1539
- case "delete":
1540
- e?.preventDefault();
1541
- this.actions.deleteSelected();
1542
- return;
1543
- case "deselect":
1544
- this.actions.deselect();
1545
- return;
1546
- case "undo":
1547
- e?.preventDefault();
1548
- this.actions.undo();
1549
- return;
1550
- case "redo":
1551
- e?.preventDefault();
1552
- this.actions.redo();
1553
- return;
1554
- case "select-all":
1555
- e?.preventDefault();
1556
- this.actions.selectAll();
1557
- return;
1558
- case "copy":
1559
- e?.preventDefault();
1560
- this.actions.copy();
1561
- return;
1562
- case "paste":
1563
- e?.preventDefault();
1564
- this.actions.paste();
1565
- return;
1566
- case "duplicate":
1567
- e?.preventDefault();
1568
- this.actions.duplicate();
1569
- return;
1570
- case "z-forward":
1571
- e?.preventDefault();
1572
- this.actions.zOrder("forward");
1573
- return;
1574
- case "z-backward":
1575
- e?.preventDefault();
1576
- this.actions.zOrder("backward");
1577
- return;
1578
- case "z-front":
1579
- e?.preventDefault();
1580
- this.actions.zOrder("front");
1581
- return;
1582
- case "z-back":
1583
- e?.preventDefault();
1584
- this.actions.zOrder("back");
1585
- return;
1586
- case "zoom-fit":
1587
- e?.preventDefault();
1588
- this.actions.zoomToFit();
1589
- return;
1590
- case "group":
1591
- e?.preventDefault();
1592
- this.actions.group();
1593
- return;
1594
- case "ungroup":
1595
- e?.preventDefault();
1596
- this.actions.ungroup();
1597
- return;
1598
- case "cut":
1599
- e?.preventDefault();
1600
- this.actions.cut();
1601
- return;
1602
- case "toggle-lock":
1603
- e?.preventDefault();
1604
- this.actions.toggleLock();
1605
- return;
1606
- case "zoom-in":
1607
- e?.preventDefault();
1608
- this.zoomByFactor(ZOOM_STEP);
1609
- return;
1610
- case "zoom-out":
1611
- e?.preventDefault();
1612
- this.zoomByFactor(1 / ZOOM_STEP);
1613
- return;
1614
- case "zoom-reset":
1615
- e?.preventDefault();
1616
- this.zoomToLevel(1);
1617
- return;
1618
- case "nudge-left":
1619
- case "nudge-right":
1620
- case "nudge-up":
1621
- case "nudge-down": {
1622
- const delta = NUDGE_DELTAS[action];
1623
- if (delta && this.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
1624
- e?.preventDefault();
1625
- }
1626
- return;
1627
- }
1628
- default:
1629
- if (action.startsWith("tool:")) {
1630
- if (this.isToolActive) return;
1631
- e?.preventDefault();
1632
- this.toolContext?.switchTool?.(action.slice("tool:".length));
1633
- return;
1634
- }
1635
- console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1636
- }
1672
+ this.keyboard.runAction(action, e);
1637
1673
  }
1638
1674
  hasClipboard() {
1639
1675
  return this.actions.hasClipboard();
@@ -2257,7 +2293,12 @@ var ElementStore = class {
2257
2293
  if (!existing) return;
2258
2294
  this.sortedCache = null;
2259
2295
  this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
2260
- const updated = { ...existing, ...partial, id: existing.id, type: existing.type };
2296
+ const updated = {
2297
+ ...existing,
2298
+ ...partial,
2299
+ id: existing.id,
2300
+ type: existing.type
2301
+ };
2261
2302
  if (updated.type === "stroke" && existing.type === "stroke") {
2262
2303
  transferStrokeRenderData(existing, updated);
2263
2304
  transferStrokeBounds(existing, updated);
@@ -2396,20 +2437,52 @@ var ElementStore = class {
2396
2437
  }
2397
2438
  };
2398
2439
 
2399
- // src/elements/arrow-render-cache.ts
2400
- var cache2 = /* @__PURE__ */ new WeakMap();
2401
- function getArrowRenderGeometry(arrow) {
2402
- const hit = cache2.get(arrow);
2403
- if (hit) return hit;
2404
- const geometry = {
2405
- controlPoint: arrow.bend !== 0 ? arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend) : null,
2406
- tangentStart: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0),
2407
- tangentEnd: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1)
2408
- };
2409
- cache2.set(arrow, geometry);
2410
- return geometry;
2411
- }
2412
-
2440
+ // src/elements/rotate-canvas.ts
2441
+ function withRotation(ctx, el, center2, draw) {
2442
+ const angle = el.rotation ?? 0;
2443
+ if (angle === 0) {
2444
+ draw();
2445
+ return;
2446
+ }
2447
+ ctx.save();
2448
+ ctx.translate(center2.x, center2.y);
2449
+ ctx.rotate(angle);
2450
+ ctx.translate(-center2.x, -center2.y);
2451
+ draw();
2452
+ ctx.restore();
2453
+ }
2454
+
2455
+ // src/elements/renderers/stroke-renderer.ts
2456
+ function renderStroke(ctx, stroke) {
2457
+ if (stroke.points.length < 2) return;
2458
+ ctx.save();
2459
+ if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
2460
+ ctx.translate(stroke.position.x, stroke.position.y);
2461
+ ctx.strokeStyle = stroke.color;
2462
+ ctx.lineCap = "round";
2463
+ ctx.lineJoin = "round";
2464
+ ctx.globalAlpha = stroke.opacity;
2465
+ const data = getStrokeRenderData(stroke);
2466
+ if (data.buckets) {
2467
+ for (const bucket of data.buckets) {
2468
+ ctx.lineWidth = bucket.width;
2469
+ ctx.stroke(bucket.path);
2470
+ }
2471
+ } else {
2472
+ for (let i = 0; i < data.segments.length; i++) {
2473
+ const seg = data.segments[i];
2474
+ const w = data.widths[i];
2475
+ if (!seg || w === void 0) continue;
2476
+ ctx.lineWidth = w;
2477
+ ctx.beginPath();
2478
+ ctx.moveTo(seg.start.x, seg.start.y);
2479
+ ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
2480
+ ctx.stroke();
2481
+ }
2482
+ }
2483
+ ctx.restore();
2484
+ }
2485
+
2413
2486
  // src/elements/shape-geometry.ts
2414
2487
  function lineFromEndpoints(a, b) {
2415
2488
  return {
@@ -2430,6 +2503,74 @@ function lineEndpoints(shape) {
2430
2503
  ];
2431
2504
  }
2432
2505
 
2506
+ // src/elements/renderers/shape-renderer.ts
2507
+ function renderShape(ctx, shape) {
2508
+ ctx.save();
2509
+ if (shape.fillColor !== "none" && shape.shape !== "line") {
2510
+ ctx.fillStyle = shape.fillColor;
2511
+ fillShapePath(ctx, shape);
2512
+ }
2513
+ if (shape.strokeWidth > 0) {
2514
+ ctx.strokeStyle = shape.strokeColor;
2515
+ ctx.lineWidth = shape.strokeWidth;
2516
+ strokeShapePath(ctx, shape);
2517
+ }
2518
+ ctx.restore();
2519
+ }
2520
+ function fillShapePath(ctx, shape) {
2521
+ switch (shape.shape) {
2522
+ case "rectangle":
2523
+ ctx.fillRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
2524
+ break;
2525
+ case "ellipse": {
2526
+ const cx = shape.position.x + shape.size.w / 2;
2527
+ const cy = shape.position.y + shape.size.h / 2;
2528
+ ctx.beginPath();
2529
+ ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
2530
+ ctx.fill();
2531
+ break;
2532
+ }
2533
+ }
2534
+ }
2535
+ function strokeShapePath(ctx, shape) {
2536
+ switch (shape.shape) {
2537
+ case "rectangle":
2538
+ ctx.strokeRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
2539
+ break;
2540
+ case "ellipse": {
2541
+ const cx = shape.position.x + shape.size.w / 2;
2542
+ const cy = shape.position.y + shape.size.h / 2;
2543
+ ctx.beginPath();
2544
+ ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
2545
+ ctx.stroke();
2546
+ break;
2547
+ }
2548
+ case "line": {
2549
+ const [a, b] = lineEndpoints(shape);
2550
+ ctx.lineCap = "round";
2551
+ ctx.beginPath();
2552
+ ctx.moveTo(a.x, a.y);
2553
+ ctx.lineTo(b.x, b.y);
2554
+ ctx.stroke();
2555
+ break;
2556
+ }
2557
+ }
2558
+ }
2559
+
2560
+ // src/elements/arrow-render-cache.ts
2561
+ var cache2 = /* @__PURE__ */ new WeakMap();
2562
+ function getArrowRenderGeometry(arrow) {
2563
+ const hit = cache2.get(arrow);
2564
+ if (hit) return hit;
2565
+ const geometry = {
2566
+ controlPoint: arrow.bend !== 0 ? arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend) : null,
2567
+ tangentStart: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0),
2568
+ tangentEnd: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1)
2569
+ };
2570
+ cache2.set(arrow, geometry);
2571
+ return geometry;
2572
+ }
2573
+
2433
2574
  // src/elements/arrow-binding.ts
2434
2575
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
2435
2576
  function isBindable(element) {
@@ -2502,38 +2643,515 @@ function updateArrowsBoundToElements(movedIds, store) {
2502
2643
  }
2503
2644
  }
2504
2645
  }
2505
- function updateBoundArrow(arrow, store) {
2506
- if (!arrow.fromBinding && !arrow.toBinding) return null;
2507
- const updates = {};
2508
- if (arrow.fromBinding) {
2509
- const el = store.getById(arrow.fromBinding.elementId);
2510
- if (el) {
2511
- const center2 = getElementCenter(el);
2512
- updates.from = center2;
2513
- updates.position = center2;
2514
- }
2646
+ function updateBoundArrow(arrow, store) {
2647
+ if (!arrow.fromBinding && !arrow.toBinding) return null;
2648
+ const updates = {};
2649
+ if (arrow.fromBinding) {
2650
+ const el = store.getById(arrow.fromBinding.elementId);
2651
+ if (el) {
2652
+ const center2 = getElementCenter(el);
2653
+ updates.from = center2;
2654
+ updates.position = center2;
2655
+ }
2656
+ }
2657
+ if (arrow.toBinding) {
2658
+ const el = store.getById(arrow.toBinding.elementId);
2659
+ if (el) {
2660
+ updates.to = getElementCenter(el);
2661
+ }
2662
+ }
2663
+ return Object.keys(updates).length > 0 ? updates : null;
2664
+ }
2665
+
2666
+ // src/elements/renderers/arrow-renderer.ts
2667
+ var ARROWHEAD_LENGTH = 12;
2668
+ var ARROWHEAD_ANGLE = Math.PI / 6;
2669
+ var ARROW_LABEL_FONT_SIZE = 14;
2670
+ function renderArrow(ctx, arrow, store, labelEditingId) {
2671
+ const geometry = getArrowRenderGeometry(arrow);
2672
+ const { visualFrom, visualTo } = getVisualEndpoints(arrow, geometry, store);
2673
+ ctx.save();
2674
+ ctx.strokeStyle = arrow.color;
2675
+ ctx.lineWidth = arrow.width;
2676
+ ctx.lineCap = "round";
2677
+ if (arrow.fromBinding || arrow.toBinding) {
2678
+ ctx.setLineDash([8, 4]);
2679
+ }
2680
+ ctx.beginPath();
2681
+ ctx.moveTo(visualFrom.x, visualFrom.y);
2682
+ if (arrow.bend !== 0) {
2683
+ const cp = geometry.controlPoint;
2684
+ if (cp) {
2685
+ ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
2686
+ }
2687
+ } else {
2688
+ ctx.lineTo(visualTo.x, visualTo.y);
2689
+ }
2690
+ ctx.stroke();
2691
+ renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2692
+ ctx.restore();
2693
+ renderArrowLabel(ctx, arrow, labelEditingId);
2694
+ }
2695
+ function renderArrowLabel(ctx, arrow, labelEditingId) {
2696
+ if (!arrow.label || arrow.label.length === 0) return;
2697
+ if (arrow.id === labelEditingId) return;
2698
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
2699
+ ctx.save();
2700
+ ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
2701
+ const metrics = ctx.measureText(arrow.label);
2702
+ const padX = 6;
2703
+ const padY = 4;
2704
+ const w = metrics.width + padX * 2;
2705
+ const h = ARROW_LABEL_FONT_SIZE + padY * 2;
2706
+ ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
2707
+ ctx.beginPath();
2708
+ ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
2709
+ ctx.fill();
2710
+ ctx.fillStyle = "#1a1a1a";
2711
+ ctx.textAlign = "center";
2712
+ ctx.textBaseline = "middle";
2713
+ ctx.fillText(arrow.label, mid.x, mid.y);
2714
+ ctx.restore();
2715
+ }
2716
+ function renderArrowhead(ctx, arrow, tip, angle) {
2717
+ ctx.beginPath();
2718
+ ctx.moveTo(tip.x, tip.y);
2719
+ ctx.lineTo(
2720
+ tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
2721
+ tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
2722
+ );
2723
+ ctx.lineTo(
2724
+ tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
2725
+ tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
2726
+ );
2727
+ ctx.closePath();
2728
+ ctx.fillStyle = arrow.color;
2729
+ ctx.fill();
2730
+ }
2731
+ function getVisualEndpoints(arrow, geometry, store) {
2732
+ let visualFrom = arrow.from;
2733
+ let visualTo = arrow.to;
2734
+ if (!store) return { visualFrom, visualTo };
2735
+ if (arrow.fromBinding) {
2736
+ const el = store.getById(arrow.fromBinding.elementId);
2737
+ if (el) {
2738
+ const bounds = getElementBounds(el);
2739
+ if (bounds) {
2740
+ const tangentAngle = geometry.tangentStart;
2741
+ const rayTarget = {
2742
+ x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
2743
+ y: arrow.from.y + Math.sin(tangentAngle) * 1e3
2744
+ };
2745
+ visualFrom = getEdgeIntersection(bounds, rayTarget);
2746
+ }
2747
+ }
2748
+ }
2749
+ if (arrow.toBinding) {
2750
+ const el = store.getById(arrow.toBinding.elementId);
2751
+ if (el) {
2752
+ const bounds = getElementBounds(el);
2753
+ if (bounds) {
2754
+ const tangentAngle = geometry.tangentEnd;
2755
+ const rayTarget = {
2756
+ x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
2757
+ y: arrow.to.y - Math.sin(tangentAngle) * 1e3
2758
+ };
2759
+ visualTo = getEdgeIntersection(bounds, rayTarget);
2760
+ }
2761
+ }
2762
+ }
2763
+ return { visualFrom, visualTo };
2764
+ }
2765
+
2766
+ // src/elements/renderers/image-renderer.ts
2767
+ function renderImage(ctx, image, imageCache, onImageLoad, onImageError) {
2768
+ if (imageCache.get(image.src) === "failed") {
2769
+ renderImagePlaceholder(ctx, image);
2770
+ return;
2771
+ }
2772
+ const img = getImage(image.src, imageCache, onImageLoad, onImageError);
2773
+ if (!img) return;
2774
+ ctx.drawImage(
2775
+ img,
2776
+ image.position.x,
2777
+ image.position.y,
2778
+ image.size.w,
2779
+ image.size.h
2780
+ );
2781
+ }
2782
+ function renderImagePlaceholder(ctx, image) {
2783
+ const { x, y } = image.position;
2784
+ const { w, h } = image.size;
2785
+ ctx.save();
2786
+ ctx.fillStyle = "#eeeeee";
2787
+ ctx.fillRect(x, y, w, h);
2788
+ ctx.strokeStyle = "#bdbdbd";
2789
+ ctx.lineWidth = 1;
2790
+ ctx.strokeRect(x, y, w, h);
2791
+ const glyph = Math.min(24, w / 2, h / 2);
2792
+ const cx = x + w / 2;
2793
+ const cy = y + h / 2;
2794
+ ctx.strokeStyle = "#9e9e9e";
2795
+ ctx.lineWidth = 2;
2796
+ ctx.beginPath();
2797
+ ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
2798
+ ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
2799
+ ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
2800
+ ctx.stroke();
2801
+ ctx.restore();
2802
+ }
2803
+ function getImage(src, imageCache, onImageLoad, onImageError) {
2804
+ const cached = imageCache.get(src);
2805
+ if (cached) {
2806
+ if (cached === "failed") return null;
2807
+ if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
2808
+ return cached;
2809
+ }
2810
+ const img = new Image();
2811
+ img.src = src;
2812
+ imageCache.set(src, img);
2813
+ img.onload = () => {
2814
+ onImageLoad?.();
2815
+ if (typeof createImageBitmap !== "undefined") {
2816
+ createImageBitmap(img).then((bitmap) => {
2817
+ imageCache.set(src, bitmap);
2818
+ onImageLoad?.();
2819
+ }).catch(() => {
2820
+ });
2821
+ }
2822
+ };
2823
+ img.onerror = (event) => {
2824
+ imageCache.set(src, "failed");
2825
+ onImageError?.(src, event);
2826
+ onImageLoad?.();
2827
+ };
2828
+ return null;
2829
+ }
2830
+
2831
+ // src/elements/hex-fill.ts
2832
+ function offsetToCube(col, row, orientation) {
2833
+ if (orientation === "pointy") {
2834
+ return { q: col - (row - (row & 1)) / 2, r: row };
2835
+ }
2836
+ return { q: col, r: row - (col - (col & 1)) / 2 };
2837
+ }
2838
+ function cubeToOffset(q, r, orientation) {
2839
+ if (orientation === "pointy") {
2840
+ return { col: q + (r - (r & 1)) / 2, row: r };
2841
+ }
2842
+ return { col: q, row: r + (q - (q & 1)) / 2 };
2843
+ }
2844
+ function offsetToPixel(col, row, cellSize, orientation) {
2845
+ if (orientation === "pointy") {
2846
+ const hexW = Math.sqrt(3) * cellSize;
2847
+ const rowH = 1.5 * cellSize;
2848
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2849
+ return { x: col * hexW + offsetX, y: row * rowH };
2850
+ }
2851
+ const hexH = Math.sqrt(3) * cellSize;
2852
+ const colW = 1.5 * cellSize;
2853
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2854
+ return { x: col * colW, y: row * hexH + offsetY };
2855
+ }
2856
+ function pixelToOffset(x, y, cellSize, orientation) {
2857
+ if (orientation === "pointy") {
2858
+ const hexW = Math.sqrt(3) * cellSize;
2859
+ const rowH = 1.5 * cellSize;
2860
+ const row = Math.round(y / rowH);
2861
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2862
+ return { col: Math.round((x - offsetX) / hexW), row };
2863
+ }
2864
+ const hexH = Math.sqrt(3) * cellSize;
2865
+ const colW = 1.5 * cellSize;
2866
+ const col = Math.round(x / colW);
2867
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2868
+ return { col, row: Math.round((y - offsetY) / hexH) };
2869
+ }
2870
+ function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
2871
+ const cells = [];
2872
+ for (let dq = -n; dq <= n; dq++) {
2873
+ const rMin = Math.max(-n, -dq - n);
2874
+ const rMax = Math.min(n, -dq + n);
2875
+ for (let dr = rMin; dr <= rMax; dr++) {
2876
+ const absQ = centerQ + dq;
2877
+ const absR = centerR + dr;
2878
+ const off = cubeToOffset(absQ, absR, orientation);
2879
+ cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
2880
+ }
2881
+ }
2882
+ return cells;
2883
+ }
2884
+ function getHexDistance(a, b, cellSize, orientation) {
2885
+ const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
2886
+ const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
2887
+ const cubeA = offsetToCube(offA.col, offA.row, orientation);
2888
+ const cubeB = offsetToCube(offB.col, offB.row, orientation);
2889
+ const dq = cubeA.q - cubeB.q;
2890
+ const dr = cubeA.r - cubeB.r;
2891
+ const ds = -dq - dr;
2892
+ return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2893
+ }
2894
+ function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2895
+ const n = Math.round(radiusCells);
2896
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2897
+ const cube = offsetToCube(off.col, off.row, orientation);
2898
+ if (n <= 0) {
2899
+ return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2900
+ }
2901
+ return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2902
+ }
2903
+ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2904
+ const n = Math.round(radiusCells);
2905
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2906
+ const cube = offsetToCube(off.col, off.row, orientation);
2907
+ const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2908
+ if (n <= 0) return [centerPixel];
2909
+ const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2910
+ const step = Math.PI / 3;
2911
+ const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2912
+ const halfAngle = Math.PI / 6 + 1e-6;
2913
+ const cells = [centerPixel];
2914
+ for (let dq = -n; dq <= n; dq++) {
2915
+ const rMin = Math.max(-n, -dq - n);
2916
+ const rMax = Math.min(n, -dq + n);
2917
+ for (let dr = rMin; dr <= rMax; dr++) {
2918
+ if (dq === 0 && dr === 0) continue;
2919
+ const absQ = cube.q + dq;
2920
+ const absR = cube.r + dr;
2921
+ const pixel = offsetToPixel(
2922
+ cubeToOffset(absQ, absR, orientation).col,
2923
+ cubeToOffset(absQ, absR, orientation).row,
2924
+ cellSize,
2925
+ orientation
2926
+ );
2927
+ const dx = pixel.x - centerPixel.x;
2928
+ const dy = pixel.y - centerPixel.y;
2929
+ let diff = Math.atan2(dy, dx) - snappedAngle;
2930
+ if (diff > Math.PI) diff -= 2 * Math.PI;
2931
+ if (diff < -Math.PI) diff += 2 * Math.PI;
2932
+ if (Math.abs(diff) <= halfAngle) {
2933
+ cells.push(pixel);
2934
+ }
2935
+ }
2936
+ }
2937
+ return cells;
2938
+ }
2939
+ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2940
+ const n = Math.round(radiusCells);
2941
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2942
+ const cube = offsetToCube(off.col, off.row, orientation);
2943
+ const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2944
+ if (n <= 0) return [centerPixel];
2945
+ const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2946
+ const step = Math.PI / 3;
2947
+ const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2948
+ const cos = Math.cos(snappedAngle);
2949
+ const sin = Math.sin(snappedAngle);
2950
+ const snapUnit = Math.sqrt(3) * cellSize;
2951
+ const lineLength = n * snapUnit;
2952
+ const halfWidth = snapUnit * 0.5 + 1e-6;
2953
+ const cells = [];
2954
+ for (let dq = -n; dq <= n; dq++) {
2955
+ const rMin = Math.max(-n, -dq - n);
2956
+ const rMax = Math.min(n, -dq + n);
2957
+ for (let dr = rMin; dr <= rMax; dr++) {
2958
+ const absQ = cube.q + dq;
2959
+ const absR = cube.r + dr;
2960
+ const pixel = offsetToPixel(
2961
+ cubeToOffset(absQ, absR, orientation).col,
2962
+ cubeToOffset(absQ, absR, orientation).row,
2963
+ cellSize,
2964
+ orientation
2965
+ );
2966
+ const dx = pixel.x - centerPixel.x;
2967
+ const dy = pixel.y - centerPixel.y;
2968
+ const along = dx * cos + dy * sin;
2969
+ const perp = Math.abs(-dx * sin + dy * cos);
2970
+ if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
2971
+ cells.push(pixel);
2972
+ }
2973
+ }
2974
+ }
2975
+ return cells;
2976
+ }
2977
+ function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2978
+ const n = Math.round(radiusCells);
2979
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2980
+ const cube = offsetToCube(off.col, off.row, orientation);
2981
+ const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2982
+ if (n <= 0) return [centerPixel];
2983
+ const snapUnit = Math.sqrt(3) * cellSize;
2984
+ const halfSide = n * snapUnit / 2;
2985
+ const cells = [];
2986
+ for (let dq = -n; dq <= n; dq++) {
2987
+ const rMin = Math.max(-n, -dq - n);
2988
+ const rMax = Math.min(n, -dq + n);
2989
+ for (let dr = rMin; dr <= rMax; dr++) {
2990
+ const absQ = cube.q + dq;
2991
+ const absR = cube.r + dr;
2992
+ const pixel = offsetToPixel(
2993
+ cubeToOffset(absQ, absR, orientation).col,
2994
+ cubeToOffset(absQ, absR, orientation).row,
2995
+ cellSize,
2996
+ orientation
2997
+ );
2998
+ if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
2999
+ cells.push(pixel);
3000
+ }
3001
+ }
3002
+ }
3003
+ return cells;
3004
+ }
3005
+ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
3006
+ const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
3007
+ ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
3008
+ for (let i = 1; i < 6; i++) {
3009
+ const a = angleOffset + Math.PI / 3 * i;
3010
+ ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
3011
+ }
3012
+ ctx.closePath();
3013
+ }
3014
+
3015
+ // src/elements/renderers/template-renderer.ts
3016
+ function renderTemplate(ctx, template, store) {
3017
+ const grid = store?.getElementsByType("grid")[0];
3018
+ if (grid && grid.gridType === "hex") {
3019
+ renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
3020
+ return;
3021
+ }
3022
+ renderGeometricTemplate(ctx, template);
3023
+ }
3024
+ function renderGeometricTemplate(ctx, template) {
3025
+ const { x: cx, y: cy } = template.position;
3026
+ const r = template.radius;
3027
+ ctx.save();
3028
+ ctx.globalAlpha = template.opacity;
3029
+ ctx.fillStyle = template.fillColor;
3030
+ ctx.strokeStyle = template.strokeColor;
3031
+ ctx.lineWidth = template.strokeWidth;
3032
+ switch (template.templateShape) {
3033
+ case "circle":
3034
+ ctx.beginPath();
3035
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
3036
+ ctx.fill();
3037
+ ctx.stroke();
3038
+ if (template.radiusFeet != null && template.radiusFeet > 0) {
3039
+ renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
3040
+ }
3041
+ break;
3042
+ case "square":
3043
+ ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
3044
+ ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
3045
+ break;
3046
+ case "cone": {
3047
+ const halfAngle = Math.atan(0.5);
3048
+ ctx.beginPath();
3049
+ ctx.moveTo(cx, cy);
3050
+ ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
3051
+ ctx.closePath();
3052
+ ctx.fill();
3053
+ ctx.stroke();
3054
+ break;
3055
+ }
3056
+ case "line": {
3057
+ const halfW = r / 12;
3058
+ const cos = Math.cos(template.angle);
3059
+ const sin = Math.sin(template.angle);
3060
+ const perpX = -sin * halfW;
3061
+ const perpY = cos * halfW;
3062
+ ctx.beginPath();
3063
+ ctx.moveTo(cx + perpX, cy + perpY);
3064
+ ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
3065
+ ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
3066
+ ctx.lineTo(cx - perpX, cy - perpY);
3067
+ ctx.closePath();
3068
+ ctx.fill();
3069
+ ctx.stroke();
3070
+ break;
3071
+ }
3072
+ }
3073
+ ctx.restore();
3074
+ }
3075
+ function renderHexTemplate(ctx, template, cellSize, orientation) {
3076
+ const snapUnit = Math.sqrt(3) * cellSize;
3077
+ const radiusCells = template.radius / snapUnit;
3078
+ const center2 = template.position;
3079
+ let cells;
3080
+ switch (template.templateShape) {
3081
+ case "circle":
3082
+ cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
3083
+ break;
3084
+ case "cone":
3085
+ cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
3086
+ break;
3087
+ case "line":
3088
+ cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
3089
+ break;
3090
+ case "square":
3091
+ cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3092
+ break;
2515
3093
  }
2516
- if (arrow.toBinding) {
2517
- const el = store.getById(arrow.toBinding.elementId);
2518
- if (el) {
2519
- updates.to = getElementCenter(el);
2520
- }
3094
+ ctx.save();
3095
+ ctx.globalAlpha = template.opacity;
3096
+ ctx.beginPath();
3097
+ for (const cell of cells) {
3098
+ drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
2521
3099
  }
2522
- return Object.keys(updates).length > 0 ? updates : null;
2523
- }
2524
-
2525
- // src/elements/rotate-canvas.ts
2526
- function withRotation(ctx, el, center2, draw) {
2527
- const angle = el.rotation ?? 0;
2528
- if (angle === 0) {
2529
- draw();
2530
- return;
3100
+ ctx.fillStyle = template.fillColor;
3101
+ ctx.fill();
3102
+ ctx.beginPath();
3103
+ for (const cell of cells) {
3104
+ drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3105
+ }
3106
+ ctx.strokeStyle = template.strokeColor;
3107
+ ctx.lineWidth = template.strokeWidth;
3108
+ ctx.stroke();
3109
+ {
3110
+ ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
3111
+ ctx.beginPath();
3112
+ drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
3113
+ ctx.fillStyle = template.strokeColor;
3114
+ ctx.fill();
3115
+ ctx.strokeStyle = template.strokeColor;
3116
+ ctx.lineWidth = template.strokeWidth;
3117
+ ctx.stroke();
3118
+ }
3119
+ if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3120
+ const r = template.radius;
3121
+ renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
2531
3122
  }
3123
+ ctx.restore();
3124
+ }
3125
+ function renderRadiusMarker(ctx, cx, cy, r, feet) {
3126
+ const markerColor = ctx.strokeStyle;
2532
3127
  ctx.save();
2533
- ctx.translate(center2.x, center2.y);
2534
- ctx.rotate(angle);
2535
- ctx.translate(-center2.x, -center2.y);
2536
- draw();
3128
+ ctx.globalAlpha = 1;
3129
+ ctx.beginPath();
3130
+ ctx.setLineDash([4, 4]);
3131
+ ctx.strokeStyle = markerColor;
3132
+ ctx.lineWidth = 1.5;
3133
+ ctx.moveTo(cx, cy);
3134
+ ctx.lineTo(cx + r, cy);
3135
+ ctx.stroke();
3136
+ ctx.setLineDash([]);
3137
+ const label = `${Math.round(feet)} ft`;
3138
+ const fontSize = Math.max(10, Math.min(14, r * 0.15));
3139
+ ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
3140
+ ctx.textAlign = "center";
3141
+ ctx.textBaseline = "bottom";
3142
+ const textX = cx + r / 2;
3143
+ const textY = cy - 4;
3144
+ const metrics = ctx.measureText(label);
3145
+ const padX = 4;
3146
+ const padY = 2;
3147
+ const textW = metrics.width + padX * 2;
3148
+ const textH = fontSize + padY * 2;
3149
+ ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3150
+ ctx.beginPath();
3151
+ ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
3152
+ ctx.fill();
3153
+ ctx.fillStyle = markerColor;
3154
+ ctx.fillText(label, textX, textY - padY);
2537
3155
  ctx.restore();
2538
3156
  }
2539
3157
 
@@ -2687,245 +3305,58 @@ function createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opac
2687
3305
  tc.globalAlpha = opacity;
2688
3306
  tc.beginPath();
2689
3307
  if (orientation === "pointy") {
2690
- const hexW = tileW;
2691
- const rowH = 1.5 * cellSize;
2692
- for (let row = -1; row <= 3; row++) {
2693
- const offX = row % 2 !== 0 ? hexW / 2 : 0;
2694
- for (let col = -1; col <= 1; col++) {
2695
- const cx = col * hexW + offX;
2696
- const cy = row * rowH;
2697
- tc.moveTo(cx + ox0, cy + oy0);
2698
- tc.lineTo(cx + ox1, cy + oy1);
2699
- tc.lineTo(cx + ox2, cy + oy2);
2700
- tc.lineTo(cx + ox3, cy + oy3);
2701
- tc.lineTo(cx + ox4, cy + oy4);
2702
- tc.lineTo(cx + ox5, cy + oy5);
2703
- tc.closePath();
2704
- }
2705
- }
2706
- } else {
2707
- const hexH = tileH;
2708
- const colW = 1.5 * cellSize;
2709
- for (let col = -1; col <= 3; col++) {
2710
- const offY = col % 2 !== 0 ? hexH / 2 : 0;
2711
- for (let row = -1; row <= 1; row++) {
2712
- const cx = col * colW;
2713
- const cy = row * hexH + offY;
2714
- tc.moveTo(cx + ox0, cy + oy0);
2715
- tc.lineTo(cx + ox1, cy + oy1);
2716
- tc.lineTo(cx + ox2, cy + oy2);
2717
- tc.lineTo(cx + ox3, cy + oy3);
2718
- tc.lineTo(cx + ox4, cy + oy4);
2719
- tc.lineTo(cx + ox5, cy + oy5);
2720
- tc.closePath();
2721
- }
2722
- }
2723
- }
2724
- tc.stroke();
2725
- return { canvas, tileW, tileH };
2726
- }
2727
- function renderHexGridTiled(ctx, bounds, cellSize, tile) {
2728
- const { tileW, tileH } = tile;
2729
- const startCol = Math.floor(bounds.minX / tileW) - 1;
2730
- const endCol = Math.ceil(bounds.maxX / tileW) + 1;
2731
- const startRow = Math.floor(bounds.minY / tileH) - 1;
2732
- const endRow = Math.ceil(bounds.maxY / tileH) + 1;
2733
- for (let row = startRow; row <= endRow; row++) {
2734
- for (let col = startCol; col <= endCol; col++) {
2735
- ctx.drawImage(tile.canvas, col * tileW, row * tileH, tileW, tileH);
2736
- }
2737
- }
2738
- }
2739
-
2740
- // src/elements/hex-fill.ts
2741
- function offsetToCube(col, row, orientation) {
2742
- if (orientation === "pointy") {
2743
- return { q: col - (row - (row & 1)) / 2, r: row };
2744
- }
2745
- return { q: col, r: row - (col - (col & 1)) / 2 };
2746
- }
2747
- function cubeToOffset(q, r, orientation) {
2748
- if (orientation === "pointy") {
2749
- return { col: q + (r - (r & 1)) / 2, row: r };
2750
- }
2751
- return { col: q, row: r + (q - (q & 1)) / 2 };
2752
- }
2753
- function offsetToPixel(col, row, cellSize, orientation) {
2754
- if (orientation === "pointy") {
2755
- const hexW = Math.sqrt(3) * cellSize;
2756
- const rowH = 1.5 * cellSize;
2757
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2758
- return { x: col * hexW + offsetX, y: row * rowH };
2759
- }
2760
- const hexH = Math.sqrt(3) * cellSize;
2761
- const colW = 1.5 * cellSize;
2762
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2763
- return { x: col * colW, y: row * hexH + offsetY };
2764
- }
2765
- function pixelToOffset(x, y, cellSize, orientation) {
2766
- if (orientation === "pointy") {
2767
- const hexW = Math.sqrt(3) * cellSize;
2768
- const rowH = 1.5 * cellSize;
2769
- const row = Math.round(y / rowH);
2770
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2771
- return { col: Math.round((x - offsetX) / hexW), row };
2772
- }
2773
- const hexH = Math.sqrt(3) * cellSize;
2774
- const colW = 1.5 * cellSize;
2775
- const col = Math.round(x / colW);
2776
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2777
- return { col, row: Math.round((y - offsetY) / hexH) };
2778
- }
2779
- function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
2780
- const cells = [];
2781
- for (let dq = -n; dq <= n; dq++) {
2782
- const rMin = Math.max(-n, -dq - n);
2783
- const rMax = Math.min(n, -dq + n);
2784
- for (let dr = rMin; dr <= rMax; dr++) {
2785
- const absQ = centerQ + dq;
2786
- const absR = centerR + dr;
2787
- const off = cubeToOffset(absQ, absR, orientation);
2788
- cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
2789
- }
2790
- }
2791
- return cells;
2792
- }
2793
- function getHexDistance(a, b, cellSize, orientation) {
2794
- const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
2795
- const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
2796
- const cubeA = offsetToCube(offA.col, offA.row, orientation);
2797
- const cubeB = offsetToCube(offB.col, offB.row, orientation);
2798
- const dq = cubeA.q - cubeB.q;
2799
- const dr = cubeA.r - cubeB.r;
2800
- const ds = -dq - dr;
2801
- return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2802
- }
2803
- function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2804
- const n = Math.round(radiusCells);
2805
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2806
- const cube = offsetToCube(off.col, off.row, orientation);
2807
- if (n <= 0) {
2808
- return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2809
- }
2810
- return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2811
- }
2812
- function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2813
- const n = Math.round(radiusCells);
2814
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2815
- const cube = offsetToCube(off.col, off.row, orientation);
2816
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2817
- if (n <= 0) return [centerPixel];
2818
- const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2819
- const step = Math.PI / 3;
2820
- const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2821
- const halfAngle = Math.PI / 6 + 1e-6;
2822
- const cells = [centerPixel];
2823
- for (let dq = -n; dq <= n; dq++) {
2824
- const rMin = Math.max(-n, -dq - n);
2825
- const rMax = Math.min(n, -dq + n);
2826
- for (let dr = rMin; dr <= rMax; dr++) {
2827
- if (dq === 0 && dr === 0) continue;
2828
- const absQ = cube.q + dq;
2829
- const absR = cube.r + dr;
2830
- const pixel = offsetToPixel(
2831
- cubeToOffset(absQ, absR, orientation).col,
2832
- cubeToOffset(absQ, absR, orientation).row,
2833
- cellSize,
2834
- orientation
2835
- );
2836
- const dx = pixel.x - centerPixel.x;
2837
- const dy = pixel.y - centerPixel.y;
2838
- let diff = Math.atan2(dy, dx) - snappedAngle;
2839
- if (diff > Math.PI) diff -= 2 * Math.PI;
2840
- if (diff < -Math.PI) diff += 2 * Math.PI;
2841
- if (Math.abs(diff) <= halfAngle) {
2842
- cells.push(pixel);
2843
- }
2844
- }
2845
- }
2846
- return cells;
2847
- }
2848
- function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2849
- const n = Math.round(radiusCells);
2850
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2851
- const cube = offsetToCube(off.col, off.row, orientation);
2852
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2853
- if (n <= 0) return [centerPixel];
2854
- const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2855
- const step = Math.PI / 3;
2856
- const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2857
- const cos = Math.cos(snappedAngle);
2858
- const sin = Math.sin(snappedAngle);
2859
- const snapUnit = Math.sqrt(3) * cellSize;
2860
- const lineLength = n * snapUnit;
2861
- const halfWidth = snapUnit * 0.5 + 1e-6;
2862
- const cells = [];
2863
- for (let dq = -n; dq <= n; dq++) {
2864
- const rMin = Math.max(-n, -dq - n);
2865
- const rMax = Math.min(n, -dq + n);
2866
- for (let dr = rMin; dr <= rMax; dr++) {
2867
- const absQ = cube.q + dq;
2868
- const absR = cube.r + dr;
2869
- const pixel = offsetToPixel(
2870
- cubeToOffset(absQ, absR, orientation).col,
2871
- cubeToOffset(absQ, absR, orientation).row,
2872
- cellSize,
2873
- orientation
2874
- );
2875
- const dx = pixel.x - centerPixel.x;
2876
- const dy = pixel.y - centerPixel.y;
2877
- const along = dx * cos + dy * sin;
2878
- const perp = Math.abs(-dx * sin + dy * cos);
2879
- if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
2880
- cells.push(pixel);
3308
+ const hexW = tileW;
3309
+ const rowH = 1.5 * cellSize;
3310
+ for (let row = -1; row <= 3; row++) {
3311
+ const offX = row % 2 !== 0 ? hexW / 2 : 0;
3312
+ for (let col = -1; col <= 1; col++) {
3313
+ const cx = col * hexW + offX;
3314
+ const cy = row * rowH;
3315
+ tc.moveTo(cx + ox0, cy + oy0);
3316
+ tc.lineTo(cx + ox1, cy + oy1);
3317
+ tc.lineTo(cx + ox2, cy + oy2);
3318
+ tc.lineTo(cx + ox3, cy + oy3);
3319
+ tc.lineTo(cx + ox4, cy + oy4);
3320
+ tc.lineTo(cx + ox5, cy + oy5);
3321
+ tc.closePath();
2881
3322
  }
2882
3323
  }
2883
- }
2884
- return cells;
2885
- }
2886
- function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2887
- const n = Math.round(radiusCells);
2888
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2889
- const cube = offsetToCube(off.col, off.row, orientation);
2890
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2891
- if (n <= 0) return [centerPixel];
2892
- const snapUnit = Math.sqrt(3) * cellSize;
2893
- const halfSide = n * snapUnit / 2;
2894
- const cells = [];
2895
- for (let dq = -n; dq <= n; dq++) {
2896
- const rMin = Math.max(-n, -dq - n);
2897
- const rMax = Math.min(n, -dq + n);
2898
- for (let dr = rMin; dr <= rMax; dr++) {
2899
- const absQ = cube.q + dq;
2900
- const absR = cube.r + dr;
2901
- const pixel = offsetToPixel(
2902
- cubeToOffset(absQ, absR, orientation).col,
2903
- cubeToOffset(absQ, absR, orientation).row,
2904
- cellSize,
2905
- orientation
2906
- );
2907
- if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
2908
- cells.push(pixel);
3324
+ } else {
3325
+ const hexH = tileH;
3326
+ const colW = 1.5 * cellSize;
3327
+ for (let col = -1; col <= 3; col++) {
3328
+ const offY = col % 2 !== 0 ? hexH / 2 : 0;
3329
+ for (let row = -1; row <= 1; row++) {
3330
+ const cx = col * colW;
3331
+ const cy = row * hexH + offY;
3332
+ tc.moveTo(cx + ox0, cy + oy0);
3333
+ tc.lineTo(cx + ox1, cy + oy1);
3334
+ tc.lineTo(cx + ox2, cy + oy2);
3335
+ tc.lineTo(cx + ox3, cy + oy3);
3336
+ tc.lineTo(cx + ox4, cy + oy4);
3337
+ tc.lineTo(cx + ox5, cy + oy5);
3338
+ tc.closePath();
2909
3339
  }
2910
3340
  }
2911
3341
  }
2912
- return cells;
3342
+ tc.stroke();
3343
+ return { canvas, tileW, tileH };
2913
3344
  }
2914
- function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2915
- const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2916
- ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
2917
- for (let i = 1; i < 6; i++) {
2918
- const a = angleOffset + Math.PI / 3 * i;
2919
- ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
3345
+ function renderHexGridTiled(ctx, bounds, cellSize, tile) {
3346
+ const { tileW, tileH } = tile;
3347
+ const startCol = Math.floor(bounds.minX / tileW) - 1;
3348
+ const endCol = Math.ceil(bounds.maxX / tileW) + 1;
3349
+ const startRow = Math.floor(bounds.minY / tileH) - 1;
3350
+ const endRow = Math.ceil(bounds.maxY / tileH) + 1;
3351
+ for (let row = startRow; row <= endRow; row++) {
3352
+ for (let col = startCol; col <= endCol; col++) {
3353
+ ctx.drawImage(tile.canvas, col * tileW, row * tileH, tileW, tileH);
3354
+ }
2920
3355
  }
2921
- ctx.closePath();
2922
3356
  }
2923
3357
 
2924
3358
  // src/elements/element-renderer.ts
2925
3359
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
2926
- var ARROWHEAD_LENGTH = 12;
2927
- var ARROWHEAD_ANGLE = Math.PI / 6;
2928
- var ARROW_LABEL_FONT_SIZE = 14;
2929
3360
  var ElementRenderer = class {
2930
3361
  store = null;
2931
3362
  imageCache = /* @__PURE__ */ new Map();
@@ -2966,206 +3397,35 @@ var ElementRenderer = class {
2966
3397
  case "stroke": {
2967
3398
  const b = getElementBounds(element);
2968
3399
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2969
- withRotation(ctx, element, c, () => this.renderStroke(ctx, element));
3400
+ withRotation(ctx, element, c, () => renderStroke(ctx, element));
2970
3401
  break;
2971
3402
  }
2972
3403
  case "arrow":
2973
- this.renderArrow(ctx, element);
3404
+ renderArrow(ctx, element, this.store, this.labelEditingId);
2974
3405
  break;
2975
3406
  case "shape": {
2976
3407
  const b = getElementBounds(element);
2977
3408
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2978
- withRotation(ctx, element, c, () => this.renderShape(ctx, element));
3409
+ withRotation(ctx, element, c, () => renderShape(ctx, element));
2979
3410
  break;
2980
3411
  }
2981
3412
  case "image": {
2982
3413
  const b = getElementBounds(element);
2983
3414
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2984
- withRotation(ctx, element, c, () => this.renderImage(ctx, element));
3415
+ withRotation(
3416
+ ctx,
3417
+ element,
3418
+ c,
3419
+ () => renderImage(ctx, element, this.imageCache, this.onImageLoad, this.onImageError)
3420
+ );
2985
3421
  break;
2986
3422
  }
2987
3423
  case "grid":
2988
3424
  this.renderGrid(ctx, element);
2989
3425
  break;
2990
3426
  case "template":
2991
- this.renderTemplate(ctx, element);
2992
- break;
2993
- }
2994
- }
2995
- renderStroke(ctx, stroke) {
2996
- if (stroke.points.length < 2) return;
2997
- ctx.save();
2998
- if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
2999
- ctx.translate(stroke.position.x, stroke.position.y);
3000
- ctx.strokeStyle = stroke.color;
3001
- ctx.lineCap = "round";
3002
- ctx.lineJoin = "round";
3003
- ctx.globalAlpha = stroke.opacity;
3004
- const data = getStrokeRenderData(stroke);
3005
- if (data.buckets) {
3006
- for (const bucket of data.buckets) {
3007
- ctx.lineWidth = bucket.width;
3008
- ctx.stroke(bucket.path);
3009
- }
3010
- } else {
3011
- for (let i = 0; i < data.segments.length; i++) {
3012
- const seg = data.segments[i];
3013
- const w = data.widths[i];
3014
- if (!seg || w === void 0) continue;
3015
- ctx.lineWidth = w;
3016
- ctx.beginPath();
3017
- ctx.moveTo(seg.start.x, seg.start.y);
3018
- ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
3019
- ctx.stroke();
3020
- }
3021
- }
3022
- ctx.restore();
3023
- }
3024
- renderArrow(ctx, arrow) {
3025
- const geometry = getArrowRenderGeometry(arrow);
3026
- const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
3027
- ctx.save();
3028
- ctx.strokeStyle = arrow.color;
3029
- ctx.lineWidth = arrow.width;
3030
- ctx.lineCap = "round";
3031
- if (arrow.fromBinding || arrow.toBinding) {
3032
- ctx.setLineDash([8, 4]);
3033
- }
3034
- ctx.beginPath();
3035
- ctx.moveTo(visualFrom.x, visualFrom.y);
3036
- if (arrow.bend !== 0) {
3037
- const cp = geometry.controlPoint;
3038
- if (cp) {
3039
- ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
3040
- }
3041
- } else {
3042
- ctx.lineTo(visualTo.x, visualTo.y);
3043
- }
3044
- ctx.stroke();
3045
- this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
3046
- ctx.restore();
3047
- this.renderArrowLabel(ctx, arrow);
3048
- }
3049
- renderArrowLabel(ctx, arrow) {
3050
- if (!arrow.label || arrow.label.length === 0) return;
3051
- if (arrow.id === this.labelEditingId) return;
3052
- const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
3053
- ctx.save();
3054
- ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
3055
- const metrics = ctx.measureText(arrow.label);
3056
- const padX = 6;
3057
- const padY = 4;
3058
- const w = metrics.width + padX * 2;
3059
- const h = ARROW_LABEL_FONT_SIZE + padY * 2;
3060
- ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
3061
- ctx.beginPath();
3062
- ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
3063
- ctx.fill();
3064
- ctx.fillStyle = "#1a1a1a";
3065
- ctx.textAlign = "center";
3066
- ctx.textBaseline = "middle";
3067
- ctx.fillText(arrow.label, mid.x, mid.y);
3068
- ctx.restore();
3069
- }
3070
- renderArrowhead(ctx, arrow, tip, angle) {
3071
- ctx.beginPath();
3072
- ctx.moveTo(tip.x, tip.y);
3073
- ctx.lineTo(
3074
- tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
3075
- tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
3076
- );
3077
- ctx.lineTo(
3078
- tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
3079
- tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
3080
- );
3081
- ctx.closePath();
3082
- ctx.fillStyle = arrow.color;
3083
- ctx.fill();
3084
- }
3085
- getVisualEndpoints(arrow, geometry) {
3086
- let visualFrom = arrow.from;
3087
- let visualTo = arrow.to;
3088
- if (!this.store) return { visualFrom, visualTo };
3089
- if (arrow.fromBinding) {
3090
- const el = this.store.getById(arrow.fromBinding.elementId);
3091
- if (el) {
3092
- const bounds = getElementBounds(el);
3093
- if (bounds) {
3094
- const tangentAngle = geometry.tangentStart;
3095
- const rayTarget = {
3096
- x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
3097
- y: arrow.from.y + Math.sin(tangentAngle) * 1e3
3098
- };
3099
- visualFrom = getEdgeIntersection(bounds, rayTarget);
3100
- }
3101
- }
3102
- }
3103
- if (arrow.toBinding) {
3104
- const el = this.store.getById(arrow.toBinding.elementId);
3105
- if (el) {
3106
- const bounds = getElementBounds(el);
3107
- if (bounds) {
3108
- const tangentAngle = geometry.tangentEnd;
3109
- const rayTarget = {
3110
- x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
3111
- y: arrow.to.y - Math.sin(tangentAngle) * 1e3
3112
- };
3113
- visualTo = getEdgeIntersection(bounds, rayTarget);
3114
- }
3115
- }
3116
- }
3117
- return { visualFrom, visualTo };
3118
- }
3119
- renderShape(ctx, shape) {
3120
- ctx.save();
3121
- if (shape.fillColor !== "none" && shape.shape !== "line") {
3122
- ctx.fillStyle = shape.fillColor;
3123
- this.fillShapePath(ctx, shape);
3124
- }
3125
- if (shape.strokeWidth > 0) {
3126
- ctx.strokeStyle = shape.strokeColor;
3127
- ctx.lineWidth = shape.strokeWidth;
3128
- this.strokeShapePath(ctx, shape);
3129
- }
3130
- ctx.restore();
3131
- }
3132
- fillShapePath(ctx, shape) {
3133
- switch (shape.shape) {
3134
- case "rectangle":
3135
- ctx.fillRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
3136
- break;
3137
- case "ellipse": {
3138
- const cx = shape.position.x + shape.size.w / 2;
3139
- const cy = shape.position.y + shape.size.h / 2;
3140
- ctx.beginPath();
3141
- ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
3142
- ctx.fill();
3143
- break;
3144
- }
3145
- }
3146
- }
3147
- strokeShapePath(ctx, shape) {
3148
- switch (shape.shape) {
3149
- case "rectangle":
3150
- ctx.strokeRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
3151
- break;
3152
- case "ellipse": {
3153
- const cx = shape.position.x + shape.size.w / 2;
3154
- const cy = shape.position.y + shape.size.h / 2;
3155
- ctx.beginPath();
3156
- ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
3157
- ctx.stroke();
3158
- break;
3159
- }
3160
- case "line": {
3161
- const [a, b] = lineEndpoints(shape);
3162
- ctx.lineCap = "round";
3163
- ctx.beginPath();
3164
- ctx.moveTo(a.x, a.y);
3165
- ctx.lineTo(b.x, b.y);
3166
- ctx.stroke();
3427
+ renderTemplate(ctx, element, this.store);
3167
3428
  break;
3168
- }
3169
3429
  }
3170
3430
  }
3171
3431
  renderGrid(ctx, grid) {
@@ -3218,183 +3478,6 @@ var ElementRenderer = class {
3218
3478
  );
3219
3479
  }
3220
3480
  }
3221
- renderTemplate(ctx, template) {
3222
- const grid = this.store?.getElementsByType("grid")[0];
3223
- if (grid && grid.gridType === "hex") {
3224
- this.renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
3225
- return;
3226
- }
3227
- this.renderGeometricTemplate(ctx, template);
3228
- }
3229
- renderGeometricTemplate(ctx, template) {
3230
- const { x: cx, y: cy } = template.position;
3231
- const r = template.radius;
3232
- ctx.save();
3233
- ctx.globalAlpha = template.opacity;
3234
- ctx.fillStyle = template.fillColor;
3235
- ctx.strokeStyle = template.strokeColor;
3236
- ctx.lineWidth = template.strokeWidth;
3237
- switch (template.templateShape) {
3238
- case "circle":
3239
- ctx.beginPath();
3240
- ctx.arc(cx, cy, r, 0, Math.PI * 2);
3241
- ctx.fill();
3242
- ctx.stroke();
3243
- if (template.radiusFeet != null && template.radiusFeet > 0) {
3244
- this.renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
3245
- }
3246
- break;
3247
- case "square":
3248
- ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
3249
- ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
3250
- break;
3251
- case "cone": {
3252
- const halfAngle = Math.atan(0.5);
3253
- ctx.beginPath();
3254
- ctx.moveTo(cx, cy);
3255
- ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
3256
- ctx.closePath();
3257
- ctx.fill();
3258
- ctx.stroke();
3259
- break;
3260
- }
3261
- case "line": {
3262
- const halfW = r / 12;
3263
- const cos = Math.cos(template.angle);
3264
- const sin = Math.sin(template.angle);
3265
- const perpX = -sin * halfW;
3266
- const perpY = cos * halfW;
3267
- ctx.beginPath();
3268
- ctx.moveTo(cx + perpX, cy + perpY);
3269
- ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
3270
- ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
3271
- ctx.lineTo(cx - perpX, cy - perpY);
3272
- ctx.closePath();
3273
- ctx.fill();
3274
- ctx.stroke();
3275
- break;
3276
- }
3277
- }
3278
- ctx.restore();
3279
- }
3280
- renderHexTemplate(ctx, template, cellSize, orientation) {
3281
- const snapUnit = Math.sqrt(3) * cellSize;
3282
- const radiusCells = template.radius / snapUnit;
3283
- const center2 = template.position;
3284
- let cells;
3285
- switch (template.templateShape) {
3286
- case "circle":
3287
- cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
3288
- break;
3289
- case "cone":
3290
- cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
3291
- break;
3292
- case "line":
3293
- cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
3294
- break;
3295
- case "square":
3296
- cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3297
- break;
3298
- }
3299
- ctx.save();
3300
- ctx.globalAlpha = template.opacity;
3301
- ctx.beginPath();
3302
- for (const cell of cells) {
3303
- drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3304
- }
3305
- ctx.fillStyle = template.fillColor;
3306
- ctx.fill();
3307
- ctx.beginPath();
3308
- for (const cell of cells) {
3309
- drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3310
- }
3311
- ctx.strokeStyle = template.strokeColor;
3312
- ctx.lineWidth = template.strokeWidth;
3313
- ctx.stroke();
3314
- {
3315
- ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
3316
- ctx.beginPath();
3317
- drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
3318
- ctx.fillStyle = template.strokeColor;
3319
- ctx.fill();
3320
- ctx.strokeStyle = template.strokeColor;
3321
- ctx.lineWidth = template.strokeWidth;
3322
- ctx.stroke();
3323
- }
3324
- if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3325
- const r = template.radius;
3326
- this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
3327
- }
3328
- ctx.restore();
3329
- }
3330
- renderRadiusMarker(ctx, cx, cy, r, feet) {
3331
- const markerColor = ctx.strokeStyle;
3332
- ctx.save();
3333
- ctx.globalAlpha = 1;
3334
- ctx.beginPath();
3335
- ctx.setLineDash([4, 4]);
3336
- ctx.strokeStyle = markerColor;
3337
- ctx.lineWidth = 1.5;
3338
- ctx.moveTo(cx, cy);
3339
- ctx.lineTo(cx + r, cy);
3340
- ctx.stroke();
3341
- ctx.setLineDash([]);
3342
- const label = `${Math.round(feet)} ft`;
3343
- const fontSize = Math.max(10, Math.min(14, r * 0.15));
3344
- ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
3345
- ctx.textAlign = "center";
3346
- ctx.textBaseline = "bottom";
3347
- const textX = cx + r / 2;
3348
- const textY = cy - 4;
3349
- const metrics = ctx.measureText(label);
3350
- const padX = 4;
3351
- const padY = 2;
3352
- const textW = metrics.width + padX * 2;
3353
- const textH = fontSize + padY * 2;
3354
- ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3355
- ctx.beginPath();
3356
- ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
3357
- ctx.fill();
3358
- ctx.fillStyle = markerColor;
3359
- ctx.fillText(label, textX, textY - padY);
3360
- ctx.restore();
3361
- }
3362
- renderImage(ctx, image) {
3363
- if (this.imageCache.get(image.src) === "failed") {
3364
- this.renderImagePlaceholder(ctx, image);
3365
- return;
3366
- }
3367
- const img = this.getImage(image.src);
3368
- if (!img) return;
3369
- ctx.drawImage(
3370
- img,
3371
- image.position.x,
3372
- image.position.y,
3373
- image.size.w,
3374
- image.size.h
3375
- );
3376
- }
3377
- renderImagePlaceholder(ctx, image) {
3378
- const { x, y } = image.position;
3379
- const { w, h } = image.size;
3380
- ctx.save();
3381
- ctx.fillStyle = "#eeeeee";
3382
- ctx.fillRect(x, y, w, h);
3383
- ctx.strokeStyle = "#bdbdbd";
3384
- ctx.lineWidth = 1;
3385
- ctx.strokeRect(x, y, w, h);
3386
- const glyph = Math.min(24, w / 2, h / 2);
3387
- const cx = x + w / 2;
3388
- const cy = y + h / 2;
3389
- ctx.strokeStyle = "#9e9e9e";
3390
- ctx.lineWidth = 2;
3391
- ctx.beginPath();
3392
- ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
3393
- ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
3394
- ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
3395
- ctx.stroke();
3396
- ctx.restore();
3397
- }
3398
3481
  getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
3399
3482
  const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
3400
3483
  if (this.hexTileCacheKey === key && this.hexTileCache) {
@@ -3407,33 +3490,6 @@ var ElementRenderer = class {
3407
3490
  }
3408
3491
  return tile;
3409
3492
  }
3410
- getImage(src) {
3411
- const cached = this.imageCache.get(src);
3412
- if (cached) {
3413
- if (cached === "failed") return null;
3414
- if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
3415
- return cached;
3416
- }
3417
- const img = new Image();
3418
- img.src = src;
3419
- this.imageCache.set(src, img);
3420
- img.onload = () => {
3421
- this.onImageLoad?.();
3422
- if (typeof createImageBitmap !== "undefined") {
3423
- createImageBitmap(img).then((bitmap) => {
3424
- this.imageCache.set(src, bitmap);
3425
- this.onImageLoad?.();
3426
- }).catch(() => {
3427
- });
3428
- }
3429
- };
3430
- img.onerror = (event) => {
3431
- this.imageCache.set(src, "failed");
3432
- this.onImageError?.(src, event);
3433
- this.onImageLoad?.();
3434
- };
3435
- return null;
3436
- }
3437
3493
  };
3438
3494
 
3439
3495
  // src/elements/element-factory.ts
@@ -9068,7 +9124,7 @@ var TemplateTool = class {
9068
9124
  };
9069
9125
 
9070
9126
  // src/index.ts
9071
- var VERSION = "0.38.3";
9127
+ var VERSION = "0.38.5";
9072
9128
  // Annotate the CommonJS export names for ESM import in node:
9073
9129
  0 && (module.exports = {
9074
9130
  ArrowTool,