@principal-ai/principal-view-react 0.13.10 → 0.13.11
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/dist/components/MultiCanvasRenderer.d.ts +75 -0
- package/dist/components/MultiCanvasRenderer.d.ts.map +1 -0
- package/dist/components/MultiCanvasRenderer.js +142 -0
- package/dist/components/MultiCanvasRenderer.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/canvasBounds.d.ts +29 -0
- package/dist/utils/canvasBounds.d.ts.map +1 -0
- package/dist/utils/canvasBounds.js +56 -0
- package/dist/utils/canvasBounds.js.map +1 -0
- package/package.json +1 -1
- package/src/components/MultiCanvasRenderer.tsx +225 -0
- package/src/index.ts +9 -0
- package/src/stories/MultiCanvasRenderer.stories.tsx +329 -0
- package/src/utils/canvasBounds.ts +91 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
|
|
3
|
+
import type { GraphRendererProps, GraphRendererHandle } from './GraphRenderer';
|
|
4
|
+
/**
|
|
5
|
+
* A canvas placement with its position in the combined coordinate space
|
|
6
|
+
*/
|
|
7
|
+
export interface CanvasPlacement {
|
|
8
|
+
/** Unique identifier for this canvas */
|
|
9
|
+
canvasId: string;
|
|
10
|
+
/** The canvas to include */
|
|
11
|
+
canvas: ExtendedCanvas;
|
|
12
|
+
/** Position offset - all nodes in this canvas will be translated by this amount */
|
|
13
|
+
position: {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
};
|
|
17
|
+
/** Optional label for the canvas (for future visual grouping) */
|
|
18
|
+
label?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Layout configuration for multiple canvases on a single graph
|
|
22
|
+
*/
|
|
23
|
+
export interface MultiCanvasLayout {
|
|
24
|
+
/** Array of canvas placements with positions */
|
|
25
|
+
placements: CanvasPlacement[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Props for the MultiCanvasRenderer component
|
|
29
|
+
*/
|
|
30
|
+
export interface MultiCanvasRendererProps extends Omit<GraphRendererProps, 'canvas' | 'editable'> {
|
|
31
|
+
/** Layout configuration containing canvas placements */
|
|
32
|
+
layout: MultiCanvasLayout;
|
|
33
|
+
/** Callback when layout changes (canvas positions updated) */
|
|
34
|
+
onLayoutChange?: (layout: MultiCanvasLayout) => void;
|
|
35
|
+
/** Whether to show group borders around each canvas (default: true) */
|
|
36
|
+
showGroups?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Merge multiple canvases into a single canvas with position offsets applied.
|
|
40
|
+
* Each canvas gets a group node border with its name as a label.
|
|
41
|
+
* Node IDs are prefixed with canvasId to avoid collisions (format: "canvasId:nodeId").
|
|
42
|
+
* Original IDs can be recovered using parseNodeId().
|
|
43
|
+
*/
|
|
44
|
+
export declare function mergeCanvases(placements: CanvasPlacement[], options?: {
|
|
45
|
+
showGroups?: boolean;
|
|
46
|
+
}): ExtendedCanvas;
|
|
47
|
+
/**
|
|
48
|
+
* Extract the original canvas ID and node ID from a merged node ID
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseNodeId(mergedNodeId: string): {
|
|
51
|
+
canvasId: string;
|
|
52
|
+
nodeId: string;
|
|
53
|
+
} | null;
|
|
54
|
+
/**
|
|
55
|
+
* Renders multiple canvases merged into a single graph view.
|
|
56
|
+
*
|
|
57
|
+
* Each canvas is positioned at its specified offset, and all nodes/edges
|
|
58
|
+
* are combined into a unified viewport. Node IDs are prefixed with their
|
|
59
|
+
* source canvas ID to avoid collisions (format: "canvasId:nodeId").
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* <MultiCanvasRenderer
|
|
64
|
+
* layout={{
|
|
65
|
+
* placements: [
|
|
66
|
+
* { canvasId: 'auth', canvas: authCanvas, position: { x: 0, y: 0 } },
|
|
67
|
+
* { canvasId: 'data', canvas: dataCanvas, position: { x: 600, y: 0 } },
|
|
68
|
+
* ],
|
|
69
|
+
* }}
|
|
70
|
+
* showControls
|
|
71
|
+
* />
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare const MultiCanvasRenderer: import("react").ForwardRefExoticComponent<MultiCanvasRendererProps & import("react").RefAttributes<GraphRendererHandle>>;
|
|
75
|
+
//# sourceMappingURL=MultiCanvasRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MultiCanvasRenderer.d.ts","sourceRoot":"","sources":["../../src/components/MultiCanvasRenderer.tsx"],"names":[],"mappings":";AACA,OAAO,KAAK,EAAE,cAAc,EAA0C,MAAM,mCAAmC,CAAC;AAEhH,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAM/E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,4BAA4B;IAC5B,MAAM,EAAE,cAAc,CAAC;IACvB,mFAAmF;IACnF,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,gDAAgD;IAChD,UAAU,EAAE,eAAe,EAAE,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,wBACf,SAAQ,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,UAAU,CAAC;IACvD,wDAAwD;IACxD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,8DAA8D;IAC9D,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACrD,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAkDD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,eAAe,EAAE,EAC7B,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,cAAc,CA8DhB;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,YAAY,EAAE,MAAM,GACnB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAO7C;AAMD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,0HAqB9B,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, forwardRef } from 'react';
|
|
3
|
+
import { GraphRenderer } from './GraphRenderer';
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Canvas Merging Utilities
|
|
6
|
+
// ============================================================================
|
|
7
|
+
const DEFAULT_NODE_WIDTH = 200;
|
|
8
|
+
const DEFAULT_NODE_HEIGHT = 100;
|
|
9
|
+
const GROUP_PADDING = 40;
|
|
10
|
+
/**
|
|
11
|
+
* Create a prefixed node ID to avoid collisions between canvases
|
|
12
|
+
*/
|
|
13
|
+
function prefixNodeId(canvasId, nodeId) {
|
|
14
|
+
return `${canvasId}:${nodeId}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Calculate the bounding box of nodes in a canvas (relative to canvas origin)
|
|
18
|
+
*/
|
|
19
|
+
function calculateCanvasBounds(canvas) {
|
|
20
|
+
if (!canvas.nodes || canvas.nodes.length === 0) {
|
|
21
|
+
return { minX: 0, minY: 0, maxX: DEFAULT_NODE_WIDTH, maxY: DEFAULT_NODE_HEIGHT };
|
|
22
|
+
}
|
|
23
|
+
let minX = Infinity;
|
|
24
|
+
let minY = Infinity;
|
|
25
|
+
let maxX = -Infinity;
|
|
26
|
+
let maxY = -Infinity;
|
|
27
|
+
for (const node of canvas.nodes) {
|
|
28
|
+
const x = node.x ?? 0;
|
|
29
|
+
const y = node.y ?? 0;
|
|
30
|
+
const width = node.width ?? DEFAULT_NODE_WIDTH;
|
|
31
|
+
const height = node.height ?? DEFAULT_NODE_HEIGHT;
|
|
32
|
+
minX = Math.min(minX, x);
|
|
33
|
+
minY = Math.min(minY, y);
|
|
34
|
+
maxX = Math.max(maxX, x + width);
|
|
35
|
+
maxY = Math.max(maxY, y + height);
|
|
36
|
+
}
|
|
37
|
+
return { minX, minY, maxX, maxY };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Merge multiple canvases into a single canvas with position offsets applied.
|
|
41
|
+
* Each canvas gets a group node border with its name as a label.
|
|
42
|
+
* Node IDs are prefixed with canvasId to avoid collisions (format: "canvasId:nodeId").
|
|
43
|
+
* Original IDs can be recovered using parseNodeId().
|
|
44
|
+
*/
|
|
45
|
+
export function mergeCanvases(placements, options = {}) {
|
|
46
|
+
const { showGroups = true } = options;
|
|
47
|
+
const groupNodes = [];
|
|
48
|
+
const mergedNodes = [];
|
|
49
|
+
const mergedEdges = [];
|
|
50
|
+
for (const placement of placements) {
|
|
51
|
+
const { canvasId, canvas, position, label } = placement;
|
|
52
|
+
// Calculate bounds for this canvas to create a group node
|
|
53
|
+
if (showGroups && canvas.nodes && canvas.nodes.length > 0) {
|
|
54
|
+
const bounds = calculateCanvasBounds(canvas);
|
|
55
|
+
const groupNode = {
|
|
56
|
+
id: `${canvasId}:__group__`,
|
|
57
|
+
type: 'group',
|
|
58
|
+
label: label ?? canvas.pv?.name ?? canvasId,
|
|
59
|
+
x: position.x + bounds.minX - GROUP_PADDING,
|
|
60
|
+
y: position.y + bounds.minY - GROUP_PADDING,
|
|
61
|
+
width: bounds.maxX - bounds.minX + GROUP_PADDING * 2,
|
|
62
|
+
height: bounds.maxY - bounds.minY + GROUP_PADDING * 2,
|
|
63
|
+
};
|
|
64
|
+
groupNodes.push(groupNode);
|
|
65
|
+
}
|
|
66
|
+
// Process nodes - apply position offset and prefix IDs
|
|
67
|
+
if (canvas.nodes) {
|
|
68
|
+
for (const node of canvas.nodes) {
|
|
69
|
+
const mergedNode = {
|
|
70
|
+
...node,
|
|
71
|
+
id: prefixNodeId(canvasId, node.id),
|
|
72
|
+
x: (node.x ?? 0) + position.x,
|
|
73
|
+
y: (node.y ?? 0) + position.y,
|
|
74
|
+
};
|
|
75
|
+
mergedNodes.push(mergedNode);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Process edges - update node references with prefixed IDs
|
|
79
|
+
if (canvas.edges) {
|
|
80
|
+
for (const edge of canvas.edges) {
|
|
81
|
+
const mergedEdge = {
|
|
82
|
+
...edge,
|
|
83
|
+
id: prefixNodeId(canvasId, edge.id),
|
|
84
|
+
fromNode: prefixNodeId(canvasId, edge.fromNode),
|
|
85
|
+
toNode: prefixNodeId(canvasId, edge.toNode),
|
|
86
|
+
};
|
|
87
|
+
mergedEdges.push(mergedEdge);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Create merged canvas - group nodes first so they render behind
|
|
92
|
+
const mergedCanvas = {
|
|
93
|
+
nodes: [...groupNodes, ...mergedNodes],
|
|
94
|
+
edges: mergedEdges,
|
|
95
|
+
pv: {
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
name: 'Multi-Canvas View',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return mergedCanvas;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract the original canvas ID and node ID from a merged node ID
|
|
104
|
+
*/
|
|
105
|
+
export function parseNodeId(mergedNodeId) {
|
|
106
|
+
const colonIndex = mergedNodeId.indexOf(':');
|
|
107
|
+
if (colonIndex === -1)
|
|
108
|
+
return null;
|
|
109
|
+
return {
|
|
110
|
+
canvasId: mergedNodeId.substring(0, colonIndex),
|
|
111
|
+
nodeId: mergedNodeId.substring(colonIndex + 1),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// MultiCanvasRenderer Component
|
|
116
|
+
// ============================================================================
|
|
117
|
+
/**
|
|
118
|
+
* Renders multiple canvases merged into a single graph view.
|
|
119
|
+
*
|
|
120
|
+
* Each canvas is positioned at its specified offset, and all nodes/edges
|
|
121
|
+
* are combined into a unified viewport. Node IDs are prefixed with their
|
|
122
|
+
* source canvas ID to avoid collisions (format: "canvasId:nodeId").
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```tsx
|
|
126
|
+
* <MultiCanvasRenderer
|
|
127
|
+
* layout={{
|
|
128
|
+
* placements: [
|
|
129
|
+
* { canvasId: 'auth', canvas: authCanvas, position: { x: 0, y: 0 } },
|
|
130
|
+
* { canvasId: 'data', canvas: dataCanvas, position: { x: 600, y: 0 } },
|
|
131
|
+
* ],
|
|
132
|
+
* }}
|
|
133
|
+
* showControls
|
|
134
|
+
* />
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export const MultiCanvasRenderer = forwardRef(function MultiCanvasRenderer({ layout, onLayoutChange: _onLayoutChange, showGroups = true, ...graphRendererProps }, ref) {
|
|
138
|
+
// Merge all canvases into a single canvas
|
|
139
|
+
const mergedCanvas = useMemo(() => mergeCanvases(layout.placements, { showGroups }), [layout.placements, showGroups]);
|
|
140
|
+
return (_jsx(GraphRenderer, { ref: ref, canvas: mergedCanvas, editable: false, ...graphRendererProps }));
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=MultiCanvasRenderer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MultiCanvasRenderer.js","sourceRoot":"","sources":["../../src/components/MultiCanvasRenderer.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
export type { GraphConfiguration, GraphEvent, GraphMetrics, Violation, Warning, ValidationResult, EventStream, NodeTypeDefinition, EdgeTypeDefinition, ConnectionRule, NodeState, EdgeState, ConfigurationFile, ConfigurationLoadResult, ComponentLibrary, LibraryNodeComponent, LibraryEdgeComponent, } from '@principal-ai/principal-view-core';
|
|
9
9
|
export { GraphRenderer } from './components/GraphRenderer';
|
|
10
10
|
export type { GraphRendererProps, GraphRendererHandle, NodePositionChange, PendingChanges, } from './components/GraphRenderer';
|
|
11
|
+
export { MultiCanvasRenderer, mergeCanvases, parseNodeId } from './components/MultiCanvasRenderer';
|
|
12
|
+
export type { MultiCanvasRendererProps, MultiCanvasLayout, CanvasPlacement, } from './components/MultiCanvasRenderer';
|
|
11
13
|
export { ConfigurationSelector } from './components/ConfigurationSelector';
|
|
12
14
|
export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
|
|
13
15
|
export { GenericNode } from './nodes/GenericNode';
|
|
@@ -25,4 +27,6 @@ export type { EdgeStateWithHandles } from './utils/graphConverter';
|
|
|
25
27
|
export { Icon, resolveIcon } from './utils/iconResolver';
|
|
26
28
|
export type { IconProps } from './utils/iconResolver';
|
|
27
29
|
export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
|
|
30
|
+
export { getCanvasBounds, getCanvasDisplaySize } from './utils/canvasBounds';
|
|
31
|
+
export type { CanvasBounds } from './utils/canvasBounds';
|
|
28
32
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,YAAY,EACV,kBAAkB,EAClB,UAAU,EACV,YAAY,EACZ,SAAS,EACT,OAAO,EACP,gBAAgB,EAChB,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,EACd,SAAS,EACT,SAAS,EACT,iBAAiB,EACjB,uBAAuB,EAEvB,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,GACf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AACnG,YAAY,EACV,wBAAwB,EACxB,iBAAiB,EACjB,eAAe,GAChB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAC3E,YAAY,EAAE,0BAA0B,EAAE,MAAM,oCAAoC,CAAC;AAGrF,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAClD,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAG3E,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC7E,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
// Export components
|
|
9
9
|
export { GraphRenderer } from './components/GraphRenderer';
|
|
10
|
+
export { MultiCanvasRenderer, mergeCanvases, parseNodeId } from './components/MultiCanvasRenderer';
|
|
10
11
|
export { ConfigurationSelector } from './components/ConfigurationSelector';
|
|
11
12
|
// Export node/edge renderers
|
|
12
13
|
export { GenericNode } from './nodes/GenericNode';
|
|
@@ -19,4 +20,5 @@ export { NodeTooltip } from './components/NodeTooltip';
|
|
|
19
20
|
export { convertToXYFlowNodes, convertToXYFlowEdges, } from './utils/graphConverter';
|
|
20
21
|
export { Icon, resolveIcon } from './utils/iconResolver';
|
|
21
22
|
export { swapGraphOrientation, swapNodePositions, swapEdgeSides, } from './utils/orientationUtils';
|
|
23
|
+
export { getCanvasBounds, getCanvasDisplaySize } from './utils/canvasBounds';
|
|
22
24
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,oBAAoB;AACpB,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAQ3D,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAOnG,OAAO,EAAE,qBAAqB,EAAE,MAAM,oCAAoC,CAAC;AAG3E,6BAA6B;AAC7B,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGhD,2BAA2B;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,mBAAmB;AACnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,GACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
|
|
2
|
+
export interface CanvasBounds {
|
|
3
|
+
minX: number;
|
|
4
|
+
minY: number;
|
|
5
|
+
maxX: number;
|
|
6
|
+
maxY: number;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Calculate the bounding box of all nodes in a canvas.
|
|
12
|
+
* Returns the min/max coordinates and total dimensions.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getCanvasBounds(canvas: ExtendedCanvas): CanvasBounds;
|
|
15
|
+
/**
|
|
16
|
+
* Calculate a recommended display size for a canvas cell.
|
|
17
|
+
* Adds padding and enforces minimum dimensions.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getCanvasDisplaySize(canvas: ExtendedCanvas, options?: {
|
|
20
|
+
padding?: number;
|
|
21
|
+
minWidth?: number;
|
|
22
|
+
minHeight?: number;
|
|
23
|
+
maxWidth?: number;
|
|
24
|
+
maxHeight?: number;
|
|
25
|
+
}): {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
29
|
+
//# sourceMappingURL=canvasBounds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvasBounds.d.ts","sourceRoot":"","sources":["../../src/utils/canvasBounds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mCAAmC,CAAC;AAExE,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAKD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,CAqCpE;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACf,GACL;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAoBnC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const DEFAULT_NODE_WIDTH = 200;
|
|
2
|
+
const DEFAULT_NODE_HEIGHT = 100;
|
|
3
|
+
/**
|
|
4
|
+
* Calculate the bounding box of all nodes in a canvas.
|
|
5
|
+
* Returns the min/max coordinates and total dimensions.
|
|
6
|
+
*/
|
|
7
|
+
export function getCanvasBounds(canvas) {
|
|
8
|
+
if (!canvas.nodes || canvas.nodes.length === 0) {
|
|
9
|
+
return {
|
|
10
|
+
minX: 0,
|
|
11
|
+
minY: 0,
|
|
12
|
+
maxX: DEFAULT_NODE_WIDTH,
|
|
13
|
+
maxY: DEFAULT_NODE_HEIGHT,
|
|
14
|
+
width: DEFAULT_NODE_WIDTH,
|
|
15
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
let minX = Infinity;
|
|
19
|
+
let minY = Infinity;
|
|
20
|
+
let maxX = -Infinity;
|
|
21
|
+
let maxY = -Infinity;
|
|
22
|
+
for (const node of canvas.nodes) {
|
|
23
|
+
const x = node.x ?? 0;
|
|
24
|
+
const y = node.y ?? 0;
|
|
25
|
+
const width = node.width ?? DEFAULT_NODE_WIDTH;
|
|
26
|
+
const height = node.height ?? DEFAULT_NODE_HEIGHT;
|
|
27
|
+
minX = Math.min(minX, x);
|
|
28
|
+
minY = Math.min(minY, y);
|
|
29
|
+
maxX = Math.max(maxX, x + width);
|
|
30
|
+
maxY = Math.max(maxY, y + height);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
minX,
|
|
34
|
+
minY,
|
|
35
|
+
maxX,
|
|
36
|
+
maxY,
|
|
37
|
+
width: maxX - minX,
|
|
38
|
+
height: maxY - minY,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Calculate a recommended display size for a canvas cell.
|
|
43
|
+
* Adds padding and enforces minimum dimensions.
|
|
44
|
+
*/
|
|
45
|
+
export function getCanvasDisplaySize(canvas, options = {}) {
|
|
46
|
+
const { padding = 100, minWidth = 300, minHeight = 200, maxWidth = 1200, maxHeight = 800, } = options;
|
|
47
|
+
const bounds = getCanvasBounds(canvas);
|
|
48
|
+
// Add padding to content dimensions
|
|
49
|
+
const contentWidth = bounds.width + padding * 2;
|
|
50
|
+
const contentHeight = bounds.height + padding * 2;
|
|
51
|
+
// Clamp to min/max
|
|
52
|
+
const width = Math.min(maxWidth, Math.max(minWidth, contentWidth));
|
|
53
|
+
const height = Math.min(maxHeight, Math.max(minHeight, contentHeight));
|
|
54
|
+
return { width, height };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=canvasBounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canvasBounds.js","sourceRoot":"","sources":["../../src/utils/canvasBounds.ts"],"names":[],"mappings":"AAWA,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAC/B,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAsB;IACpD,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE;QAC9C,OAAO;YACL,IAAI,EAAE,CAAC;YACP,IAAI,EAAE,CAAC;YACP,IAAI,EAAE,kBAAkB;YACxB,IAAI,EAAE,mBAAmB;YACzB,KAAK,EAAE,kBAAkB;YACzB,MAAM,EAAE,mBAAmB;SAC5B,CAAC;KACH;IAED,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,QAAQ,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IACrB,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC;IAErB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE;QAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,kBAAkB,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,mBAAmB,CAAC;QAElD,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QACzB,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC;QACjC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC;KACnC;IAED,OAAO;QACL,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,IAAI;QACJ,KAAK,EAAE,IAAI,GAAG,IAAI;QAClB,MAAM,EAAE,IAAI,GAAG,IAAI;KACpB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAsB,EACtB,UAMI,EAAE;IAEN,MAAM,EACJ,OAAO,GAAG,GAAG,EACb,QAAQ,GAAG,GAAG,EACd,SAAS,GAAG,GAAG,EACf,QAAQ,GAAG,IAAI,EACf,SAAS,GAAG,GAAG,GAChB,GAAG,OAAO,CAAC;IAEZ,MAAM,MAAM,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;IAEvC,oCAAoC;IACpC,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,GAAG,OAAO,GAAG,CAAC,CAAC;IAChD,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,GAAG,OAAO,GAAG,CAAC,CAAC;IAElD,mBAAmB;IACnB,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC;IAEvE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useMemo, forwardRef } from 'react';
|
|
2
|
+
import type { ExtendedCanvas, ExtendedCanvasNode, ExtendedCanvasEdge } from '@principal-ai/principal-view-core';
|
|
3
|
+
import { GraphRenderer } from './GraphRenderer';
|
|
4
|
+
import type { GraphRendererProps, GraphRendererHandle } from './GraphRenderer';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A canvas placement with its position in the combined coordinate space
|
|
12
|
+
*/
|
|
13
|
+
export interface CanvasPlacement {
|
|
14
|
+
/** Unique identifier for this canvas */
|
|
15
|
+
canvasId: string;
|
|
16
|
+
/** The canvas to include */
|
|
17
|
+
canvas: ExtendedCanvas;
|
|
18
|
+
/** Position offset - all nodes in this canvas will be translated by this amount */
|
|
19
|
+
position: { x: number; y: number };
|
|
20
|
+
/** Optional label for the canvas (for future visual grouping) */
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Layout configuration for multiple canvases on a single graph
|
|
26
|
+
*/
|
|
27
|
+
export interface MultiCanvasLayout {
|
|
28
|
+
/** Array of canvas placements with positions */
|
|
29
|
+
placements: CanvasPlacement[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Props for the MultiCanvasRenderer component
|
|
34
|
+
*/
|
|
35
|
+
export interface MultiCanvasRendererProps
|
|
36
|
+
extends Omit<GraphRendererProps, 'canvas' | 'editable'> {
|
|
37
|
+
/** Layout configuration containing canvas placements */
|
|
38
|
+
layout: MultiCanvasLayout;
|
|
39
|
+
/** Callback when layout changes (canvas positions updated) */
|
|
40
|
+
onLayoutChange?: (layout: MultiCanvasLayout) => void;
|
|
41
|
+
/** Whether to show group borders around each canvas (default: true) */
|
|
42
|
+
showGroups?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Canvas Merging Utilities
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
const DEFAULT_NODE_WIDTH = 200;
|
|
50
|
+
const DEFAULT_NODE_HEIGHT = 100;
|
|
51
|
+
const GROUP_PADDING = 40;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a prefixed node ID to avoid collisions between canvases
|
|
55
|
+
*/
|
|
56
|
+
function prefixNodeId(canvasId: string, nodeId: string): string {
|
|
57
|
+
return `${canvasId}:${nodeId}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Calculate the bounding box of nodes in a canvas (relative to canvas origin)
|
|
62
|
+
*/
|
|
63
|
+
function calculateCanvasBounds(canvas: ExtendedCanvas): {
|
|
64
|
+
minX: number;
|
|
65
|
+
minY: number;
|
|
66
|
+
maxX: number;
|
|
67
|
+
maxY: number;
|
|
68
|
+
} {
|
|
69
|
+
if (!canvas.nodes || canvas.nodes.length === 0) {
|
|
70
|
+
return { minX: 0, minY: 0, maxX: DEFAULT_NODE_WIDTH, maxY: DEFAULT_NODE_HEIGHT };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let minX = Infinity;
|
|
74
|
+
let minY = Infinity;
|
|
75
|
+
let maxX = -Infinity;
|
|
76
|
+
let maxY = -Infinity;
|
|
77
|
+
|
|
78
|
+
for (const node of canvas.nodes) {
|
|
79
|
+
const x = node.x ?? 0;
|
|
80
|
+
const y = node.y ?? 0;
|
|
81
|
+
const width = node.width ?? DEFAULT_NODE_WIDTH;
|
|
82
|
+
const height = node.height ?? DEFAULT_NODE_HEIGHT;
|
|
83
|
+
|
|
84
|
+
minX = Math.min(minX, x);
|
|
85
|
+
minY = Math.min(minY, y);
|
|
86
|
+
maxX = Math.max(maxX, x + width);
|
|
87
|
+
maxY = Math.max(maxY, y + height);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { minX, minY, maxX, maxY };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Merge multiple canvases into a single canvas with position offsets applied.
|
|
95
|
+
* Each canvas gets a group node border with its name as a label.
|
|
96
|
+
* Node IDs are prefixed with canvasId to avoid collisions (format: "canvasId:nodeId").
|
|
97
|
+
* Original IDs can be recovered using parseNodeId().
|
|
98
|
+
*/
|
|
99
|
+
export function mergeCanvases(
|
|
100
|
+
placements: CanvasPlacement[],
|
|
101
|
+
options: { showGroups?: boolean } = {}
|
|
102
|
+
): ExtendedCanvas {
|
|
103
|
+
const { showGroups = true } = options;
|
|
104
|
+
const groupNodes: ExtendedCanvasNode[] = [];
|
|
105
|
+
const mergedNodes: ExtendedCanvasNode[] = [];
|
|
106
|
+
const mergedEdges: ExtendedCanvasEdge[] = [];
|
|
107
|
+
|
|
108
|
+
for (const placement of placements) {
|
|
109
|
+
const { canvasId, canvas, position, label } = placement;
|
|
110
|
+
|
|
111
|
+
// Calculate bounds for this canvas to create a group node
|
|
112
|
+
if (showGroups && canvas.nodes && canvas.nodes.length > 0) {
|
|
113
|
+
const bounds = calculateCanvasBounds(canvas);
|
|
114
|
+
const groupNode: ExtendedCanvasNode = {
|
|
115
|
+
id: `${canvasId}:__group__`,
|
|
116
|
+
type: 'group',
|
|
117
|
+
label: label ?? canvas.pv?.name ?? canvasId,
|
|
118
|
+
x: position.x + bounds.minX - GROUP_PADDING,
|
|
119
|
+
y: position.y + bounds.minY - GROUP_PADDING,
|
|
120
|
+
width: bounds.maxX - bounds.minX + GROUP_PADDING * 2,
|
|
121
|
+
height: bounds.maxY - bounds.minY + GROUP_PADDING * 2,
|
|
122
|
+
};
|
|
123
|
+
groupNodes.push(groupNode);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Process nodes - apply position offset and prefix IDs
|
|
127
|
+
if (canvas.nodes) {
|
|
128
|
+
for (const node of canvas.nodes) {
|
|
129
|
+
const mergedNode: ExtendedCanvasNode = {
|
|
130
|
+
...node,
|
|
131
|
+
id: prefixNodeId(canvasId, node.id),
|
|
132
|
+
x: (node.x ?? 0) + position.x,
|
|
133
|
+
y: (node.y ?? 0) + position.y,
|
|
134
|
+
};
|
|
135
|
+
mergedNodes.push(mergedNode);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Process edges - update node references with prefixed IDs
|
|
140
|
+
if (canvas.edges) {
|
|
141
|
+
for (const edge of canvas.edges) {
|
|
142
|
+
const mergedEdge: ExtendedCanvasEdge = {
|
|
143
|
+
...edge,
|
|
144
|
+
id: prefixNodeId(canvasId, edge.id),
|
|
145
|
+
fromNode: prefixNodeId(canvasId, edge.fromNode),
|
|
146
|
+
toNode: prefixNodeId(canvasId, edge.toNode),
|
|
147
|
+
};
|
|
148
|
+
mergedEdges.push(mergedEdge);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Create merged canvas - group nodes first so they render behind
|
|
154
|
+
const mergedCanvas: ExtendedCanvas = {
|
|
155
|
+
nodes: [...groupNodes, ...mergedNodes],
|
|
156
|
+
edges: mergedEdges,
|
|
157
|
+
pv: {
|
|
158
|
+
version: '1.0.0',
|
|
159
|
+
name: 'Multi-Canvas View',
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return mergedCanvas;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extract the original canvas ID and node ID from a merged node ID
|
|
168
|
+
*/
|
|
169
|
+
export function parseNodeId(
|
|
170
|
+
mergedNodeId: string
|
|
171
|
+
): { canvasId: string; nodeId: string } | null {
|
|
172
|
+
const colonIndex = mergedNodeId.indexOf(':');
|
|
173
|
+
if (colonIndex === -1) return null;
|
|
174
|
+
return {
|
|
175
|
+
canvasId: mergedNodeId.substring(0, colonIndex),
|
|
176
|
+
nodeId: mergedNodeId.substring(colonIndex + 1),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// MultiCanvasRenderer Component
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Renders multiple canvases merged into a single graph view.
|
|
186
|
+
*
|
|
187
|
+
* Each canvas is positioned at its specified offset, and all nodes/edges
|
|
188
|
+
* are combined into a unified viewport. Node IDs are prefixed with their
|
|
189
|
+
* source canvas ID to avoid collisions (format: "canvasId:nodeId").
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```tsx
|
|
193
|
+
* <MultiCanvasRenderer
|
|
194
|
+
* layout={{
|
|
195
|
+
* placements: [
|
|
196
|
+
* { canvasId: 'auth', canvas: authCanvas, position: { x: 0, y: 0 } },
|
|
197
|
+
* { canvasId: 'data', canvas: dataCanvas, position: { x: 600, y: 0 } },
|
|
198
|
+
* ],
|
|
199
|
+
* }}
|
|
200
|
+
* showControls
|
|
201
|
+
* />
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export const MultiCanvasRenderer = forwardRef<
|
|
205
|
+
GraphRendererHandle,
|
|
206
|
+
MultiCanvasRendererProps
|
|
207
|
+
>(function MultiCanvasRenderer(
|
|
208
|
+
{ layout, onLayoutChange: _onLayoutChange, showGroups = true, ...graphRendererProps },
|
|
209
|
+
ref
|
|
210
|
+
) {
|
|
211
|
+
// Merge all canvases into a single canvas
|
|
212
|
+
const mergedCanvas = useMemo(
|
|
213
|
+
() => mergeCanvases(layout.placements, { showGroups }),
|
|
214
|
+
[layout.placements, showGroups]
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<GraphRenderer
|
|
219
|
+
ref={ref}
|
|
220
|
+
canvas={mergedCanvas}
|
|
221
|
+
editable={false}
|
|
222
|
+
{...graphRendererProps}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,13 @@ export type {
|
|
|
37
37
|
PendingChanges,
|
|
38
38
|
} from './components/GraphRenderer';
|
|
39
39
|
|
|
40
|
+
export { MultiCanvasRenderer, mergeCanvases, parseNodeId } from './components/MultiCanvasRenderer';
|
|
41
|
+
export type {
|
|
42
|
+
MultiCanvasRendererProps,
|
|
43
|
+
MultiCanvasLayout,
|
|
44
|
+
CanvasPlacement,
|
|
45
|
+
} from './components/MultiCanvasRenderer';
|
|
46
|
+
|
|
40
47
|
export { ConfigurationSelector } from './components/ConfigurationSelector';
|
|
41
48
|
export type { ConfigurationSelectorProps } from './components/ConfigurationSelector';
|
|
42
49
|
|
|
@@ -70,3 +77,5 @@ export {
|
|
|
70
77
|
swapNodePositions,
|
|
71
78
|
swapEdgeSides,
|
|
72
79
|
} from './utils/orientationUtils';
|
|
80
|
+
export { getCanvasBounds, getCanvasDisplaySize } from './utils/canvasBounds';
|
|
81
|
+
export type { CanvasBounds } from './utils/canvasBounds';
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { MultiCanvasRenderer } from '../components/MultiCanvasRenderer';
|
|
4
|
+
import type { MultiCanvasLayout } from '../components/MultiCanvasRenderer';
|
|
5
|
+
import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
|
|
6
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: 'Components/MultiCanvasRenderer',
|
|
10
|
+
component: MultiCanvasRenderer,
|
|
11
|
+
parameters: {
|
|
12
|
+
layout: 'fullscreen',
|
|
13
|
+
},
|
|
14
|
+
tags: ['autodocs'],
|
|
15
|
+
decorators: [
|
|
16
|
+
(Story) => (
|
|
17
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
18
|
+
<div style={{ width: '100vw', height: '100vh' }}>
|
|
19
|
+
<Story />
|
|
20
|
+
</div>
|
|
21
|
+
</ThemeProvider>
|
|
22
|
+
),
|
|
23
|
+
],
|
|
24
|
+
} satisfies Meta<typeof MultiCanvasRenderer>;
|
|
25
|
+
|
|
26
|
+
export default meta;
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Sample Canvases
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const authFlowCanvas: ExtendedCanvas = {
|
|
34
|
+
nodes: [
|
|
35
|
+
{
|
|
36
|
+
id: 'login',
|
|
37
|
+
type: 'text',
|
|
38
|
+
x: 0,
|
|
39
|
+
y: 0,
|
|
40
|
+
width: 120,
|
|
41
|
+
height: 60,
|
|
42
|
+
text: 'Login',
|
|
43
|
+
color: '#4A90E2',
|
|
44
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'User' },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'validate',
|
|
48
|
+
type: 'text',
|
|
49
|
+
x: 170,
|
|
50
|
+
y: 0,
|
|
51
|
+
width: 120,
|
|
52
|
+
height: 60,
|
|
53
|
+
text: 'Validate',
|
|
54
|
+
color: '#F5A623',
|
|
55
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'Shield' },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'session',
|
|
59
|
+
type: 'text',
|
|
60
|
+
x: 340,
|
|
61
|
+
y: 0,
|
|
62
|
+
width: 120,
|
|
63
|
+
height: 60,
|
|
64
|
+
text: 'Session',
|
|
65
|
+
color: '#7ED321',
|
|
66
|
+
pv: { nodeType: 'data', shape: 'rectangle', icon: 'Key' },
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
edges: [
|
|
70
|
+
{ id: 'e1', fromNode: 'login', toNode: 'validate', pv: { edgeType: 'flow' } },
|
|
71
|
+
{ id: 'e2', fromNode: 'validate', toNode: 'session', pv: { edgeType: 'flow' } },
|
|
72
|
+
],
|
|
73
|
+
pv: {
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
name: 'Authentication Flow',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const dataProcessingCanvas: ExtendedCanvas = {
|
|
80
|
+
nodes: [
|
|
81
|
+
{
|
|
82
|
+
id: 'input',
|
|
83
|
+
type: 'text',
|
|
84
|
+
x: 0,
|
|
85
|
+
y: 0,
|
|
86
|
+
width: 100,
|
|
87
|
+
height: 100,
|
|
88
|
+
text: 'Input',
|
|
89
|
+
color: '#7B68EE',
|
|
90
|
+
pv: { nodeType: 'data', shape: 'circle', icon: 'Download' },
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'transform',
|
|
94
|
+
type: 'text',
|
|
95
|
+
x: 150,
|
|
96
|
+
y: 15,
|
|
97
|
+
width: 140,
|
|
98
|
+
height: 70,
|
|
99
|
+
text: 'Transform',
|
|
100
|
+
color: '#4A90E2',
|
|
101
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'Settings' },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'output',
|
|
105
|
+
type: 'text',
|
|
106
|
+
x: 340,
|
|
107
|
+
y: 0,
|
|
108
|
+
width: 100,
|
|
109
|
+
height: 100,
|
|
110
|
+
text: 'Output',
|
|
111
|
+
color: '#7B68EE',
|
|
112
|
+
pv: { nodeType: 'data', shape: 'circle', icon: 'Upload' },
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
edges: [
|
|
116
|
+
{ id: 'e1', fromNode: 'input', toNode: 'transform', pv: { edgeType: 'dataflow' } },
|
|
117
|
+
{ id: 'e2', fromNode: 'transform', toNode: 'output', pv: { edgeType: 'dataflow' } },
|
|
118
|
+
],
|
|
119
|
+
pv: {
|
|
120
|
+
version: '1.0.0',
|
|
121
|
+
name: 'Data Processing Pipeline',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const notificationCanvas: ExtendedCanvas = {
|
|
126
|
+
nodes: [
|
|
127
|
+
{
|
|
128
|
+
id: 'event',
|
|
129
|
+
type: 'text',
|
|
130
|
+
x: 0,
|
|
131
|
+
y: 30,
|
|
132
|
+
width: 100,
|
|
133
|
+
height: 60,
|
|
134
|
+
text: 'Event',
|
|
135
|
+
color: '#F5A623',
|
|
136
|
+
pv: { nodeType: 'trigger', shape: 'rectangle', icon: 'Zap' },
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'router',
|
|
140
|
+
type: 'text',
|
|
141
|
+
x: 150,
|
|
142
|
+
y: 0,
|
|
143
|
+
width: 100,
|
|
144
|
+
height: 100,
|
|
145
|
+
text: 'Router',
|
|
146
|
+
color: '#4A90E2',
|
|
147
|
+
pv: { nodeType: 'process', shape: 'diamond', icon: 'GitBranch' },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'email',
|
|
151
|
+
type: 'text',
|
|
152
|
+
x: 300,
|
|
153
|
+
y: -30,
|
|
154
|
+
width: 100,
|
|
155
|
+
height: 50,
|
|
156
|
+
text: 'Email',
|
|
157
|
+
color: '#7ED321',
|
|
158
|
+
pv: { nodeType: 'action', shape: 'rectangle', icon: 'Mail' },
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: 'sms',
|
|
162
|
+
type: 'text',
|
|
163
|
+
x: 300,
|
|
164
|
+
y: 40,
|
|
165
|
+
width: 100,
|
|
166
|
+
height: 50,
|
|
167
|
+
text: 'SMS',
|
|
168
|
+
color: '#7ED321',
|
|
169
|
+
pv: { nodeType: 'action', shape: 'rectangle', icon: 'MessageSquare' },
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'push',
|
|
173
|
+
type: 'text',
|
|
174
|
+
x: 300,
|
|
175
|
+
y: 110,
|
|
176
|
+
width: 100,
|
|
177
|
+
height: 50,
|
|
178
|
+
text: 'Push',
|
|
179
|
+
color: '#7ED321',
|
|
180
|
+
pv: { nodeType: 'action', shape: 'rectangle', icon: 'Bell' },
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
edges: [
|
|
184
|
+
{ id: 'e1', fromNode: 'event', toNode: 'router', pv: { edgeType: 'flow' } },
|
|
185
|
+
{ id: 'e2', fromNode: 'router', toNode: 'email', pv: { edgeType: 'flow' } },
|
|
186
|
+
{ id: 'e3', fromNode: 'router', toNode: 'sms', pv: { edgeType: 'flow' } },
|
|
187
|
+
{ id: 'e4', fromNode: 'router', toNode: 'push', pv: { edgeType: 'flow' } },
|
|
188
|
+
],
|
|
189
|
+
pv: {
|
|
190
|
+
version: '1.0.0',
|
|
191
|
+
name: 'Notification System',
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const monitoringCanvas: ExtendedCanvas = {
|
|
196
|
+
nodes: [
|
|
197
|
+
{
|
|
198
|
+
id: 'metrics',
|
|
199
|
+
type: 'text',
|
|
200
|
+
x: 0,
|
|
201
|
+
y: 0,
|
|
202
|
+
width: 100,
|
|
203
|
+
height: 60,
|
|
204
|
+
text: 'Metrics',
|
|
205
|
+
color: '#4A90E2',
|
|
206
|
+
pv: { nodeType: 'data', shape: 'rectangle', icon: 'BarChart2' },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: 'alert',
|
|
210
|
+
type: 'text',
|
|
211
|
+
x: 150,
|
|
212
|
+
y: 0,
|
|
213
|
+
width: 100,
|
|
214
|
+
height: 60,
|
|
215
|
+
text: 'Alert',
|
|
216
|
+
color: '#D0021B',
|
|
217
|
+
pv: { nodeType: 'process', shape: 'rectangle', icon: 'AlertTriangle' },
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
edges: [
|
|
221
|
+
{ id: 'e1', fromNode: 'metrics', toNode: 'alert', pv: { edgeType: 'flow' } },
|
|
222
|
+
],
|
|
223
|
+
pv: {
|
|
224
|
+
version: '1.0.0',
|
|
225
|
+
name: 'Monitoring',
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// ============================================================================
|
|
230
|
+
// Stories
|
|
231
|
+
// ============================================================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Two canvases side by side on the same graph
|
|
235
|
+
*/
|
|
236
|
+
const twoCanvasLayout: MultiCanvasLayout = {
|
|
237
|
+
placements: [
|
|
238
|
+
{ canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
|
|
239
|
+
{ canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 600, y: 0 } },
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const TwoCanvasesSideBySide: Story = {
|
|
244
|
+
args: {
|
|
245
|
+
layout: twoCanvasLayout,
|
|
246
|
+
showControls: true,
|
|
247
|
+
showBackground: true,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Four canvases arranged in a 2x2 grid pattern
|
|
253
|
+
*/
|
|
254
|
+
const fourCanvasLayout: MultiCanvasLayout = {
|
|
255
|
+
placements: [
|
|
256
|
+
{ canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
|
|
257
|
+
{ canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 600, y: 0 } },
|
|
258
|
+
{ canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 200 } },
|
|
259
|
+
{ canvasId: 'monitor', canvas: monitoringCanvas, position: { x: 600, y: 200 } },
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const FourCanvasesGrid: Story = {
|
|
264
|
+
args: {
|
|
265
|
+
layout: fourCanvasLayout,
|
|
266
|
+
showControls: true,
|
|
267
|
+
showBackground: true,
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Canvases stacked vertically
|
|
273
|
+
*/
|
|
274
|
+
const verticalLayout: MultiCanvasLayout = {
|
|
275
|
+
placements: [
|
|
276
|
+
{ canvasId: 'auth', canvas: authFlowCanvas, position: { x: 0, y: 0 } },
|
|
277
|
+
{ canvasId: 'data', canvas: dataProcessingCanvas, position: { x: 0, y: 150 } },
|
|
278
|
+
{ canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 300 } },
|
|
279
|
+
],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export const VerticalStack: Story = {
|
|
283
|
+
args: {
|
|
284
|
+
layout: verticalLayout,
|
|
285
|
+
showControls: true,
|
|
286
|
+
showBackground: true,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Single canvas (baseline comparison)
|
|
292
|
+
*/
|
|
293
|
+
const singleCanvasLayout: MultiCanvasLayout = {
|
|
294
|
+
placements: [
|
|
295
|
+
{ canvasId: 'notify', canvas: notificationCanvas, position: { x: 0, y: 0 } },
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
export const SingleCanvas: Story = {
|
|
300
|
+
args: {
|
|
301
|
+
layout: singleCanvasLayout,
|
|
302
|
+
showControls: true,
|
|
303
|
+
showBackground: true,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* With minimap enabled to navigate the combined view
|
|
309
|
+
*/
|
|
310
|
+
export const WithMinimap: Story = {
|
|
311
|
+
args: {
|
|
312
|
+
layout: fourCanvasLayout,
|
|
313
|
+
showControls: true,
|
|
314
|
+
showMinimap: true,
|
|
315
|
+
showBackground: true,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Without group borders - just the nodes merged together
|
|
321
|
+
*/
|
|
322
|
+
export const WithoutGroups: Story = {
|
|
323
|
+
args: {
|
|
324
|
+
layout: fourCanvasLayout,
|
|
325
|
+
showControls: true,
|
|
326
|
+
showBackground: true,
|
|
327
|
+
showGroups: false,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
|
|
2
|
+
|
|
3
|
+
export interface CanvasBounds {
|
|
4
|
+
minX: number;
|
|
5
|
+
minY: number;
|
|
6
|
+
maxX: number;
|
|
7
|
+
maxY: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DEFAULT_NODE_WIDTH = 200;
|
|
13
|
+
const DEFAULT_NODE_HEIGHT = 100;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Calculate the bounding box of all nodes in a canvas.
|
|
17
|
+
* Returns the min/max coordinates and total dimensions.
|
|
18
|
+
*/
|
|
19
|
+
export function getCanvasBounds(canvas: ExtendedCanvas): CanvasBounds {
|
|
20
|
+
if (!canvas.nodes || canvas.nodes.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
minX: 0,
|
|
23
|
+
minY: 0,
|
|
24
|
+
maxX: DEFAULT_NODE_WIDTH,
|
|
25
|
+
maxY: DEFAULT_NODE_HEIGHT,
|
|
26
|
+
width: DEFAULT_NODE_WIDTH,
|
|
27
|
+
height: DEFAULT_NODE_HEIGHT,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let minX = Infinity;
|
|
32
|
+
let minY = Infinity;
|
|
33
|
+
let maxX = -Infinity;
|
|
34
|
+
let maxY = -Infinity;
|
|
35
|
+
|
|
36
|
+
for (const node of canvas.nodes) {
|
|
37
|
+
const x = node.x ?? 0;
|
|
38
|
+
const y = node.y ?? 0;
|
|
39
|
+
const width = node.width ?? DEFAULT_NODE_WIDTH;
|
|
40
|
+
const height = node.height ?? DEFAULT_NODE_HEIGHT;
|
|
41
|
+
|
|
42
|
+
minX = Math.min(minX, x);
|
|
43
|
+
minY = Math.min(minY, y);
|
|
44
|
+
maxX = Math.max(maxX, x + width);
|
|
45
|
+
maxY = Math.max(maxY, y + height);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
minX,
|
|
50
|
+
minY,
|
|
51
|
+
maxX,
|
|
52
|
+
maxY,
|
|
53
|
+
width: maxX - minX,
|
|
54
|
+
height: maxY - minY,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Calculate a recommended display size for a canvas cell.
|
|
60
|
+
* Adds padding and enforces minimum dimensions.
|
|
61
|
+
*/
|
|
62
|
+
export function getCanvasDisplaySize(
|
|
63
|
+
canvas: ExtendedCanvas,
|
|
64
|
+
options: {
|
|
65
|
+
padding?: number;
|
|
66
|
+
minWidth?: number;
|
|
67
|
+
minHeight?: number;
|
|
68
|
+
maxWidth?: number;
|
|
69
|
+
maxHeight?: number;
|
|
70
|
+
} = {}
|
|
71
|
+
): { width: number; height: number } {
|
|
72
|
+
const {
|
|
73
|
+
padding = 100,
|
|
74
|
+
minWidth = 300,
|
|
75
|
+
minHeight = 200,
|
|
76
|
+
maxWidth = 1200,
|
|
77
|
+
maxHeight = 800,
|
|
78
|
+
} = options;
|
|
79
|
+
|
|
80
|
+
const bounds = getCanvasBounds(canvas);
|
|
81
|
+
|
|
82
|
+
// Add padding to content dimensions
|
|
83
|
+
const contentWidth = bounds.width + padding * 2;
|
|
84
|
+
const contentHeight = bounds.height + padding * 2;
|
|
85
|
+
|
|
86
|
+
// Clamp to min/max
|
|
87
|
+
const width = Math.min(maxWidth, Math.max(minWidth, contentWidth));
|
|
88
|
+
const height = Math.min(maxHeight, Math.max(minHeight, contentHeight));
|
|
89
|
+
|
|
90
|
+
return { width, height };
|
|
91
|
+
}
|