글목록

2022년 3월 26일

도형 변환하기 - 7. 여러개 곡선 결합하기

여러 개의 Node로 연결된 곡선이나 도형을 분할하는 것은 모든 꼭지점을 잘라내고 다시 Bezier 곡선을 생성하는 방식으로 쉽게 분할이 됩니다. 이는 이미 결합 순서, 위치 정보를 모두 가지고 있기 때문에 단순히 잘라내는 작업일 뿐이라 특별한 순서가 필요없습니다만, 여러 개의 직선이나 곡선을 1개 도형으로 연결하는 것은 일정한 순서로 작업이 필요합니다.

만약, 도형을 선택한 순서가 정확하게 연결하려는 순서와 같고, 1번 도형의 시작점→끝점 → 2번 도형의 시작점→끝점 → 3번 도형의 시작점→끝점 ... 순서로 연결하는 것이라면 그냥 Node 배열을 단순히 결합해주면 됩니다만, 슬라이드에 임의로 만들어놓은 도형을 선택할 때, 각 도형의 시작점이 어디인지, 마우스 드래그로 도형을 여러개 선택했을 때 도형 선택 순서는 어떻게 되는지 눈에 보이지 않기 때문에 작업자 입장에서는 도형을 연결하기 위한 수작업이 더 많이 들어가게 됩니다.

따라서, 작업자가 연결하고자 하는 여러 개의 도형을 연결하고자 하는 모양대로 대략적으로 배치해둔 상태에서 자동으로 가장 인접한 도형을 찾아 연결해주는 매크로를 작성하는 것이 작업 편의성을 높일 수 있습니다. 이러한 작업을 위해 아래와 같은 순서로 매크로를 작성합니다.


1개 도형의 Node 정보는 1~N번까지 각 점들의 위치 정보로 구성되어있으며, 1번부터 N번의 점을 연결하는 방식입니다. 그러나, 도형 사이를 연결하기 위해 거리를 계산하는 데에는 시작점과 끝점의 정보만 필요할 뿐 중간점들의 정보는 필요하지 않습니다. Node 배열에서 시작점을 S, 끝점을 F라고 했을 때, (S1, F1), (S2, F2), ... 좌표만 먼저 추출해준 후, 이들의 거리를 구해줍니다.

위의 그림을 기준으로 설명드리면, 우선 1번 도형의 (S1, F1)를 기준으로, 각 도형의 시작점과 끝점의 거리를 계산해줍니다. 가장 가까운 점은 S1-S4입니다. 따라서, 1번 도형은 F1에서 시작해서 S1으로 끝나는 곡선으로 바꿔주어야 합니다. (나중에 전체 node를 결합할 때, 1번 도형의 node는 역순으로 결합해주어야 합니다.)

1번과 가장 가까운 도형은 선택순으로는 4번 도형(Sh4)이지만, 연결 순서는 2번째로 설정해주어야 합니다. 4번 도형은 시작점 S4가 1번 도형과 연결되도록 했으므로, Node 연결 순서는 정방향으로 결합해주도록 설정해주고, 끝점인 F4와 다른 도형의 거리를 다시 계산해줍니다.

4번 도형의 끝점(F4)과 가장 가까운 2번 도형의 끝점(F2)을 찾아서, 2번 도형을 3번째 연결 순서로 설정하고, Node 연결방향은 역순이며, 다시 S2와 가까운 도형을 찾아주도록 합니다.

이러한 방법으로 각 도형의 연결 순서와 연결 방향이 지정되고 나면, 각 도형의 Node 좌표를 1개 배열로 만든 후 곡선을 생성해주고, 원본 도형은 삭제합니다.

'-----------------------------
'도형, 시작점과 끝점, 결합 방향 속성을 갖도록 Type을 지정해줍니다.
Public Type typeSortNode
  Nodes() As Single
  PS() As Single
  PF() As Single
  LinkStart As Boolean
