글목록

레이블이 PowerPoint / Macro / VBA인 게시물을 표시합니다. 모든 게시물 표시
레이블이 PowerPoint / Macro / VBA인 게시물을 표시합니다. 모든 게시물 표시

2022년 12월 21일

PowerPoint MiniTool - 단락 서식 복사하기

☞ PowerPoint 추가기능 파일 다운로드 페이지 (MiniTool.ppam) 


파워포인트 또는 워드에서 글상자 내 텍스트는 다음과 같이 구분됩니다.

여러개의 문장으로 구성된 문단을 '단락(Paragraph)'이라고 하고, 단락과 단락은 Enter키로 구분하게 됩니다. '문장(Sentence)'은 마침표(.)로 구분된 단어의 집합이며, 만약 의미상으로는 문장이 아니라 하더라도, 단어들의 나열 중간에 마침표를 표시하게 되면 문장으로 구분하게 됩니다. 단어(Word)는 공백이나 기호로 구분되거나 문자의 종류(한글, 영어, 특수 문자 등..)가 바뀌게 되면 단어로 구분하게 됩니다.

매크로를 작성할 때에는 위와 같은 고전적인 구분(단어, 문장, 단락) 뿐만 아니라, 문자열 속성(문자의 종류, 글꼴, 글자 서식 등)이 변경되는 지점을 'Run'으로 구분하게 되며, 공백이 있더라도 문자의 속성이 동일하면 단일한 Run으로 취급합니다. 또한, 단일 문장이라도 행이 바뀌게 되면 'Line'으로 구분합니다.

파워포인트로 문서를 작성할 때, 특정 단어나 문장을 강조하기 위해 bold, italic, 밑줄 또는 색상과 같은 글자 속성을 변경하기도 하지만, 일반적으로는 동일한 단락은 동일한 서식을 가지도록 작성하게 됩니다. 또한, PPT 문서 특성상 1개 단락에는 몇줄에 걸친 여러 개의 문장으로 구성하기보다는 서술형이든 축약된 형태든 1개 문장으로 구성되는 경우가 많고, 문자열의 서식은 대체로 단락 단위로 지정하게 됩니다. 즉, 동일한 서식을 갖는 단락으로 전체 내용을 구성한 후, 필요한 단어나 문장만 서식을 변경하여 강조하는 방식으로 작성하게 됩니다.

특별한 서식을 지정하지 않고 내용을 작성한 후, 대표 단락에만 서식을 지정하고, 나머지 글상자들에게 서식을 복사해 넣을 수 있다면, 매번 글상자마다 서식을 맞추느라 시간을 들이는 수고를 줄일 수 있습니다.


아래와 같이 여러개의 글상자에 내용을 작성한 후, 1번째 단락에 대해서만 글꼴, 글자크기, 단락 서식 등을 지정해두고, 여러개의 글상자를 함께 선택합니다.


MiniTool 추가 기능의 '단락 서식 복사'를 실행합니다.

선택 영역에 여러개의 단락이 포함되어 있다면, 1번째 선택된 글상자의 1번째 단락의 서식을 복사하여, 나머지 단락에 동일하게 적용시킬지 물어보게 됩니다. 만약, 선택된 단락이 1개밖에 없다면, 아무 메세지없이 해당 단락의 서식만 복사해둡니다.


작업이 완료되면, 아래와 같이 선택된 텍스트 영역의 각 단락에 서식을 붙여넣게 됩니다.


만약, 서식을 적용할 단락이 원본 단락과 따로 떨어져있다면, 아래와 같이 서식을 적용할 단락을 선택합니다.


MiniTool 추가 기능에서 '단락 서식 붙이기'를 실행합니다.


작업이 완료되면 선택된 텍스트를 포함하는 단락들에 서식을 적용하게 됩니다.

만약, 모든 글상자들이 1개 단락으로만 구성되어있다면, '그림 서식 복사/붙이기' 기능으로도 단락의 서식을 그대로 복사해갈 수 있습니다만, 글상자들이 여러개의 단락으로 구성되어 있다면 잘 적용되지 않습니다.

이전 글에서부터 유심히 보신 분들은 아시겠지만.. 이 기능은 지난번 글을 쓴 이후에 추가시킨 기능입니다. 글머리 기호에 따라 자동으로 서식을 적용하는 기능은 전체 글 구성을 마무리한 후에 일괄적으로 서식을 적용하는 경우에서는 편리할 수 있지만, 글을 작성하는 중에는 많이 쓰지 않게 되어, 원하는 단락의 서식을 원하는 위치에 복사하는 기능을 추가하게 되었습니다.


2022년 10월 19일

PowerPoint 매크로 - 실행 취소 지점 만들기

엑셀과 달리 파워포인트에서는 매크로로 실행하더라도 Ctrl+Z를 눌러 매크로 실행 전 상태로 실행취소가 가능합니다. 그런데, 매크로를 연속해서 실행하고, 바로 1단계 이전으로 되돌리고 싶은데 Ctrl+Z를 누르면, 전체 매크로 실행전으로 되돌아가버립니다. 물론 매크로 실행 후 파워포인트 작업 창에서 무언가 작업을 하게 되면 실행취소 지점이 생길 수는 있습니다만, 번거로운 일이지요.

만약 각 매크로를 실행한 후, 각단계마다 실행 취소 지점을 강제로 만들어줄 수 있다면 여러가지 매크로를 실행하더라도 1단계씩 실행취소가 가능할 것입니다.


아래에 두 매크로를 실행하신 후, Ctrl+Z를 눌러보시면 쉽게 차이를 이해하실 수 있습니다.

첫번째 '전체되돌리기()'를 실행하면, 5개의 도형이 생성되고, 실행취소 또는 Ctrl+Z 키를 누르면, 5개의 도형이 모두 사라지지만, 두번째 '단계별되돌리기()'를 실행한 후, 실행취소를 반복하게 되면 생성된 순서의 역순으로 1개씩 사라지는 것을 알 수 있습니다.

-----------------------------------------------------------
Sub 전체되돌리기()
  Dim tSlide As Slide
  Set tSlide = ActiveWindow.Selection.SlideRange(1)
  For i = 1 To 5
    tSlide.Shapes.AddShape i, 100 * i, 100, 80, 80
  Next
End Sub
-----------------------------------------------------------
Sub 단계별되돌리기()
  Dim tSlide As Slide
  Set tSlide = ActiveWindow.Selection.SlideRange(1)
  For i = 1 To 5
    tSlide.Shapes.AddShape i, 100 * i, 100, 80, 80
    Application.StartNewUndoEntry
  Next
End Sub
-----------------------------------------------------------

2022년 10월 17일

PowerPoint 매크로 - 특정 슬라이드 또는 특정 개체 매끄럽게 선택하기

PowerPoint 매크로를 이용해 특정 슬라이드로 이동하거나, 특정 도형을 선택하고 싶은 경우가 있습니다. 단순히 Slides(i).Select 또는 Shape.Select msoTrue 를 사용하면 쉽게 될 것 같습니다만, 현재 작업창에서 어떤 개체나 어떤 창을 선택한 상태이냐에 따라 이게 매끄럽지 않은 경우가 있습니다.

예를 들어, 1번 슬라이드를 선택한 상태에서, 3번 슬라이드의 2번째 도형을 선택하라고 하면, 

ActivePresentation.Slides(3).Shapes(2).Select msoTrue

이렇게 작성하면 해당 도형을 선택된 상태가 될 것 같지만, 오류가 발생됩니다. 해당 슬라이드가 활성화되어 있지 않기 때문입니다.

유사하게, 특정 슬라이드를 선택한 상태를 만들고 싶을 때에도

ActivePresentation.Slides(1).Select

이렇게 하면 될 것 같은데, 오류메세지는 발생하지 않지만, 이상하게도 화면이 전환이 되지 않은 상태로 매크로가 종료되는 경우가 종종 있습니다. 아마, 작업창에서 현재 작업 영역 또는 선택도니 상태에 따라서, 매크로가 비정상적으로 작동하는 것으로 판단됩니다.

가장 일반적인 작업 상태, 즉 PowerPoint를 처음 실행하면 활성화되는 상태로 현재상태를 만들어준 후, 원하는 개체(슬라이드나 도형)를 선택하도록 하면 매크로를 이용해 개체 선택을 변경하는 것이 매끄러워 집니다.

아래에 특정 도형이나 슬라이드를 선택한 상태를 만드는 함수 예시를 작성하였으니, 필요에 맞게 응용해서 사용하시면 되겠습니다.

