@fieldnotes/core 0.38.4 → 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();
@@ -2401,21 +2437,53 @@ var ElementStore = class {
2401
2437
  }
2402
2438
  };
2403
2439
 
2404
- // src/elements/arrow-render-cache.ts
2405
- var cache2 = /* @__PURE__ */ new WeakMap();
2406
- function getArrowRenderGeometry(arrow) {
2407
- const hit = cache2.get(arrow);
2408
- if (hit) return hit;
2409
- const geometry = {
2410
- controlPoint: arrow.bend !== 0 ? arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend) : null,
2411
- tangentStart: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0),
2412
- tangentEnd: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1)
2413
- };
2414
- cache2.set(arrow, geometry);
2415
- return geometry;
2416
- }
2417
-
2418
- // src/elements/shape-geometry.ts
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
+
2486
+ // src/elements/shape-geometry.ts
2419
2487
  function lineFromEndpoints(a, b) {
2420
2488
  return {
2421
2489
  position: { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y) },
@@ -2435,6 +2503,74 @@ function lineEndpoints(shape) {
2435
2503
  ];
2436
2504
  }
2437
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
+
2438
2574
  // src/elements/arrow-binding.ts
2439
2575
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
2440
2576
  function isBindable(element) {
@@ -2507,38 +2643,515 @@ function updateArrowsBoundToElements(movedIds, store) {
2507
2643
  }
2508
2644
  }
2509
2645
  }
2510
- function updateBoundArrow(arrow, store) {
2511
- if (!arrow.fromBinding && !arrow.toBinding) return null;
2512
- const updates = {};
2513
- if (arrow.fromBinding) {
2514
- const el = store.getById(arrow.fromBinding.elementId);
2515
- if (el) {
2516
- const center2 = getElementCenter(el);
2517
- updates.from = center2;
2518
- updates.position = center2;
2519
- }
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;
2520
3093
  }
2521
- if (arrow.toBinding) {
2522
- const el = store.getById(arrow.toBinding.elementId);
2523
- if (el) {
2524
- updates.to = getElementCenter(el);
2525
- }
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);
2526
3099
  }
2527
- return Object.keys(updates).length > 0 ? updates : null;
2528
- }
2529
-
2530
- // src/elements/rotate-canvas.ts
2531
- function withRotation(ctx, el, center2, draw) {
2532
- const angle = el.rotation ?? 0;
2533
- if (angle === 0) {
2534
- draw();
2535
- 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);
2536
3122
  }
3123
+ ctx.restore();
3124
+ }
3125
+ function renderRadiusMarker(ctx, cx, cy, r, feet) {
3126
+ const markerColor = ctx.strokeStyle;
2537
3127
  ctx.save();
2538
- ctx.translate(center2.x, center2.y);
2539
- ctx.rotate(angle);
2540
- ctx.translate(-center2.x, -center2.y);
2541
- 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);
2542
3155
  ctx.restore();
2543
3156
  }
2544
3157
 
@@ -2692,245 +3305,58 @@ function createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opac
2692
3305
  tc.globalAlpha = opacity;
2693
3306
  tc.beginPath();
