function applyGravity()

in src/state/layout/hierarchicalLayout.ts [315:416]


function applyGravity(directions: NodeDirection[], gravityAngle: number, gravityStrength: number): void {
  // No nodes or no gravity, nothing to do here.
  if (!directions.length || gravityStrength <= 0) {
    return;
  }

  // If it's one node and it's free, move it to `gravityAngle`.
  // If it's fixed, nothing to do.
  if (directions.length === 1) {
    if (!directions[0].isFixed) {
      directions[0].angle = gravityAngle;
    }
    return;
  }

  const incIndex = (i: number) => ((i + 1) % directions.length);
  const decIndex = (i: number) => (i === 0 ? (directions.length - 1) : (i - 1));

  // Find two nodes exactly before and after `gravityAngle`.
  let nextIndex = 0;
  while (nextIndex < directions.length - 1 && directions[nextIndex].angle < gravityAngle) {
    nextIndex += 1;
  }
  if (directions[nextIndex].angle < gravityAngle) {
    nextIndex = 0;
  }

  const previousIndex = decIndex(nextIndex);

  const anglePrevious = directions[previousIndex].angle;
  const angleNext = directions[nextIndex].angle;

  // Push `previousIndex` and `nextIndex` towards `gravityAngle` with a "special care":
  // Consider which of them are fixed and which are free and push the closest right to `gravityAngle`,
  // while also ensuring that we don't press towards the neighbor too hard.

  if (directions[previousIndex].isFixed && directions[nextIndex].isFixed) {
    // Both sides are fixed, nothing to do.
  } else if (directions[previousIndex].isFixed) {
    // If `previousIndex` is fixed, move the free `nextIndex` towards gravity.
    const pushedAngle = pushTowardsAngle(angleNext, anglePrevious, gravityStrength);
    if (angleBetween(gravityAngle, anglePrevious) >= angleBetween(pushedAngle, anglePrevious)) {
      // If moving node directly to `gravityAngle` won't squeeze it too hard to neighbor, let's do it.
      directions[nextIndex].angle = gravityAngle;
    } else {
      // However if it gets too close, use regular `pushTowardsAngle` formula instead.
      directions[nextIndex].angle = pushedAngle;
    }
  } else if (directions[nextIndex].isFixed) {
    // If `nextIndex` is fixed, move the free `previousIndex` towards gravity.
    const pushedAngle = pushTowardsAngle(anglePrevious, angleNext, gravityStrength);
    if (angleBetween(gravityAngle, angleNext) >= angleBetween(pushedAngle, angleNext)) {
      // If moving node directly to `gravityAngle` won't squeeze it too hard to neighbor, let's do it.
      directions[previousIndex].angle = gravityAngle;
    } else {
      // However if it gets too close, use regular `pushTowardsAngle` formula instead.
      directions[previousIndex].angle = pushedAngle;
    }
  } else {
    // Both `previousIndex` and `nextIndex` are free.
    const anglePreviousToGravity = angleBetween(anglePrevious, gravityAngle);
    const angleNextToGravity = angleBetween(angleNext, gravityAngle);
    if (Math.abs(angleNextToGravity - anglePreviousToGravity) < 0.01) {
      // Two free nodes are equally close to gravity. Move them equally closer, based on `gravityStrength`.
      directions[previousIndex].angle =
        pushTowardsAngle(directions[previousIndex].angle, gravityAngle, gravityStrength);

      directions[nextIndex].angle =
        pushTowardsAngle(directions[nextIndex].angle, gravityAngle, gravityStrength);
    } else if (anglePreviousToGravity <= angleNextToGravity) {
      // `previousIndex` is closer to `gravityAngle`. Move it right to `gravityAngle`.
      directions[previousIndex].angle = gravityAngle;
      directions[nextIndex].angle = pushTowardsAngle(angleNext, gravityAngle, gravityStrength);
    } else {
      // `nextIndex` is closer to `gravityAngle`. Move it right to `gravityAngle`.
      directions[nextIndex].angle = gravityAngle;
      directions[previousIndex].angle = pushTowardsAngle(anglePrevious, gravityAngle, gravityStrength);
    }
  }

  // Going counter-clockwise until we reach the diamterically opposite angle to `gravityAngle`.
  let index = incIndex(nextIndex);
  while (index !== previousIndex && isLeftTurn(gravityAngle, directions[index].angle)) {
    if (!directions[index].isFixed) {
      directions[index].angle =
        pushTowardsAngle(directions[index].angle, directions[decIndex(index)].angle, gravityStrength);
    }
    index = incIndex(index);
  }

  // Going clockwise until we reach the diamterically opposite angle to `gravityAngle`.
  index = decIndex(previousIndex);
  while (index !== nextIndex && !isLeftTurn(gravityAngle, directions[index].angle)) {
    if (!directions[index].isFixed) {
      directions[index].angle =
        pushTowardsAngle(directions[index].angle, directions[incIndex(index)].angle, gravityStrength);
    }
    index = decIndex(index);
  }

  sortNodeDirections(directions);
}