@principal-ai/subsystems-react 0.51.3 → 0.51.4

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.
@@ -330,6 +330,70 @@ export function calculatePathLength(points: Point[]): number {
330
330
  return total;
331
331
  }
332
332
 
333
+ /**
334
+ * Point a given fraction along a polyline, by arc length.
335
+ *
336
+ * Using arc length, not the nearest-point snap, is what keeps a label chip on
337
+ * the visual middle of a *bent* route. Nearest-point (`closestPointOnPath`)
338
+ * jumps to whichever segment happens to be nearest the target, so a fanned-out
339
+ * orthogonal route put the chip on a corner near the source node.
340
+ *
341
+ * @public Exported for testing
342
+ */
343
+ export function pointAlongPath(points: Point[], fraction: number): Point {
344
+ if (points.length === 0) return { x: 0, y: 0 };
345
+ if (points.length === 1) return points[0];
346
+ const total = calculatePathLength(points);
347
+ if (total === 0) return points[0];
348
+
349
+ let remaining = Math.max(0, Math.min(1, fraction)) * total;
350
+ for (let i = 1; i < points.length; i++) {
351
+ const a = points[i - 1];
352
+ const b = points[i];
353
+ const seg = Math.hypot(b.x - a.x, b.y - a.y);
354
+ if (seg === 0) continue;
355
+ if (remaining <= seg) {
356
+ const t = remaining / seg;
357
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
358
+ }
359
+ remaining -= seg;
360
+ }
361
+ return points[points.length - 1];
362
+ }
363
+
364
+ /**
365
+ * How far along a polyline a point lies, as a 0…1 fraction of arc length.
366
+ * The point is assumed to lie on (or near) the path.
367
+ *
368
+ * @public Exported for testing
369
+ */
370
+ export function fractionAlongPath(points: Point[], at: Point): number {
371
+ if (points.length < 2) return 0.5;
372
+ const total = calculatePathLength(points);
373
+ if (total === 0) return 0.5;
374
+
375
+ let travelled = 0;
376
+ let bestDist = Infinity;
377
+ let bestTravelled = 0;
378
+ for (let i = 1; i < points.length; i++) {
379
+ const a = points[i - 1];
380
+ const b = points[i];
381
+ const seg = Math.hypot(b.x - a.x, b.y - a.y);
382
+ const dx = b.x - a.x;
383
+ const dy = b.y - a.y;
384
+ const t = seg === 0 ? 0 : Math.max(0, Math.min(1, ((at.x - a.x) * dx + (at.y - a.y) * dy) / (seg * seg)));
385
+ const px = a.x + dx * t;
386
+ const py = a.y + dy * t;
387
+ const dist = Math.hypot(at.x - px, at.y - py);
388
+ if (dist < bestDist) {
389
+ bestDist = dist;
390
+ bestTravelled = travelled + seg * t;
391
+ }
392
+ travelled += seg;
393
+ }
394
+ return bestTravelled / total;
395
+ }
396
+
333
397
  /**
334
398
  * Get ELK layout options based on configuration
335
399
  */
@@ -958,10 +1022,14 @@ export async function computeElkLayout(
958
1022
  lx += sourceOffset.x + (targetOffset.x - sourceOffset.x) * t;
959
1023
  ly += sourceOffset.y + (targetOffset.y - sourceOffset.y) * t;
960
1024
  }
961
- // Snap onto the polyline stroke. ELK's label box can sit slightly
962
- // off the route (side selection / reserved label space); we keep
963
- // its along-edge placement but center on the actual path.
964
- const onPath = closestPointOnPath(allPoints, { x: lx, y: ly });
1025
+ // Place the chip by arc length, using ELK's label position only as a
1026
+ // hint for *how far along* the route it belongs. Snapping to the
1027
+ // nearest point (the old behaviour) jumped to whichever segment was
1028
+ // nearest the target, which put fanned-out orthogonal labels on a
1029
+ // corner near the source instead of the middle of the line.
1030
+ const hint = closestPointOnPath(allPoints, { x: lx, y: ly });
1031
+ const hintFraction = fractionAlongPath(allPoints, hint);
1032
+ const onPath = pointAlongPath(allPoints, hintFraction);
965
1033
  edgeLabelPositions.set(edge.id, onPath);
966
1034
  }
967
1035