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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +9 -3
- data/lib/canvas_erd/cli.rb +4 -1
- data/lib/canvas_erd/diagram_store.rb +16 -2
- data/lib/canvas_erd/highlighter.rb +57 -0
- data/lib/canvas_erd/model_details.rb +105 -0
- data/lib/canvas_erd/server.rb +79 -4
- data/lib/canvas_erd/source_files.rb +70 -0
- data/lib/canvas_erd/version.rb +1 -2
- data/lib/canvas_erd/web/app.js +1437 -91
- data/lib/canvas_erd/web/document.js +532 -2
- data/lib/canvas_erd/web/index.html +215 -1
- data/lib/canvas_erd/web/styles.css +295 -0
- data/lib/canvas_erd.rb +3 -0
- metadata +26 -3
|
@@ -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,
|