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