@ankhorage/zora 22.1.1 → 22.1.3

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +1 -1
  3. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.d.ts +3 -3
  4. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js +3 -3
  5. package/dist/features/graph-view/adapters/inbound/web-artifact/GraphView.js.map +1 -1
  6. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.d.ts +6 -5
  7. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.d.ts.map +1 -1
  8. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.js +107 -171
  9. package/dist/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.js.map +1 -1
  10. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts +4 -0
  11. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.d.ts.map +1 -1
  12. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js +8 -0
  13. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphController.js.map +1 -1
  14. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js +5 -1
  15. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.js.map +1 -1
  16. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.d.ts +9 -0
  17. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.d.ts.map +1 -0
  18. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.js +234 -0
  19. package/dist/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.js.map +1 -0
  20. package/dist/types/graph-view-spacing.d.ts +15 -0
  21. package/dist/types/graph-view-spacing.d.ts.map +1 -0
  22. package/dist/types/graph-view-spacing.js +2 -0
  23. package/dist/types/graph-view-spacing.js.map +1 -0
  24. package/dist/types/graph-view.d.ts +4 -1
  25. package/dist/types/graph-view.d.ts.map +1 -1
  26. package/dist/types/graph-view.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/features/graph-view/adapters/inbound/web-artifact/GraphView.tsx +3 -3
  29. package/src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.test.ts +195 -69
  30. package/src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts +162 -241
  31. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.test.ts +2 -0
  32. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphController.ts +8 -0
  33. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphRuntime.ts +5 -1
  34. package/src/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.ts +338 -0
  35. package/src/types/graph-view-spacing.ts +16 -0
  36. package/src/types/graph-view.ts +4 -1
  37. package/web-dist/components/graph-view/GraphView.d.ts +3 -3
  38. package/web-dist/components/graph-view/GraphView.js +237 -126
  39. package/web-dist/components/graph-view/GraphView.types-0.d.ts +4 -1
@@ -122375,82 +122375,35 @@ var EVENT_TYPES = [
122375
122375
  ["mouseout", "pointer-leave"]
122376
122376
  ];
122377
122377
 
122378
- // node_modules/@ankhorage/utility/dist/algorithms/findMinimumAcceptedNumber.js
122379
- var DEFAULT_ITERATIONS = 16;
122380
- var MAX_ITERATIONS = 64;
122381
- function findMinimumAcceptedNumber(input) {
122382
- validateInput(input);
122383
- const { accepts, maximum, minimum } = input;
122384
- const iterations = input.iterations ?? DEFAULT_ITERATIONS;
122385
- if (accepts(minimum))
122386
- return minimum;
122387
- if (!accepts(maximum))
122388
- return;
122389
- return searchAcceptedBoundary(minimum, maximum, iterations, accepts);
122390
- }
122391
- function searchAcceptedBoundary(minimum, maximum, iterations, accepts) {
122392
- let lower = minimum;
122393
- let upper = maximum;
122394
- for (let iteration = 0;iteration < iterations; iteration += 1) {
122395
- const midpoint = lower + (upper - lower) / 2;
122396
- if (midpoint === lower || midpoint === upper)
122397
- break;
122398
- if (accepts(midpoint))
122399
- upper = midpoint;
122400
- else
122401
- lower = midpoint;
122402
- }
122403
- return upper;
122404
- }
122405
- function validateInput(input) {
122406
- const iterations = input.iterations ?? DEFAULT_ITERATIONS;
122407
- if (!Number.isFinite(input.minimum) || !Number.isFinite(input.maximum)) {
122408
- throw new RangeError("Search bounds must be finite numbers.");
122409
- }
122410
- if (input.minimum > input.maximum) {
122411
- throw new RangeError("Search minimum must not exceed maximum.");
122412
- }
122413
- if (!Number.isInteger(iterations) || iterations < 0 || iterations > MAX_ITERATIONS) {
122414
- throw new RangeError(`Search iterations must be an integer from 0 to ${MAX_ITERATIONS}.`);
122415
- }
122416
- }
122417
- // src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts
122418
- var LABEL_OVERLAP_RATIO = 0.12;
122419
- var MAX_EXPANSION_ATTEMPTS = 8;
122420
- var MAX_NODE_COUNT = 2000;
122378
+ // src/features/graph-view/adapters/inbound/web-artifact/createGraphSpacingEvaluator.ts
122421
122379
  var MAX_PAIR_CHECKS = 200000;