2694
3307
  if (orientation === "pointy") {
2695
- const hexW = tileW;
2696
- const rowH = 1.5 * cellSize;
2697
- for (let row = -1; row <= 3; row++) {
2698
- const offX = row % 2 !== 0 ? hexW / 2 : 0;
2699
- for (let col = -1; col <= 1; col++) {
2700
- const cx = col * hexW + offX;
2701
- const cy = row * rowH;
2702
- tc.moveTo(cx + ox0, cy + oy0);
2703
- tc.lineTo(cx + ox1, cy + oy1);
2704
- tc.lineTo(cx + ox2, cy + oy2);
2705
- tc.lineTo(cx + ox3, cy + oy3);
2706
- tc.lineTo(cx + ox4, cy + oy4);
2707
- tc.lineTo(cx + ox5, cy + oy5);
2708
- tc.closePath();
2709
- }
2710
- }
2711
- } else {
2712
- const hexH = tileH;
2713
- const colW = 1.5 * cellSize;
2714
- for (let col = -1; col <= 3; col++) {
2715
- const offY = col % 2 !== 0 ? hexH / 2 : 0;
2716
- for (let row = -1; row <= 1; row++) {
2717
- const cx = col * colW;
2718
- const cy = row * hexH + offY;
2719
- tc.moveTo(cx + ox0, cy + oy0);
2720
- tc.lineTo(cx + ox1, cy + oy1);
2721
- tc.lineTo(cx + ox2, cy + oy2);
2722
- tc.lineTo(cx + ox3, cy + oy3);
2723
- tc.lineTo(cx + ox4, cy + oy4);
2724
- tc.lineTo(cx + ox5, cy + oy5);
2725
- tc.closePath();
2726
- }
2727
- }
2728
- }
2729
- tc.stroke();
2730
- return { canvas, tileW, tileH };
2731
- }
2732
- function renderHexGridTiled(ctx, bounds, cellSize, tile) {
2733
- const { tileW, tileH } = tile;
2734
- const startCol = Math.floor(bounds.minX / tileW) - 1;
2735
- const endCol = Math.ceil(bounds.maxX / tileW) + 1;
2736
- const startRow = Math.floor(bounds.minY / tileH) - 1;
2737
- const endRow = Math.ceil(bounds.maxY / tileH) + 1;
2738
- for (let row = startRow; row <= endRow; row++) {
2739
- for (let col = startCol; col <= endCol; col++) {
2740
- ctx.drawImage(tile.canvas, col * tileW, row * tileH, tileW, tileH);
2741
- }
2742
- }
2743
- }
2744
-
2745
- // src/elements/hex-fill.ts
2746
- function offsetToCube(col, row, orientation) {
2747
- if (orientation === "pointy") {
2748
- return { q: col - (row - (row & 1)) / 2, r: row };
2749
- }
2750
- return { q: col, r: row - (col - (col & 1)) / 2 };
2751
- }
2752
- function cubeToOffset(q, r, orientation) {
2753
- if (orientation === "pointy") {
2754
- return { col: q + (r - (r & 1)) / 2, row: r };
2755
- }
2756
- return { col: q, row: r + (q - (q & 1)) / 2 };
2757
- }
2758
- function offsetToPixel(col, row, cellSize, orientation) {
2759
- if (orientation === "pointy") {
2760
- const hexW = Math.sqrt(3) * cellSize;
2761
- const rowH = 1.5 * cellSize;
2762
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2763
- return { x: col * hexW + offsetX, y: row * rowH };
2764
- }
2765
- const hexH = Math.sqrt(3) * cellSize;
2766
- const colW = 1.5 * cellSize;
2767
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2768
- return { x: col * colW, y: row * hexH + offsetY };
2769
- }
2770
- function pixelToOffset(x, y, cellSize, orientation) {
2771
- if (orientation === "pointy") {
2772
- const hexW = Math.sqrt(3) * cellSize;
2773
- const rowH = 1.5 * cellSize;
2774
- const row = Math.round(y / rowH);
2775
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2776
- return { col: Math.round((x - offsetX) / hexW), row };
2777
- }
2778
- const hexH = Math.sqrt(3) * cellSize;
2779
- const colW = 1.5 * cellSize;
2780
- const col = Math.round(x / colW);
2781
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2782
- return { col, row: Math.round((y - offsetY) / hexH) };
2783
- }
2784
- function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
2785
- const cells = [];
2786
- for (let dq = -n; dq <= n; dq++) {
2787
- const rMin = Math.max(-n, -dq - n);
2788
- const rMax = Math.min(n, -dq + n);
2789
- for (let dr = rMin; dr <= rMax; dr++) {
2790
- const absQ = centerQ + dq;
2791
- const absR = centerR + dr;
2792
- const off = cubeToOffset(absQ, absR, orientation);
2793
- cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
2794
- }
2795
- }
2796
- return cells;
2797
- }
2798
- function getHexDistance(a, b, cellSize, orientation) {
2799
- const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
2800
- const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
2801
- const cubeA = offsetToCube(offA.col, offA.row, orientation);
2802
- const cubeB = offsetToCube(offB.col, offB.row, orientation);
2803
- const dq = cubeA.q - cubeB.q;
2804
- const dr = cubeA.r - cubeB.r;
2805
- const ds = -dq - dr;
2806
- return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2807
- }
2808
- function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2809
- const n = Math.round(radiusCells);
2810
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2811
- const cube = offsetToCube(off.col, off.row, orientation);
2812
- if (n <= 0) {
2813
- return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2814
- }
2815
- return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2816
- }
2817
- function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2818
- const n = Math.round(radiusCells);
2819
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2820
- const cube = offsetToCube(off.col, off.row, orientation);
2821
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2822
- if (n <= 0) return [centerPixel];
2823
- const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2824
- const step = Math.PI / 3;
2825
- const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2826
- const halfAngle = Math.PI / 6 + 1e-6;
2827
- const cells = [centerPixel];
2828
- for (let dq = -n; dq <= n; dq++) {
2829
- const rMin = Math.max(-n, -dq - n);
2830
- const rMax = Math.min(n, -dq + n);
2831
- for (let dr = rMin; dr <= rMax; dr++) {
2832
- if (dq === 0 && dr === 0) continue;
2833
- const absQ = cube.q + dq;
2834
- const absR = cube.r + dr;
2835
- const pixel = offsetToPixel(
2836
- cubeToOffset(absQ, absR, orientation).col,
2837
- cubeToOffset(absQ, absR, orientation).row,
2838
- cellSize,
2839
- orientation
2840
- );
2841
- const dx = pixel.x - centerPixel.x;
2842
- const dy = pixel.y - centerPixel.y;
2843
- let diff = Math.atan2(dy, dx) - snappedAngle;
2844
- if (diff > Math.PI) diff -= 2 * Math.PI;
2845
- if (diff < -Math.PI) diff += 2 * Math.PI;
2846
- if (Math.abs(diff) <= halfAngle) {
2847
- cells.push(pixel);
2848
- }
2849
- }
2850
- }
2851
- return cells;
2852
- }
2853
- function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2854
- const n = Math.round(radiusCells);
2855
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2856
- const cube = offsetToCube(off.col, off.row, orientation);
2857
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2858
- if (n <= 0) return [centerPixel];
2859
- const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2860
- const step = Math.PI / 3;
2861
- const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2862
- const cos = Math.cos(snappedAngle);
2863
- const sin = Math.sin(snappedAngle);
2864
- const snapUnit = Math.sqrt(3) * cellSize;
2865
- const lineLength = n * snapUnit;
2866
- const halfWidth = snapUnit * 0.5 + 1e-6;
2867
- const cells = [];
2868
- for (let dq = -n; dq <= n; dq++) {
2869
- const rMin = Math.max(-n, -dq - n);
2870
- const rMax = Math.min(n, -dq + n);
2871
- for (let dr = rMin; dr <= rMax; dr++) {
2872
- const absQ = cube.q + dq;
2873
- const absR = cube.r + dr;
2874
- const pixel = offsetToPixel(
2875
- cubeToOffset(absQ, absR, orientation).col,
2876
- cubeToOffset(absQ, absR, orientation).row,
2877
- cellSize,
2878
- orientation
2879
- );
2880
- const dx = pixel.x - centerPixel.x;
2881
- const dy = pixel.y - centerPixel.y;
2882
- const along = dx * cos + dy * sin;
2883
- const perp = Math.abs(-dx * sin + dy * cos);
2884
- if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
2885
- 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();
2886
3322
  }
2887
3323
  }
