@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.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();
|
|
@@ -2176,7 +2212,12 @@ var ElementStore = class {
|
|
|
2176
2212
|
if (!existing) return;
|
|
2177
2213
|
this.sortedCache = null;
|
|
2178
2214
|
this._versions.set(id, (this._versions.get(id) ?? 0) + 1);
|
|
2179
|
-
const updated = {
|
|
2215
|
+
const updated = {
|
|
2216
|
+
...existing,
|
|
2217
|
+
...partial,
|
|
2218
|
+
id: existing.id,
|
|
2219
|
+
type: existing.type
|
|
2220
|
+
};
|
|
2180
2221
|
if (updated.type === "stroke" && existing.type === "stroke") {
|
|
2181
2222
|
transferStrokeRenderData(existing, updated);
|
|
2182
2223
|
transferStrokeBounds(existing, updated);
|
|
@@ -2315,20 +2356,52 @@ var ElementStore = class {
|
|
|
2315
2356
|
}
|
|
2316
2357
|
};
|
|
2317
2358
|
|
|
2318
|
-
// src/elements/
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
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
|
+
|
|
2332
2405
|
// src/elements/shape-geometry.ts
|
|
2333
2406
|
function lineFromEndpoints(a, b) {
|
|
2334
2407
|
return {
|
|
@@ -2349,6 +2422,74 @@ function lineEndpoints(shape) {
|
|
|
2349
2422
|
];
|
|
2350
2423
|
}
|
|
2351
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
|
+
|
|
2352
2493
|
// src/elements/arrow-binding.ts
|
|
2353
2494
|
var BINDABLE_TYPES = /* @__PURE__ */ new Set(["note", "text", "image", "html", "shape"]);
|
|
2354
2495
|
function isBindable(element) {
|
|
@@ -2421,38 +2562,515 @@ function updateArrowsBoundToElements(movedIds, store) {
|
|
|
2421
2562
|
}
|
|
2422
2563
|
}
|
|
2423
2564
|
}
|
|
2424
|
-
function updateBoundArrow(arrow, store) {
|
|
2425
|
-
if (!arrow.fromBinding && !arrow.toBinding) return null;
|
|
2426
|
-
const updates = {};
|
|
2427
|
-
if (arrow.fromBinding) {
|
|
2428
|
-
const el = store.getById(arrow.fromBinding.elementId);
|
|
2429
|
-
if (el) {
|
|
2430
|
-
const center2 = getElementCenter(el);
|
|
2431
|
-
updates.from = center2;
|
|
2432
|
-
updates.position = center2;
|
|
2433
|
-
}
|
|
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;
|
|
2434
3012
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
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);
|
|
2440
3018
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
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);
|
|
2450
3041
|
}
|
|
3042
|
+
ctx.restore();
|
|
3043
|
+
}
|
|
3044
|
+
function renderRadiusMarker(ctx, cx, cy, r, feet) {
|
|
3045
|
+
const markerColor = ctx.strokeStyle;
|
|
2451
3046
|
ctx.save();
|
|
2452
|
-
ctx.
|
|
2453
|
-
ctx.
|
|
2454
|
-
ctx.
|
|
2455
|
-
|
|
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);
|
|
2456
3074
|
ctx.restore();
|
|
2457
3075
|
}
|
|
2458
3076
|
|
|
@@ -2606,245 +3224,58 @@ function createHexGridTile(cellSize, orientation, strokeColor, strokeWidth, opac
|
|
|
2606
3224
|
tc.globalAlpha = opacity;
|
|
2607
3225
|
tc.beginPath();
|
|
2608
3226
|
if (orientation === "pointy") {
|
|
2609
|
-
const hexW = tileW;
|
|
2610
|
-
const rowH = 1.5 * cellSize;
|
|
2611
|
-
for (let row = -1; row <= 3; row++) {
|
|
2612
|
-
const offX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
2613
|
-
for (let col = -1; col <= 1; col++) {
|
|
2614
|
-
const cx = col * hexW + offX;
|
|
2615
|
-
const cy = row * rowH;
|
|
2616
|
-
tc.moveTo(cx + ox0, cy + oy0);
|
|
2617
|
-
tc.lineTo(cx + ox1, cy + oy1);
|
|
2618
|
-
tc.lineTo(cx + ox2, cy + oy2);
|
|
2619
|
-
tc.lineTo(cx + ox3, cy + oy3);
|
|
2620
|
-
tc.lineTo(cx + ox4, cy + oy4);
|
|
2621
|
-
tc.lineTo(cx + ox5, cy + oy5);
|
|
2622
|
-
tc.closePath();
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
} else {
|
|
2626
|
-
const hexH = tileH;
|
|
2627
|
-
const colW = 1.5 * cellSize;
|
|
2628
|
-
for (let col = -1; col <= 3; col++) {
|
|
2629
|
-
const offY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
2630
|
-
for (let row = -1; row <= 1; row++) {
|
|
2631
|
-
const cx = col * colW;
|
|
2632
|
-
const cy = row * hexH + offY;
|
|
2633
|
-
tc.moveTo(cx + ox0, cy + oy0);
|
|
2634
|
-
tc.lineTo(cx + ox1, cy + oy1);
|
|
2635
|
-
tc.lineTo(cx + ox2, cy + oy2);
|
|
2636
|
-
tc.lineTo(cx + ox3, cy + oy3);
|
|
2637
|
-
tc.lineTo(cx + ox4, cy + oy4);
|
|
2638
|
-
tc.lineTo(cx + ox5, cy + oy5);
|
|
2639
|
-
tc.closePath();
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
}
|
|
2643
|
-
tc.stroke();
|
|
2644
|
-
return { canvas, tileW, tileH };
|
|
2645
|
-
}
|
|
2646
|
-
function renderHexGridTiled(ctx, bounds, cellSize, tile) {
|
|
2647
|
-
const { tileW, tileH } = tile;
|
|
2648
|
-
const startCol = Math.floor(bounds.minX / tileW) - 1;
|
|
2649
|
-
const endCol = Math.ceil(bounds.maxX / tileW) + 1;
|
|
2650
|
-
const startRow = Math.floor(bounds.minY / tileH) - 1;
|
|
2651
|
-
const endRow = Math.ceil(bounds.maxY / tileH) + 1;
|
|
2652
|
-
for (let row = startRow; row <= endRow; row++) {
|
|
2653
|
-
for (let col = startCol; col <= endCol; col++) {
|
|
2654
|
-
ctx.drawImage(tile.canvas, col * tileW, row * tileH, tileW, tileH);
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
// src/elements/hex-fill.ts
|
|
2660
|
-
function offsetToCube(col, row, orientation) {
|
|
2661
|
-
if (orientation === "pointy") {
|
|
2662
|
-
return { q: col - (row - (row & 1)) / 2, r: row };
|
|
2663
|
-
}
|
|
2664
|
-
return { q: col, r: row - (col - (col & 1)) / 2 };
|
|
2665
|
-
}
|
|
2666
|
-
function cubeToOffset(q, r, orientation) {
|
|
2667
|
-
if (orientation === "pointy") {
|
|
2668
|
-
return { col: q + (r - (r & 1)) / 2, row: r };
|
|
2669
|
-
}
|
|
2670
|
-
return { col: q, row: r + (q - (q & 1)) / 2 };
|
|
2671
|
-
}
|
|
2672
|
-
function offsetToPixel(col, row, cellSize, orientation) {
|
|
2673
|
-
if (orientation === "pointy") {
|
|
2674
|
-
const hexW = Math.sqrt(3) * cellSize;
|
|
2675
|
-
const rowH = 1.5 * cellSize;
|
|
2676
|
-
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
2677
|
-
return { x: col * hexW + offsetX, y: row * rowH };
|
|
2678
|
-
}
|
|
2679
|
-
const hexH = Math.sqrt(3) * cellSize;
|
|
2680
|
-
const colW = 1.5 * cellSize;
|
|
2681
|
-
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
2682
|
-
return { x: col * colW, y: row * hexH + offsetY };
|
|
2683
|
-
}
|
|
2684
|
-
function pixelToOffset(x, y, cellSize, orientation) {
|
|
2685
|
-
if (orientation === "pointy") {
|
|
2686
|
-
const hexW = Math.sqrt(3) * cellSize;
|
|
2687
|
-
const rowH = 1.5 * cellSize;
|
|
2688
|
-
const row = Math.round(y / rowH);
|
|
2689
|
-
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
2690
|
-
return { col: Math.round((x - offsetX) / hexW), row };
|
|
2691
|
-
}
|
|
2692
|
-
const hexH = Math.sqrt(3) * cellSize;
|
|
2693
|
-
const colW = 1.5 * cellSize;
|
|
2694
|
-
const col = Math.round(x / colW);
|
|
2695
|
-
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
2696
|
-
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
2697
|
-
}
|
|
2698
|
-
function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
|
|
2699
|
-
const cells = [];
|
|
2700
|
-
for (let dq = -n; dq <= n; dq++) {
|
|
2701
|
-
const rMin = Math.max(-n, -dq - n);
|
|
2702
|
-
const rMax = Math.min(n, -dq + n);
|
|
2703
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2704
|
-
const absQ = centerQ + dq;
|
|
2705
|
-
const absR = centerR + dr;
|
|
2706
|
-
const off = cubeToOffset(absQ, absR, orientation);
|
|
2707
|
-
cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
return cells;
|
|
2711
|
-
}
|
|
2712
|
-
function getHexDistance(a, b, cellSize, orientation) {
|
|
2713
|
-
const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
|
|
2714
|
-
const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
|
|
2715
|
-
const cubeA = offsetToCube(offA.col, offA.row, orientation);
|
|
2716
|
-
const cubeB = offsetToCube(offB.col, offB.row, orientation);
|
|
2717
|
-
const dq = cubeA.q - cubeB.q;
|
|
2718
|
-
const dr = cubeA.r - cubeB.r;
|
|
2719
|
-
const ds = -dq - dr;
|
|
2720
|
-
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
2721
|
-
}
|
|
2722
|
-
function getHexCellsInRadius(center2, radiusCells, cellSize, orientation) {
|
|
2723
|
-
const n = Math.round(radiusCells);
|
|
2724
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2725
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2726
|
-
if (n <= 0) {
|
|
2727
|
-
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
2728
|
-
}
|
|
2729
|
-
return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
|
|
2730
|
-
}
|
|
2731
|
-
function getHexCellsInCone(center2, angle, radiusCells, cellSize, orientation) {
|
|
2732
|
-
const n = Math.round(radiusCells);
|
|
2733
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2734
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2735
|
-
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2736
|
-
if (n <= 0) return [centerPixel];
|
|
2737
|
-
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
2738
|
-
const step = Math.PI / 3;
|
|
2739
|
-
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
2740
|
-
const halfAngle = Math.PI / 6 + 1e-6;
|
|
2741
|
-
const cells = [centerPixel];
|
|
2742
|
-
for (let dq = -n; dq <= n; dq++) {
|
|
2743
|
-
const rMin = Math.max(-n, -dq - n);
|
|
2744
|
-
const rMax = Math.min(n, -dq + n);
|
|
2745
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2746
|
-
if (dq === 0 && dr === 0) continue;
|
|
2747
|
-
const absQ = cube.q + dq;
|
|
2748
|
-
const absR = cube.r + dr;
|
|
2749
|
-
const pixel = offsetToPixel(
|
|
2750
|
-
cubeToOffset(absQ, absR, orientation).col,
|
|
2751
|
-
cubeToOffset(absQ, absR, orientation).row,
|
|
2752
|
-
cellSize,
|
|
2753
|
-
orientation
|
|
2754
|
-
);
|
|
2755
|
-
const dx = pixel.x - centerPixel.x;
|
|
2756
|
-
const dy = pixel.y - centerPixel.y;
|
|
2757
|
-
let diff = Math.atan2(dy, dx) - snappedAngle;
|
|
2758
|
-
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
2759
|
-
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
2760
|
-
if (Math.abs(diff) <= halfAngle) {
|
|
2761
|
-
cells.push(pixel);
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
return cells;
|
|
2766
|
-
}
|
|
2767
|
-
function getHexCellsInLine(center2, angle, radiusCells, cellSize, orientation) {
|
|
2768
|
-
const n = Math.round(radiusCells);
|
|
2769
|
-
const off = pixelToOffset(center2.x, center2.y, cellSize, orientation);
|
|
2770
|
-
const cube = offsetToCube(off.col, off.row, orientation);
|
|
2771
|
-
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
2772
|
-
if (n <= 0) return [centerPixel];
|
|
2773
|
-
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
2774
|
-
const step = Math.PI / 3;
|
|
2775
|
-
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
2776
|
-
const cos = Math.cos(snappedAngle);
|
|
2777
|
-
const sin = Math.sin(snappedAngle);
|
|
2778
|
-
const snapUnit = Math.sqrt(3) * cellSize;
|
|
2779
|
-
const lineLength = n * snapUnit;
|
|
2780
|
-
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
2781
|
-
const cells = [];
|
|
2782
|
-
for (let dq = -n; dq <= n; dq++) {
|
|
2783
|
-
const rMin = Math.max(-n, -dq - n);
|
|
2784
|
-
const rMax = Math.min(n, -dq + n);
|
|
2785
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2786
|
-
const absQ = cube.q + dq;
|
|
2787
|
-
const absR = cube.r + dr;
|
|
2788
|
-
const pixel = offsetToPixel(
|
|
2789
|
-
cubeToOffset(absQ, absR, orientation).col,
|
|
2790
|
-
cubeToOffset(absQ, absR, orientation).row,
|
|
2791
|
-
cellSize,
|
|
2792
|
-
orientation
|
|
2793
|
-
);
|
|
2794
|
-
const dx = pixel.x - centerPixel.x;
|
|
2795
|
-
const dy = pixel.y - centerPixel.y;
|
|
2796
|
-
const along = dx * cos + dy * sin;
|
|
2797
|
-
const perp = Math.abs(-dx * sin + dy * cos);
|
|
2798
|
-
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
2799
|
-
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();
|
|
2800
3241
|
}
|
|
2801
3242
|
}
|
|
2802
|
-
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
for (let dr = rMin; dr <= rMax; dr++) {
|
|
2818
|
-
const absQ = cube.q + dq;
|
|
2819
|
-
const absR = cube.r + dr;
|
|
2820
|
-
const pixel = offsetToPixel(
|
|
2821
|
-
cubeToOffset(absQ, absR, orientation).col,
|
|
2822
|
-
cubeToOffset(absQ, absR, orientation).row,
|
|
2823
|
-
cellSize,
|
|
2824
|
-
orientation
|
|
2825
|
-
);
|
|
2826
|
-
if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
|
|
2827
|
-
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();
|
|
2828
3258
|
}
|
|
2829
3259
|
}
|
|
2830
3260
|
}
|
|
2831
|
-
|
|
3261
|
+
tc.stroke();
|
|
3262
|
+
return { canvas, tileW, tileH };
|
|
2832
3263
|
}
|
|
2833
|
-
function
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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
|
+
}
|
|
2839
3274
|
}
|
|
2840
|
-
ctx.closePath();
|
|
2841
3275
|
}
|
|
2842
3276
|
|
|
2843
3277
|
// src/elements/element-renderer.ts
|
|
2844
3278
|
var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
|
|
2845
|
-
var ARROWHEAD_LENGTH = 12;
|
|
2846
|
-
var ARROWHEAD_ANGLE = Math.PI / 6;
|
|
2847
|
-
var ARROW_LABEL_FONT_SIZE = 14;
|
|
2848
3279
|
var ElementRenderer = class {
|
|
2849
3280
|
store = null;
|
|
2850
3281
|
imageCache = /* @__PURE__ */ new Map();
|
|
@@ -2885,206 +3316,35 @@ var ElementRenderer = class {
|
|
|
2885
3316
|
case "stroke": {
|
|
2886
3317
|
const b = getElementBounds(element);
|
|
2887
3318
|
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2888
|
-
withRotation(ctx, element, c, () =>
|
|
3319
|
+
withRotation(ctx, element, c, () => renderStroke(ctx, element));
|
|
2889
3320
|
break;
|
|
2890
3321
|
}
|
|
2891
3322
|
case "arrow":
|
|
2892
|
-
|
|
3323
|
+
renderArrow(ctx, element, this.store, this.labelEditingId);
|
|
2893
3324
|
break;
|
|
2894
3325
|
case "shape": {
|
|
2895
3326
|
const b = getElementBounds(element);
|
|
2896
3327
|
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2897
|
-
withRotation(ctx, element, c, () =>
|
|
3328
|
+
withRotation(ctx, element, c, () => renderShape(ctx, element));
|
|
2898
3329
|
break;
|
|
2899
3330
|
}
|
|
2900
3331
|
case "image": {
|
|
2901
3332
|
const b = getElementBounds(element);
|
|
2902
3333
|
const c = b ? { x: b.x + b.w / 2, y: b.y + b.h / 2 } : element.position;
|
|
2903
|
-
withRotation(
|
|
3334
|
+
withRotation(
|
|
3335
|
+
ctx,
|
|
3336
|
+
element,
|
|
3337
|
+
c,
|
|
3338
|
+
() => renderImage(ctx, element, this.imageCache, this.onImageLoad, this.onImageError)
|
|
3339
|
+
);
|
|
2904
3340
|
break;
|
|
2905
3341
|
}
|
|
2906
3342
|
case "grid":
|
|
2907
3343
|
this.renderGrid(ctx, element);
|
|
2908
3344
|
break;
|
|
2909
3345
|
case "template":
|
|
2910
|
-
|
|
2911
|
-
break;
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
|
-
renderStroke(ctx, stroke) {
|
|
2915
|
-
if (stroke.points.length < 2) return;
|
|
2916
|
-
ctx.save();
|
|
2917
|
-
if (stroke.blendMode) ctx.globalCompositeOperation = stroke.blendMode;
|
|
2918
|
-
ctx.translate(stroke.position.x, stroke.position.y);
|
|
2919
|
-
ctx.strokeStyle = stroke.color;
|
|
2920
|
-
ctx.lineCap = "round";
|
|
2921
|
-
ctx.lineJoin = "round";
|
|
2922
|
-
ctx.globalAlpha = stroke.opacity;
|
|
2923
|
-
const data = getStrokeRenderData(stroke);
|
|
2924
|
-
if (data.buckets) {
|
|
2925
|
-
for (const bucket of data.buckets) {
|
|
2926
|
-
ctx.lineWidth = bucket.width;
|
|
2927
|
-
ctx.stroke(bucket.path);
|
|
2928
|
-
}
|
|
2929
|
-
} else {
|
|
2930
|
-
for (let i = 0; i < data.segments.length; i++) {
|
|
2931
|
-
const seg = data.segments[i];
|
|
2932
|
-
const w = data.widths[i];
|
|
2933
|
-
if (!seg || w === void 0) continue;
|
|
2934
|
-
ctx.lineWidth = w;
|
|
2935
|
-
ctx.beginPath();
|
|
2936
|
-
ctx.moveTo(seg.start.x, seg.start.y);
|
|
2937
|
-
ctx.bezierCurveTo(seg.cp1.x, seg.cp1.y, seg.cp2.x, seg.cp2.y, seg.end.x, seg.end.y);
|
|
2938
|
-
ctx.stroke();
|
|
2939
|
-
}
|
|
2940
|
-
}
|
|
2941
|
-
ctx.restore();
|
|
2942
|
-
}
|
|
2943
|
-
renderArrow(ctx, arrow) {
|
|
2944
|
-
const geometry = getArrowRenderGeometry(arrow);
|
|
2945
|
-
const { visualFrom, visualTo } = this.getVisualEndpoints(arrow, geometry);
|
|
2946
|
-
ctx.save();
|
|
2947
|
-
ctx.strokeStyle = arrow.color;
|
|
2948
|
-
ctx.lineWidth = arrow.width;
|
|
2949
|
-
ctx.lineCap = "round";
|
|
2950
|
-
if (arrow.fromBinding || arrow.toBinding) {
|
|
2951
|
-
ctx.setLineDash([8, 4]);
|
|
2952
|
-
}
|
|
2953
|
-
ctx.beginPath();
|
|
2954
|
-
ctx.moveTo(visualFrom.x, visualFrom.y);
|
|
2955
|
-
if (arrow.bend !== 0) {
|
|
2956
|
-
const cp = geometry.controlPoint;
|
|
2957
|
-
if (cp) {
|
|
2958
|
-
ctx.quadraticCurveTo(cp.x, cp.y, visualTo.x, visualTo.y);
|
|
2959
|
-
}
|
|
2960
|
-
} else {
|
|
2961
|
-
ctx.lineTo(visualTo.x, visualTo.y);
|
|
2962
|
-
}
|
|
2963
|
-
ctx.stroke();
|
|
2964
|
-
this.renderArrowhead(ctx, arrow, visualTo, geometry.tangentEnd);
|
|
2965
|
-
ctx.restore();
|
|
2966
|
-
this.renderArrowLabel(ctx, arrow);
|
|
2967
|
-
}
|
|
2968
|
-
renderArrowLabel(ctx, arrow) {
|
|
2969
|
-
if (!arrow.label || arrow.label.length === 0) return;
|
|
2970
|
-
if (arrow.id === this.labelEditingId) return;
|
|
2971
|
-
const mid = getArrowMidpoint(arrow.from, arrow.to, arrow.bend);
|
|
2972
|
-
ctx.save();
|
|
2973
|
-
ctx.font = `${ARROW_LABEL_FONT_SIZE}px system-ui, sans-serif`;
|
|
2974
|
-
const metrics = ctx.measureText(arrow.label);
|
|
2975
|
-
const padX = 6;
|
|
2976
|
-
const padY = 4;
|
|
2977
|
-
const w = metrics.width + padX * 2;
|
|
2978
|
-
const h = ARROW_LABEL_FONT_SIZE + padY * 2;
|
|
2979
|
-
ctx.fillStyle = "rgba(255, 255, 255, 0.9)";
|
|
2980
|
-
ctx.beginPath();
|
|
2981
|
-
ctx.roundRect(mid.x - w / 2, mid.y - h / 2, w, h, 4);
|
|
2982
|
-
ctx.fill();
|
|
2983
|
-
ctx.fillStyle = "#1a1a1a";
|
|
2984
|
-
ctx.textAlign = "center";
|
|
2985
|
-
ctx.textBaseline = "middle";
|
|
2986
|
-
ctx.fillText(arrow.label, mid.x, mid.y);
|
|
2987
|
-
ctx.restore();
|
|
2988
|
-
}
|
|
2989
|
-
renderArrowhead(ctx, arrow, tip, angle) {
|
|
2990
|
-
ctx.beginPath();
|
|
2991
|
-
ctx.moveTo(tip.x, tip.y);
|
|
2992
|
-
ctx.lineTo(
|
|
2993
|
-
tip.x - ARROWHEAD_LENGTH * Math.cos(angle - ARROWHEAD_ANGLE),
|
|
2994
|
-
tip.y - ARROWHEAD_LENGTH * Math.sin(angle - ARROWHEAD_ANGLE)
|
|
2995
|
-
);
|
|
2996
|
-
ctx.lineTo(
|
|
2997
|
-
tip.x - ARROWHEAD_LENGTH * Math.cos(angle + ARROWHEAD_ANGLE),
|
|
2998
|
-
tip.y - ARROWHEAD_LENGTH * Math.sin(angle + ARROWHEAD_ANGLE)
|
|
2999
|
-
);
|
|
3000
|
-
ctx.closePath();
|
|
3001
|
-
ctx.fillStyle = arrow.color;
|
|
3002
|
-
ctx.fill();
|
|
3003
|
-
}
|
|
3004
|
-
getVisualEndpoints(arrow, geometry) {
|
|
3005
|
-
let visualFrom = arrow.from;
|
|
3006
|
-
let visualTo = arrow.to;
|
|
3007
|
-
if (!this.store) return { visualFrom, visualTo };
|
|
3008
|
-
if (arrow.fromBinding) {
|
|
3009
|
-
const el = this.store.getById(arrow.fromBinding.elementId);
|
|
3010
|
-
if (el) {
|
|
3011
|
-
const bounds = getElementBounds(el);
|
|
3012
|
-
if (bounds) {
|
|
3013
|
-
const tangentAngle = geometry.tangentStart;
|
|
3014
|
-
const rayTarget = {
|
|
3015
|
-
x: arrow.from.x + Math.cos(tangentAngle) * 1e3,
|
|
3016
|
-
y: arrow.from.y + Math.sin(tangentAngle) * 1e3
|
|
3017
|
-
};
|
|
3018
|
-
visualFrom = getEdgeIntersection(bounds, rayTarget);
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
}
|
|
3022
|
-
if (arrow.toBinding) {
|
|
3023
|
-
const el = this.store.getById(arrow.toBinding.elementId);
|
|
3024
|
-
if (el) {
|
|
3025
|
-
const bounds = getElementBounds(el);
|
|
3026
|
-
if (bounds) {
|
|
3027
|
-
const tangentAngle = geometry.tangentEnd;
|
|
3028
|
-
const rayTarget = {
|
|
3029
|
-
x: arrow.to.x - Math.cos(tangentAngle) * 1e3,
|
|
3030
|
-
y: arrow.to.y - Math.sin(tangentAngle) * 1e3
|
|
3031
|
-
};
|
|
3032
|
-
visualTo = getEdgeIntersection(bounds, rayTarget);
|
|
3033
|
-
}
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
return { visualFrom, visualTo };
|
|
3037
|
-
}
|
|
3038
|
-
renderShape(ctx, shape) {
|
|
3039
|
-
ctx.save();
|
|
3040
|
-
if (shape.fillColor !== "none" && shape.shape !== "line") {
|
|
3041
|
-
ctx.fillStyle = shape.fillColor;
|
|
3042
|
-
this.fillShapePath(ctx, shape);
|
|
3043
|
-
}
|
|
3044
|
-
if (shape.strokeWidth > 0) {
|
|
3045
|
-
ctx.strokeStyle = shape.strokeColor;
|
|
3046
|
-
ctx.lineWidth = shape.strokeWidth;
|
|
3047
|
-
this.strokeShapePath(ctx, shape);
|
|
3048
|
-
}
|
|
3049
|
-
ctx.restore();
|
|
3050
|
-
}
|
|
3051
|
-
fillShapePath(ctx, shape) {
|
|
3052
|
-
switch (shape.shape) {
|
|
3053
|
-
case "rectangle":
|
|
3054
|
-
ctx.fillRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
|
|
3055
|
-
break;
|
|
3056
|
-
case "ellipse": {
|
|
3057
|
-
const cx = shape.position.x + shape.size.w / 2;
|
|
3058
|
-
const cy = shape.position.y + shape.size.h / 2;
|
|
3059
|
-
ctx.beginPath();
|
|
3060
|
-
ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
|
|
3061
|
-
ctx.fill();
|
|
3062
|
-
break;
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
}
|
|
3066
|
-
strokeShapePath(ctx, shape) {
|
|
3067
|
-
switch (shape.shape) {
|
|
3068
|
-
case "rectangle":
|
|
3069
|
-
ctx.strokeRect(shape.position.x, shape.position.y, shape.size.w, shape.size.h);
|
|
3070
|
-
break;
|
|
3071
|
-
case "ellipse": {
|
|
3072
|
-
const cx = shape.position.x + shape.size.w / 2;
|
|
3073
|
-
const cy = shape.position.y + shape.size.h / 2;
|
|
3074
|
-
ctx.beginPath();
|
|
3075
|
-
ctx.ellipse(cx, cy, shape.size.w / 2, shape.size.h / 2, 0, 0, Math.PI * 2);
|
|
3076
|
-
ctx.stroke();
|
|
3077
|
-
break;
|
|
3078
|
-
}
|
|
3079
|
-
case "line": {
|
|
3080
|
-
const [a, b] = lineEndpoints(shape);
|
|
3081
|
-
ctx.lineCap = "round";
|
|
3082
|
-
ctx.beginPath();
|
|
3083
|
-
ctx.moveTo(a.x, a.y);
|
|
3084
|
-
ctx.lineTo(b.x, b.y);
|
|
3085
|
-
ctx.stroke();
|
|
3346
|
+
renderTemplate(ctx, element, this.store);
|
|
3086
3347
|
break;
|
|
3087
|
-
}
|
|
3088
3348
|
}
|
|
3089
3349
|
}
|
|
3090
3350
|
renderGrid(ctx, grid) {
|
|
@@ -3137,183 +3397,6 @@ var ElementRenderer = class {
|
|
|
3137
3397
|
);
|
|
3138
3398
|
}
|
|
3139
3399
|
}
|
|
3140
|
-
renderTemplate(ctx, template) {
|
|
3141
|
-
const grid = this.store?.getElementsByType("grid")[0];
|
|
3142
|
-
if (grid && grid.gridType === "hex") {
|
|
3143
|
-
this.renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
|
|
3144
|
-
return;
|
|
3145
|
-
}
|
|
3146
|
-
this.renderGeometricTemplate(ctx, template);
|
|
3147
|
-
}
|
|
3148
|
-
renderGeometricTemplate(ctx, template) {
|
|
3149
|
-
const { x: cx, y: cy } = template.position;
|
|
3150
|
-
const r = template.radius;
|
|
3151
|
-
ctx.save();
|
|
3152
|
-
ctx.globalAlpha = template.opacity;
|
|
3153
|
-
ctx.fillStyle = template.fillColor;
|
|
3154
|
-
ctx.strokeStyle = template.strokeColor;
|
|
3155
|
-
ctx.lineWidth = template.strokeWidth;
|
|
3156
|
-
switch (template.templateShape) {
|
|
3157
|
-
case "circle":
|
|
3158
|
-
ctx.beginPath();
|
|
3159
|
-
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
3160
|
-
ctx.fill();
|
|
3161
|
-
ctx.stroke();
|
|
3162
|
-
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3163
|
-
this.renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
|
|
3164
|
-
}
|
|
3165
|
-
break;
|
|
3166
|
-
case "square":
|
|
3167
|
-
ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
|
|
3168
|
-
ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
|
|
3169
|
-
break;
|
|
3170
|
-
case "cone": {
|
|
3171
|
-
const halfAngle = Math.atan(0.5);
|
|
3172
|
-
ctx.beginPath();
|
|
3173
|
-
ctx.moveTo(cx, cy);
|
|
3174
|
-
ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
|
|
3175
|
-
ctx.closePath();
|
|
3176
|
-
ctx.fill();
|
|
3177
|
-
ctx.stroke();
|
|
3178
|
-
break;
|
|
3179
|
-
}
|
|
3180
|
-
case "line": {
|
|
3181
|
-
const halfW = r / 12;
|
|
3182
|
-
const cos = Math.cos(template.angle);
|
|
3183
|
-
const sin = Math.sin(template.angle);
|
|
3184
|
-
const perpX = -sin * halfW;
|
|
3185
|
-
const perpY = cos * halfW;
|
|
3186
|
-
ctx.beginPath();
|
|
3187
|
-
ctx.moveTo(cx + perpX, cy + perpY);
|
|
3188
|
-
ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
|
|
3189
|
-
ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
|
|
3190
|
-
ctx.lineTo(cx - perpX, cy - perpY);
|
|
3191
|
-
ctx.closePath();
|
|
3192
|
-
ctx.fill();
|
|
3193
|
-
ctx.stroke();
|
|
3194
|
-
break;
|
|
3195
|
-
}
|
|
3196
|
-
}
|
|
3197
|
-
ctx.restore();
|
|
3198
|
-
}
|
|
3199
|
-
renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
3200
|
-
const snapUnit = Math.sqrt(3) * cellSize;
|
|
3201
|
-
const radiusCells = template.radius / snapUnit;
|
|
3202
|
-
const center2 = template.position;
|
|
3203
|
-
let cells;
|
|
3204
|
-
switch (template.templateShape) {
|
|
3205
|
-
case "circle":
|
|
3206
|
-
cells = getHexCellsInRadius(center2, radiusCells, cellSize, orientation);
|
|
3207
|
-
break;
|
|
3208
|
-
case "cone":
|
|
3209
|
-
cells = getHexCellsInCone(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3210
|
-
break;
|
|
3211
|
-
case "line":
|
|
3212
|
-
cells = getHexCellsInLine(center2, template.angle, radiusCells, cellSize, orientation);
|
|
3213
|
-
break;
|
|
3214
|
-
case "square":
|
|
3215
|
-
cells = getHexCellsInSquare(center2, radiusCells, cellSize, orientation);
|
|
3216
|
-
break;
|
|
3217
|
-
}
|
|
3218
|
-
ctx.save();
|
|
3219
|
-
ctx.globalAlpha = template.opacity;
|
|
3220
|
-
ctx.beginPath();
|
|
3221
|
-
for (const cell of cells) {
|
|
3222
|
-
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
3223
|
-
}
|
|
3224
|
-
ctx.fillStyle = template.fillColor;
|
|
3225
|
-
ctx.fill();
|
|
3226
|
-
ctx.beginPath();
|
|
3227
|
-
for (const cell of cells) {
|
|
3228
|
-
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
3229
|
-
}
|
|
3230
|
-
ctx.strokeStyle = template.strokeColor;
|
|
3231
|
-
ctx.lineWidth = template.strokeWidth;
|
|
3232
|
-
ctx.stroke();
|
|
3233
|
-
{
|
|
3234
|
-
ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
|
|
3235
|
-
ctx.beginPath();
|
|
3236
|
-
drawHexPath(ctx, center2.x, center2.y, cellSize, orientation);
|
|
3237
|
-
ctx.fillStyle = template.strokeColor;
|
|
3238
|
-
ctx.fill();
|
|
3239
|
-
ctx.strokeStyle = template.strokeColor;
|
|
3240
|
-
ctx.lineWidth = template.strokeWidth;
|
|
3241
|
-
ctx.stroke();
|
|
3242
|
-
}
|
|
3243
|
-
if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
|
|
3244
|
-
const r = template.radius;
|
|
3245
|
-
this.renderRadiusMarker(ctx, center2.x, center2.y, r, template.radiusFeet);
|
|
3246
|
-
}
|
|
3247
|
-
ctx.restore();
|
|
3248
|
-
}
|
|
3249
|
-
renderRadiusMarker(ctx, cx, cy, r, feet) {
|
|
3250
|
-
const markerColor = ctx.strokeStyle;
|
|
3251
|
-
ctx.save();
|
|
3252
|
-
ctx.globalAlpha = 1;
|
|
3253
|
-
ctx.beginPath();
|
|
3254
|
-
ctx.setLineDash([4, 4]);
|
|
3255
|
-
ctx.strokeStyle = markerColor;
|
|
3256
|
-
ctx.lineWidth = 1.5;
|
|
3257
|
-
ctx.moveTo(cx, cy);
|
|
3258
|
-
ctx.lineTo(cx + r, cy);
|
|
3259
|
-
ctx.stroke();
|
|
3260
|
-
ctx.setLineDash([]);
|
|
3261
|
-
const label = `${Math.round(feet)} ft`;
|
|
3262
|
-
const fontSize = Math.max(10, Math.min(14, r * 0.15));
|
|
3263
|
-
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
3264
|
-
ctx.textAlign = "center";
|
|
3265
|
-
ctx.textBaseline = "bottom";
|
|
3266
|
-
const textX = cx + r / 2;
|
|
3267
|
-
const textY = cy - 4;
|
|
3268
|
-
const metrics = ctx.measureText(label);
|
|
3269
|
-
const padX = 4;
|
|
3270
|
-
const padY = 2;
|
|
3271
|
-
const textW = metrics.width + padX * 2;
|
|
3272
|
-
const textH = fontSize + padY * 2;
|
|
3273
|
-
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
3274
|
-
ctx.beginPath();
|
|
3275
|
-
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
3276
|
-
ctx.fill();
|
|
3277
|
-
ctx.fillStyle = markerColor;
|
|
3278
|
-
ctx.fillText(label, textX, textY - padY);
|
|
3279
|
-
ctx.restore();
|
|
3280
|
-
}
|
|
3281
|
-
renderImage(ctx, image) {
|
|
3282
|
-
if (this.imageCache.get(image.src) === "failed") {
|
|
3283
|
-
this.renderImagePlaceholder(ctx, image);
|
|
3284
|
-
return;
|
|
3285
|
-
}
|
|
3286
|
-
const img = this.getImage(image.src);
|
|
3287
|
-
if (!img) return;
|
|
3288
|
-
ctx.drawImage(
|
|
3289
|
-
img,
|
|
3290
|
-
image.position.x,
|
|
3291
|
-
image.position.y,
|
|
3292
|
-
image.size.w,
|
|
3293
|
-
image.size.h
|
|
3294
|
-
);
|
|
3295
|
-
}
|
|
3296
|
-
renderImagePlaceholder(ctx, image) {
|
|
3297
|
-
const { x, y } = image.position;
|
|
3298
|
-
const { w, h } = image.size;
|
|
3299
|
-
ctx.save();
|
|
3300
|
-
ctx.fillStyle = "#eeeeee";
|
|
3301
|
-
ctx.fillRect(x, y, w, h);
|
|
3302
|
-
ctx.strokeStyle = "#bdbdbd";
|
|
3303
|
-
ctx.lineWidth = 1;
|
|
3304
|
-
ctx.strokeRect(x, y, w, h);
|
|
3305
|
-
const glyph = Math.min(24, w / 2, h / 2);
|
|
3306
|
-
const cx = x + w / 2;
|
|
3307
|
-
const cy = y + h / 2;
|
|
3308
|
-
ctx.strokeStyle = "#9e9e9e";
|
|
3309
|
-
ctx.lineWidth = 2;
|
|
3310
|
-
ctx.beginPath();
|
|
3311
|
-
ctx.arc(cx, cy, glyph / 2, 0, Math.PI * 2);
|
|
3312
|
-
ctx.moveTo(cx - glyph / 2, cy + glyph / 2);
|
|
3313
|
-
ctx.lineTo(cx + glyph / 2, cy - glyph / 2);
|
|
3314
|
-
ctx.stroke();
|
|
3315
|
-
ctx.restore();
|
|
3316
|
-
}
|
|
3317
3400
|
getHexTile(cellSize, orientation, strokeColor, strokeWidth, opacity, scale) {
|
|
3318
3401
|
const key = `${cellSize}:${orientation}:${strokeColor}:${strokeWidth}:${opacity}:${scale}`;
|
|
3319
3402
|
if (this.hexTileCacheKey === key && this.hexTileCache) {
|
|
@@ -3326,33 +3409,6 @@ var ElementRenderer = class {
|
|
|
3326
3409
|
}
|
|
3327
3410
|
return tile;
|
|
3328
3411
|
}
|
|
3329
|
-
getImage(src) {
|
|
3330
|
-
const cached = this.imageCache.get(src);
|
|
3331
|
-
if (cached) {
|
|
3332
|
-
if (cached === "failed") return null;
|
|
3333
|
-
if (cached instanceof HTMLImageElement) return cached.complete ? cached : null;
|
|
3334
|
-
return cached;
|
|
3335
|
-
}
|
|
3336
|
-
const img = new Image();
|
|
3337
|
-
img.src = src;
|
|
3338
|
-
this.imageCache.set(src, img);
|
|
3339
|
-
img.onload = () => {
|
|
3340
|
-
this.onImageLoad?.();
|
|
3341
|
-
if (typeof createImageBitmap !== "undefined") {
|
|
3342
|
-
createImageBitmap(img).then((bitmap) => {
|
|
3343
|
-
this.imageCache.set(src, bitmap);
|
|
3344
|
-
this.onImageLoad?.();
|
|
3345
|
-
}).catch(() => {
|
|
3346
|
-
});
|
|
3347
|
-
}
|
|
3348
|
-
};
|
|
3349
|
-
img.onerror = (event) => {
|
|
3350
|
-
this.imageCache.set(src, "failed");
|
|
3351
|
-
this.onImageError?.(src, event);
|
|
3352
|
-
this.onImageLoad?.();
|
|
3353
|
-
};
|
|
3354
|
-
return null;
|
|
3355
|
-
}
|
|
3356
3412
|
};
|
|
3357
3413
|
|
|
3358
3414
|
// src/elements/element-factory.ts
|
|
@@ -8987,7 +9043,7 @@ var TemplateTool = class {
|
|
|
8987
9043
|
};
|
|
8988
9044
|
|
|
8989
9045
|
// src/index.ts
|
|
8990
|
-
var VERSION = "0.38.
|
|
9046
|
+
var VERSION = "0.38.5";
|
|
8991
9047
|
export {
|
|
8992
9048
|
ArrowTool,
|
|
8993
9049
|
AutoSave,
|