canvas_erd 0.1.0 → 0.3.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.
@@ -11,6 +11,43 @@
11
11
  const CARD_PADDING = 10;
12
12
  const NOTE_PADDING = 12;
13
13
  const CANVAS_FONT_FAMILY = "Arial";
14
+ const DEFAULT_BACKGROUND_COLORS = Object.freeze([
15
+ "#dbeafe", "#fee2e2", "#ffedd5", "#fff2a8", "#dcfce7",
16
+ "#cffafe", "#ede9fe", "#f3f4f6"
17
+ ]);
18
+ const DEFAULT_FOREGROUND_COLORS = Object.freeze([
19
+ "#243b63", "#7f1d1d", "#9a3412", "#854d0e", "#166534",
20
+ "#155e75", "#6b21a8", "#374151"
21
+ ]);
22
+ const NOTE_PARAGRAPH_STYLES = Object.freeze({
23
+ title: Object.freeze({ fontFamily: "Georgia", fontSize: 32, fontWeight: "bold", lineHeight: 1.15 }),
24
+ heading: Object.freeze({ fontFamily: "Georgia", fontSize: 22, fontWeight: "bold", lineHeight: 1.15 }),
25
+ paragraph: Object.freeze({ fontFamily: "Arial", fontSize: 15, fontWeight: "normal", lineHeight: 1.25 }),
26
+ code: Object.freeze({ fontFamily: "Courier New", fontSize: 14, fontWeight: "normal", lineHeight: 1.2 })
27
+ });
28
+ const CODE_FONT_SIZES = Object.freeze({ small: 12, medium: 14, large: 18 });
29
+ const DEFAULT_TABLE_HEADER_COLOR = "#243b63";
30
+ const DEFAULT_TABLE_HEADER_COLORS = DEFAULT_FOREGROUND_COLORS;
31
+ const DEFAULT_ARROW_FORMATTING = Object.freeze({
32
+ color: DEFAULT_FOREGROUND_COLORS[0],
33
+ strokeWidth: 2.25,
34
+ lineStyle: "solid",
35
+ startHead: "none",
36
+ endHead: "filled"
37
+ });
38
+ const DEFAULT_FRAME_FORMATTING = Object.freeze({
39
+ color: DEFAULT_FOREGROUND_COLORS[0],
40
+ lineStyle: "solid"
41
+ });
42
+ const DEFAULT_NOTE_FORMATTING = Object.freeze({
43
+ paragraphStyle: "paragraph",
44
+ ...NOTE_PARAGRAPH_STYLES.paragraph,
45
+ fontStyle: "normal",
46
+ underline: false,
47
+ fill: "#374151",
48
+ backgroundColor: "#fff2a8",
49
+ textAlign: "left"
50
+ });
14
51
 