2888
- }
2889
- return cells;
2890
- }
2891
- function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2892
- const n = Math.round(radiusCells);
2893
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2894
- const cube = offsetToCube(off.col, off.row, orientation);
2895
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2896
- if (n <= 0) return [centerPixel];
2897
- const snapUnit = Math.sqrt(3) * cellSize;
2898
- const halfSide = n * snapUnit / 2;
2899
- const cells = [];
2900
- for (let dq = -n; dq <= n; dq++) {
2901
- const rMin = Math.max(-n, -dq - n);
2902
- const rMax = Math.min(n, -dq + n);
2903
- for (let dr = rMin; dr <= rMax; dr++) {
2904
- const absQ = cube.q + dq;
2905
- const absR = cube.r + dr;
2906
- const pixel = offsetToPixel(
2907
- cubeToOffset(absQ, absR, orientation).col,
2908
- cubeToOffset(absQ, absR, orientation).row,
2909
- cellSize,
2910
- orientation
2911
- );
2912
- if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
2913
- 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();
2914
3339
  }
2915
3340
  }
2916
3341
  }
2917
- return cells;
3342
+ tc.stroke();
3343
+ return { canvas, tileW, tileH };
2918
3344
  }
2919
- function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2920
- const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2921
- ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
2922
- for (let i = 1; i < 6; i++) {
2923
- const a = angleOffset + Math.PI / 3 * i;
2924
- 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
+ }
2925
3355
  }
