@visualli/react 0.1.7 → 0.2.1
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 +13 -5
- package/dist/index.cjs +1900 -2033
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +354 -97
- package/dist/index.d.ts +354 -97
- package/dist/index.js +2005 -2095
- package/dist/index.js.map +1 -1
- package/fonts/Caveat-Variable.ttf +0 -0
- package/licenses/visualli-design-system/LICENSE +9 -0
- package/licenses/visualli-design-system/NOTICE.md +6 -0
- package/package.json +9 -4
- package/src/VisualliCanvas.tsx +452 -409
- package/src/VisualliRenderer.tsx +90 -112
- package/src/components/A11yLayer.tsx +73 -0
- package/src/components/KonvaContainer.tsx +28 -237
- package/src/components/KonvaContainerLabelLayer.tsx +41 -0
- package/src/components/KonvaContainerLayer.tsx +34 -33
- package/src/components/KonvaEdge.tsx +25 -240
- package/src/components/KonvaEdgeLayer.tsx +69 -49
- package/src/components/KonvaNode.tsx +21 -156
- package/src/components/KonvaNodeLayer.tsx +102 -34
- package/src/components/KonvaStage.tsx +17 -3
- package/src/components/NavigationStack.tsx +16 -159
- package/src/components/Overlays.tsx +327 -0
- package/src/components/ZoomControls.tsx +3 -107
- package/src/components/useFrames.ts +30 -0
- package/src/context/VisualliContext.tsx +3 -3
- package/src/design/choreography.ts +157 -0
- package/src/design/containers.ts +150 -0
- package/src/design/design.ts +115 -0
- package/src/design/drawing.ts +303 -0
- package/src/design/measure.ts +34 -0
- package/src/design/runtime.ts +154 -0
- package/src/design/useDesign.ts +38 -0
- package/src/generated/specCss.ts +15 -0
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useKonvaRenderer.ts +13 -18
- package/src/hooks/useLayerChoreography.ts +159 -0
- package/src/hooks/useViewportNodes.ts +0 -1
- package/src/index.ts +22 -22
- package/src/stores/stores.ts +2 -2
- package/src/stores/useViewportStore.ts +12 -2
- package/src/animations/index.ts +0 -2
- package/src/animations/konvaLayerTransition.ts +0 -223
- package/src/components/SketchyBoxKonva.tsx +0 -121
- package/src/config/blobShapes.ts +0 -204
- package/src/config/index.ts +0 -21
- package/src/config/textScaling.ts +0 -69
- package/src/hooks/useKonvaLayerTransition.ts +0 -221
|
@@ -1,254 +1,45 @@
|
|
|
1
|
-
// ─── KonvaContainer
|
|
1
|
+
// ─── KonvaContainer ───────────────────────────────────────────────────────────
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// pill
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// • Dashed stroke "#747270", no fill
|
|
9
|
-
// • Pill label: parchment bg (#ddd9d0), Playpen Sans 800, inverse-zoom-scaled
|
|
10
|
-
// • Label anchored to the hull edge (bottom for horizontal, top for vertical)
|
|
3
|
+
// One container — the design system's dashed hull around a group of ideas, with
|
|
4
|
+
// its name as a pill on the hull's edge — for composing custom Konva scenes.
|
|
5
|
+
// It only knows its own members, so the name avoids them (not other ideas or
|
|
6
|
+
// connectors); the canvas uses KonvaContainerLayer + KonvaContainerLabelLayer,
|
|
7
|
+
// which place names against the whole layer.
|
|
11
8
|
|
|
12
9
|
import React, { memo, useMemo } from 'react';
|
|
13
|
-
import {
|
|
14
|
-
KGroup as Group,
|
|
15
|
-
KLine as Line,
|
|
16
|
-
KRect as Rect,
|
|
17
|
-
KText as Text,
|
|
18
|
-
} from '../konvaCompat';
|
|
10
|
+
import { KShape as Shape } from '../konvaCompat';
|
|
19
11
|
import type { FlatNode } from '@visualli/core';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
CONTAINER_LABEL_BASE_FONT_PX,
|
|
24
|
-
computeNodeTextScreenScale,
|
|
25
|
-
} from '../config/textScaling';
|
|
26
|
-
|
|
27
|
-
// ── Constants (mirrors visualli.ai) ──────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
const CONTAINER_PADDING = 50;
|
|
30
|
-
const CONTAINER_DASH = [10, 5] as const;
|
|
31
|
-
const LABEL_BG = '#ddd9d0';
|
|
32
|
-
const LABEL_BORDER_RADIUS = 20;
|
|
33
|
-
const LABEL_FONT_SIZE = CONTAINER_LABEL_BASE_FONT_PX;
|
|
34
|
-
const LABEL_FONT_WEIGHT = '800';
|
|
35
|
-
const LABEL_FONT_FAMILY = "'Playpen Sans', Quicksand, sans-serif";
|
|
36
|
-
const LABEL_PAD_H = 20;
|
|
37
|
-
const LABEL_PAD_V = 10;
|
|
38
|
-
const LABEL_LINE_HEIGHT = 1.2;
|
|
39
|
-
// Max normalized blob extents — Shape 5 reaches ±1.275 X, ±1.234 Y
|
|
40
|
-
const MAX_BLOB_EXTENT_X = 1.275;
|
|
41
|
-
const MAX_BLOB_EXTENT_Y = 1.234;
|
|
42
|
-
|
|
43
|
-
// ── Text measurement ──────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
let _labelCtx: CanvasRenderingContext2D | null = null;
|
|
46
|
-
function measureLabelBoxWidth(text: string, fontSize: number): number {
|
|
47
|
-
if (!_labelCtx) {
|
|
48
|
-
const c = document.createElement('canvas');
|
|
49
|
-
_labelCtx = c.getContext('2d');
|
|
50
|
-
}
|
|
51
|
-
if (!_labelCtx) return Math.ceil(text.length * fontSize * 0.65 + 2 * LABEL_PAD_H);
|
|
52
|
-
_labelCtx.font = `${LABEL_FONT_WEIGHT} ${fontSize}px ${LABEL_FONT_FAMILY}`;
|
|
53
|
-
return Math.ceil(_labelCtx.measureText(text).width + 2 * LABEL_PAD_H);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// ── Convex hull (Graham scan) ─────────────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
type Pt = { x: number; y: number };
|
|
59
|
-
|
|
60
|
-
function isLeftTurn(p1: Pt, p2: Pt, p3: Pt): boolean {
|
|
61
|
-
return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x) > 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function convexHull(points: Pt[]): Pt[] {
|
|
65
|
-
if (points.length < 3) return points;
|
|
66
|
-
let start = 0;
|
|
67
|
-
for (let i = 1; i < points.length; i++) {
|
|
68
|
-
if (points[i].y < points[start].y ||
|
|
69
|
-
(points[i].y === points[start].y && points[i].x < points[start].x)) start = i;
|
|
70
|
-
}
|
|
71
|
-
const sorted = [...points];
|
|
72
|
-
const sp = sorted.splice(start, 1)[0];
|
|
73
|
-
sorted.sort((a, b) => {
|
|
74
|
-
const da = Math.atan2(a.y - sp.y, a.x - sp.x);
|
|
75
|
-
const db = Math.atan2(b.y - sp.y, b.x - sp.x);
|
|
76
|
-
if (da !== db) return da - db;
|
|
77
|
-
return Math.hypot(a.x - sp.x, a.y - sp.y) - Math.hypot(b.x - sp.x, b.y - sp.y);
|
|
78
|
-
});
|
|
79
|
-
const hull: Pt[] = [sp, sorted[0]];
|
|
80
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
81
|
-
while (hull.length > 1 && !isLeftTurn(hull[hull.length - 2], hull[hull.length - 1], sorted[i])) hull.pop();
|
|
82
|
-
hull.push(sorted[i]);
|
|
83
|
-
}
|
|
84
|
-
return hull;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ── Chaikin smoothing (6 passes, returns flat [x,y,…] array) ─────────────────
|
|
88
|
-
|
|
89
|
-
function chaikinSmooth(pts: Pt[], passes = 6): number[] {
|
|
90
|
-
let cur = pts;
|
|
91
|
-
for (let iter = 0; iter < passes; iter++) {
|
|
92
|
-
const next: Pt[] = [];
|
|
93
|
-
const n = cur.length;
|
|
94
|
-
for (let i = 0; i < n; i++) {
|
|
95
|
-
const a = cur[i], b = cur[(i + 1) % n];
|
|
96
|
-
next.push({ x: 0.75 * a.x + 0.25 * b.x, y: 0.75 * a.y + 0.25 * b.y });
|
|
97
|
-
next.push({ x: 0.25 * a.x + 0.75 * b.x, y: 0.25 * a.y + 0.75 * b.y });
|
|
98
|
-
}
|
|
99
|
-
cur = next;
|
|
100
|
-
}
|
|
101
|
-
return cur.flatMap(p => [p.x, p.y]);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// ── Hull edge Y at a given X (for label anchor) ───────────────────────────────
|
|
105
|
-
|
|
106
|
-
function hullEdgeYAtX(points: number[], targetX: number, isBottom: boolean): number {
|
|
107
|
-
const n = points.length;
|
|
108
|
-
let result = isBottom ? -Infinity : Infinity;
|
|
109
|
-
for (let i = 0; i < n; i += 2) {
|
|
110
|
-
const x1 = points[i], y1 = points[i + 1];
|
|
111
|
-
const x2 = points[(i + 2) % n], y2 = points[(i + 3) % n];
|
|
112
|
-
if ((x1 < targetX && x2 >= targetX) || (x2 < targetX && x1 >= targetX)) {
|
|
113
|
-
const t = (targetX - x1) / (x2 - x1);
|
|
114
|
-
const y = y1 + t * (y2 - y1);
|
|
115
|
-
if (isBottom) result = Math.max(result, y); else result = Math.min(result, y);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (!isFinite(result)) {
|
|
119
|
-
for (let i = 1; i < n; i += 2) {
|
|
120
|
-
if (isBottom) result = Math.max(result, points[i]); else result = Math.min(result, points[i]);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── Hull calculation ──────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
function calculateHull(
|
|
129
|
-
containerNodes: FlatNode[],
|
|
130
|
-
): { points: number[]; anchorX: number; anchorEdgeY: number } | null {
|
|
131
|
-
if (containerNodes.length === 0) return null;
|
|
132
|
-
|
|
133
|
-
const NUM_SAMPLES = 48;
|
|
134
|
-
const samples: Pt[] = [];
|
|
135
|
-
for (const node of containerNodes) {
|
|
136
|
-
const baseRX = node.width / 2;
|
|
137
|
-
const baseRY = Math.max((NODE_HEIGHT * 1.6) / 2, baseRX * 0.74);
|
|
138
|
-
const halfW = baseRX * MAX_BLOB_EXTENT_X + CONTAINER_PADDING;
|
|
139
|
-
const halfH = baseRY * MAX_BLOB_EXTENT_Y + CONTAINER_PADDING;
|
|
140
|
-
for (let j = 0; j < NUM_SAMPLES; j++) {
|
|
141
|
-
const angle = (j / NUM_SAMPLES) * Math.PI * 2;
|
|
142
|
-
samples.push({ x: node.x + halfW * Math.cos(angle), y: node.y + halfH * Math.sin(angle) });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const hull = convexHull(samples);
|
|
147
|
-
const points = chaikinSmooth(hull, 6);
|
|
148
|
-
|
|
149
|
-
const xs = containerNodes.map(n => n.x);
|
|
150
|
-
const ys = containerNodes.map(n => n.y);
|
|
151
|
-
const spanX = Math.max(...xs) - Math.min(...xs);
|
|
152
|
-
const spanY = Math.max(...ys) - Math.min(...ys);
|
|
153
|
-
const useTopEdge = spanY > spanX * 2.0;
|
|
154
|
-
|
|
155
|
-
let hullMinX = Infinity, hullMaxX = -Infinity;
|
|
156
|
-
for (let i = 0; i < points.length; i += 2) {
|
|
157
|
-
if (points[i] < hullMinX) hullMinX = points[i];
|
|
158
|
-
if (points[i] > hullMaxX) hullMaxX = points[i];
|
|
159
|
-
}
|
|
160
|
-
const anchorX = (hullMinX + hullMaxX) / 2;
|
|
161
|
-
const anchorEdgeY = hullEdgeYAtX(points, anchorX, !useTopEdge);
|
|
162
|
-
|
|
163
|
-
return { points, anchorX, anchorEdgeY };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── Props ─────────────────────────────────────────────────────────────────────
|
|
12
|
+
import { DEFAULT_DESIGN, type Design } from '../design/design';
|
|
13
|
+
import { drawHull, drawPill, layoutContainers } from '../design/containers';
|
|
167
14
|
|
|
168
15
|
export interface KonvaContainerProps {
|
|
169
16
|
/** All nodes in this container group (already resolved from nodeMap). */
|
|
170
17
|
nodes: FlatNode[];
|
|
171
|
-
/**
|
|
18
|
+
/** The group's name, drawn as a pill on the hull. */
|
|
172
19
|
label?: string;
|
|
173
|
-
/**
|
|
20
|
+
/** @deprecated unused: the hull and name use the design system's container style. */
|
|
174
21
|
color?: string;
|
|
22
|
+
/** @deprecated pass `design`. */
|
|
175
23
|
isDark?: boolean;
|
|
24
|
+
zoomLevel?: number;
|
|
25
|
+
design?: Design;
|
|
176
26
|
}
|
|
177
27
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const zoomLevel = useViewportStore(s => s.zoomLevel);
|
|
185
|
-
|
|
186
|
-
const hullData = useMemo(() => calculateHull(nodes), [nodes]);
|
|
187
|
-
|
|
188
|
-
if (!hullData) return null;
|
|
189
|
-
|
|
190
|
-
// Inverse-scaled label metrics
|
|
191
|
-
const refNode = nodes[0];
|
|
192
|
-
const screenScale = refNode ? computeNodeTextScreenScale(refNode.width, zoomLevel) : 1;
|
|
193
|
-
const labelFontSize = LABEL_FONT_SIZE * screenScale;
|
|
194
|
-
const labelBoxHeight = Math.ceil(labelFontSize * LABEL_LINE_HEIGHT + 2 * LABEL_PAD_V);
|
|
195
|
-
const boxWidth = label ? measureLabelBoxWidth(label, labelFontSize) : 0;
|
|
196
|
-
const boxX = hullData.anchorX - boxWidth / 2;
|
|
197
|
-
const boxY = hullData.anchorEdgeY - labelBoxHeight / 2;
|
|
198
|
-
|
|
28
|
+
const KonvaContainer = memo(function KonvaContainer({ nodes, label = '', zoomLevel = 1, design = DEFAULT_DESIGN }: KonvaContainerProps) {
|
|
29
|
+
const placed = useMemo(() => {
|
|
30
|
+
const map = new Map(nodes.map((n) => [n.id, n] as const));
|
|
31
|
+
return layoutContainers([{ id: 'c', label: label || undefined, nodeIds: nodes.map((n) => n.id) }], map, [], design)[0] ?? null;
|
|
32
|
+
}, [nodes, label, design]);
|
|
33
|
+
if (!placed) return null;
|
|
199
34
|
return (
|
|
200
|
-
<
|
|
201
|
-
{
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
tension={0}
|
|
209
|
-
listening={false}
|
|
210
|
-
perfectDrawEnabled={false}
|
|
211
|
-
/>
|
|
212
|
-
|
|
213
|
-
{/* Inverse-scaled pill label */}
|
|
214
|
-
{label.length > 0 && (
|
|
215
|
-
<Group
|
|
216
|
-
x={boxX + boxWidth / 2}
|
|
217
|
-
y={boxY + labelBoxHeight / 2}
|
|
218
|
-
offsetX={boxWidth / 2}
|
|
219
|
-
offsetY={labelBoxHeight / 2}
|
|
220
|
-
scaleX={1 / zoomLevel}
|
|
221
|
-
scaleY={1 / zoomLevel}
|
|
222
|
-
visible={zoomLevel >= TEXT_LABEL_HIDE_BELOW_ZOOM}
|
|
223
|
-
listening={false}
|
|
224
|
-
>
|
|
225
|
-
<Rect
|
|
226
|
-
width={boxWidth}
|
|
227
|
-
height={labelBoxHeight}
|
|
228
|
-
fill={LABEL_BG}
|
|
229
|
-
cornerRadius={LABEL_BORDER_RADIUS}
|
|
230
|
-
shadowEnabled={false}
|
|
231
|
-
perfectDrawEnabled={false}
|
|
232
|
-
/>
|
|
233
|
-
<Text
|
|
234
|
-
x={LABEL_PAD_H}
|
|
235
|
-
y={LABEL_PAD_V}
|
|
236
|
-
width={boxWidth - 2 * LABEL_PAD_H}
|
|
237
|
-
height={labelBoxHeight - 2 * LABEL_PAD_V}
|
|
238
|
-
text={label}
|
|
239
|
-
fontSize={labelFontSize}
|
|
240
|
-
fontFamily={LABEL_FONT_FAMILY}
|
|
241
|
-
fontStyle={LABEL_FONT_WEIGHT}
|
|
242
|
-
fill="rgba(0,0,0,1)"
|
|
243
|
-
align="center"
|
|
244
|
-
verticalAlign="middle"
|
|
245
|
-
lineHeight={LABEL_LINE_HEIGHT}
|
|
246
|
-
perfectDrawEnabled={false}
|
|
247
|
-
listening={false}
|
|
248
|
-
/>
|
|
249
|
-
</Group>
|
|
250
|
-
)}
|
|
251
|
-
</Group>
|
|
35
|
+
<Shape
|
|
36
|
+
listening={false}
|
|
37
|
+
perfectDrawEnabled={false}
|
|
38
|
+
sceneFunc={(ctx: { _context: CanvasRenderingContext2D }) => {
|
|
39
|
+
drawHull(ctx._context, placed.hull, design);
|
|
40
|
+
if (placed.pill) drawPill(ctx._context, placed.pill, design, zoomLevel);
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
252
43
|
);
|
|
253
44
|
});
|
|
254
45
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ─── KonvaContainerLabelLayer ────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Container names: pills straddling their hull's edge, at the spot the design
|
|
4
|
+
// system's placeContainerLabel picks (clear of ideas, connectors and other
|
|
5
|
+
// names). Painted above connectors and ideas so nothing runs over a name
|
|
6
|
+
// (non-interactive). Shares its layout with KonvaContainerLayer.
|
|
7
|
+
|
|
8
|
+
import React, { useRef, useLayoutEffect } from 'react';
|
|
9
|
+
import { KLayer as Layer, KShape as Shape } from '../konvaCompat';
|
|
10
|
+
import type Konva from 'konva';
|
|
11
|
+
import { DEFAULT_DESIGN, type Design } from '../design/design';
|
|
12
|
+
import { drawPill, type ConnectionInput } from '../design/containers';
|
|
13
|
+
import { useContainerLayout, type ContainerGroup } from './KonvaContainerLayer';
|
|
14
|
+
|
|
15
|
+
export interface KonvaContainerLabelLayerProps {
|
|
16
|
+
containers: ContainerGroup[];
|
|
17
|
+
connections?: ConnectionInput[];
|
|
18
|
+
design?: Design;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function KonvaContainerLabelLayer({ containers, connections = [], design = DEFAULT_DESIGN }: KonvaContainerLabelLayerProps) {
|
|
22
|
+
const layerRef = useRef<Konva.Layer | null>(null);
|
|
23
|
+
const placed = useContainerLayout(containers, connections, design);
|
|
24
|
+
|
|
25
|
+
useLayoutEffect(() => { layerRef.current?.batchDraw(); }, [placed, design]);
|
|
26
|
+
|
|
27
|
+
if (!placed.some((p) => p.pill)) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Layer ref={layerRef} listening={false}>
|
|
31
|
+
<Shape
|
|
32
|
+
listening={false}
|
|
33
|
+
perfectDrawEnabled={false}
|
|
34
|
+
sceneFunc={(ctx: { _context: CanvasRenderingContext2D }, shape: Konva.Shape) => {
|
|
35
|
+
const zoom = shape.getStage()?.scaleX() || 1;
|
|
36
|
+
for (const p of placed) if (p.pill) drawPill(ctx._context, p.pill, design, zoom);
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
</Layer>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
// ─── KonvaContainerLayer ─────────────────────────────────────────────────────
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Container hulls (the design system's dashed hull, geometry/container.ts),
|
|
4
|
+
// beneath connectors and ideas (non-interactive). Their names are painted above
|
|
5
|
+
// the ideas by KonvaContainerLabelLayer from the same layout. Member positions
|
|
6
|
+
// come from the live node store so hulls follow dragged ideas.
|
|
7
7
|
|
|
8
|
-
import React, { useRef, useLayoutEffect } from 'react';
|
|
9
|
-
import { KLayer as Layer } from '../konvaCompat';
|
|
8
|
+
import React, { useRef, useLayoutEffect, useMemo } from 'react';
|
|
9
|
+
import { KLayer as Layer, KShape as Shape } from '../konvaCompat';
|
|
10
10
|
import type Konva from 'konva';
|
|
11
11
|
import type { FlatNode } from '@visualli/core';
|
|
12
12
|
import { useNodeStore } from '../stores/useNodeStore';
|
|
13
|
-
import
|
|
13
|
+
import { DEFAULT_DESIGN, type Design } from '../design/design';
|
|
14
|
+
import { useFontsEpoch } from '../design/runtime';
|
|
15
|
+
import { drawHull, layoutContainers, type ConnectionInput, type PlacedContainer } from '../design/containers';
|
|
14
16
|
|
|
15
17
|
export interface ContainerGroup {
|
|
16
18
|
id: string;
|
|
@@ -20,44 +22,43 @@ export interface ContainerGroup {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export interface KonvaContainerLayerProps {
|
|
23
|
-
/** Layout-snapshot nodes
|
|
25
|
+
/** Layout-snapshot nodes (kept for API compatibility). */
|
|
24
26
|
nodes: FlatNode[];
|
|
25
27
|
containers: ContainerGroup[];
|
|
28
|
+
/** The layer's connections: container names are placed clear of them. */
|
|
29
|
+
connections?: ConnectionInput[];
|
|
30
|
+
/** @deprecated pass `design`. */
|
|
26
31
|
isDark?: boolean;
|
|
32
|
+
design?: Design;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
/** Hulls and name positions for the current layer, from the live node store (shared by both container layers). */
|
|
36
|
+
export function useContainerLayout(containers: ContainerGroup[], connections: ConnectionInput[] = [], design: Design = DEFAULT_DESIGN): PlacedContainer[] {
|
|
37
|
+
const fonts = useFontsEpoch();
|
|
38
|
+
const storeNodes = useNodeStore((s) => s.nodes);
|
|
39
|
+
return useMemo(
|
|
40
|
+
() => layoutContainers(containers, storeNodes, connections, design),
|
|
41
|
+
// fonts: the names are measured, so measure again once the web fonts are in.
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
[containers, connections, storeNodes, design, fonts],
|
|
44
|
+
);
|
|
45
|
+
}
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const storeNodes = useNodeStore(s => s.nodes);
|
|
47
|
+
export default function KonvaContainerLayer({ containers, connections = [], design = DEFAULT_DESIGN }: KonvaContainerLayerProps) {
|
|
48
|
+
const layerRef = useRef<Konva.Layer | null>(null);
|
|
49
|
+
const placed = useContainerLayout(containers, connections, design);
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
// position changes (including mid-drag RAF updates).
|
|
39
|
-
useLayoutEffect(() => { layerRef.current?.batchDraw(); }, [containers, isDark, storeNodes]);
|
|
51
|
+
useLayoutEffect(() => { layerRef.current?.batchDraw(); }, [placed, design]);
|
|
40
52
|
|
|
41
53
|
if (containers.length === 0) return null;
|
|
42
54
|
|
|
43
55
|
return (
|
|
44
56
|
<Layer ref={layerRef} listening={false}>
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (groupNodes.length === 0) return null;
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<KonvaContainer
|
|
54
|
-
key={c.id}
|
|
55
|
-
nodes={groupNodes}
|
|
56
|
-
label={c.label}
|
|
57
|
-
isDark={isDark}
|
|
58
|
-
/>
|
|
59
|
-
);
|
|
60
|
-
})}
|
|
57
|
+
<Shape
|
|
58
|
+
listening={false}
|
|
59
|
+
perfectDrawEnabled={false}
|
|
60
|
+
sceneFunc={(ctx: { _context: CanvasRenderingContext2D }) => { for (const p of placed) drawHull(ctx._context, p.hull, design); }}
|
|
61
|
+
/>
|
|
61
62
|
</Layer>
|
|
62
63
|
);
|
|
63
64
|
}
|