15
52
  function topLeft(options = {}) {
16
53
  return {
@@ -37,12 +74,224 @@
37
74
  return NOTE_PADDING * zoom;
38
75
  }
39
76
 
77
+ function noteFormatting(note = {}) {
78
+ const paragraphStyle = noteParagraphStyle(note);
79
+ const style = NOTE_PARAGRAPH_STYLES[paragraphStyle];
80
+ return {
81
+ paragraphStyle,
82
+ fontWeight: ["normal", "bold"].includes(note.fontWeight) ? note.fontWeight : style.fontWeight,
83
+ fontStyle: note.fontStyle === "italic" ? "italic" : DEFAULT_NOTE_FORMATTING.fontStyle,
84
+ underline: note.underline === true,
85
+ fill: typeof note.fill === "string" ? note.fill : DEFAULT_NOTE_FORMATTING.fill,
86
+ backgroundColor: typeof note.backgroundColor === "string"
87
+ ? note.backgroundColor
88
+ : DEFAULT_NOTE_FORMATTING.backgroundColor,
89
+ fontFamily: style.fontFamily,
90
+ fontSize: style.fontSize,
91
+ lineHeight: style.lineHeight,
92
+ textAlign: ["left", "center", "right"].includes(note.textAlign)
93
+ ? note.textAlign
94
+ : DEFAULT_NOTE_FORMATTING.textAlign
95
+ };
96
+ }
97
+
98
+ function noteParagraphStyle(note = {}) {
99
+ if (NOTE_PARAGRAPH_STYLES[note.paragraphStyle]) return note.paragraphStyle;
100
+ if (note.fontFamily === "Courier New") return "code";
101
+ if (["Georgia", "Times New Roman"].includes(note.fontFamily)) {
102
+ return note.fontSize >= 28 ? "title" : "heading";
103
+ }
104
+ return "paragraph";
105
+ }
106
+
107
+ function noteParagraphStyleFormatting(paragraphStyle) {
108
+ const resolved = NOTE_PARAGRAPH_STYLES[paragraphStyle] ? paragraphStyle : "paragraph";
109
+ return { paragraphStyle: resolved, ...NOTE_PARAGRAPH_STYLES[resolved] };
110
+ }
111
+
112
+ function inheritedNoteFormatting(note) {
113
+ return { ...noteFormatting(note || {}) };
114
+ }
115
+
116
+ function noteColorSwatches(notes, property, defaults) {
117
+ const colors = [...defaults];
118
+ notes.forEach((note) => {
119
+ const color = note[property];
120
+ if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) return;
121
+ const normalized = color.toLowerCase();
122
+ if (!colors.includes(normalized)) colors.push(normalized);
123
+ });
124
+ return colors;
125
+ }
126
+
127
+ function colorPaletteSections(colors, defaults) {
128
+ return {
129
+ defaults: colors.slice(0, defaults.length),
130
+ custom: colors.slice(defaults.length)
131
+ };
132
+ }
133
+
134
+ function noteBorderColor(backgroundColor) {
135
+ if (backgroundColor === "transparent") return "#94a3b8";
136
+ const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(backgroundColor);
137
+ if (!match) return "#94a3b8";
138
+
139
+ const channels = match.slice(1).map((channel) => parseInt(channel, 16));
140
+ const luminance = channels[0] * 0.299 + channels[1] * 0.587 + channels[2] * 0.114;
141
+ const target = luminance > 150 ? 0 : 255;
142
+ const mixed = channels.map((channel) => Math.round(channel * 0.7 + target * 0.3));
143
+ return `#${mixed.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
144
+ }
145
+
146
+ function tableHeaderColor(state, entityId) {
147
+ return state.tableHeaderColors?.[entityId] || DEFAULT_TABLE_HEADER_COLOR;
148
+ }
149
+
150
+ function setTableHeaderColor(state, entityIds, color) {
151
+ const tableHeaderColors = { ...(state.tableHeaderColors || {}) };
152
+ entityIds.forEach((entityId) => {
153
+ if (color === DEFAULT_TABLE_HEADER_COLOR) delete tableHeaderColors[entityId];
154
+ else tableHeaderColors[entityId] = color;
155
+ });
156
+ return { ...state, tableHeaderColors };
157
+ }
158
+
159
+ function tableHeaderColorSwatches(tableHeaderColors = {}) {
160
+ const colors = [...DEFAULT_TABLE_HEADER_COLORS];
161
+ Object.values(tableHeaderColors).forEach((color) => {
162
+ if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) return;
163
+ const normalized = color.toLowerCase();
164
+ if (!colors.includes(normalized)) colors.push(normalized);
165
+ });
166
+ return colors;
167
+ }
168
+
169
+ function selectedTableHeaderColor(state, entityIds) {
170
+ if (entityIds.length === 0) return null;
171
+ const colors = entityIds.map((entityId) => tableHeaderColor(state, entityId));
172
+ return colors.every((color) => color === colors[0]) ? colors[0] : null;
173
+ }
174
+
175
+ function arrowFormatting(arrow = {}) {
176
+ const strokeWidth = Number(arrow.strokeWidth);
177
+ const headStyles = [
178
+ "none", "filled", "open", "circle", "circle-open", "diamond", "diamond-open"
179
+ ];
180
+ return {
181
+ color: typeof arrow.color === "string" ? arrow.color : DEFAULT_ARROW_FORMATTING.color,
182
+ strokeWidth: Number.isFinite(strokeWidth)
183
+ ? Math.min(20, Math.max(0.5, strokeWidth))
184
+ : DEFAULT_ARROW_FORMATTING.strokeWidth,
185
+ lineStyle: ["solid", "dashed", "dotted"].includes(arrow.lineStyle)
186
+ ? arrow.lineStyle
187
+ : DEFAULT_ARROW_FORMATTING.lineStyle,
188
+ startHead: headStyles.includes(arrow.startHead)
189
+ ? arrow.startHead
190
+ : DEFAULT_ARROW_FORMATTING.startHead,
191
+ endHead: headStyles.includes(arrow.endHead)
192
+ ? arrow.endHead
193
+ : DEFAULT_ARROW_FORMATTING.endHead
194
+ };
195
+ }
196
+
197
+ function arrowStrokeDashArray(lineStyle) {
198
+ return { dashed: [10, 7], dotted: [2, 6] }[lineStyle] || null;
199
+ }
200
+
201
+ function arrowHeadGeometry(style, strokeWidth) {
202
+ const width = arrowFormatting({ strokeWidth }).strokeWidth;
203
+ const length = 10 + width * 1.5;
204
+ const halfWidth = 5 + width;
205
+ const radius = halfWidth * 0.72;
206
+ return {
207
+ length,
208
+ halfWidth,
209
+ radius,
210
+ shaftInset: style === "circle-open" ? radius * 2 : (style === "diamond-open" ? length : 0)
211
+ };
212
+ }
213
+
214
+ function shortenedArrowLinePoints(points, startHead, endHead, strokeWidth) {
215
+ const x = points.x2 - points.x1;
216
+ const y = points.y2 - points.y1;
217
+ const distance = Math.hypot(x, y);
218
+ if (distance === 0) return { ...points };
219
+
220
+ const startInset = arrowHeadGeometry(startHead, strokeWidth).shaftInset;
221
+ const endInset = arrowHeadGeometry(endHead, strokeWidth).shaftInset;
222
+ const scale = startInset + endInset > distance ? distance / (startInset + endInset) : 1;
223
+ const unitX = x / distance;
224
+ const unitY = y / distance;
225
+ return {
226
+ x1: points.x1 + unitX * startInset * scale,
227
+ y1: points.y1 + unitY * startInset * scale,
228
+ x2: points.x2 - unitX * endInset * scale,
229
+ y2: points.y2 - unitY * endInset * scale
230
+ };
231
+ }
232
+
233
+ function arrowColorSwatches(arrows = []) {
234
+ const colors = [...DEFAULT_FOREGROUND_COLORS];
235
+ arrows.forEach((arrow) => {
236
+ const color = arrowFormatting(arrow).color;
237
+ if (!/^#[0-9a-f]{6}$/i.test(color)) return;
238
+ const normalized = color.toLowerCase();
239
+ if (!colors.includes(normalized)) colors.push(normalized);
240
+ });
241
+ return colors;
242
+ }
243
+
244
+ function frameFormatting(frame = {}) {
245
+ return {
246
+ color: typeof frame.color === "string" ? frame.color : DEFAULT_FRAME_FORMATTING.color,
247
+ lineStyle: ["solid", "dashed", "dotted"].includes(frame.lineStyle)
248
+ ? frame.lineStyle
249
+ : DEFAULT_FRAME_FORMATTING.lineStyle
250
+ };
251
+ }
252
+
253
+ function frameColorSwatches(frames = []) {
254
+ const colors = [...DEFAULT_FOREGROUND_COLORS];
255
+ frames.forEach((frame) => {
256
+ const color = frameFormatting(frame).color;
257
+ if (!/^#[0-9a-f]{6}$/i.test(color)) return;
258
+ const normalized = color.toLowerCase();
259
+ if (!colors.includes(normalized)) colors.push(normalized);
260
+ });
261
+ return colors;
262
+ }
263
+
264
+ function frameBounds(first, second) {
265
+ return {
266
+ x: Math.min(first.x, second.x),
267
+ y: Math.min(first.y, second.y),
268
+ width: Math.abs(second.x - first.x),
269
+ height: Math.abs(second.y - first.y)
270
+ };
271
+ }
272
+
273
+ function frameLabelPosition(frame) {
274
+ return { x: frame.x + 10, y: frame.y };
275
+ }
276
+
277
+ function frameLabelCanvasStyle(frame) {
278
+ const position = frameLabelPosition(frame);
279
+ return {
280
+ left: position.x,
281
+ top: position.y,
282
+ originY: "center",
283
+ fill: frameFormatting(frame).color,
284
+ backgroundColor: "#f7f8fb"
285
+ };
286
+ }
287
+
40
288
  function noteCanvasGeometry(note) {
41
289
  return {
42
290
  left: note.x,
43
291
  top: note.y,
44
292
  width: note.width,
45
- angle: note.angle ?? 0
293
+ angle: note.angle ?? 0,
294
+ ...noteFormatting(note)
46
295
  };
47
296
  }
48
297
 
@@ -52,10 +301,120 @@
52
301
  y: object.top,
53
302
  width: object.width * object.scaleX,
54
303
  angle: object.angle,
55
- text: object.text
304
+ text: object.text,
305
+ ...noteFormatting(object)
56
306
  };
57
307
  }
58
308
 
309
+ function codeFontSize(block) {
310
+ return CODE_FONT_SIZES[block.size] || CODE_FONT_SIZES.medium;
311
+ }
312
+
313
+ function codeCanvasGeometry(block) {
314
+ const style = NOTE_PARAGRAPH_STYLES.code;
315
+ return {
316
+ left: block.x,
317
+ top: block.y,
318
+ width: block.width,
319
+ angle: block.angle ?? 0,
320
+ fontFamily: style.fontFamily,
321
+ fontSize: codeFontSize(block),
322
+ lineHeight: style.lineHeight
323
+ };
324
+ }
325
+
326
+ function codeStateGeometry(object) {
327
+ return {
328
+ x: object.left,
329
+ y: object.top,
330
+ width: object.width * object.scaleX,
331
+ angle: object.angle,
332
+ code: object.text
333
+ };
334
+ }
335
+
336
+ function codeTokenStyles(runs) {
337
+ const styles = {};
338
+ let line = 0;
339
+ let character = 0;
340
+ runs.forEach(([text, style]) => {
341
+ text.split("\n").forEach((segment, index) => {
342
+ if (index > 0) {
343
+ line += 1;
344
+ character = 0;
345
+ }
346
+ Array.from(segment).forEach(() => {
347
+ styles[line] = styles[line] || {};
348
+ styles[line][character] = { ...style };
349
+ character += 1;
350
+ });
351
+ });
352
+ });
353
+ return styles;
354
+ }
355
+
356
+ function sourceFromFields(path, startLine, endLine) {
357
+ const trimmed = String(path || "").trim();
358
+ if (!trimmed) return null;
359
+ const lineNumber = (value) => {
360
+ const number = Number.parseInt(value, 10);
361
+ return number > 0 ? number : null;
362
+ };
363
+ return { path: trimmed, startLine: lineNumber(startLine), endLine: lineNumber(endLine) };
364
+ }
365
+
366
+ function codeLineLabels(block, lineCount) {
367
+ if (block.source?.model) {
368
+ return Array.from({ length: lineCount }, (_, index) => block.lineNumbers?.[index] ?? null);
369
+ }
370
+ const first = block.source?.startLine || 1;
371
+ return Array.from({ length: lineCount }, (_, index) => first + index);
372
+ }
373
+
374
+ function codeGutterWidth(labels, fontSize) {
375
+ const digits = Math.max(1, ...labels.filter((label) => label !== null).map((label) => String(label).length));
376
+ return (digits + 2) * fontSize * 0.6;
377
+ }
378
+
379
+ function codeRowAt(rowHeights, y) {
380
+ let top = 0;
381
+ for (let index = 0; index < rowHeights.length; index += 1) {
382
+ if (y >= top && y < top + rowHeights[index]) return index;
383
+ top += rowHeights[index];
384
+ }
385
+ return -1;
386
+ }
387
+
388
+ function toggleHighlightedLine(lines = [], label) {
389
+ if (lines.includes(label)) return lines.filter((line) => line !== label);
390
+ return [...lines, label].sort((first, second) => first - second);
391
+ }
392
+
393
+ function highlightLineRange(lines = [], labels, anchor, label) {
394
+ const start = labels.indexOf(anchor);
395
+ const end = labels.indexOf(label);
396
+ if (start < 0 || end < 0) return toggleHighlightedLine(lines, label);
397
+
398
+ const range = labels.slice(Math.min(start, end), Math.max(start, end) + 1).filter((line) => line !== null);
399
+ if (!lines.includes(anchor)) return lines.filter((line) => !range.includes(line));
400
+ return [...new Set([...lines, ...range])].sort((first, second) => first - second);
401
+ }
402
+
403
+ function modelDetailBlock(codeBlocks, entityId, detail) {
404
+ return codeBlocks.find((block) => block.source?.model === entityId && block.source.detail === detail) || null;
405
+ }
406
+
407
+ function modelDetailOrigin(tablePosition) {
408
+ return { x: tablePosition.x + CARD_WIDTH + 80, y: tablePosition.y };
409
+ }
410
+
411
+ function sourceReferenceLabel(source) {
412
+ if (source.model) return `${source.model} ${source.detail}`;
413
+ if (!source.startLine && !source.endLine) return source.path;
414
+ if (source.startLine === source.endLine) return `${source.path}:${source.startLine}`;
415
+ return `${source.path}:${source.startLine || 1}-${source.endLine || "end"}`;
416
+ }
417
+
59
418
  function erdFilename(value) {
60
419
  const basename = String(value).trim().replace(/(?:\.erd)?\.png$/i, "");
61
420
  return `${basename || "diagram"}.erd.png`;
@@ -81,6 +440,7 @@
81
440
 
82
441
  return {
83
442
  n: "addNote",
443
+ c: "addCode",
84
444
  a: "arrow",
85
445
  t: "toggleTables",
86
446
  l: "layoutTables",
@@ -96,6 +456,19 @@
96
456
  return shortcutAction(event) === "save";
97
457
  }
98
458
 
459
+ function noteFormattingShortcut(event) {
460
+ if (!(event.metaKey || event.ctrlKey)) return null;
461
+ if (event.altKey) {
462
+ return {
463
+ Digit1: "title",
464
+ Digit2: "heading",
465
+ Digit3: "paragraph",
466
+ Digit4: "code"
467
+ }[event.code] || null;
468
+ }
469
+ return { b: "bold", i: "italic", u: "underline" }[String(event.key).toLowerCase()] || null;
470
+ }
471
+
99
472
  function selectedDiagramFilename(filename, availableFilenames) {
100
473
  return filename && availableFilenames.includes(filename) ? filename : "";
101
474
  }
@@ -136,10 +509,17 @@
136
509
  function arrowAttachment(target) {
137
510
  if (target?.canvasErdType === "entity") return { type: "entity", id: target.entityId };
138
511
  if (target?.canvasErdType === "note") return { type: "note", id: target.noteId };
512
+ if (target?.canvasErdType === "code") return { type: "code", id: target.codeBlockId };
139
513
  return null;
140
514
  }
141
515
 
516
+ function arrowIdForEndpointEditing(target) {
517
+ return target?.canvasErdType === "arrow" ? target.arrowId : null;
518
+ }
519
+
142
520
  function canvasLayer(type) {
521
+ if (["frame", "frame-preview"].includes(type)) return -2;
522
+ if (type === "frame-label") return -1;
143
523
  if (type === "relationship") return 0;
144
524
  if (["arrow", "arrow-preview"].includes(type)) return 2;
145
525
  if (type === "arrow-endpoint") return 3;
@@ -211,7 +591,10 @@
211
591
  return {
212
592
  includedEntityIds: schema.entities.map((entity) => entity.id),
213
593
  positions,
594
+ tableHeaderColors: {},
595
+ frames: [],
214
596
  notes: [],
597
+ codeBlocks: [],
215
598
  arrows: []
216
599
  };
217
600
  }
@@ -271,7 +654,13 @@
271
654
  ...state,
272
655
  includedEntityIds: state.includedEntityIds.filter((id) => previousEntityIds.has(id) && nextEntityIds.has(id)),
273
656
  positions,
657
+ tableHeaderColors: { ...(state.tableHeaderColors || {}) },
658
+ frames: (state.frames || []).map((frame) => ({ ...frame })),
274
659
  notes: state.notes.map((note) => ({ ...note })),
660
+ codeBlocks: (state.codeBlocks || []).map((block) => ({
661
+ ...block,
662
+ source: block.source ? { ...block.source } : null
663
+ })),
275
664
  arrows: (state.arrows || []).map((arrow) => ({
276
665
  ...arrow,
277
666
  start: { ...arrow.start, attachment: arrow.start.attachment ? { ...arrow.start.attachment } : null },
@@ -346,6 +735,98 @@
346
735
  return `${rangeLabel(relationship.cardinality.source)} — ${rangeLabel(relationship.cardinality.destination)}`;
347
736
  }
348
737
 
738
+ function crowFootCardinality(range) {
739
+ const minimum = Number(range.minimum);
740
+ const maximum = range.maximum === null ? null : Number(range.maximum);
741
+ if (![0, 1].includes(minimum) || ![1, null].includes(maximum)) return null;
742
+ return { optional: minimum === 0, many: maximum === null };
743
+ }
744
+
745
+ function cardinalityEndpointGeometry(boundary, outward, range) {
746
+ const magnitude = Math.hypot(outward.x, outward.y) || 1;
747
+ const unit = { x: outward.x / magnitude, y: outward.y / magnitude };
748
+ const tangent = { x: -unit.y, y: unit.x };
749
+ const point = (origin, along, across = 0) => ({
750
+ x: origin.x + unit.x * along + tangent.x * across,
751
+ y: origin.y + unit.y * along + tangent.y * across
752
+ });
753
+ const cardinality = crowFootCardinality(range);
754
+ if (!cardinality) {
755
+ return {
756
+ boundary: { ...boundary },
757
+ shaftPoint: { ...boundary },
758
+ segments: [],
759
+ circles: [],
760
+ label: rangeLabel(range),
761
+ labelPosition: point(boundary, 18)
762
+ };
763
+ }
764
+
765
+ const segments = [];
766
+ const circles = [];
767
+ const maximumMarker = point(boundary, 9);
768
+ let cursor;
769
+ if (cardinality.many) {
770
+ const vertex = point(maximumMarker, 12);
771
+ segments.push(
772
+ { start: { ...boundary }, end: maximumMarker },
773
+ { start: vertex, end: point(maximumMarker, 0, -7) },
774
+ { start: vertex, end: maximumMarker },
775
+ { start: vertex, end: point(maximumMarker, 0, 7) }
776
+ );
777
+ cursor = vertex;
778
+ } else {
779
+ cursor = point(maximumMarker, 5);
780
+ segments.push(
781
+ { start: { ...boundary }, end: cursor },
782
+ { start: point(maximumMarker, 0, -7), end: point(maximumMarker, 0, 7) }
783
+ );
784
+ }
785
+
786
+ let shaftPoint;
787
+ if (cardinality.optional) {
788
+ const nearEdge = point(cursor, 3);
789
+ const center = point(nearEdge, 4);
790
+ shaftPoint = point(center, 7);
791
+ segments.push({ start: cursor, end: nearEdge });
792
+ circles.push({ center, radius: 4 });
793
+ } else {
794
+ const minimumMarker = point(cursor, 6);
795
+ shaftPoint = point(cursor, 12);
796
+ segments.push(
797
+ { start: cursor, end: shaftPoint },
798
+ { start: point(minimumMarker, 0, -7), end: point(minimumMarker, 0, 7) }
799
+ );
800
+ }
801
+
802
+ return {
803
+ boundary: { ...boundary },
804
+ shaftPoint,
805
+ segments,
806
+ circles,
807
+ label: null,
808
+ labelPosition: null
809
+ };
810
+ }
811
+
812
+ function relationshipEndpointGeometry(bounds, toward, range) {
813
+ const center = {
814
+ x: bounds.left + bounds.width / 2,
815
+ y: bounds.top + bounds.height / 2
816
+ };
817
+ const delta = { x: toward.x - center.x, y: toward.y - center.y };
818
+ const magnitude = Math.hypot(delta.x, delta.y) || 1;
819
+ const unit = { x: delta.x / magnitude, y: delta.y / magnitude };
820
+ const xDistance = unit.x === 0 ? Infinity : bounds.width / 2 / Math.abs(unit.x);
821
+ const yDistance = unit.y === 0 ? Infinity : bounds.height / 2 / Math.abs(unit.y);
822
+ const distance = Math.min(xDistance, yDistance);
823
+ const boundary = {
824
+ x: center.x + unit.x * distance,
825
+ y: center.y + unit.y * distance
826
+ };
827
+ return cardinalityEndpointGeometry(boundary, unit, range);
828
+ }
829
+
349
830
  function fitViewport(bounds, viewport, padding = 64, maximumZoom = 1) {
350
831
  if (!bounds || bounds.width <= 0 || bounds.height <= 0) return [1, 0, 0, 1, 0, 0];
351
832
 
@@ -435,22 +916,67 @@
435
916
  ROW_HEIGHT,
436
917
  NOTE_PADDING,
437
918
  CANVAS_FONT_FAMILY,
919
+ DEFAULT_BACKGROUND_COLORS,
920
+ DEFAULT_FOREGROUND_COLORS,
921
+ NOTE_PARAGRAPH_STYLES,
922
+ DEFAULT_TABLE_HEADER_COLOR,
923
+ DEFAULT_TABLE_HEADER_COLORS,
924
+ DEFAULT_ARROW_FORMATTING,
925
+ DEFAULT_FRAME_FORMATTING,
926
+ DEFAULT_NOTE_FORMATTING,
438
927
  topLeft,
439
928
  uncachedText,
440
929
  paddedBackgroundBounds,
441
930
  noteSelectionPadding,
931
+ noteFormatting,
932
+ noteParagraphStyle,
933
+ noteParagraphStyleFormatting,
934
+ inheritedNoteFormatting,
935
+ noteColorSwatches,
936
+ colorPaletteSections,
937
+ noteBorderColor,
938
+ tableHeaderColor,
939
+ setTableHeaderColor,
940
+ tableHeaderColorSwatches,
941
+ selectedTableHeaderColor,
942
+ arrowFormatting,
943
+ arrowStrokeDashArray,
944
+ arrowHeadGeometry,
945
+ shortenedArrowLinePoints,
946
+ arrowColorSwatches,
947
+ frameFormatting,
948
+ frameColorSwatches,
949
+ frameBounds,
950
+ frameLabelPosition,
951
+ frameLabelCanvasStyle,
442
952
  noteCanvasGeometry,
443
953
  noteStateGeometry,
954
+ CODE_FONT_SIZES,
955
+ codeFontSize,
956
+ codeCanvasGeometry,
957
+ codeStateGeometry,
958
+ codeTokenStyles,
959
+ sourceFromFields,
960
+ sourceReferenceLabel,
961
+ codeLineLabels,
962
+ codeGutterWidth,
963
+ codeRowAt,
964
+ toggleHighlightedLine,
965
+ highlightLineRange,
966
+ modelDetailBlock,
967
+ modelDetailOrigin,
444
968
  erdFilename,
445
969
  diagramName,
446
970
  confirmDiscardChanges,
447
971
  shortcutAction,
448
972
  isSaveShortcut,
973
+ noteFormattingShortcut,
449
974
  selectedDiagramFilename,
450
975
  notePositionAt,
451
976
  snapPointToBounds,
452
977
  pointAtAnchor,
453
978
  arrowAttachment,
979
+ arrowIdForEndpointEditing,
454
980
  canvasLayer,
455
981
  transformedArrowEndpoints,
456
982
  createChangeTracker,
@@ -462,7 +988,11 @@
462
988
  setEntityIncluded,
463
989
  selectedEntityIds,
464
990
  visibleRelationships,
991
+ rangeLabel,
465
992
  cardinalityLabel,
993
+ crowFootCardinality,
994
+ cardinalityEndpointGeometry,
995
+ relationshipEndpointGeometry,
466
996
  fitViewport,
467
997
  panViewport,
468
998
  zoomForWheel,