2926
- ctx.closePath();
2927
3356
  }
2928
3357
 
2929
3358
  // src/elements/element-renderer.ts
2930
3359
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
2931
- var ARROWHEAD_LENGTH = 12;
2932
- var ARROWHEAD_ANGLE = Math.PI / 6;
2933
- var ARROW_LABEL_FONT_SIZE = 14;
2934
3360
  var ElementRenderer = class {
2935
3361
  store = null;
2936
3362
  imageCache = /* @__PURE__ */ new Map();
@@ -2971,206 +3397,35 @@ var ElementRenderer = class {
2971
3397
  case "stroke": {
2972
3398
  const b = getElementBounds(element);
2973
3399
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2974
- withRotation(ctx, element, c, () => this.renderStroke(ctx, element));
3400
+ withRotation(ctx, element, c, () => renderStroke(ctx, element));
2975
3401
  break;
2976
3402
  }
2977
3403
  case "arrow":
2978
- this.renderArrow(ctx, element);
3404
+ renderArrow(ctx, element, this.store, this.labelEditingId);
2979
3405
  break;
2980
3406
  case "shape": {
2981
3407
  const b = getElementBounds(element);
2982
3408
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2983
- withRotation(ctx, element, c, () => this.renderShape(ctx, element));
3409
+ withRotation(ctx, element, c, () => renderShape(ctx, element));
2984
3410
  break;
2985
3411
  }
2986
3412
  case "image": {
2987
3413
  const b = getElementBounds(element);
2988
3414
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2989
- 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
+ );
2990
3421
  break;
2991
3422
  }
2992
3423
  case "grid":
2993
3424
  this.renderGrid(ctx, element);
2994
3425
  break;
2995
3426
  case "template":
2996
- this.renderTemplate(ctx, element);
2997
- break;
2998
- }
2999
- }
3000
- renderStroke(ctx, stroke) {
3001
- if (stroke.points.length < 2) return;
3002
- ctx.save();
3003
- if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
3004
- ctx.translate(stroke.position.x, stroke.position.y);
3005
- ctx.strokeStyle = stroke.color;
3006
- ctx.lineCap = "round";
3007
- ctx.lineJoin = "round";
3008
- ctx.globalAlpha = stroke.opacity;
3009
- const data = getStrokeRenderData(stroke);
3010
- if (data.buckets) {
3011
- for (const bucket of data.buckets) {
3012
- ctx.lineWidth = bucket.width;
3013
- ctx.stroke(bucket.path);
3014
- }
3015
- } else {
3016
- for (let i = 0; i < data.segments.length; i++) {
3017
- const seg = data.segments[i];
3018
- const w = data.widths[i];
3019
- if (!seg || w === void 0) continue;
3020
- ctx.lineWidth = w;
3021
- ctx.beginPath();
3022
- ctx.moveTo(seg.start.x, seg.start.y);
3023
- ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
3024
- ctx.stroke();
3025
- }
3026
- }
3027
- ctx.restore();
3028
- }
3029
- renderArrow(ctx, arrow) {
3030
- const geometry = getArrowRenderGeometry(arrow);
3031
- const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
3032
- ctx.save();
3033
- ctx.strokeStyle = arrow.color;
3034
- ctx.lineWidth = arrow.width;
3035
- ctx.lineCap = "round";
3036
- if (arrow.fromBinding || arrow.toBinding) {
3037
- ctx.setLineDash([8, 4]);
3038
- }
3039
- ctx.beginPath();
3040
- ctx.moveTo(visualFrom.x, visualFrom.y);
3041
- if (arrow.bend !== 0) {
3042
- const cp = geometry.controlPoint;
3043
- if (cp) {
3044
- ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
3045
- }
3046
- } else {
3047
- ctx.lineTo(visualTo.x, visualTo.y);
3048
- }
3049
- ctx.stroke();
3050
- this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
3051
- ctx.restore();
3052
- this.renderArrowLabel(ctx, arrow);
3053
- }
3054
- renderArrowLabel(ctx, arrow) {
3055
- if (!arrow.label || arrow.label.length === 0) return;
3056
- if (arrow.id === this.labelEditingId) return;
3057
- const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
3058
- ctx.save();
3059
- ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
3060
- const metrics = ctx.measureText(arrow.label);
3061
- const padX = 6;
3062
- const padY = 4;
3063
- const w = metrics.width + padX * 2;
3064
- const h = ARROW_LABEL_FONT_SIZE + padY * 2;
3065
- ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
3066
- ctx.beginPath();
3067
- ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
3068
- ctx.fill();
3069
- ctx.fillStyle = "#1a1a1a";
3070
- ctx.textAlign = "center";
3071
- ctx.textBaseline = "middle";
3072
- ctx.fillText(arrow.label, mid.x, mid.y);
3073
- ctx.restore();
3074
- }
3075
- renderArrowhead(ctx, arrow, tip, angle) {
3076
- ctx.beginPath();
3077
- ctx.moveTo(tip.x, tip.y);
3078
- ctx.lineTo(
3079
- tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
3080
- tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
3081
- );
3082
- ctx.lineTo(
3083
- tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
3084
- tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
3085
- );
3086
- ctx.closePath();
3087
- ctx.fillStyle = arrow.color;
3088
- ctx.fill();
3089
- }
3090
- getVisualEndpoints(arrow, geometry) {
3091
- let visualFrom = arrow.from;
3092
- let visualTo = arrow.to;
3093
- if (!this.store) return { visualFrom, visualTo };
3094
- if (arrow.fromBinding) {
3095
- const el = this.store.getById(arrow.fromBinding.elementId);
3096
- if (el) {
3097
- const bounds = getElementBounds(el);
3098
- if (bounds) {
3099
- const tangentAngle = geometry.tangentStart;
3100
- const rayTarget = {
3101
- x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
3102
- y: arrow.from.y + Math.sin(tangentAngle) * 1e3
3103
- };
3104
- visualFrom = getEdgeIntersection(bounds, rayTarget);
3105
- }
3106
- }
3107
- }
3108
- if (arrow.toBinding) {
3109
- const el = this.store.getById(arrow.toBinding.elementId);
3110
- if (el) {
3111
- const bounds = getElementBounds(el);
3112
- if (bounds) {
3113
- const tangentAngle = geometry.tangentEnd;
3114
- const rayTarget = {
3115
- x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
3116
- y: arrow.to.y - Math.sin(tangentAngle) * 1e3
3117
- };
3118
- visualTo = getEdgeIntersection(bounds, rayTarget);
3119
- }
3120
- }
3121
- }
3122
- return { visualFrom, visualTo };
3123
- }
3124
- renderShape(ctx, shape) {
3125
- ctx.save();
3126
- if (shape.fillColor !== "none" && shape.shape !== "line") {
3127
- ctx.fillStyle = shape.fillColor;
3128
- this.fillShapePath(ctx, shape);
3129
- }
3130
- if (shape.strokeWidth > 0) {
3131
- ctx.strokeStyle = shape.strokeColor;
3132
- ctx.lineWidth = shape.strokeWidth;
3133
- this.strokeShapePath(ctx, shape);
3134
- }
3135
- ctx.restore();
3136
- }
3137
- fillShapePath(ctx, shape) {
3138
- switch (shape.shape) {
3139
- case "rectangle":
3140
- ctx.fillRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
3141
- break;
3142
- case "ellipse": {
3143
- const cx = shape.position.x + shape.size.w / 2;
3144
- const cy = shape.position.y + shape.size.h / 2;
3145
- ctx.beginPath();
3146
- ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
3147
- ctx.fill();
3148
- break;
3149
- }
3150
- }
3151
- }
3152
- strokeShapePath(ctx, shape) {
3153
- switch (shape.shape) {
3154
- case "rectangle":
3155
- ctx.strokeRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
3156
- break;
3157
- case "ellipse": {
3158
- const cx = shape.position.x + shape.size.w / 2;
3159
- const cy = shape.position.y + shape.size.h / 2;
3160
- ctx.beginPath();
3161
- ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
3162
- ctx.stroke();
3163
- break;
3164
- }
3165
- case "line": {
3166
- const [a, b] = lineEndpoints(shape);
3167
- ctx.lineCap = "round";
3168
- ctx.beginPath();
3169
- ctx.moveTo(a.x, a.y);
3170
- ctx.lineTo(b.x, b.y);
3171
- ctx.stroke();
3427
+ renderTemplate(ctx, element, this.store);
3172
3428
  break;
3173
- }
3174
3429
  }
3175
3430
  }