--------------------------------------------------------------
Function SelectSlide(iSlideNum As Long)
  With ActivePresentation
    If .Slides.Count = 0 Or iSlideNum > .Slides.Count Then Exit Sub
  '해당 프리젠테이션의 슬라이드 번호를 입력하면, PowerPoint 보기 모드를 왼쪽에는 썸네일이, 오른쪽에는 슬라이드가 보이는 Normal 상태로 바꾸줍니다.
    ActiveWindow.ViewType = ppViewNormal
  '작업창에서 2번째 Panes, 즉 오른쪽의 슬라이드 작업 영역을 활성화시켜줍니다
    ActiveWindow.Panes(2).Activate
  '원하는 슬라이드 번호를 선택해줍니다.
    .Slides(iSlideNum).Select

  End With
End Function
--------------------------------------------------------------
Function SelectShape(iShape As Shape)
  Dim tObj, tSlide As Slide, i As Integer
  '현재 활성화된 슬라이드에 있는 개체가 아니라면, 개체만 입력하고 선택하라고 하면 오류가 발생합니다. 해당 개체가 있는 슬라이드를 먼저 찾아주어야 합니다. 부모개체가 슬라이드가 될 때까지 반복하여 찾아줍니다. Shape 뿐만 아니라 다른 개체(TextRange..)라도 5회 이상은 실행할 일이 없을 겁니다.
  Set tObj = iShape
  For i = 1 To 5
    Set tObj = tObj.Parent
    If TypeName(tObj) = "Slide" Then Set tSlide = tObj: Exit For
  Next
  '슬라이드가 찾아졌다면, 해당 슬라이드로 이동한 후, 지정한 도형을 선택해줍니다.
  If Not tSlide Is Nothing Then

    ActiveWindow.ViewType = ppViewNormal
    ActiveWindow.Panes(2).Activate
    tSlide.Select
    iShape.Select msoTrue
  End If
End Function
--------------------------------------------------------------

PowerPoint 매크로 - 새 슬라이드 추가하기

PowerPoint에서 매크로를 작성하다보면, 빈 슬라이드를 계속 추가해야하는 경우가 있습니다.

그런데, Presentation.Slides.AddSlide로 새 슬라이드를 추가하려고 하면, 레이아웃을 필수로 지정해야하는데, 이게 숫자가 아니라 CustomLayout 개체를 입력해주어야 합니다.

Powerpoint에서 대부분의 개체 생성 method는 입력값이 숫자 형태 또는 기껏해야 숫자 배열 형태인 경우가 많아서 쉽게 응용할 수 있지만, AddSlide method에 입력해주어야하는 CustomLayout은 어떻게 처리해야하는지도 모르겠는데, 선택 사항이 아니다보니 오류가 쉽게 발생합니다. 어찌보면 쉬울 것 같은데 말이죠..

아래와 같은 방법으로 오류없이 특정 슬라이드 뒤에 새 슬라이드를 만드는 함수를 생성할 수 있습니다.

---------------------------------------------------------

Function AddNewSlide(iAfter As Long, Optional iMakeBlank As Boolean = False) As Slide
  Dim tIndex As Long
  With ActivePresentation
  '특정 슬라이드 번호(iAfter) 뒤에 새 슬라이드를 추가하도록 슬라이드 번호를 입력합니다.
  '새로 생성하려는 슬라이드의 번호는 입력받은 값 iAfter보다 1 크게 지정합니다.
  '입력받은 슬라이드 번호를 tIndex에 지정한 후, 만약 프리젠테이션에 슬라이드가 없다면 1번 슬라이드를 생성하고, 지정한 슬라이드번호 iAfter가 슬라이드 갯수보다 크면, 마지막 슬라이드 뒤에 새 슬라이드를 생성하도록 합니다.
    tIndex = iAfter
    If .Slides.Count = 0 Then tIndex = 0
    If tIndex > .Slides.Count Then tIndex = .Slides.Count
    If tIndex = 0 Then
     '만약 첫 슬라이드를 생성한다고 하면, 슬라이드 번호는 1번, CustomLayout은 슬라이드마스터에 있는 1번 레이아웃을 불러옵니다.
      Set AddNewSlide = .Slides.AddSlide(1, .SlideMaster.CustomLayouts(1))

    Else
     '이미 슬라이드가 있다면, 추가하려는 슬라이드는 바로 이전 슬라이드의 레이아웃을 불러오도록 합니다.
      Set AddNewSlide = .Slides.AddSlide(tIndex + 1, .Slides(tIndex).CustomLayout)

    End If
  End With
  
   '만약, 슬라이드에 있는 모든 글상자나 도형을 없애고 빈 슬라이드를 만들고 싶다면, 아래와 같이 슬라이드 내에 있는 모든 개체를 삭제합니다.
  If iMakeBlank Then
    For i = tSlide.Shapes.Count To 1 Step -1
      tSlide.Shapes(i).Delete
    Next
  End if
  Set AddNewSlide = tSlide
End Function

---------------------------------------------------------

2022년 10월 15일

파워포인트 매크로 Addin 파일 배포

<Addin 파일 다운로드 : MiniTool.ppam>

파워포인트용 Addin 파일(.ppam)입니다. 구글 드라이브로 연결되며, 악성 코드는 심어두지 않았으니 안심하시고 사용하셔도 됩니다. 혹시 실행하다가 오류가 발생되거나, 추가하고 싶으신 기능이 있다면 댓글로 달아두시면 제가 할 수 있는 범위에서 추가해보도록 하겠습니다.

원래 블로그를 개설할 때에는, 아래의 각 기능들 하나씩 코딩한 과정을 내용으로 글을 업로드하려고 했는데, 기본 function과 실행 Sub가 서로 얽히다보니 코드를 짜는 것보다, 코드를 해설하는게 더 어렵네요. 우선 만들어놓은 파일을 배포해드리고, 나중에는 각각의 기능에 대한 사용법을 위주로 글을 작성해볼까 합니다.


<설치법>

파워포인트에서 '옵션'→'추가기능'→하단의 콤보박스에서 'PowerPoint 추가 기능' 선택 → '이동' 버튼을 눌러서 다운받은 MiniTool.ppam 파일을 추가하시면 됩니다. 정상적으로 설치가 되면 리본 메뉴의 마지막 탭에 'MiniTool' 탭이 생성되고, 아래와 같은 메뉴가 확인되시면 설치가 완료된 것입니다.


<주용도>

리본 메뉴에서 보시는 바와 같이, 

1. 그림을 한꺼번에 불러오고, 일괄 편집하며, 원하는 위치에 자동 배열하는 기능
2. 기본 도형 이외에 도형을 자유롭게 생성하고, 변환/병합하는 기능
3. 편집시 자주 사용하는 텍스트 서식 편집이나 프리젠테이션을 매끄럽게 하기 위한 개체 삽입 기능 등이 포함됩니다.

어디까지나 PowerPoint를 이용하여 간단한 개념도를 만들거나, 사진 자료들을 반복해서 배열해야하는 작업들을 간소화하기 위해 만든 것이며, CAD처럼 정밀한 작업을 하거나, 특수한 기능을 포함한 것은 아닙니다. 그럼에도 사진이나 그림 배열과 같은 반복 작업이 많으신 분들에게는 조금은 도움이 되지 않을까 해서 공유합니다. (단, 보고서 작성용으로 사용할 것을 감안하여 만든 것이라, 수천개 수만개 사진 파일을 정리하는 것과 같이 대량 작업을 염두에 둔 것은 아닙니다. 사실 그정도 작업이 되면 파일 저장부터 문제가 될 거라...)

혹시 사용하시다가 인터넷으로 찾아보면 위에 포함된 기능들과 유사한 매크로를 찾아보실 수 있을 겁니다. 제가 작업하는데 필요한 기능들을 만들면서 인터넷으로 참고한 부분들도 있습니다. 다만, 특수한 용도로만 쓰는게 아니라, 일반적인 기능으로 사용할 수 있고 상황에 따른 오류 발생 가능성을 최소화시키려다 보니 코드가 더 복잡하게 되었습니다.

그나마 다행인 것은, PowerPoint는 매크로로 작업을 하더라도 Ctrl+Z로 실행 취소가 가능합니다. 원하는대로 작업이 되지 않았다면, Ctrl+Z로 실행취소하시면 되니 부담없이 사용하실 수 있을 겁니다.


<작업환경>

제가 작업한 환경은 Windows 10/ MS Office 2016과 Windows 11 / MS Office 2021 버전입니다. Office 2007~2013 버전에서도 별 무리없이 실행될 것으로 생각됩니다만, 혹시 다른 환경에서 에러가 발생하면 댓글로 남겨주세요.


