@coral-viz/viz 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -4
- package/dist/compatibility/CompatibilityEdge.d.ts +16 -11
- package/dist/compatibility/CompatibilityEdge.d.ts.map +1 -1
- package/dist/compatibility/CompatibilityEdge.js +144 -28
- package/dist/compatibility/CompatibilityEdge.js.map +1 -1
- package/dist/components/DiagramFontFaces.d.ts +8 -0
- package/dist/components/DiagramFontFaces.d.ts.map +1 -0
- package/dist/components/DiagramFontFaces.js +18 -0
- package/dist/components/DiagramFontFaces.js.map +1 -0
- package/dist/components/SymbolNode.d.ts.map +1 -1
- package/dist/components/SymbolNode.js +59 -11
- package/dist/components/SymbolNode.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/editor/CoralEditor.js +1 -1
- package/dist/editor/CoralEditor.js.map +1 -1
- package/dist/editor/converter.d.ts +1 -1
- package/dist/editor/converter.d.ts.map +1 -1
- package/dist/editor/converter.js +41 -21
- package/dist/editor/converter.js.map +1 -1
- package/dist/editor/dimensions.d.ts +7 -0
- package/dist/editor/dimensions.d.ts.map +1 -0
- package/dist/editor/dimensions.js +26 -0
- package/dist/editor/dimensions.js.map +1 -0
- package/dist/file/deserialize.d.ts.map +1 -1
- package/dist/file/deserialize.js +9 -14
- package/dist/file/deserialize.js.map +1 -1
- package/dist/file/index.d.ts +2 -1
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +2 -1
- package/dist/file/index.js.map +1 -1
- package/dist/file/layoutProfile.d.ts +11 -0
- package/dist/file/layoutProfile.d.ts.map +1 -0
- package/dist/file/layoutProfile.js +89 -0
- package/dist/file/layoutProfile.js.map +1 -0
- package/dist/file/migrate.d.ts.map +1 -1
- package/dist/file/migrate.js +15 -3
- package/dist/file/migrate.js.map +1 -1
- package/dist/file/schema.d.ts +27 -0
- package/dist/file/schema.d.ts.map +1 -1
- package/dist/file/schema.js +70 -0
- package/dist/file/schema.js.map +1 -1
- package/dist/file/serialize.d.ts.map +1 -1
- package/dist/file/serialize.js +61 -16
- package/dist/file/serialize.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/layout/elk.d.ts +2 -51
- package/dist/layout/elk.d.ts.map +1 -1
- package/dist/layout/elk.js +107 -337
- package/dist/layout/elk.js.map +1 -1
- package/dist/layout/nodeSizing.d.ts +4 -61
- package/dist/layout/nodeSizing.d.ts.map +1 -1
- package/dist/layout/nodeSizing.js +4 -315
- package/dist/layout/nodeSizing.js.map +1 -1
- package/dist/layout/positionStability.d.ts +2 -1
- package/dist/layout/positionStability.d.ts.map +1 -1
- package/dist/layout/positionStability.js +40 -29
- package/dist/layout/positionStability.js.map +1 -1
- package/dist/layout/useAxisConstrain.d.ts.map +1 -1
- package/dist/layout/useAxisConstrain.js +20 -20
- package/dist/layout/useAxisConstrain.js.map +1 -1
- package/dist/layout/useDiagramState.d.ts +2 -1
- package/dist/layout/useDiagramState.d.ts.map +1 -1
- package/dist/layout/useLayoutHistory.d.ts +8 -0
- package/dist/layout/useLayoutHistory.d.ts.map +1 -1
- package/dist/layout/useLayoutHistory.js +78 -100
- package/dist/layout/useLayoutHistory.js.map +1 -1
- package/dist/properties/NodePropertiesPanel.d.ts +5 -1
- package/dist/properties/NodePropertiesPanel.d.ts.map +1 -1
- package/dist/properties/NodePropertiesPanel.js +95 -28
- package/dist/properties/NodePropertiesPanel.js.map +1 -1
- package/dist/settings/FontSettingsForm.d.ts.map +1 -1
- package/dist/settings/FontSettingsForm.js +9 -4
- package/dist/settings/FontSettingsForm.js.map +1 -1
- package/dist/settings/LayoutSettingsForm.d.ts +3 -1
- package/dist/settings/LayoutSettingsForm.d.ts.map +1 -1
- package/dist/settings/LayoutSettingsForm.js +43 -20
- package/dist/settings/LayoutSettingsForm.js.map +1 -1
- package/dist/settings/SettingsPanel.d.ts +10 -2
- package/dist/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/settings/SettingsPanel.js +45 -3
- package/dist/settings/SettingsPanel.js.map +1 -1
- package/dist/settings/useSettings.d.ts +8 -2
- package/dist/settings/useSettings.d.ts.map +1 -1
- package/dist/settings/useSettings.js +29 -7
- package/dist/settings/useSettings.js.map +1 -1
- package/dist/text-editor/SplitEditor.d.ts +5 -1
- package/dist/text-editor/SplitEditor.d.ts.map +1 -1
- package/dist/text-editor/SplitEditor.js +5 -12
- package/dist/text-editor/SplitEditor.js.map +1 -1
- package/dist/text-editor/TextEditor.d.ts +45 -33
- package/dist/text-editor/TextEditor.d.ts.map +1 -1
- package/dist/text-editor/TextEditor.js +178 -168
- package/dist/text-editor/TextEditor.js.map +1 -1
- package/dist/text-editor/hooks/useBidirectionalSync.d.ts +15 -0
- package/dist/text-editor/hooks/useBidirectionalSync.d.ts.map +1 -1
- package/dist/text-editor/hooks/useBidirectionalSync.js +9 -22
- package/dist/text-editor/hooks/useBidirectionalSync.js.map +1 -1
- package/dist/text-editor/index.d.ts +2 -2
- package/dist/text-editor/index.d.ts.map +1 -1
- package/dist/text-editor/index.js +1 -1
- package/dist/text-editor/index.js.map +1 -1
- package/dist/types.d.ts +24 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -1
- package/dist/types.js.map +1 -1
- package/dist/viewMetadata.d.ts +4 -0
- package/dist/viewMetadata.d.ts.map +1 -0
- package/dist/viewMetadata.js +63 -0
- package/dist/viewMetadata.js.map +1 -0
- package/dist/waypoints/useWaypointManager.d.ts.map +1 -1
- package/dist/waypoints/useWaypointManager.js +60 -84
- package/dist/waypoints/useWaypointManager.js.map +1 -1
- package/package.json +10 -9
|
@@ -1,319 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Compatibility exports for deterministic node sizing.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - Shape geometry constraints (textBoundsRatio)
|
|
7
|
-
* - Minimum size requirements
|
|
8
|
-
* - Padding specifications
|
|
4
|
+
* The implementation lives in @coral-viz/render so the editor, server-side
|
|
5
|
+
* renderer, exporters, and MCP tools all use the same bundled font metrics.
|
|
9
6
|
*/
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Default sizing for shapes without explicit sizing metadata
|
|
13
|
-
*/
|
|
14
|
-
const DEFAULT_SIZING = {
|
|
15
|
-
textBoundsRatio: { x: 1.0, y: 1.0 },
|
|
16
|
-
minSize: { width: 60, height: 30 },
|
|
17
|
-
padding: { x: 16, y: 12 },
|
|
18
|
-
};
|
|
19
|
-
/**
|
|
20
|
-
* Default font settings
|
|
21
|
-
*/
|
|
22
|
-
const DEFAULT_FONT = {
|
|
23
|
-
fontSize: 12,
|
|
24
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
25
|
-
fontWeight: 500,
|
|
26
|
-
lineHeight: 1.4,
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Canvas context for text measurement (cached for performance)
|
|
30
|
-
*/
|
|
31
|
-
let measureContext = null;
|
|
32
|
-
/**
|
|
33
|
-
* Get or create a canvas context for text measurement
|
|
34
|
-
*/
|
|
35
|
-
function getMeasureContext() {
|
|
36
|
-
if (measureContext) {
|
|
37
|
-
return measureContext;
|
|
38
|
-
}
|
|
39
|
-
// Check if we're in a browser environment
|
|
40
|
-
if (typeof document === 'undefined') {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const canvas = document.createElement('canvas');
|
|
44
|
-
measureContext = canvas.getContext('2d');
|
|
45
|
-
return measureContext;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Measure text dimensions using canvas
|
|
49
|
-
*
|
|
50
|
-
* Uses the Canvas API's measureText() for accurate text width measurement,
|
|
51
|
-
* with word wrapping support when maxWidth is specified.
|
|
52
|
-
*/
|
|
53
|
-
export function measureText(options) {
|
|
54
|
-
const { text, fontSize = DEFAULT_FONT.fontSize, fontFamily = DEFAULT_FONT.fontFamily, fontWeight = DEFAULT_FONT.fontWeight, maxWidth, lineHeight = DEFAULT_FONT.lineHeight, } = options;
|
|
55
|
-
const ctx = getMeasureContext();
|
|
56
|
-
// Fallback for non-browser environments (SSR, tests)
|
|
57
|
-
if (!ctx) {
|
|
58
|
-
return measureTextFallback(options);
|
|
59
|
-
}
|
|
60
|
-
// Set font for measurement
|
|
61
|
-
ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
|
|
62
|
-
// If no max width, measure as single line
|
|
63
|
-
if (!maxWidth) {
|
|
64
|
-
const metrics = ctx.measureText(text);
|
|
65
|
-
return {
|
|
66
|
-
width: Math.ceil(metrics.width),
|
|
67
|
-
height: Math.ceil(fontSize * lineHeight),
|
|
68
|
-
lines: [text],
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
// Word wrap to fit maxWidth
|
|
72
|
-
const lines = wrapText(ctx, text, maxWidth);
|
|
73
|
-
const lineHeightPx = fontSize * lineHeight;
|
|
74
|
-
// Find the widest line
|
|
75
|
-
let maxLineWidth = 0;
|
|
76
|
-
for (const line of lines) {
|
|
77
|
-
const metrics = ctx.measureText(line);
|
|
78
|
-
maxLineWidth = Math.max(maxLineWidth, metrics.width);
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
width: Math.ceil(maxLineWidth),
|
|
82
|
-
height: Math.ceil(lines.length * lineHeightPx),
|
|
83
|
-
lines,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Wrap text to fit within maxWidth
|
|
88
|
-
*/
|
|
89
|
-
function wrapText(ctx, text, maxWidth) {
|
|
90
|
-
// Handle empty text
|
|
91
|
-
if (!text || text.trim() === '') {
|
|
92
|
-
return [''];
|
|
93
|
-
}
|
|
94
|
-
// Split by explicit line breaks first
|
|
95
|
-
const paragraphs = text.split('\n');
|
|
96
|
-
const lines = [];
|
|
97
|
-
for (const paragraph of paragraphs) {
|
|
98
|
-
const words = paragraph.split(/\s+/).filter((w) => w.length > 0);
|
|
99
|
-
if (words.length === 0) {
|
|
100
|
-
lines.push('');
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
let currentLine = words[0];
|
|
104
|
-
for (let i = 1; i < words.length; i++) {
|
|
105
|
-
const word = words[i];
|
|
106
|
-
const testLine = currentLine + ' ' + word;
|
|
107
|
-
const metrics = ctx.measureText(testLine);
|
|
108
|
-
if (metrics.width <= maxWidth) {
|
|
109
|
-
currentLine = testLine;
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
lines.push(currentLine);
|
|
113
|
-
currentLine = word;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
lines.push(currentLine);
|
|
117
|
-
}
|
|
118
|
-
return lines;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Fallback text measurement for non-browser environments
|
|
122
|
-
* Uses character-based approximation
|
|
123
|
-
*/
|
|
124
|
-
function measureTextFallback(options) {
|
|
125
|
-
const { text, fontSize = DEFAULT_FONT.fontSize, lineHeight = DEFAULT_FONT.lineHeight, maxWidth, } = options;
|
|
126
|
-
// Approximate character width (varies by font, but 0.6 is reasonable for sans-serif)
|
|
127
|
-
const avgCharWidth = fontSize * 0.6;
|
|
128
|
-
const lineHeightPx = fontSize * lineHeight;
|
|
129
|
-
// Always split by newlines first
|
|
130
|
-
const paragraphs = text.split('\n');
|
|
131
|
-
if (!maxWidth) {
|
|
132
|
-
// No wrapping, just return paragraphs as lines
|
|
133
|
-
const maxLineWidth = Math.max(0, ...paragraphs.map((p) => p.length * avgCharWidth));
|
|
134
|
-
return {
|
|
135
|
-
width: Math.ceil(maxLineWidth),
|
|
136
|
-
height: Math.ceil(paragraphs.length * lineHeightPx),
|
|
137
|
-
lines: paragraphs,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
// Approximate word wrapping
|
|
141
|
-
const charsPerLine = Math.floor(maxWidth / avgCharWidth);
|
|
142
|
-
const lines = [];
|
|
143
|
-
for (const paragraph of paragraphs) {
|
|
144
|
-
if (paragraph.length === 0) {
|
|
145
|
-
lines.push('');
|
|
146
|
-
}
|
|
147
|
-
else if (paragraph.length <= charsPerLine) {
|
|
148
|
-
lines.push(paragraph);
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
// Simple word-aware wrapping
|
|
152
|
-
const words = paragraph.split(/\s+/);
|
|
153
|
-
let currentLine = '';
|
|
154
|
-
for (const word of words) {
|
|
155
|
-
if ((currentLine + ' ' + word).length <= charsPerLine) {
|
|
156
|
-
currentLine = currentLine ? currentLine + ' ' + word : word;
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
if (currentLine)
|
|
160
|
-
lines.push(currentLine);
|
|
161
|
-
currentLine = word;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
if (currentLine)
|
|
165
|
-
lines.push(currentLine);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const maxLineWidth = Math.max(0, ...lines.map((l) => l.length * avgCharWidth));
|
|
169
|
-
return {
|
|
170
|
-
width: Math.ceil(maxLineWidth),
|
|
171
|
-
height: Math.ceil(lines.length * lineHeightPx),
|
|
172
|
-
lines,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Compute optimal node size based on text content and shape geometry
|
|
177
|
-
*
|
|
178
|
-
* The sizing algorithm:
|
|
179
|
-
* 1. Measure text dimensions
|
|
180
|
-
* 2. Apply shape's textBoundsRatio to account for geometry (e.g., diamond needs more space)
|
|
181
|
-
* 3. Add padding for visual breathing room
|
|
182
|
-
* 4. Enforce minimum size constraints
|
|
183
|
-
* 5. Optionally apply uniform sizing across nodes of the same shape
|
|
184
|
-
*/
|
|
185
|
-
export function computeNodeSize(options) {
|
|
186
|
-
const { text, shapeId, sizingMode, fontSize = DEFAULT_FONT.fontSize, fontFamily = DEFAULT_FONT.fontFamily, fontWeight = DEFAULT_FONT.fontWeight, minWidth, minHeight, uniformSizes, } = options;
|
|
187
|
-
// Get shape definition and sizing metadata
|
|
188
|
-
const shape = shapeRegistry.get(shapeId);
|
|
189
|
-
const sizing = shape?.sizing || DEFAULT_SIZING;
|
|
190
|
-
const defaultSize = shape?.defaultSize || { width: 100, height: 60 };
|
|
191
|
-
// For uniform mode, use cached sizes if available
|
|
192
|
-
if (sizingMode === 'uniform' && uniformSizes?.has(shapeId)) {
|
|
193
|
-
const uniformSize = uniformSizes.get(shapeId);
|
|
194
|
-
return {
|
|
195
|
-
width: uniformSize.width,
|
|
196
|
-
height: uniformSize.height,
|
|
197
|
-
textBox: computeTextBox(uniformSize.width, uniformSize.height, sizing),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
// Measure text with initial max width estimate
|
|
201
|
-
const initialMaxWidth = defaultSize.width - sizing.padding.x * 2;
|
|
202
|
-
const textDims = measureText({
|
|
203
|
-
text: text || '',
|
|
204
|
-
fontSize,
|
|
205
|
-
fontFamily,
|
|
206
|
-
fontWeight,
|
|
207
|
-
maxWidth: initialMaxWidth > 0 ? initialMaxWidth : undefined,
|
|
208
|
-
});
|
|
209
|
-
// Apply textBoundsRatio to account for shape geometry
|
|
210
|
-
// For a diamond, text occupies ~70% of the bounding box, so ratio is 1.45
|
|
211
|
-
const contentWidth = textDims.width * sizing.textBoundsRatio.x + sizing.padding.x * 2;
|
|
212
|
-
const contentHeight = textDims.height * sizing.textBoundsRatio.y + sizing.padding.y * 2;
|
|
213
|
-
// Determine final size based on sizing mode
|
|
214
|
-
let width;
|
|
215
|
-
let height;
|
|
216
|
-
switch (sizingMode) {
|
|
217
|
-
case 'adaptive':
|
|
218
|
-
// Size adapts to content
|
|
219
|
-
width = Math.max(contentWidth, sizing.minSize.width, minWidth || 0);
|
|
220
|
-
height = Math.max(contentHeight, sizing.minSize.height, minHeight || 0);
|
|
221
|
-
break;
|
|
222
|
-
case 'uniform':
|
|
223
|
-
// All nodes of this shape use the same size (largest needed)
|
|
224
|
-
// This will be populated on a second pass
|
|
225
|
-
width = Math.max(contentWidth, sizing.minSize.width, minWidth || 0);
|
|
226
|
-
height = Math.max(contentHeight, sizing.minSize.height, minHeight || 0);
|
|
227
|
-
break;
|
|
228
|
-
case 'hybrid':
|
|
229
|
-
default:
|
|
230
|
-
// Blend: adaptive width, uniform height per shape type
|
|
231
|
-
width = Math.max(contentWidth, sizing.minSize.width, minWidth || 0);
|
|
232
|
-
height = Math.max(sizing.minSize.height, defaultSize.height, minHeight || 0);
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
// Round to reasonable values
|
|
236
|
-
width = Math.ceil(width);
|
|
237
|
-
height = Math.ceil(height);
|
|
238
|
-
return {
|
|
239
|
-
width,
|
|
240
|
-
height,
|
|
241
|
-
textBox: computeTextBox(width, height, sizing),
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Compute the text bounding box within a node
|
|
246
|
-
* This is where text can be safely rendered without clipping
|
|
247
|
-
*/
|
|
248
|
-
function computeTextBox(width, height, sizing) {
|
|
249
|
-
// Text box is centered within the shape, reduced by the textBoundsRatio
|
|
250
|
-
const textWidth = (width - sizing.padding.x * 2) / sizing.textBoundsRatio.x;
|
|
251
|
-
const textHeight = (height - sizing.padding.y * 2) / sizing.textBoundsRatio.y;
|
|
252
|
-
return {
|
|
253
|
-
x: (width - textWidth) / 2,
|
|
254
|
-
y: (height - textHeight) / 2,
|
|
255
|
-
width: textWidth,
|
|
256
|
-
height: textHeight,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Compute uniform sizes for all shapes in a diagram
|
|
261
|
-
*
|
|
262
|
-
* For uniform sizing mode, we need to find the maximum required size
|
|
263
|
-
* for each shape type, then apply that to all nodes of that type.
|
|
264
|
-
*/
|
|
265
|
-
export function computeUniformSizes(nodes, options) {
|
|
266
|
-
const uniformSizes = new Map();
|
|
267
|
-
// First pass: compute adaptive sizes for all nodes
|
|
268
|
-
const sizesByShape = new Map();
|
|
269
|
-
for (const node of nodes) {
|
|
270
|
-
const dims = computeNodeSize({
|
|
271
|
-
text: node.text,
|
|
272
|
-
shapeId: node.shapeId,
|
|
273
|
-
sizingMode: 'adaptive',
|
|
274
|
-
...options,
|
|
275
|
-
});
|
|
276
|
-
if (!sizesByShape.has(node.shapeId)) {
|
|
277
|
-
sizesByShape.set(node.shapeId, []);
|
|
278
|
-
}
|
|
279
|
-
sizesByShape.get(node.shapeId).push({ width: dims.width, height: dims.height });
|
|
280
|
-
}
|
|
281
|
-
// Second pass: find max size for each shape
|
|
282
|
-
for (const [shapeId, sizes] of sizesByShape) {
|
|
283
|
-
const maxWidth = Math.max(...sizes.map((s) => s.width));
|
|
284
|
-
const maxHeight = Math.max(...sizes.map((s) => s.height));
|
|
285
|
-
uniformSizes.set(shapeId, { width: maxWidth, height: maxHeight });
|
|
286
|
-
}
|
|
287
|
-
return uniformSizes;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Apply adaptive sizing to a set of nodes
|
|
291
|
-
*
|
|
292
|
-
* This is the main entry point for layout integration.
|
|
293
|
-
* Returns nodes with updated width/height properties.
|
|
294
|
-
*/
|
|
295
|
-
export function applyAdaptiveSizing(nodes, options) {
|
|
296
|
-
const { sizingMode, getShapeId, fontSize, fontFamily, fontWeight } = options;
|
|
297
|
-
// For uniform mode, pre-compute sizes
|
|
298
|
-
let uniformSizes;
|
|
299
|
-
if (sizingMode === 'uniform') {
|
|
300
|
-
uniformSizes = computeUniformSizes(nodes.map((n) => ({ text: n.data.label || '', shapeId: getShapeId(n) })), { fontSize, fontFamily, fontWeight });
|
|
301
|
-
}
|
|
302
|
-
return nodes.map((node) => {
|
|
303
|
-
const dims = computeNodeSize({
|
|
304
|
-
text: node.data.label || '',
|
|
305
|
-
shapeId: getShapeId(node),
|
|
306
|
-
sizingMode,
|
|
307
|
-
fontSize,
|
|
308
|
-
fontFamily,
|
|
309
|
-
fontWeight,
|
|
310
|
-
uniformSizes,
|
|
311
|
-
});
|
|
312
|
-
return {
|
|
313
|
-
...node,
|
|
314
|
-
width: dims.width,
|
|
315
|
-
height: dims.height,
|
|
316
|
-
};
|
|
317
|
-
});
|
|
318
|
-
}
|
|
7
|
+
export { applyAdaptiveSizing, computeNodeSize, computeUniformSizes, measureText, } from '@coral-viz/render';
|
|
319
8
|
//# sourceMappingURL=nodeSizing.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nodeSizing.js","sourceRoot":"","sources":["../../src/layout/nodeSizing.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"nodeSizing.js","sourceRoot":"","sources":["../../src/layout/nodeSizing.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EACnB,WAAW,GACZ,MAAM,mBAAmB,CAAC"}
|
|
@@ -39,6 +39,7 @@ interface LayoutNodeInfo {
|
|
|
39
39
|
label: string;
|
|
40
40
|
width: number;
|
|
41
41
|
height: number;
|
|
42
|
+
parentId?: string;
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
44
45
|
* Edge info for incremental layout
|
|
@@ -68,6 +69,6 @@ export declare function incrementalLayout(nodes: LayoutNodeInfo[], edges: Layout
|
|
|
68
69
|
direction?: 'DOWN' | 'UP' | 'LEFT' | 'RIGHT';
|
|
69
70
|
algorithm?: string;
|
|
70
71
|
spacing?: number;
|
|
71
|
-
}): Promise<Map<string, Position>>;
|
|
72
|
+
}, signal?: AbortSignal): Promise<Map<string, Position>>;
|
|
72
73
|
export {};
|
|
73
74
|
//# sourceMappingURL=positionStability.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"positionStability.d.ts","sourceRoot":"","sources":["../../src/layout/positionStability.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"positionStability.d.ts","sourceRoot":"","sources":["../../src/layout/positionStability.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAgB,SAAS,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAExG;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,GAAG,SAAS,CAwCtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,aAAa,EACvB,QAAQ,EAAE,aAAa,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,GACtC,kBAAkB,CA6BpB;AAED;;GAEG;AACH,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,cAAc,EAAE,EACvB,KAAK,EAAE,cAAc,EAAE,EACvB,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EACtC,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,CAAC,EAAE;IACR,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,EACD,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAsGhC"}
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Separates parse, render, and layout operations to prevent undo/redo
|
|
6
6
|
* positions from being overwritten by automatic layout.
|
|
7
7
|
*/
|
|
8
|
-
import
|
|
9
|
-
const elk = new ELK();
|
|
8
|
+
import { computeElkLayout } from '@coral-viz/render/layout';
|
|
10
9
|
/**
|
|
11
10
|
* Compare two graphs and identify added, removed, modified, and unchanged nodes.
|
|
12
11
|
*
|
|
@@ -37,7 +36,8 @@ export function diffGraphs(oldGraph, newGraph) {
|
|
|
37
36
|
// Node doesn't exist in old graph - it's new
|
|
38
37
|
added.push(node.id);
|
|
39
38
|
}
|
|
40
|
-
else if (oldNode.type !== node.type || oldNode.label !== node.label
|
|
39
|
+
else if (oldNode.type !== node.type || oldNode.label !== node.label
|
|
40
|
+
|| oldNode.parentId !== node.parentId) {
|
|
41
41
|
// Node exists but has changed type or label
|
|
42
42
|
modified.push(node.id);
|
|
43
43
|
}
|
|
@@ -71,9 +71,12 @@ export function resolvePositions(oldGraph, newGraph, currentPositions) {
|
|
|
71
71
|
const positions = new Map();
|
|
72
72
|
const needsLayout = [];
|
|
73
73
|
// Process unchanged and modified nodes - keep their positions
|
|
74
|
+
const oldById = new Map(oldGraph.nodes.map((node) => [node.id, node]));
|
|
75
|
+
const nextById = new Map(newGraph.nodes.map((node) => [node.id, node]));
|
|
74
76
|
for (const id of [...diff.unchanged, ...diff.modified]) {
|
|
75
77
|
const pos = currentPositions.get(id);
|
|
76
|
-
|
|
78
|
+
const reparented = oldById.get(id)?.parentId !== nextById.get(id)?.parentId;
|
|
79
|
+
if (pos && !reparented) {
|
|
77
80
|
positions.set(id, pos);
|
|
78
81
|
}
|
|
79
82
|
else {
|
|
@@ -106,7 +109,7 @@ export function resolvePositions(oldGraph, newGraph, currentPositions) {
|
|
|
106
109
|
* @param options - Optional layout options (direction, algorithm, etc.)
|
|
107
110
|
* @returns Map of node IDs to computed positions
|
|
108
111
|
*/
|
|
109
|
-
export async function incrementalLayout(nodes, edges, pinnedPositions, needsLayout, options) {
|
|
112
|
+
export async function incrementalLayout(nodes, edges, pinnedPositions, needsLayout, options, signal) {
|
|
110
113
|
// Handle empty case
|
|
111
114
|
if (nodes.length === 0) {
|
|
112
115
|
return new Map();
|
|
@@ -126,8 +129,13 @@ export async function incrementalLayout(nodes, edges, pinnedPositions, needsLayo
|
|
|
126
129
|
// Strategy: Run ELK on only the nodes that need layout, plus any nodes
|
|
127
130
|
// they connect to. Then merge results with pinned positions.
|
|
128
131
|
// For simplicity, we'll run ELK on all nodes but restore pinned positions after
|
|
129
|
-
const
|
|
130
|
-
|
|
132
|
+
const byParent = new Map();
|
|
133
|
+
for (const node of nodes) {
|
|
134
|
+
if (node.parentId)
|
|
135
|
+
byParent.set(node.parentId, [...(byParent.get(node.parentId) ?? []), node]);
|
|
136
|
+
}
|
|
137
|
+
const toLayoutNode = (node) => {
|
|
138
|
+
const layoutNode = {
|
|
131
139
|
id: node.id,
|
|
132
140
|
width: node.width,
|
|
133
141
|
height: node.height,
|
|
@@ -135,32 +143,33 @@ export async function incrementalLayout(nodes, edges, pinnedPositions, needsLayo
|
|
|
135
143
|
// Set initial position for ELK (helps with layout stability)
|
|
136
144
|
const pinnedPos = pinnedPositions.get(node.id);
|
|
137
145
|
if (pinnedPos) {
|
|
138
|
-
|
|
139
|
-
elkNode.y = pinnedPos.y;
|
|
146
|
+
Object.assign(layoutNode, { position: pinnedPos });
|
|
140
147
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
sources: [edge.source],
|
|
146
|
-
targets: [edge.target],
|
|
147
|
-
}));
|
|
148
|
-
const elkGraph = {
|
|
149
|
-
id: 'root',
|
|
150
|
-
layoutOptions: {
|
|
151
|
-
'elk.algorithm': options?.algorithm || 'layered',
|
|
152
|
-
'elk.direction': options?.direction || 'DOWN',
|
|
153
|
-
'elk.spacing.nodeNode': String(options?.spacing || 50),
|
|
154
|
-
'elk.layered.spacing.nodeNodeBetweenLayers': '70',
|
|
155
|
-
},
|
|
156
|
-
children: elkChildren,
|
|
157
|
-
edges: elkEdges,
|
|
148
|
+
const children = byParent.get(node.id);
|
|
149
|
+
if (children?.length)
|
|
150
|
+
layoutNode.children = children.map(toLayoutNode);
|
|
151
|
+
return layoutNode;
|
|
158
152
|
};
|
|
153
|
+
const knownIds = new Set(nodes.map((node) => node.id));
|
|
154
|
+
const layoutNodes = nodes.filter((node) => !node.parentId || !knownIds.has(node.parentId)).map(toLayoutNode);
|
|
159
155
|
try {
|
|
160
|
-
const layoutedGraph = await
|
|
156
|
+
const layoutedGraph = await computeElkLayout({
|
|
157
|
+
id: 'root',
|
|
158
|
+
defaults: {
|
|
159
|
+
algorithm: 'layered', direction: 'DOWN',
|
|
160
|
+
spacing: { nodeNode: 50, layerSpacing: 70 },
|
|
161
|
+
},
|
|
162
|
+
options: {
|
|
163
|
+
algorithm: options?.algorithm,
|
|
164
|
+
direction: options?.direction,
|
|
165
|
+
spacing: options?.spacing === undefined ? undefined : { nodeNode: options.spacing },
|
|
166
|
+
},
|
|
167
|
+
nodes: layoutNodes,
|
|
168
|
+
edges,
|
|
169
|
+
}, { signal });
|
|
161
170
|
// Collect positions from layout result, but restore pinned positions
|
|
162
171
|
const result = new Map();
|
|
163
|
-
for (const elkNode of layoutedGraph.
|
|
172
|
+
for (const elkNode of layoutedGraph.nodes) {
|
|
164
173
|
// For pinned nodes, use the original position
|
|
165
174
|
const pinnedPos = pinnedPositions.get(elkNode.id);
|
|
166
175
|
if (pinnedPos && !needsLayoutSet.has(elkNode.id)) {
|
|
@@ -168,12 +177,14 @@ export async function incrementalLayout(nodes, edges, pinnedPositions, needsLayo
|
|
|
168
177
|
}
|
|
169
178
|
else {
|
|
170
179
|
// Use ELK-computed position for new nodes
|
|
171
|
-
result.set(elkNode.id,
|
|
180
|
+
result.set(elkNode.id, elkNode.position);
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
return result;
|
|
175
184
|
}
|
|
176
185
|
catch (error) {
|
|
186
|
+
if (error.name === 'AbortError' || signal?.aborted)
|
|
187
|
+
throw error;
|
|
177
188
|
console.error('Incremental layout failed:', error);
|
|
178
189
|
// Fallback: return pinned positions and place new nodes nearby
|
|
179
190
|
const result = new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"positionStability.js","sourceRoot":"","sources":["../../src/layout/positionStability.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"positionStability.js","sourceRoot":"","sources":["../../src/layout/positionStability.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAxisConstrain.d.ts","sourceRoot":"","sources":["../../src/layout/useAxisConstrain.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAc,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAWvE,MAAM,WAAW,sBAAsB,CAAC,QAAQ,SAAS,QAAQ,GAAG,QAAQ;IAC1E,6EAA6E;IAC7E,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,SAAS,QAAQ,GAAG,QAAQ,EACnE,qBAAqB,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,EAChE,KAAK,EAAE,QAAQ,EAAE,GAChB,sBAAsB,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"useAxisConstrain.d.ts","sourceRoot":"","sources":["../../src/layout/useAxisConstrain.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAc,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAWvE,MAAM,WAAW,sBAAsB,CAAC,QAAQ,SAAS,QAAQ,GAAG,QAAQ;IAC1E,6EAA6E;IAC7E,aAAa,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,CAAC;CAC1D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,SAAS,QAAQ,GAAG,QAAQ,EACnE,qBAAqB,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,EAChE,KAAK,EAAE,QAAQ,EAAE,GAChB,sBAAsB,CAAC,QAAQ,CAAC,CA8FlC"}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* axis is detected after a small movement threshold.
|
|
7
7
|
*/
|
|
8
8
|
import { useCallback, useRef, useEffect } from 'react';
|
|
9
|
-
/** Threshold in
|
|
9
|
+
/** Threshold in diagram-space units before locking to an axis */
|
|
10
10
|
const AXIS_LOCK_THRESHOLD = 5;
|
|
11
11
|
/**
|
|
12
12
|
* Wraps onNodesChange to add Shift+drag axis constraint.
|
|
@@ -16,7 +16,7 @@ const AXIS_LOCK_THRESHOLD = 5;
|
|
|
16
16
|
*/
|
|
17
17
|
export function useAxisConstrain(originalOnNodesChange, nodes) {
|
|
18
18
|
const shiftHeld = useRef(false);
|
|
19
|
-
const
|
|
19
|
+
const dragStates = useRef(new Map());
|
|
20
20
|
// Track Shift key globally
|
|
21
21
|
useEffect(() => {
|
|
22
22
|
const onKeyDown = (e) => {
|
|
@@ -24,12 +24,17 @@ export function useAxisConstrain(originalOnNodesChange, nodes) {
|
|
|
24
24
|
shiftHeld.current = true;
|
|
25
25
|
};
|
|
26
26
|
const onKeyUp = (e) => {
|
|
27
|
-
if (e.key === 'Shift')
|
|
27
|
+
if (e.key === 'Shift') {
|
|
28
28
|
shiftHeld.current = false;
|
|
29
|
+
dragStates.current.clear();
|
|
30
|
+
}
|
|
29
31
|
};
|
|
32
|
+
const onBlur = () => { shiftHeld.current = false; dragStates.current.clear(); };
|
|
33
|
+
window.addEventListener('blur', onBlur);
|
|
30
34
|
window.addEventListener('keydown', onKeyDown);
|
|
31
35
|
window.addEventListener('keyup', onKeyUp);
|
|
32
36
|
return () => {
|
|
37
|
+
window.removeEventListener('blur', onBlur);
|
|
33
38
|
window.removeEventListener('keydown', onKeyDown);
|
|
34
39
|
window.removeEventListener('keyup', onKeyUp);
|
|
35
40
|
};
|
|
@@ -37,8 +42,7 @@ export function useAxisConstrain(originalOnNodesChange, nodes) {
|
|
|
37
42
|
const onNodesChange = useCallback((changes) => {
|
|
38
43
|
if (!shiftHeld.current) {
|
|
39
44
|
// No Shift held — pass through unmodified, reset drag state
|
|
40
|
-
|
|
41
|
-
dragState.current = null;
|
|
45
|
+
dragStates.current.clear();
|
|
42
46
|
originalOnNodesChange(changes);
|
|
43
47
|
return;
|
|
44
48
|
}
|
|
@@ -46,28 +50,24 @@ export function useAxisConstrain(originalOnNodesChange, nodes) {
|
|
|
46
50
|
if (change.type !== 'position')
|
|
47
51
|
return change;
|
|
48
52
|
const posChange = change;
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
dragState.current = null;
|
|
53
|
-
}
|
|
53
|
+
// Programmatic positions are not gestures. The terminal drag event
|
|
54
|
+
// still uses the existing lock before that lock is released.
|
|
55
|
+
if (posChange.dragging === undefined)
|
|
54
56
|
return change;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
let state = dragStates.current.get(posChange.id);
|
|
58
|
+
if (posChange.dragging === false)
|
|
59
|
+
dragStates.current.delete(posChange.id);
|
|
57
60
|
if (!posChange.position)
|
|
58
61
|
return change;
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
if (!state) {
|
|
63
|
+
if (posChange.dragging === false)
|
|
64
|
+
return change;
|
|
61
65
|
const node = nodes.find((n) => n.id === posChange.id);
|
|
62
66
|
if (!node)
|
|
63
67
|
return change;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
startPos: { x: node.position.x, y: node.position.y },
|
|
67
|
-
axis: null,
|
|
68
|
-
};
|
|
68
|
+
state = { nodeId: posChange.id, startPos: { ...node.position }, axis: null };
|
|
69
|
+
dragStates.current.set(posChange.id, state);
|
|
69
70
|
}
|
|
70
|
-
const state = dragState.current;
|
|
71
71
|
const dx = posChange.position.x - state.startPos.x;
|
|
72
72
|
const dy = posChange.position.y - state.startPos.y;
|
|
73
73
|
// Detect axis if not yet locked
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAxisConstrain.js","sourceRoot":"","sources":["../../src/layout/useAxisConstrain.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvD,
|
|
1
|
+
{"version":3,"file":"useAxisConstrain.js","sourceRoot":"","sources":["../../src/layout/useAxisConstrain.ts"],"names":[],"mappings":"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"}
|