vizcraft 0.2.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/LICENSE.txt +21 -0
- package/README.md +104 -2
- package/dist/anim/animationBuilder.d.ts +2 -0
- package/dist/anim/animationBuilder.js +6 -1
- package/dist/anim/spec.d.ts +1 -1
- package/dist/anim/vizcraftAdapter.js +68 -1
- package/dist/builder.d.ts +70 -2
- package/dist/builder.js +719 -118
- package/dist/edgeLabels.d.ts +15 -0
- package/dist/edgeLabels.js +26 -0
- package/dist/edgePaths.d.ts +43 -0
- package/dist/edgePaths.js +253 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/overlayBuilder.d.ts +50 -0
- package/dist/overlayBuilder.js +80 -0
- package/dist/overlays.d.ts +113 -21
- package/dist/overlays.js +319 -1
- package/dist/runtimePatcher.d.ts +3 -3
- package/dist/runtimePatcher.js +231 -31
- package/dist/shapes.d.ts +25 -3
- package/dist/shapes.js +1009 -0
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +24 -0
- package/dist/types.d.ts +207 -1
- package/dist/types.js +2 -1
- package/package.json +1 -1
- package/dist/anim/player.test.d.ts +0 -1
- package/dist/anim/player.test.js +0 -49
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -66
package/dist/shapes.js
CHANGED
|
@@ -87,10 +87,796 @@ const diamondBehavior = {
|
|
|
87
87
|
};
|
|
88
88
|
},
|
|
89
89
|
};
|
|
90
|
+
function cylinderGeometry(shape, pos) {
|
|
91
|
+
const rx = shape.w / 2;
|
|
92
|
+
const ry = shape.arcHeight ?? Math.round(shape.h * 0.15);
|
|
93
|
+
const topY = pos.y - shape.h / 2;
|
|
94
|
+
const bottomY = pos.y + shape.h / 2;
|
|
95
|
+
const x0 = pos.x - rx;
|
|
96
|
+
const x1 = pos.x + rx;
|
|
97
|
+
const bodyD = `M ${x0} ${topY} A ${rx} ${ry} 0 0 1 ${x1} ${topY} V ${bottomY} A ${rx} ${ry} 0 0 1 ${x0} ${bottomY} V ${topY} Z`;
|
|
98
|
+
return { rx, ry, topY, bottomY, x0, x1, bodyD };
|
|
99
|
+
}
|
|
100
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
101
|
+
const cylinderBehavior = {
|
|
102
|
+
kind: 'cylinder',
|
|
103
|
+
tagName: 'g',
|
|
104
|
+
applyGeometry(el, shape, pos) {
|
|
105
|
+
const { rx, ry, topY, bodyD } = cylinderGeometry(shape, pos);
|
|
106
|
+
// Get or create body path
|
|
107
|
+
let body = el.querySelector('[data-viz-cyl="body"]');
|
|
108
|
+
if (!body) {
|
|
109
|
+
body = document.createElementNS(SVG_NS, 'path');
|
|
110
|
+
body.setAttribute('data-viz-cyl', 'body');
|
|
111
|
+
el.appendChild(body);
|
|
112
|
+
}
|
|
113
|
+
body.setAttribute('d', bodyD);
|
|
114
|
+
// Get or create top cap ellipse (drawn on top for 3D effect)
|
|
115
|
+
let cap = el.querySelector('[data-viz-cyl="cap"]');
|
|
116
|
+
if (!cap) {
|
|
117
|
+
cap = document.createElementNS(SVG_NS, 'ellipse');
|
|
118
|
+
cap.setAttribute('data-viz-cyl', 'cap');
|
|
119
|
+
el.appendChild(cap);
|
|
120
|
+
}
|
|
121
|
+
cap.setAttribute('cx', String(pos.x));
|
|
122
|
+
cap.setAttribute('cy', String(topY));
|
|
123
|
+
cap.setAttribute('rx', String(rx));
|
|
124
|
+
cap.setAttribute('ry', String(ry));
|
|
125
|
+
},
|
|
126
|
+
svgMarkup(shape, pos, attrs) {
|
|
127
|
+
const { rx, ry, topY, bodyD } = cylinderGeometry(shape, pos);
|
|
128
|
+
const end = '</g>';
|
|
129
|
+
return (`<g class="viz-node-shape" data-viz-role="node-shape"${attrs}>` +
|
|
130
|
+
`<path d="${bodyD}" data-viz-cyl="body"/>` +
|
|
131
|
+
`<ellipse cx="${pos.x}" cy="${topY}" rx="${rx}" ry="${ry}" data-viz-cyl="cap"/>` +
|
|
132
|
+
end);
|
|
133
|
+
},
|
|
134
|
+
anchorBoundary(pos, target, shape) {
|
|
135
|
+
// Approximate as rectangle bounding box
|
|
136
|
+
const dx = target.x - pos.x;
|
|
137
|
+
const dy = target.y - pos.y;
|
|
138
|
+
if (dx === 0 && dy === 0)
|
|
139
|
+
return { x: pos.x, y: pos.y };
|
|
140
|
+
const hw = shape.w / 2;
|
|
141
|
+
const hh = shape.h / 2;
|
|
142
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
143
|
+
return {
|
|
144
|
+
x: pos.x + dx * scale,
|
|
145
|
+
y: pos.y + dy * scale,
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
function hexagonPoints(pos, r, orientation) {
|
|
150
|
+
const pts = [];
|
|
151
|
+
// pointy-top: first vertex at top (angle offset -90°)
|
|
152
|
+
// flat-top: first vertex at right (angle offset 0°)
|
|
153
|
+
const angleOffset = orientation === 'pointy' ? -Math.PI / 2 : 0;
|
|
154
|
+
for (let i = 0; i < 6; i++) {
|
|
155
|
+
const angle = angleOffset + (Math.PI / 3) * i;
|
|
156
|
+
const px = pos.x + r * Math.cos(angle);
|
|
157
|
+
const py = pos.y + r * Math.sin(angle);
|
|
158
|
+
pts.push(`${px},${py}`);
|
|
159
|
+
}
|
|
160
|
+
return pts.join(' ');
|
|
161
|
+
}
|
|
162
|
+
const hexagonBehavior = {
|
|
163
|
+
kind: 'hexagon',
|
|
164
|
+
tagName: 'polygon',
|
|
165
|
+
applyGeometry(el, shape, pos) {
|
|
166
|
+
const orientation = shape.orientation ?? 'pointy';
|
|
167
|
+
el.setAttribute('points', hexagonPoints(pos, shape.r, orientation));
|
|
168
|
+
},
|
|
169
|
+
svgMarkup(shape, pos, attrs) {
|
|
170
|
+
const orientation = shape.orientation ?? 'pointy';
|
|
171
|
+
const pts = hexagonPoints(pos, shape.r, orientation);
|
|
172
|
+
return `<polygon points="${pts}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
173
|
+
},
|
|
174
|
+
anchorBoundary(pos, target, shape) {
|
|
175
|
+
// Use the circumscribed circle as the boundary approximation
|
|
176
|
+
const dx = target.x - pos.x;
|
|
177
|
+
const dy = target.y - pos.y;
|
|
178
|
+
if (dx === 0 && dy === 0)
|
|
179
|
+
return { x: pos.x, y: pos.y };
|
|
180
|
+
const dist = Math.hypot(dx, dy) || 1;
|
|
181
|
+
const scale = shape.r / dist;
|
|
182
|
+
return {
|
|
183
|
+
x: pos.x + dx * scale,
|
|
184
|
+
y: pos.y + dy * scale,
|
|
185
|
+
};
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const ellipseBehavior = {
|
|
189
|
+
kind: 'ellipse',
|
|
190
|
+
tagName: 'ellipse',
|
|
191
|
+
applyGeometry(el, shape, pos) {
|
|
192
|
+
el.setAttribute('cx', String(pos.x));
|
|
193
|
+
el.setAttribute('cy', String(pos.y));
|
|
194
|
+
el.setAttribute('rx', String(shape.rx));
|
|
195
|
+
el.setAttribute('ry', String(shape.ry));
|
|
196
|
+
},
|
|
197
|
+
svgMarkup(shape, pos, attrs) {
|
|
198
|
+
return `<ellipse cx="${pos.x}" cy="${pos.y}" rx="${shape.rx}" ry="${shape.ry}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
199
|
+
},
|
|
200
|
+
anchorBoundary(pos, target, shape) {
|
|
201
|
+
const dx = target.x - pos.x;
|
|
202
|
+
const dy = target.y - pos.y;
|
|
203
|
+
if (dx === 0 && dy === 0)
|
|
204
|
+
return { x: pos.x, y: pos.y };
|
|
205
|
+
const denom = Math.sqrt(shape.rx * shape.rx * dy * dy + shape.ry * shape.ry * dx * dx) || 1;
|
|
206
|
+
return {
|
|
207
|
+
x: pos.x + (shape.rx * shape.ry * dx) / denom,
|
|
208
|
+
y: pos.y + (shape.rx * shape.ry * dy) / denom,
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
function arcPathD(shape, pos) {
|
|
213
|
+
const toRad = Math.PI / 180;
|
|
214
|
+
const s = shape.startAngle * toRad;
|
|
215
|
+
const e = shape.endAngle * toRad;
|
|
216
|
+
const r = shape.r;
|
|
217
|
+
const sx = pos.x + r * Math.cos(s);
|
|
218
|
+
const sy = pos.y + r * Math.sin(s);
|
|
219
|
+
const ex = pos.x + r * Math.cos(e);
|
|
220
|
+
const ey = pos.y + r * Math.sin(e);
|
|
221
|
+
const sweep = shape.endAngle - shape.startAngle;
|
|
222
|
+
const largeArc = ((sweep % 360) + 360) % 360 > 180 ? 1 : 0;
|
|
223
|
+
const closed = shape.closed !== false;
|
|
224
|
+
if (closed) {
|
|
225
|
+
return `M ${pos.x} ${pos.y} L ${sx} ${sy} A ${r} ${r} 0 ${largeArc} 1 ${ex} ${ey} Z`;
|
|
226
|
+
}
|
|
227
|
+
return `M ${sx} ${sy} A ${r} ${r} 0 ${largeArc} 1 ${ex} ${ey}`;
|
|
228
|
+
}
|
|
229
|
+
const arcBehavior = {
|
|
230
|
+
kind: 'arc',
|
|
231
|
+
tagName: 'path',
|
|
232
|
+
applyGeometry(el, shape, pos) {
|
|
233
|
+
el.setAttribute('d', arcPathD(shape, pos));
|
|
234
|
+
},
|
|
235
|
+
svgMarkup(shape, pos, attrs) {
|
|
236
|
+
const d = arcPathD(shape, pos);
|
|
237
|
+
return `<path d="${d}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
238
|
+
},
|
|
239
|
+
anchorBoundary(pos, target, shape) {
|
|
240
|
+
// Approximate as circumscribed circle
|
|
241
|
+
const dx = target.x - pos.x;
|
|
242
|
+
const dy = target.y - pos.y;
|
|
243
|
+
if (dx === 0 && dy === 0)
|
|
244
|
+
return { x: pos.x, y: pos.y };
|
|
245
|
+
const dist = Math.hypot(dx, dy) || 1;
|
|
246
|
+
const scale = shape.r / dist;
|
|
247
|
+
return {
|
|
248
|
+
x: pos.x + dx * scale,
|
|
249
|
+
y: pos.y + dy * scale,
|
|
250
|
+
};
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
function blockArrowPoints(shape, pos) {
|
|
254
|
+
const halfBody = shape.bodyWidth / 2;
|
|
255
|
+
const halfHead = shape.headWidth / 2;
|
|
256
|
+
const halfLen = shape.length / 2;
|
|
257
|
+
const neckX = halfLen - shape.headLength;
|
|
258
|
+
const dir = shape.direction ?? 'right';
|
|
259
|
+
const angle = dir === 'left'
|
|
260
|
+
? Math.PI
|
|
261
|
+
: dir === 'up'
|
|
262
|
+
? -Math.PI / 2
|
|
263
|
+
: dir === 'down'
|
|
264
|
+
? Math.PI / 2
|
|
265
|
+
: 0;
|
|
266
|
+
const cos = Math.cos(angle);
|
|
267
|
+
const sin = Math.sin(angle);
|
|
268
|
+
const basePts = [
|
|
269
|
+
[-halfLen, -halfBody],
|
|
270
|
+
[neckX, -halfBody],
|
|
271
|
+
[neckX, -halfHead],
|
|
272
|
+
[halfLen, 0],
|
|
273
|
+
[neckX, halfHead],
|
|
274
|
+
[neckX, halfBody],
|
|
275
|
+
[-halfLen, halfBody],
|
|
276
|
+
];
|
|
277
|
+
return basePts
|
|
278
|
+
.map(([px, py]) => {
|
|
279
|
+
const rx = px * cos - py * sin;
|
|
280
|
+
const ry = px * sin + py * cos;
|
|
281
|
+
return `${pos.x + rx},${pos.y + ry}`;
|
|
282
|
+
})
|
|
283
|
+
.join(' ');
|
|
284
|
+
}
|
|
285
|
+
const blockArrowBehavior = {
|
|
286
|
+
kind: 'blockArrow',
|
|
287
|
+
tagName: 'polygon',
|
|
288
|
+
applyGeometry(el, shape, pos) {
|
|
289
|
+
el.setAttribute('points', blockArrowPoints(shape, pos));
|
|
290
|
+
},
|
|
291
|
+
svgMarkup(shape, pos, attrs) {
|
|
292
|
+
const pts = blockArrowPoints(shape, pos);
|
|
293
|
+
return `<polygon points="${pts}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
294
|
+
},
|
|
295
|
+
anchorBoundary(pos, target, shape) {
|
|
296
|
+
const dx = target.x - pos.x;
|
|
297
|
+
const dy = target.y - pos.y;
|
|
298
|
+
if (dx === 0 && dy === 0)
|
|
299
|
+
return { x: pos.x, y: pos.y };
|
|
300
|
+
const dir = shape.direction ?? 'right';
|
|
301
|
+
const hw = dir === 'up' || dir === 'down' ? shape.headWidth / 2 : shape.length / 2;
|
|
302
|
+
const hh = dir === 'up' || dir === 'down' ? shape.length / 2 : shape.headWidth / 2;
|
|
303
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
304
|
+
return {
|
|
305
|
+
x: pos.x + dx * scale,
|
|
306
|
+
y: pos.y + dy * scale,
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
function calloutPathD(shape, pos) {
|
|
311
|
+
const hw = shape.w / 2;
|
|
312
|
+
const hh = shape.h / 2;
|
|
313
|
+
const r = Math.min(shape.rx ?? 0, hw, hh);
|
|
314
|
+
const side = shape.pointerSide ?? 'bottom';
|
|
315
|
+
const pH = shape.pointerHeight ?? Math.round(shape.h * 0.25);
|
|
316
|
+
const pW = shape.pointerWidth ?? Math.round(shape.w * 0.2);
|
|
317
|
+
const pp = shape.pointerPosition ?? 0.3;
|
|
318
|
+
const left = pos.x - hw;
|
|
319
|
+
const right = pos.x + hw;
|
|
320
|
+
const top = pos.y - hh;
|
|
321
|
+
const bottom = pos.y + hh;
|
|
322
|
+
// pointer base coords along the side (0..sideLen)
|
|
323
|
+
const segments = [];
|
|
324
|
+
const arc = (cx, cy, startAngle) => {
|
|
325
|
+
if (r === 0)
|
|
326
|
+
return '';
|
|
327
|
+
const s = startAngle;
|
|
328
|
+
const e = s + Math.PI / 2;
|
|
329
|
+
const ex = cx + r * Math.cos(e);
|
|
330
|
+
const ey = cy + r * Math.sin(e);
|
|
331
|
+
return `A ${r} ${r} 0 0 1 ${ex} ${ey}`;
|
|
332
|
+
};
|
|
333
|
+
// build CW from top-left
|
|
334
|
+
// top-left corner
|
|
335
|
+
segments.push(`M ${left + r} ${top}`);
|
|
336
|
+
// top edge
|
|
337
|
+
if (side === 'top') {
|
|
338
|
+
const sideLen = shape.w - 2 * r;
|
|
339
|
+
const b1 = left + r + sideLen * pp;
|
|
340
|
+
const b2 = b1 + pW;
|
|
341
|
+
segments.push(`L ${b1} ${top}`);
|
|
342
|
+
segments.push(`L ${(b1 + b2) / 2} ${top - pH}`);
|
|
343
|
+
segments.push(`L ${Math.min(b2, right - r)} ${top}`);
|
|
344
|
+
}
|
|
345
|
+
segments.push(`L ${right - r} ${top}`);
|
|
346
|
+
// top-right corner
|
|
347
|
+
segments.push(arc(right - r, top + r, -Math.PI / 2));
|
|
348
|
+
// right edge
|
|
349
|
+
if (side === 'right') {
|
|
350
|
+
const sideLen = shape.h - 2 * r;
|
|
351
|
+
const b1 = top + r + sideLen * pp;
|
|
352
|
+
const b2 = b1 + pW;
|
|
353
|
+
segments.push(`L ${right} ${b1}`);
|
|
354
|
+
segments.push(`L ${right + pH} ${(b1 + b2) / 2}`);
|
|
355
|
+
segments.push(`L ${right} ${Math.min(b2, bottom - r)}`);
|
|
356
|
+
}
|
|
357
|
+
segments.push(`L ${right} ${bottom - r}`);
|
|
358
|
+
// bottom-right corner
|
|
359
|
+
segments.push(arc(right - r, bottom - r, 0));
|
|
360
|
+
// bottom edge
|
|
361
|
+
if (side === 'bottom') {
|
|
362
|
+
const sideLen = shape.w - 2 * r;
|
|
363
|
+
const b2 = right - r - sideLen * pp;
|
|
364
|
+
const b1 = b2 - pW;
|
|
365
|
+
segments.push(`L ${b2} ${bottom}`);
|
|
366
|
+
segments.push(`L ${(b1 + b2) / 2} ${bottom + pH}`);
|
|
367
|
+
segments.push(`L ${Math.max(b1, left + r)} ${bottom}`);
|
|
368
|
+
}
|
|
369
|
+
segments.push(`L ${left + r} ${bottom}`);
|
|
370
|
+
// bottom-left corner
|
|
371
|
+
segments.push(arc(left + r, bottom - r, Math.PI / 2));
|
|
372
|
+
// left edge
|
|
373
|
+
if (side === 'left') {
|
|
374
|
+
const sideLen = shape.h - 2 * r;
|
|
375
|
+
const b2 = bottom - r - sideLen * pp;
|
|
376
|
+
const b1 = b2 - pW;
|
|
377
|
+
segments.push(`L ${left} ${b2}`);
|
|
378
|
+
segments.push(`L ${left - pH} ${(b1 + b2) / 2}`);
|
|
379
|
+
segments.push(`L ${left} ${Math.max(b1, top + r)}`);
|
|
380
|
+
}
|
|
381
|
+
segments.push(`L ${left} ${top + r}`);
|
|
382
|
+
// close back to top-left (arc for last corner)
|
|
383
|
+
segments.push(arc(left + r, top + r, Math.PI));
|
|
384
|
+
segments.push('Z');
|
|
385
|
+
return segments.filter(Boolean).join(' ');
|
|
386
|
+
}
|
|
387
|
+
const calloutBehavior = {
|
|
388
|
+
kind: 'callout',
|
|
389
|
+
tagName: 'path',
|
|
390
|
+
applyGeometry(el, shape, pos) {
|
|
391
|
+
el.setAttribute('d', calloutPathD(shape, pos));
|
|
392
|
+
},
|
|
393
|
+
svgMarkup(shape, pos, attrs) {
|
|
394
|
+
const d = calloutPathD(shape, pos);
|
|
395
|
+
return `<path d="${d}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
396
|
+
},
|
|
397
|
+
anchorBoundary(pos, target, shape) {
|
|
398
|
+
const dx = target.x - pos.x;
|
|
399
|
+
const dy = target.y - pos.y;
|
|
400
|
+
if (dx === 0 && dy === 0)
|
|
401
|
+
return { x: pos.x, y: pos.y };
|
|
402
|
+
const hw = shape.w / 2;
|
|
403
|
+
const hh = shape.h / 2;
|
|
404
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
405
|
+
return {
|
|
406
|
+
x: pos.x + dx * scale,
|
|
407
|
+
y: pos.y + dy * scale,
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
/**
|
|
412
|
+
* Compute the SVG path for a cloud shape that fits within a w×h bounding box
|
|
413
|
+
* centered at `pos`. The outline is built from 6 cubic Bézier bumps.
|
|
414
|
+
*/
|
|
415
|
+
function cloudPathD(shape, pos) {
|
|
416
|
+
const hw = shape.w / 2;
|
|
417
|
+
const hh = shape.h / 2;
|
|
418
|
+
const cx = pos.x;
|
|
419
|
+
const cy = pos.y;
|
|
420
|
+
// 6 control-point "bumps" expressed as fractions of half-width / half-height.
|
|
421
|
+
// Each bump: [startX, startY, cp1x, cp1y, cp2x, cp2y, endX, endY]
|
|
422
|
+
const bumps = [
|
|
423
|
+
[-0.35, -0.85, -0.75, -1.2, -1.1, -0.5, -0.95, -0.2],
|
|
424
|
+
[-0.95, -0.2, -1.2, 0.3, -0.9, 0.9, -0.45, 0.85],
|
|
425
|
+
[-0.45, 0.85, -0.15, 1.15, 0.35, 1.15, 0.55, 0.8],
|
|
426
|
+
[0.55, 0.8, 0.85, 0.95, 1.15, 0.45, 1.0, 0.05],
|
|
427
|
+
[1.0, 0.05, 1.2, -0.45, 0.85, -0.95, 0.4, -0.85],
|
|
428
|
+
[0.4, -0.85, 0.05, -1.2, -0.45, -1.1, -0.35, -0.85],
|
|
429
|
+
];
|
|
430
|
+
const parts = [
|
|
431
|
+
`M ${cx + bumps[0][0] * hw} ${cy + bumps[0][1] * hh}`,
|
|
432
|
+
];
|
|
433
|
+
for (const [, , c1x, c1y, c2x, c2y, ex, ey] of bumps) {
|
|
434
|
+
parts.push(`C ${cx + c1x * hw} ${cy + c1y * hh} ${cx + c2x * hw} ${cy + c2y * hh} ${cx + ex * hw} ${cy + ey * hh}`);
|
|
435
|
+
}
|
|
436
|
+
parts.push('Z');
|
|
437
|
+
return parts.join(' ');
|
|
438
|
+
}
|
|
439
|
+
const cloudBehavior = {
|
|
440
|
+
kind: 'cloud',
|
|
441
|
+
tagName: 'path',
|
|
442
|
+
applyGeometry(el, shape, pos) {
|
|
443
|
+
el.setAttribute('d', cloudPathD(shape, pos));
|
|
444
|
+
},
|
|
445
|
+
svgMarkup(shape, pos, attrs) {
|
|
446
|
+
const d = cloudPathD(shape, pos);
|
|
447
|
+
return `<path d="${d}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
448
|
+
},
|
|
449
|
+
anchorBoundary(pos, target, shape) {
|
|
450
|
+
// Bounding-ellipse approximation
|
|
451
|
+
const dx = target.x - pos.x;
|
|
452
|
+
const dy = target.y - pos.y;
|
|
453
|
+
if (dx === 0 && dy === 0)
|
|
454
|
+
return { x: pos.x, y: pos.y };
|
|
455
|
+
const a = shape.w / 2;
|
|
456
|
+
const b = shape.h / 2;
|
|
457
|
+
const denom = Math.sqrt(a * a * dy * dy + b * b * dx * dx) || 1;
|
|
458
|
+
return {
|
|
459
|
+
x: pos.x + (a * b * dx) / denom,
|
|
460
|
+
y: pos.y + (a * b * dy) / denom,
|
|
461
|
+
};
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
function crossPoints(shape, pos) {
|
|
465
|
+
const hs = shape.size / 2;
|
|
466
|
+
const bw = (shape.barWidth ?? Math.round(shape.size / 3)) / 2;
|
|
467
|
+
// 12-vertex plus sign, CW from top-left of vertical bar
|
|
468
|
+
return [
|
|
469
|
+
`${pos.x - bw},${pos.y - hs}`,
|
|
470
|
+
`${pos.x + bw},${pos.y - hs}`,
|
|
471
|
+
`${pos.x + bw},${pos.y - bw}`,
|
|
472
|
+
`${pos.x + hs},${pos.y - bw}`,
|
|
473
|
+
`${pos.x + hs},${pos.y + bw}`,
|
|
474
|
+
`${pos.x + bw},${pos.y + bw}`,
|
|
475
|
+
`${pos.x + bw},${pos.y + hs}`,
|
|
476
|
+
`${pos.x - bw},${pos.y + hs}`,
|
|
477
|
+
`${pos.x - bw},${pos.y + bw}`,
|
|
478
|
+
`${pos.x - hs},${pos.y + bw}`,
|
|
479
|
+
`${pos.x - hs},${pos.y - bw}`,
|
|
480
|
+
`${pos.x - bw},${pos.y - bw}`,
|
|
481
|
+
].join(' ');
|
|
482
|
+
}
|
|
483
|
+
const crossBehavior = {
|
|
484
|
+
kind: 'cross',
|
|
485
|
+
tagName: 'polygon',
|
|
486
|
+
applyGeometry(el, shape, pos) {
|
|
487
|
+
el.setAttribute('points', crossPoints(shape, pos));
|
|
488
|
+
},
|
|
489
|
+
svgMarkup(shape, pos, attrs) {
|
|
490
|
+
const pts = crossPoints(shape, pos);
|
|
491
|
+
return `<polygon points="${pts}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
492
|
+
},
|
|
493
|
+
anchorBoundary(pos, target, shape) {
|
|
494
|
+
const dx = target.x - pos.x;
|
|
495
|
+
const dy = target.y - pos.y;
|
|
496
|
+
if (dx === 0 && dy === 0)
|
|
497
|
+
return { x: pos.x, y: pos.y };
|
|
498
|
+
const hs = shape.size / 2;
|
|
499
|
+
const scale = Math.min(hs / Math.abs(dx || 1e-6), hs / Math.abs(dy || 1e-6));
|
|
500
|
+
return {
|
|
501
|
+
x: pos.x + dx * scale,
|
|
502
|
+
y: pos.y + dy * scale,
|
|
503
|
+
};
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
function cubeVertices(shape, pos) {
|
|
507
|
+
const hw = shape.w / 2;
|
|
508
|
+
const hh = shape.h / 2;
|
|
509
|
+
const d = shape.depth ?? Math.round(shape.w * 0.2);
|
|
510
|
+
// Front face centered at pos so labels align naturally
|
|
511
|
+
const ftl = { x: pos.x - hw, y: pos.y - hh };
|
|
512
|
+
const ftr = { x: pos.x + hw, y: pos.y - hh };
|
|
513
|
+
const fbr = { x: pos.x + hw, y: pos.y + hh };
|
|
514
|
+
const fbl = { x: pos.x - hw, y: pos.y + hh };
|
|
515
|
+
// Back top corners (shifted by +d in x, -d in y)
|
|
516
|
+
const btl = { x: ftl.x + d, y: ftl.y - d };
|
|
517
|
+
const btr = { x: ftr.x + d, y: ftr.y - d };
|
|
518
|
+
// Back bottom-right (for right face)
|
|
519
|
+
const bbr = { x: fbr.x + d, y: fbr.y - d };
|
|
520
|
+
return { ftl, ftr, fbr, fbl, btl, btr, bbr, d };
|
|
521
|
+
}
|
|
522
|
+
function polyStr(...pts) {
|
|
523
|
+
return pts.map((p) => `${p.x},${p.y}`).join(' ');
|
|
524
|
+
}
|
|
525
|
+
const cubeBehavior = {
|
|
526
|
+
kind: 'cube',
|
|
527
|
+
tagName: 'g',
|
|
528
|
+
applyGeometry(el, shape, pos) {
|
|
529
|
+
const { ftl, ftr, fbr, fbl, btl, btr, bbr } = cubeVertices(shape, pos);
|
|
530
|
+
let front = el.querySelector('[data-viz-cube="front"]');
|
|
531
|
+
if (!front) {
|
|
532
|
+
front = document.createElementNS(SVG_NS, 'polygon');
|
|
533
|
+
front.setAttribute('data-viz-cube', 'front');
|
|
534
|
+
el.appendChild(front);
|
|
535
|
+
}
|
|
536
|
+
front.setAttribute('points', polyStr(ftl, ftr, fbr, fbl));
|
|
537
|
+
let top = el.querySelector('[data-viz-cube="top"]');
|
|
538
|
+
if (!top) {
|
|
539
|
+
top = document.createElementNS(SVG_NS, 'polygon');
|
|
540
|
+
top.setAttribute('data-viz-cube', 'top');
|
|
541
|
+
el.appendChild(top);
|
|
542
|
+
}
|
|
543
|
+
top.setAttribute('points', polyStr(ftl, ftr, btr, btl));
|
|
544
|
+
top.style.filter = 'brightness(0.85)';
|
|
545
|
+
let right = el.querySelector('[data-viz-cube="right"]');
|
|
546
|
+
if (!right) {
|
|
547
|
+
right = document.createElementNS(SVG_NS, 'polygon');
|
|
548
|
+
right.setAttribute('data-viz-cube', 'right');
|
|
549
|
+
el.appendChild(right);
|
|
550
|
+
}
|
|
551
|
+
right.setAttribute('points', polyStr(ftr, btr, bbr, fbr));
|
|
552
|
+
right.style.filter = 'brightness(0.7)';
|
|
553
|
+
},
|
|
554
|
+
svgMarkup(shape, pos, attrs) {
|
|
555
|
+
const { ftl, ftr, fbr, fbl, btl, btr, bbr } = cubeVertices(shape, pos);
|
|
556
|
+
const end = '</g>';
|
|
557
|
+
return (`<g class="viz-node-shape" data-viz-role="node-shape"${attrs}>` +
|
|
558
|
+
`<polygon points="${polyStr(ftl, ftr, fbr, fbl)}" data-viz-cube="front"/>` +
|
|
559
|
+
`<polygon points="${polyStr(ftl, ftr, btr, btl)}" data-viz-cube="top" style="filter:brightness(0.85)"/>` +
|
|
560
|
+
`<polygon points="${polyStr(ftr, btr, bbr, fbr)}" data-viz-cube="right" style="filter:brightness(0.7)"/>` +
|
|
561
|
+
end);
|
|
562
|
+
},
|
|
563
|
+
anchorBoundary(pos, target, shape) {
|
|
564
|
+
const dx = target.x - pos.x;
|
|
565
|
+
const dy = target.y - pos.y;
|
|
566
|
+
if (dx === 0 && dy === 0)
|
|
567
|
+
return { x: pos.x, y: pos.y };
|
|
568
|
+
const d = shape.depth ?? Math.round(shape.w * 0.2);
|
|
569
|
+
const hw = shape.w / 2 + d / 2;
|
|
570
|
+
const hh = shape.h / 2 + d / 2;
|
|
571
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
572
|
+
return {
|
|
573
|
+
x: pos.x + dx * scale,
|
|
574
|
+
y: pos.y + dy * scale,
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
};
|
|
578
|
+
const pathBehavior = {
|
|
579
|
+
kind: 'path',
|
|
580
|
+
tagName: 'path',
|
|
581
|
+
applyGeometry(el, shape, pos) {
|
|
582
|
+
el.setAttribute('d', shape.d);
|
|
583
|
+
el.setAttribute('transform', `translate(${pos.x - shape.w / 2},${pos.y - shape.h / 2})`);
|
|
584
|
+
},
|
|
585
|
+
svgMarkup(shape, pos, attrs) {
|
|
586
|
+
const tx = pos.x - shape.w / 2;
|
|
587
|
+
const ty = pos.y - shape.h / 2;
|
|
588
|
+
return `<path d="${shape.d}" transform="translate(${tx},${ty})" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
589
|
+
},
|
|
590
|
+
anchorBoundary(pos, target, shape) {
|
|
591
|
+
const dx = target.x - pos.x;
|
|
592
|
+
const dy = target.y - pos.y;
|
|
593
|
+
if (dx === 0 && dy === 0)
|
|
594
|
+
return { x: pos.x, y: pos.y };
|
|
595
|
+
const hw = shape.w / 2;
|
|
596
|
+
const hh = shape.h / 2;
|
|
597
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
598
|
+
return {
|
|
599
|
+
x: pos.x + dx * scale,
|
|
600
|
+
y: pos.y + dy * scale,
|
|
601
|
+
};
|
|
602
|
+
},
|
|
603
|
+
};
|
|
604
|
+
function documentPathD(shape, pos) {
|
|
605
|
+
const hw = shape.w / 2;
|
|
606
|
+
const hh = shape.h / 2;
|
|
607
|
+
const wh = shape.waveHeight ?? Math.round(shape.h * 0.1);
|
|
608
|
+
const x0 = pos.x - hw;
|
|
609
|
+
const x1 = pos.x + hw;
|
|
610
|
+
const y0 = pos.y - hh;
|
|
611
|
+
const y1 = pos.y + hh - wh;
|
|
612
|
+
// Top-left → top-right → bottom-right → wavy bottom → close
|
|
613
|
+
return (`M ${x0} ${y0}` +
|
|
614
|
+
` H ${x1}` +
|
|
615
|
+
` V ${y1}` +
|
|
616
|
+
` C ${x1 - hw * 0.5} ${y1 + wh * 2}, ${x0 + hw * 0.5} ${y1 - wh}, ${x0} ${y1}` +
|
|
617
|
+
' Z');
|
|
618
|
+
}
|
|
619
|
+
const documentBehavior = {
|
|
620
|
+
kind: 'document',
|
|
621
|
+
tagName: 'path',
|
|
622
|
+
applyGeometry(el, shape, pos) {
|
|
623
|
+
el.setAttribute('d', documentPathD(shape, pos));
|
|
624
|
+
},
|
|
625
|
+
svgMarkup(shape, pos, attrs) {
|
|
626
|
+
const d = documentPathD(shape, pos);
|
|
627
|
+
return `<path d="${d}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
628
|
+
},
|
|
629
|
+
anchorBoundary(pos, target, shape) {
|
|
630
|
+
const dx = target.x - pos.x;
|
|
631
|
+
const dy = target.y - pos.y;
|
|
632
|
+
if (dx === 0 && dy === 0)
|
|
633
|
+
return { x: pos.x, y: pos.y };
|
|
634
|
+
const hw = shape.w / 2;
|
|
635
|
+
const hh = shape.h / 2;
|
|
636
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
637
|
+
return {
|
|
638
|
+
x: pos.x + dx * scale,
|
|
639
|
+
y: pos.y + dy * scale,
|
|
640
|
+
};
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
function noteVertices(shape, pos) {
|
|
644
|
+
const hw = shape.w / 2;
|
|
645
|
+
const hh = shape.h / 2;
|
|
646
|
+
const f = shape.foldSize ?? 15;
|
|
647
|
+
const x0 = pos.x - hw;
|
|
648
|
+
const x1 = pos.x + hw;
|
|
649
|
+
const y0 = pos.y - hh;
|
|
650
|
+
const y1 = pos.y + hh;
|
|
651
|
+
return { x0, x1, y0, y1, f };
|
|
652
|
+
}
|
|
653
|
+
const noteBehavior = {
|
|
654
|
+
kind: 'note',
|
|
655
|
+
tagName: 'g',
|
|
656
|
+
applyGeometry(el, shape, pos) {
|
|
657
|
+
const { x0, x1, y0, y1, f } = noteVertices(shape, pos);
|
|
658
|
+
let body = el.querySelector('[data-viz-note="body"]');
|
|
659
|
+
if (!body) {
|
|
660
|
+
body = document.createElementNS(SVG_NS, 'polygon');
|
|
661
|
+
body.setAttribute('data-viz-note', 'body');
|
|
662
|
+
el.appendChild(body);
|
|
663
|
+
}
|
|
664
|
+
body.setAttribute('points', `${x0},${y0} ${x1 - f},${y0} ${x1},${y0 + f} ${x1},${y1} ${x0},${y1}`);
|
|
665
|
+
let fold = el.querySelector('[data-viz-note="fold"]');
|
|
666
|
+
if (!fold) {
|
|
667
|
+
fold = document.createElementNS(SVG_NS, 'polygon');
|
|
668
|
+
fold.setAttribute('data-viz-note', 'fold');
|
|
669
|
+
el.appendChild(fold);
|
|
670
|
+
}
|
|
671
|
+
fold.setAttribute('points', `${x1 - f},${y0} ${x1 - f},${y0 + f} ${x1},${y0 + f}`);
|
|
672
|
+
fold.style.filter = 'brightness(0.8)';
|
|
673
|
+
},
|
|
674
|
+
svgMarkup(shape, pos, attrs) {
|
|
675
|
+
const { x0, x1, y0, y1, f } = noteVertices(shape, pos);
|
|
676
|
+
const bodyPts = `${x0},${y0} ${x1 - f},${y0} ${x1},${y0 + f} ${x1},${y1} ${x0},${y1}`;
|
|
677
|
+
const foldPts = `${x1 - f},${y0} ${x1 - f},${y0 + f} ${x1},${y0 + f}`;
|
|
678
|
+
const end = '</g>';
|
|
679
|
+
return (`<g class="viz-node-shape" data-viz-role="node-shape"${attrs}>` +
|
|
680
|
+
`<polygon points="${bodyPts}" data-viz-note="body"/>` +
|
|
681
|
+
`<polygon points="${foldPts}" data-viz-note="fold" style="filter:brightness(0.8)"/>` +
|
|
682
|
+
end);
|
|
683
|
+
},
|
|
684
|
+
anchorBoundary(pos, target, shape) {
|
|
685
|
+
const dx = target.x - pos.x;
|
|
686
|
+
const dy = target.y - pos.y;
|
|
687
|
+
if (dx === 0 && dy === 0)
|
|
688
|
+
return { x: pos.x, y: pos.y };
|
|
689
|
+
const hw = shape.w / 2;
|
|
690
|
+
const hh = shape.h / 2;
|
|
691
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
692
|
+
return {
|
|
693
|
+
x: pos.x + dx * scale,
|
|
694
|
+
y: pos.y + dy * scale,
|
|
695
|
+
};
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
function parallelogramPoints(shape, pos) {
|
|
699
|
+
const hw = shape.w / 2;
|
|
700
|
+
const hh = shape.h / 2;
|
|
701
|
+
const sk = shape.skew ?? Math.round(shape.w * 0.2);
|
|
702
|
+
const half = sk / 2;
|
|
703
|
+
return [
|
|
704
|
+
`${pos.x - hw - half},${pos.y + hh}`,
|
|
705
|
+
`${pos.x + hw - half},${pos.y + hh}`,
|
|
706
|
+
`${pos.x + hw + half},${pos.y - hh}`,
|
|
707
|
+
`${pos.x - hw + half},${pos.y - hh}`,
|
|
708
|
+
].join(' ');
|
|
709
|
+
}
|
|
710
|
+
const parallelogramBehavior = {
|
|
711
|
+
kind: 'parallelogram',
|
|
712
|
+
tagName: 'polygon',
|
|
713
|
+
applyGeometry(el, shape, pos) {
|
|
714
|
+
el.setAttribute('points', parallelogramPoints(shape, pos));
|
|
715
|
+
},
|
|
716
|
+
svgMarkup(shape, pos, attrs) {
|
|
717
|
+
const pts = parallelogramPoints(shape, pos);
|
|
718
|
+
return `<polygon points="${pts}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
719
|
+
},
|
|
720
|
+
anchorBoundary(pos, target, shape) {
|
|
721
|
+
const dx = target.x - pos.x;
|
|
722
|
+
const dy = target.y - pos.y;
|
|
723
|
+
if (dx === 0 && dy === 0)
|
|
724
|
+
return { x: pos.x, y: pos.y };
|
|
725
|
+
const sk = shape.skew ?? Math.round(shape.w * 0.2);
|
|
726
|
+
const hw = shape.w / 2 + sk / 2;
|
|
727
|
+
const hh = shape.h / 2;
|
|
728
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
729
|
+
return {
|
|
730
|
+
x: pos.x + dx * scale,
|
|
731
|
+
y: pos.y + dy * scale,
|
|
732
|
+
};
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
function starPoints(shape, pos) {
|
|
736
|
+
const n = shape.points;
|
|
737
|
+
const outer = shape.outerR;
|
|
738
|
+
const inner = shape.innerR ?? Math.round(outer * 0.4);
|
|
739
|
+
const verts = [];
|
|
740
|
+
for (let i = 0; i < n * 2; i++) {
|
|
741
|
+
const r = i % 2 === 0 ? outer : inner;
|
|
742
|
+
const angle = (Math.PI * i) / n - Math.PI / 2;
|
|
743
|
+
verts.push(`${pos.x + r * Math.cos(angle)},${pos.y + r * Math.sin(angle)}`);
|
|
744
|
+
}
|
|
745
|
+
return verts.join(' ');
|
|
746
|
+
}
|
|
747
|
+
const starBehavior = {
|
|
748
|
+
kind: 'star',
|
|
749
|
+
tagName: 'polygon',
|
|
750
|
+
applyGeometry(el, shape, pos) {
|
|
751
|
+
el.setAttribute('points', starPoints(shape, pos));
|
|
752
|
+
},
|
|
753
|
+
svgMarkup(shape, pos, attrs) {
|
|
754
|
+
const pts = starPoints(shape, pos);
|
|
755
|
+
return `<polygon points="${pts}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
756
|
+
},
|
|
757
|
+
anchorBoundary(pos, target, shape) {
|
|
758
|
+
const dx = target.x - pos.x;
|
|
759
|
+
const dy = target.y - pos.y;
|
|
760
|
+
if (dx === 0 && dy === 0)
|
|
761
|
+
return { x: pos.x, y: pos.y };
|
|
762
|
+
const r = shape.outerR;
|
|
763
|
+
const scale = r / Math.sqrt(dx * dx + dy * dy);
|
|
764
|
+
return {
|
|
765
|
+
x: pos.x + dx * scale,
|
|
766
|
+
y: pos.y + dy * scale,
|
|
767
|
+
};
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
function trapezoidPoints(shape, pos) {
|
|
771
|
+
const htw = shape.topW / 2;
|
|
772
|
+
const hbw = shape.bottomW / 2;
|
|
773
|
+
const hh = shape.h / 2;
|
|
774
|
+
return [
|
|
775
|
+
`${pos.x - htw},${pos.y - hh}`,
|
|
776
|
+
`${pos.x + htw},${pos.y - hh}`,
|
|
777
|
+
`${pos.x + hbw},${pos.y + hh}`,
|
|
778
|
+
`${pos.x - hbw},${pos.y + hh}`,
|
|
779
|
+
].join(' ');
|
|
780
|
+
}
|
|
781
|
+
const trapezoidBehavior = {
|
|
782
|
+
kind: 'trapezoid',
|
|
783
|
+
tagName: 'polygon',
|
|
784
|
+
applyGeometry(el, shape, pos) {
|
|
785
|
+
el.setAttribute('points', trapezoidPoints(shape, pos));
|
|
786
|
+
},
|
|
787
|
+
svgMarkup(shape, pos, attrs) {
|
|
788
|
+
const pts = trapezoidPoints(shape, pos);
|
|
789
|
+
return `<polygon points="${pts}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
790
|
+
},
|
|
791
|
+
anchorBoundary(pos, target, shape) {
|
|
792
|
+
const dx = target.x - pos.x;
|
|
793
|
+
const dy = target.y - pos.y;
|
|
794
|
+
if (dx === 0 && dy === 0)
|
|
795
|
+
return { x: pos.x, y: pos.y };
|
|
796
|
+
const hw = Math.max(shape.topW, shape.bottomW) / 2;
|
|
797
|
+
const hh = shape.h / 2;
|
|
798
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
799
|
+
return {
|
|
800
|
+
x: pos.x + dx * scale,
|
|
801
|
+
y: pos.y + dy * scale,
|
|
802
|
+
};
|
|
803
|
+
},
|
|
804
|
+
};
|
|
805
|
+
function trianglePoints(shape, pos) {
|
|
806
|
+
const hw = shape.w / 2;
|
|
807
|
+
const hh = shape.h / 2;
|
|
808
|
+
const dir = shape.direction ?? 'up';
|
|
809
|
+
switch (dir) {
|
|
810
|
+
case 'up':
|
|
811
|
+
return [
|
|
812
|
+
`${pos.x},${pos.y - hh}`,
|
|
813
|
+
`${pos.x + hw},${pos.y + hh}`,
|
|
814
|
+
`${pos.x - hw},${pos.y + hh}`,
|
|
815
|
+
].join(' ');
|
|
816
|
+
case 'down':
|
|
817
|
+
return [
|
|
818
|
+
`${pos.x},${pos.y + hh}`,
|
|
819
|
+
`${pos.x - hw},${pos.y - hh}`,
|
|
820
|
+
`${pos.x + hw},${pos.y - hh}`,
|
|
821
|
+
].join(' ');
|
|
822
|
+
case 'left':
|
|
823
|
+
return [
|
|
824
|
+
`${pos.x - hw},${pos.y}`,
|
|
825
|
+
`${pos.x + hw},${pos.y - hh}`,
|
|
826
|
+
`${pos.x + hw},${pos.y + hh}`,
|
|
827
|
+
].join(' ');
|
|
828
|
+
case 'right':
|
|
829
|
+
return [
|
|
830
|
+
`${pos.x + hw},${pos.y}`,
|
|
831
|
+
`${pos.x - hw},${pos.y + hh}`,
|
|
832
|
+
`${pos.x - hw},${pos.y - hh}`,
|
|
833
|
+
].join(' ');
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const triangleBehavior = {
|
|
837
|
+
kind: 'triangle',
|
|
838
|
+
tagName: 'polygon',
|
|
839
|
+
applyGeometry(el, shape, pos) {
|
|
840
|
+
el.setAttribute('points', trianglePoints(shape, pos));
|
|
841
|
+
},
|
|
842
|
+
svgMarkup(shape, pos, attrs) {
|
|
843
|
+
const pts = trianglePoints(shape, pos);
|
|
844
|
+
return `<polygon points="${pts}" class="viz-node-shape" data-viz-role="node-shape"${attrs} />`;
|
|
845
|
+
},
|
|
846
|
+
anchorBoundary(pos, target, shape) {
|
|
847
|
+
const dx = target.x - pos.x;
|
|
848
|
+
const dy = target.y - pos.y;
|
|
849
|
+
if (dx === 0 && dy === 0)
|
|
850
|
+
return { x: pos.x, y: pos.y };
|
|
851
|
+
const hw = shape.w / 2;
|
|
852
|
+
const hh = shape.h / 2;
|
|
853
|
+
const scale = Math.min(hw / Math.abs(dx || 1e-6), hh / Math.abs(dy || 1e-6));
|
|
854
|
+
return {
|
|
855
|
+
x: pos.x + dx * scale,
|
|
856
|
+
y: pos.y + dy * scale,
|
|
857
|
+
};
|
|
858
|
+
},
|
|
859
|
+
};
|
|
90
860
|
const shapeBehaviorRegistry = {
|
|
91
861
|
circle: circleBehavior,
|
|
92
862
|
rect: rectBehavior,
|
|
93
863
|
diamond: diamondBehavior,
|
|
864
|
+
cylinder: cylinderBehavior,
|
|
865
|
+
hexagon: hexagonBehavior,
|
|
866
|
+
ellipse: ellipseBehavior,
|
|
867
|
+
arc: arcBehavior,
|
|
868
|
+
blockArrow: blockArrowBehavior,
|
|
869
|
+
callout: calloutBehavior,
|
|
870
|
+
cloud: cloudBehavior,
|
|
871
|
+
cross: crossBehavior,
|
|
872
|
+
cube: cubeBehavior,
|
|
873
|
+
path: pathBehavior,
|
|
874
|
+
document: documentBehavior,
|
|
875
|
+
note: noteBehavior,
|
|
876
|
+
parallelogram: parallelogramBehavior,
|
|
877
|
+
star: starBehavior,
|
|
878
|
+
trapezoid: trapezoidBehavior,
|
|
879
|
+
triangle: triangleBehavior,
|
|
94
880
|
};
|
|
95
881
|
export function getShapeBehavior(shape) {
|
|
96
882
|
return shapeBehaviorRegistry[shape.kind];
|
|
@@ -111,3 +897,226 @@ export function computeNodeAnchor(node, target, anchor) {
|
|
|
111
897
|
const behavior = getShapeBehavior(node.shape);
|
|
112
898
|
return behavior.anchorBoundary(pos, target, node.shape);
|
|
113
899
|
}
|
|
900
|
+
// ── Connection Ports ────────────────────────────────────────────────────────
|
|
901
|
+
/**
|
|
902
|
+
* Return the default (implicit) ports for a node shape.
|
|
903
|
+
*
|
|
904
|
+
* Every shape provides at least `top`, `right`, `bottom`, `left`.
|
|
905
|
+
* Hexagons also include diagonal vertices.
|
|
906
|
+
* Default ports are **offsets relative to the node center**.
|
|
907
|
+
*/
|
|
908
|
+
export function getDefaultPorts(shape) {
|
|
909
|
+
switch (shape.kind) {
|
|
910
|
+
case 'circle':
|
|
911
|
+
return [
|
|
912
|
+
{ id: 'top', offset: { x: 0, y: -shape.r }, direction: 270 },
|
|
913
|
+
{ id: 'right', offset: { x: shape.r, y: 0 }, direction: 0 },
|
|
914
|
+
{ id: 'bottom', offset: { x: 0, y: shape.r }, direction: 90 },
|
|
915
|
+
{ id: 'left', offset: { x: -shape.r, y: 0 }, direction: 180 },
|
|
916
|
+
];
|
|
917
|
+
case 'rect':
|
|
918
|
+
return [
|
|
919
|
+
{ id: 'top', offset: { x: 0, y: -shape.h / 2 }, direction: 270 },
|
|
920
|
+
{ id: 'right', offset: { x: shape.w / 2, y: 0 }, direction: 0 },
|
|
921
|
+
{ id: 'bottom', offset: { x: 0, y: shape.h / 2 }, direction: 90 },
|
|
922
|
+
{ id: 'left', offset: { x: -shape.w / 2, y: 0 }, direction: 180 },
|
|
923
|
+
];
|
|
924
|
+
case 'diamond':
|
|
925
|
+
return [
|
|
926
|
+
{ id: 'top', offset: { x: 0, y: -shape.h / 2 }, direction: 270 },
|
|
927
|
+
{ id: 'right', offset: { x: shape.w / 2, y: 0 }, direction: 0 },
|
|
928
|
+
{ id: 'bottom', offset: { x: 0, y: shape.h / 2 }, direction: 90 },
|
|
929
|
+
{ id: 'left', offset: { x: -shape.w / 2, y: 0 }, direction: 180 },
|
|
930
|
+
];
|
|
931
|
+
case 'ellipse':
|
|
932
|
+
return [
|
|
933
|
+
{ id: 'top', offset: { x: 0, y: -shape.ry }, direction: 270 },
|
|
934
|
+
{ id: 'right', offset: { x: shape.rx, y: 0 }, direction: 0 },
|
|
935
|
+
{ id: 'bottom', offset: { x: 0, y: shape.ry }, direction: 90 },
|
|
936
|
+
{ id: 'left', offset: { x: -shape.rx, y: 0 }, direction: 180 },
|
|
937
|
+
];
|
|
938
|
+
case 'hexagon': {
|
|
939
|
+
const r = shape.r;
|
|
940
|
+
const orientation = shape.orientation ?? 'pointy';
|
|
941
|
+
if (orientation === 'pointy') {
|
|
942
|
+
// Pointy-top: vertices at top, top-right, bottom-right, bottom, bottom-left, top-left
|
|
943
|
+
const sin60 = r * Math.sin(Math.PI / 3); // ≈ 0.866r
|
|
944
|
+
const cos60 = r * Math.cos(Math.PI / 3); // = 0.5r
|
|
945
|
+
return [
|
|
946
|
+
{ id: 'top', offset: { x: 0, y: -r }, direction: 270 },
|
|
947
|
+
{ id: 'top-right', offset: { x: sin60, y: -cos60 }, direction: 330 },
|
|
948
|
+
{
|
|
949
|
+
id: 'bottom-right',
|
|
950
|
+
offset: { x: sin60, y: cos60 },
|
|
951
|
+
direction: 30,
|
|
952
|
+
},
|
|
953
|
+
{ id: 'bottom', offset: { x: 0, y: r }, direction: 90 },
|
|
954
|
+
{
|
|
955
|
+
id: 'bottom-left',
|
|
956
|
+
offset: { x: -sin60, y: cos60 },
|
|
957
|
+
direction: 150,
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
id: 'top-left',
|
|
961
|
+
offset: { x: -sin60, y: -cos60 },
|
|
962
|
+
direction: 210,
|
|
963
|
+
},
|
|
964
|
+
];
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
// Flat-top: vertices at right, bottom-right, bottom-left, left, top-left, top-right
|
|
968
|
+
const sin60 = r * Math.sin(Math.PI / 3);
|
|
969
|
+
const cos60 = r * Math.cos(Math.PI / 3);
|
|
970
|
+
return [
|
|
971
|
+
{ id: 'right', offset: { x: r, y: 0 }, direction: 0 },
|
|
972
|
+
{
|
|
973
|
+
id: 'bottom-right',
|
|
974
|
+
offset: { x: cos60, y: sin60 },
|
|
975
|
+
direction: 60,
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
id: 'bottom-left',
|
|
979
|
+
offset: { x: -cos60, y: sin60 },
|
|
980
|
+
direction: 120,
|
|
981
|
+
},
|
|
982
|
+
{ id: 'left', offset: { x: -r, y: 0 }, direction: 180 },
|
|
983
|
+
{
|
|
984
|
+
id: 'top-left',
|
|
985
|
+
offset: { x: -cos60, y: -sin60 },
|
|
986
|
+
direction: 240,
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
id: 'top-right',
|
|
990
|
+
offset: { x: cos60, y: -sin60 },
|
|
991
|
+
direction: 300,
|
|
992
|
+
},
|
|
993
|
+
];
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
case 'cylinder':
|
|
997
|
+
return [
|
|
998
|
+
{ id: 'top', offset: { x: 0, y: -shape.h / 2 }, direction: 270 },
|
|
999
|
+
{ id: 'right', offset: { x: shape.w / 2, y: 0 }, direction: 0 },
|
|
1000
|
+
{ id: 'bottom', offset: { x: 0, y: shape.h / 2 }, direction: 90 },
|
|
1001
|
+
{ id: 'left', offset: { x: -shape.w / 2, y: 0 }, direction: 180 },
|
|
1002
|
+
];
|
|
1003
|
+
case 'triangle': {
|
|
1004
|
+
const hw = shape.w / 2;
|
|
1005
|
+
const hh = shape.h / 2;
|
|
1006
|
+
const dir = shape.direction ?? 'up';
|
|
1007
|
+
// Three vertices based on direction
|
|
1008
|
+
switch (dir) {
|
|
1009
|
+
case 'up':
|
|
1010
|
+
return [
|
|
1011
|
+
{ id: 'top', offset: { x: 0, y: -hh }, direction: 270 },
|
|
1012
|
+
{ id: 'bottom-right', offset: { x: hw, y: hh }, direction: 30 },
|
|
1013
|
+
{ id: 'bottom-left', offset: { x: -hw, y: hh }, direction: 150 },
|
|
1014
|
+
{ id: 'bottom', offset: { x: 0, y: hh }, direction: 90 },
|
|
1015
|
+
];
|
|
1016
|
+
case 'down':
|
|
1017
|
+
return [
|
|
1018
|
+
{ id: 'top-left', offset: { x: -hw, y: -hh }, direction: 210 },
|
|
1019
|
+
{ id: 'top-right', offset: { x: hw, y: -hh }, direction: 330 },
|
|
1020
|
+
{ id: 'bottom', offset: { x: 0, y: hh }, direction: 90 },
|
|
1021
|
+
{ id: 'top', offset: { x: 0, y: -hh }, direction: 270 },
|
|
1022
|
+
];
|
|
1023
|
+
case 'left':
|
|
1024
|
+
return [
|
|
1025
|
+
{ id: 'left', offset: { x: -hw, y: 0 }, direction: 180 },
|
|
1026
|
+
{ id: 'top-right', offset: { x: hw, y: -hh }, direction: 330 },
|
|
1027
|
+
{ id: 'bottom-right', offset: { x: hw, y: hh }, direction: 30 },
|
|
1028
|
+
{ id: 'right', offset: { x: hw, y: 0 }, direction: 0 },
|
|
1029
|
+
];
|
|
1030
|
+
case 'right':
|
|
1031
|
+
return [
|
|
1032
|
+
{ id: 'top-left', offset: { x: -hw, y: -hh }, direction: 210 },
|
|
1033
|
+
{ id: 'right', offset: { x: hw, y: 0 }, direction: 0 },
|
|
1034
|
+
{ id: 'bottom-left', offset: { x: -hw, y: hh }, direction: 150 },
|
|
1035
|
+
{ id: 'left', offset: { x: -hw, y: 0 }, direction: 180 },
|
|
1036
|
+
];
|
|
1037
|
+
}
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
// Fallback for all remaining shapes: approximate as bounding-box midpoints
|
|
1041
|
+
default: {
|
|
1042
|
+
const bb = shapeBoundingBox(shape);
|
|
1043
|
+
return [
|
|
1044
|
+
{ id: 'top', offset: { x: 0, y: -bb.hh }, direction: 270 },
|
|
1045
|
+
{ id: 'right', offset: { x: bb.hw, y: 0 }, direction: 0 },
|
|
1046
|
+
{ id: 'bottom', offset: { x: 0, y: bb.hh }, direction: 90 },
|
|
1047
|
+
{ id: 'left', offset: { x: -bb.hw, y: 0 }, direction: 180 },
|
|
1048
|
+
];
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Get the effective ports for a node: explicit `node.ports` if set,
|
|
1054
|
+
* otherwise the shape's default ports.
|
|
1055
|
+
*/
|
|
1056
|
+
export function getNodePorts(node) {
|
|
1057
|
+
return node.ports ?? getDefaultPorts(node.shape);
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Find a port on a node by its id. Returns `undefined` if not found.
|
|
1061
|
+
*/
|
|
1062
|
+
export function findPort(node, portId) {
|
|
1063
|
+
return getNodePorts(node).find((p) => p.id === portId);
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Resolve a port to an absolute position (node center + port offset).
|
|
1067
|
+
* Returns `undefined` if the port id is not found.
|
|
1068
|
+
*/
|
|
1069
|
+
export function resolvePortPosition(node, portId) {
|
|
1070
|
+
const port = findPort(node, portId);
|
|
1071
|
+
if (!port)
|
|
1072
|
+
return undefined;
|
|
1073
|
+
const pos = effectivePos(node);
|
|
1074
|
+
return { x: pos.x + port.offset.x, y: pos.y + port.offset.y };
|
|
1075
|
+
}
|
|
1076
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
1077
|
+
/** Approximate half-width / half-height for shapes that have a bounding box. */
|
|
1078
|
+
function shapeBoundingBox(shape) {
|
|
1079
|
+
switch (shape.kind) {
|
|
1080
|
+
case 'circle':
|
|
1081
|
+
return { hw: shape.r, hh: shape.r };
|
|
1082
|
+
case 'rect':
|
|
1083
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1084
|
+
case 'diamond':
|
|
1085
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1086
|
+
case 'ellipse':
|
|
1087
|
+
return { hw: shape.rx, hh: shape.ry };
|
|
1088
|
+
case 'hexagon':
|
|
1089
|
+
return { hw: shape.r, hh: shape.r };
|
|
1090
|
+
case 'cylinder':
|
|
1091
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1092
|
+
case 'arc':
|
|
1093
|
+
return { hw: shape.r, hh: shape.r };
|
|
1094
|
+
case 'blockArrow':
|
|
1095
|
+
return { hw: shape.length / 2, hh: shape.headWidth / 2 };
|
|
1096
|
+
case 'callout':
|
|
1097
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1098
|
+
case 'cloud':
|
|
1099
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1100
|
+
case 'cross':
|
|
1101
|
+
return { hw: shape.size / 2, hh: shape.size / 2 };
|
|
1102
|
+
case 'cube':
|
|
1103
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1104
|
+
case 'path':
|
|
1105
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1106
|
+
case 'document':
|
|
1107
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1108
|
+
case 'note':
|
|
1109
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1110
|
+
case 'parallelogram':
|
|
1111
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1112
|
+
case 'star':
|
|
1113
|
+
return { hw: shape.outerR, hh: shape.outerR };
|
|
1114
|
+
case 'trapezoid':
|
|
1115
|
+
return {
|
|
1116
|
+
hw: Math.max(shape.topW, shape.bottomW) / 2,
|
|
1117
|
+
hh: shape.h / 2,
|
|
1118
|
+
};
|
|
1119
|
+
case 'triangle':
|
|
1120
|
+
return { hw: shape.w / 2, hh: shape.h / 2 };
|
|
1121
|
+
}
|
|
1122
|
+
}
|