<업데이트>

MiniTool 추가 기능 파일은 수시로 업데이트됩니다. 제가 쓰다가 필요하다 싶으면 기능을 추가하기도 하고, 사용하기가 번거로운 부분이 있으면 수정하고, 버그가 있으면 새로 고치고... 실행파일이 아니다보니 버전을 별도로 표기하기는 어려워서 수시로 수정되고, 수정된 파일을 새로 업데이트해둡니다. 다운로드 링크는 그대로이기 때문에 쓰시다가 혹시 에러가 발생되면 다시 다운로드받으셔서 덮어씌우시면 됩니다. 만약 새 버전에서 오류가 그대로 있으면 댓글로 남겨주시기 바랍니다.

2022년 10월 8일

베지어 곡선(Bezier)을 모양을 유지하면서 점 추가/분할하기

파워포인트에 삽입되는 모든 곡선의 각 segment들은 베지어 곡선이며, '점편집' 메뉴를 이용해서 시작 및 끝점과 제어점을 수정할 수 있습니다.



만약, 아래와 같이 점을 곡선의 중간 지점에 추가하고자 한다면, 곡선의 모양이 찌그러지면서 사용자가 원하는대로 수정되지 않습니다. 심지어는 새로 추가한 점 뿐만 아니라, 점을 삽입하려는 segment의 앞, 뒤 점까지 뒤틀리면서 모양이 엉망이 됩니다.



Powerpoint에 포함되는 도형들이 벡터형 방식이라, 점 편집 기능만 충분히 편리하게 해주면 좋겠지만, 위의 예와 같이 Powerpoint를 이용하여 곡선형 도형을 작업하는 매우 번거롭고 귀찮은 작업입니다. 따라서, Powerpoint에서 도형 작업은 특수한 방법을 사용하지 않으면 작업하기가 어렵고, 결국 벡터형 그래픽을 지원해주는 소프트웨어를 이용하여, SVG 파일이나 메타파일로 만들어 삽입하는 방법이 그나마 도형 작업을 쉽게하는 방법이 될 것입니다.


만약, 점 편집을 자유롭게 할 수만 있다면 Powerpoint는 충분히 좋은 그래픽 도구가 될 수도 있습니다. CAD 수준의 정밀도를 바라지는 않지만, 간단한 개념도 정도를 그리는 수준에서 별도의 소프트웨어 도움없이도 쉽게 작업이 가능해질 수 있습니다.


1개의 Bezier 곡선에 현재 모양을 그대로 유지하면서 점을 추가할 수 있을까요? Powerpoint에서는 1개의 곡선을 정의하기 위해서는 시작점과 2개의 제어점, 끝점으로 구성되는 Cubic Bezier 곡선을 사용합니다. 따라서, 곡선상의 임의의 점을 삽입하는 문제는, 제어점을 어떻게 지정해줄 것인가하는 문제로 귀결됩니다.

이번 글에서는 이 문제에 대한 수학적인 증명과 제어점의 변동에 대해 설명드릴 예정입니다. 다만, 매크로 코드는 포함하지 않았습니다. 원리를 알면 계산용 함수는 쉽게 구현이 가능할 것이기 때문입니다.



위와 같이 P1, P4를 끝점으로 하고, P2, P3를 제어점으로 하는 빨간색 곡선을 가정해보겠습니다. 이 곡선상의 임의의 점 P에 점을 하나 추가한다고 하겠습니다.

Bezier 곡선의 매개 변수를 T라고 하고, S=1-T로 정의하면, 임의의 점 P의 좌표는 아래와 같이 정의됩니다. 이때, P를 그리그 위해서는 P1~P2를 연결하는 선을 T:S로 분할하는 점 P2', P2~P3를 T:S로 분할하는 점과 P2'을 다시 T:S로 분할하는 점 P3'과 P4'(=P)의 좌표는 아래와 같이 정의됩니다.



이제 P1~P4'을 끝점으로 하고, P2', P3'을 제어점으로 하는 cubic Bezier 곡선상에 있는 임의의 점 p를 아래와 같이 풀어주면, p는 P1, P2, P3, P4로 생성된 Bezier 곡선상의 한 점임을 계산할 수 있습니다.

따라서, 매개변수 T값을 지정하여 P2', P3', P4'만 계산해주면, P4' 위치에 점을 삽입할 수 있게 됩니다. 물론, 위의 계산 결과는 P1~P4' 구간의 곡선이며, P4'~P2 구간에 대한 계산도 동일한 방식으로 수행할 수 있습니다.

