@fieldnotes/core 0.38.4 → 0.38.6
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 +852 -801
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +852 -801
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1308,17 +1308,174 @@ var ShortcutMap = class {
|
|
|
1308
1308
|
}
|
|
1309
1309
|
};
|
|
1310
1310
|
|
|
1311
|
-
// src/canvas/
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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/
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
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
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
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
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
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.
|
|
2539
|
-
ctx.
|
|
2540
|
-
ctx.
|
|
2541
|
-
|
|
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
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
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
|
-
|
|
3342
|
+
tc.stroke();
|
|
3343
|
+
return { canvas, tileW, tileH };
|
|
2918
3344
|
}
|
|
2919
|
-
function
|
|
2920
|
-
const
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
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, () =>
|
|
3400
|
+
withRotation(ctx, element, c, () => renderStroke(ctx, element));
|
|
2975
3401
|
break;
|
|
2976
3402
|
}
|
|
2977
3403
|
case "arrow":
|
|
2978
|
-
|
|
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, () =>
|
|
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(
|
|
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
|
-
|
|
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.
|
|
9127
|
+
var VERSION = "0.38.6";
|
|
9077
9128
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9078
9129
|
0 && (module.exports = {
|
|
9079
9130
|
ArrowTool,
|