생성위치 알고리즘 정하기

첫번째 시도

처음에는 노드들 위치의 평균을 구해 평균 위치에 노드를 추가하는 방식을 생각해 보았습니다.


function averageLocation(children: number[], data: NodeData, parentNode?: Node) {
  if (!children.length) {
    return parentNode ? { x: parentNode.location.x + 30, y: parentNode.location.y + 30 } : { x: 0, y: 0 };
  }
  const x = children.reduce((acc, curr) => data[curr.toString()].location.x + acc, 0) / children.length;
  const y = children.reduce((acc, curr) => data[curr.toString()].location.y + acc, 0) / children.length;
  return { x, y };
}

하지만 노드들의 사이에 생성하게 되면 충돌방지 로직이 있기 때문에 최선의 방법이 아니라 판단하였습니다.

두번째 시도

그래서 이후에 생각했던 방식이 마지막 노드와 상위 노드에 수직벡터를 구해 일정 위치만큼 떨어져 생성하는 방식이었습니다. 그렇게 되면 우선 한쪽 방향으로 생성하기 때문에 덜 겹치게 할 수 있다는 장점이 있었습니다. 아래의 코드를 통해 수직벡터를 구하고 단위벡터에 내가 원하는 위치를 곱해 떨어지게끔 코드를 구현하였습니다.

export function vector(a: Location, b: Location) {
  return { x: b.x - a.x, y: b.y - a.y };
}

export function perpendicularVector(a: Location, b: Location) {
  return { x: a.y - b.y, y: b.x - a.x };
}

export function unitVector(a: Location, b: Location) {
  const v = perpendicularVector(a, b);
  const vectorLength = Math.sqrt(v.x ** 2 + v.y ** 2);
  return { x: v.x / vectorLength, y: v.y / vectorLength };
}

function getNewNodePosition(children: number[], data: NodeData, parentNode: Node) {
  if (!children.length) {
    return parentNode ? { x: parentNode.location.x + 30, y: parentNode.location.y + 30 } : { x: 0, y: 0 };
  }
  const lastChildren = data[children[children.length - 1]];
  const uv = unitVector(parentNode.location, lastChildren.location);

  return {
    x: lastChildren.location.x + uv.x * 50,
    y: lastChildren.location.y + uv.y * 50,
  };
}

하지만 이 문제 또한 많이 아쉬웠습니다. 우선 노드를 생성할때 90도로 생성을 하기 때문에 상위 노드와의 위치가 생성할때마다 늘어나게 됩니다. 그리고 토네이도 돌듯 생성이 되게 됩니다. 그래서 아쉬움이 많았습니다.

최종 개선

마인드맵 재정렬 기능에서도 그렇고 지속적으로 벡터와 관련된 함수를 쓰고 있다는 판단이 들었습니다. 그래서 시작점과 끝점 그리고 그에 맞는 각도와 길이를 주입했을 때 원하는 위치를 반환해주는 벡터함수를 만들어 보았습니다. 코드는 아래와 같습니다.

export function calculateVector(
  rootNodeLocation: Location,
  parentNodeLocation: Location,
  angleDegrees: number,
  magnitude = 1,
) {
  const dx = parentNodeLocation.x - rootNodeLocation.x;
  const dy = parentNodeLocation.y - rootNodeLocation.y;

  const length = Math.sqrt(dx ** 2 + dy ** 2);
  const unitX = dx / length;
  const unitY = dy / length;

  const angleRadians = (angleDegrees * Math.PI) / 180;
  const rotatedX = unitX * Math.cos(angleRadians) - unitY * Math.sin(angleRadians);
  const rotatedY = unitX * Math.sin(angleRadians) + unitY * Math.cos(angleRadians);

  return {
    x: rotatedX * magnitude,
    y: rotatedY * magnitude,
  };
}

그리고 위의 코드를 활용하여 최종 노드 생성 위치를 계산해 주는 로직을 짜 보았습니다.


const lineLength = [360, 360, 360, 270];
const angle = [104, 105, 103, 104];
const distance = [240, 200, 160, 120];

export function getNewNodePosition(data: NodeData, nodeId: number) {
  const rootKey = findRootNodeKey(data);
  const node = data[nodeId];
  const children = node.children;
  const depth = node.depth;

  if (!children.length) {
    if (node.id === rootKey)
      return {
        x: node.location.x + NODE_RADIUS(depth) * 7,
        y: node.location.y,
      };
    const { x, y } = calculateVector(data[rootKey].location, node.location, -60, lineLength[depth - 1]);
    return node ? { x: node.location.x + x, y: node.location.y + y } : { x: 0, y: 0 };
  }
  const lastChildren = data[children[children.length - 1]];
  const uv = calculateVector(node.location, lastChildren.location, angle[depth - 1], distance[depth - 1]);
  return {
    x: lastChildren.location.x + uv.x,
    y: lastChildren.location.y + uv.y,
  };
}

우선 위와 같이 코드를 짜보았는데 우선 위에 const 요소들이 있는 이유는 depth들마다 반지름 길이와 사이 선들의 길이가 모두 다르다 보니 그것을 고려하여 각도와 길이를 다 다르게 배치를 해주어야 했습니다. 그리고 첫 노드를 생성할 위치와 이후 생성할 노드들의 위치는 다르다 보니 나누어 구현이 필요하였습니다.

우선 전 노드와 동일선상에 추가를 하게 되면 그 이후 추가하는 노드들은 그 노드를 기준으로 오른쪽으로 생성을 하는 것이기 때문에 효율적으로 공간을 활용하지 못합니다. 그래서 전 노드와 동일선상 기준으로 왼쪽 60도 위치에 생성을 해주도록 하였습니다. 그리고 그 이후에는 위 angle에 맞게 생성을 하게 되면 적절한 위치에 노드들이 생성될 수 있도록 해줍니다. 이렇게 적절한 위치에 생성할 수 있도록 해 보았습니다.

image.png