End Type
'-----------------------------
Function Sh_UnGroup(iSh() As Shape, oSh() As Shape, Optional iNum As Long = 0)
  Dim i As Long, n As Long, tSL As ShapeRange, tSh() As Shape
  '도형이 그룹화된 경우, 개개의 도형으로 분리해주기 위한 함수입니다. 재귀함수이며, 도형이 그룹화되어있으면 자기함수를 다시 호출해서 계속 Ungroup으로 그룹해제하여 단일 도형으로 분리해줍니다.
  For i = LBound(iSh) To UBound(iSh)
    If iSh(i).Type = msoGroup Then
      Set tSL = iSh(i).Ungroup
      ReDim tSh(1 To tSL.Count)
      For j = 1 To tSL.Count
        Set tSh(j) = tSL(j)
      Next
      Sh_UnGroup tSh, oSh, iNum
    Else
      iNum = iNum + 1
      ReDim Preserve oSh(1 To iNum)
      Set oSh(iNum) = iSh(i)
    End If
  Next
End Function
'-----------------------------
Function Sh_SortNode(iSh() As Shape) As typeSortNode()
  Dim tSN() As typeSortNode, n1 As Long, n2 As Long
  Dim tSNTemp As typeSortNode, tP() As Single, tV(1 To 2) As Single, i As Long, j As Long, n As Long
  Dim tMin As Single, tDir As Boolean, tDir2 As Boolean, tVal As Single

  '선택된 도형 배열을 입력받으면, 도형을 미리 정의한 type 변수에 도형을 지정해두고, 시작점과 끝점을 각각 .PS, .PF에 저장해둡니다.
  n1 = LBound(iSh)

  n2 = UBound(iSh)
  ReDim tSN(n1 To n2)
  For i = n1 To n2
    With tSN(i)
      Set .Sh = iSh(i)
      .PS = .Sh.Nodes(1).Points
      .PF = .Sh.Nodes(.Sh.Nodes.Count).Points
      .LinkStart = True
    End With
  Next

  '1번 도형의 끝점과 2번 도형의 시작점의 거리를 최소값으로 정의해두고, 1번과 나머지 도형의 시작점, 끝점의 거리를 모두 계산해서 가장 가까운 점을 찾습니다. 만약, 1번 도형의 시작점이 다른 도형과 가장 가깝다면, 1번 도형은 역방향으로 지정하고, 끝점이 다른 도형과 가깝다면 정방향으로 지정합니다. LinkStart는 현재 도형의 시작점(Start)을 연결하라는 의미입니다.
  tV(1) = tSN(n1).PF(1, 1) - tSN(n1 + 1).PS(1, 1)

  tV(2) = tSN(n1).PF(1, 2) - tSN(n1 + 1).PS(1, 2)
  tMin = tV(1) ^ 2 + tV(2) ^ 2
  For j = n1 + 1 To n2
    tV(1) = tSN(n1).PS(1, 1) - tSN(j).PS(1, 1): tV(2) = tSN(n1).PS(1, 2) - tSN(j).PS(1, 2): tVal = tV(1) ^ 2 + tV(2) ^ 2
    If tMin > tVal Then tMin = tVal: n = j: tDir = False: tDir2 = True
    tV(1) = tSN(n1).PS(1, 1) - tSN(j).PF(1, 1): tV(2) = tSN(n1).PS(1, 2) - tSN(j).PF(1, 2): tVal = tV(1) ^ 2 + tV(2) ^ 2
    If tMin > tVal Then tMin = tVal: n = j: tDir = False: tDir2 = False
    
    tV(1) = tSN(n1).PF(1, 1) - tSN(j).PS(1, 1): tV(2) = tSN(n1).PF(1, 2) - tSN(j).PS(1, 2): tVal = tV(1) ^ 2 + tV(2) ^ 2
    If tMin > tVal Then tMin = tVal: n = j: tDir = True: tDir2 = True
    tV(1) = tSN(n1).PF(1, 1) - tSN(j).PF(1, 1): tV(2) = tSN(n1).PF(1, 2) - tSN(j).PF(1, 2): tVal = tV(1) ^ 2 + tV(2) ^ 2
    If tMin > tVal Then tMin = tVal: n = j: tDir = True: tDir2 = False
  Next
  tSN(1).LinkStart = tDir
  '가장 가까운 도형(n번 도형)을 찾았다면, 2번 도형과 n번 도형의 순서를 바꾸고, 연결 방향을 지정해줍니다.
  tSNTemp = tSN(2): tSN(2) = tSN(n): tSN(n) = tSNTemp

  tSN(2).LinkStart = tDir2

 '1번과 2번 도형의 순서와 연결방향이 결정되었으므로, 2번 도형의 끝점과 3번 이상의 도형의 거리를 계산해서 가장 가까운 도형을 반복해서 배열해줍니다.
  For i = n1 + 2 To n2
    If tSN(i - 1).LinkStart Then tPEnd = tSN(i - 1).PF Else tPEnd = tSN(i - 1).PS
    tV(1) = tSN(i).PS(1, 1) - tPEnd(1, 1): tV(2) = tSN(i).PS(1, 2) - tPEnd(1, 2): tMin = tV(1) ^ 2 + tV(2) ^ 2
    n = i: tDir = True
    tV(1) = tSN(i).PF(1, 1) - tPEnd(1, 1): tV(2) = tSN(i).PF(1, 2) - tPEnd(1, 2): tVal = tV(1) ^ 2 + tV(2) ^ 2
    If tMin > tVal Then tMin = tVal: tDir = False
    For j = i + 1 To n
      tV(1) = tSN(j).PS(1, 1) - tPEnd(1, 1): tV(2) = tSN(j).PS(1, 2) - tPEnd(1, 2): tMin = tV(1) ^ 2 + tV(2) ^ 2
      If tMin > tVal Then tMin = tVal: n = j: tDir = True
      tV(1) = tSN(j).PF(1, 1) - tPEnd(1, 1): tV(2) = tSN(j).PF(1, 2) - tPEnd(1, 2): tMin = tV(1) ^ 2 + tV(2) ^ 2
      If tMin > tVal Then tMin = tVal: n = j: tDir = False
    Next
    tSNTemp = tSN(i): tSN(i) = tSN(n): tSN(n) = tSNTemp
    tSN(i).LinkStart = tDir
  Next
  Sh_SortNode = tSN
