마인드맵 요소들 겹치지 않도록!

depth 3 이었을 때 로직

처음 기획했던 부분은 depth가 3까지만 보여질 수 있게 하였다. depth가 많아지면 너무 복잡해 보일 것 같아 우선 depth 3을 기준으로 마인드맵의 요소들을 겹치치 않게 보여줄 수 있을까 고민하였다.

우선 마인드맵이 주로 원의 형태를 띄기 때문에 처음 depth가 2인 요소들은 둥그런 부분을 고르게 나눠서 가져가면 되겠다는 생각을 하였다. 그래서 360도를 depth가 2인 요소의 개수만큼 나눠 갖으면 된다.

이제 depth가 3인 요소들을 겹치지 않게 배치해야한다. 근데 이미 대분류 쪽으로 배치할 수는 없기때문에 그림처럼 180도를 depth 3인 요소들이 나눠서 일정하게 배치될 수 있도록 구현했습니다.

image.png

depth 5로 바뀌고..

프로젝트가 진행되며 사용자들이 좀 더 많은 내용을 마인드맵에 담을 수 있도록 depth를 늘리자고 얘기가 나왔다. 기존에는 depth를 2일때, 3일때에 따라 다르게 계산을 해 배치하고 있었기 때문에 기존의 코드를 수정해야했다. 그리고 depth3일때 위치를 배치하는 것을 4, 5에도 하게 되면 여러 방향으로 뻗어나가게 되기 때문에 겹쳐지는 부분들이 많아질 것으로 예상돼서 새로운 방법을 찾아야만 했습니다.

정말 많은 방법들을 생각해보고 적용해보았지만 그 중 최대한 안겹치는 경우로 결정을 하였습니다. 우선 코드는 아래와 같습니다.

function calculateNodePosition({ nodeId, length, index, xPos, yPos }: CalculateNodePositionParams) {
    const node = data[nodeId];
    const depth = node.depth;
    const radius = NODE_RADIUS(depth);

    const applyVectorAdjustment = (origin: { x: number; y: number }, angle: number, distance: number) => {
      const { x, y } = calculateVector(origin, { x: xPos, y: yPos }, angle, distance);
      xPos += x;
      yPos += y;
    };

    switch (depth) {
      case 2: {
        const angle = (360 / length) * index;
        const radianAngle = (Math.PI / 180) * angle;
        xPos += radius * Math.cos(radianAngle) * 8;
        yPos += radius * Math.sin(radianAngle) * 8;
        break;
      }
      case 3: {
        const divideAngle = 180 / (length + 1);
        const adjustedAngle = divideAngle * (index + 1) - 90;
        applyVectorAdjustment(rootLocation, adjustedAngle, 360);
        break;
      }
      case 4: {
        const divideAngle = 180 / (length + 3);
        const adjustedAngle = divideAngle * (index + 2) - 90;
        applyVectorAdjustment(rootLocation, adjustedAngle, 360);
        break;
      }
      case 5: {
        const ancestor = findAncestor(data, nodeId, 2);
        const divideAngle = 180 / (length + 1);
        const adjustedAngle = divideAngle * (index + 1) - 90;
        const calculatedDistance =
          Math.sqrt((2 * radius * radius) / (1 - Math.cos((Math.PI / 180) * divideAngle))) * 1.8;
        const maxDistance = Math.max(calculatedDistance, 200);
        applyVectorAdjustment(ancestor.location, adjustedAngle, maxDistance);
        break;
      }
      default:
        break;
    }
    node.location.x = xPos;
    node.location.y = yPos;

    {
      node.children.map((childId: number, childIndex: number) => {
        calculateNodePosition({
          nodeId: childId,
          length: node.children.length,
          xPos,
          yPos,
          index: childIndex,
        });
      });
    }
  }

위의 코드에 좀 더 부가 설명을 달아 보겠습니다.

우선 depth3까지는 위의 방식과 똑같게 하였습니다. 다르게도 할까 고민해보았지만 너무 좁게 각도를 가져가도 다음 요소들이 충돌되고 또 너무 크게 가져가도 옆 요소들과 충돌된다는 문제점이 있어 180도를 노드의 갯수만큼 나눠 가져갈 수 있도록 하였습니다.

이후 depth4, depth5는 새롭게 고민해 보았습니다.

depth4, 5

우선 depth3와 같은 방식을 가져가려고 해보았습니다. 하지만 depth3과 depth4가 같은 방식으로 위치를 펼치게 되면 depth5에서 펼쳤을때 너무 많이 겹치게 되어 좀 더 좋은 방법이 없을까 생각해보았습니다.

depth4

그래서 생각한게 depth3의 방식을 가져가되 양쪽에 한개씩 더 있다고 가정을 하고 각도를 좀더 좁히면 좋겠다는 생각을 할 수 있었습니다. 아래와 같이 length +1 에서 length + 3으로 양쪽에 하나씩 늘어난 것을 확인할 수 있고 index도 한번 띄어서 시작하기 때문에 +1이 더 되었습니다.

case 3: {
        const divideAngle = 180 / (length + 1);
        const adjustedAngle = divideAngle * (index + 1) - 90;
        applyVectorAdjustment(rootLocation, adjustedAngle, 360);
        break;
      }
      case 4: {
        const divideAngle = 180 / (length + 3);
        const adjustedAngle = divideAngle * (index + 2) - 90;
        applyVectorAdjustment(rootLocation, adjustedAngle, 360);
        break;
      }

depth5