즉, Powerpoint에서 매크로를 이용하여 node 정보 (P1, P2, P3, P4)를 읽어온 후, (P1, P2', P3', P4', P5', P6', P4) 좌표를 계산하고, Shapes.AddCurve method를 이용하여 곡선을 생성해주면 원래 곡선에 1개의 점을 추가한 도형이 생성됩니다.


얼핏 생각해보면, 분할점 P4'만 계산한 후, 이미 있는 도형에 점을 삽입하는 방식이 되면 좋겠습니다만, 안타깝게도 Powerpoint 매크로에서는 제어점을 추가하는 method가 없으며, 강제로 점을 추가하게 되면  작업창에서 점 편집을 이용할 때와 마찬가지로 곡선의 모양이 뒤틀릴 수 있으며, 작업량이 100개 이상으로 많다면 속도가 급격히 느려지는 문제가 있어서, 각 점 P2'~P6'에 대해 계산한 후 곡선을 새로 생성하는 방식을 추천드립니다.



2022년 7월 2일

곡선형 도형 변환 - 4. 3점 위치로부터 중간점의 제어점(Control Point) 구하기

CASE 1. 앞, 뒤 knot을 잇는 선분과 나란하게 (Powerpoint 기본값)



K2의 앞, 뒤에 control point를 생성하기 위해서, 바로 앞과 뒤의 knot 위치로부터 나란한 벡터 V_tan를 구하고, 이 벡터 길이의 1/6에 해당하는 벡터를 K2 위치에 빼주거나, 합쳐주면 control point가 됩니다.

Powerpoint 자유형 곡선을 생성할 때, 마우스로 여러 개의 점을 클릭해서 찍어주면 자동으로 생성해주는 control point와 같습니다. 점 편집 메뉴에서 임의로 수정했다가 원래 상태로 되돌릴 때 사용할 수 있습니다.


CASE 2. 앞, 뒤 knot을 잇는 선분과 나란하면서 원형이 되도록 control point 생성하기


4점을 이용한 Bezier 곡선으로 완벽한 원을 그리는 것은 불가능합니다. 그러나, 원에 최대한 근접한 곡선을 생성할 수는 있습니다. Knot이 되는 점 P1, P2, P3이 원의 일부분인 호를 생성한다고 가정할 때, control point인 C1과 P2 사이의 길이 Lc는 위의 그림과 같이 삼각함수로 주어지게 됩니다. P1, P2, P3의 관계로부터 θ값과 곡률반경 R을 구한 후, 위의 함수에 입력해주면 Lc를 구할 수 있습니다.


CASE 3. 앞, 뒤 knot을 연결한 선이 중간점에서 반사되는 형태의 control point


문장으로 쓰는 것보다는 그림으로 이해하시면 쉬울 듯 합니다. Control point와 knot을 연결한 선이 앞, 뒤 knot을 연결한 선의 반사면이 되도록 Control point의 위치를 설정하는 방법입니다. Control의 길이는 앞, 뒤 knot과 연결한 선의 1/6이 되도록하는 기본값과 원형이 되도록 길이를 조정하는 방법이 있습니다.


Control point의 위치를 계산하는 함수는 이전글에서 소개드렸던 벡터 계산 함수들을 조합하여 구할 수 있습니다. 또한 여기에서 소개드린 방법 이외에도 여러 점들을 부드러운 곡선으로 연결하는 다양한 경로를 생각할 수 있습니다. 따라서, 개인적으로 생각하기에 좀더 부드러운 곡선을 생성할 방법을 고안한다면 다양한 곡선 형태를 얻을 수 있을 것입니다.

2022년 4월 1일

곡선형 도형 변환 - 3. 2차원 벡터 계산용 함수

Shape.Vertices 속성으로부터 도형의 모든 Nodes의 위치 정보를 가져오게 되면, Vertices는 (1 to n, 1 to 2) 배열값을 가집니다. 첫번째 인덱스 1~n은 Nodes의 순서이며, 두번째 인덱스는 가로 방향 위치 X와 세로방향 위치 Y를 의미합니다. 즉, (n, 1)은 n번째 점의 X좌표, (n, 2)는 n번째 점의 Y좌표를 의미합니다.

만약, Vertices 배열을 가져온 후, i 번째 점의 위치만 추출하고자 한다면, 아래와 같은 함수를 쓸 수 있습니다. 또한 모든 점들에 대한 정보는 2개의 숫자 배열이며, 이는 벡터와 동일한 역할을 합니다. 점들의 좌표, 즉 벡터에 대한 연산을 위해 아래와 같이 간단한 함수들을 미리 만들어두면 향후 코드가 간결해질 수 있습니다.

'----------------------
'Vertices 배열에서 특정 인덱스의 점을 추출
Function Node_Point(iVertices() As Single, iIndex As Long) As Single()
  Dim tNode(1 To 2) As Single
  tNode(1) = 
iVertices(iIndex, 1)
  tNode(2) = 
iVertices(iIndex, 2)
  Node_Point = tNode
End Function

'----------------------
'내적 함수
Function Vec_Inner(iVec1() As Single, iVec2() As Single) As Single
  Vec_Inner = iVec1(1) * iVec2(1) + iVec1(2) * iVec2(2)
End Function

'----------------------
'벡터에 상수를 사칙연산
Function Vec_OprConst(iVec() As Single, iConst As Single, Optional iOperation As String = "+") As Single()
  Dim tVec() As Single
  ReDim tVec(1 To 2)
  Select Case iOperation
    Case "+"
      tVec(1) = iVec(1) + iConst: tVec(2) = iVec(2) + iConst
    Case "-"
      tVec(1) = iVec(1) - iConst: tVec(2) = iVec(2) - iConst
    Case "*"
      tVec(1) = iVec(1) * iConst: tVec(2) = iVec(2) * iConst
    Case "/"
      If iConst = 0 Then
        tVec(1) = iVec(1): tVec(2) = iVec(2)
      Else
        tVec(1) = iVec(1) / iConst: tVec(2) = iVec(2) / iConst
      End If
  End Select
  Vec_OprConst = tVec
End Function
'----------------------
'벡터와 벡터에 대한 사칙연산
Function Vec_Opr(iVec1() As Single, iVec2() As Single, Optional iOperation As String = "+") As Single()
  Dim tVec() As Single
  ReDim tVec(1 To 2)
  Select Case iOperation
    Case "+"
      tVec(1) = iVec1(1) + iVec2(1): tVec(2) = iVec1(2) + iVec2(2)
    Case "-"
      tVec(1) = iVec1(1) - iVec2(1): tVec(2) = iVec1(2) - iVec2(2)
    Case "*"
      tVec(1) = iVec1(1) * iVec2(1): tVec(2) = iVec1(2) * iVec2(2)
    Case "/"
      tVec(1) = iVec1(1) / iVec2(1): tVec(2) = iVec1(2) / iVec2(2)
  End Select
  Vec_Opr = tVec
End Function
'----------------------

위의 함수들은 단순히 배열에 대한 연산이니 쉽게 이해될 수 있습니다. 앞으로 모든 좌표들은 2차원 벡터 연산이 주로 되는데, 그러다보면 어떤 벡터에 수직인 성분과 수평인 성분으로 분해하는 경우가 자주 있습니다. 따라서, 아래와 같이, 임의의 벡터를 다른 벡터에 나란한 성분과 수직인 성분으로 분해하는 함수를 만들어둘 수 있습니다.




'----------------------
'임의의 벡터를 다른 벡터에 대해 평행한 성분과 수직인 성분으로 분해
Function Vec_Decomp(iVec() As Single, iVecTool() As Single, oTan() As Single, oNorm() As Single)
  Dim tL As Single
  ReDim oTan(1 To 2): ReDim oNorm(1 To 2)
  tL = Sqr(Vec_Inner(iVecTool, iVecTool))
  If tL = 0 Then
    oTan(1) = 0: oTan(2) = 0
    oNorm = iVec
  Else
    oTan = Vec_OprConst(iVecTool, Vec_Inner(iVec, iVecTool) / tL ^ 2, "*")
    oNorm = Vec_Opr(iVec, oTan, "-")
  End If
End Function
'----------------------

곡선형 도형 변환 - 2. Powerpoint에서 사용되는 Bezier 곡선과 B-Spline

Bezier 곡선에 대해 조금만 더 부연설명드리려고 합니다. Bezier 곡선은 시작점(P1)과 출발 방향을 조절하는 1번 제어점 (P2), 도착점(P4)과 도착 방향을 조절하는 2번 제어점(P3)으로 구성됩니다. P1~P4가 결정되면, 이들 점 사이를 연결하는 곡선은 3차 식으로 표현할 수 있으며, (X,Y)는 0~1까지 연속으로 변하는 매개변수로 표현되며, (X, Y) = (1-t)^3*P1+P2*3*(1-t)^2*t+P3*3*(1-t)*t^3+P4*t^3 식으로 나타냅니다. 


출발점 P1에서 처음 출발하는 방향은 P1-P2를 연결하는 직선과 평행한 방향이며, P4에 도착하는 방향은 P3-P4를 연결하는 선과 평행한 방향입니다. 따라서, P4 다음에 P3-P4와 반대 방향의 위치에 새로운 control point를 두면 P4는 매끄러운 곡선으로 연결됩니다.

P1~P4 곡선 상의 임의 위치를 구하고 싶다면, t 값을 0~1 사이의 값을 넣어 계산하면 됩니다만, t값이 등간격이라 하더라도, 곡선상의 점 간격은 다른 값을 가집니다. 위의 그림에서 빨간 색 점들은 t값이 0~1까지 0.1씩 증가시켜서 생성된 점의 위치이며, 시작점과 끝점에서는 간격이 넓지만, 중간 부분은 간격이 좁아지는 것을 볼 수 있습니다.

이러한 경향은 P1-P2 길이, P2-P3 길이, P3-P4 길이에 따라 경향이 달라집니다. 만약, P1, P2, P3, P4가 일직선 상에 있다면, 생성된 Bezier 곡선도 직선으로 나타나게 됩니다만, P2, P3의 위치를 다르게 두었을 때 t에 따라 곡선상의 점들의 간격은 아래와 같이 달라집니다.


만약, P1, P2, P3, P4의 간격이 동일한 경우(C/L=1/3), t값이 등간격이라면 직선상의 점들의 간격도 등간격이 됩니다. 그러나, P1-P2, P3-P4 간격이 좁고, P2-P3 간격이 넓다면, 중앙 부분의 점들이 넓어지고, 반대로, P2-P3 간격이 넓어지면, 점들의 간격도 넓어지게 됩니다. 또한, 아래 그림과 같이, 각 점들이 직선에 있지 않은 곡선이라 해도 마찬가지 경향을 가집니다. 뿐만 아니라, 그림에서 확인되는 것처럼 End point-Cotrol Point 간 거리가 멀어질수록, 곡선은 크게 꺽인 형태로 변하며, Control point 길이가 짧아질수록 P1-P4를 연결하는 직선에 가까워지는 것을 알 수 있습니다.



Powerpoint에서 그리는 곡선은 단일한 Bezier 곡선이 아니라, Bezier 곡선이 연결된 B-Spline입니다. 여러개의 Bezier 곡선이 end point를 공유하면서 연결되어있다고 생각하시면 되는데, 단일 Bezier 곡선 구간을 Segment라고 하고, 각 segment를 연결하는 점을 knot이라고 표현합니다. 그러나 powerpoint 매크로를 작성할 때에 knot이라는 속성은 없고, 'Nodes'라는 속성으로 표현됩니다.

곡선 구간의 경우에는 1개 segment를 표현할 때, 양끝의 End point에 해당하는 2개의 knot과 곡선의 모양을 결정하는 2개의 control point로 표현이 됩니다. 그러나, 직선 구간의 segment는 segment 자체를 직선 구간으로 정의한 후, 2개의 knot만 지정해줍니다.

한편, msoFreedom 속성을 가진 도형에서 Shape.Vertices 속성에 knot과 contol point가 일련의 배열로 저장되어 있는데, 직선과 곡선 구간이 섞여있다면 Segment의 특성을 확인하지 않으면 Vertices 배열만으로는 곡선 구간인지, 직선 구간인지 구분할 수 없습니다. 

  • 곡선형 Vertices : K1 - CP2 - CP3 - K4 - CP5 - CP6 - K7 - ... 
  • 직선형 Vertices : K1 - K2 - K3 - K4 - ...
  • 혼합형 Vertices : K1 - CP2 - CP3 - K4 - K5 - CP6 - CP7 - K8 - ...

따라서, 이전 글에서 GetVertices 함수를 만들어, knot만 추출하거나, 직선 구간에도 control point를 강제로 삽입하여 모든 구간을 곡선화하도록 했던 것입니다.


Powerpoint에서 Knot 또는 End point는 도형 템플릿 중 자유 곡선 그리기로 그릴 때 마우스로 클릭하는 점이 되며, 다각형 도형 등으로 그릴 때에는 꼭지점들에 해당하는 점이 됩니다. Control point는 powerpoint에서 자동으로 계산해주며, 굳이 변경하고자 한다면, 도형을 선택한 상태에서 마우스 오른쪽 버튼을 클릭하여 '점 편집'을 선택하면 각 점들의 위치를 조절할 수 있습니다. 그러나, '점 편집'을 이용해서 도형 모양을 조절하는 것은 매우 번거로울 뿐만 아니라, 원하는대로 도형이 조절되지 않는다는 문제가 있습니다. 약간 세밀한 작업이 필요하다면 Powerpoint에서 그림 그리기는 포기하고 별도의 소프트웨어를 선택하게 됩니다.


만약, 도형의 Knot들만 적당히 조절한 상태에서 control point를 자동으로 조절해준다면 어떨까요? 기본적으로 Powerpoint에서는 곡선 그리기를 할 때, control point를 아래와 같은 방식으로 삽입해줍니다. K1, K2, K3에 마우스를 클릭해서 점을 찍게 되면, K2에 삽입되는 control point는 K1-K3를 연결하는 두 선과 나란하고, control point간 길이는 K1-K3 길이의 1/3이 되도록, 즉 Control point-K2 길이를 K1-K3 길이의 1/6이 되도록 2개의 control point를 삽입해줍니다. 만약, 연속해서 K4, K5, ... 를 그리게 되면 K3-K4-K5 위치, K4-K5-K6 위치로부터 각각 control point를 삽입해줍니다. 이때, K2 점의 control point는 K1과 K3의 위치에만 영향을 받고, K4 이후의 점들에는 영향받지 않습니다.


여러가지 도형을 그리다보면, Powerpoint에서 자동으로 계산해주는 control point보다는 새로운 방법으로 control point를 그리고 싶을 수 있습니다. 예를 들어, 위 그림의 아래쪽 예시와 같이, K2의 control point를 K1-K2-K3가 이루는 각도를 2분할한 방향에 수직선, 다시말해 K1→K2 방향과 K2→K3 방향이 거울 반사가 되도록 control point를 삽입해주는 방법입니다. 이렇게 하면, 오른쪽의 4개의 Knot을 입력했을 때, Powerpoint에서 자동으로 그려주는 것보다 K2, K3를 통과하는 곡선이 조금더 둥근 형태로 바뀌는 것을 알 수 있습니다.

이외에도, K1, K2, K3를 입력하면 K1, K2, K3를 통과하는 원이 되도록 control point를 삽입할 수도 있으며, 이러한 경우에는 knot을 통과하는 곡선이 더욱 둥근 형태가 될 수 있습니다.

2022년 3월 28일

곡선형 도형 변환 - 1. Bezier 곡선 소개

Powerpoint에 삽입되는 도형은 벡터 이미지이며, 그림을 확대하더라도 높은 해상도를 유지할 수 있습니다. 따라서, JPG나 PNG 파일과 같은 그림 파일과 달리 고품질 이미지를 생성할 수 있으면서도 저장 용량이 많이 줄어드는 장점이 있습니다.

직선을 정의하기 위해서는 두 점의 위치만 지정해주고 이들을 연결하도록 선을 그어줄 수 있습니다. 곡선의 경우, 곡선 위의 모든 점에 대한 정보를 갖고 있는 것이 아니라, 특별한 규칙을 따라 선을 그릴 수 있는 정보만 갖고 있습니다.

Powerpoint에서는 Bezier 곡선을 사용하여 곡선을 그립니다. 엄밀히 말하면, B-Spline 곡선으로 연속된 곡선을 그리는 것이며, spline 곡선의 각 segment가 Bezier 곡선으로 구성된다고 생각하시면 되겠습니다. 따라서, Bezier 곡선이 어떻게 그려지는지를 알게 되면, 곡선을 보다 자유롭게 처리할 수 있습니다.

Bezier 곡선은 벡터 그래픽에서 사용되는 기본적인 곡선으로 사용되는데, 폰트 크기를 키우더라도 가장자리가 매끈하게 보이는 글꼴을 디자인할 때에도 사용되며, 그 용도에 따라 3차, 4차, 5차... Bezier 곡선을 사용할 수 있습니다. Powerpoint에서 곡선을 그리기 위해서는 반드시 4개의 점이 필요합니다. 즉 2개의 end point와 2개의 control point로 구성이 되며, 이 점들에 대한 정보를 일련의 숫자 배열로 가지고 있습니다.

한편, powerpoint에서 선을 그리다보면, 직선 구간과 곡선 구간이 혼합되어 있는 경우도 있습니다. 이때에는 각 구간(Segment)를 직선형인지, 곡선형인지 속성을 지정해두게 되는데, shape.Vertices로 추출한 점들의 위치 정보만으로는 직선, 곡선을 구분할 수 없고 shape.Nodes(i).SegmentType 속성을 확인해야 합니다. 만약, 각 선 구간을 곡선형 혹은 직선형으로 변경하고자 한다면, shape.Nodes.SetSegmentType이라는 method로 특정 segment의 속성 변경도 가능합니다.

문제는 각 segment의 속성을 변경하게 되면, Cotrol point의 위치는 powerpoint에서 자동으로 계산해서 변경해버리는데, 사용자가 원하는 위치로 변경되지 않기 때문에 수동으로 조절해야할 뿐만 아니라, node 수가 많아질수록 기하급수적으로 실행 속도가 느려집니다. 또한, control point를 원하는 위치로 배열시켜서 매끄러운 도형으로 변경하기 위해서는 결국 control point를 일일이 수작업으로 고쳐주어야 하는데, 이 작업이 매우 번거롭고, 원하는대로 잘 조절이 되지 않습니다. 따라서, 매크로를 이용하여 control point를 변경하기 위해서는 현재 지정된 end point들로부터 control point 위치를 어떻게 계산할 것인지에 대한 문제가 발생됩니다.

이러한 작업을 위해서는 Bezier 곡선에 대한 정확한 계산식을 이해할 필요가 있습니다. Cubic Bezier 곡선식은 3차 매개함수이긴 하나, 식보다는 그림으로 이해하는 것이 훨씬 도움이 됩니다. 아래에 Powerpoint에서 사용되는 Cubic Bezier curve를 작도하는 방법입니다.

P1과 P4는 End Point이며, 그 사이에 2개의 Control Point P2, P3를 삽입해둡니다. 이렇게 4개의 점이 정의되면, P1과 P4를 연결하는 선은 처음에는 P2를 향하는 선으로 시작해서, 점점 P3쪽으로 꺾였다가, 마지막에는 P4에 도착하는 곡선(검은선)을 생성시키게 됩니다.

도형을 선택한 상태에서 마우스 오른쪽 버튼을 눌러 '점 편집'으로 들어가게 되면, P1과 P4는 곡선상에 찍혀있는 점에 해당하고, P2와 P3는 P1이나 P4를 클릭했을 때 곡선의 모양을 변경할 수 있도록 나타나는 직선의 끝점에 해당합니다.

도형 삽입 템플릿 중 '곡선'을 삽입하게 되면, 사용자가 마우스로 클릭을 할 때마다 곡선을 그리기 위한 점들(end point)이 찍히고, 점들을 찍어나가다 보면 곡선의 모양이 계속 바뀌면서 최대한 매끄러운 곡선이 되도록 변경되는 것을 알 수 있습니다.

만약, 임의의 점들에 대한 정보를 입력해두고, 이 점들을 매끄러운 곡선으로 연결시킬 수 있다면, 다양한 도형을 만들 수 있을 것입니다. 예를 들면, 아래와 같이 Sin 곡선을 만들기 위해 몇 개의 점만 계산하고 나서 직선으로 연결하게 되면, 매끄러운 곡선이 만들어지지 않지만, 이들을 Bezier 곡선으로 변환시키게 되면 오른쪽 그림과 같이 부드러운 Sin 곡선으로 바뀌게 됩니다. 또한, 4각형 모양의 선을 연결한 후 적절하게 control point를 삽입해서 곡선으로 변환하면 나선형 곡선을 얻을 수도 있습니다.


이와 같이 몇 개의 End point 위치를 계산하여 배치시킨 후, 적절한 방법으로 Control point를 삽입해주는 것만으로도 많은 형태의 도형을 생성시킬 수 있습니다. 또한, End point는 계산할 필요없이 자유형 도형을 마우스로 클릭해서 적절하게 배치시킬 수도 있고, 도형 템플릿에 있는 다각형 도형을 조합하거나, 이전글에서 소개해드린 바와 같이 몇 개의 직선을 배치시킨 후 '곡선 결합하기'를 이용하여 원하는 도형이 되도록 End point를 배치시킬 수도 있습니다.

다음 글에서는 이렇게 배열된 End point 위치로부터 Control point를 계산하는 방법들을 소개해드리고자 합니다.

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


2022년 3월 25일

도형 변환하기 - 6. 자유형 도형(msoFreedom)의 Node 분할하기

Powerpoint는 직선이나 다각형, 원 등 완성된 도형은 템플릿으로 구할 수 있지만, 원의 일부분인 호나 직각선, 혹은 특정 각도를 갖는 선, 호와 선이 연결된 도형, 나선 등.. 여러가지 도형을 자유롭게 생성하기 어렵습니다.

비록 도형의 병합, 분할 등의 기능을 이용하여 여러가지 모양을 만들어 낼 수는 있습니다만, 면이 아닌 선으로만 구성된 도형을 생성하는 것은 쉽지 않습니다. 물론 수작업으로 점을 그려나가는 방법도 있지만, 모양이 원하는대로 잘 그려지지 않습니다.

아래와 같이 원기둥을 그린다고 하겠습니다. 도형 템플릿에 이미 원기둥 모양이 있으니 그냥 사용하셔도 되고, 원을 그린 후 3차원 설정을 할수도 있습니다. 혹은 원과 사각형을 조합하여 도형 병합과 빼기를 이용할 수도 있습니다.

여기에서는 새로운 방법으로 아래와 같이 원과 직선을 그리고, 원의 node를 분할한 후, 일부 곡선들을 다시 연결해서 원기둥의 옆면을 만들고, 다시 원을 붙여서 아래와 같이 원기둥 모양을 만들 수 있습니다.



도형을 위와 같이 node를 분할하고, 다시 원하는 선들만 모아서 결합할 수 있다면, 새로운 많은 도형을 생성할 수 있습니다. 아래와 같이 원의 node를 분할해서, 분할된 곡선의 크기를 조금씩 바꿔서 다시 연결하면 나선을 만드는 것도 가능합니다.



이러한 작업을 위해 우선 임의의 도형의 node를 분할해서 각각의 선으로 나눠주는 매크로를 작성하도록 하겠습니다. 다음에는 나눠진 선들을 다시 결합하여 1개 도형을 생성하도록 할 예정입니다.


'-------------------------------------
Function Sh_IsDrawing(iSh As Shape) As Boolean
  '현재 선택된 도형이 Drawing 형태인지 체크하는 함수입니다. 취급하려는 개체를 아래의 종류로만 한정했습니다만, 사용자가 자주 사용하는 shape 종류를 추가로 포함하거나, 사용하지 않는 경우에는 제외시켜도 됩니다.
  Sh_IsDrawing = (iSh.Type = msoAutoShape Or iSh.Type = msoFreeform Or iSh.Type = msoGroup Or iSh.Type = msoLine Or iSh.Type = msoTextBox)
End Function
'-------------------------------------
Function Sh_SplitNodes(iSlide As Slide, iSh As Shape) As Shape
  On Error GoTo ErrorHandler
  Dim tSL As ShapeRange, tSh() As Shape, tNode() As Single, tPoints() As Single, n As Long
  If Not Sh_IsDrawing(iSh) Then Set Sh_SplitNodes = iSh: Exit Function
  '단일 도형을 입력했을 때, 각 node에서 분할하는 함수입니다.
  '미리 만들어둔 함수를 이용하여, 도형의 각 node, 즉 end point와 control point의 좌표를 구해줍니다. 이때, 곡선을 유지할 수 있도록 모든 segment를 곡선형으로 만들어줍니다.
  tPoints = GetVertices(iSh, False)
  ReDim tNode(1 To 4, 1 To 2)
  n = 0
  '각 4개의 node, 즉 1~4, 4~7, 7~11.. 구간의 node만 잘라서 곡선을 생성해주고 원본은 삭제합니다.
  For i = 1 To UBound(tPoints, 1) - 1 Step 3
    n = n + 1
    ReDim Preserve tSh(1 To n)
    For j = 1 To 4
      tNode(j, 1) = tPoints(i + j - 1, 1): tNode(j, 2) = tPoints(i + j - 1, 2)
    Next
    Set tSh(n) = iSlide.Shapes.AddCurve(tNode)
  Next
  iSh.Delete
  '만약 1개의 segment였다면 생성된 현을 그대로 반환하고, 2개 이상의 segment로 나눠져있다면 생성된 도형을 그룹화해서 반환해줍니다. 여러 개로 나눠진 경우, 도형을 다시 선택할 때 귀찮기 때문에 그룹화해서 반환합니다.
  If n = 1 Then Set Sh_SplitNodes = tSh(n) Else Set Sh_SplitNodes = GetShapeRange(iSlide, tSh).Group
ErrorHandler:
  Erase tSh, tNode, tPoints
End Function
'-------------------------------------
Function Shs_SplitNodes(iSlide As Slide, iSh As Shape) As Shape
  On Error GoTo ErrorHandler
  Dim tSL As ShapeRange, tSh() As Shape, i As Long
  If Not Sh_IsDrawing(iSh) Then Set Shs_SplitNodes = iSh: Exit Function
  '슬라이드에서 여러 개의 도형을 선택한 상태에서 일괄 분할하기 위한 함수입니다.
  '재귀함수이며, 그룹화된 도형은 계속 그룹해제하고, 단일 도형인 경우에는 node를 분할해줍니다. 아래와 같이 생성하게 되면, 원래 그룹화된 순서를 그대로 유지하면서 segment로 모두 분할됩니다.
  If iSh.Type = msoGroup Then
    Set tSL = iSh.Ungroup
    ReDim tSh(1 To tSL.Count)
    For i = 1 To tSL.Count
      Set tSh(i) = Shs_SplitNodes(iSlide, tSL(i))
    Next
    Set Shs_SplitNodes = GetShapeRange(iSlide, tSh).Group
  Else
    Set Shs_SplitNodes = Sh_SplitNodes(iSlide, iSh)
  End If
ErrorHandler:
  Erase tSh
End Function
'-------------------------------------
Sub 도형생성_선분할()
  On Error GoTo ErrorHandler
  Dim tSR As ShapeRange, tSh() As Shape, i As Long, tSlide As Slide
  '현재 슬라이드에서 도형을 선택한 상태에서 매크로를 실행하게 되면, 선택된 모든 도형을  node에서 분할하여 segment로 나눠주게 됩니다.
  Set tSlide = ActiveSlide
  Set tSR = SelectedShapeRange(False)
  ReDim tSh(1 To tSR.Count)
  For i = LBound(tSh) To UBound(tSh)
    Set tSh(i) = Shs_SplitNodes(tSlide, tSR(i))
  Next
  GetShapeRange(tSlide, tSh).Select msoTrue
ErrorHandler:
  Erase tSh
End Sub


2022년 3월 22일

도형 변환하기 - 5. 자유형 도형(msoFreedom)의 꼭지점 좌표 구하기

Powerpoint를 도형을 생성하게 되면 아래와 같이 직선 및 곡선 Segment를 조합하여 그림을 그립니다. 직선형인 경우, P1, P2, P3.. 점들은 꼭지점이 되고, 각 꼭지점을 연결한 직선으로 구성이 됩니다. 그러나, 곡선인 경우, P1~P4가 1개의 Bezier 곡선을 그리고, P4~P7이 다음 Bezier 곡선을 그려서 연결하는 방식으로 도형을 만들어 나갑니다. 따라서, 순수한 직선형인 경우, n개의 Segment가 있으면 n+1개 점(node)의 위치 정보만 있으면 되고, 순수한 곡선형인 경우, n개의 Segment를 표현하기 위해 3n-2개 점의 위치 정보가 있으면 되며, 점들의 위치 정보만으로도 도형을 완벽하게 재현해낼 수 있습니다.

그러나, 경우에 따라서는 곡선형 Segment와 직선형 Segment가 연속되어있는 경우도 있으며, 연속된 점들의 정보만으로는 직선인지, 곡선인지 구분할 수 없기 때문에, Segment의 특성을 직선형 또는 곡선형으로 정의해 주어야 합니다. 따라서, 각 점의 위치 정보만으로 도형을 완벽하게 재현하지 못합니다.





곡선형인 경우, P1, P4, P7... 의 점은 Bezier 곡선을 그릴 때, 시작 및 끝점(End point)에 해당하고, 이 점들은 자유형 그림을 그릴 때, 사용자가 마우스로 클릭해주는 위치입니다. P2, P3, P5, P7은 곡선형 도형을 생성할 때 Powerpoint에서 자동으로 삽입해주며, 도형을 클릭해서 '점편집' 메뉴를 이용하여 인위적으로 변경할 수 있습니다. 이 점들은 곡선의 꺾인 정도나 모양을 결정해주는 점들이며 Bezier 곡선의 Control Point라고 합니다.

만약, 곡선형 도형의 점들 중, 3n+1번째 점들만 남겨놓고 나머지 점들을 지워버린다면 P1, P4, P7... 의 정보만 남게 되며, 점들만 선으로 연결하면, 직선형으로 바뀌게 됩니다.

반대로, P1, P2, P3... 의 직선형 도형에 인위적으로 Control Point를 삽입해주면 원하는 형태의 곡선형 도형으로도 바꿔줄 수 있습니다. 예를 들어, Powerpoint에서 원은 마름모 모양으로 배열된 4개의 End point와 원이 될수 있도록 적절한 위치에 Control Point를 삽입해준 형태이며, Control Point를 삭제하게 되면 다시 마름모가 됩니다.


도형의 모든 점들의 위치 정보는 Shape.Vertices 속성으로부터 쉽게 얻을 수 있습니다. 그러나, Vertices 속성은 msoFreedom 형식, 즉 자유형 도형에만 지원하기 때문에 도형 템플릿으로 그린 도형은 바로 구할 수 없습니다. 따라서, 도형이 가진 점들의 위치 정보를 얻기 위해서는 이전 글에서 소개해드린 함수를 이용하여 자유형 도형으로 변환한 후 Shape.Vertices 속성으로 구하게 됩니다.


점에 대한 정보를 구하는 목적은 해당 점들을 이용해서 새로운 도형을 생성하거나, 점을 새로 배치해서 도형을 변형하는 등의 작업을 하기 위해서 입니다. 만약 도형에 점을 추가하고 싶다면, Shape.Nodes.Insert를 사용할 수 있습니다. 직선형이나 곡선형 모두 추가할 수 있고, 점의 위치만 정확하게 지정해주면 됩니다. 1~2개 점은 이런 식으로 추가할 수 있지만, 도형에 포함된 node 갯수가 증가할수록 실행 속도는 점점 느려집니다. 100여개 정도의 점을 추가해주려고 하니 PC 사양에 따라서는 몇 분 이상이 걸리기도 합니다. 따라서, Node가 많은 여러 개의 도형에 대해 작업하려고 한다면, 작업 시간이 매우 느려지는 문제가 있습니다. (freehand 형태의 도형으로 작업하다보면, 포함되는 node 갯수가 많기 때문에, 때로는 Powerpoint가 멈춰버리는 문제도 생깁니다.)

한편, 모든 점들에 대한 정보만 있다면, 직선형인 경우 Slide.Shapes.AddPolyLine(좌표배열), 곡선형인 경우 Slide.Shapes.AddCurve(좌표배열)을 이용하여 한꺼번에 도형을 생성하게 되면 상대적으로 실행속도가 매우 빠른 것을 알 수 있습니다. 따라서, 처리해야할 점의 갯수가 많다면, 점들의 정보를 모두 읽어와서, 이 점들의 정보로부터 새로운 좌표배열을 생성한 후, 완전히 새로운 도형으로 생성하는 것이 속도면에서는 훨씬 빠릅니다. 다만, AddPolyLine이나 AddCurve는 도형의 모든 node가 직선형이거나 곡선형이어야 한다는 제약이 있습니다.

따라서, 도형의 좌표를 읽어올 때, 처음부터 곡선형 좌표 또는 직선형 좌표 배열 형식으로 읽어오도록 함수를 만들어 둡니다.

아래에 도형(Drawing)의 좌표배열을 구하는 함수를 생성해두었습니다. 앞으로 슬라이드에 간단히 생성한 도형을 변형하거나, 배열하는데 있어 이러한 점 좌표 배열을 이용할 예정입니다.


'---------------------------------------------
Function GetVertices(iSh As Shape, Optional iDelControl As Boolean = False) As Single()
  On Error GoTo ErrorHandler
  Dim tSh As Shape, tP() As Single, tPX() As Single, tPY() As Single, i As Long, j As Long, n As Long
  '만약 입력된 도형이 msoFreedom 형식이 아니라면, msoFreedom으로 변환합니다.
  If iSh.Type = msoFreeform Then 
    Set tSh = iSh
  Else 
    Set tSh = Sh_ConvAutoShape(iSh, False)
  End If

  '도형으로부터 좌표 배열을 읽어옵니다. (1 to n, 1 to 2) 배열이며, (n, 1)에는 n번째 점의 X좌표가, (n, 2)에는 n번째 점의 Y좌표가 입력됩니다. 여기에서 X좌표는 슬라이드 왼쪽 끝으로부터 거리이며, Y좌표는 슬라이드 위쪽 끝으로부터 거리입니다.
  tP = tSh.Vertices
  n = 1
  ReDim Preserve tPX(1 To 1): ReDim Preserve tPY(1 To 1)
  tPX(1) = tP(1, 1): tPY(1) = tP(1, 2)

  '1번점은 시작점이며, 2번째 node의 종류에 따라 좌표를 읽어옵니다. 만약, Control Point를 모두 삭제하고 꼭지점에 해당하는 End Point만 읽어오려고 한다면, 직선형 Node는 바로 다음 점을, 곡선형 Node는 2번째 뒤의 점을 읽어 옵니다. 이렇게 읽은 점 배열은 이후 AddPolyLine으로 다시 그리게 되면, 다각형이 생성됩니다.
  If iDelControl Then
    For i = 2 To tSh.Nodes.Count
      n = n + 1
      ReDim Preserve tPX(1 To n): ReDim Preserve tPY(1 To n)
      If tSh.Nodes(i).SegmentType = msoSegmentLine Then
        tPX(n) = tP(i, 1): tPY(n) = tP(i, 2)
      Else
        i = i + 2
        tPX(n) = tP(i, 1): tPY(n) = tP(i, 2)
      End If
    Next
    ReDim tP(1 To n, 1 To 2)
    For i = 1 To n: tP(i, 1) = tPX(i): tP(i, 2) = tPY(i): Next
  Else
  '만약, 모든 점을 곡선형의 좌표배열로 만들어서 출력합니다. 만약, 해당 Node가 직선형이라면, 이전 점의 좌표와 현재 점의 좌표를 Control Point로 추가해주며, 나중에 AddCurve로 그리더라도 직선으로 표현이 됩니다. 만약, 해당 Node가 곡선형이라면 좌표를 그대로 읽어옵니다.
    For i = 2 To tSh.Nodes.Count
      n = n + 3
      ReDim Preserve tPX(1 To n): ReDim Preserve tPY(1 To n)
      If tSh.Nodes(i).SegmentType = msoSegmentLine Then
        tPX(n - 2) = tP(i - 1, 1): tPY(n - 2) = tP(i - 1, 2)
        tPX(n - 1) = tP(i, 1): tPY(n - 1) = tP(i, 2)
        tPX(n) = tP(i, 1): tPY(n) = tP(i, 2)
      Else
        i = i + 2
        tPX(n - 2) = tP(i - 2, 1): tPY(n - 2) = tP(i - 2, 2)
        tPX(n - 1) = tP(i - 1, 1): tPY(n - 1) = tP(i - 1, 2)
        tPX(n) = tP(i, 1): tPY(n) = tP(i, 2)
      End If
    Next
    ReDim tP(1 To n, 1 To 2)
    For i = 1 To n: tP(i, 1) = tPX(i): tP(i, 2) = tPY(i): Next
  End If
  GetVertices = tP
  '만약, 입력된 도형이 msoFreedom 형식이 아니라면, 좌표를 읽기 위해 생성시킨 tSh를 삭제합니다. 
  If Not iSh.Type = msoFreeform Then tSh.Delete
ErrorHandler:
  Erase tP, tPX, tPY
End Function


2022년 3월 20일

도형 변환하기 - 4. 도형을 Polyline 형태(msoFreedom)로 변환하기

도형(drawing)의 종류에 따라 도형이 가지고 있는 꼭지점의 위치 정보를 알아내는 방법이 다릅니다. 꼭지점의 정보를 알아내기 위해서는 우선 모든 도형을 자유형(msoFreedom), 즉 polyline 형태의 도형으로 변환시켜야 합니다.

한가지 방법으로 변환시킬 수도 있겠습니다만, 도형에 맞게 방법을 달리 하도록 함수를 작성하였으며, 자세한 방법은 함수의 주석으로 설명을 드리겠습니다.

아래와 같이 생성된 도형은 원본과 모양은 똑같으나, 선이나 채우기 특성은 달라집니다. 필요하다면 모든 작업을 완료한 후, 원본의 속성을 복사하도록 합니다.

'------------------------------------
Function Sh_ConvAutoShape(iSh As Shape, Optional iDelOrigin As Boolean = False) As Shape
  Dim tSlide As Slide, tSL As ShapeRange, tSh As Shape, tSh2 As Shape, tNode As ShapeNode, tIsCurve As Boolean
  Dim tPos() As Single, tX(1 To 2) As Single, tY(1 To 2) As Single, tRot As Single, tVal As Single
  Set tSlide = ActiveSlide
  Select Case iSh.Type
    Case msoAutoShape
  'AutoShape인 경우, 모든 도형이 닫힌 형태의 도형입니다. 자유형으로 변환시키기 위해서는 1개의 node만 직선형에서 곡선형으로 바꿔주면 자유형으로 바뀝니다. 그반대로 곡선형 node를 직선형 node로 바꿔주어도 자유형으로 바뀌지만 모양이 유지되지 않습니다. 따라서, 직선형 node만 곡선으로 바꿔줍니다. 
      Set tSh = Sh_Copy(iSh)
      i = 0: tIsCurve = True
      For i = 1 to tSh.Nodes.Count
        i = i + 1
        If tSh.Nodes(i).SegmentType = msoSegmentLine Then
          tSh.Nodes.SetSegmentType i, msoSegmentCurve
          tIsCurve = False
        End If
      Next
  '만약 원과 같이 모든 node가 곡선형인 경우, 직선형 node를 포함하지 않기 때문에 이런 도형은 아래와 같이, 해당 도형을 복제해서 병합해줍니다. 
      If tIsCurve Then

        Set tSh2 = Sh_Copy(tSh)
        Set tSL = tSlide.Shapes.Range(Array(tSh.Name, tSh2.Name))
        tSL.MergeShapes msoMergeUnion
        Set tSh = tSlide.Shapes(tSlide.Shapes.Count)
      End If
  '입력해준 도형을 지우고 자유형만 남기고 싶다면 원본은 삭제합니다.
      If iDelOrigin Then iSh.Delete


    Case msoFreeform
  '자유형이 입력되면 아무것도 하지 않습니다. 다만 다른 형태의 도형인 경우와 맞추기 위해 , 원본을 남기도록 한다면, 1개를 더 복제해 줍니다.
      If iDelOrigin Then Set tSh = iSh Else Set tSh = Sh_Copy(iSh)


    Case msoLine
  'Line인 경우, 직선형과 나머지(곡선형 또는 꺾은선 형태 등)를 구분합니다. 그림을 그리다보면 개체를 연결하는 곡선보다는 직선을 주로 많이 사용하게 되는데, 직선인 경우에는 아래와 같이 생성해줍니다.
      Select Case iSh.ConnectorFormat.Type

        Case msoConnectorStraight
  '직선을 생성한 후 회전된 경우가 있기 때문에 원본을 1개 복사해줍니다. 회전하지 않은 직선 상태로 되돌린 후, 좌측 상단, 우측 하단의 위치를 구해줍니다. 이때, 좌측 상단에서 우측 하단으로 향하는 직선이 기준이 됩니다.
          Set tSh = Sh_Copy(iSh)
          tSh.Rotation = 0
          With tSh
            tX(1) = tSh.Left: tX(2) = tSh.Left + tSh.Width: tY(1) = tSh.Top: tY(2) = tSh.Top + tSh.Height
  '만약, 좌측 하단에서 우측 상단으로 향하는 직선이라면, 도형은 상하 방향으로 flip된 형태입니다. 따라서, 상/하 좌표를 바꿔줍니다.
            If iSh.VerticalFlip Then tVal = tY(1): tY(1) = tY(2): tY(2) = tVal
  '마찬가지로, 우측에서 좌측으로 향하는 화살표라면 좌우 방향으로 flip된 형태의 속성을 가집니다.
            If iSh.HorizontalFlip Then tVal = tX(1): tX(1) = tX(2): tX(2) = tVal
  '직선의 속성을 확인했다면 복제된 직선은 삭제를 합니다.
            .Delete
          End With
  '직선의 시작점과 끝점을 확인했으므로, AddPolyLine method를 이용하여, 직선을 생성해준 후, 원본 회전 각도만큼 회전시켜주면, msoLine으로 생성된 직선을 자유형으로 변환할 수 있습니다.
          ReDim tPos(1 To 2, 1 To 2)

          tPos(1, 1) = tX(1): tPos(1, 2) = tY(1)
          tPos(2, 1) = tX(2): tPos(2, 2) = tY(2)
          Set tSh = tSlide.Shapes.AddPolyline(tPos)
          tSh.Rotation = iSh.Rotation
        Case Else
  'msoLine 중 곡선형태 또는 꺾은선형태라면, 꼭지점 위치를 구할 수 없습니다. 따라서, 아래와 같이, EMF 형식의 그림으로 변환한 후, 다시 Shape으로 다시 변환하는 방식으로 자유형으로 변환합니다. 이를 위해서는 이미 만들어둔 함수를 활용할 수 있습니다.
          Set tSh2 = Sh_Copy(iSh)
          With tSh2
  '만약, msoLine이 점선 형태라면, EMF로 변환할 때, 각 점들이 각각의 선으로 변환되기 때문에 1개의 선이 아니라 여러 개의 선으로 바뀝니다. 따라서, DashStyle을 점선이 아닌 직선으로 바꿔줍니다. 또한 시작 또는 끝점이 화살표나 원형점 등으로 지정되어있다면 다른 도형으로 변환되기 때문에 이러한 특성도 모두 해제해서 선만 남겨두며, 회전이 되어있다면 회전각도 초기화시킵니다.
            .Line.DashStyle = msoLineSolid

            .Line.BeginArrowheadStyle = msoArrowheadNone
            .Line.EndArrowheadStyle = msoArrowheadNone
            tRot = .Rotation
            .Rotation = 0
            tX(1) = .Width: tX(2) = .Height
          End With
  'EMF로 변환했다가 다시 Shape으로 변환하게 되면, 선만 남게 됩니다. 그러나, EMF로 변환하면서 삽입된 frame으로 인해 도형의 크기가 원본과 약간 차이가 나게 됩니다. 따라서, 최종 변환된 도형을 원본과 크기와 회전 속성을 원본과 동일하게 맞춰줍니다.
          Set tSh =
Convert2Pic(tSh2, ppPasteEnhancedMetafile, 0, 0)
          Set tSh = ConvertEMF2Shape(tSh, True, 0, 0)
          tSh2.Delete
          With tSh
            .LockAspectRatio = msoFalse
            .Width = tX(1): .Height = tX(2)
            .Rotation = tRot
            .Left = iSh.Left: .Top = iSh.Top
          End With
      End Select
      If iDelOrigin Then iSh.Delete

    Case msoTextBox
  '텍스트 박스는 텍스트 상자의 위치, 크기 및 회전 정보로부터 4각형의 좌표를 생성한 후 AddPolyline으로 4각형을 생성하고, 회전 속성을 입력해주면 됩니다.
      ReDim tPos(1 To 5, 1 To 2)

      tX(1) = iSh.Left: tX(2) = iSh.Left + iSh.Width: tY(1) = iSh.Top: tY(2) = iSh.Top + iSh.Height
      tPos(1, 1) = tX(1): tPos(1, 2) = tY(1)
      tPos(2, 1) = tX(2): tPos(2, 2) = tY(1)
      tPos(3, 1) = tX(2): tPos(3, 2) = tY(2)
      tPos(4, 1) = tX(1): tPos(4, 2) = tY(2)
      tPos(5, 1) = tX(1): tPos(5, 2) = tY(1)
      Set tSh = tSlide.Shapes.AddPolyline(tPos)
      tSh.Rotation = iSh.Rotation
      If iDelOrigin Then iSh.Delete
  End Select
  Set Sh_ConvAutoShape = tSh
  Erase tPos, tX, tY
End Function

많이 본 글 :