@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.js CHANGED
@@ -1227,17 +1227,174 @@ var ShortcutMap = class {
1227
1227
  }
1228
1228
  };
1229
1229
 
1230
- // src/canvas/input-handler.ts
1231
- var ZOOM_SENSITIVITY = 1e-3;
1230
+ // src/canvas/keyboard-handler.ts
1232
1231
  var ZOOM_STEP = 1.2;
1233
- var MIDDLE_BUTTON = 1;
1234
- var LONG_PRESS_MS = 500;
1235
1232
  var NUDGE_DELTAS = {
1236
1233
  "nudge-left": [-1, 0],
1237
1234
  "nudge-right": [1, 0],
1238
1235
  "nudge-up": [0, -1],
1239
1236
  "nudge-down": [0, 1]
1240
1237
  };
1238
+ var KeyboardHandler = class {
1239
+ constructor(deps) {
1240
+ this.deps = deps;
1241
+ this.shortcutMap = new ShortcutMap(deps.shortcuts?.bindings);
1242
+ window.addEventListener("keydown", this.onKeyDown, { signal: deps.abortSignal });
1243
+ window.addEventListener("keyup", this.onKeyUp, { signal: deps.abortSignal });
1244
+ }
1245
+ shortcutMap;
1246
+ get shortcuts() {
1247
+ return this.shortcutMap;
1248
+ }
1249
+ viewportCenter() {
1250
+ const rect = this.deps.element.getBoundingClientRect();
1251
+ return { x: rect.width / 2, y: rect.height / 2 };
1252
+ }
1253
+ zoomByFactor(factor) {
1254
+ this.deps.camera.zoomAt(this.deps.camera.zoom * factor, this.viewportCenter());
1255
+ }
1256
+ zoomToLevel(level) {
1257
+ this.deps.camera.zoomAt(level, this.viewportCenter());
1258
+ }
1259
+ onKeyDown = (e) => {
1260
+ const target = e.target;
1261
+ if (target?.isContentEditable) return;
1262
+ const tag = target?.tagName;
1263
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1264
+ if (!this.isInScope()) return;
1265
+ if (e.key === " ") {
1266
+ this.deps.setSpaceHeld(true);
1267
+ }
1268
+ const action = this.shortcutMap.match(e);
1269
+ if (action !== null) {
1270
+ this.runAction(action, e);
1271
+ }
1272
+ };
1273
+ onKeyUp = (e) => {
1274
+ if (e.key === " ") {
1275
+ this.deps.setSpaceHeld(false);
1276
+ if (this.deps.getActivePointerCount() === 0) {
1277
+ const lastPointerEvent = this.deps.getLastPointerEvent();
1278
+ if (lastPointerEvent) {
1279
+ this.deps.dispatchToolHover(lastPointerEvent);
1280
+ } else {
1281
+ this.deps.getToolContext()?.setCursor?.("default");
1282
+ }
1283
+ }
1284
+ }
1285
+ };
1286
+ runAction(action, e) {
1287
+ switch (action) {
1288
+ case "delete":
1289
+ e?.preventDefault();
1290
+ this.deps.actions.deleteSelected();
1291
+ return;
1292
+ case "deselect":
1293
+ this.deps.actions.deselect();
1294
+ return;
1295
+ case "undo":
1296
+ e?.preventDefault();
1297
+ this.deps.actions.undo();
1298
+ return;
1299
+ case "redo":
1300
+ e?.preventDefault();
1301
+ this.deps.actions.redo();
1302
+ return;
1303
+ case "select-all":
1304
+ e?.preventDefault();
1305
+ this.deps.actions.selectAll();
1306
+ return;
1307
+ case "copy":
1308
+ e?.preventDefault();
1309
+ this.deps.actions.copy();
1310
+ return;
1311
+ case "paste":
1312
+ e?.preventDefault();
1313
+ this.deps.actions.paste();
1314
+ return;
1315
+ case "duplicate":
1316
+ e?.preventDefault();
1317
+ this.deps.actions.duplicate();
1318
+ return;
1319
+ case "z-forward":
1320
+ e?.preventDefault();
1321
+ this.deps.actions.zOrder("forward");
1322
+ return;
1323
+ case "z-backward":
1324
+ e?.preventDefault();
1325
+ this.deps.actions.zOrder("backward");
1326
+ return;
1327
+ case "z-front":
1328
+ e?.preventDefault();
1329
+ this.deps.actions.zOrder("front");
1330
+ return;
1331
+ case "z-back":
1332
+ e?.preventDefault();
1333
+ this.deps.actions.zOrder("back");
1334
+ return;
1335
+ case "zoom-fit":
1336
+ e?.preventDefault();
1337
+ this.deps.actions.zoomToFit();
1338
+ return;
1339
+ case "group":
1340
+ e?.preventDefault();
1341
+ this.deps.actions.group();
1342
+ return;
1343
+ case "ungroup":
1344
+ e?.preventDefault();
1345
+ this.deps.actions.ungroup();
1346
+ return;
1347
+ case "cut":
1348
+ e?.preventDefault();
1349
+ this.deps.actions.cut();
1350
+ return;
1351
+ case "toggle-lock":
1352
+ e?.preventDefault();
1353
+ this.deps.actions.toggleLock();
1354
+ return;
1355
+ case "zoom-in":
1356
+ e?.preventDefault();
1357
+ this.zoomByFactor(ZOOM_STEP);
1358
+ return;
1359
+ case "zoom-out":
1360
+ e?.preventDefault();
1361
+ this.zoomByFactor(1 / ZOOM_STEP);
1362
+ return;
1363
+ case "zoom-reset":
1364
+ e?.preventDefault();
1365
+ this.zoomToLevel(1);
1366
+ return;
1367
+ case "nudge-left":
1368
+ case "nudge-right":
1369
+ case "nudge-up":
1370
+ case "nudge-down": {
1371
+ const delta = NUDGE_DELTAS[action];
1372
+ if (delta && this.deps.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
1373
+ e?.preventDefault();
1374
+ }
1375
+ return;
1376
+ }
1377
+ default:
1378
+ if (action.startsWith("tool:")) {
1379
+ if (this.deps.getIsToolActive()) return;
1380
+ e?.preventDefault();
1381
+ this.deps.getToolContext()?.switchTool?.(action.slice("tool:".length));
1382
+ return;
1383
+ }
1384
+ console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1385
+ }
1386
+ }
1387
+ isInScope() {
1388
+ if (this.deps.scope === "window") return true;
1389
+ const active = document.activeElement;
1390
+ return active === this.deps.element || this.deps.element.contains(active);
1391
+ }
1392
+ };
1393
+
1394
+ // src/canvas/input-handler.ts
1395
+ var ZOOM_SENSITIVITY = 1e-3;
1396
+ var MIDDLE_BUTTON = 1;
1397
+ var LONG_PRESS_MS = 500;
1241
1398
  var InputHandler = class {
1242
1399
  constructor(element, camera, options = {}) {
1243
1400
  this.element = element;
@@ -1259,8 +1416,23 @@ var InputHandler = class {
1259
1416
  getLastPointerWorld: () => this.lastPointerWorld()
1260
1417
  });
1261
1418
  this.openContextMenu = options.openContextMenu;
1262
- this.shortcutMap = new ShortcutMap(options.shortcuts?.bindings);
1263
1419
  this.scope = options.shortcuts?.scope ?? "focus";
1420
+ this.keyboard = new KeyboardHandler({
1421
+ element: this.element,
1422
+ camera: this.camera,
1423
+ actions: this.actions,
1424
+ scope: this.scope,
1425
+ shortcuts: options.shortcuts,
1426
+ abortSignal: this.abortController.signal,
1427
+ getToolContext: () => this.toolContext,
1428
+ getIsToolActive: () => this.isToolActive,
1429
+ getLastPointerEvent: () => this.lastPointerEvent,
1430
+ setSpaceHeld: (v) => {
1431
+ this.spaceHeld = v;
1432
+ },
1433
+ getActivePointerCount: () => this.activePointers.size,
1434
+ dispatchToolHover: (e) => this.dispatchToolHover(e)
1435
+ });
1264
1436
  this.element.style.touchAction = "none";
1265
1437
  if (this.scope === "focus") {
1266
1438
  this.element.tabIndex = 0;
@@ -1286,7 +1458,7 @@ var InputHandler = class {
1286
1458
  longPressStart = null;
1287
1459
  abortController = new AbortController();
1288
1460
  actions;
1289
- shortcutMap;
1461
+ keyboard;
1290
1462
  scope;
1291
1463
  openContextMenu;
1292
1464
  setToolManager(toolManager, toolContext) {
@@ -1297,7 +1469,7 @@ var InputHandler = class {
1297
1469
  this.actions.flushPendingNudge();
1298
1470
  }
1299
1471
  get shortcuts() {
1300
- return this.shortcutMap;
1472
+ return this.keyboard.shortcuts;
1301
1473
  }
1302
1474
  destroy() {
1303
1475
  this.actions.dispose();
@@ -1320,18 +1492,6 @@ var InputHandler = class {
1320
1492
  this.element.addEventListener("pointerleave", this.onPointerLeave, opts);
1321
1493
  this.element.addEventListener("pointercancel", this.onPointerUp, opts);
1322
1494
  this.element.addEventListener("contextmenu", this.onContextMenu, opts);
1323
- window.addEventListener("keydown", this.onKeyDown, opts);
1324
- window.addEventListener("keyup", this.onKeyUp, opts);
1325
- }
1326
- viewportCenter() {
1327
- const rect = this.element.getBoundingClientRect();
1328
- return { x: rect.width / 2, y: rect.height / 2 };
1329
- }
1330
- zoomByFactor(factor) {
1331
- this.camera.zoomAt(this.camera.zoom * factor, this.viewportCenter());
1332
- }
1333
- zoomToLevel(level) {
1334
- this.camera.zoomAt(level, this.viewportCenter());
1335
1495
  }
1336
1496
  onWheel = (e) => {
1337
1497
  e.preventDefault();
@@ -1427,132 +1587,8 @@ var InputHandler = class {
1427
1587
  this.deferredDown = null;
1428
1588
  }
1429
1589
  };
1430
- onKeyDown = (e) => {
1431
- const target = e.target;
1432
- if (target?.isContentEditable) return;
1433
- const tag = target?.tagName;
1434
- if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
1435
- if (!this.isInScope()) return;
1436
- if (e.key === " ") {
1437
- this.spaceHeld = true;
1438
- }
1439
- const action = this.shortcutMap.match(e);
1440
- if (action !== null) {
1441
- this.runAction(action, e);
1442
- }
1443
- };
1444
- onKeyUp = (e) => {
1445
- if (e.key === " ") {
1446
- this.spaceHeld = false;
1447
- if (this.activePointers.size === 0) {
1448
- if (this.lastPointerEvent) {
1449
- this.dispatchToolHover(this.lastPointerEvent);
1450
- } else {
1451
- this.toolContext?.setCursor?.("default");
1452
- }
1453
- }
1454
- }
1455
- };
1456
1590
  runAction(action, e) {
1457
- switch (action) {
1458
- case "delete":
1459
- e?.preventDefault();
1460
- this.actions.deleteSelected();
1461
- return;
1462
- case "deselect":
1463
- this.actions.deselect();
1464
- return;
1465
- case "undo":
1466
- e?.preventDefault();
1467
- this.actions.undo();
1468
- return;
1469
- case "redo":
1470
- e?.preventDefault();
1471
- this.actions.redo();
1472
- return;
1473
- case "select-all":
1474
- e?.preventDefault();
1475
- this.actions.selectAll();
1476
- return;
1477
- case "copy":
1478
- e?.preventDefault();
1479
- this.actions.copy();
1480
- return;
1481
- case "paste":
1482
- e?.preventDefault();
1483
- this.actions.paste();
1484
- return;
1485
- case "duplicate":
1486
- e?.preventDefault();
1487
- this.actions.duplicate();
1488
- return;
1489
- case "z-forward":
1490
- e?.preventDefault();
1491
- this.actions.zOrder("forward");
1492
- return;
1493
- case "z-backward":
1494
- e?.preventDefault();
1495
- this.actions.zOrder("backward");
1496
- return;
1497
- case "z-front":
1498
- e?.preventDefault();
1499
- this.actions.zOrder("front");
1500
- return;
1501
- case "z-back":
1502
- e?.preventDefault();
1503
- this.actions.zOrder("back");
1504
- return;
1505
- case "zoom-fit":
1506
- e?.preventDefault();
1507
- this.actions.zoomToFit();
1508
- return;
1509
- case "group":
1510
- e?.preventDefault();
1511
- this.actions.group();
1512
- return;
1513
- case "ungroup":
1514
- e?.preventDefault();
1515
- this.actions.ungroup();
1516
- return;
1517
- case "cut":
1518
- e?.preventDefault();
1519
- this.actions.cut();
1520
- return;
1521
- case "toggle-lock":
1522
- e?.preventDefault();
1523
- this.actions.toggleLock();
1524
- return;
1525
- case "zoom-in":
1526
- e?.preventDefault();
1527
- this.zoomByFactor(ZOOM_STEP);
1528
- return;
1529
- case "zoom-out":
1530
- e?.preventDefault();
1531
- this.zoomByFactor(1 / ZOOM_STEP);
1532
- return;
1533
- case "zoom-reset":
1534
- e?.preventDefault();
1535
- this.zoomToLevel(1);
1536
- return;
1537
- case "nudge-left":
1538
- case "nudge-right":
1539
- case "nudge-up":
1540
- case "nudge-down": {
1541
- const delta = NUDGE_DELTAS[action];
1542
- if (delta && this.actions.nudge(delta[0], delta[1], e?.shiftKey ?? false)) {
1543
- e?.preventDefault();
1544
- }
1545
- return;
1546
- }
1547
- default:
1548
- if (action.startsWith("tool:")) {
1549
- if (this.isToolActive) return;
1550
- e?.preventDefault();
1551
- this.toolContext?.switchTool?.(action.slice("tool:".length));
1552
- return;
1553
- }
1554
- console.warn(`[fieldnotes] unknown shortcut action "${action}"`);
1555
- }
1591
+ this.keyboard.runAction(action, e);
1556
1592
  }
1557
1593
  hasClipboard() {
1558
1594
  return this.actions.hasClipboard();
@@ -2320,21 +2356,53 @@ var ElementStore = class {
2320
2356
  }
2321
2357
  };
2322
2358
 
2323
- // src/elements/arrow-render-cache.ts
2324
- var cache2 = /* @__PURE__ */ new WeakMap();
2325
- function getArrowRenderGeometry(arrow) {
2326
- const hit = cache2.get(arrow);
2327
- if (hit) return hit;
2328
- const geometry = {
2329
- controlPoint: arrow.bend !== 0 ? arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend) : null,
2330
- tangentStart: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0),
2331
- tangentEnd: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1)
2332
- };
2333
- cache2.set(arrow, geometry);
2334
- return geometry;
2335
- }
2336
-
2337
- // src/elements/shape-geometry.ts
2359
+ // src/elements/rotate-canvas.ts
2360
+ function withRotation(ctx, el, center2, draw) {
2361
+ const angle = el.rotation ?? 0;
2362
+ if (angle === 0) {
2363
+ draw();
2364
+ return;
2365
+ }
2366
+ ctx.save();
2367
+ ctx.translate(center2.x, center2.y);
2368
+ ctx.rotate(angle);
2369
+ ctx.translate(-center2.x, -center2.y);
2370
+ draw();
2371
+ ctx.restore();
2372
+ }
2373
+
2374
+ // src/elements/renderers/stroke-renderer.ts
2375
+ function renderStroke(ctx, stroke) {
2376
+ if (stroke.points.length < 2) return;
2377
+ ctx.save();
2378
+ if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
2379
+ ctx.translate(stroke.position.x, stroke.position.y);
2380
+ ctx.strokeStyle = stroke.color;
2381
+ ctx.lineCap = "round";
2382
+ ctx.lineJoin = "round";
2383
+ ctx.globalAlpha = stroke.opacity;
2384
+ const data = getStrokeRenderData(stroke);
2385
+ if (data.buckets) {
2386
+ for (const bucket of data.buckets) {
2387
+ ctx.lineWidth = bucket.width;
2388
+ ctx.stroke(bucket.path);
2389
+ }
2390
+ } else {
2391
+ for (let i = 0; i < data.segments.length; i++) {
2392
+ const seg = data.segments[i];
2393
+ const w = data.widths[i];
2394
+ if (!seg || w === void 0) continue;
2395
+ ctx.lineWidth = w;
2396
+ ctx.beginPath();
2397
+ ctx.moveTo(seg.start.x, seg.start.y);
2398
+ ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
2399
+ ctx.stroke();
2400
+ }
2401
+ }
2402
+ ctx.restore();
2403
+ }
2404
+
2405
+ // src/elements/shape-geometry.ts
2338
2406
  function lineFromEndpoints(a, b) {
2339
2407
  return {
2340
2408
  position: { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y) },
@@ -2354,6 +2422,74 @@ function lineEndpoints(shape) {
2354
2422
  ];
2355
2423
  }
2356
2424
 
2425
+ // src/elements/renderers/shape-renderer.ts
2426
+ function renderShape(ctx, shape) {
2427
+ ctx.save();
2428
+ if (shape.fillColor !== "none" && shape.shape !== "line") {
2429
+ ctx.fillStyle = shape.fillColor;
2430
+ fillShapePath(ctx, shape);
2431
+ }
2432
+ if (shape.strokeWidth > 0) {
2433
+ ctx.strokeStyle = shape.strokeColor;
2434
+ ctx.lineWidth = shape.strokeWidth;
2435
+ strokeShapePath(ctx, shape);
2436
+ }
2437
+ ctx.restore();
2438
+ }
2439
+ function fillShapePath(ctx, shape) {
2440
+ switch (shape.shape) {
2441
+ case "rectangle":
2442
+ ctx.fillRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
2443
+ break;
2444
+ case "ellipse": {
2445
+ const cx = shape.position.x + shape.size.w / 2;
2446
+ const cy = shape.position.y + shape.size.h / 2;
2447
+ ctx.beginPath();
2448
+ ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
2449
+ ctx.fill();
2450
+ break;
2451
+ }
2452
+ }
2453
+ }
2454
+ function strokeShapePath(ctx, shape) {
2455
+ switch (shape.shape) {
2456
+ case "rectangle":
2457
+ ctx.strokeRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
2458
+ break;
2459
+ case "ellipse": {
2460
+ const cx = shape.position.x + shape.size.w / 2;
2461
+ const cy = shape.position.y + shape.size.h / 2;
2462
+ ctx.beginPath();
2463
+ ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
2464
+ ctx.stroke();
2465
+ break;
2466
+ }
2467
+ case "line": {
2468
+ const [a, b] = lineEndpoints(shape);
2469
+ ctx.lineCap = "round";
2470
+ ctx.beginPath();
2471
+ ctx.moveTo(a.x, a.y);
2472
+ ctx.lineTo(b.x, b.y);
2473
+ ctx.stroke();
2474
+ break;
2475
+ }
2476
+ }
2477
+ }
2478
+
2479
+ // src/elements/arrow-render-cache.ts
2480
+ var cache2 = /* @__PURE__ */ new WeakMap();
2481
+ function getArrowRenderGeometry(arrow) {
2482
+ const hit = cache2.get(arrow);
2483
+ if (hit) return hit;
2484
+ const geometry = {
2485
+ controlPoint: arrow.bend !== 0 ? arrow.cachedControlPoint ?? getArrowControlPoint(arrow.from, arrow.to, arrow.bend) : null,
2486
+ tangentStart: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 0),
2487
+ tangentEnd: getArrowTangentAngle(arrow.from, arrow.to, arrow.bend, 1)
2488
+ };
2489
+ cache2.set(arrow, geometry);
2490
+ return geometry;
2491
+ }
2492
+
2357
2493
  // src/elements/arrow-binding.ts
2358
2494
  var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
2359
2495
  function isBindable(element) {
@@ -2426,38 +2562,515 @@ function updateArrowsBoundToElements(movedIds, store) {
2426
2562
  }
2427
2563
  }
2428
2564
  }