End Function
'-----------------------------
Function Sh_MergeLines(iSh() As Shape, Optional iAddLines As Boolean = False) As Shape
  Dim tSN() As typeSortNode, tUG() As Shape, tNode() As Single, tP() As Single, tP1() As Single, tP2() As Single, tPX() As Single, tPY() As Single, i As Long, j As Long, n As Long, tV() As Single
  '사용자가 선택한 도형을 입력받으면, 우선 그룹화된 도형을 모두 그룹해제하여 단일 도형으로 분리해줍니다. 분리된 도형의 Node를 곡선형으로 구하고, 곡선을 생성해줍니다. 처음 도형을 그냥 사용하지 않는 이유는 선택된 도형이 모두 자유형 도형(msoFreedom)이 아니라 Powerpoint의 AutoShape이나 직선으로 그린 도형(AutoShape이나 Line), 또는 곡선과 직선이 혼합된 도형이라도 결합이 가능하도록 하기 위함입니다.
  Sh_UnGroup iSh, tUG, 0

  For i = LBound(tUG) To UBound(tUG)
    tNode = GetVertices(tUG(i), False)
    tUG(i).Delete
    Set tUG(i) = ActiveSlide.Shapes.AddCurve(tNode)
  Next
  '도형을 배치 상태에 따라 인접한 도형 순서로 배열해줍니다.
  tSN = Sh_SortNode(tUG)

  n = 0
  ReDim tV(1 To 2)

  '배열된 도형의 각 Node 배열을 1개 배열로 결합해줍니다.
  For i = LBound(tSN) To UBound(tSN)
  '도형을 이전 도형의 끝점에 맞춰서 이동하여 연결하는 방법과, 현재 위치를 그대로 유지한 상태에서 각 끝점을 연결하는 방식으로 도형을 연결할 수 있습니다. 만약, 전자의 경우라면, 곡선형으로 변환된 도형을 이전 도형의 끝점에 맞게 도형을 이동시킨 후, node를 다시 읽어와서 결합시켜줍니다.
    If i > LBound(tSN) Then
  '이전 도형의 끝점과 현재 도형의 시작점을 구해줍니다.
      If tSN(i - 1).LinkStart Then tP1 = tSN(i - 1).PF Else tP1 = tSN(i - 1).PS

      If tSN(i).LinkStart Then tP2 = tSN(i).PS Else tP2 = tSN(i).PF
      If iAddLines Then
  '곡선들을 현재 위치에 그대로 두고, Line으로 연결해주려고 한다면, 연결되는 점 사이에 Control Point를 계산해서 삽입해줍니다.
        If (tP1(1, 1) = tP2(1, 1) And tP1(1, 2) = tP2(1, 2)) Then
          n = n - 1: ReDim Preserve tPX(1 To n): ReDim Preserve tPY(1 To n)
        Else
          n = n + 2: ReDim Preserve tPX(1 To n): ReDim Preserve tPY(1 To n)
          tPX(n - 1) = (tP1(1, 1) + 2 * tP2(1, 1)) / 3: tPY(n - 1) = (tP1(1, 2) + 2 * tP2(1, 2)) / 3
          tPX(n) = (2 * tP1(1, 1) + tP2(1, 1)) / 3: tPY(n) = (2 * tP1(1, 2) + tP2(1, 2)) / 3
        End If
      Else
  '도형을 끝점에 맞춰서 이동한 후 결합한다면, 생성된 곡선을 실제로 이동시킨 후 좌표를 읽어와서 배열에 결합시킵니다.
        With tSN(i)

          .Sh.Left = .Sh.Left + tP1(1, 1) - tP2(1, 1): .Sh.Top = .Sh.Top + tP1(1, 2) - tP2(1, 2)
          tV(1) = tP1(1, 1) - tP2(1, 1): tV(2) = tP1(1, 2) - tP2(1, 2)
          .PS(1, 1) = .PS(1, 1) + tV(1): .PS(1, 2) = .PS(1, 2) + tV(2)
          .PF(1, 1) = .PF(1, 1) + tV(1): .PF(1, 2) = .PF(1, 2) + tV(2)
        End With
        n = n - 1: ReDim Preserve tPX(1 To n): ReDim Preserve tPY(1 To n)
      End If
    End If
  '연결할 도형의 좌표를 읽어와서, X, Y 좌표를 1차원 배열로 만들어 계속 확장해서 결합해줍니다.
    tP = tSN(i).Sh.Vertices
    If tSN(i).LinkStart Then
      For j = 1 To UBound(tP, 1)
        n = n + 1: ReDim Preserve tPX(1 To n): ReDim Preserve tPY(1 To n)
        tPX(n) = tP(j, 1): tPY(n) = tP(j, 2)
      Next
    Else
      For j = UBound(tP, 1) To 1 Step -1
        n = n + 1: ReDim Preserve tPX(1 To n): ReDim Preserve tPY(1 To n)
        tPX(n) = tP(j, 1): tPY(n) = tP(j, 2)
      Next
    End If
  Next
  '곡선을 생성하기 위한 2차원 배열을 만들어서, X,Y 좌표쌍으로 변환해주고, 곡선을 생성합니다.
  '필요하면, 원본 도형의 선 색상, 굵기, 형태 등의 서식을 복사해줄 수도 있습니다만, 여기에서는 생략하였습니다.
  ReDim tNode(1 To n, 1 To 2)
  For i = 1 To n: tNode(i, 1) = tPX(i): tNode(i, 2) = tPY(i): Next
  Set Sh_MergeLines = ActiveSlide.Shapes.AddCurve(tNode)
  '원본을 삭제하고 함수를 마칩니다.
  For i = LBound(tUG) To UBound(tUG): tUG(i).Delete: Next

End Function
'-----------------------------
Sub 도형생성_선병합()
  On Error GoTo ErrorHandler
  Dim tSR As ShapeRange, tSh() As Shape, i As Long, tMSG
  'Powerpoint에서 실행하기 위한 Sub 프로시저입니다.
  '도형의 끝점을 맞춰서 연결할지, 현재 위치를 유지하고 연결선을 삽입할지 확인하고, 선택 결과에 따라 곡선을 연결합니다.
  tMSG = MsgBox("각 라인의 시작과 끝점을 연결하시겠습니까?" & vbCrLf & _
    " -Yes : 각 도형의 끝점 맞춤 (도형 위치 이동)" & vbCrLf & _
    " -No : 라인 사이를 선으로 연결 (각 도형 위치 유지)" & vbCrLf & _
    " -Cancel : 작업 취소", vbYesNoCancel, "작업 확인")
  If tMSG = vbCancel Then Exit Sub
  Set tSR = SelectedShapeRange(False)
  ReDim tSh(1 To tSR.Count)
  For i = 1 To tSR.Count: Set tSh(i) = tSR(i): Next
  Sh_MergeLines(tSh, tMSG = vbNo).Select msoTrue
  ErrorHandler:
End Sub


많이 본 글 :