@principal-ai/principal-view-react 0.6.11 → 0.6.13

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.
@@ -577,6 +577,7 @@ const EditableTemplate = () => {
577
577
  width={800}
578
578
  height={500}
579
579
  editable={true}
580
+ autoUpdateEdgeSides={true}
580
581
  onPendingChangesChange={setHasChanges}
581
582
  />
582
583
  </div>
@@ -52,6 +52,23 @@ export interface EdgeStateWithHandles extends EdgeState {
52
52
  targetHandle?: string;
53
53
  }
54
54
 
55
+ /**
56
+ * Map canvas side to source handle ID
57
+ * Source handles use '-out' suffix
58
+ */
59
+ function sideToSourceHandle(side?: string): string | undefined {
60
+ if (!side) return undefined;
61
+ return `${side}-out`;
62
+ }
63
+
64
+ /**
65
+ * Map canvas side to target handle ID
66
+ * Target handles use the side name directly
67
+ */
68
+ function sideToTargetHandle(side?: string): string | undefined {
69
+ return side;
70
+ }
71
+
55
72
  /**
56
73
  * Convert our EdgeState to xyflow Edge format
57
74
  */
@@ -71,6 +88,12 @@ export function convertToXYFlowEdges(
71
88
  const hasViolations = violations.some((v) => v.context?.edgeId === edge.id);
72
89
  const edgeWithHandles = edge as EdgeStateWithHandles;
73
90
 
91
+ // Get handle IDs from edge data (fromSide/toSide) or explicit handles
92
+ const fromSide = edge.data?.fromSide as string | undefined;
93
+ const toSide = edge.data?.toSide as string | undefined;
94
+ const sourceHandle = edgeWithHandles.sourceHandle || sideToSourceHandle(fromSide);
95
+ const targetHandle = edgeWithHandles.targetHandle || sideToTargetHandle(toSide);
96
+
74
97
  // Add arrow marker if edge type is directed
75
98
  // Color priority: edge data color > type definition color > default
76
99
  const edgeColor = edge.data?.color as string | undefined;
@@ -88,8 +111,8 @@ export function convertToXYFlowEdges(
88
111
  id: edge.id,
89
112
  source: edge.from,
90
113
  target: edge.to,
91
- sourceHandle: edgeWithHandles.sourceHandle,
92
- targetHandle: edgeWithHandles.targetHandle,
114
+ sourceHandle,
115
+ targetHandle,
93
116
  type: 'custom',
94
117
  animated: typeDefinition?.style === 'animated',
95
118
  markerEnd,
@@ -97,6 +120,7 @@ export function convertToXYFlowEdges(
97
120
  typeDefinition,
98
121
  hasViolations,
99
122
  data: edge.data,
123
+ edgeType: edge.type,
100
124
  },
101
125
  };
102
126
  });
@@ -234,3 +258,75 @@ function applyCircularLayout<T extends Record<string, unknown>>(nodes: Node<T>[]
234
258
  };
235
259
  });
236
260
  }
261
+
262
+ /**
263
+ * Check if there is a cycle between two nodes (i.e., a path from target back to source).
264
+ * Returns true if adding an edge from `from` to `to` would complete a cycle.
265
+ */
266
+ export function hasCycleBetweenNodes(
267
+ from: string,
268
+ to: string,
269
+ edges: Array<{ from: string; to: string }>
270
+ ): boolean {
271
+ // Check if there's already a path from `to` back to `from`
272
+ // using BFS/DFS from `to` node
273
+ const visited = new Set<string>();
274
+ const queue = [to];
275
+
276
+ while (queue.length > 0) {
277
+ const current = queue.shift()!;
278
+ if (current === from) {
279
+ return true; // Found a path back, there would be a cycle
280
+ }
281
+ if (visited.has(current)) {
282
+ continue;
283
+ }
284
+ visited.add(current);
285
+
286
+ // Find all outgoing edges from current node
287
+ for (const edge of edges) {
288
+ if (edge.from === current && !visited.has(edge.to)) {
289
+ queue.push(edge.to);
290
+ }
291
+ }
292
+ }
293
+
294
+ return false;
295
+ }
296
+
297
+ export type CanvasSide = 'top' | 'right' | 'bottom' | 'left';
298
+
299
+ /**
300
+ * Compute optimal edge sides based on relative node positions.
301
+ * Returns the best fromSide and toSide for connecting two nodes.
302
+ */
303
+ export function computeOptimalEdgeSides(
304
+ fromPosition: { x: number; y: number },
305
+ toPosition: { x: number; y: number }
306
+ ): { fromSide: CanvasSide; toSide: CanvasSide } {
307
+ const dx = toPosition.x - fromPosition.x;
308
+ const dy = toPosition.y - fromPosition.y;
309
+
310
+ // Determine primary direction based on which axis has larger delta
311
+ const isHorizontalDominant = Math.abs(dx) > Math.abs(dy);
312
+
313
+ if (isHorizontalDominant) {
314
+ // Horizontal connection
315
+ if (dx > 0) {
316
+ // Target is to the right of source
317
+ return { fromSide: 'right', toSide: 'left' };
318
+ } else {
319
+ // Target is to the left of source
320
+ return { fromSide: 'left', toSide: 'right' };
321
+ }
322
+ } else {
323
+ // Vertical connection
324
+ if (dy > 0) {
325
+ // Target is below source
326
+ return { fromSide: 'bottom', toSide: 'top' };
327
+ } else {
328
+ // Target is above source
329
+ return { fromSide: 'top', toSide: 'bottom' };
330
+ }
331
+ }
332
+ }