2429
- function updateBoundArrow(arrow, store) {
2430
- if (!arrow.fromBinding && !arrow.toBinding) return null;
2431
- const updates = {};
2432
- if (arrow.fromBinding) {
2433
- const el = store.getById(arrow.fromBinding.elementId);
2434
- if (el) {
2435
- const center2 = getElementCenter(el);
2436
- updates.from = center2;
2437
- updates.position = center2;
2438
- }
2565
+ function updateBoundArrow(arrow, store) {
2566
+ if (!arrow.fromBinding && !arrow.toBinding) return null;
2567
+ const updates = {};
2568
+ if (arrow.fromBinding) {
2569
+ const el = store.getById(arrow.fromBinding.elementId);
2570
+ if (el) {
2571
+ const center2 = getElementCenter(el);
2572
+ updates.from = center2;
2573
+ updates.position = center2;
2574
+ }
2575
+ }
2576
+ if (arrow.toBinding) {
2577
+ const el = store.getById(arrow.toBinding.elementId);
2578
+ if (el) {
2579
+ updates.to = getElementCenter(el);
2580
+ }
2581
+ }
2582
+ return Object.keys(updates).length > 0 ? updates : null;
2583
+ }
2584
+
2585
+ // src/elements/renderers/arrow-renderer.ts
2586
+ var ARROWHEAD_LENGTH = 12;
2587
+ var ARROWHEAD_ANGLE = Math.PI / 6;
2588
+ var ARROW_LABEL_FONT_SIZE = 14;
2589
+ function renderArrow(ctx, arrow, store, labelEditingId) {
2590
+ const geometry = getArrowRenderGeometry(arrow);
2591
+ const { visualFrom, visualTo } = getVisualEndpoints(arrow, geometry, store);
2592
+ ctx.save();
2593
+ ctx.strokeStyle = arrow.color;
2594
+ ctx.lineWidth = arrow.width;
2595
+ ctx.lineCap = "round";
2596
+ if (arrow.fromBinding || arrow.toBinding) {
2597
+ ctx.setLineDash([8, 4]);
2598
+ }
2599
+ ctx.beginPath();
2600
+ ctx.moveTo(visualFrom.x, visualFrom.y);
2601
+ if (arrow.bend !== 0) {
2602
+ const cp = geometry.controlPoint;
2603
+ if (cp) {
2604
+ ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
2605
+ }
2606
+ } else {
2607
+ ctx.lineTo(visualTo.x, visualTo.y);
2608
+ }
2609
+ ctx.stroke();
2610
+ renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2611
+ ctx.restore();
2612
+ renderArrowLabel(ctx, arrow, labelEditingId);
2613
+ }
2614
+ function renderArrowLabel(ctx, arrow, labelEditingId) {
2615
+ if (!arrow.label || arrow.label.length === 0) return;
2616
+ if (arrow.id === labelEditingId) return;
2617
+ const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
2618
+ ctx.save();
2619
+ ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
2620
+ const metrics = ctx.measureText(arrow.label);
2621
+ const padX = 6;
2622
+ const padY = 4;
2623
+ const w = metrics.width + padX * 2;
2624
+ const h = ARROW_LABEL_FONT_SIZE + padY * 2;
2625
+ ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
2626
+ ctx.beginPath();
2627
+ ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
2628
+ ctx.fill();
2629
+ ctx.fillStyle = "#1a1a1a";
2630
+ ctx.textAlign = "center";
2631
+ ctx.textBaseline = "middle";
2632
+ ctx.fillText(arrow.label, mid.x, mid.y);
2633
+ ctx.restore();
2634
+ }
2635
+ function renderArrowhead(ctx, arrow, tip, angle) {
2636
+ ctx.beginPath();
2637
+ ctx.moveTo(tip.x, tip.y);
2638
+ ctx.lineTo(
2639
+ tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
2640
+ tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
2641
+ );
2642
+ ctx.lineTo(
2643
+ tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
2644
+ tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
2645
+ );
2646
+ ctx.closePath();
2647
+ ctx.fillStyle = arrow.color;
2648
+ ctx.fill();
2649
+ }
2650
+ function getVisualEndpoints(arrow, geometry, store) {
2651
+ let visualFrom = arrow.from;
2652
+ let visualTo = arrow.to;
2653
+ if (!store) return { visualFrom, visualTo };
2654
+ if (arrow.fromBinding) {
2655
+ const el = store.getById(arrow.fromBinding.elementId);
2656
+ if (el) {
2657
+ const bounds = getElementBounds(el);
2658
+ if (bounds) {
2659
+ const tangentAngle = geometry.tangentStart;
2660
+ const rayTarget = {
2661
+ x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
2662
+ y: arrow.from.y + Math.sin(tangentAngle) * 1e3
2663
+ };
2664
+ visualFrom = getEdgeIntersection(bounds, rayTarget);
2665
+ }
2666
+ }
2667
+ }
2668
+ if (arrow.toBinding) {
2669
+ const el = store.getById(arrow.toBinding.elementId);
2670
+ if (el) {
2671
+ const bounds = getElementBounds(el);
2672
+ if (bounds) {
2673
+ const tangentAngle = geometry.tangentEnd;
2674
+ const rayTarget = {
2675
+ x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
2676
+ y: arrow.to.y - Math.sin(tangentAngle) * 1e3
2677
+ };
2678
+ visualTo = getEdgeIntersection(bounds, rayTarget);
2679
+ }
2680
+ }
2681
+ }
2682
+ return { visualFrom, visualTo };
2683
+ }
2684
+
2685
+ // src/elements/renderers/image-renderer.ts
2686
+ function renderImage(ctx, image, imageCache, onImageLoad, onImageError) {
2687
+ if (imageCache.get(image.src) === "failed") {
2688
+ renderImagePlaceholder(ctx, image);
2689
+ return;
2690
+ }
2691
+ const img = getImage(image.src, imageCache, onImageLoad, onImageError);
2692
+ if (!img) return;
2693
+ ctx.drawImage(
2694
+ img,
2695
+ image.position.x,
2696
+ image.position.y,
2697
+ image.size.w,
2698
+ image.size.h
2699
+ );
2700
+ }
2701
+ function renderImagePlaceholder(ctx, image) {
2702
+ const { x, y } = image.position;
2703
+ const { w, h } = image.size;
2704
+ ctx.save();
2705
+ ctx.fillStyle = "#eeeeee";
2706
+ ctx.fillRect(x, y, w, h);
2707
+ ctx.strokeStyle = "#bdbdbd";
2708
+ ctx.lineWidth = 1;
2709
+ ctx.strokeRect(x, y, w, h);
2710
+ const glyph = Math.min(24, w / 2, h / 2);
2711
+ const cx = x + w / 2;
2712
+ const cy = y + h / 2;
2713
+ ctx.strokeStyle = "#9e9e9e";
2714
+ ctx.lineWidth = 2;
2715
+ ctx.beginPath();
2716
+ ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
2717
+ ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
2718
+ ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
2719
+ ctx.stroke();
2720
+ ctx.restore();
2721
+ }
2722
+ function getImage(src, imageCache, onImageLoad, onImageError) {
2723
+ const cached = imageCache.get(src);
2724
+ if (cached) {
2725
+ if (cached === "failed") return null;
2726
+ if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
2727
+ return cached;
2728
+ }
2729
+ const img = new Image();
2730
+ img.src = src;
2731
+ imageCache.set(src, img);
2732
+ img.onload = () => {
2733
+ onImageLoad?.();
2734
+ if (typeof createImageBitmap !== "undefined") {
2735
+ createImageBitmap(img).then((bitmap) => {
2736
+ imageCache.set(src, bitmap);
2737
+ onImageLoad?.();
2738
+ }).catch(() => {
2739
+ });
2740
+ }
2741
+ };
2742
+ img.onerror = (event) => {
2743
+ imageCache.set(src, "failed");
2744
+ onImageError?.(src, event);
2745
+ onImageLoad?.();
2746
+ };
2747
+ return null;
2748
+ }
2749
+
2750
+ // src/elements/hex-fill.ts
2751
+ function offsetToCube(col, row, orientation) {
2752
+ if (orientation === "pointy") {
2753
+ return { q: col - (row - (row & 1)) / 2, r: row };
2754
+ }
2755
+ return { q: col, r: row - (col - (col & 1)) / 2 };
2756
+ }
2757
+ function cubeToOffset(q, r, orientation) {
2758
+ if (orientation === "pointy") {
2759
+ return { col: q + (r - (r & 1)) / 2, row: r };
2760
+ }
2761
+ return { col: q, row: r + (q - (q & 1)) / 2 };
2762
+ }
2763
+ function offsetToPixel(col, row, cellSize, orientation) {
2764
+ if (orientation === "pointy") {
2765
+ const hexW = Math.sqrt(3) * cellSize;
2766
+ const rowH = 1.5 * cellSize;
2767
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2768
+ return { x: col * hexW + offsetX, y: row * rowH };
2769
+ }
2770
+ const hexH = Math.sqrt(3) * cellSize;
2771
+ const colW = 1.5 * cellSize;
2772
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2773
+ return { x: col * colW, y: row * hexH + offsetY };
2774
+ }
2775
+ function pixelToOffset(x, y, cellSize, orientation) {
2776
+ if (orientation === "pointy") {
2777
+ const hexW = Math.sqrt(3) * cellSize;
2778
+ const rowH = 1.5 * cellSize;
2779
+ const row = Math.round(y / rowH);
2780
+ const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2781
+ return { col: Math.round((x - offsetX) / hexW), row };
2782
+ }
2783
+ const hexH = Math.sqrt(3) * cellSize;
2784
+ const colW = 1.5 * cellSize;
2785
+ const col = Math.round(x / colW);
2786
+ const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2787
+ return { col, row: Math.round((y - offsetY) / hexH) };
2788
+ }
2789
+ function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
2790
+ const cells = [];
2791
+ for (let dq = -n; dq <= n; dq++) {
2792
+ const rMin = Math.max(-n, -dq - n);
2793
+ const rMax = Math.min(n, -dq + n);
2794
+ for (let dr = rMin; dr <= rMax; dr++) {
2795
+ const absQ = centerQ + dq;
2796
+ const absR = centerR + dr;
2797
+ const off = cubeToOffset(absQ, absR, orientation);
2798
+ cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
2799
+ }
2800
+ }
2801
+ return cells;
2802
+ }
2803
+ function getHexDistance(a, b, cellSize, orientation) {
2804
+ const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
2805
+ const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
2806
+ const cubeA = offsetToCube(offA.col, offA.row, orientation);
2807
+ const cubeB = offsetToCube(offB.col, offB.row, orientation);
2808
+ const dq = cubeA.q - cubeB.q;
2809
+ const dr = cubeA.r - cubeB.r;
2810
+ const ds = -dq - dr;
2811
+ return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2812
+ }
2813
+ function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2814
+ const n = Math.round(radiusCells);
2815
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2816
+ const cube = offsetToCube(off.col, off.row, orientation);
2817
+ if (n <= 0) {
2818
+ return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2819
+ }
2820
+ return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2821
+ }
2822
+ function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2823
+ const n = Math.round(radiusCells);
2824
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2825
+ const cube = offsetToCube(off.col, off.row, orientation);
2826
+ const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2827
+ if (n <= 0) return [centerPixel];
2828
+ const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2829
+ const step = Math.PI / 3;
2830
+ const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2831
+ const halfAngle = Math.PI / 6 + 1e-6;
2832
+ const cells = [centerPixel];
2833
+ for (let dq = -n; dq <= n; dq++) {
2834
+ const rMin = Math.max(-n, -dq - n);
2835
+ const rMax = Math.min(n, -dq + n);
2836
+ for (let dr = rMin; dr <= rMax; dr++) {
2837
+ if (dq === 0 && dr === 0) continue;
2838
+ const absQ = cube.q + dq;
2839
+ const absR = cube.r + dr;
2840
+ const pixel = offsetToPixel(
2841
+ cubeToOffset(absQ, absR, orientation).col,
2842
+ cubeToOffset(absQ, absR, orientation).row,
2843
+ cellSize,
2844
+ orientation
2845
+ );
2846
+ const dx = pixel.x - centerPixel.x;
2847
+ const dy = pixel.y - centerPixel.y;
2848
+ let diff = Math.atan2(dy, dx) - snappedAngle;
2849
+ if (diff > Math.PI) diff -= 2 * Math.PI;
2850
+ if (diff < -Math.PI) diff += 2 * Math.PI;
2851
+ if (Math.abs(diff) <= halfAngle) {
2852
+ cells.push(pixel);
2853
+ }
2854
+ }
2855
+ }
2856
+ return cells;
2857
+ }
2858
+ function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2859
+ const n = Math.round(radiusCells);
2860
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2861
+ const cube = offsetToCube(off.col, off.row, orientation);
2862
+ const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2863
+ if (n <= 0) return [centerPixel];
2864
+ const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2865
+ const step = Math.PI / 3;
2866
+ const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2867
+ const cos = Math.cos(snappedAngle);
2868
+ const sin = Math.sin(snappedAngle);
2869
+ const snapUnit = Math.sqrt(3) * cellSize;
2870
+ const lineLength = n * snapUnit;
2871
+ const halfWidth = snapUnit * 0.5 + 1e-6;
2872
+ const cells = [];
2873
+ for (let dq = -n; dq <= n; dq++) {
2874
+ const rMin = Math.max(-n, -dq - n);
2875
+ const rMax = Math.min(n, -dq + n);
2876
+ for (let dr = rMin; dr <= rMax; dr++) {
2877
+ const absQ = cube.q + dq;
2878
+ const absR = cube.r + dr;
2879
+ const pixel = offsetToPixel(
2880
+ cubeToOffset(absQ, absR, orientation).col,
2881
+ cubeToOffset(absQ, absR, orientation).row,
2882
+ cellSize,
2883
+ orientation
2884
+ );
2885
+ const dx = pixel.x - centerPixel.x;
2886
+ const dy = pixel.y - centerPixel.y;
2887
+ const along = dx * cos + dy * sin;
2888
+ const perp = Math.abs(-dx * sin + dy * cos);
2889
+ if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
2890
+ cells.push(pixel);
2891
+ }
2892
+ }
2893
+ }
2894
+ return cells;
2895
+ }
2896
+ function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2897
+ const n = Math.round(radiusCells);
2898
+ const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2899
+ const cube = offsetToCube(off.col, off.row, orientation);
2900
+ const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2901
+ if (n <= 0) return [centerPixel];
2902
+ const snapUnit = Math.sqrt(3) * cellSize;
2903
+ const halfSide = n * snapUnit / 2;
2904
+ const cells = [];
2905
+ for (let dq = -n; dq <= n; dq++) {
2906
+ const rMin = Math.max(-n, -dq - n);
2907
+ const rMax = Math.min(n, -dq + n);
2908
+ for (let dr = rMin; dr <= rMax; dr++) {
2909
+ const absQ = cube.q + dq;
2910
+ const absR = cube.r + dr;
2911
+ const pixel = offsetToPixel(
2912
+ cubeToOffset(absQ, absR, orientation).col,
2913
+ cubeToOffset(absQ, absR, orientation).row,
2914
+ cellSize,
2915
+ orientation
2916
+ );
2917
+ if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
2918
+ cells.push(pixel);
2919
+ }
2920
+ }
2921
+ }
2922
+ return cells;
2923
+ }
2924
+ function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2925
+ const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2926
+ ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
2927
+ for (let i = 1; i < 6; i++) {
2928
+ const a = angleOffset + Math.PI / 3 * i;
2929
+ ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
2930
+ }
2931
+ ctx.closePath();
2932
+ }
2933
+
2934
+ // src/elements/renderers/template-renderer.ts
2935
+ function renderTemplate(ctx, template, store) {
2936
+ const grid = store?.getElementsByType("grid")[0];
2937
+ if (grid && grid.gridType === "hex") {
2938
+ renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
2939
+ return;
2940
+ }
2941
+ renderGeometricTemplate(ctx, template);
2942
+ }
2943
+ function renderGeometricTemplate(ctx, template) {
2944
+ const { x: cx, y: cy } = template.position;
2945
+ const r = template.radius;
2946
+ ctx.save();
2947
+ ctx.globalAlpha = template.opacity;
2948
+ ctx.fillStyle = template.fillColor;
2949
+ ctx.strokeStyle = template.strokeColor;
2950
+ ctx.lineWidth = template.strokeWidth;
2951
+ switch (template.templateShape) {
2952
+ case "circle":
2953
+ ctx.beginPath();
2954
+ ctx.arc(cx, cy, r, 0, Math.PI * 2);
2955
+ ctx.fill();
2956
+ ctx.stroke();
2957
+ if (template.radiusFeet != null && template.radiusFeet > 0) {
2958
+ renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
2959
+ }
2960
+ break;
2961
+ case "square":
2962
+ ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
2963
+ ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
2964
+ break;
2965
+ case "cone": {
2966
+ const halfAngle = Math.atan(0.5);
2967
+ ctx.beginPath();
2968
+ ctx.moveTo(cx, cy);
2969
+ ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
2970
+ ctx.closePath();
2971
+ ctx.fill();
2972
+ ctx.stroke();
2973
+ break;
2974
+ }
2975
+ case "line": {
2976
+ const halfW = r / 12;
2977
+ const cos = Math.cos(template.angle);
2978
+ const sin = Math.sin(template.angle);
2979
+ const perpX = -sin * halfW;
2980
+ const perpY = cos * halfW;
2981
+ ctx.beginPath();
2982
+ ctx.moveTo(cx + perpX, cy + perpY);
2983
+ ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
2984
+ ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
2985
+ ctx.lineTo(cx - perpX, cy - perpY);
2986
+ ctx.closePath();
2987
+ ctx.fill();
2988
+ ctx.stroke();
2989
+ break;
2990
+ }
2991
+ }
2992
+ ctx.restore();
2993
+ }
2994
+ function renderHexTemplate(ctx, template, cellSize, orientation) {
2995
+ const snapUnit = Math.sqrt(3) * cellSize;
2996
+ const radiusCells = template.radius / snapUnit;
2997
+ const center2 = template.position;
2998
+ let cells;
2999
+ switch (template.templateShape) {
3000
+ case "circle":
3001
+ cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
3002
+ break;
3003
+ case "cone":
3004
+ cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
3005
+ break;
3006
+ case "line":
3007
+ cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
3008
+ break;
3009
+ case "square":
3010
+ cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3011
+ break;
2439
3012
  }
2440
- if (arrow.toBinding) {
2441
- const el = store.getById(arrow.toBinding.elementId);
2442
- if (el) {
2443
- updates.to = getElementCenter(el);
2444
- }
3013
+ ctx.save();
3014
+ ctx.globalAlpha = template.opacity;
3015
+ ctx.beginPath();
3016
+ for (const cell of cells) {
3017
+ drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
2445
3018
  }
2446
- return Object.keys(updates).length > 0 ? updates : null;
2447
- }
2448
-
2449
- // src/elements/rotate-canvas.ts
2450
- function withRotation(ctx, el, center2, draw) {
2451
- const angle = el.rotation ?? 0;
2452
- if (angle === 0) {
2453
- draw();
2454
- return;
3019
+ ctx.fillStyle = template.fillColor;
3020
+ ctx.fill();
3021
+ ctx.beginPath();
3022
+ for (const cell of cells) {
3023
+ drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3024
+ }
3025
+ ctx.strokeStyle = template.strokeColor;
3026
+ ctx.lineWidth = template.strokeWidth;
3027
+ ctx.stroke();
3028
+ {
3029
+ ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
3030
+ ctx.beginPath();
3031
+ drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
3032
+ ctx.fillStyle = template.strokeColor;
3033
+ ctx.fill();
3034
+ ctx.strokeStyle = template.strokeColor;
3035
+ ctx.lineWidth = template.strokeWidth;
3036
+ ctx.stroke();
3037
+ }
3038
+ if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3039
+ const r = template.radius;
3040
+ renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
2455
3041
  }
3042
+ ctx.restore();
3043
+ }
3044
+ function renderRadiusMarker(ctx, cx, cy, r, feet) {
3045
+ const markerColor = ctx.strokeStyle;
2456
3046
  ctx.save();
2457
- ctx.translate(center2.x, center2.y);
2458
- ctx.rotate(angle);
2459
- ctx.translate(-center2.x, -center2.y);
2460
- draw();
3047
+ ctx.globalAlpha = 1;
3048
+ ctx.beginPath();
3049
+ ctx.setLineDash([4, 4]);
3050
+ ctx.strokeStyle = markerColor;
3051
+ ctx.lineWidth = 1.5;
3052
+ ctx.moveTo(cx, cy);
3053
+ ctx.lineTo(cx + r, cy);
3054
+ ctx.stroke();
3055
+ ctx.setLineDash([]);
3056
+ const label = `${Math.round(feet)} ft`;
3057
+ const fontSize = Math.max(10, Math.min(14, r * 0.15));
3058
+ ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
3059
+ ctx.textAlign = "center";
3060
+ ctx.textBaseline = "bottom";
3061
+ const textX = cx + r / 2;
3062
+ const textY = cy - 4;
3063
+ const metrics = ctx.measureText(label);
3064
+ const padX = 4;
3065
+ const padY = 2;
3066
+ const textW = metrics.width + padX * 2;
3067
+ const textH = fontSize + padY * 2;
3068
+ ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3069
+ ctx.beginPath();
3070
+ ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
3071
+ ctx.fill();
3072
+ ctx.fillStyle = markerColor;
3073
+ ctx.fillText(label, textX, textY - padY);
2461
3074
  ctx.restore();
2462
3075
  }
2463
3076
 
@@ -2611,245 +3224,58 @@ function createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opac
2611
3224
  tc.globalAlpha = opacity;
2612
3225
  tc.beginPath();
2613
3226
  if (orientation === "pointy") {
2614
- const hexW = tileW;
2615
- const rowH = 1.5 * cellSize;
2616
- for (let row = -1; row <= 3; row++) {
2617
- const offX = row % 2 !== 0 ? hexW / 2 : 0;
2618
- for (let col = -1; col <= 1; col++) {
2619
- const cx = col * hexW + offX;
2620
- const cy = row * rowH;
2621
- tc.moveTo(cx + ox0, cy + oy0);
2622
- tc.lineTo(cx + ox1, cy + oy1);
2623
- tc.lineTo(cx + ox2, cy + oy2);
2624
- tc.lineTo(cx + ox3, cy + oy3);
2625
- tc.lineTo(cx + ox4, cy + oy4);
2626
- tc.lineTo(cx + ox5, cy + oy5);
2627
- tc.closePath();
2628
- }
2629
- }
2630
- } else {
2631
- const hexH = tileH;
2632
- const colW = 1.5 * cellSize;
2633
- for (let col = -1; col <= 3; col++) {
2634
- const offY = col % 2 !== 0 ? hexH / 2 : 0;
2635
- for (let row = -1; row <= 1; row++) {
2636
- const cx = col * colW;
2637
- const cy = row * hexH + offY;
2638
- tc.moveTo(cx + ox0, cy + oy0);
2639
- tc.lineTo(cx + ox1, cy + oy1);
2640
- tc.lineTo(cx + ox2, cy + oy2);
2641
- tc.lineTo(cx + ox3, cy + oy3);
2642
- tc.lineTo(cx + ox4, cy + oy4);
2643
- tc.lineTo(cx + ox5, cy + oy5);
2644
- tc.closePath();
2645
- }
2646
- }
2647
- }
2648
- tc.stroke();
2649
- return { canvas, tileW, tileH };
2650
- }
2651
- function renderHexGridTiled(ctx, bounds, cellSize, tile) {
2652
- const { tileW, tileH } = tile;
2653
- const startCol = Math.floor(bounds.minX / tileW) - 1;
2654
- const endCol = Math.ceil(bounds.maxX / tileW) + 1;
2655
- const startRow = Math.floor(bounds.minY / tileH) - 1;
2656
- const endRow = Math.ceil(bounds.maxY / tileH) + 1;
2657
- for (let row = startRow; row <= endRow; row++) {
2658
- for (let col = startCol; col <= endCol; col++) {
2659
- ctx.drawImage(tile.canvas, col * tileW, row * tileH, tileW, tileH);
2660
- }
2661
- }
2662
- }
2663
-
2664
- // src/elements/hex-fill.ts
2665
- function offsetToCube(col, row, orientation) {
2666
- if (orientation === "pointy") {
2667
- return { q: col - (row - (row & 1)) / 2, r: row };
2668
- }
2669
- return { q: col, r: row - (col - (col & 1)) / 2 };
2670
- }
2671
- function cubeToOffset(q, r, orientation) {
2672
- if (orientation === "pointy") {
2673
- return { col: q + (r - (r & 1)) / 2, row: r };
2674
- }
2675
- return { col: q, row: r + (q - (q & 1)) / 2 };
2676
- }
2677
- function offsetToPixel(col, row, cellSize, orientation) {
2678
- if (orientation === "pointy") {
2679
- const hexW = Math.sqrt(3) * cellSize;
2680
- const rowH = 1.5 * cellSize;
2681
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2682
- return { x: col * hexW + offsetX, y: row * rowH };
2683
- }
2684
- const hexH = Math.sqrt(3) * cellSize;
2685
- const colW = 1.5 * cellSize;
2686
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2687
- return { x: col * colW, y: row * hexH + offsetY };
2688
- }
2689
- function pixelToOffset(x, y, cellSize, orientation) {
2690
- if (orientation === "pointy") {
2691
- const hexW = Math.sqrt(3) * cellSize;
2692
- const rowH = 1.5 * cellSize;
2693
- const row = Math.round(y / rowH);
2694
- const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
2695
- return { col: Math.round((x - offsetX) / hexW), row };
2696
- }
2697
- const hexH = Math.sqrt(3) * cellSize;
2698
- const colW = 1.5 * cellSize;
2699
- const col = Math.round(x / colW);
2700
- const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
2701
- return { col, row: Math.round((y - offsetY) / hexH) };
2702
- }
2703
- function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
2704
- const cells = [];
2705
- for (let dq = -n; dq <= n; dq++) {
2706
- const rMin = Math.max(-n, -dq - n);
2707
- const rMax = Math.min(n, -dq + n);
2708
- for (let dr = rMin; dr <= rMax; dr++) {
2709
- const absQ = centerQ + dq;
2710
- const absR = centerR + dr;
2711
- const off = cubeToOffset(absQ, absR, orientation);
2712
- cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
2713
- }
2714
- }
2715
- return cells;
2716
- }
2717
- function getHexDistance(a, b, cellSize, orientation) {
2718
- const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
2719
- const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
2720
- const cubeA = offsetToCube(offA.col, offA.row, orientation);
2721
- const cubeB = offsetToCube(offB.col, offB.row, orientation);
2722
- const dq = cubeA.q - cubeB.q;
2723
- const dr = cubeA.r - cubeB.r;
2724
- const ds = -dq - dr;
2725
- return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
2726
- }
2727
- function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
2728
- const n = Math.round(radiusCells);
2729
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2730
- const cube = offsetToCube(off.col, off.row, orientation);
2731
- if (n <= 0) {
2732
- return [offsetToPixel(off.col, off.row, cellSize, orientation)];
2733
- }
2734
- return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
2735
- }
2736
- function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
2737
- const n = Math.round(radiusCells);
2738
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2739
- const cube = offsetToCube(off.col, off.row, orientation);
2740
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2741
- if (n <= 0) return [centerPixel];
2742
- const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2743
- const step = Math.PI / 3;
2744
- const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2745
- const halfAngle = Math.PI / 6 + 1e-6;
2746
- const cells = [centerPixel];
2747
- for (let dq = -n; dq <= n; dq++) {
2748
- const rMin = Math.max(-n, -dq - n);
2749
- const rMax = Math.min(n, -dq + n);
2750
- for (let dr = rMin; dr <= rMax; dr++) {
2751
- if (dq === 0 && dr === 0) continue;
2752
- const absQ = cube.q + dq;
2753
- const absR = cube.r + dr;
2754
- const pixel = offsetToPixel(
2755
- cubeToOffset(absQ, absR, orientation).col,
2756
- cubeToOffset(absQ, absR, orientation).row,
2757
- cellSize,
2758
- orientation
2759
- );
2760
- const dx = pixel.x - centerPixel.x;
2761
- const dy = pixel.y - centerPixel.y;
2762
- let diff = Math.atan2(dy, dx) - snappedAngle;
2763
- if (diff > Math.PI) diff -= 2 * Math.PI;
2764
- if (diff < -Math.PI) diff += 2 * Math.PI;
2765
- if (Math.abs(diff) <= halfAngle) {
2766
- cells.push(pixel);
2767
- }
2768
- }
2769
- }
2770
- return cells;
2771
- }
2772
- function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
2773
- const n = Math.round(radiusCells);
2774
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2775
- const cube = offsetToCube(off.col, off.row, orientation);
2776
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2777
- if (n <= 0) return [centerPixel];
2778
- const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2779
- const step = Math.PI / 3;
2780
- const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
2781
- const cos = Math.cos(snappedAngle);
2782
- const sin = Math.sin(snappedAngle);
2783
- const snapUnit = Math.sqrt(3) * cellSize;
2784
- const lineLength = n * snapUnit;
2785
- const halfWidth = snapUnit * 0.5 + 1e-6;
2786
- const cells = [];
2787
- for (let dq = -n; dq <= n; dq++) {
2788
- const rMin = Math.max(-n, -dq - n);
2789
- const rMax = Math.min(n, -dq + n);
2790
- for (let dr = rMin; dr <= rMax; dr++) {
2791
- const absQ = cube.q + dq;
2792
- const absR = cube.r + dr;
2793
- const pixel = offsetToPixel(
2794
- cubeToOffset(absQ, absR, orientation).col,
2795
- cubeToOffset(absQ, absR, orientation).row,
2796
- cellSize,
2797
- orientation
2798
- );
2799
- const dx = pixel.x - centerPixel.x;
2800
- const dy = pixel.y - centerPixel.y;
2801
- const along = dx * cos + dy * sin;
2802
- const perp = Math.abs(-dx * sin + dy * cos);
2803
- if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
2804
- cells.push(pixel);
3227
+ const hexW = tileW;
3228
+ const rowH = 1.5 * cellSize;
3229
+ for (let row = -1; row <= 3; row++) {
3230
+ const offX = row % 2 !== 0 ? hexW / 2 : 0;
3231
+ for (let col = -1; col <= 1; col++) {
3232
+ const cx = col * hexW + offX;
3233
+ const cy = row * rowH;
3234
+ tc.moveTo(cx + ox0, cy + oy0);
3235
+ tc.lineTo(cx + ox1, cy + oy1);
3236
+ tc.lineTo(cx + ox2, cy + oy2);
3237
+ tc.lineTo(cx + ox3, cy + oy3);
3238
+ tc.lineTo(cx + ox4, cy + oy4);
3239
+ tc.lineTo(cx + ox5, cy + oy5);
3240
+ tc.closePath();
2805
3241
  }
2806
3242
  }
2807
- }
2808
- return cells;
2809
- }
2810
- function getHexCellsInSquare(center2, radiusCells, cellSize, orientation) {
2811
- const n = Math.round(radiusCells);
2812
- const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
2813
- const cube = offsetToCube(off.col, off.row, orientation);
2814
- const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
2815
- if (n <= 0) return [centerPixel];
2816
- const snapUnit = Math.sqrt(3) * cellSize;
2817
- const halfSide = n * snapUnit / 2;
2818
- const cells = [];
2819
- for (let dq = -n; dq <= n; dq++) {
2820
- const rMin = Math.max(-n, -dq - n);
2821
- const rMax = Math.min(n, -dq + n);
2822
- for (let dr = rMin; dr <= rMax; dr++) {
2823
- const absQ = cube.q + dq;
2824
- const absR = cube.r + dr;
2825
- const pixel = offsetToPixel(
2826
- cubeToOffset(absQ, absR, orientation).col,
2827
- cubeToOffset(absQ, absR, orientation).row,
2828
- cellSize,
2829
- orientation
2830
- );
2831
- if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
2832
- cells.push(pixel);
3243
+ } else {
3244
+ const hexH = tileH;
3245
+ const colW = 1.5 * cellSize;
3246
+ for (let col = -1; col <= 3; col++) {
3247
+ const offY = col % 2 !== 0 ? hexH / 2 : 0;
3248
+ for (let row = -1; row <= 1; row++) {
3249
+ const cx = col * colW;
3250
+ const cy = row * hexH + offY;
3251
+ tc.moveTo(cx + ox0, cy + oy0);
3252
+ tc.lineTo(cx + ox1, cy + oy1);
3253
+ tc.lineTo(cx + ox2, cy + oy2);
3254
+ tc.lineTo(cx + ox3, cy + oy3);
3255
+ tc.lineTo(cx + ox4, cy + oy4);
3256
+ tc.lineTo(cx + ox5, cy + oy5);
3257
+ tc.closePath();
2833
3258
  }
2834
3259
  }
2835
3260
  }
2836
- return cells;
3261
+ tc.stroke();
3262
+ return { canvas, tileW, tileH };
2837
3263
  }
2838
- function drawHexPath(ctx, cx, cy, cellSize, orientation) {
2839
- const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
2840
- ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
2841
- for (let i = 1; i < 6; i++) {
2842
- const a = angleOffset + Math.PI / 3 * i;
2843
- ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
3264
+ function renderHexGridTiled(ctx, bounds, cellSize, tile) {
3265
+ const { tileW, tileH } = tile;
3266
+ const startCol = Math.floor(bounds.minX / tileW) - 1;
3267
+ const endCol = Math.ceil(bounds.maxX / tileW) + 1;
3268
+ const startRow = Math.floor(bounds.minY / tileH) - 1;
3269
+ const endRow = Math.ceil(bounds.maxY / tileH) + 1;
3270
+ for (let row = startRow; row <= endRow; row++) {
3271
+ for (let col = startCol; col <= endCol; col++) {
3272
+ ctx.drawImage(tile.canvas, col * tileW, row * tileH, tileW, tileH);
3273
+ }
2844
3274
  }
2845
- ctx.closePath();
2846
3275
  }
2847
3276
 
2848
3277
  // src/elements/element-renderer.ts
2849
3278
  var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
2850
- var ARROWHEAD_LENGTH = 12;
2851
- var ARROWHEAD_ANGLE = Math.PI / 6;
2852
- var ARROW_LABEL_FONT_SIZE = 14;
2853
3279
  var ElementRenderer = class {
2854
3280
  store = null;
2855
3281
  imageCache = /* @__PURE__ */ new Map();
@@ -2890,206 +3316,35 @@ var ElementRenderer = class {
2890
3316
  case "stroke": {
2891
3317
  const b = getElementBounds(element);
2892
3318
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2893
- withRotation(ctx, element, c, () => this.renderStroke(ctx, element));
3319
+ withRotation(ctx, element, c, () => renderStroke(ctx, element));
2894
3320
  break;
2895
3321
  }
2896
3322
  case "arrow":
2897
- this.renderArrow(ctx, element);
3323
+ renderArrow(ctx, element, this.store, this.labelEditingId);
2898
3324
  break;
2899
3325
  case "shape": {
2900
3326
  const b = getElementBounds(element);
2901
3327
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2902
- withRotation(ctx, element, c, () => this.renderShape(ctx, element));
3328
+ withRotation(ctx, element, c, () => renderShape(ctx, element));
2903
3329
  break;
2904
3330
  }
2905
3331
  case "image": {
2906
3332
  const b = getElementBounds(element);
2907
3333
  const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
2908
- withRotation(ctx, element, c, () => this.renderImage(ctx, element));
3334
+ withRotation(
3335
+ ctx,
3336
+ element,
3337
+ c,
3338
+ () => renderImage(ctx, element, this.imageCache, this.onImageLoad, this.onImageError)
3339
+ );
2909
3340
  break;
2910
3341
  }
2911
3342
  case "grid":
2912
3343
  this.renderGrid(ctx, element);
2913
3344
  break;
2914
3345
  case "template":
2915
- this.renderTemplate(ctx, element);
2916
- break;
2917
- }
2918
- }
2919
- renderStroke(ctx, stroke) {
2920
- if (stroke.points.length < 2) return;
2921
- ctx.save();
2922
- if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
2923
- ctx.translate(stroke.position.x, stroke.position.y);
2924
- ctx.strokeStyle = stroke.color;
2925
- ctx.lineCap = "round";
2926
- ctx.lineJoin = "round";
2927
- ctx.globalAlpha = stroke.opacity;
2928
- const data = getStrokeRenderData(stroke);
2929
- if (data.buckets) {
2930
- for (const bucket of data.buckets) {
2931
- ctx.lineWidth = bucket.width;
2932
- ctx.stroke(bucket.path);
2933
- }
2934
- } else {
2935
- for (let i = 0; i < data.segments.length; i++) {
2936
- const seg = data.segments[i];
2937
- const w = data.widths[i];
2938
- if (!seg || w === void 0) continue;
2939
- ctx.lineWidth = w;
2940
- ctx.beginPath();
2941
- ctx.moveTo(seg.start.x, seg.start.y);
2942
- ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
2943
- ctx.stroke();
2944
- }
2945
- }
2946
- ctx.restore();
2947
- }
2948
- renderArrow(ctx, arrow) {
2949
- const geometry = getArrowRenderGeometry(arrow);
2950
- const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
2951
- ctx.save();
2952
- ctx.strokeStyle = arrow.color;
2953
- ctx.lineWidth = arrow.width;
2954
- ctx.lineCap = "round";
2955
- if (arrow.fromBinding || arrow.toBinding) {
2956
- ctx.setLineDash([8, 4]);
2957
- }
2958
- ctx.beginPath();
2959
- ctx.moveTo(visualFrom.x, visualFrom.y);
2960
- if (arrow.bend !== 0) {
2961
- const cp = geometry.controlPoint;
2962
- if (cp) {
2963
- ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
2964
- }
2965
- } else {
2966
- ctx.lineTo(visualTo.x, visualTo.y);
2967
- }
2968
- ctx.stroke();
2969
- this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
2970
- ctx.restore();
2971
- this.renderArrowLabel(ctx, arrow);
2972
- }
2973
- renderArrowLabel(ctx, arrow) {
2974
- if (!arrow.label || arrow.label.length === 0) return;
2975
- if (arrow.id === this.labelEditingId) return;
2976
- const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
2977
- ctx.save();
2978
- ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
2979
- const metrics = ctx.measureText(arrow.label);
2980
- const padX = 6;
2981
- const padY = 4;
2982
- const w = metrics.width + padX * 2;
2983
- const h = ARROW_LABEL_FONT_SIZE + padY * 2;
2984
- ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
2985
- ctx.beginPath();
2986
- ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
2987
- ctx.fill();
2988
- ctx.fillStyle = "#1a1a1a";
2989
- ctx.textAlign = "center";
2990
- ctx.textBaseline = "middle";
2991
- ctx.fillText(arrow.label, mid.x, mid.y);
2992
- ctx.restore();
2993
- }
2994
- renderArrowhead(ctx, arrow, tip, angle) {
2995
- ctx.beginPath();
2996
- ctx.moveTo(tip.x, tip.y);
2997
- ctx.lineTo(
2998
- tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
2999
- tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
3000
- );
3001
- ctx.lineTo(
3002
- tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
3003
- tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
3004
- );
3005
- ctx.closePath();
3006
- ctx.fillStyle = arrow.color;
3007
- ctx.fill();
3008
- }
3009
- getVisualEndpoints(arrow, geometry) {
3010
- let visualFrom = arrow.from;
3011
- let visualTo = arrow.to;
3012
- if (!this.store) return { visualFrom, visualTo };
3013
- if (arrow.fromBinding) {
3014
- const el = this.store.getById(arrow.fromBinding.elementId);
3015
- if (el) {
3016
- const bounds = getElementBounds(el);
3017
- if (bounds) {
3018
- const tangentAngle = geometry.tangentStart;
3019
- const rayTarget = {
3020
- x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
3021
- y: arrow.from.y + Math.sin(tangentAngle) * 1e3
3022
- };
3023
- visualFrom = getEdgeIntersection(bounds, rayTarget);
3024
- }
3025
- }
3026
- }
3027
- if (arrow.toBinding) {
3028
- const el = this.store.getById(arrow.toBinding.elementId);
3029
- if (el) {
3030
- const bounds = getElementBounds(el);
3031
- if (bounds) {
3032
- const tangentAngle = geometry.tangentEnd;
3033
- const rayTarget = {
3034
- x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
3035
- y: arrow.to.y - Math.sin(tangentAngle) * 1e3
3036
- };
3037
- visualTo = getEdgeIntersection(bounds, rayTarget);
3038
- }
3039
- }
3040
- }
3041
- return { visualFrom, visualTo };
3042
- }
3043
- renderShape(ctx, shape) {
3044
- ctx.save();
3045
- if (shape.fillColor !== "none" && shape.shape !== "line") {
3046
- ctx.fillStyle = shape.fillColor;
3047
- this.fillShapePath(ctx, shape);
3048
- }
3049
- if (shape.strokeWidth > 0) {
3050
- ctx.strokeStyle = shape.strokeColor;
3051
- ctx.lineWidth = shape.strokeWidth;
3052
- this.strokeShapePath(ctx, shape);
3053
- }
3054
- ctx.restore();
3055
- }
3056
- fillShapePath(ctx, shape) {
3057
- switch (shape.shape) {
3058
- case "rectangle":
3059
- ctx.fillRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
3060
- break;
3061
- case "ellipse": {
3062
- const cx = shape.position.x + shape.size.w / 2;
3063
- const cy = shape.position.y + shape.size.h / 2;
3064
- ctx.beginPath();
3065
- ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
3066
- ctx.fill();
3067
- break;
3068
- }
3069
- }
3070
- }
3071
- strokeShapePath(ctx, shape) {
3072
- switch (shape.shape) {
3073
- case "rectangle":
3074
- ctx.strokeRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
3075
- break;
3076
- case "ellipse": {
3077
- const cx = shape.position.x + shape.size.w / 2;
3078
- const cy = shape.position.y + shape.size.h / 2;
3079
- ctx.beginPath();
3080
- ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
3081
- ctx.stroke();
3082
- break;
3083
- }
3084
- case "line": {
3085
- const [a, b] = lineEndpoints(shape);
3086
- ctx.lineCap = "round";
3087
- ctx.beginPath();
3088
- ctx.moveTo(a.x, a.y);
3089
- ctx.lineTo(b.x, b.y);
3090
- ctx.stroke();
3346
+ renderTemplate(ctx, element, this.store);
3091
3347
  break;
3092
- }
3093
3348
  }
3094
3349
  }
3095
3350
  renderGrid(ctx, grid) {
@@ -3142,183 +3397,6 @@ var ElementRenderer = class {
3142
3397
  );
3143
3398
  }
3144
3399
  }
3145
- renderTemplate(ctx, template) {
3146
- const grid = this.store?.getElementsByType("grid")[0];
3147
- if (grid && grid.gridType === "hex") {
3148
- this.renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
3149
- return;
3150
- }
3151
- this.renderGeometricTemplate(ctx, template);
3152
- }
3153
- renderGeometricTemplate(ctx, template) {
3154
- const { x: cx, y: cy } = template.position;
3155
- const r = template.radius;
3156
- ctx.save();
3157
- ctx.globalAlpha = template.opacity;
3158
- ctx.fillStyle = template.fillColor;
3159
- ctx.strokeStyle = template.strokeColor;
3160
- ctx.lineWidth = template.strokeWidth;
3161
- switch (template.templateShape) {
3162
- case "circle":
3163
- ctx.beginPath();
3164
- ctx.arc(cx, cy, r, 0, Math.PI * 2);
3165
- ctx.fill();
3166
- ctx.stroke();
3167
- if (template.radiusFeet != null && template.radiusFeet > 0) {
3168
- this.renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
3169
- }
3170
- break;
3171
- case "square":
3172
- ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
3173
- ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
3174
- break;
3175
- case "cone": {
3176
- const halfAngle = Math.atan(0.5);
3177
- ctx.beginPath();
3178
- ctx.moveTo(cx, cy);
3179
- ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
3180
- ctx.closePath();
3181
- ctx.fill();
3182
- ctx.stroke();
3183
- break;
3184
- }
3185
- case "line": {
3186
- const halfW = r / 12;
3187
- const cos = Math.cos(template.angle);
3188
- const sin = Math.sin(template.angle);
3189
- const perpX = -sin * halfW;
3190
- const perpY = cos * halfW;
3191
- ctx.beginPath();
3192
- ctx.moveTo(cx + perpX, cy + perpY);
3193
- ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
3194
- ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
3195
- ctx.lineTo(cx - perpX, cy - perpY);
3196
- ctx.closePath();
3197
- ctx.fill();
3198
- ctx.stroke();
3199
- break;
3200
- }
3201
- }
3202
- ctx.restore();
3203
- }
3204
- renderHexTemplate(ctx, template, cellSize, orientation) {
3205
- const snapUnit = Math.sqrt(3) * cellSize;
3206
- const radiusCells = template.radius / snapUnit;
3207
- const center2 = template.position;
3208
- let cells;
3209
- switch (template.templateShape) {
3210
- case "circle":
3211
- cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
3212
- break;
3213
- case "cone":
3214
- cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
3215
- break;
3216
- case "line":
3217
- cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
3218
- break;
3219
- case "square":
3220
- cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
3221
- break;
3222
- }
3223
- ctx.save();
3224
- ctx.globalAlpha = template.opacity;
3225
- ctx.beginPath();
3226
- for (const cell of cells) {
3227
- drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3228
- }
3229
- ctx.fillStyle = template.fillColor;
3230
- ctx.fill();
3231
- ctx.beginPath();
3232
- for (const cell of cells) {
3233
- drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
3234
- }
3235
- ctx.strokeStyle = template.strokeColor;
3236
- ctx.lineWidth = template.strokeWidth;
3237
- ctx.stroke();
3238
- {
3239
- ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
3240
- ctx.beginPath();
3241
- drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
3242
- ctx.fillStyle = template.strokeColor;
3243
- ctx.fill();
3244
- ctx.strokeStyle = template.strokeColor;
3245
- ctx.lineWidth = template.strokeWidth;
3246
- ctx.stroke();
3247
- }
3248
- if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
3249
- const r = template.radius;
3250
- this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
3251
- }
3252
- ctx.restore();
3253
- }
3254
- renderRadiusMarker(ctx, cx, cy, r, feet) {
3255
- const markerColor = ctx.strokeStyle;
3256
- ctx.save();
3257
- ctx.globalAlpha = 1;
3258
- ctx.beginPath();
3259
- ctx.setLineDash([4, 4]);
3260
- ctx.strokeStyle = markerColor;
3261
- ctx.lineWidth = 1.5;
3262
- ctx.moveTo(cx, cy);
3263
- ctx.lineTo(cx + r, cy);
3264
- ctx.stroke();
3265
- ctx.setLineDash([]);
3266
- const label = `${Math.round(feet)} ft`;
3267
- const fontSize = Math.max(10, Math.min(14, r * 0.15));
3268
- ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
3269
- ctx.textAlign = "center";
3270
- ctx.textBaseline = "bottom";
3271
- const textX = cx + r / 2;
3272
- const textY = cy - 4;
3273
- const metrics = ctx.measureText(label);
3274
- const padX = 4;
3275
- const padY = 2;
3276
- const textW = metrics.width + padX * 2;
3277
- const textH = fontSize + padY * 2;
3278
- ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
3279
- ctx.beginPath();
3280
- ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
3281
- ctx.fill();
3282
- ctx.fillStyle = markerColor;
3283
- ctx.fillText(label, textX, textY - padY);
3284
- ctx.restore();
3285
- }
3286
- renderImage(ctx, image) {
3287
- if (this.imageCache.get(image.src) === "failed") {
3288
- this.renderImagePlaceholder(ctx, image);
3289
- return;
3290
- }
3291
- const img = this.getImage(image.src);
3292
- if (!img) return;
3293
- ctx.drawImage(
3294
- img,
3295
- image.position.x,
3296
- image.position.y,
3297
- image.size.w,
3298
- image.size.h
3299
- );
3300
- }
3301
- renderImagePlaceholder(ctx, image) {
3302
- const { x, y } = image.position;
3303
- const { w, h } = image.size;
3304
- ctx.save();
3305
- ctx.fillStyle = "#eeeeee";
3306
- ctx.fillRect(x, y, w, h);
3307
- ctx.strokeStyle = "#bdbdbd";
3308
- ctx.lineWidth = 1;
3309
- ctx.strokeRect(x, y, w, h);
3310
- const glyph = Math.min(24, w / 2, h / 2);
3311
- const cx = x + w / 2;
3312
- const cy = y + h / 2;
3313
- ctx.strokeStyle = "#9e9e9e";
3314
- ctx.lineWidth = 2;
3315
- ctx.beginPath();
3316
- ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
3317
- ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
3318
- ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
3319
- ctx.stroke();
3320
- ctx.restore();
3321
- }
3322
3400
  getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
3323
3401
  const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
3324
3402
  if (this.hexTileCacheKey === key && this.hexTileCache) {
@@ -3331,33 +3409,6 @@ var ElementRenderer = class {
3331
3409
  }
3332
3410
  return tile;
3333
3411
  }
3334
- getImage(src) {
3335
- const cached = this.imageCache.get(src);
3336
- if (cached) {
3337
- if (cached === "failed") return null;
3338
- if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
3339
- return cached;
3340
- }
3341
- const img = new Image();
3342
- img.src = src;
3343
- this.imageCache.set(src, img);
3344
- img.onload = () => {
3345
- this.onImageLoad?.();
3346
- if (typeof createImageBitmap !== "undefined") {
3347
- createImageBitmap(img).then((bitmap) => {
3348
- this.imageCache.set(src, bitmap);
3349
- this.onImageLoad?.();
3350
- }).catch(() => {
3351
- });
3352
- }
3353
- };
3354
- img.onerror = (event) => {
3355
- this.imageCache.set(src, "failed");
3356
- this.onImageError?.(src, event);
3357
- this.onImageLoad?.();
3358
- };
3359
- return null;
3360
- }
3361
3412
  };
3362
3413
 
3363
3414
  // src/elements/element-factory.ts
@@ -8992,7 +9043,7 @@ var TemplateTool = class {
8992
9043
  };
8993
9044
 
8994
9045
  // src/index.ts
8995
- var VERSION = "0.38.4";
9046
+ var VERSION = "0.38.5";
8996
9047
  export {
8997
9048
  ArrowTool,
8998
9049
  AutoSave,