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