canvas_erd 0.1.0 → 0.2.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 +11 -0
- data/README.md +6 -2
- data/lib/canvas_erd/diagram_store.rb +16 -2
- data/lib/canvas_erd/server.rb +10 -1
- data/lib/canvas_erd/version.rb +1 -2
- data/lib/canvas_erd/web/app.js +845 -70
- data/lib/canvas_erd/web/document.js +401 -2
- data/lib/canvas_erd/web/index.html +153 -1
- data/lib/canvas_erd/web/styles.css +208 -0
- metadata +1 -1
|
@@ -11,6 +11,42 @@
|
|
|
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 DEFAULT_TABLE_HEADER_COLOR = "#243b63";
|
|
29
|
+
const DEFAULT_TABLE_HEADER_COLORS = DEFAULT_FOREGROUND_COLORS;
|
|
30
|
+
const DEFAULT_ARROW_FORMATTING = Object.freeze({
|
|
31
|
+
color: DEFAULT_FOREGROUND_COLORS[0],
|
|
32
|
+
strokeWidth: 2.25,
|
|
33
|
+
lineStyle: "solid",
|
|
34
|
+
startHead: "none",
|
|
35
|
+
endHead: "filled"
|
|
36
|
+
});
|
|
37
|
+
const DEFAULT_FRAME_FORMATTING = Object.freeze({
|
|
38
|
+
color: DEFAULT_FOREGROUND_COLORS[0],
|
|
39
|
+
lineStyle: "solid"
|
|
40
|
+
});
|
|
41
|
+
const DEFAULT_NOTE_FORMATTING = Object.freeze({
|
|
42
|
+
paragraphStyle: "paragraph",
|
|
43
|
+
...NOTE_PARAGRAPH_STYLES.paragraph,
|
|
44
|
+
fontStyle: "normal",
|
|
45
|
+
underline: false,
|
|
46
|
+
fill: "#374151",
|
|
47
|
+
backgroundColor: "#fff2a8",
|
|
48
|
+
textAlign: "left"
|
|
49
|
+
});
|
|
14
50
|
|
|
15
51
|
function topLeft(options = {}) {
|
|
16
52
|
return {
|
|
@@ -37,12 +73,224 @@
|
|
|
37
73
|
return NOTE_PADDING * zoom;
|
|
38
74
|
}
|
|
39
75
|
|
|
76
|
+
function noteFormatting(note = {}) {
|
|
77
|
+
const paragraphStyle = noteParagraphStyle(note);
|
|
78
|
+
const style = NOTE_PARAGRAPH_STYLES[paragraphStyle];
|
|
79
|
+
return {
|
|
80
|
+
paragraphStyle,
|
|
81
|
+
fontWeight: ["normal", "bold"].includes(note.fontWeight) ? note.fontWeight : style.fontWeight,
|
|
82
|
+
fontStyle: note.fontStyle === "italic" ? "italic" : DEFAULT_NOTE_FORMATTING.fontStyle,
|
|
83
|
+
underline: note.underline === true,
|
|
84
|
+
fill: typeof note.fill === "string" ? note.fill : DEFAULT_NOTE_FORMATTING.fill,
|
|
85
|
+
backgroundColor: typeof note.backgroundColor === "string"
|
|
86
|
+
? note.backgroundColor
|
|
87
|
+
: DEFAULT_NOTE_FORMATTING.backgroundColor,
|
|
88
|
+
fontFamily: style.fontFamily,
|
|
89
|
+
fontSize: style.fontSize,
|
|
90
|
+
lineHeight: style.lineHeight,
|
|
91
|
+
textAlign: ["left", "center", "right"].includes(note.textAlign)
|
|
92
|
+
? note.textAlign
|
|
93
|
+
: DEFAULT_NOTE_FORMATTING.textAlign
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function noteParagraphStyle(note = {}) {
|
|
98
|
+
if (NOTE_PARAGRAPH_STYLES[note.paragraphStyle]) return note.paragraphStyle;
|
|
99
|
+
if (note.fontFamily === "Courier New") return "code";
|
|
100
|
+
if (["Georgia", "Times New Roman"].includes(note.fontFamily)) {
|
|
101
|
+
return note.fontSize >= 28 ? "title" : "heading";
|
|
102
|
+
}
|
|
103
|
+
return "paragraph";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function noteParagraphStyleFormatting(paragraphStyle) {
|
|
107
|
+
const resolved = NOTE_PARAGRAPH_STYLES[paragraphStyle] ? paragraphStyle : "paragraph";
|
|
108
|
+
return { paragraphStyle: resolved, ...NOTE_PARAGRAPH_STYLES[resolved] };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function inheritedNoteFormatting(note) {
|
|
112
|
+
return { ...noteFormatting(note || {}) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function noteColorSwatches(notes, property, defaults) {
|
|
116
|
+
const colors = [...defaults];
|
|
117
|
+
notes.forEach((note) => {
|
|
118
|
+
const color = note[property];
|
|
119
|
+
if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) return;
|
|
120
|
+
const normalized = color.toLowerCase();
|
|
121
|
+
if (!colors.includes(normalized)) colors.push(normalized);
|
|
122
|
+
});
|
|
123
|
+
return colors;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function colorPaletteSections(colors, defaults) {
|
|
127
|
+
return {
|
|
128
|
+
defaults: colors.slice(0, defaults.length),
|
|
129
|
+
custom: colors.slice(defaults.length)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function noteBorderColor(backgroundColor) {
|
|
134
|
+
if (backgroundColor === "transparent") return "#94a3b8";
|
|
135
|
+
const match = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(backgroundColor);
|
|
136
|
+
if (!match) return "#94a3b8";
|
|
137
|
+
|
|
138
|
+
const channels = match.slice(1).map((channel) => parseInt(channel, 16));
|
|
139
|
+
const luminance = channels[0] * 0.299 + channels[1] * 0.587 + channels[2] * 0.114;
|
|
140
|
+
const target = luminance > 150 ? 0 : 255;
|
|
141
|
+
const mixed = channels.map((channel) => Math.round(channel * 0.7 + target * 0.3));
|
|
142
|
+
return `#${mixed.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function tableHeaderColor(state, entityId) {
|
|
146
|
+
return state.tableHeaderColors?.[entityId] || DEFAULT_TABLE_HEADER_COLOR;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function setTableHeaderColor(state, entityIds, color) {
|
|
150
|
+
const tableHeaderColors = { ...(state.tableHeaderColors || {}) };
|
|
151
|
+
entityIds.forEach((entityId) => {
|
|
152
|
+
if (color === DEFAULT_TABLE_HEADER_COLOR) delete tableHeaderColors[entityId];
|
|
153
|
+
else tableHeaderColors[entityId] = color;
|
|
154
|
+
});
|
|
155
|
+
return { ...state, tableHeaderColors };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function tableHeaderColorSwatches(tableHeaderColors = {}) {
|
|
159
|
+
const colors = [...DEFAULT_TABLE_HEADER_COLORS];
|
|
160
|
+
Object.values(tableHeaderColors).forEach((color) => {
|
|
161
|
+
if (typeof color !== "string" || !/^#[0-9a-f]{6}$/i.test(color)) return;
|
|
162
|
+
const normalized = color.toLowerCase();
|
|
163
|
+
if (!colors.includes(normalized)) colors.push(normalized);
|
|
164
|
+
});
|
|
165
|
+
return colors;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function selectedTableHeaderColor(state, entityIds) {
|
|
169
|
+
if (entityIds.length === 0) return null;
|
|
170
|
+
const colors = entityIds.map((entityId) => tableHeaderColor(state, entityId));
|
|
171
|
+
return colors.every((color) => color === colors[0]) ? colors[0] : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function arrowFormatting(arrow = {}) {
|
|
175
|
+
const strokeWidth = Number(arrow.strokeWidth);
|
|
176
|
+
const headStyles = [
|
|
177
|
+
"none", "filled", "open", "circle", "circle-open", "diamond", "diamond-open"
|
|
178
|
+
];
|
|
179
|
+
return {
|
|
180
|
+
color: typeof arrow.color === "string" ? arrow.color : DEFAULT_ARROW_FORMATTING.color,
|
|
181
|
+
strokeWidth: Number.isFinite(strokeWidth)
|
|
182
|
+
? Math.min(20, Math.max(0.5, strokeWidth))
|
|
183
|
+
: DEFAULT_ARROW_FORMATTING.strokeWidth,
|
|
184
|
+
lineStyle: ["solid", "dashed", "dotted"].includes(arrow.lineStyle)
|
|
185
|
+
? arrow.lineStyle
|
|
186
|
+
: DEFAULT_ARROW_FORMATTING.lineStyle,
|
|
187
|
+
startHead: headStyles.includes(arrow.startHead)
|
|
188
|
+
? arrow.startHead
|
|
189
|
+
: DEFAULT_ARROW_FORMATTING.startHead,
|
|
190
|
+
endHead: headStyles.includes(arrow.endHead)
|
|
191
|
+
? arrow.endHead
|
|
192
|
+
: DEFAULT_ARROW_FORMATTING.endHead
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function arrowStrokeDashArray(lineStyle) {
|
|
197
|
+
return { dashed: [10, 7], dotted: [2, 6] }[lineStyle] || null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function arrowHeadGeometry(style, strokeWidth) {
|
|
201
|
+
const width = arrowFormatting({ strokeWidth }).strokeWidth;
|
|
202
|
+
const length = 10 + width * 1.5;
|
|
203
|
+
const halfWidth = 5 + width;
|
|
204
|
+
const radius = halfWidth * 0.72;
|
|
205
|
+
return {
|
|
206
|
+
length,
|
|
207
|
+
halfWidth,
|
|
208
|
+
radius,
|
|
209
|
+
shaftInset: style === "circle-open" ? radius * 2 : (style === "diamond-open" ? length : 0)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function shortenedArrowLinePoints(points, startHead, endHead, strokeWidth) {
|
|
214
|
+
const x = points.x2 - points.x1;
|
|
215
|
+
const y = points.y2 - points.y1;
|
|
216
|
+
const distance = Math.hypot(x, y);
|
|
217
|
+
if (distance === 0) return { ...points };
|
|
218
|
+
|
|
219
|
+
const startInset = arrowHeadGeometry(startHead, strokeWidth).shaftInset;
|
|
220
|
+
const endInset = arrowHeadGeometry(endHead, strokeWidth).shaftInset;
|
|
221
|
+
const scale = startInset + endInset > distance ? distance / (startInset + endInset) : 1;
|
|
222
|
+
const unitX = x / distance;
|
|
223
|
+
const unitY = y / distance;
|
|
224
|
+
return {
|
|
225
|
+
x1: points.x1 + unitX * startInset * scale,
|
|
226
|
+
y1: points.y1 + unitY * startInset * scale,
|
|
227
|
+
x2: points.x2 - unitX * endInset * scale,
|
|
228
|
+
y2: points.y2 - unitY * endInset * scale
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function arrowColorSwatches(arrows = []) {
|
|
233
|
+
const colors = [...DEFAULT_FOREGROUND_COLORS];
|
|
234
|
+
arrows.forEach((arrow) => {
|
|
235
|
+
const color = arrowFormatting(arrow).color;
|
|
236
|
+
if (!/^#[0-9a-f]{6}$/i.test(color)) return;
|
|
237
|
+
const normalized = color.toLowerCase();
|
|
238
|
+
if (!colors.includes(normalized)) colors.push(normalized);
|
|
239
|
+
});
|
|
240
|
+
return colors;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function frameFormatting(frame = {}) {
|
|
244
|
+
return {
|
|
245
|
+
color: typeof frame.color === "string" ? frame.color : DEFAULT_FRAME_FORMATTING.color,
|
|
246
|
+
lineStyle: ["solid", "dashed", "dotted"].includes(frame.lineStyle)
|
|
247
|
+
? frame.lineStyle
|
|
248
|
+
: DEFAULT_FRAME_FORMATTING.lineStyle
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function frameColorSwatches(frames = []) {
|
|
253
|
+
const colors = [...DEFAULT_FOREGROUND_COLORS];
|
|
254
|
+
frames.forEach((frame) => {
|
|
255
|
+
const color = frameFormatting(frame).color;
|
|
256
|
+
if (!/^#[0-9a-f]{6}$/i.test(color)) return;
|
|
257
|
+
const normalized = color.toLowerCase();
|
|
258
|
+
if (!colors.includes(normalized)) colors.push(normalized);
|
|
259
|
+
});
|
|
260
|
+
return colors;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function frameBounds(first, second) {
|
|
264
|
+
return {
|
|
265
|
+
x: Math.min(first.x, second.x),
|
|
266
|
+
y: Math.min(first.y, second.y),
|
|
267
|
+
width: Math.abs(second.x - first.x),
|
|
268
|
+
height: Math.abs(second.y - first.y)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function frameLabelPosition(frame) {
|
|
273
|
+
return { x: frame.x + 10, y: frame.y };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function frameLabelCanvasStyle(frame) {
|
|
277
|
+
const position = frameLabelPosition(frame);
|
|
278
|
+
return {
|
|
279
|
+
left: position.x,
|
|
280
|
+
top: position.y,
|
|
281
|
+
originY: "center",
|
|
282
|
+
fill: frameFormatting(frame).color,
|
|
283
|
+
backgroundColor: "#f7f8fb"
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
40
287
|
function noteCanvasGeometry(note) {
|
|
41
288
|
return {
|
|
42
289
|
left: note.x,
|
|
43
290
|
top: note.y,
|
|
44
291
|
width: note.width,
|
|
45
|
-
angle: note.angle ?? 0
|
|
292
|
+
angle: note.angle ?? 0,
|
|
293
|
+
...noteFormatting(note)
|
|
46
294
|
};
|
|
47
295
|
}
|
|
48
296
|
|
|
@@ -52,7 +300,8 @@
|
|
|
52
300
|
y: object.top,
|
|
53
301
|
width: object.width * object.scaleX,
|
|
54
302
|
angle: object.angle,
|
|
55
|
-
text: object.text
|
|
303
|
+
text: object.text,
|
|
304
|
+
...noteFormatting(object)
|
|
56
305
|
};
|
|
57
306
|
}
|
|
58
307
|
|
|
@@ -96,6 +345,19 @@
|
|
|
96
345
|
return shortcutAction(event) === "save";
|
|
97
346
|
}
|
|
98
347
|
|
|
348
|
+
function noteFormattingShortcut(event) {
|
|
349
|
+
if (!(event.metaKey || event.ctrlKey)) return null;
|
|
350
|
+
if (event.altKey) {
|
|
351
|
+
return {
|
|
352
|
+
Digit1: "title",
|
|
353
|
+
Digit2: "heading",
|
|
354
|
+
Digit3: "paragraph",
|
|
355
|
+
Digit4: "code"
|
|
356
|
+
}[event.code] || null;
|
|
357
|
+
}
|
|
358
|
+
return { b: "bold", i: "italic", u: "underline" }[String(event.key).toLowerCase()] || null;
|
|
359
|
+
}
|
|
360
|
+
|
|
99
361
|
function selectedDiagramFilename(filename, availableFilenames) {
|
|
100
362
|
return filename && availableFilenames.includes(filename) ? filename : "";
|
|
101
363
|
}
|
|
@@ -139,7 +401,13 @@
|
|
|
139
401
|
return null;
|
|
140
402
|
}
|
|
141
403
|
|
|
404
|
+
function arrowIdForEndpointEditing(target) {
|
|
405
|
+
return target?.canvasErdType === "arrow" ? target.arrowId : null;
|
|
406
|
+
}
|
|
407
|
+
|
|
142
408
|
function canvasLayer(type) {
|
|
409
|
+
if (["frame", "frame-preview"].includes(type)) return -2;
|
|
410
|
+
if (type === "frame-label") return -1;
|
|
143
411
|
if (type === "relationship") return 0;
|
|
144
412
|
if (["arrow", "arrow-preview"].includes(type)) return 2;
|
|
145
413
|
if (type === "arrow-endpoint") return 3;
|
|
@@ -211,6 +479,8 @@
|
|
|
211
479
|
return {
|
|
212
480
|
includedEntityIds: schema.entities.map((entity) => entity.id),
|
|
213
481
|
positions,
|
|
482
|
+
tableHeaderColors: {},
|
|
483
|
+
frames: [],
|
|
214
484
|
notes: [],
|
|
215
485
|
arrows: []
|
|
216
486
|
};
|
|
@@ -271,6 +541,8 @@
|
|
|
271
541
|
...state,
|
|
272
542
|
includedEntityIds: state.includedEntityIds.filter((id) => previousEntityIds.has(id) && nextEntityIds.has(id)),
|
|
273
543
|
positions,
|
|
544
|
+
tableHeaderColors: { ...(state.tableHeaderColors || {}) },
|
|
545
|
+
frames: (state.frames || []).map((frame) => ({ ...frame })),
|
|
274
546
|
notes: state.notes.map((note) => ({ ...note })),
|
|
275
547
|
arrows: (state.arrows || []).map((arrow) => ({
|
|
276
548
|
...arrow,
|
|
@@ -346,6 +618,98 @@
|
|
|
346
618
|
return `${rangeLabel(relationship.cardinality.source)} — ${rangeLabel(relationship.cardinality.destination)}`;
|
|
347
619
|
}
|
|
348
620
|
|
|
621
|
+
function crowFootCardinality(range) {
|
|
622
|
+
const minimum = Number(range.minimum);
|
|
623
|
+
const maximum = range.maximum === null ? null : Number(range.maximum);
|
|
624
|
+
if (![0, 1].includes(minimum) || ![1, null].includes(maximum)) return null;
|
|
625
|
+
return { optional: minimum === 0, many: maximum === null };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function cardinalityEndpointGeometry(boundary, outward, range) {
|
|
629
|
+
const magnitude = Math.hypot(outward.x, outward.y) || 1;
|
|
630
|
+
const unit = { x: outward.x / magnitude, y: outward.y / magnitude };
|
|
631
|
+
const tangent = { x: -unit.y, y: unit.x };
|
|
632
|
+
const point = (origin, along, across = 0) => ({
|
|
633
|
+
x: origin.x + unit.x * along + tangent.x * across,
|
|
634
|
+
y: origin.y + unit.y * along + tangent.y * across
|
|
635
|
+
});
|
|
636
|
+
const cardinality = crowFootCardinality(range);
|
|
637
|
+
if (!cardinality) {
|
|
638
|
+
return {
|
|
639
|
+
boundary: { ...boundary },
|
|
640
|
+
shaftPoint: { ...boundary },
|
|
641
|
+
segments: [],
|
|
642
|
+
circles: [],
|
|
643
|
+
label: rangeLabel(range),
|
|
644
|
+
labelPosition: point(boundary, 18)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const segments = [];
|
|
649
|
+
const circles = [];
|
|
650
|
+
const maximumMarker = point(boundary, 9);
|
|
651
|
+
let cursor;
|
|
652
|
+
if (cardinality.many) {
|
|
653
|
+
const vertex = point(maximumMarker, 12);
|
|
654
|
+
segments.push(
|
|
655
|
+
{ start: { ...boundary }, end: maximumMarker },
|
|
656
|
+
{ start: vertex, end: point(maximumMarker, 0, -7) },
|
|
657
|
+
{ start: vertex, end: maximumMarker },
|
|
658
|
+
{ start: vertex, end: point(maximumMarker, 0, 7) }
|
|
659
|
+
);
|
|
660
|
+
cursor = vertex;
|
|
661
|
+
} else {
|
|
662
|
+
cursor = point(maximumMarker, 5);
|
|
663
|
+
segments.push(
|
|
664
|
+
{ start: { ...boundary }, end: cursor },
|
|
665
|
+
{ start: point(maximumMarker, 0, -7), end: point(maximumMarker, 0, 7) }
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let shaftPoint;
|
|
670
|
+
if (cardinality.optional) {
|
|
671
|
+
const nearEdge = point(cursor, 3);
|
|
672
|
+
const center = point(nearEdge, 4);
|
|
673
|
+
shaftPoint = point(center, 7);
|
|
674
|
+
segments.push({ start: cursor, end: nearEdge });
|
|
675
|
+
circles.push({ center, radius: 4 });
|
|
676
|
+
} else {
|
|
677
|
+
const minimumMarker = point(cursor, 6);
|
|
678
|
+
shaftPoint = point(cursor, 12);
|
|
679
|
+
segments.push(
|
|
680
|
+
{ start: cursor, end: shaftPoint },
|
|
681
|
+
{ start: point(minimumMarker, 0, -7), end: point(minimumMarker, 0, 7) }
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
boundary: { ...boundary },
|
|
687
|
+
shaftPoint,
|
|
688
|
+
segments,
|
|
689
|
+
circles,
|
|
690
|
+
label: null,
|
|
691
|
+
labelPosition: null
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function relationshipEndpointGeometry(bounds, toward, range) {
|
|
696
|
+
const center = {
|
|
697
|
+
x: bounds.left + bounds.width / 2,
|
|
698
|
+
y: bounds.top + bounds.height / 2
|
|
699
|
+
};
|
|
700
|
+
const delta = { x: toward.x - center.x, y: toward.y - center.y };
|
|
701
|
+
const magnitude = Math.hypot(delta.x, delta.y) || 1;
|
|
702
|
+
const unit = { x: delta.x / magnitude, y: delta.y / magnitude };
|
|
703
|
+
const xDistance = unit.x === 0 ? Infinity : bounds.width / 2 / Math.abs(unit.x);
|
|
704
|
+
const yDistance = unit.y === 0 ? Infinity : bounds.height / 2 / Math.abs(unit.y);
|
|
705
|
+
const distance = Math.min(xDistance, yDistance);
|
|
706
|
+
const boundary = {
|
|
707
|
+
x: center.x + unit.x * distance,
|
|
708
|
+
y: center.y + unit.y * distance
|
|
709
|
+
};
|
|
710
|
+
return cardinalityEndpointGeometry(boundary, unit, range);
|
|
711
|
+
}
|
|
712
|
+
|
|
349
713
|
function fitViewport(bounds, viewport, padding = 64, maximumZoom = 1) {
|
|
350
714
|
if (!bounds || bounds.width <= 0 || bounds.height <= 0) return [1, 0, 0, 1, 0, 0];
|
|
351
715
|
|
|
@@ -435,10 +799,39 @@
|
|
|
435
799
|
ROW_HEIGHT,
|
|
436
800
|
NOTE_PADDING,
|
|
437
801
|
CANVAS_FONT_FAMILY,
|
|
802
|
+
DEFAULT_BACKGROUND_COLORS,
|
|
803
|
+
DEFAULT_FOREGROUND_COLORS,
|
|
804
|
+
NOTE_PARAGRAPH_STYLES,
|
|
805
|
+
DEFAULT_TABLE_HEADER_COLOR,
|
|
806
|
+
DEFAULT_TABLE_HEADER_COLORS,
|
|
807
|
+
DEFAULT_ARROW_FORMATTING,
|
|
808
|
+
DEFAULT_FRAME_FORMATTING,
|
|
809
|
+
DEFAULT_NOTE_FORMATTING,
|
|
438
810
|
topLeft,
|
|
439
811
|
uncachedText,
|
|
440
812
|
paddedBackgroundBounds,
|
|
441
813
|
noteSelectionPadding,
|
|
814
|
+
noteFormatting,
|
|
815
|
+
noteParagraphStyle,
|
|
816
|
+
noteParagraphStyleFormatting,
|
|
817
|
+
inheritedNoteFormatting,
|
|
818
|
+
noteColorSwatches,
|
|
819
|
+
colorPaletteSections,
|
|
820
|
+
noteBorderColor,
|
|
821
|
+
tableHeaderColor,
|
|
822
|
+
setTableHeaderColor,
|
|
823
|
+
tableHeaderColorSwatches,
|
|
824
|
+
selectedTableHeaderColor,
|
|
825
|
+
arrowFormatting,
|
|
826
|
+
arrowStrokeDashArray,
|
|
827
|
+
arrowHeadGeometry,
|
|
828
|
+
shortenedArrowLinePoints,
|
|
829
|
+
arrowColorSwatches,
|
|
830
|
+
frameFormatting,
|
|
831
|
+
frameColorSwatches,
|
|
832
|
+
frameBounds,
|
|
833
|
+
frameLabelPosition,
|
|
834
|
+
frameLabelCanvasStyle,
|
|
442
835
|
noteCanvasGeometry,
|
|
443
836
|
noteStateGeometry,
|
|
444
837
|
erdFilename,
|
|
@@ -446,11 +839,13 @@
|
|
|
446
839
|
confirmDiscardChanges,
|
|
447
840
|
shortcutAction,
|
|
448
841
|
isSaveShortcut,
|
|
842
|
+
noteFormattingShortcut,
|
|
449
843
|
selectedDiagramFilename,
|
|
450
844
|
notePositionAt,
|
|
451
845
|
snapPointToBounds,
|
|
452
846
|
pointAtAnchor,
|
|
453
847
|
arrowAttachment,
|
|
848
|
+
arrowIdForEndpointEditing,
|
|
454
849
|
canvasLayer,
|
|
455
850
|
transformedArrowEndpoints,
|
|
456
851
|
createChangeTracker,
|
|
@@ -462,7 +857,11 @@
|
|
|
462
857
|
setEntityIncluded,
|
|
463
858
|
selectedEntityIds,
|
|
464
859
|
visibleRelationships,
|
|
860
|
+
rangeLabel,
|
|
465
861
|
cardinalityLabel,
|
|
862
|
+
crowFootCardinality,
|
|
863
|
+
cardinalityEndpointGeometry,
|
|
864
|
+
relationshipEndpointGeometry,
|
|
466
865
|
fitViewport,
|
|
467
866
|
panViewport,
|
|
468
867
|
zoomForWheel,
|
|
@@ -66,6 +66,13 @@
|
|
|
66
66
|
<div><dt>New</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>N</kbd></dd></div>
|
|
67
67
|
<div><dt>Save</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>S</kbd></dd></div>
|
|
68
68
|
<div><dt>Add note at cursor</dt><dd><kbd>N</kbd></dd></div>
|
|
69
|
+
<div><dt>Bold note</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>B</kbd></dd></div>
|
|
70
|
+
<div><dt>Italic note</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>I</kbd></dd></div>
|
|
71
|
+
<div><dt>Underline note</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>U</kbd></dd></div>
|
|
72
|
+
<div><dt>Title style</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>Alt</kbd> <kbd>1</kbd></dd></div>
|
|
73
|
+
<div><dt>Heading style</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>Alt</kbd> <kbd>2</kbd></dd></div>
|
|
74
|
+
<div><dt>Paragraph style</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>Alt</kbd> <kbd>3</kbd></dd></div>
|
|
75
|
+
<div><dt>Code style</dt><dd><kbd>⌘/Ctrl</kbd> <kbd>Alt</kbd> <kbd>4</kbd></dd></div>
|
|
69
76
|
<div><dt>Arrow tool</dt><dd><kbd>A</kbd></dd></div>
|
|
70
77
|
<div><dt>Free arrow endpoint</dt><dd><kbd>Ctrl</kbd></dd></div>
|
|
71
78
|
<div><dt>Toggle tables</dt><dd><kbd>T</kbd></dd></div>
|
|
@@ -76,6 +83,148 @@
|
|
|
76
83
|
<div><dt>Shortcut help</dt><dd><kbd>?</kbd></dd></div>
|
|
77
84
|
</dl>
|
|
78
85
|
</div>
|
|
86
|
+
<div id="note-formatting" class="note-formatting" role="toolbar" aria-label="Note formatting" hidden>
|
|
87
|
+
<button id="note-bold" class="format-button note-bold" type="button" aria-label="Bold" title="Bold (⌘/Ctrl-B)" aria-pressed="false">B</button>
|
|
88
|
+
<button id="note-italic" class="format-button note-italic" type="button" aria-label="Italic" title="Italic (⌘/Ctrl-I)" aria-pressed="false">I</button>
|
|
89
|
+
<button id="note-underline" class="format-button note-underline" type="button" aria-label="Underline" title="Underline (⌘/Ctrl-U)" aria-pressed="false">U</button>
|
|
90
|
+
<div class="color-control">
|
|
91
|
+
<button id="note-text-color-button" class="note-color-button" type="button" aria-label="Text color" title="Text color" aria-expanded="false">
|
|
92
|
+
<span id="note-text-color-preview" class="color-preview" aria-hidden="true"></span>
|
|
93
|
+
<span>Text</span>
|
|
94
|
+
</button>
|
|
95
|
+
<div id="note-text-color-menu" class="color-menu" hidden>
|
|
96
|
+
<div id="note-text-color-swatches" class="color-swatches" role="group" aria-label="Text color swatches"></div>
|
|
97
|
+
<label class="custom-color">
|
|
98
|
+
<span>Custom…</span>
|
|
99
|
+
<input id="note-text-custom-color" type="color" value="#374151" aria-label="Custom text color">
|
|
100
|
+
</label>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="color-control">
|
|
104
|
+
<button id="note-background-color-button" class="note-color-button" type="button" aria-label="Background color" title="Background color" aria-expanded="false">
|
|
105
|
+
<span id="note-background-color-preview" class="color-preview" aria-hidden="true"></span>
|
|
106
|
+
<span>Fill</span>
|
|
107
|
+
</button>
|
|
108
|
+
<div id="note-background-color-menu" class="color-menu" hidden>
|
|
109
|
+
<div id="note-background-color-swatches" class="color-swatches" role="group" aria-label="Background color swatches"></div>
|
|
110
|
+
<div class="color-menu-actions">
|
|
111
|
+
<button id="note-background-transparent" class="transparent-option" type="button" aria-label="Transparent background" aria-pressed="false">
|
|
112
|
+
<span class="color-preview transparent-color" aria-hidden="true"></span>
|
|
113
|
+
</button>
|
|
114
|
+
<label class="custom-color">
|
|
115
|
+
<span>Custom…</span>
|
|
116
|
+
<input id="note-background-custom-color" type="color" value="#fff2a8" aria-label="Custom background color">
|
|
117
|
+
</label>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
<label class="note-format-field">
|
|
122
|
+
<span>Style</span>
|
|
123
|
+
<select id="note-paragraph-style" aria-label="Paragraph style">
|
|
124
|
+
<option value="title">Title</option>
|
|
125
|
+
<option value="heading">Heading</option>
|
|
126
|
+
<option value="paragraph" selected>Paragraph</option>
|
|
127
|
+
<option value="code">Code</option>
|
|
128
|
+
</select>
|
|
129
|
+
</label>
|
|
130
|
+
<label class="note-format-field">
|
|
131
|
+
<span>Align</span>
|
|
132
|
+
<select id="note-alignment" aria-label="Text alignment">
|
|
133
|
+
<option value="left">Left</option>
|
|
134
|
+
<option value="center">Center</option>
|
|
135
|
+
<option value="right">Right</option>
|
|
136
|
+
</select>
|
|
137
|
+
</label>
|
|
138
|
+
</div>
|
|
139
|
+
<div id="table-formatting" class="table-formatting" role="toolbar" aria-label="Table formatting" hidden>
|
|
140
|
+
<div class="color-control">
|
|
141
|
+
<button id="table-header-color-button" class="note-color-button" type="button" aria-label="Table header color" title="Table header color" aria-expanded="false">
|
|
142
|
+
<span id="table-header-color-preview" class="color-preview" aria-hidden="true"></span>
|
|
143
|
+
<span>Header</span>
|
|
144
|
+
</button>
|
|
145
|
+
<div id="table-header-color-menu" class="color-menu" hidden>
|
|
146
|
+
<div id="table-header-color-swatches" class="color-swatches" role="group" aria-label="Table header color swatches"></div>
|
|
147
|
+
<label class="custom-color">
|
|
148
|
+
<span>Custom…</span>
|
|
149
|
+
<input id="table-header-custom-color" type="color" value="#243b63" aria-label="Custom table header color">
|
|
150
|
+
</label>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div id="arrow-formatting" class="arrow-formatting" role="toolbar" aria-label="Arrow formatting" hidden>
|
|
155
|
+
<div class="color-control">
|
|
156
|
+
<button id="arrow-color-button" class="note-color-button" type="button" aria-label="Arrow color" title="Arrow color" aria-expanded="false">
|
|
157
|
+
<span id="arrow-color-preview" class="color-preview" aria-hidden="true"></span>
|
|
158
|
+
<span>Color</span>
|
|
159
|
+
</button>
|
|
160
|
+
<div id="arrow-color-menu" class="color-menu" hidden>
|
|
161
|
+
<div id="arrow-color-swatches" class="color-swatches" role="group" aria-label="Arrow color swatches"></div>
|
|
162
|
+
<label class="custom-color">
|
|
163
|
+
<span>Custom…</span>
|
|
164
|
+
<input id="arrow-custom-color" type="color" value="#243b63" aria-label="Custom arrow color">
|
|
165
|
+
</label>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<label class="note-format-field">
|
|
169
|
+
<span>Start</span>
|
|
170
|
+
<select id="arrow-start-head" aria-label="Start arrowhead">
|
|
171
|
+
<option value="none" selected>None</option>
|
|
172
|
+
<option value="open">Arrow — open</option>
|
|
173
|
+
<option value="filled">Arrow — closed</option>
|
|
174
|
+
<option value="circle-open">Circle — open</option>
|
|
175
|
+
<option value="circle">Circle — closed</option>
|
|
176
|
+
<option value="diamond-open">Diamond — open</option>
|
|
177
|
+
<option value="diamond">Diamond — closed</option>
|
|
178
|
+
</select>
|
|
179
|
+
</label>
|
|
180
|
+
<label class="note-format-field">
|
|
181
|
+
<span>End</span>
|
|
182
|
+
<select id="arrow-end-head" aria-label="End arrowhead">
|
|
183
|
+
<option value="none">None</option>
|
|
184
|
+
<option value="open">Arrow — open</option>
|
|
185
|
+
<option value="filled" selected>Arrow — closed</option>
|
|
186
|
+
<option value="circle-open">Circle — open</option>
|
|
187
|
+
<option value="circle">Circle — closed</option>
|
|
188
|
+
<option value="diamond-open">Diamond — open</option>
|
|
189
|
+
<option value="diamond">Diamond — closed</option>
|
|
190
|
+
</select>
|
|
191
|
+
</label>
|
|
192
|
+
<label class="note-format-field">
|
|
193
|
+
<span>Line</span>
|
|
194
|
+
<select id="arrow-line-style" aria-label="Arrow line style">
|
|
195
|
+
<option value="solid" selected>Solid</option>
|
|
196
|
+
<option value="dashed">Dashed</option>
|
|
197
|
+
<option value="dotted">Dotted</option>
|
|
198
|
+
</select>
|
|
199
|
+
</label>
|
|
200
|
+
<label class="note-format-field">
|
|
201
|
+
<span>Width</span>
|
|
202
|
+
<input id="arrow-stroke-width" type="number" min="0.5" max="20" step="0.25" value="2.25" aria-label="Arrow line width">
|
|
203
|
+
</label>
|
|
204
|
+
</div>
|
|
205
|
+
<div id="frame-formatting" class="frame-formatting" role="toolbar" aria-label="Frame formatting" hidden>
|
|
206
|
+
<div class="color-control">
|
|
207
|
+
<button id="frame-color-button" class="note-color-button" type="button" aria-label="Frame color" title="Frame color" aria-expanded="false">
|
|
208
|
+
<span id="frame-color-preview" class="color-preview" aria-hidden="true"></span>
|
|
209
|
+
<span>Color</span>
|
|
210
|
+
</button>
|
|
211
|
+
<div id="frame-color-menu" class="color-menu" hidden>
|
|
212
|
+
<div id="frame-color-swatches" class="color-swatches" role="group" aria-label="Frame color swatches"></div>
|
|
213
|
+
<label class="custom-color">
|
|
214
|
+
<span>Custom…</span>
|
|
215
|
+
<input id="frame-custom-color" type="color" value="#243b63" aria-label="Custom frame color">
|
|
216
|
+
</label>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<label class="note-format-field">
|
|
220
|
+
<span>Border</span>
|
|
221
|
+
<select id="frame-line-style" aria-label="Frame border style">
|
|
222
|
+
<option value="solid" selected>Solid</option>
|
|
223
|
+
<option value="dashed">Dashed</option>
|
|
224
|
+
<option value="dotted">Dotted</option>
|
|
225
|
+
</select>
|
|
226
|
+
</label>
|
|
227
|
+
</div>
|
|
79
228
|
<div class="canvas-tools" aria-label="Canvas tools">
|
|
80
229
|
<button id="toggle-tables" class="icon-button" type="button" aria-label="Show tables" title="Show tables (T)" aria-expanded="false">
|
|
81
230
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M3 20.4V3.6C3 3.26863 3.26863 3 3.6 3H20.4C20.7314 3 21 3.26863 21 3.6V20.4C21 20.7314 20.7314 21 20.4 21H3.6C3.26863 21 3 20.7314 3 20.4Z" stroke="#000000" stroke-width="1.5"></path><path d="M3 16.5H21" stroke="#000000" stroke-width="1.5"></path><path d="M3 12H21" stroke="#000000" stroke-width="1.5"></path><path d="M21 7.5H3" stroke="#000000" stroke-width="1.5"></path><path d="M12 21V3" stroke="#000000" stroke-width="1.5"></path></svg>
|
|
@@ -83,10 +232,13 @@
|
|
|
83
232
|
<button id="layout-tables" class="icon-button" type="button" aria-label="Layout tables" title="Layout tables (L)" aria-expanded="false">
|
|
84
233
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M21 3.6V12H12V3H20.4C20.7314 3 21 3.26863 21 3.6Z" stroke="#000000" stroke-width="1.5"></path><path d="M21 20.4V12H12V21H20.4C20.7314 21 21 20.7314 21 20.4Z" stroke="#000000" stroke-width="1.5"></path><path d="M3 12V3.6C3 3.26863 3.26863 3 3.6 3H12V12H3Z" stroke="#000000" stroke-width="1.5"></path><path d="M3 12V20.4C3 20.7314 3.26863 21 3.6 21H12V12H3Z" stroke="#000000" stroke-width="1.5"></path></svg>
|
|
85
234
|
</button>
|
|
235
|
+
<button id="add-frame" class="icon-button" type="button" aria-label="Frame tool" title="Frame tool" aria-pressed="false">
|
|
236
|
+
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><rect x="3" y="4" width="18" height="16" rx="1" stroke="#000000" stroke-width="1.5" stroke-dasharray="3 2"></rect><path d="M6 8H12" stroke="#000000" stroke-width="1.5" stroke-linecap="round"></path></svg>
|
|
237
|
+
</button>
|
|
86
238
|
<button id="add-note" class="icon-button" type="button" aria-label="Add note" title="Add note (N)">
|
|
87
239
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M12 8L12 16M12 8H8M12 8H16" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 13.5V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V13.5M21 10.5V5C21 3.89543 20.1046 3 19 3H5C3.89543 3 3 3.89543 3 5V10.5" stroke="#000000" stroke-width="1.5" stroke-linejoin="round"></path><path d="M19.5 13.5V10.5H22.5V13.5H19.5Z" stroke="#000000" stroke-width="1.5" stroke-linejoin="round"></path><path d="M1.5 13.5V10.5H4.5V13.5H1.5Z" stroke="#000000" stroke-width="1.5" stroke-linejoin="round"></path></svg>
|
|
88
240
|
</button>
|
|
89
|
-
<button id="add-arrow" class="icon-button" type="button" aria-label="Arrow tool" title="Arrow tool (A) ·
|
|
241
|
+
<button id="add-arrow" class="icon-button" type="button" aria-label="Arrow tool" title="Arrow tool (A) · Select an arrow to edit endpoints · Hold Ctrl for a free endpoint" aria-pressed="false">
|
|
90
242
|
<svg width="24px" height="24px" viewBox="0 0 24 24" fill="none" color="#000000" aria-hidden="true" focusable="false"><path d="M6.00005 19L19 5.99996M19 5.99996V18.48M19 5.99996H6.52005" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>
|
|
91
243
|
</button>
|
|
92
244
|
</div>
|