122422
- var SEARCH_ITERATIONS = 8;
122423
- function compactGraphSpacing(cy, spacingFactor) {
122380
+ function createGraphSpacingEvaluator(cy, options = {}) {
122424
122381
  const leaves = cy.nodes(":childless");
122425
- if (leaves.length < 2 || cy.nodes().length > MAX_NODE_COUNT || cy.nodes(":locked").length > 0)
122426
- return spacingFactor;
122427
- const bounds = leaves.boundingBox();
122428
- const center = { x: (bounds.x1 + bounds.x2) / 2, y: (bounds.y1 + bounds.y2) / 2 };
122382
+ const viewport = { width: cy.width(), height: cy.height() };
122383
+ if (leaves.length < 2 || viewport.width <= 0 || viewport.height <= 0)
122384
+ return null;
122385
+ const leafBounds = leaves.boundingBox();
122386
+ const center = {
122387
+ x: (leafBounds.x1 + leafBounds.x2) / 2,
122388
+ y: (leafBounds.y1 + leafBounds.y2) / 2
122389
+ };
122429
122390
  const leafGeometry = leaves.map(readLeafGeometry);
122430
- const groups = cy.nodes(":parent").map((node) => readGroupGeometry(node.id(), node.ancestors().map((parent) => parent.id()), node.boundingBox(), leafGeometry));
122431
- const accepts = (factor) => !hasUnsafeCollisions(resolveCollisionBoxes(leafGeometry, groups, center, factor));
122432
- const currentAccepted = accepts(1);
122433
- const minimum = currentAccepted ? Math.min(1, 0.1 / spacingFactor) : 1;
122434
- const maximum = currentAccepted ? 1 : findExpansionMaximum(accepts);
122435
- if (maximum === undefined)
122436
- return spacingFactor;
122437
- const factor = findMinimumAcceptedNumber({
122438
- minimum,
122439
- maximum,
122440
- iterations: SEARCH_ITERATIONS,
122441
- accepts
122442
- });
122443
- if (factor === undefined)
122444
- return spacingFactor;
122445
- applyScale(cy, leafGeometry, center, factor);
122446
- return spacingFactor * factor;
122391
+ const context = {
122392
+ center,
122393
+ fitPadding: Math.max(0, options.fitPadding ?? 50),
122394
+ leaves: leafGeometry,
122395
+ groups: cy.nodes(":parent").map((node) => readGroupGeometry(node, leafGeometry)),
122396
+ maxFitZoom: positiveNumber(options.maxFitZoom, Infinity),
122397
+ viewport
122398
+ };
122399
+ return {
122400
+ evaluate: (factor) => evaluateSpacingCandidate(context, factor),
122401
+ apply: (factor) => applyScale(cy, leafGeometry, center, factor)
122402
+ };
122447
122403
  }
122448
122404
  function readLeafGeometry(node) {
122449
- const nodeBox = node.boundingBox({
122450
- includeLabels: true,
122451
- includeOverlays: false,
122452
- includeUnderlays: false
122453
- });
122405
+ const fitBox = readNodeBox(node, true);
122406
+ const visualBox = readNodeBox(node, false);
122454
122407
  const labelBox = node.boundingBox({
122455
122408
  includeNodes: false,
122456
122409
  includeEdges: false,
@@ -122462,17 +122415,28 @@ function readLeafGeometry(node) {
122462
122415
  id: node.id(),
122463
122416
  ancestors: new Set(node.ancestors().map((parent) => parent.id())),
122464
122417
  position: { ...node.position() },
122465
- labelBox: isUsableBox(labelBox) ? labelBox : nodeBox,
122466
- nodeBox
122418
+ fitBox,
122419
+ labelBox: isUsableBox(labelBox) ? labelBox : null,
122420
+ visualBox: isUsableBox(visualBox) ? visualBox : fitBox
122467
122421
  };
122468
122422
  }
122469
- function readGroupGeometry(id, ancestorIds, parentBox, leaves) {
122470
- const descendants = leaves.filter((leaf) => leaf.ancestors.has(id));
122471
- const descendantUnion = unionBounds(descendants.map((leaf) => leaf.nodeBox)) ?? parentBox;
122423
+ function readNodeBox(node, includeLabels) {
122424
+ return node.boundingBox({
122425
+ includeNodes: true,
122426
+ includeEdges: false,
122427
+ includeLabels,
122428
+ includeOverlays: false,
122429
+ includeUnderlays: false
122430
+ });
122431
+ }
122432
+ function readGroupGeometry(node, leaves) {
122433
+ const descendants = leaves.filter((leaf) => leaf.ancestors.has(node.id()));
122434
+ const parentBox = readNodeBox(node, true);
122435
+ const descendantUnion = unionBounds(descendants.map((leaf) => leaf.fitBox)) ?? parentBox;
122472
122436
  return {
122473
- id,
122474
- ancestors: new Set(ancestorIds),
122475
- descendants,
122437
+ id: node.id(),
122438
+ ancestors: new Set(node.ancestors().map((parent) => parent.id())),
122439
+ descendantIds: descendants.map((leaf) => leaf.id),
122476
122440
  insets: {
122477
122441
  left: Math.max(0, descendantUnion.x1 - parentBox.x1),
122478
122442
  right: Math.max(0, parentBox.x2 - descendantUnion.x2),
@@ -122481,78 +122445,104 @@ function readGroupGeometry(id, ancestorIds, parentBox, leaves) {
122481
122445
  }
122482
122446
  };
122483
122447
  }
122484
- function resolveCollisionBoxes(leaves, groups, center, factor) {
122485
- const labels = leaves.map((leaf) => ({
122448
+ function evaluateSpacingCandidate(context, factor) {
122449
+ const leaves = context.leaves.map((leaf) => resolveLeafGeometry(leaf, context.center, factor));
122450
+ const byId = new Map(leaves.map((leaf) => [leaf.id, leaf]));
122451
+ const groups = context.groups.flatMap((group) => resolveGroupBox(group, byId));
122452
+ const fitBounds = unionBounds([
122453
+ ...leaves.map((leaf) => leaf.fitBox),
122454
+ ...groups.map((group) => group.box)
122455
+ ]);
122456
+ const rawFitZoom = fitBounds === undefined ? 1 : getFitZoom(fitBounds, context);
122457
+ const effectiveFitZoom = Math.min(rawFitZoom, context.maxFitZoom);
122458
+ return {
122459
+ effectiveFitZoom,
122460
+ ...readCollisionState(createCollisionBoxes(leaves, groups), effectiveFitZoom)
122461
+ };
122462
+ }
122463
+ function resolveLeafGeometry(leaf, center, factor) {
122464
+ return {
122486
122465
  id: leaf.id,
122487
122466
  ancestors: leaf.ancestors,
122488
- kind: "label",
122489
- box: shiftBounds(leaf.labelBox, leaf.position, center, factor)
122490
- }));
122491
- const compoundBoxes = groups.flatMap((group) => {
122492
- const descendants = group.descendants.map((leaf) => shiftBounds(leaf.nodeBox, leaf.position, center, factor));
122493
- const union = unionBounds(descendants);
122494
- if (!union)
122495
- return [];
122496
- return [
122497
- {
122498
- id: group.id,
122499
- ancestors: group.ancestors,
122500
- kind: "group",
122501
- box: expandBounds(union, group.insets)
122502
- }
122503
- ];
122467
+ fitBox: shiftBounds(leaf.fitBox, leaf.position, center, factor),
122468
+ labelBox: leaf.labelBox === null ? null : shiftBounds(leaf.labelBox, leaf.position, center, factor),
122469
+ visualBox: shiftBounds(leaf.visualBox, leaf.position, center, factor)
122470
+ };
122471
+ }
122472
+ function resolveGroupBox(group, leaves) {
122473
+ const descendantBounds = group.descendantIds.flatMap((id) => {
122474
+ const leaf = leaves.get(id);
122475
+ return leaf === undefined ? [] : [leaf.fitBox];
122504
122476
  });
122505
- return [...labels, ...compoundBoxes];
122477
+ const union = unionBounds(descendantBounds);
122478
+ return union === undefined ? [] : [
122479
+ {
122480
+ id: group.id,
122481
+ ancestors: group.ancestors,
122482
+ kind: "group",
122483
+ box: expandBounds(union, group.insets)
122484
+ }
122485
+ ];
122506
122486
  }
122507
- function findExpansionMaximum(accepts) {
122508
- return Array.from({ length: MAX_EXPANSION_ATTEMPTS }, (_, index) => 2 ** (index + 1)).find(accepts);
122487
+ function createCollisionBoxes(leaves, groups) {
122488
+ return [
122489
+ ...leaves.flatMap((leaf) => leaf.labelBox === null ? [] : [{ id: leaf.id, ancestors: leaf.ancestors, kind: "label", box: leaf.labelBox }]),
122490
+ ...leaves.map((leaf) => ({
122491
+ id: leaf.id,
122492
+ ancestors: leaf.ancestors,
122493
+ kind: "visual",
122494
+ box: leaf.visualBox
122495
+ })),
122496
+ ...groups
122497
+ ];
122509
122498
  }
122510
- function hasUnsafeCollisions(boxes) {
122499
+ function readCollisionState(boxes, zoom) {
122511
122500
  const sorted = [...boxes].sort((first, second) => first.box.x1 - second.box.x1);
122512
122501
  const budget = { remaining: MAX_PAIR_CHECKS };
122502
+ const overlap = { maximum: 0 };
122513
122503
  for (const [index, first] of sorted.entries()) {
122514
122504
  for (let next = index + 1;next < sorted.length; next += 1) {
122515
122505
  const second = sorted.at(next);
122516
- if (!second)
122506
+ if (second === undefined)
122517
122507
  break;
122518
122508
  if (second.box.x1 >= first.box.x2)
122519
122509
  break;
122510
+ if (first.id === second.id || isIntentionalContainment(first, second))
122511
+ continue;
122512
+ const penetration = getOverlapPenetration(first.box, second.box);
122513
+ if (penetration <= 0)
122514
+ continue;
122520
122515
  budget.remaining -= 1;
122521
122516
  if (budget.remaining < 0)
122522
- return true;
122523
- if (isIntentionalContainment(first, second))
122517
+ return { hardCollision: true, renderedBackgroundOverlap: Infinity };
122518
+ if (first.kind === "visual" && second.kind === "visual") {
122519
+ overlap.maximum = Math.max(overlap.maximum, penetration * zoom);
122524
122520
  continue;
122525
- if (isUnsafeOverlap(first, second))
122526
- return true;
122521
+ }
122522
+ return { hardCollision: true, renderedBackgroundOverlap: overlap.maximum };
122527
122523
  }
122528
122524
  }
122529
- return false;
122525
+ return { hardCollision: false, renderedBackgroundOverlap: overlap.maximum };
122530
122526
  }
122531
122527
  function isIntentionalContainment(first, second) {
122532
122528
  return first.ancestors.has(second.id) || second.ancestors.has(first.id);
122533
122529
  }
122534
- function isUnsafeOverlap(first, second) {
122535
- const overlapWidth = Math.min(first.box.x2, second.box.x2) - Math.max(first.box.x1, second.box.x1);
122536
- const overlapHeight = Math.min(first.box.y2, second.box.y2) - Math.max(first.box.y1, second.box.y1);
122537
- if (overlapWidth <= 0 || overlapHeight <= 0)
122538
- return false;
122539
- if (first.kind !== "label" || second.kind !== "label")
122540
- return true;
122541
- const widthRatio = overlapWidth / Math.min(first.box.w, second.box.w);
122542
- const heightRatio = overlapHeight / Math.min(first.box.h, second.box.h);
122543
- return widthRatio > LABEL_OVERLAP_RATIO && heightRatio > LABEL_OVERLAP_RATIO;
122530
+ function getOverlapPenetration(first, second) {
122531
+ const width = Math.min(first.x2, second.x2) - Math.max(first.x1, second.x1);
122532
+ const height = Math.min(first.y2, second.y2) - Math.max(first.y1, second.y1);
122533
+ return width > 0 && height > 0 ? Math.min(width, height) : 0;
122534
+ }
122535
+ function getFitZoom(bounds, context) {
122536
+ const width = context.viewport.width - 2 * context.fitPadding;
122537
+ const height = context.viewport.height - 2 * context.fitPadding;
122538
+ if (width <= 0 || height <= 0 || bounds.w <= 0 || bounds.h <= 0)
122539
+ return 1;
122540
+ return Math.min(width / bounds.w, height / bounds.h);
122544
122541
  }
122545
122542
  function shiftBounds(box, position, center, factor) {
122546
122543
  const dx = (position.x - center.x) * (factor - 1);
122547
122544
  const dy = (position.y - center.y) * (factor - 1);
122548
- return {
122549
- x1: box.x1 + dx,
122550
- y1: box.y1 + dy,
122551
- x2: box.x2 + dx,
122552
- y2: box.y2 + dy,
122553
- w: box.w,
122554
- h: box.h
122555
- };
122545
+ return { ...box, x1: box.x1 + dx, y1: box.y1 + dy, x2: box.x2 + dx, y2: box.y2 + dy };
122556
122546
  }
122557
122547
  function expandBounds(box, insets) {
122558
122548
  const x1 = box.x1 - insets.left;
@@ -122573,6 +122563,9 @@ function unionBounds(boxes) {
122573
122563
  function isUsableBox(box) {
122574
122564
  return box.w > 0 && box.h > 0;
122575
122565
  }
122566
+ function positiveNumber(value, fallback) {
122567
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
122568
+ }
122576
122569
  function applyScale(cy, leaves, center, factor) {
122577
122570
  const positions = new Map(leaves.map((leaf) => [leaf.id, leaf.position]));
122578
122571
  cy.batch(() => {
@@ -122586,6 +122579,114 @@ function applyScale(cy, leaves, center, factor) {
122586
122579
  });
122587
122580
  }
122588
122581
 
122582
+ // src/features/graph-view/adapters/inbound/web-artifact/compactGraphSpacing.ts
122583
+ var MAX_EXPANSION_ATTEMPTS = 8;
122584
+ var MAX_NODE_COUNT = 2000;
122585
+ var MAX_RENDERED_BACKGROUND_OVERLAP = 4;
122586
+ var SEARCH_ITERATIONS = 8;
122587
+ var ZERO_TOLERANCE = 0.001;
122588
+ function compactGraphSpacing(cy, spacingFactor, options = {}) {
122589
+ if (spacingFactor <= 0 || cy.nodes(":childless").length < 2 || cy.nodes().length > MAX_NODE_COUNT || cy.nodes(":locked").length > 0)
122590
+ return spacingFactor;
122591
+ const evaluator = createGraphSpacingEvaluator(cy, options);
122592
+ if (evaluator === null)
122593
+ return spacingFactor;
122594
+ const current = evaluator.evaluate(1);
122595
+ const maxFitZoom = options.maxFitZoom ?? Infinity;
122596
+ const minReadableZoom = options.minReadableZoom ?? 0;
122597
+ const factor = needsExpansion(current) ? chooseExpansionFactor(evaluator.evaluate, minReadableZoom) : chooseCompactionFactor(evaluator.evaluate, spacingFactor, maxFitZoom, minReadableZoom, current);
122598
+ if (factor === 1)
122599
+ return spacingFactor;
122600
+ evaluator.apply(factor);
122601
+ return spacingFactor * factor;
122602
+ }
122603
+ function chooseCompactionFactor(evaluate, spacingFactor, maxFitZoom, minReadableZoom, current) {
122604
+ if (reachesFitTarget(current, maxFitZoom))
122605
+ return 1;
122606
+ const minimum = Math.min(1, 0.1 / spacingFactor);
122607
+ if (current.renderedBackgroundOverlap > ZERO_TOLERANCE) {
122608
+ if (reachesReadableTarget(current, minReadableZoom))
122609
+ return 1;
122610
+ return chooseSoftCompactionFactor(evaluate, minimum, 1, minReadableZoom);
122611
+ }
122612
+ const strictMinimum = findMinimumAcceptedFactor(minimum, 1, (factor) => isStrictCandidate(evaluate(factor)));
122613
+ if (strictMinimum === undefined)
122614
+ return 1;
122615
+ const strict = evaluate(strictMinimum);
122616
+ if (reachesFitTarget(strict, maxFitZoom)) {
122617
+ return findMaximumAcceptedFactor(strictMinimum, 1, (factor) => {
122618
+ const candidate = evaluate(factor);
122619
+ return isStrictCandidate(candidate) && reachesFitTarget(candidate, maxFitZoom);
122620
+ }) ?? strictMinimum;
122621
+ }
122622
+ if (reachesReadableTarget(strict, minReadableZoom))
122623
+ return strictMinimum;
122624
+ return chooseSoftCompactionFactor(evaluate, minimum, strictMinimum, minReadableZoom);
122625
+ }
122626
+ function chooseSoftCompactionFactor(evaluate, minimum, maximum, minReadableZoom) {
122627
+ const softMinimum = findMinimumAcceptedFactor(minimum, maximum, (factor) => isSoftCandidate(evaluate(factor)));
122628
+ if (softMinimum === undefined)
122629
+ return maximum;
122630
+ const soft = evaluate(softMinimum);
122631
+ if (!reachesReadableTarget(soft, minReadableZoom))
122632
+ return softMinimum;
122633
+ return findMaximumAcceptedFactor(softMinimum, maximum, (factor) => {
122634
+ const candidate = evaluate(factor);
122635
+ return isSoftCandidate(candidate) && reachesReadableTarget(candidate, minReadableZoom);
122636
+ }) ?? softMinimum;
122637
+ }
122638
+ function chooseExpansionFactor(evaluate, minReadableZoom) {
122639
+ const strict = findExpansionFactor((factor) => isStrictCandidate(evaluate(factor)));
122640
+ if (strict !== undefined && reachesReadableTarget(evaluate(strict), minReadableZoom))
122641
+ return strict;
122642
+ return findExpansionFactor((factor) => isSoftCandidate(evaluate(factor))) ?? strict ?? 1;
122643
+ }
122644
+ function isStrictCandidate(candidate) {
122645
+ return !candidate.hardCollision && candidate.renderedBackgroundOverlap <= ZERO_TOLERANCE;
122646
+ }
122647
+ function isSoftCandidate(candidate) {
122648
+ return !candidate.hardCollision && candidate.renderedBackgroundOverlap <= MAX_RENDERED_BACKGROUND_OVERLAP + ZERO_TOLERANCE;
122649
+ }
122650
+ function needsExpansion(candidate) {
122651
+ return candidate.hardCollision || !isSoftCandidate(candidate);
122652
+ }
122653
+ function reachesFitTarget(candidate, maxFitZoom) {
122654
+ if (!Number.isFinite(maxFitZoom))
122655
+ return false;
122656
+ return candidate.effectiveFitZoom >= maxFitZoom - ZERO_TOLERANCE;
122657
+ }
122658
+ function reachesReadableTarget(candidate, minReadableZoom) {
122659
+ return minReadableZoom <= 0 || candidate.effectiveFitZoom >= minReadableZoom - ZERO_TOLERANCE;
122660
+ }
122661
+ function findMinimumAcceptedFactor(minimum, maximum, accepts) {
122662
+ if (!accepts(maximum))
122663
+ return;
122664
+ if (accepts(minimum))
122665
+ return minimum;
122666
+ return Array.from({ length: SEARCH_ITERATIONS }).reduce((range) => {
122667
+ const factor = (range.low + range.high) / 2;
122668
+ return accepts(factor) ? { low: range.low, high: factor } : { low: factor, high: range.high };
122669
+ }, { low: minimum, high: maximum }).high;
122670
+ }
122671
+ function findMaximumAcceptedFactor(minimum, maximum, accepts) {
122672
+ if (!accepts(minimum))
122673
+ return;
122674
+ if (accepts(maximum))
122675
+ return maximum;
122676
+ return Array.from({ length: SEARCH_ITERATIONS }).reduce((range) => {
122677
+ const factor = (range.low + range.high) / 2;
122678
+ return accepts(factor) ? { low: factor, high: range.high } : { low: range.low, high: factor };
122679
+ }, { low: minimum, high: maximum }).low;
122680
+ }
122681
+ function findExpansionFactor(accepts) {
122682
+ if (accepts(1))
122683
+ return 1;
122684
+ const maximum = Array.from({ length: MAX_EXPANSION_ATTEMPTS }, (_, index) => 2 ** (index + 1)).find(accepts);
122685
+ if (maximum === undefined)
122686
+ return;
122687
+ return findMinimumAcceptedFactor(maximum / 2, maximum, accepts);
122688
+ }
122689
+
122589
122690
  // src/features/graph-view/adapters/inbound/web-artifact/fitGraphViewport.ts
122590
122691
  function fitGraphViewport(cy, options = {}, maxZoom = Infinity) {
122591
122692
  if (cy.destroyed())
@@ -122661,6 +122762,12 @@ function createGraphController(cy, fitPaddingRef, optimizeFit) {
122661
122762
  state.scale = 1;
122662
122763
  applyZoomLimits(cy, state);
122663
122764
  },
122765
+ getFitZoomLimit() {
122766
+ return state.relative ? state.fitZoom : Math.min(state.fitZoom, state.max);
122767
+ },
122768
+ getReadableZoomTarget() {
122769
+ return state.readableZoom;
122770
+ },
122664
122771
  settle(fit) {
122665
122772
  const logicalZoom = cy.zoom() / state.scale;
122666
122773
  const fonts = cy.nodes().filter((node) => !node.isParent() && Boolean(node.style("label"))).map((node) => Number.parseFloat(String(node.style("font-size")))).filter((size) => Number.isFinite(size) && size > 0);
@@ -123379,7 +123486,11 @@ function requestOptimizedFit(state, options) {
123379
123486
  if (!input)
123380
123487
  return;
123381
123488
  const previous = state.optimizedSpacingRef.current ?? input.spacingFactor ?? 1;
123382
- const spacing = compactGraphSpacing(state.cy, previous);
123489
+ const spacing = compactGraphSpacing(state.cy, previous, {
123490
+ fitPadding: options.padding ?? state.fitPaddingRef.current,
123491
+ maxFitZoom: state.viewport.getFitZoomLimit(),
123492
+ minReadableZoom: state.viewport.getReadableZoomTarget()
123493
+ });
123383
123494
  state.geometryRef.current = getGraphGeometryKey(state.cy);
123384
123495
  state.loopSizer.update();
123385
123496
  state.viewport.settle(false);
@@ -54,7 +54,10 @@ export interface GraphViewViewport {
54
54
  readonly pan: GraphViewPoint;
55
55
  }
56
56
  export interface GraphViewFitOptions {
57
- /** Optimize existing layout spacing without rerunning its algorithm; whole-graph fits only. */
57
+ /**
58
+ * Optimize effective rendered fit without rerunning the layout algorithm; whole-graph fits only.
59
+ * Stops at the configured fit-size target and keeps semantic labels collision-safe.
60
+ */
58
61
  readonly optimizeSpacing?: boolean;
59
62
  readonly nodeIds?: readonly string[];
60
63
  readonly padding?: number;