3176
3431
  renderGrid(ctx, grid) {
@@ -3223,183 +3478,6 @@ var ElementRenderer = class {
3223
3478
  );
3224
3479
  }
3225
3480
  }
3226
- renderTemplate(ctx, template) {
3227
- const grid = this.store?.getElementsByType("grid")[0];
3228
- if (grid && grid.gridType === "hex") {
3229
- this.renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
3230
- return;
3231
- }
3232
- this.renderGeometricTemplate(ctx, template);
3233
- }
3234
- renderGeometricTemplate(ctx, template) {
3235
- const { x: cx, y: cy } = template.position;
3236
- const r = template.radius;
3237
- ctx.save();
3238
- ctx.globalAlpha = template.opacity;
3239
- ctx.fillStyle = template.fillColor;
3240
- ctx.strokeStyle = template.strokeColor;
3241
- ctx.lineWidth = template.strokeWidth;
3242
- switch (template.templateShape) {
3243
- case "circle":
3244
- ctx.beginPath();
3245
- ctx.arc(cx, cy, r, 0, Math.PI * 2);
3246
- ctx.fill();
3247
- ctx.stroke();
3248
- if (template.radiusFeet != null && template.radiusFeet > 0) {
3249
- this.renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
3250
- }
3251
- break;
3252
- case "square":
3253
- ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
3254
- ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
3255
- break;
3256
- case "cone": {
3257
- const halfAngle = Math.atan(0.5);
3258
- ctx.beginPath();
3259
- ctx.moveTo(cx, cy);
3260
- ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
3261
- ctx.closePath();
3262
- ctx.fill();
3263
- ctx.stroke();
3264
- break;
3265
- }
3266
- case "line": {
3267
- const halfW = r / 12;
3268
- const cos = Math.cos(template.angle);
3269
- const sin = Math.sin(template.angle);
3270
- const perpX = -sin * halfW;
3271
- const perpY = cos * halfW;
3272
- ctx.beginPath();
3273
- ctx.moveTo(cx + perpX, cy + perpY);
3274
- ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
3275
- ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
3276
- ctx.lineTo(cx - perpX, cy - perpY);
3277
- ctx.closePath();
3278
- ctx.fill();
3279
- ctx.stroke();
3280
- break;
3281
- }
3282
- }
3283
- ctx.restore();
3284
- }
3285
- renderHexTemplate(ctx, template, cellSize, orientation) {
3286
- const snapUnit = Math.sqrt(3) * cellSize;
3287
- const radiusCells = template.radius / snapUnit;
3288
- const center2 = template.position;
3289
- let cells;
3290
- switch (template.templateShape) {
3291
- case "circle":
3292
- cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
3293
- break;
3294
- case "cone":
3295
- cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
3296
- break;
3297
- case "line":
3298
- cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
3299
- break;
3300
- case "square":
3301
- cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3302
- break;
3303
- }
3304
- ctx.save();
3305
- ctx.globalAlpha = template.opacity;
3306
- ctx.beginPath();
3307
- for (const cell of cells) {
3308
- drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3309
- }
3310
- ctx.fillStyle = template.fillColor;
3311
- ctx.fill();
3312
- ctx.beginPath();
3313
- for (const cell of cells) {
3314
- drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3315
- }
3316
- ctx.strokeStyle = template.strokeColor;
3317
- ctx.lineWidth = template.strokeWidth;
3318
- ctx.stroke();
3319
- {
3320
- ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
3321
- ctx.beginPath();
3322
- drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
3323
- ctx.fillStyle = template.strokeColor;
3324
- ctx.fill();
3325
- ctx.strokeStyle = template.strokeColor;
3326
- ctx.lineWidth = template.strokeWidth;
3327
- ctx.stroke();
3328
- }
3329
- if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3330
- const r = template.radius;
3331
- this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
3332
- }
3333
- ctx.restore();
3334
- }
3335
- renderRadiusMarker(ctx, cx, cy, r, feet) {
3336
- const markerColor = ctx.strokeStyle;
3337
- ctx.save();
3338
- ctx.globalAlpha = 1;
3339
- ctx.beginPath();
3340
- ctx.setLineDash([4, 4]);
3341
- ctx.strokeStyle = markerColor;
3342
- ctx.lineWidth = 1.5;
3343
- ctx.moveTo(cx, cy);
3344
- ctx.lineTo(cx + r, cy);
3345
- ctx.stroke();
3346
- ctx.setLineDash([]);
3347
- const label = `${Math.round(feet)} ft`;
3348
- const fontSize = Math.max(10, Math.min(14, r * 0.15));
3349
- ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
3350
- ctx.textAlign = "center";
3351
- ctx.textBaseline = "bottom";
3352
- const textX = cx + r / 2;
3353
- const textY = cy - 4;
3354
- const metrics = ctx.measureText(label);
3355
- const padX = 4;
3356
- const padY = 2;
3357
- const textW = metrics.width + padX * 2;
3358
- const textH = fontSize + padY * 2;
3359
- ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3360
- ctx.beginPath();
3361
- ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
3362
- ctx.fill();
3363
- ctx.fillStyle = markerColor;
3364
- ctx.fillText(label, textX, textY - padY);
3365
- ctx.restore();
3366
- }
3367
- renderImage(ctx, image) {
3368
- if (this.imageCache.get(image.src) === "failed") {
3369
- this.renderImagePlaceholder(ctx, image);
3370
- return;
3371
- }
3372
- const img = this.getImage(image.src);
3373
- if (!img) return;
3374
- ctx.drawImage(
3375
- img,
3376
- image.position.x,
3377
- image.position.y,
3378
- image.size.w,
3379
- image.size.h
3380
- );
3381
- }
3382
- renderImagePlaceholder(ctx, image) {
3383
- const { x, y } = image.position;
3384
- const { w, h } = image.size;
3385
- ctx.save();
3386
- ctx.fillStyle = "#eeeeee";
3387
- ctx.fillRect(x, y, w, h);
3388
- ctx.strokeStyle = "#bdbdbd";
3389
- ctx.lineWidth = 1;
3390
- ctx.strokeRect(x, y, w, h);
3391
- const glyph = Math.min(24, w / 2, h / 2);
3392
- const cx = x + w / 2;
3393
- const cy = y + h / 2;
3394
- ctx.strokeStyle = "#9e9e9e";
3395
- ctx.lineWidth = 2;
3396
- ctx.beginPath();
3397
- ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
3398
- ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
3399
- ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
3400
- ctx.stroke();
3401
- ctx.restore();
3402
- }
3403
3481
  getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
