@fieldnotes/core 0.8.11 → 0.10.0
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/README.md +63 -9
- package/dist/index.cjs +1582 -199
- package/dist/index.d.cts +244 -65
- package/dist/index.d.ts +244 -65
- package/dist/index.js +1561 -199
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,8 @@ __export(index_exports, {
|
|
|
27
27
|
BatchCommand: () => BatchCommand,
|
|
28
28
|
Camera: () => Camera,
|
|
29
29
|
CreateLayerCommand: () => CreateLayerCommand,
|
|
30
|
+
DEFAULT_FONT_SIZE_PRESETS: () => DEFAULT_FONT_SIZE_PRESETS,
|
|
31
|
+
DEFAULT_NOTE_FONT_SIZE: () => DEFAULT_NOTE_FONT_SIZE,
|
|
30
32
|
ElementRenderer: () => ElementRenderer,
|
|
31
33
|
ElementStore: () => ElementStore,
|
|
32
34
|
EraserTool: () => EraserTool,
|
|
@@ -37,14 +39,17 @@ __export(index_exports, {
|
|
|
37
39
|
ImageTool: () => ImageTool,
|
|
38
40
|
InputHandler: () => InputHandler,
|
|
39
41
|
LayerManager: () => LayerManager,
|
|
42
|
+
MeasureTool: () => MeasureTool,
|
|
40
43
|
NoteEditor: () => NoteEditor,
|
|
41
44
|
NoteTool: () => NoteTool,
|
|
45
|
+
NoteToolbar: () => NoteToolbar,
|
|
42
46
|
PencilTool: () => PencilTool,
|
|
43
47
|
Quadtree: () => Quadtree,
|
|
44
48
|
RemoveElementCommand: () => RemoveElementCommand,
|
|
45
49
|
RemoveLayerCommand: () => RemoveLayerCommand,
|
|
46
50
|
SelectTool: () => SelectTool,
|
|
47
51
|
ShapeTool: () => ShapeTool,
|
|
52
|
+
TemplateTool: () => TemplateTool,
|
|
48
53
|
TextTool: () => TextTool,
|
|
49
54
|
ToolManager: () => ToolManager,
|
|
50
55
|
UpdateElementCommand: () => UpdateElementCommand,
|
|
@@ -61,11 +66,14 @@ __export(index_exports, {
|
|
|
61
66
|
createNote: () => createNote,
|
|
62
67
|
createShape: () => createShape,
|
|
63
68
|
createStroke: () => createStroke,
|
|
69
|
+
createTemplate: () => createTemplate,
|
|
64
70
|
createText: () => createText,
|
|
71
|
+
drawHexPath: () => drawHexPath,
|
|
65
72
|
exportImage: () => exportImage,
|
|
66
73
|
exportState: () => exportState,
|
|
67
74
|
findBindTarget: () => findBindTarget,
|
|
68
75
|
findBoundArrows: () => findBoundArrows,
|
|
76
|
+
getActiveFormats: () => getActiveFormats,
|
|
69
77
|
getArrowBounds: () => getArrowBounds,
|
|
70
78
|
getArrowControlPoint: () => getArrowControlPoint,
|
|
71
79
|
getArrowMidpoint: () => getArrowMidpoint,
|
|
@@ -74,10 +82,23 @@ __export(index_exports, {
|
|
|
74
82
|
getEdgeIntersection: () => getEdgeIntersection,
|
|
75
83
|
getElementBounds: () => getElementBounds,
|
|
76
84
|
getElementCenter: () => getElementCenter,
|
|
85
|
+
getHexCellsInCone: () => getHexCellsInCone,
|
|
86
|
+
getHexCellsInLine: () => getHexCellsInLine,
|
|
87
|
+
getHexCellsInRadius: () => getHexCellsInRadius,
|
|
88
|
+
getHexCellsInSquare: () => getHexCellsInSquare,
|
|
89
|
+
getHexDistance: () => getHexDistance,
|
|
77
90
|
isBindable: () => isBindable,
|
|
78
91
|
isNearBezier: () => isNearBezier,
|
|
79
92
|
parseState: () => parseState,
|
|
93
|
+
sanitizeNoteHtml: () => sanitizeNoteHtml,
|
|
94
|
+
setFontSize: () => setFontSize,
|
|
95
|
+
smartSnap: () => smartSnap,
|
|
80
96
|
snapPoint: () => snapPoint,
|
|
97
|
+
snapToHexCenter: () => snapToHexCenter,
|
|
98
|
+
toggleBold: () => toggleBold,
|
|
99
|
+
toggleItalic: () => toggleItalic,
|
|
100
|
+
toggleStrikethrough: () => toggleStrikethrough,
|
|
101
|
+
toggleUnderline: () => toggleUnderline,
|
|
81
102
|
unbindArrow: () => unbindArrow,
|
|
82
103
|
updateBoundArrow: () => updateBoundArrow
|
|
83
104
|
});
|
|
@@ -254,6 +275,120 @@ var Quadtree = class {
|
|
|
254
275
|
}
|
|
255
276
|
};
|
|
256
277
|
|
|
278
|
+
// src/elements/note-sanitizer.ts
|
|
279
|
+
var BOLD_TAGS = /* @__PURE__ */ new Set(["b", "strong"]);
|
|
280
|
+
var ITALIC_TAGS = /* @__PURE__ */ new Set(["i", "em"]);
|
|
281
|
+
var UNDERLINE_TAGS = /* @__PURE__ */ new Set(["u"]);
|
|
282
|
+
var STRIKE_TAGS = /* @__PURE__ */ new Set(["s", "strike", "del"]);
|
|
283
|
+
var BLOCK_TAGS = /* @__PURE__ */ new Set(["div"]);
|
|
284
|
+
function parseStyledRuns(html, baseFontSize) {
|
|
285
|
+
if (!html) return [];
|
|
286
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
287
|
+
const runs = [];
|
|
288
|
+
const baseStyle = {
|
|
289
|
+
bold: false,
|
|
290
|
+
italic: false,
|
|
291
|
+
underline: false,
|
|
292
|
+
strikethrough: false,
|
|
293
|
+
fontSize: baseFontSize
|
|
294
|
+
};
|
|
295
|
+
walkNodes(doc.body, baseStyle, runs);
|
|
296
|
+
return runs;
|
|
297
|
+
}
|
|
298
|
+
function walkNodes(node, style, runs) {
|
|
299
|
+
for (const child of Array.from(node.childNodes)) {
|
|
300
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
301
|
+
const text = child.textContent ?? "";
|
|
302
|
+
if (text) {
|
|
303
|
+
runs.push({ text, ...style });
|
|
304
|
+
}
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (child.nodeType !== Node.ELEMENT_NODE) continue;
|
|
308
|
+
const el = child;
|
|
309
|
+
const tag = el.tagName.toLowerCase();
|
|
310
|
+
if (tag === "br") {
|
|
311
|
+
runs.push({ text: "\n", ...style });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (BLOCK_TAGS.has(tag) && runs.length > 0) {
|
|
315
|
+
const lastRun = runs[runs.length - 1];
|
|
316
|
+
if (lastRun && !lastRun.text.endsWith("\n")) {
|
|
317
|
+
runs.push({ text: "\n", ...style });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const childStyle = { ...style };
|
|
321
|
+
if (BOLD_TAGS.has(tag)) childStyle.bold = true;
|
|
322
|
+
if (ITALIC_TAGS.has(tag)) childStyle.italic = true;
|
|
323
|
+
if (UNDERLINE_TAGS.has(tag)) childStyle.underline = true;
|
|
324
|
+
if (STRIKE_TAGS.has(tag)) childStyle.strikethrough = true;
|
|
325
|
+
if (tag === "span") {
|
|
326
|
+
const fontSize = el.style.fontSize;
|
|
327
|
+
if (fontSize) {
|
|
328
|
+
childStyle.fontSize = parseInt(fontSize, 10) || style.fontSize;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
walkNodes(el, childStyle, runs);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
var ALLOWED_TAGS = /* @__PURE__ */ new Set([
|
|
335
|
+
"b",
|
|
336
|
+
"strong",
|
|
337
|
+
"i",
|
|
338
|
+
"em",
|
|
339
|
+
"u",
|
|
340
|
+
"s",
|
|
341
|
+
"strike",
|
|
342
|
+
"del",
|
|
343
|
+
"span",
|
|
344
|
+
"br",
|
|
345
|
+
"div"
|
|
346
|
+
]);
|
|
347
|
+
function sanitizeNoteHtml(html) {
|
|
348
|
+
if (!html) return "";
|
|
349
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
350
|
+
sanitizeNode(doc.body);
|
|
351
|
+
return doc.body.innerHTML;
|
|
352
|
+
}
|
|
353
|
+
function sanitizeNode(node) {
|
|
354
|
+
const children = Array.from(node.childNodes);
|
|
355
|
+
for (const child of children) {
|
|
356
|
+
if (child.nodeType === Node.TEXT_NODE) continue;
|
|
357
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
358
|
+
child.remove();
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const el = child;
|
|
362
|
+
const tag = el.tagName.toLowerCase();
|
|
363
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
364
|
+
const fragment = document.createDocumentFragment();
|
|
365
|
+
while (el.firstChild) {
|
|
366
|
+
fragment.appendChild(el.firstChild);
|
|
367
|
+
}
|
|
368
|
+
node.replaceChild(fragment, el);
|
|
369
|
+
sanitizeNode(node);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
sanitizeAttributes(el, tag);
|
|
373
|
+
sanitizeNode(el);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function sanitizeAttributes(el, tag) {
|
|
377
|
+
const attrs = Array.from(el.attributes);
|
|
378
|
+
for (const attr of attrs) {
|
|
379
|
+
if (tag === "span" && attr.name === "style") {
|
|
380
|
+
const fontSize = el.style.fontSize;
|
|
381
|
+
if (fontSize) {
|
|
382
|
+
el.setAttribute("style", `font-size: ${fontSize};`);
|
|
383
|
+
} else {
|
|
384
|
+
el.removeAttribute("style");
|
|
385
|
+
}
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
el.removeAttribute(attr.name);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
257
392
|
// src/core/state-serializer.ts
|
|
258
393
|
var CURRENT_VERSION = 2;
|
|
259
394
|
function exportState(elements, camera, layers = []) {
|
|
@@ -316,7 +451,17 @@ function validateState(data) {
|
|
|
316
451
|
];
|
|
317
452
|
}
|
|
318
453
|
}
|
|
319
|
-
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
454
|
+
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
455
|
+
"stroke",
|
|
456
|
+
"note",
|
|
457
|
+
"arrow",
|
|
458
|
+
"image",
|
|
459
|
+
"html",
|
|
460
|
+
"text",
|
|
461
|
+
"shape",
|
|
462
|
+
"grid",
|
|
463
|
+
"template"
|
|
464
|
+
]);
|
|
320
465
|
function validateElement(el) {
|
|
321
466
|
if (!el || typeof el !== "object") {
|
|
322
467
|
throw new Error("Invalid element: expected an object");
|
|
@@ -366,6 +511,9 @@ function migrateElement(obj) {
|
|
|
366
511
|
if (obj["type"] === "note" && typeof obj["textColor"] !== "string") {
|
|
367
512
|
obj["textColor"] = "#000000";
|
|
368
513
|
}
|
|
514
|
+
if (obj["type"] === "note" && typeof obj["text"] === "string") {
|
|
515
|
+
obj["text"] = sanitizeNoteHtml(obj["text"]);
|
|
516
|
+
}
|
|
369
517
|
}
|
|
370
518
|
|
|
371
519
|
// src/core/snap.ts
|
|
@@ -375,6 +523,30 @@ function snapPoint(point, gridSize) {
|
|
|
375
523
|
y: Math.round(point.y / gridSize) * gridSize || 0
|
|
376
524
|
};
|
|
377
525
|
}
|
|
526
|
+
function snapToHexCenter(point, cellSize, orientation) {
|
|
527
|
+
if (orientation === "pointy") {
|
|
528
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
529
|
+
const rowH = 1.5 * cellSize;
|
|
530
|
+
const row = Math.round(point.y / rowH);
|
|
531
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
532
|
+
const col = Math.round((point.x - offsetX) / hexW);
|
|
533
|
+
return { x: col * hexW + offsetX || 0, y: row * rowH || 0 };
|
|
534
|
+
} else {
|
|
535
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
536
|
+
const colW = 1.5 * cellSize;
|
|
537
|
+
const col = Math.round(point.x / colW);
|
|
538
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
539
|
+
const row = Math.round((point.y - offsetY) / hexH);
|
|
540
|
+
return { x: col * colW || 0, y: row * hexH + offsetY || 0 };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function smartSnap(point, ctx) {
|
|
544
|
+
if (!ctx.snapToGrid || !ctx.gridSize) return point;
|
|
545
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
546
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
547
|
+
}
|
|
548
|
+
return snapPoint(point, ctx.gridSize);
|
|
549
|
+
}
|
|
378
550
|
|
|
379
551
|
// src/core/auto-save.ts
|
|
380
552
|
var DEFAULT_KEY = "fieldnotes-autosave";
|
|
@@ -1027,6 +1199,9 @@ function getElementBounds(element) {
|
|
|
1027
1199
|
if (element.type === "arrow") {
|
|
1028
1200
|
return getArrowBoundsAnalytical(element.from, element.to, element.bend);
|
|
1029
1201
|
}
|
|
1202
|
+
if (element.type === "template") {
|
|
1203
|
+
return getTemplateBounds(element);
|
|
1204
|
+
}
|
|
1030
1205
|
return null;
|
|
1031
1206
|
}
|
|
1032
1207
|
function getArrowBoundsAnalytical(from, to, bend) {
|
|
@@ -1067,6 +1242,62 @@ function getArrowBoundsAnalytical(from, to, bend) {
|
|
|
1067
1242
|
}
|
|
1068
1243
|
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1069
1244
|
}
|
|
1245
|
+
function getTemplateBounds(el) {
|
|
1246
|
+
const { x: cx, y: cy } = el.position;
|
|
1247
|
+
const r = el.radius;
|
|
1248
|
+
switch (el.templateShape) {
|
|
1249
|
+
case "circle":
|
|
1250
|
+
return { x: cx - r, y: cy - r, w: 2 * r, h: 2 * r };
|
|
1251
|
+
case "square":
|
|
1252
|
+
return { x: cx - r / 2, y: cy - r / 2, w: r, h: r };
|
|
1253
|
+
case "cone": {
|
|
1254
|
+
const halfAngle = Math.atan(0.5);
|
|
1255
|
+
const tipX = cx;
|
|
1256
|
+
const tipY = cy;
|
|
1257
|
+
const leftX = cx + r * Math.cos(el.angle - halfAngle);
|
|
1258
|
+
const leftY = cy + r * Math.sin(el.angle - halfAngle);
|
|
1259
|
+
const rightX = cx + r * Math.cos(el.angle + halfAngle);
|
|
1260
|
+
const rightY = cy + r * Math.sin(el.angle + halfAngle);
|
|
1261
|
+
const farX = cx + r * Math.cos(el.angle);
|
|
1262
|
+
const farY = cy + r * Math.sin(el.angle);
|
|
1263
|
+
const xs = [tipX, leftX, rightX, farX];
|
|
1264
|
+
const ys = [tipY, leftY, rightY, farY];
|
|
1265
|
+
let minX = Infinity;
|
|
1266
|
+
let minY = Infinity;
|
|
1267
|
+
let maxX = -Infinity;
|
|
1268
|
+
let maxY = -Infinity;
|
|
1269
|
+
for (let i = 0; i < xs.length; i++) {
|
|
1270
|
+
const px = xs[i];
|
|
1271
|
+
const py = ys[i];
|
|
1272
|
+
if (px !== void 0 && px < minX) minX = px;
|
|
1273
|
+
if (px !== void 0 && px > maxX) maxX = px;
|
|
1274
|
+
if (py !== void 0 && py < minY) minY = py;
|
|
1275
|
+
if (py !== void 0 && py > maxY) maxY = py;
|
|
1276
|
+
}
|
|
1277
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1278
|
+
}
|
|
1279
|
+
case "line": {
|
|
1280
|
+
const halfW = r / 12;
|
|
1281
|
+
const cos = Math.cos(el.angle);
|
|
1282
|
+
const sin = Math.sin(el.angle);
|
|
1283
|
+
const perpX = -sin * halfW;
|
|
1284
|
+
const perpY = cos * halfW;
|
|
1285
|
+
const x0 = cx + perpX;
|
|
1286
|
+
const y0 = cy + perpY;
|
|
1287
|
+
const x1 = cx + r * cos + perpX;
|
|
1288
|
+
const y1 = cy + r * sin + perpY;
|
|
1289
|
+
const x2 = cx + r * cos - perpX;
|
|
1290
|
+
const y2 = cy + r * sin - perpY;
|
|
1291
|
+
const x3 = cx - perpX;
|
|
1292
|
+
const y3 = cy - perpY;
|
|
1293
|
+
const minX = Math.min(x0, x1, x2, x3);
|
|
1294
|
+
const minY = Math.min(y0, y1, y2, y3);
|
|
1295
|
+
const maxX = Math.max(x0, x1, x2, x3);
|
|
1296
|
+
const maxY = Math.max(y0, y1, y2, y3);
|
|
1297
|
+
return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1070
1301
|
function boundsIntersect(a, b) {
|
|
1071
1302
|
return a.x <= b.x + b.w && a.x + a.w >= b.x && a.y <= b.y + b.h && a.y + a.h >= b.y;
|
|
1072
1303
|
}
|
|
@@ -1587,6 +1818,190 @@ function renderHexGridTiled(ctx, bounds, cellSize, tile) {
|
|
|
1587
1818
|
}
|
|
1588
1819
|
}
|
|
1589
1820
|
|
|
1821
|
+
// src/elements/hex-fill.ts
|
|
1822
|
+
function offsetToCube(col, row, orientation) {
|
|
1823
|
+
if (orientation === "pointy") {
|
|
1824
|
+
return { q: col - (row - (row & 1)) / 2, r: row };
|
|
1825
|
+
}
|
|
1826
|
+
return { q: col, r: row - (col - (col & 1)) / 2 };
|
|
1827
|
+
}
|
|
1828
|
+
function cubeToOffset(q, r, orientation) {
|
|
1829
|
+
if (orientation === "pointy") {
|
|
1830
|
+
return { col: q + (r - (r & 1)) / 2, row: r };
|
|
1831
|
+
}
|
|
1832
|
+
return { col: q, row: r + (q - (q & 1)) / 2 };
|
|
1833
|
+
}
|
|
1834
|
+
function offsetToPixel(col, row, cellSize, orientation) {
|
|
1835
|
+
if (orientation === "pointy") {
|
|
1836
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
1837
|
+
const rowH = 1.5 * cellSize;
|
|
1838
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
1839
|
+
return { x: col * hexW + offsetX, y: row * rowH };
|
|
1840
|
+
}
|
|
1841
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
1842
|
+
const colW = 1.5 * cellSize;
|
|
1843
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
1844
|
+
return { x: col * colW, y: row * hexH + offsetY };
|
|
1845
|
+
}
|
|
1846
|
+
function pixelToOffset(x, y, cellSize, orientation) {
|
|
1847
|
+
if (orientation === "pointy") {
|
|
1848
|
+
const hexW = Math.sqrt(3) * cellSize;
|
|
1849
|
+
const rowH = 1.5 * cellSize;
|
|
1850
|
+
const row = Math.round(y / rowH);
|
|
1851
|
+
const offsetX = row % 2 !== 0 ? hexW / 2 : 0;
|
|
1852
|
+
return { col: Math.round((x - offsetX) / hexW), row };
|
|
1853
|
+
}
|
|
1854
|
+
const hexH = Math.sqrt(3) * cellSize;
|
|
1855
|
+
const colW = 1.5 * cellSize;
|
|
1856
|
+
const col = Math.round(x / colW);
|
|
1857
|
+
const offsetY = col % 2 !== 0 ? hexH / 2 : 0;
|
|
1858
|
+
return { col, row: Math.round((y - offsetY) / hexH) };
|
|
1859
|
+
}
|
|
1860
|
+
function enumerateHexRing(centerQ, centerR, n, orientation, cellSize) {
|
|
1861
|
+
const cells = [];
|
|
1862
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1863
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1864
|
+
const rMax = Math.min(n, -dq + n);
|
|
1865
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1866
|
+
const absQ = centerQ + dq;
|
|
1867
|
+
const absR = centerR + dr;
|
|
1868
|
+
const off = cubeToOffset(absQ, absR, orientation);
|
|
1869
|
+
cells.push(offsetToPixel(off.col, off.row, cellSize, orientation));
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
return cells;
|
|
1873
|
+
}
|
|
1874
|
+
function getHexDistance(a, b, cellSize, orientation) {
|
|
1875
|
+
const offA = pixelToOffset(a.x, a.y, cellSize, orientation);
|
|
1876
|
+
const offB = pixelToOffset(b.x, b.y, cellSize, orientation);
|
|
1877
|
+
const cubeA = offsetToCube(offA.col, offA.row, orientation);
|
|
1878
|
+
const cubeB = offsetToCube(offB.col, offB.row, orientation);
|
|
1879
|
+
const dq = cubeA.q - cubeB.q;
|
|
1880
|
+
const dr = cubeA.r - cubeB.r;
|
|
1881
|
+
const ds = -dq - dr;
|
|
1882
|
+
return Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));
|
|
1883
|
+
}
|
|
1884
|
+
function getHexCellsInRadius(center, radiusCells, cellSize, orientation) {
|
|
1885
|
+
const n = Math.round(radiusCells);
|
|
1886
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1887
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1888
|
+
if (n <= 0) {
|
|
1889
|
+
return [offsetToPixel(off.col, off.row, cellSize, orientation)];
|
|
1890
|
+
}
|
|
1891
|
+
return enumerateHexRing(cube.q, cube.r, n, orientation, cellSize);
|
|
1892
|
+
}
|
|
1893
|
+
function getHexCellsInCone(center, angle, radiusCells, cellSize, orientation) {
|
|
1894
|
+
const n = Math.round(radiusCells);
|
|
1895
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1896
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1897
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
1898
|
+
if (n <= 0) return [centerPixel];
|
|
1899
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
1900
|
+
const step = Math.PI / 3;
|
|
1901
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
1902
|
+
const halfAngle = Math.PI / 6 + 1e-6;
|
|
1903
|
+
const cells = [centerPixel];
|
|
1904
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1905
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1906
|
+
const rMax = Math.min(n, -dq + n);
|
|
1907
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1908
|
+
if (dq === 0 && dr === 0) continue;
|
|
1909
|
+
const absQ = cube.q + dq;
|
|
1910
|
+
const absR = cube.r + dr;
|
|
1911
|
+
const pixel = offsetToPixel(
|
|
1912
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
1913
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
1914
|
+
cellSize,
|
|
1915
|
+
orientation
|
|
1916
|
+
);
|
|
1917
|
+
const dx = pixel.x - centerPixel.x;
|
|
1918
|
+
const dy = pixel.y - centerPixel.y;
|
|
1919
|
+
let diff = Math.atan2(dy, dx) - snappedAngle;
|
|
1920
|
+
if (diff > Math.PI) diff -= 2 * Math.PI;
|
|
1921
|
+
if (diff < -Math.PI) diff += 2 * Math.PI;
|
|
1922
|
+
if (Math.abs(diff) <= halfAngle) {
|
|
1923
|
+
cells.push(pixel);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
return cells;
|
|
1928
|
+
}
|
|
1929
|
+
function getHexCellsInLine(center, angle, radiusCells, cellSize, orientation) {
|
|
1930
|
+
const n = Math.round(radiusCells);
|
|
1931
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1932
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1933
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
1934
|
+
if (n <= 0) return [centerPixel];
|
|
1935
|
+
const vertexOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
1936
|
+
const step = Math.PI / 3;
|
|
1937
|
+
const snappedAngle = Math.round((angle - vertexOffset) / step) * step + vertexOffset;
|
|
1938
|
+
const cos = Math.cos(snappedAngle);
|
|
1939
|
+
const sin = Math.sin(snappedAngle);
|
|
1940
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
1941
|
+
const lineLength = n * snapUnit;
|
|
1942
|
+
const halfWidth = snapUnit * 0.5 + 1e-6;
|
|
1943
|
+
const cells = [];
|
|
1944
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1945
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1946
|
+
const rMax = Math.min(n, -dq + n);
|
|
1947
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1948
|
+
const absQ = cube.q + dq;
|
|
1949
|
+
const absR = cube.r + dr;
|
|
1950
|
+
const pixel = offsetToPixel(
|
|
1951
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
1952
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
1953
|
+
cellSize,
|
|
1954
|
+
orientation
|
|
1955
|
+
);
|
|
1956
|
+
const dx = pixel.x - centerPixel.x;
|
|
1957
|
+
const dy = pixel.y - centerPixel.y;
|
|
1958
|
+
const along = dx * cos + dy * sin;
|
|
1959
|
+
const perp = Math.abs(-dx * sin + dy * cos);
|
|
1960
|
+
if (along >= -snapUnit * 0.1 && along <= lineLength + snapUnit * 0.1 && perp <= halfWidth) {
|
|
1961
|
+
cells.push(pixel);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return cells;
|
|
1966
|
+
}
|
|
1967
|
+
function getHexCellsInSquare(center, radiusCells, cellSize, orientation) {
|
|
1968
|
+
const n = Math.round(radiusCells);
|
|
1969
|
+
const off = pixelToOffset(center.x, center.y, cellSize, orientation);
|
|
1970
|
+
const cube = offsetToCube(off.col, off.row, orientation);
|
|
1971
|
+
const centerPixel = offsetToPixel(off.col, off.row, cellSize, orientation);
|
|
1972
|
+
if (n <= 0) return [centerPixel];
|
|
1973
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
1974
|
+
const halfSide = n * snapUnit / 2;
|
|
1975
|
+
const cells = [];
|
|
1976
|
+
for (let dq = -n; dq <= n; dq++) {
|
|
1977
|
+
const rMin = Math.max(-n, -dq - n);
|
|
1978
|
+
const rMax = Math.min(n, -dq + n);
|
|
1979
|
+
for (let dr = rMin; dr <= rMax; dr++) {
|
|
1980
|
+
const absQ = cube.q + dq;
|
|
1981
|
+
const absR = cube.r + dr;
|
|
1982
|
+
const pixel = offsetToPixel(
|
|
1983
|
+
cubeToOffset(absQ, absR, orientation).col,
|
|
1984
|
+
cubeToOffset(absQ, absR, orientation).row,
|
|
1985
|
+
cellSize,
|
|
1986
|
+
orientation
|
|
1987
|
+
);
|
|
1988
|
+
if (Math.abs(pixel.x - centerPixel.x) <= halfSide && Math.abs(pixel.y - centerPixel.y) <= halfSide) {
|
|
1989
|
+
cells.push(pixel);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return cells;
|
|
1994
|
+
}
|
|
1995
|
+
function drawHexPath(ctx, cx, cy, cellSize, orientation) {
|
|
1996
|
+
const angleOffset = orientation === "pointy" ? Math.PI / 6 : 0;
|
|
1997
|
+
ctx.moveTo(cx + cellSize * Math.cos(angleOffset), cy + cellSize * Math.sin(angleOffset));
|
|
1998
|
+
for (let i = 1; i < 6; i++) {
|
|
1999
|
+
const a = angleOffset + Math.PI / 3 * i;
|
|
2000
|
+
ctx.lineTo(cx + cellSize * Math.cos(a), cy + cellSize * Math.sin(a));
|
|
2001
|
+
}
|
|
2002
|
+
ctx.closePath();
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1590
2005
|
// src/elements/element-renderer.ts
|
|
1591
2006
|
var DOM_ELEMENT_TYPES = /* @__PURE__ */ new Set(["note", "html", "text"]);
|
|
1592
2007
|
var ARROWHEAD_LENGTH = 12;
|
|
@@ -1631,6 +2046,9 @@ var ElementRenderer = class {
|
|
|
1631
2046
|
case "grid":
|
|
1632
2047
|
this.renderGrid(ctx, element);
|
|
1633
2048
|
break;
|
|
2049
|
+
case "template":
|
|
2050
|
+
this.renderTemplate(ctx, element);
|
|
2051
|
+
break;
|
|
1634
2052
|
}
|
|
1635
2053
|
}
|
|
1636
2054
|
renderStroke(ctx, stroke) {
|
|
@@ -1818,6 +2236,147 @@ var ElementRenderer = class {
|
|
|
1818
2236
|
);
|
|
1819
2237
|
}
|
|
1820
2238
|
}
|
|
2239
|
+
renderTemplate(ctx, template) {
|
|
2240
|
+
const grid = this.store?.getElementsByType("grid")[0];
|
|
2241
|
+
if (grid && grid.gridType === "hex") {
|
|
2242
|
+
this.renderHexTemplate(ctx, template, grid.cellSize, grid.hexOrientation);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
this.renderGeometricTemplate(ctx, template);
|
|
2246
|
+
}
|
|
2247
|
+
renderGeometricTemplate(ctx, template) {
|
|
2248
|
+
const { x: cx, y: cy } = template.position;
|
|
2249
|
+
const r = template.radius;
|
|
2250
|
+
ctx.save();
|
|
2251
|
+
ctx.globalAlpha = template.opacity;
|
|
2252
|
+
ctx.fillStyle = template.fillColor;
|
|
2253
|
+
ctx.strokeStyle = template.strokeColor;
|
|
2254
|
+
ctx.lineWidth = template.strokeWidth;
|
|
2255
|
+
switch (template.templateShape) {
|
|
2256
|
+
case "circle":
|
|
2257
|
+
ctx.beginPath();
|
|
2258
|
+
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
|
2259
|
+
ctx.fill();
|
|
2260
|
+
ctx.stroke();
|
|
2261
|
+
if (template.radiusFeet != null && template.radiusFeet > 0) {
|
|
2262
|
+
this.renderRadiusMarker(ctx, cx, cy, r, template.radiusFeet);
|
|
2263
|
+
}
|
|
2264
|
+
break;
|
|
2265
|
+
case "square":
|
|
2266
|
+
ctx.fillRect(cx - r / 2, cy - r / 2, r, r);
|
|
2267
|
+
ctx.strokeRect(cx - r / 2, cy - r / 2, r, r);
|
|
2268
|
+
break;
|
|
2269
|
+
case "cone": {
|
|
2270
|
+
const halfAngle = Math.atan(0.5);
|
|
2271
|
+
ctx.beginPath();
|
|
2272
|
+
ctx.moveTo(cx, cy);
|
|
2273
|
+
ctx.arc(cx, cy, r, template.angle - halfAngle, template.angle + halfAngle);
|
|
2274
|
+
ctx.closePath();
|
|
2275
|
+
ctx.fill();
|
|
2276
|
+
ctx.stroke();
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
case "line": {
|
|
2280
|
+
const halfW = r / 12;
|
|
2281
|
+
const cos = Math.cos(template.angle);
|
|
2282
|
+
const sin = Math.sin(template.angle);
|
|
2283
|
+
const perpX = -sin * halfW;
|
|
2284
|
+
const perpY = cos * halfW;
|
|
2285
|
+
ctx.beginPath();
|
|
2286
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
2287
|
+
ctx.lineTo(cx + r * cos + perpX, cy + r * sin + perpY);
|
|
2288
|
+
ctx.lineTo(cx + r * cos - perpX, cy + r * sin - perpY);
|
|
2289
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
2290
|
+
ctx.closePath();
|
|
2291
|
+
ctx.fill();
|
|
2292
|
+
ctx.stroke();
|
|
2293
|
+
break;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
ctx.restore();
|
|
2297
|
+
}
|
|
2298
|
+
renderHexTemplate(ctx, template, cellSize, orientation) {
|
|
2299
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
2300
|
+
const radiusCells = template.radius / snapUnit;
|
|
2301
|
+
const center = template.position;
|
|
2302
|
+
let cells;
|
|
2303
|
+
switch (template.templateShape) {
|
|
2304
|
+
case "circle":
|
|
2305
|
+
cells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
|
|
2306
|
+
break;
|
|
2307
|
+
case "cone":
|
|
2308
|
+
cells = getHexCellsInCone(center, template.angle, radiusCells, cellSize, orientation);
|
|
2309
|
+
break;
|
|
2310
|
+
case "line":
|
|
2311
|
+
cells = getHexCellsInLine(center, template.angle, radiusCells, cellSize, orientation);
|
|
2312
|
+
break;
|
|
2313
|
+
case "square":
|
|
2314
|
+
cells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
|
|
2315
|
+
break;
|
|
2316
|
+
}
|
|
2317
|
+
ctx.save();
|
|
2318
|
+
ctx.globalAlpha = template.opacity;
|
|
2319
|
+
ctx.beginPath();
|
|
2320
|
+
for (const cell of cells) {
|
|
2321
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
2322
|
+
}
|
|
2323
|
+
ctx.fillStyle = template.fillColor;
|
|
2324
|
+
ctx.fill();
|
|
2325
|
+
ctx.beginPath();
|
|
2326
|
+
for (const cell of cells) {
|
|
2327
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
2328
|
+
}
|
|
2329
|
+
ctx.strokeStyle = template.strokeColor;
|
|
2330
|
+
ctx.lineWidth = template.strokeWidth;
|
|
2331
|
+
ctx.stroke();
|
|
2332
|
+
{
|
|
2333
|
+
ctx.globalAlpha = Math.min(template.opacity + 0.1, 1);
|
|
2334
|
+
ctx.beginPath();
|
|
2335
|
+
drawHexPath(ctx, center.x, center.y, cellSize, orientation);
|
|
2336
|
+
ctx.fillStyle = template.strokeColor;
|
|
2337
|
+
ctx.fill();
|
|
2338
|
+
ctx.strokeStyle = template.strokeColor;
|
|
2339
|
+
ctx.lineWidth = template.strokeWidth;
|
|
2340
|
+
ctx.stroke();
|
|
2341
|
+
}
|
|
2342
|
+
if (template.templateShape === "circle" && template.radiusFeet != null && template.radiusFeet > 0) {
|
|
2343
|
+
const r = template.radius;
|
|
2344
|
+
this.renderRadiusMarker(ctx, center.x, center.y, r, template.radiusFeet);
|
|
2345
|
+
}
|
|
2346
|
+
ctx.restore();
|
|
2347
|
+
}
|
|
2348
|
+
renderRadiusMarker(ctx, cx, cy, r, feet) {
|
|
2349
|
+
const markerColor = ctx.strokeStyle;
|
|
2350
|
+
ctx.save();
|
|
2351
|
+
ctx.globalAlpha = 1;
|
|
2352
|
+
ctx.beginPath();
|
|
2353
|
+
ctx.setLineDash([4, 4]);
|
|
2354
|
+
ctx.strokeStyle = markerColor;
|
|
2355
|
+
ctx.lineWidth = 1.5;
|
|
2356
|
+
ctx.moveTo(cx, cy);
|
|
2357
|
+
ctx.lineTo(cx + r, cy);
|
|
2358
|
+
ctx.stroke();
|
|
2359
|
+
ctx.setLineDash([]);
|
|
2360
|
+
const label = `${Math.round(feet)} ft`;
|
|
2361
|
+
const fontSize = Math.max(10, Math.min(14, r * 0.15));
|
|
2362
|
+
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
2363
|
+
ctx.textAlign = "center";
|
|
2364
|
+
ctx.textBaseline = "bottom";
|
|
2365
|
+
const textX = cx + r / 2;
|
|
2366
|
+
const textY = cy - 4;
|
|
2367
|
+
const metrics = ctx.measureText(label);
|
|
2368
|
+
const padX = 4;
|
|
2369
|
+
const padY = 2;
|
|
2370
|
+
const textW = metrics.width + padX * 2;
|
|
2371
|
+
const textH = fontSize + padY * 2;
|
|
2372
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
2373
|
+
ctx.beginPath();
|
|
2374
|
+
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
2375
|
+
ctx.fill();
|
|
2376
|
+
ctx.fillStyle = markerColor;
|
|
2377
|
+
ctx.fillText(label, textX, textY - padY);
|
|
2378
|
+
ctx.restore();
|
|
2379
|
+
}
|
|
1821
2380
|
renderImage(ctx, image) {
|
|
1822
2381
|
const img = this.getImage(image.src);
|
|
1823
2382
|
if (!img) return;
|
|
@@ -1864,7 +2423,359 @@ var ElementRenderer = class {
|
|
|
1864
2423
|
}
|
|
1865
2424
|
};
|
|
1866
2425
|
|
|
2426
|
+
// src/elements/create-id.ts
|
|
2427
|
+
var counter = 0;
|
|
2428
|
+
function createId(prefix) {
|
|
2429
|
+
return `${prefix}_${Date.now().toString(36)}_${(counter++).toString(36)}`;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
// src/elements/element-factory.ts
|
|
2433
|
+
var DEFAULT_NOTE_FONT_SIZE = 18;
|
|
2434
|
+
function createStroke(input) {
|
|
2435
|
+
return {
|
|
2436
|
+
id: createId("stroke"),
|
|
2437
|
+
type: "stroke",
|
|
2438
|
+
position: input.position ?? { x: 0, y: 0 },
|
|
2439
|
+
zIndex: input.zIndex ?? 0,
|
|
2440
|
+
locked: input.locked ?? false,
|
|
2441
|
+
layerId: input.layerId ?? "",
|
|
2442
|
+
points: input.points,
|
|
2443
|
+
color: input.color ?? "#000000",
|
|
2444
|
+
width: input.width ?? 2,
|
|
2445
|
+
opacity: input.opacity ?? 1
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function createNote(input) {
|
|
2449
|
+
return {
|
|
2450
|
+
id: createId("note"),
|
|
2451
|
+
type: "note",
|
|
2452
|
+
position: input.position,
|
|
2453
|
+
zIndex: input.zIndex ?? 0,
|
|
2454
|
+
locked: input.locked ?? false,
|
|
2455
|
+
layerId: input.layerId ?? "",
|
|
2456
|
+
size: input.size ?? { w: 200, h: 100 },
|
|
2457
|
+
text: input.text ?? "",
|
|
2458
|
+
backgroundColor: input.backgroundColor ?? "#ffeb3b",
|
|
2459
|
+
textColor: input.textColor ?? "#000000",
|
|
2460
|
+
fontSize: input.fontSize ?? DEFAULT_NOTE_FONT_SIZE
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
function createArrow(input) {
|
|
2464
|
+
const bend = input.bend ?? 0;
|
|
2465
|
+
const result = {
|
|
2466
|
+
id: createId("arrow"),
|
|
2467
|
+
type: "arrow",
|
|
2468
|
+
position: input.position ?? { x: 0, y: 0 },
|
|
2469
|
+
zIndex: input.zIndex ?? 0,
|
|
2470
|
+
locked: input.locked ?? false,
|
|
2471
|
+
layerId: input.layerId ?? "",
|
|
2472
|
+
from: input.from,
|
|
2473
|
+
to: input.to,
|
|
2474
|
+
bend,
|
|
2475
|
+
color: input.color ?? "#000000",
|
|
2476
|
+
width: input.width ?? 2,
|
|
2477
|
+
cachedControlPoint: getArrowControlPoint(input.from, input.to, bend)
|
|
2478
|
+
};
|
|
2479
|
+
if (input.fromBinding) result.fromBinding = input.fromBinding;
|
|
2480
|
+
if (input.toBinding) result.toBinding = input.toBinding;
|
|
2481
|
+
return result;
|
|
2482
|
+
}
|
|
2483
|
+
function createImage(input) {
|
|
2484
|
+
return {
|
|
2485
|
+
id: createId("image"),
|
|
2486
|
+
type: "image",
|
|
2487
|
+
position: input.position,
|
|
2488
|
+
zIndex: input.zIndex ?? 0,
|
|
2489
|
+
locked: input.locked ?? false,
|
|
2490
|
+
layerId: input.layerId ?? "",
|
|
2491
|
+
size: input.size,
|
|
2492
|
+
src: input.src
|
|
2493
|
+
};
|
|
2494
|
+
}
|
|
2495
|
+
function createHtmlElement(input) {
|
|
2496
|
+
const el = {
|
|
2497
|
+
id: createId("html"),
|
|
2498
|
+
type: "html",
|
|
2499
|
+
position: input.position,
|
|
2500
|
+
zIndex: input.zIndex ?? 0,
|
|
2501
|
+
locked: input.locked ?? false,
|
|
2502
|
+
layerId: input.layerId ?? "",
|
|
2503
|
+
size: input.size
|
|
2504
|
+
};
|
|
2505
|
+
if (input.domId) el.domId = input.domId;
|
|
2506
|
+
return el;
|
|
2507
|
+
}
|
|
2508
|
+
function createShape(input) {
|
|
2509
|
+
return {
|
|
2510
|
+
id: createId("shape"),
|
|
2511
|
+
type: "shape",
|
|
2512
|
+
position: input.position,
|
|
2513
|
+
zIndex: input.zIndex ?? 0,
|
|
2514
|
+
locked: input.locked ?? false,
|
|
2515
|
+
layerId: input.layerId ?? "",
|
|
2516
|
+
shape: input.shape ?? "rectangle",
|
|
2517
|
+
size: input.size,
|
|
2518
|
+
strokeColor: input.strokeColor ?? "#000000",
|
|
2519
|
+
strokeWidth: input.strokeWidth ?? 2,
|
|
2520
|
+
fillColor: input.fillColor ?? "none"
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
function createGrid(input) {
|
|
2524
|
+
return {
|
|
2525
|
+
id: createId("grid"),
|
|
2526
|
+
type: "grid",
|
|
2527
|
+
position: input.position ?? { x: 0, y: 0 },
|
|
2528
|
+
zIndex: input.zIndex ?? 0,
|
|
2529
|
+
locked: input.locked ?? false,
|
|
2530
|
+
layerId: input.layerId ?? "",
|
|
2531
|
+
gridType: input.gridType ?? "square",
|
|
2532
|
+
hexOrientation: input.hexOrientation ?? "pointy",
|
|
2533
|
+
cellSize: input.cellSize ?? 40,
|
|
2534
|
+
strokeColor: input.strokeColor ?? "#000000",
|
|
2535
|
+
strokeWidth: input.strokeWidth ?? 1,
|
|
2536
|
+
opacity: input.opacity ?? 1
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
function createText(input) {
|
|
2540
|
+
return {
|
|
2541
|
+
id: createId("text"),
|
|
2542
|
+
type: "text",
|
|
2543
|
+
position: input.position,
|
|
2544
|
+
zIndex: input.zIndex ?? 0,
|
|
2545
|
+
locked: input.locked ?? false,
|
|
2546
|
+
layerId: input.layerId ?? "",
|
|
2547
|
+
size: input.size ?? { w: 200, h: 28 },
|
|
2548
|
+
text: input.text ?? "",
|
|
2549
|
+
fontSize: input.fontSize ?? 16,
|
|
2550
|
+
color: input.color ?? "#1a1a1a",
|
|
2551
|
+
textAlign: input.textAlign ?? "left"
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
function createTemplate(input) {
|
|
2555
|
+
return {
|
|
2556
|
+
id: createId("template"),
|
|
2557
|
+
type: "template",
|
|
2558
|
+
position: input.position,
|
|
2559
|
+
zIndex: input.zIndex ?? 0,
|
|
2560
|
+
locked: input.locked ?? false,
|
|
2561
|
+
layerId: input.layerId ?? "",
|
|
2562
|
+
templateShape: input.templateShape,
|
|
2563
|
+
radius: input.radius,
|
|
2564
|
+
angle: input.angle ?? 0,
|
|
2565
|
+
fillColor: input.fillColor ?? "rgba(255, 87, 34, 0.2)",
|
|
2566
|
+
strokeColor: input.strokeColor ?? "#FF5722",
|
|
2567
|
+
strokeWidth: input.strokeWidth ?? 2,
|
|
2568
|
+
opacity: input.opacity ?? 0.6,
|
|
2569
|
+
feetPerCell: input.feetPerCell,
|
|
2570
|
+
radiusFeet: input.radiusFeet
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// src/elements/note-formatting.ts
|
|
2575
|
+
function toggleBold() {
|
|
2576
|
+
document.execCommand("bold");
|
|
2577
|
+
}
|
|
2578
|
+
function toggleItalic() {
|
|
2579
|
+
document.execCommand("italic");
|
|
2580
|
+
}
|
|
2581
|
+
function toggleUnderline() {
|
|
2582
|
+
document.execCommand("underline");
|
|
2583
|
+
}
|
|
2584
|
+
function toggleStrikethrough() {
|
|
2585
|
+
document.execCommand("strikeThrough");
|
|
2586
|
+
}
|
|
2587
|
+
function setFontSize(size) {
|
|
2588
|
+
const sel = window.getSelection();
|
|
2589
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
2590
|
+
const range = sel.getRangeAt(0);
|
|
2591
|
+
if (range.collapsed) return;
|
|
2592
|
+
const span = document.createElement("span");
|
|
2593
|
+
span.style.fontSize = `${size}px`;
|
|
2594
|
+
try {
|
|
2595
|
+
range.surroundContents(span);
|
|
2596
|
+
} catch {
|
|
2597
|
+
span.appendChild(range.extractContents());
|
|
2598
|
+
range.insertNode(span);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
function getActiveFormats() {
|
|
2602
|
+
const query = (cmd) => {
|
|
2603
|
+
try {
|
|
2604
|
+
return document.queryCommandState(cmd);
|
|
2605
|
+
} catch {
|
|
2606
|
+
return false;
|
|
2607
|
+
}
|
|
2608
|
+
};
|
|
2609
|
+
return {
|
|
2610
|
+
bold: query("bold"),
|
|
2611
|
+
italic: query("italic"),
|
|
2612
|
+
underline: query("underline"),
|
|
2613
|
+
strikethrough: query("strikeThrough")
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
// src/elements/note-toolbar.ts
|
|
2618
|
+
var TOOLBAR_HEIGHT = 32;
|
|
2619
|
+
var TOOLBAR_GAP = 4;
|
|
2620
|
+
var FORMAT_BUTTONS = [
|
|
2621
|
+
{ label: "B", format: "bold", command: "bold" },
|
|
2622
|
+
{ label: "I", format: "italic", command: "italic" },
|
|
2623
|
+
{ label: "U", format: "underline", command: "underline" },
|
|
2624
|
+
{ label: "S", format: "strikethrough", command: "strikeThrough" }
|
|
2625
|
+
];
|
|
2626
|
+
var DEFAULT_FONT_SIZE_PRESETS = [
|
|
2627
|
+
{ label: "Small", size: 14 },
|
|
2628
|
+
{ label: "Normal", size: 18 },
|
|
2629
|
+
{ label: "Large", size: 24 },
|
|
2630
|
+
{ label: "Heading", size: 32 }
|
|
2631
|
+
];
|
|
2632
|
+
var NoteToolbar = class {
|
|
2633
|
+
el = null;
|
|
2634
|
+
anchor = null;
|
|
2635
|
+
selectionListener = null;
|
|
2636
|
+
fontSizePresets;
|
|
2637
|
+
constructor(fontSizePresets) {
|
|
2638
|
+
this.fontSizePresets = fontSizePresets ?? DEFAULT_FONT_SIZE_PRESETS;
|
|
2639
|
+
}
|
|
2640
|
+
show(anchor) {
|
|
2641
|
+
this.hide();
|
|
2642
|
+
this.anchor = anchor;
|
|
2643
|
+
this.el = this.createToolbarElement();
|
|
2644
|
+
document.body.appendChild(this.el);
|
|
2645
|
+
this.positionToolbar(anchor);
|
|
2646
|
+
this.selectionListener = () => this.updateActiveStates();
|
|
2647
|
+
document.addEventListener("selectionchange", this.selectionListener);
|
|
2648
|
+
}
|
|
2649
|
+
hide() {
|
|
2650
|
+
if (this.selectionListener) {
|
|
2651
|
+
document.removeEventListener("selectionchange", this.selectionListener);
|
|
2652
|
+
this.selectionListener = null;
|
|
2653
|
+
}
|
|
2654
|
+
if (this.el) {
|
|
2655
|
+
this.el.remove();
|
|
2656
|
+
this.el = null;
|
|
2657
|
+
}
|
|
2658
|
+
this.anchor = null;
|
|
2659
|
+
}
|
|
2660
|
+
getElement() {
|
|
2661
|
+
return this.el;
|
|
2662
|
+
}
|
|
2663
|
+
updatePosition(anchor) {
|
|
2664
|
+
if (this.el) {
|
|
2665
|
+
this.positionToolbar(anchor);
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
createToolbarElement() {
|
|
2669
|
+
const toolbar = document.createElement("div");
|
|
2670
|
+
toolbar.dataset["noteToolbar"] = "";
|
|
2671
|
+
Object.assign(toolbar.style, {
|
|
2672
|
+
position: "fixed",
|
|
2673
|
+
display: "flex",
|
|
2674
|
+
alignItems: "center",
|
|
2675
|
+
gap: "2px",
|
|
2676
|
+
padding: "2px 4px",
|
|
2677
|
+
background: "#fff",
|
|
2678
|
+
border: "1px solid #ccc",
|
|
2679
|
+
borderRadius: "4px",
|
|
2680
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
|
2681
|
+
zIndex: "10000",
|
|
2682
|
+
height: `${TOOLBAR_HEIGHT}px`,
|
|
2683
|
+
userSelect: "none"
|
|
2684
|
+
});
|
|
2685
|
+
for (const btn of FORMAT_BUTTONS) {
|
|
2686
|
+
toolbar.appendChild(this.createFormatButton(btn));
|
|
2687
|
+
}
|
|
2688
|
+
toolbar.appendChild(this.createFontSizeSelect());
|
|
2689
|
+
return toolbar;
|
|
2690
|
+
}
|
|
2691
|
+
createFormatButton(config) {
|
|
2692
|
+
const btn = document.createElement("button");
|
|
2693
|
+
btn.dataset["format"] = config.format;
|
|
2694
|
+
btn.textContent = config.label;
|
|
2695
|
+
Object.assign(btn.style, {
|
|
2696
|
+
border: "1px solid transparent",
|
|
2697
|
+
borderRadius: "3px",
|
|
2698
|
+
background: "none",
|
|
2699
|
+
cursor: "pointer",
|
|
2700
|
+
padding: "2px 6px",
|
|
2701
|
+
fontSize: "13px",
|
|
2702
|
+
fontWeight: config.format === "bold" ? "bold" : "normal",
|
|
2703
|
+
fontStyle: config.format === "italic" ? "italic" : "normal",
|
|
2704
|
+
textDecoration: config.format === "underline" ? "underline" : config.format === "strikethrough" ? "line-through" : "none",
|
|
2705
|
+
minWidth: "24px",
|
|
2706
|
+
height: "24px",
|
|
2707
|
+
lineHeight: "24px"
|
|
2708
|
+
});
|
|
2709
|
+
btn.addEventListener("pointerdown", (e) => {
|
|
2710
|
+
e.preventDefault();
|
|
2711
|
+
document.execCommand(config.command);
|
|
2712
|
+
this.updateActiveStates();
|
|
2713
|
+
});
|
|
2714
|
+
return btn;
|
|
2715
|
+
}
|
|
2716
|
+
createFontSizeSelect() {
|
|
2717
|
+
const select = document.createElement("select");
|
|
2718
|
+
Object.assign(select.style, {
|
|
2719
|
+
border: "1px solid #ccc",
|
|
2720
|
+
borderRadius: "3px",
|
|
2721
|
+
background: "#fff",
|
|
2722
|
+
cursor: "pointer",
|
|
2723
|
+
padding: "2px",
|
|
2724
|
+
fontSize: "12px",
|
|
2725
|
+
height: "24px",
|
|
2726
|
+
marginLeft: "4px"
|
|
2727
|
+
});
|
|
2728
|
+
for (const preset of this.fontSizePresets) {
|
|
2729
|
+
const option = document.createElement("option");
|
|
2730
|
+
option.value = String(preset.size);
|
|
2731
|
+
option.textContent = preset.label;
|
|
2732
|
+
select.appendChild(option);
|
|
2733
|
+
}
|
|
2734
|
+
select.value = String(DEFAULT_NOTE_FONT_SIZE);
|
|
2735
|
+
select.addEventListener("pointerdown", (e) => {
|
|
2736
|
+
e.stopPropagation();
|
|
2737
|
+
});
|
|
2738
|
+
select.addEventListener("change", () => {
|
|
2739
|
+
setFontSize(Number(select.value));
|
|
2740
|
+
this.updateActiveStates();
|
|
2741
|
+
this.anchor?.focus();
|
|
2742
|
+
});
|
|
2743
|
+
return select;
|
|
2744
|
+
}
|
|
2745
|
+
positionToolbar(anchor) {
|
|
2746
|
+
if (!this.el) return;
|
|
2747
|
+
const rect = anchor.getBoundingClientRect();
|
|
2748
|
+
const toolbarWidth = this.el.offsetWidth || 200;
|
|
2749
|
+
let top = rect.top - TOOLBAR_HEIGHT - TOOLBAR_GAP;
|
|
2750
|
+
if (top < 0) {
|
|
2751
|
+
top = rect.bottom + TOOLBAR_GAP;
|
|
2752
|
+
}
|
|
2753
|
+
let left = rect.left + (rect.width - toolbarWidth) / 2;
|
|
2754
|
+
left = Math.max(4, left);
|
|
2755
|
+
Object.assign(this.el.style, {
|
|
2756
|
+
top: `${top}px`,
|
|
2757
|
+
left: `${left}px`
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
updateActiveStates() {
|
|
2761
|
+
if (!this.el) return;
|
|
2762
|
+
const active = getActiveFormats();
|
|
2763
|
+
for (const config of FORMAT_BUTTONS) {
|
|
2764
|
+
const btn = this.el.querySelector(`[data-format="${config.format}"]`);
|
|
2765
|
+
if (!btn) continue;
|
|
2766
|
+
const isActive = active[config.format] ?? false;
|
|
2767
|
+
btn.style.background = isActive ? "#e0e0e0" : "none";
|
|
2768
|
+
btn.style.borderColor = isActive ? "#bbb" : "transparent";
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
};
|
|
2772
|
+
|
|
1867
2773
|
// src/elements/note-editor.ts
|
|
2774
|
+
var FORMAT_SHORTCUTS = {
|
|
2775
|
+
b: toggleBold,
|
|
2776
|
+
i: toggleItalic,
|
|
2777
|
+
u: toggleUnderline
|
|
2778
|
+
};
|
|
1868
2779
|
var NoteEditor = class {
|
|
1869
2780
|
editingId = null;
|
|
1870
2781
|
editingNode = null;
|
|
@@ -1873,6 +2784,10 @@ var NoteEditor = class {
|
|
|
1873
2784
|
pointerHandler = null;
|
|
1874
2785
|
pendingEditId = null;
|
|
1875
2786
|
onStopCallback = null;
|
|
2787
|
+
toolbar;
|
|
2788
|
+
constructor(options) {
|
|
2789
|
+
this.toolbar = options?.toolbar === false ? null : new NoteToolbar(options?.fontSizePresets);
|
|
2790
|
+
}
|
|
1876
2791
|
get isEditing() {
|
|
1877
2792
|
return this.editingId !== null;
|
|
1878
2793
|
}
|
|
@@ -1897,13 +2812,6 @@ var NoteEditor = class {
|
|
|
1897
2812
|
stopEditing(store) {
|
|
1898
2813
|
this.pendingEditId = null;
|
|
1899
2814
|
if (!this.editingId || !this.editingNode) return;
|
|
1900
|
-
const text = this.editingNode.textContent ?? "";
|
|
1901
|
-
store.update(this.editingId, { text });
|
|
1902
|
-
this.editingNode.contentEditable = "false";
|
|
1903
|
-
Object.assign(this.editingNode.style, {
|
|
1904
|
-
userSelect: "none",
|
|
1905
|
-
cursor: "default"
|
|
1906
|
-
});
|
|
1907
2815
|
if (this.blurHandler) {
|
|
1908
2816
|
this.editingNode.removeEventListener("blur", this.blurHandler);
|
|
1909
2817
|
}
|
|
@@ -1913,6 +2821,14 @@ var NoteEditor = class {
|
|
|
1913
2821
|
if (this.pointerHandler) {
|
|
1914
2822
|
this.editingNode.removeEventListener("pointerdown", this.pointerHandler);
|
|
1915
2823
|
}
|
|
2824
|
+
const text = sanitizeNoteHtml(this.editingNode.innerHTML);
|
|
2825
|
+
store.update(this.editingId, { text });
|
|
2826
|
+
this.editingNode.contentEditable = "false";
|
|
2827
|
+
Object.assign(this.editingNode.style, {
|
|
2828
|
+
userSelect: "none",
|
|
2829
|
+
cursor: "default"
|
|
2830
|
+
});
|
|
2831
|
+
this.toolbar?.hide();
|
|
1916
2832
|
if (this.editingId && this.onStopCallback) {
|
|
1917
2833
|
this.onStopCallback(this.editingId);
|
|
1918
2834
|
}
|
|
@@ -1928,6 +2844,11 @@ var NoteEditor = class {
|
|
|
1928
2844
|
this.stopEditing(store);
|
|
1929
2845
|
}
|
|
1930
2846
|
}
|
|
2847
|
+
updateToolbarPosition() {
|
|
2848
|
+
if (this.editingNode) {
|
|
2849
|
+
this.toolbar?.updatePosition(this.editingNode);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
1931
2852
|
activateEditing(node, elementId, store) {
|
|
1932
2853
|
this.editingId = elementId;
|
|
1933
2854
|
this.editingNode = node;
|
|
@@ -1946,8 +2867,21 @@ var NoteEditor = class {
|
|
|
1946
2867
|
selection.removeAllRanges();
|
|
1947
2868
|
selection.addRange(range);
|
|
1948
2869
|
}
|
|
1949
|
-
this.
|
|
2870
|
+
this.toolbar?.show(node);
|
|
2871
|
+
this.blurHandler = (e) => {
|
|
2872
|
+
const related = e.relatedTarget;
|
|
2873
|
+
if (related && this.toolbar?.getElement()?.contains(related)) return;
|
|
2874
|
+
this.stopEditing(store);
|
|
2875
|
+
};
|
|
1950
2876
|
this.keyHandler = (e) => {
|
|
2877
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && !e.altKey) {
|
|
2878
|
+
const action = FORMAT_SHORTCUTS[e.key.toLowerCase()];
|
|
2879
|
+
if (action) {
|
|
2880
|
+
e.preventDefault();
|
|
2881
|
+
action();
|
|
2882
|
+
return;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
1951
2885
|
if (e.key === "Escape") {
|
|
1952
2886
|
node.blur();
|
|
1953
2887
|
}
|
|
@@ -2199,131 +3133,86 @@ var HistoryRecorder = class {
|
|
|
2199
3133
|
}
|
|
2200
3134
|
};
|
|
2201
3135
|
|
|
2202
|
-
// src/
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
}
|
|
2282
|
-
function createShape(input) {
|
|
2283
|
-
return {
|
|
2284
|
-
id: createId("shape"),
|
|
2285
|
-
type: "shape",
|
|
2286
|
-
position: input.position,
|
|
2287
|
-
zIndex: input.zIndex ?? 0,
|
|
2288
|
-
locked: input.locked ?? false,
|
|
2289
|
-
layerId: input.layerId ?? "",
|
|
2290
|
-
shape: input.shape ?? "rectangle",
|
|
2291
|
-
size: input.size,
|
|
2292
|
-
strokeColor: input.strokeColor ?? "#000000",
|
|
2293
|
-
strokeWidth: input.strokeWidth ?? 2,
|
|
2294
|
-
fillColor: input.fillColor ?? "none"
|
|
2295
|
-
};
|
|
2296
|
-
}
|
|
2297
|
-
function createGrid(input) {
|
|
2298
|
-
return {
|
|
2299
|
-
id: createId("grid"),
|
|
2300
|
-
type: "grid",
|
|
2301
|
-
position: input.position ?? { x: 0, y: 0 },
|
|
2302
|
-
zIndex: input.zIndex ?? 0,
|
|
2303
|
-
locked: input.locked ?? false,
|
|
2304
|
-
layerId: input.layerId ?? "",
|
|
2305
|
-
gridType: input.gridType ?? "square",
|
|
2306
|
-
hexOrientation: input.hexOrientation ?? "pointy",
|
|
2307
|
-
cellSize: input.cellSize ?? 40,
|
|
2308
|
-
strokeColor: input.strokeColor ?? "#000000",
|
|
2309
|
-
strokeWidth: input.strokeWidth ?? 1,
|
|
2310
|
-
opacity: input.opacity ?? 1
|
|
2311
|
-
};
|
|
2312
|
-
}
|
|
2313
|
-
function createText(input) {
|
|
2314
|
-
return {
|
|
2315
|
-
id: createId("text"),
|
|
2316
|
-
type: "text",
|
|
2317
|
-
position: input.position,
|
|
2318
|
-
zIndex: input.zIndex ?? 0,
|
|
2319
|
-
locked: input.locked ?? false,
|
|
2320
|
-
layerId: input.layerId ?? "",
|
|
2321
|
-
size: input.size ?? { w: 200, h: 28 },
|
|
2322
|
-
text: input.text ?? "",
|
|
2323
|
-
fontSize: input.fontSize ?? 16,
|
|
2324
|
-
color: input.color ?? "#1a1a1a",
|
|
2325
|
-
textAlign: input.textAlign ?? "left"
|
|
2326
|
-
};
|
|
3136
|
+
// src/canvas/note-canvas-renderer.ts
|
|
3137
|
+
function renderNoteOnCanvas(ctx, note) {
|
|
3138
|
+
const { x, y } = note.position;
|
|
3139
|
+
const { w, h } = note.size;
|
|
3140
|
+
const r = 4;
|
|
3141
|
+
const pad = 8;
|
|
3142
|
+
const baseFontSize = note.fontSize ?? DEFAULT_NOTE_FONT_SIZE;
|
|
3143
|
+
ctx.save();
|
|
3144
|
+
ctx.fillStyle = note.backgroundColor;
|
|
3145
|
+
ctx.beginPath();
|
|
3146
|
+
ctx.moveTo(x + r, y);
|
|
3147
|
+
ctx.lineTo(x + w - r, y);
|
|
3148
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
3149
|
+
ctx.lineTo(x + w, y + h - r);
|
|
3150
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
3151
|
+
ctx.lineTo(x + r, y + h);
|
|
3152
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
3153
|
+
ctx.lineTo(x, y + r);
|
|
3154
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
3155
|
+
ctx.closePath();
|
|
3156
|
+
ctx.fill();
|
|
3157
|
+
if (note.text) {
|
|
3158
|
+
ctx.fillStyle = note.textColor;
|
|
3159
|
+
const runs = parseStyledRuns(note.text, baseFontSize);
|
|
3160
|
+
renderStyledRuns(ctx, runs, x + pad, y + pad, w - pad * 2);
|
|
3161
|
+
}
|
|
3162
|
+
ctx.restore();
|
|
3163
|
+
}
|
|
3164
|
+
function buildFontString(run) {
|
|
3165
|
+
const style = run.italic ? "italic" : "normal";
|
|
3166
|
+
const weight = run.bold ? "bold" : "normal";
|
|
3167
|
+
return `${style} ${weight} ${run.fontSize}px system-ui, sans-serif`;
|
|
3168
|
+
}
|
|
3169
|
+
function renderStyledRuns(ctx, runs, startX, startY, maxWidth) {
|
|
3170
|
+
ctx.textBaseline = "top";
|
|
3171
|
+
let cursorX = startX;
|
|
3172
|
+
let cursorY = startY;
|
|
3173
|
+
let lineHeight = 0;
|
|
3174
|
+
for (const run of runs) {
|
|
3175
|
+
ctx.font = buildFontString(run);
|
|
3176
|
+
const runLineHeight = run.fontSize * 1.3;
|
|
3177
|
+
lineHeight = Math.max(lineHeight, runLineHeight);
|
|
3178
|
+
const words = run.text.split(/(\n| )/);
|
|
3179
|
+
for (const word of words) {
|
|
3180
|
+
if (word === "\n") {
|
|
3181
|
+
cursorX = startX;
|
|
3182
|
+
cursorY += lineHeight;
|
|
3183
|
+
lineHeight = runLineHeight;
|
|
3184
|
+
continue;
|
|
3185
|
+
}
|
|
3186
|
+
if (word === " ") {
|
|
3187
|
+
const spaceWidth = ctx.measureText(" ").width;
|
|
3188
|
+
if (cursorX + spaceWidth > startX + maxWidth && cursorX > startX) {
|
|
3189
|
+
cursorX = startX;
|
|
3190
|
+
cursorY += lineHeight;
|
|
3191
|
+
lineHeight = runLineHeight;
|
|
3192
|
+
} else {
|
|
3193
|
+
cursorX += spaceWidth;
|
|
3194
|
+
}
|
|
3195
|
+
continue;
|
|
3196
|
+
}
|
|
3197
|
+
if (!word) continue;
|
|
3198
|
+
const metrics = ctx.measureText(word);
|
|
3199
|
+
if (cursorX + metrics.width > startX + maxWidth && cursorX > startX) {
|
|
3200
|
+
cursorX = startX;
|
|
3201
|
+
cursorY += lineHeight;
|
|
3202
|
+
lineHeight = runLineHeight;
|
|
3203
|
+
}
|
|
3204
|
+
ctx.fillText(word, cursorX, cursorY);
|
|
3205
|
+
if (run.underline) {
|
|
3206
|
+
const underY = cursorY + run.fontSize + 1;
|
|
3207
|
+
ctx.fillRect(cursorX, underY, metrics.width, 1);
|
|
3208
|
+
}
|
|
3209
|
+
if (run.strikethrough) {
|
|
3210
|
+
const strikeY = cursorY + run.fontSize * 0.55;
|
|
3211
|
+
ctx.fillRect(cursorX, strikeY, metrics.width, 1);
|
|
3212
|
+
}
|
|
3213
|
+
cursorX += metrics.width;
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
2327
3216
|
}
|
|
2328
3217
|
|
|
2329
3218
|
// src/canvas/export-image.ts
|
|
@@ -2361,6 +3250,11 @@ function getElementRect(el) {
|
|
|
2361
3250
|
}
|
|
2362
3251
|
case "grid":
|
|
2363
3252
|
return null;
|
|
3253
|
+
case "template": {
|
|
3254
|
+
const bounds = getElementBounds(el);
|
|
3255
|
+
if (!bounds) return null;
|
|
3256
|
+
return bounds;
|
|
3257
|
+
}
|
|
2364
3258
|
case "note":
|
|
2365
3259
|
case "image":
|
|
2366
3260
|
case "html":
|
|
@@ -2397,33 +3291,6 @@ function computeBounds(elements, padding) {
|
|
|
2397
3291
|
h: maxY - minY + padding * 2
|
|
2398
3292
|
};
|
|
2399
3293
|
}
|
|
2400
|
-
function renderNoteOnCanvas(ctx, note) {
|
|
2401
|
-
const { x, y } = note.position;
|
|
2402
|
-
const { w, h } = note.size;
|
|
2403
|
-
const r = 4;
|
|
2404
|
-
const pad = 8;
|
|
2405
|
-
ctx.save();
|
|
2406
|
-
ctx.fillStyle = note.backgroundColor;
|
|
2407
|
-
ctx.beginPath();
|
|
2408
|
-
ctx.moveTo(x + r, y);
|
|
2409
|
-
ctx.lineTo(x + w - r, y);
|
|
2410
|
-
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
2411
|
-
ctx.lineTo(x + w, y + h - r);
|
|
2412
|
-
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
2413
|
-
ctx.lineTo(x + r, y + h);
|
|
2414
|
-
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
2415
|
-
ctx.lineTo(x, y + r);
|
|
2416
|
-
ctx.arcTo(x, y, x + r, y, r);
|
|
2417
|
-
ctx.closePath();
|
|
2418
|
-
ctx.fill();
|
|
2419
|
-
if (note.text) {
|
|
2420
|
-
ctx.fillStyle = note.textColor;
|
|
2421
|
-
ctx.font = "14px system-ui, sans-serif";
|
|
2422
|
-
ctx.textBaseline = "top";
|
|
2423
|
-
wrapText(ctx, note.text, x + pad, y + pad, w - pad * 2, 18);
|
|
2424
|
-
}
|
|
2425
|
-
ctx.restore();
|
|
2426
|
-
}
|
|
2427
3294
|
function renderTextOnCanvas(ctx, text) {
|
|
2428
3295
|
if (!text.text) return;
|
|
2429
3296
|
ctx.save();
|
|
@@ -2448,25 +3315,6 @@ function renderTextOnCanvas(ctx, text) {
|
|
|
2448
3315
|
}
|
|
2449
3316
|
ctx.restore();
|
|
2450
3317
|
}
|
|
2451
|
-
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
|
|
2452
|
-
const words = text.split(" ");
|
|
2453
|
-
let line = "";
|
|
2454
|
-
let offsetY = 0;
|
|
2455
|
-
for (const word of words) {
|
|
2456
|
-
const testLine = line ? `${line} ${word}` : word;
|
|
2457
|
-
const metrics = ctx.measureText(testLine);
|
|
2458
|
-
if (metrics.width > maxWidth && line) {
|
|
2459
|
-
ctx.fillText(line, x, y + offsetY);
|
|
2460
|
-
line = word;
|
|
2461
|
-
offsetY += lineHeight;
|
|
2462
|
-
} else {
|
|
2463
|
-
line = testLine;
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
if (line) {
|
|
2467
|
-
ctx.fillText(line, x, y + offsetY);
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
3318
|
function renderGridForBounds(ctx, grid, bounds) {
|
|
2471
3319
|
const visibleBounds = {
|
|
2472
3320
|
minX: bounds.x,
|
|
@@ -2865,13 +3713,13 @@ var DomNodeManager = class {
|
|
|
2865
3713
|
padding: "8px",
|
|
2866
3714
|
borderRadius: "4px",
|
|
2867
3715
|
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
|
2868
|
-
fontSize:
|
|
3716
|
+
fontSize: `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`,
|
|
2869
3717
|
overflow: "hidden",
|
|
2870
3718
|
cursor: "default",
|
|
2871
3719
|
userSelect: "none",
|
|
2872
3720
|
wordWrap: "break-word"
|
|
2873
3721
|
});
|
|
2874
|
-
node.
|
|
3722
|
+
node.innerHTML = element.text || "";
|
|
2875
3723
|
node.addEventListener("dblclick", (e) => {
|
|
2876
3724
|
e.stopPropagation();
|
|
2877
3725
|
const id = node.dataset["elementId"];
|
|
@@ -2879,11 +3727,13 @@ var DomNodeManager = class {
|
|
|
2879
3727
|
});
|
|
2880
3728
|
}
|
|
2881
3729
|
if (!this.isEditingElement(element.id)) {
|
|
2882
|
-
|
|
2883
|
-
|
|
3730
|
+
const text = element.text || "";
|
|
3731
|
+
if (node.innerHTML !== text) {
|
|
3732
|
+
node.innerHTML = text;
|
|
2884
3733
|
}
|
|
2885
3734
|
node.style.backgroundColor = element.backgroundColor;
|
|
2886
3735
|
node.style.color = element.textColor;
|
|
3736
|
+
node.style.fontSize = `${element.fontSize ?? DEFAULT_NOTE_FONT_SIZE}px`;
|
|
2887
3737
|
}
|
|
2888
3738
|
}
|
|
2889
3739
|
if (element.type === "html" && !node.dataset["initialized"]) {
|
|
@@ -3105,7 +3955,15 @@ var RenderLoop = class {
|
|
|
3105
3955
|
ctx.save();
|
|
3106
3956
|
ctx.scale(dpr, dpr);
|
|
3107
3957
|
this.renderer.setCanvasSize(cssWidth, cssHeight);
|
|
3108
|
-
this.
|
|
3958
|
+
const hasGridElement = this.store.getElementsByType("grid").length > 0;
|
|
3959
|
+
if (hasGridElement) {
|
|
3960
|
+
ctx.save();
|
|
3961
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
3962
|
+
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
|
3963
|
+
ctx.restore();
|
|
3964
|
+
} else {
|
|
3965
|
+
this.background.render(ctx, this.camera);
|
|
3966
|
+
}
|
|
3109
3967
|
ctx.save();
|
|
3110
3968
|
ctx.translate(this.camera.position.x, this.camera.position.y);
|
|
3111
3969
|
ctx.scale(this.camera.zoom, this.camera.zoom);
|
|
@@ -3306,7 +4164,10 @@ var Viewport = class {
|
|
|
3306
4164
|
this.renderLoop.markAllLayersDirty();
|
|
3307
4165
|
this.requestRender();
|
|
3308
4166
|
});
|
|
3309
|
-
this.noteEditor = new NoteEditor(
|
|
4167
|
+
this.noteEditor = new NoteEditor({
|
|
4168
|
+
fontSizePresets: options.fontSizePresets,
|
|
4169
|
+
toolbar: options.toolbar
|
|
4170
|
+
});
|
|
3310
4171
|
this.noteEditor.setOnStop((id) => this.onTextEditStop(id));
|
|
3311
4172
|
this.history = new HistoryStack();
|
|
3312
4173
|
this.historyRecorder = new HistoryRecorder(this.store, this.history);
|
|
@@ -3362,20 +4223,24 @@ var Viewport = class {
|
|
|
3362
4223
|
});
|
|
3363
4224
|
this.unsubCamera = this.camera.onChange(() => {
|
|
3364
4225
|
this.applyCameraTransform();
|
|
4226
|
+
this.noteEditor.updateToolbarPosition();
|
|
3365
4227
|
this.requestRender();
|
|
3366
4228
|
});
|
|
3367
4229
|
this.unsubStore = [
|
|
3368
4230
|
this.store.on("add", (el) => {
|
|
4231
|
+
if (el.type === "grid") this.syncGridContext();
|
|
3369
4232
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
3370
4233
|
this.requestRender();
|
|
3371
4234
|
}),
|
|
3372
4235
|
this.store.on("remove", (el) => {
|
|
4236
|
+
if (el.type === "grid") this.syncGridContext();
|
|
3373
4237
|
this.unbindArrowsFrom(el);
|
|
3374
4238
|
this.domNodeManager.removeDomNode(el.id);
|
|
3375
4239
|
this.renderLoop.markLayerDirty(el.layerId);
|
|
3376
4240
|
this.requestRender();
|
|
3377
4241
|
}),
|
|
3378
4242
|
this.store.on("update", ({ previous, current }) => {
|
|
4243
|
+
if (current.type === "grid") this.syncGridContext();
|
|
3379
4244
|
this.renderLoop.markLayerDirty(current.layerId);
|
|
3380
4245
|
if (previous.layerId !== current.layerId) {
|
|
3381
4246
|
this.renderLoop.markLayerDirty(previous.layerId);
|
|
@@ -3385,6 +4250,7 @@ var Viewport = class {
|
|
|
3385
4250
|
this.store.on("clear", () => {
|
|
3386
4251
|
this.domNodeManager.clearDomNodes();
|
|
3387
4252
|
this.renderLoop.markAllLayersDirty();
|
|
4253
|
+
this.syncGridContext();
|
|
3388
4254
|
this.requestRender();
|
|
3389
4255
|
})
|
|
3390
4256
|
];
|
|
@@ -3398,6 +4264,7 @@ var Viewport = class {
|
|
|
3398
4264
|
this.observeResize();
|
|
3399
4265
|
this.syncCanvasSize();
|
|
3400
4266
|
this.renderLoop.start();
|
|
4267
|
+
this.syncGridContext();
|
|
3401
4268
|
}
|
|
3402
4269
|
camera;
|
|
3403
4270
|
store;
|
|
@@ -3714,6 +4581,18 @@ var Viewport = class {
|
|
|
3714
4581
|
this.renderLoop.setCanvasSize(rect.width * dpr, rect.height * dpr);
|
|
3715
4582
|
this.requestRender();
|
|
3716
4583
|
}
|
|
4584
|
+
syncGridContext() {
|
|
4585
|
+
const grid = this.store.getElementsByType("grid")[0];
|
|
4586
|
+
if (grid) {
|
|
4587
|
+
this.toolContext.gridSize = grid.cellSize;
|
|
4588
|
+
this.toolContext.gridType = grid.gridType;
|
|
4589
|
+
this.toolContext.hexOrientation = grid.hexOrientation;
|
|
4590
|
+
} else {
|
|
4591
|
+
this.toolContext.gridSize = this._gridSize;
|
|
4592
|
+
this.toolContext.gridType = void 0;
|
|
4593
|
+
this.toolContext.hexOrientation = void 0;
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
3717
4596
|
observeResize() {
|
|
3718
4597
|
if (typeof ResizeObserver === "undefined") return;
|
|
3719
4598
|
this.resizeObserver = new ResizeObserver(() => this.syncCanvasSize());
|
|
@@ -4088,7 +4967,7 @@ var SelectTool = class {
|
|
|
4088
4967
|
ctx.setCursor?.("default");
|
|
4089
4968
|
}
|
|
4090
4969
|
snap(point, ctx) {
|
|
4091
|
-
return
|
|
4970
|
+
return smartSnap(point, ctx);
|
|
4092
4971
|
}
|
|
4093
4972
|
onPointerDown(state, ctx) {
|
|
4094
4973
|
this.ctx = ctx;
|
|
@@ -4105,6 +4984,12 @@ var SelectTool = class {
|
|
|
4105
4984
|
ctx.requestRender();
|
|
4106
4985
|
return;
|
|
4107
4986
|
}
|
|
4987
|
+
const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
|
|
4988
|
+
if (templateResizeHit) {
|
|
4989
|
+
this.mode = { type: "resizing-template", elementId: templateResizeHit };
|
|
4990
|
+
ctx.requestRender();
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4108
4993
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
4109
4994
|
if (resizeHit) {
|
|
4110
4995
|
const el = ctx.store.getById(resizeHit.elementId);
|
|
@@ -4139,6 +5024,11 @@ var SelectTool = class {
|
|
|
4139
5024
|
applyArrowHandleDrag(this.mode.handle, this.mode.elementId, world, ctx);
|
|
4140
5025
|
return;
|
|
4141
5026
|
}
|
|
5027
|
+
if (this.mode.type === "resizing-template") {
|
|
5028
|
+
ctx.setCursor?.("nwse-resize");
|
|
5029
|
+
this.handleTemplateResize(world, ctx);
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
4142
5032
|
if (this.mode.type === "resizing") {
|
|
4143
5033
|
ctx.setCursor?.(HANDLE_CURSORS[this.mode.handle]);
|
|
4144
5034
|
this.handleResize(world, ctx);
|
|
@@ -4162,6 +5052,16 @@ var SelectTool = class {
|
|
|
4162
5052
|
from: { x: el.from.x + dx, y: el.from.y + dy },
|
|
4163
5053
|
to: { x: el.to.x + dx, y: el.to.y + dy }
|
|
4164
5054
|
});
|
|
5055
|
+
} else if (ctx.gridType && "size" in el) {
|
|
5056
|
+
const centerX = el.position.x + el.size.w / 2 + dx;
|
|
5057
|
+
const centerY = el.position.y + el.size.h / 2 + dy;
|
|
5058
|
+
const snappedCenter = this.snap({ x: centerX, y: centerY }, ctx);
|
|
5059
|
+
ctx.store.update(id, {
|
|
5060
|
+
position: {
|
|
5061
|
+
x: snappedCenter.x - el.size.w / 2,
|
|
5062
|
+
y: snappedCenter.y - el.size.h / 2
|
|
5063
|
+
}
|
|
5064
|
+
});
|
|
4165
5065
|
} else {
|
|
4166
5066
|
ctx.store.update(id, {
|
|
4167
5067
|
position: { x: el.position.x + dx, y: el.position.y + dy }
|
|
@@ -4236,6 +5136,11 @@ var SelectTool = class {
|
|
|
4236
5136
|
ctx.setCursor?.(getArrowHandleCursor(arrowHit.handle, false));
|
|
4237
5137
|
return;
|
|
4238
5138
|
}
|
|
5139
|
+
const templateResizeHit = this.hitTestTemplateResizeHandle(world, ctx);
|
|
5140
|
+
if (templateResizeHit) {
|
|
5141
|
+
ctx.setCursor?.("nwse-resize");
|
|
5142
|
+
return;
|
|
5143
|
+
}
|
|
4239
5144
|
const resizeHit = this.hitTestResizeHandle(world, ctx);
|
|
4240
5145
|
if (resizeHit) {
|
|
4241
5146
|
ctx.setCursor?.(HANDLE_CURSORS[resizeHit.handle]);
|
|
@@ -4372,6 +5277,24 @@ var SelectTool = class {
|
|
|
4372
5277
|
);
|
|
4373
5278
|
}
|
|
4374
5279
|
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
5280
|
+
} else if (el.type === "template") {
|
|
5281
|
+
canvasCtx.setLineDash([]);
|
|
5282
|
+
canvasCtx.fillStyle = "#ffffff";
|
|
5283
|
+
const hx = bounds.x + bounds.w;
|
|
5284
|
+
const hy = bounds.y + bounds.h;
|
|
5285
|
+
canvasCtx.fillRect(
|
|
5286
|
+
hx - handleWorldSize / 2,
|
|
5287
|
+
hy - handleWorldSize / 2,
|
|
5288
|
+
handleWorldSize,
|
|
5289
|
+
handleWorldSize
|
|
5290
|
+
);
|
|
5291
|
+
canvasCtx.strokeRect(
|
|
5292
|
+
hx - handleWorldSize / 2,
|
|
5293
|
+
hy - handleWorldSize / 2,
|
|
5294
|
+
handleWorldSize,
|
|
5295
|
+
handleWorldSize
|
|
5296
|
+
);
|
|
5297
|
+
canvasCtx.setLineDash([4 / zoom, 4 / zoom]);
|
|
4375
5298
|
}
|
|
4376
5299
|
}
|
|
4377
5300
|
canvasCtx.restore();
|
|
@@ -4396,6 +5319,43 @@ var SelectTool = class {
|
|
|
4396
5319
|
}
|
|
4397
5320
|
canvasCtx.restore();
|
|
4398
5321
|
}
|
|
5322
|
+
hitTestTemplateResizeHandle(world, ctx) {
|
|
5323
|
+
if (this._selectedIds.length === 0) return null;
|
|
5324
|
+
const zoom = ctx.camera.zoom;
|
|
5325
|
+
const handleHalf = (HANDLE_SIZE / 2 + HANDLE_HIT_PADDING2) / zoom;
|
|
5326
|
+
for (const id of this._selectedIds) {
|
|
5327
|
+
const el = ctx.store.getById(id);
|
|
5328
|
+
if (!el || el.type !== "template") continue;
|
|
5329
|
+
const bounds = getElementBounds(el);
|
|
5330
|
+
if (!bounds) continue;
|
|
5331
|
+
const hx = bounds.x + bounds.w;
|
|
5332
|
+
const hy = bounds.y + bounds.h;
|
|
5333
|
+
if (Math.abs(world.x - hx) <= handleHalf && Math.abs(world.y - hy) <= handleHalf) {
|
|
5334
|
+
return id;
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
return null;
|
|
5338
|
+
}
|
|
5339
|
+
handleTemplateResize(world, ctx) {
|
|
5340
|
+
if (this.mode.type !== "resizing-template") return;
|
|
5341
|
+
const el = ctx.store.getById(this.mode.elementId);
|
|
5342
|
+
if (!el || el.type !== "template" || el.locked) return;
|
|
5343
|
+
const dx = world.x - el.position.x;
|
|
5344
|
+
const dy = world.y - el.position.y;
|
|
5345
|
+
let newRadius = Math.sqrt(dx * dx + dy * dy);
|
|
5346
|
+
if (ctx.snapToGrid && ctx.gridSize && ctx.gridSize > 0) {
|
|
5347
|
+
const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
|
|
5348
|
+
newRadius = Math.max(snapUnit, Math.round(newRadius / snapUnit) * snapUnit);
|
|
5349
|
+
}
|
|
5350
|
+
newRadius = Math.max(MIN_ELEMENT_SIZE, newRadius);
|
|
5351
|
+
const updates = { radius: newRadius };
|
|
5352
|
+
if (el.feetPerCell != null && ctx.gridSize && ctx.gridSize > 0) {
|
|
5353
|
+
const snapUnit = ctx.gridType === "hex" ? Math.sqrt(3) * ctx.gridSize : ctx.gridSize;
|
|
5354
|
+
updates.radiusFeet = newRadius / snapUnit * el.feetPerCell;
|
|
5355
|
+
}
|
|
5356
|
+
ctx.store.update(this.mode.elementId, updates);
|
|
5357
|
+
ctx.requestRender();
|
|
5358
|
+
}
|
|
4399
5359
|
getMarqueeRect() {
|
|
4400
5360
|
if (this.mode.type !== "marquee") return null;
|
|
4401
5361
|
const { start } = this.mode;
|
|
@@ -4452,6 +5412,11 @@ var SelectTool = class {
|
|
|
4452
5412
|
if (el.type === "arrow") {
|
|
4453
5413
|
return isNearBezier(point, el.from, el.to, el.bend, 10);
|
|
4454
5414
|
}
|
|
5415
|
+
if (el.type === "template") {
|
|
5416
|
+
const bounds = getElementBounds(el);
|
|
5417
|
+
if (!bounds) return false;
|
|
5418
|
+
return point.x >= bounds.x && point.x <= bounds.x + bounds.w && point.y >= bounds.y && point.y <= bounds.y + bounds.h;
|
|
5419
|
+
}
|
|
4455
5420
|
return false;
|
|
4456
5421
|
}
|
|
4457
5422
|
};
|
|
@@ -4509,7 +5474,7 @@ var ArrowTool = class {
|
|
|
4509
5474
|
this.fromBinding = { elementId: target.id };
|
|
4510
5475
|
this.fromTarget = target;
|
|
4511
5476
|
} else {
|
|
4512
|
-
this.start =
|
|
5477
|
+
this.start = smartSnap(world, ctx);
|
|
4513
5478
|
this.fromBinding = void 0;
|
|
4514
5479
|
this.fromTarget = null;
|
|
4515
5480
|
}
|
|
@@ -4527,7 +5492,7 @@ var ArrowTool = class {
|
|
|
4527
5492
|
this.end = getElementCenter(target);
|
|
4528
5493
|
this.toTarget = target;
|
|
4529
5494
|
} else {
|
|
4530
|
-
this.end =
|
|
5495
|
+
this.end = smartSnap(world, ctx);
|
|
4531
5496
|
this.toTarget = null;
|
|
4532
5497
|
}
|
|
4533
5498
|
ctx.requestRender();
|
|
@@ -4608,17 +5573,20 @@ var NoteTool = class {
|
|
|
4608
5573
|
backgroundColor;
|
|
4609
5574
|
textColor;
|
|
4610
5575
|
size;
|
|
5576
|
+
fontSize;
|
|
4611
5577
|
optionListeners = /* @__PURE__ */ new Set();
|
|
4612
5578
|
constructor(options = {}) {
|
|
4613
5579
|
this.backgroundColor = options.backgroundColor ?? "#ffeb3b";
|
|
4614
5580
|
this.textColor = options.textColor ?? "#000000";
|
|
4615
5581
|
this.size = options.size ?? { w: 200, h: 100 };
|
|
5582
|
+
this.fontSize = options.fontSize ?? DEFAULT_NOTE_FONT_SIZE;
|
|
4616
5583
|
}
|
|
4617
5584
|
getOptions() {
|
|
4618
5585
|
return {
|
|
4619
5586
|
backgroundColor: this.backgroundColor,
|
|
4620
5587
|
textColor: this.textColor,
|
|
4621
|
-
size: { ...this.size }
|
|
5588
|
+
size: { ...this.size },
|
|
5589
|
+
fontSize: this.fontSize
|
|
4622
5590
|
};
|
|
4623
5591
|
}
|
|
4624
5592
|
onOptionsChange(listener) {
|
|
@@ -4629,6 +5597,7 @@ var NoteTool = class {
|
|
|
4629
5597
|
if (options.backgroundColor !== void 0) this.backgroundColor = options.backgroundColor;
|
|
4630
5598
|
if (options.textColor !== void 0) this.textColor = options.textColor;
|
|
4631
5599
|
if (options.size !== void 0) this.size = options.size;
|
|
5600
|
+
if (options.fontSize !== void 0) this.fontSize = options.fontSize;
|
|
4632
5601
|
this.notifyOptionsChange();
|
|
4633
5602
|
}
|
|
4634
5603
|
notifyOptionsChange() {
|
|
@@ -4640,14 +5609,13 @@ var NoteTool = class {
|
|
|
4640
5609
|
}
|
|
4641
5610
|
onPointerUp(state, ctx) {
|
|
4642
5611
|
let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
4643
|
-
|
|
4644
|
-
world = snapPoint(world, ctx.gridSize);
|
|
4645
|
-
}
|
|
5612
|
+
world = smartSnap(world, ctx);
|
|
4646
5613
|
const note = createNote({
|
|
4647
5614
|
position: world,
|
|
4648
5615
|
size: { ...this.size },
|
|
4649
5616
|
backgroundColor: this.backgroundColor,
|
|
4650
5617
|
textColor: this.textColor,
|
|
5618
|
+
fontSize: this.fontSize,
|
|
4651
5619
|
layerId: ctx.activeLayerId ?? ""
|
|
4652
5620
|
});
|
|
4653
5621
|
ctx.store.add(note);
|
|
@@ -4697,9 +5665,7 @@ var TextTool = class {
|
|
|
4697
5665
|
}
|
|
4698
5666
|
onPointerUp(state, ctx) {
|
|
4699
5667
|
let world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
4700
|
-
|
|
4701
|
-
world = snapPoint(world, ctx.gridSize);
|
|
4702
|
-
}
|
|
5668
|
+
world = smartSnap(world, ctx);
|
|
4703
5669
|
const textEl = createText({
|
|
4704
5670
|
position: world,
|
|
4705
5671
|
fontSize: this.fontSize,
|
|
@@ -4732,8 +5698,12 @@ var ImageTool = class {
|
|
|
4732
5698
|
onPointerUp(state, ctx) {
|
|
4733
5699
|
if (!this.src) return;
|
|
4734
5700
|
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5701
|
+
const snapped = smartSnap(world, ctx);
|
|
4735
5702
|
const image = createImage({
|
|
4736
|
-
position:
|
|
5703
|
+
position: {
|
|
5704
|
+
x: snapped.x - this.size.w / 2,
|
|
5705
|
+
y: snapped.y - this.size.h / 2
|
|
5706
|
+
},
|
|
4737
5707
|
size: { ...this.size },
|
|
4738
5708
|
src: this.src
|
|
4739
5709
|
});
|
|
@@ -4870,7 +5840,7 @@ var ShapeTool = class {
|
|
|
4870
5840
|
for (const listener of this.optionListeners) listener();
|
|
4871
5841
|
}
|
|
4872
5842
|
snap(point, ctx) {
|
|
4873
|
-
return
|
|
5843
|
+
return smartSnap(point, ctx);
|
|
4874
5844
|
}
|
|
4875
5845
|
onKeyDown = (e) => {
|
|
4876
5846
|
if (e.key === "Shift") this.shiftHeld = true;
|
|
@@ -4880,6 +5850,398 @@ var ShapeTool = class {
|
|
|
4880
5850
|
};
|
|
4881
5851
|
};
|
|
4882
5852
|
|
|
5853
|
+
// src/tools/measure-tool.ts
|
|
5854
|
+
var MeasureTool = class {
|
|
5855
|
+
name = "measure";
|
|
5856
|
+
start = null;
|
|
5857
|
+
end = null;
|
|
5858
|
+
gridSize = 1;
|
|
5859
|
+
gridType;
|
|
5860
|
+
hexOrientation;
|
|
5861
|
+
feetPerCell;
|
|
5862
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
5863
|
+
constructor(options = {}) {
|
|
5864
|
+
this.feetPerCell = options.feetPerCell ?? 5;
|
|
5865
|
+
}
|
|
5866
|
+
getOptions() {
|
|
5867
|
+
return { feetPerCell: this.feetPerCell };
|
|
5868
|
+
}
|
|
5869
|
+
setOptions(options) {
|
|
5870
|
+
if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
|
|
5871
|
+
this.notifyOptionsChange();
|
|
5872
|
+
}
|
|
5873
|
+
onOptionsChange(listener) {
|
|
5874
|
+
this.optionListeners.add(listener);
|
|
5875
|
+
return () => this.optionListeners.delete(listener);
|
|
5876
|
+
}
|
|
5877
|
+
onPointerDown(state, ctx) {
|
|
5878
|
+
this.gridSize = ctx.gridSize ?? 1;
|
|
5879
|
+
this.gridType = ctx.gridType;
|
|
5880
|
+
this.hexOrientation = ctx.hexOrientation;
|
|
5881
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5882
|
+
this.start = this.snapToGrid(world, ctx);
|
|
5883
|
+
this.end = { ...this.start };
|
|
5884
|
+
}
|
|
5885
|
+
onPointerMove(state, ctx) {
|
|
5886
|
+
if (!this.start) return;
|
|
5887
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
5888
|
+
this.end = this.snapToGrid(world, ctx);
|
|
5889
|
+
ctx.requestRender();
|
|
5890
|
+
}
|
|
5891
|
+
onPointerUp(_state, ctx) {
|
|
5892
|
+
if (!this.start) return;
|
|
5893
|
+
this.start = null;
|
|
5894
|
+
this.end = null;
|
|
5895
|
+
ctx.requestRender();
|
|
5896
|
+
}
|
|
5897
|
+
onDeactivate(_ctx) {
|
|
5898
|
+
this.start = null;
|
|
5899
|
+
this.end = null;
|
|
5900
|
+
}
|
|
5901
|
+
getMeasurement() {
|
|
5902
|
+
if (!this.start || !this.end) return null;
|
|
5903
|
+
const dx = this.end.x - this.start.x;
|
|
5904
|
+
const dy = this.end.y - this.start.y;
|
|
5905
|
+
const worldDistance = Math.sqrt(dx * dx + dy * dy);
|
|
5906
|
+
let cells;
|
|
5907
|
+
if (this.gridType === "hex" && this.hexOrientation) {
|
|
5908
|
+
cells = getHexDistance(this.start, this.end, this.gridSize, this.hexOrientation);
|
|
5909
|
+
} else {
|
|
5910
|
+
const snapUnit = this.gridSize;
|
|
5911
|
+
cells = worldDistance / snapUnit;
|
|
5912
|
+
}
|
|
5913
|
+
const feet = cells * this.feetPerCell;
|
|
5914
|
+
return {
|
|
5915
|
+
start: { ...this.start },
|
|
5916
|
+
end: { ...this.end },
|
|
5917
|
+
worldDistance,
|
|
5918
|
+
cells,
|
|
5919
|
+
feet
|
|
5920
|
+
};
|
|
5921
|
+
}
|
|
5922
|
+
renderOverlay(ctx) {
|
|
5923
|
+
const m = this.getMeasurement();
|
|
5924
|
+
if (!m) return;
|
|
5925
|
+
ctx.save();
|
|
5926
|
+
ctx.strokeStyle = "#FF5722";
|
|
5927
|
+
ctx.setLineDash([8, 4]);
|
|
5928
|
+
ctx.lineWidth = 2;
|
|
5929
|
+
ctx.beginPath();
|
|
5930
|
+
ctx.moveTo(m.start.x, m.start.y);
|
|
5931
|
+
ctx.lineTo(m.end.x, m.end.y);
|
|
5932
|
+
ctx.stroke();
|
|
5933
|
+
ctx.setLineDash([]);
|
|
5934
|
+
ctx.fillStyle = "#FF5722";
|
|
5935
|
+
const dotRadius = 4;
|
|
5936
|
+
ctx.beginPath();
|
|
5937
|
+
ctx.arc(m.start.x, m.start.y, dotRadius, 0, Math.PI * 2);
|
|
5938
|
+
ctx.fill();
|
|
5939
|
+
ctx.beginPath();
|
|
5940
|
+
ctx.arc(m.end.x, m.end.y, dotRadius, 0, Math.PI * 2);
|
|
5941
|
+
ctx.fill();
|
|
5942
|
+
const label = `${Math.round(m.feet)} ft`;
|
|
5943
|
+
const midX = (m.start.x + m.end.x) / 2;
|
|
5944
|
+
const midY = (m.start.y + m.end.y) / 2;
|
|
5945
|
+
ctx.font = "14px sans-serif";
|
|
5946
|
+
const metrics = ctx.measureText(label);
|
|
5947
|
+
const padX = 6;
|
|
5948
|
+
const padY = 4;
|
|
5949
|
+
const textH = 14;
|
|
5950
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.75)";
|
|
5951
|
+
ctx.beginPath();
|
|
5952
|
+
ctx.roundRect(
|
|
5953
|
+
midX - metrics.width / 2 - padX,
|
|
5954
|
+
midY - textH / 2 - padY,
|
|
5955
|
+
metrics.width + padX * 2,
|
|
5956
|
+
textH + padY * 2,
|
|
5957
|
+
4
|
|
5958
|
+
);
|
|
5959
|
+
ctx.fill();
|
|
5960
|
+
ctx.fillStyle = "#FFFFFF";
|
|
5961
|
+
ctx.textAlign = "center";
|
|
5962
|
+
ctx.textBaseline = "middle";
|
|
5963
|
+
ctx.fillText(label, midX, midY);
|
|
5964
|
+
ctx.restore();
|
|
5965
|
+
}
|
|
5966
|
+
snapToGrid(point, ctx) {
|
|
5967
|
+
if (!ctx.gridSize) return point;
|
|
5968
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
5969
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
5970
|
+
}
|
|
5971
|
+
if (ctx.gridType === "square") {
|
|
5972
|
+
return snapPoint(point, ctx.gridSize);
|
|
5973
|
+
}
|
|
5974
|
+
if (ctx.snapToGrid) {
|
|
5975
|
+
return snapPoint(point, ctx.gridSize);
|
|
5976
|
+
}
|
|
5977
|
+
return point;
|
|
5978
|
+
}
|
|
5979
|
+
notifyOptionsChange() {
|
|
5980
|
+
for (const listener of this.optionListeners) listener();
|
|
5981
|
+
}
|
|
5982
|
+
};
|
|
5983
|
+
|
|
5984
|
+
// src/tools/template-tool.ts
|
|
5985
|
+
var TemplateTool = class {
|
|
5986
|
+
name = "template";
|
|
5987
|
+
drawing = false;
|
|
5988
|
+
origin = { x: 0, y: 0 };
|
|
5989
|
+
current = { x: 0, y: 0 };
|
|
5990
|
+
gridSize = 1;
|
|
5991
|
+
gridType;
|
|
5992
|
+
hexOrientation;
|
|
5993
|
+
snapEnabled = false;
|
|
5994
|
+
templateShape;
|
|
5995
|
+
fillColor;
|
|
5996
|
+
strokeColor;
|
|
5997
|
+
strokeWidth;
|
|
5998
|
+
opacity;
|
|
5999
|
+
feetPerCell;
|
|
6000
|
+
optionListeners = /* @__PURE__ */ new Set();
|
|
6001
|
+
constructor(options = {}) {
|
|
6002
|
+
this.templateShape = options.templateShape ?? "circle";
|
|
6003
|
+
this.fillColor = options.fillColor ?? "rgba(255, 87, 34, 0.2)";
|
|
6004
|
+
this.strokeColor = options.strokeColor ?? "#FF5722";
|
|
6005
|
+
this.strokeWidth = options.strokeWidth ?? 2;
|
|
6006
|
+
this.opacity = options.opacity ?? 0.6;
|
|
6007
|
+
this.feetPerCell = options.feetPerCell ?? 5;
|
|
6008
|
+
}
|
|
6009
|
+
getOptions() {
|
|
6010
|
+
return {
|
|
6011
|
+
templateShape: this.templateShape,
|
|
6012
|
+
fillColor: this.fillColor,
|
|
6013
|
+
strokeColor: this.strokeColor,
|
|
6014
|
+
strokeWidth: this.strokeWidth,
|
|
6015
|
+
opacity: this.opacity,
|
|
6016
|
+
feetPerCell: this.feetPerCell
|
|
6017
|
+
};
|
|
6018
|
+
}
|
|
6019
|
+
setOptions(options) {
|
|
6020
|
+
if (options.templateShape !== void 0) this.templateShape = options.templateShape;
|
|
6021
|
+
if (options.fillColor !== void 0) this.fillColor = options.fillColor;
|
|
6022
|
+
if (options.strokeColor !== void 0) this.strokeColor = options.strokeColor;
|
|
6023
|
+
if (options.strokeWidth !== void 0) this.strokeWidth = options.strokeWidth;
|
|
6024
|
+
if (options.opacity !== void 0) this.opacity = options.opacity;
|
|
6025
|
+
if (options.feetPerCell !== void 0) this.feetPerCell = options.feetPerCell;
|
|
6026
|
+
this.notifyOptionsChange();
|
|
6027
|
+
}
|
|
6028
|
+
onOptionsChange(listener) {
|
|
6029
|
+
this.optionListeners.add(listener);
|
|
6030
|
+
return () => this.optionListeners.delete(listener);
|
|
6031
|
+
}
|
|
6032
|
+
onPointerDown(state, ctx) {
|
|
6033
|
+
this.drawing = true;
|
|
6034
|
+
this.gridSize = ctx.gridSize ?? 1;
|
|
6035
|
+
this.gridType = ctx.gridType;
|
|
6036
|
+
this.hexOrientation = ctx.hexOrientation;
|
|
6037
|
+
this.snapEnabled = !!ctx.gridType || (ctx.snapToGrid ?? false);
|
|
6038
|
+
const world = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
6039
|
+
this.origin = this.snapToGrid(world, ctx);
|
|
6040
|
+
this.current = { ...this.origin };
|
|
6041
|
+
}
|
|
6042
|
+
onPointerMove(state, ctx) {
|
|
6043
|
+
if (!this.drawing) return;
|
|
6044
|
+
this.current = ctx.camera.screenToWorld({ x: state.x, y: state.y });
|
|
6045
|
+
ctx.requestRender();
|
|
6046
|
+
}
|
|
6047
|
+
onPointerUp(_state, ctx) {
|
|
6048
|
+
if (!this.drawing) return;
|
|
6049
|
+
this.drawing = false;
|
|
6050
|
+
const radius = this.computeRadius();
|
|
6051
|
+
if (radius <= 0) return;
|
|
6052
|
+
const angle = this.computeAngle();
|
|
6053
|
+
const gridSize = ctx.gridSize;
|
|
6054
|
+
const snapUnit = gridSize && gridSize > 0 ? ctx.gridType === "hex" ? Math.sqrt(3) * gridSize : gridSize : 0;
|
|
6055
|
+
const cells = snapUnit > 0 ? radius / snapUnit : 0;
|
|
6056
|
+
const radiusFeet = cells * this.feetPerCell;
|
|
6057
|
+
const element = createTemplate({
|
|
6058
|
+
position: { ...this.origin },
|
|
6059
|
+
templateShape: this.templateShape,
|
|
6060
|
+
radius,
|
|
6061
|
+
angle,
|
|
6062
|
+
fillColor: this.fillColor,
|
|
6063
|
+
strokeColor: this.strokeColor,
|
|
6064
|
+
strokeWidth: this.strokeWidth,
|
|
6065
|
+
opacity: this.opacity,
|
|
6066
|
+
feetPerCell: this.feetPerCell,
|
|
6067
|
+
radiusFeet: radiusFeet > 0 ? radiusFeet : void 0,
|
|
6068
|
+
layerId: ctx.activeLayerId ?? ""
|
|
6069
|
+
});
|
|
6070
|
+
ctx.store.add(element);
|
|
6071
|
+
ctx.requestRender();
|
|
6072
|
+
ctx.switchTool?.("select");
|
|
6073
|
+
}
|
|
6074
|
+
onDeactivate(_ctx) {
|
|
6075
|
+
this.drawing = false;
|
|
6076
|
+
this.origin = { x: 0, y: 0 };
|
|
6077
|
+
this.current = { x: 0, y: 0 };
|
|
6078
|
+
}
|
|
6079
|
+
renderOverlay(ctx) {
|
|
6080
|
+
if (!this.drawing) return;
|
|
6081
|
+
const radius = this.computeRadius();
|
|
6082
|
+
if (radius <= 0) return;
|
|
6083
|
+
if (this.gridType === "hex" && this.hexOrientation) {
|
|
6084
|
+
this.renderHexOverlay(ctx, radius);
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
6087
|
+
this.renderGeometricOverlay(ctx, radius);
|
|
6088
|
+
}
|
|
6089
|
+
renderGeometricOverlay(ctx, radius) {
|
|
6090
|
+
const cx = this.origin.x;
|
|
6091
|
+
const cy = this.origin.y;
|
|
6092
|
+
const angle = this.computeAngle();
|
|
6093
|
+
ctx.save();
|
|
6094
|
+
ctx.globalAlpha = 0.4;
|
|
6095
|
+
ctx.fillStyle = this.fillColor;
|
|
6096
|
+
ctx.strokeStyle = this.strokeColor;
|
|
6097
|
+
ctx.lineWidth = this.strokeWidth;
|
|
6098
|
+
switch (this.templateShape) {
|
|
6099
|
+
case "circle":
|
|
6100
|
+
ctx.beginPath();
|
|
6101
|
+
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
|
6102
|
+
ctx.fill();
|
|
6103
|
+
ctx.stroke();
|
|
6104
|
+
break;
|
|
6105
|
+
case "square":
|
|
6106
|
+
ctx.fillRect(cx - radius / 2, cy - radius / 2, radius, radius);
|
|
6107
|
+
ctx.strokeRect(cx - radius / 2, cy - radius / 2, radius, radius);
|
|
6108
|
+
break;
|
|
6109
|
+
case "cone": {
|
|
6110
|
+
const halfAngle = Math.atan(0.5);
|
|
6111
|
+
ctx.beginPath();
|
|
6112
|
+
ctx.moveTo(cx, cy);
|
|
6113
|
+
ctx.arc(cx, cy, radius, angle - halfAngle, angle + halfAngle);
|
|
6114
|
+
ctx.closePath();
|
|
6115
|
+
ctx.fill();
|
|
6116
|
+
ctx.stroke();
|
|
6117
|
+
break;
|
|
6118
|
+
}
|
|
6119
|
+
case "line": {
|
|
6120
|
+
const halfW = radius / 12;
|
|
6121
|
+
const cos = Math.cos(angle);
|
|
6122
|
+
const sin = Math.sin(angle);
|
|
6123
|
+
const perpX = -sin * halfW;
|
|
6124
|
+
const perpY = cos * halfW;
|
|
6125
|
+
ctx.beginPath();
|
|
6126
|
+
ctx.moveTo(cx + perpX, cy + perpY);
|
|
6127
|
+
ctx.lineTo(cx + radius * cos + perpX, cy + radius * sin + perpY);
|
|
6128
|
+
ctx.lineTo(cx + radius * cos - perpX, cy + radius * sin - perpY);
|
|
6129
|
+
ctx.lineTo(cx - perpX, cy - perpY);
|
|
6130
|
+
ctx.closePath();
|
|
6131
|
+
ctx.fill();
|
|
6132
|
+
ctx.stroke();
|
|
6133
|
+
break;
|
|
6134
|
+
}
|
|
6135
|
+
}
|
|
6136
|
+
ctx.restore();
|
|
6137
|
+
}
|
|
6138
|
+
renderHexOverlay(ctx, radius) {
|
|
6139
|
+
const orientation = this.hexOrientation;
|
|
6140
|
+
if (!orientation) return;
|
|
6141
|
+
const cellSize = this.gridSize;
|
|
6142
|
+
const snapUnit = Math.sqrt(3) * cellSize;
|
|
6143
|
+
const radiusCells = radius / snapUnit;
|
|
6144
|
+
const angle = this.computeAngle();
|
|
6145
|
+
const center = this.origin;
|
|
6146
|
+
let hexCells;
|
|
6147
|
+
switch (this.templateShape) {
|
|
6148
|
+
case "circle":
|
|
6149
|
+
hexCells = getHexCellsInRadius(center, radiusCells, cellSize, orientation);
|
|
6150
|
+
break;
|
|
6151
|
+
case "cone":
|
|
6152
|
+
hexCells = getHexCellsInCone(center, angle, radiusCells, cellSize, orientation);
|
|
6153
|
+
break;
|
|
6154
|
+
case "line":
|
|
6155
|
+
hexCells = getHexCellsInLine(center, angle, radiusCells, cellSize, orientation);
|
|
6156
|
+
break;
|
|
6157
|
+
case "square":
|
|
6158
|
+
hexCells = getHexCellsInSquare(center, radiusCells, cellSize, orientation);
|
|
6159
|
+
break;
|
|
6160
|
+
}
|
|
6161
|
+
ctx.save();
|
|
6162
|
+
ctx.globalAlpha = 0.4;
|
|
6163
|
+
ctx.beginPath();
|
|
6164
|
+
for (const cell of hexCells) {
|
|
6165
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
6166
|
+
}
|
|
6167
|
+
ctx.fillStyle = this.fillColor;
|
|
6168
|
+
ctx.fill();
|
|
6169
|
+
ctx.beginPath();
|
|
6170
|
+
for (const cell of hexCells) {
|
|
6171
|
+
drawHexPath(ctx, cell.x, cell.y, cellSize, orientation);
|
|
6172
|
+
}
|
|
6173
|
+
ctx.strokeStyle = this.strokeColor;
|
|
6174
|
+
ctx.lineWidth = this.strokeWidth;
|
|
6175
|
+
ctx.stroke();
|
|
6176
|
+
if (this.templateShape === "cone" || this.templateShape === "line" || this.templateShape === "circle" || this.templateShape === "square") {
|
|
6177
|
+
ctx.globalAlpha = 0.5;
|
|
6178
|
+
ctx.beginPath();
|
|
6179
|
+
drawHexPath(ctx, center.x, center.y, cellSize, orientation);
|
|
6180
|
+
ctx.fillStyle = this.strokeColor;
|
|
6181
|
+
ctx.fill();
|
|
6182
|
+
ctx.strokeStyle = this.strokeColor;
|
|
6183
|
+
ctx.lineWidth = this.strokeWidth;
|
|
6184
|
+
ctx.stroke();
|
|
6185
|
+
}
|
|
6186
|
+
if (this.templateShape === "circle") {
|
|
6187
|
+
const feet = radiusCells * this.feetPerCell;
|
|
6188
|
+
if (feet > 0) {
|
|
6189
|
+
ctx.globalAlpha = 1;
|
|
6190
|
+
const label = `${Math.round(feet)} ft`;
|
|
6191
|
+
const fontSize = Math.max(10, Math.min(14, radius * 0.15));
|
|
6192
|
+
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
|
|
6193
|
+
ctx.textAlign = "center";
|
|
6194
|
+
ctx.textBaseline = "bottom";
|
|
6195
|
+
const textX = center.x;
|
|
6196
|
+
const textY = center.y - 4;
|
|
6197
|
+
const metrics = ctx.measureText(label);
|
|
6198
|
+
const padX = 4;
|
|
6199
|
+
const padY = 2;
|
|
6200
|
+
const textW = metrics.width + padX * 2;
|
|
6201
|
+
const textH = fontSize + padY * 2;
|
|
6202
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
|
|
6203
|
+
ctx.beginPath();
|
|
6204
|
+
ctx.roundRect(textX - textW / 2, textY - textH, textW, textH, 3);
|
|
6205
|
+
ctx.fill();
|
|
6206
|
+
ctx.fillStyle = this.strokeColor;
|
|
6207
|
+
ctx.fillText(label, textX, textY - padY);
|
|
6208
|
+
}
|
|
6209
|
+
}
|
|
6210
|
+
ctx.restore();
|
|
6211
|
+
}
|
|
6212
|
+
computeRadius() {
|
|
6213
|
+
const dx = this.current.x - this.origin.x;
|
|
6214
|
+
const dy = this.current.y - this.origin.y;
|
|
6215
|
+
const raw = Math.sqrt(dx * dx + dy * dy);
|
|
6216
|
+
if (this.snapEnabled && this.gridSize > 0) {
|
|
6217
|
+
const snapUnit = this.gridType === "hex" ? Math.sqrt(3) * this.gridSize : this.gridSize;
|
|
6218
|
+
return Math.max(snapUnit, Math.round(raw / snapUnit) * snapUnit);
|
|
6219
|
+
}
|
|
6220
|
+
return raw;
|
|
6221
|
+
}
|
|
6222
|
+
computeAngle() {
|
|
6223
|
+
const dx = this.current.x - this.origin.x;
|
|
6224
|
+
const dy = this.current.y - this.origin.y;
|
|
6225
|
+
return Math.atan2(dy, dx);
|
|
6226
|
+
}
|
|
6227
|
+
snapToGrid(point, ctx) {
|
|
6228
|
+
if (!ctx.gridSize) return point;
|
|
6229
|
+
if (ctx.gridType === "hex" && ctx.hexOrientation) {
|
|
6230
|
+
return snapToHexCenter(point, ctx.gridSize, ctx.hexOrientation);
|
|
6231
|
+
}
|
|
6232
|
+
if (ctx.gridType === "square") {
|
|
6233
|
+
return snapPoint(point, ctx.gridSize);
|
|
6234
|
+
}
|
|
6235
|
+
if (ctx.snapToGrid) {
|
|
6236
|
+
return snapPoint(point, ctx.gridSize);
|
|
6237
|
+
}
|
|
6238
|
+
return point;
|
|
6239
|
+
}
|
|
6240
|
+
notifyOptionsChange() {
|
|
6241
|
+
for (const listener of this.optionListeners) listener();
|
|
6242
|
+
}
|
|
6243
|
+
};
|
|
6244
|
+
|
|
4883
6245
|
// src/history/layer-commands.ts
|
|
4884
6246
|
var CreateLayerCommand = class {
|
|
4885
6247
|
constructor(manager, layer) {
|
|
@@ -4921,7 +6283,7 @@ var UpdateLayerCommand = class {
|
|
|
4921
6283
|
};
|
|
4922
6284
|
|
|
4923
6285
|
// src/index.ts
|
|
4924
|
-
var VERSION = "0.
|
|
6286
|
+
var VERSION = "0.10.0";
|
|
4925
6287
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4926
6288
|
0 && (module.exports = {
|
|
4927
6289
|
AddElementCommand,
|
|
@@ -4931,6 +6293,8 @@ var VERSION = "0.8.11";
|
|
|
4931
6293
|
BatchCommand,
|
|
4932
6294
|
Camera,
|
|
4933
6295
|
CreateLayerCommand,
|
|
6296
|
+
DEFAULT_FONT_SIZE_PRESETS,
|
|
6297
|
+
DEFAULT_NOTE_FONT_SIZE,
|
|
4934
6298
|
ElementRenderer,
|
|
4935
6299
|
ElementStore,
|
|
4936
6300
|
EraserTool,
|
|
@@ -4941,14 +6305,17 @@ var VERSION = "0.8.11";
|
|
|
4941
6305
|
ImageTool,
|
|
4942
6306
|
InputHandler,
|
|
4943
6307
|
LayerManager,
|
|
6308
|
+
MeasureTool,
|
|
4944
6309
|
NoteEditor,
|
|
4945
6310
|
NoteTool,
|
|
6311
|
+
NoteToolbar,
|
|
4946
6312
|
PencilTool,
|
|
4947
6313
|
Quadtree,
|
|
4948
6314
|
RemoveElementCommand,
|
|
4949
6315
|
RemoveLayerCommand,
|
|
4950
6316
|
SelectTool,
|
|
4951
6317
|
ShapeTool,
|
|
6318
|
+
TemplateTool,
|
|
4952
6319
|
TextTool,
|
|
4953
6320
|
ToolManager,
|
|
4954
6321
|
UpdateElementCommand,
|
|
@@ -4965,11 +6332,14 @@ var VERSION = "0.8.11";
|
|
|
4965
6332
|
createNote,
|
|
4966
6333
|
createShape,
|
|
4967
6334
|
createStroke,
|
|
6335
|
+
createTemplate,
|
|
4968
6336
|
createText,
|
|
6337
|
+
drawHexPath,
|
|
4969
6338
|
exportImage,
|
|
4970
6339
|
exportState,
|
|
4971
6340
|
findBindTarget,
|
|
4972
6341
|
findBoundArrows,
|
|
6342
|
+
getActiveFormats,
|
|
4973
6343
|
getArrowBounds,
|
|
4974
6344
|
getArrowControlPoint,
|
|
4975
6345
|
getArrowMidpoint,
|
|
@@ -4978,10 +6348,23 @@ var VERSION = "0.8.11";
|
|
|
4978
6348
|
getEdgeIntersection,
|
|
4979
6349
|
getElementBounds,
|
|
4980
6350
|
getElementCenter,
|
|
6351
|
+
getHexCellsInCone,
|
|
6352
|
+
getHexCellsInLine,
|
|
6353
|
+
getHexCellsInRadius,
|
|
6354
|
+
getHexCellsInSquare,
|
|
6355
|
+
getHexDistance,
|
|
4981
6356
|
isBindable,
|
|
4982
6357
|
isNearBezier,
|
|
4983
6358
|
parseState,
|
|
6359
|
+
sanitizeNoteHtml,
|
|
6360
|
+
setFontSize,
|
|
6361
|
+
smartSnap,
|
|
4984
6362
|
snapPoint,
|
|
6363
|
+
snapToHexCenter,
|
|
6364
|
+
toggleBold,
|
|
6365
|
+
toggleItalic,
|
|
6366
|
+
toggleStrikethrough,
|
|
6367
|
+
toggleUnderline,
|
|
4985
6368
|
unbindArrow,
|
|
4986
6369
|
updateBoundArrow
|
|
4987
6370
|
});
|