3404
3482
  const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
3405
3483
  if (this.hexTileCacheKey === key && this.hexTileCache) {
@@ -3412,33 +3490,6 @@ var ElementRenderer = class {
3412
3490
  }
3413
3491
  return tile;
3414
3492
  }
3415
- getImage(src) {
3416
- const cached = this.imageCache.get(src);
3417
- if (cached) {
3418
- if (cached === "failed") return null;
3419
- if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
3420
- return cached;
3421
- }
3422
- const img = new Image();
3423
- img.src = src;
3424
- this.imageCache.set(src, img);
3425
- img.onload = () => {
3426
- this.onImageLoad?.();
3427
- if (typeof createImageBitmap !== "undefined") {
3428
- createImageBitmap(img).then((bitmap) => {
3429
- this.imageCache.set(src, bitmap);
3430
- this.onImageLoad?.();
3431
- }).catch(() => {
3432
- });
3433
- }
3434
- };
3435
- img.onerror = (event) => {
3436
- this.imageCache.set(src, "failed");
3437
- this.onImageError?.(src, event);
3438
- this.onImageLoad?.();
3439
- };
3440
- return null;
3441
- }
3442
3493
  };
3443
3494
 
3444
3495
  // src/elements/element-factory.ts
@@ -9073,7 +9124,7 @@ var TemplateTool = class {
9073
9124
  };
9074
9125
 
9075
9126
  // src/index.ts
9076
- var VERSION = "0.38.4";
9127
+ var VERSION = "0.38.5";
9077
9128
  // Annotate the CommonJS export names for ESM import in node:
9078
9129
  0 && (module.exports = {
9079
9130
  ArrowTool,