.선택의 정확도 향상

ApiDraw 도형의 종류에 상관없이 8개의 트래커를 그리는데 직선의 경우는 트래커가 다소 어울리지 않는다. 직선의 끝이 아닌 모서리를 끌어서 직선의 크기나 모양을 변경할 있는데 이런 동작이 상당히 부자연스러워 보인다. 그래서 직선의 경우는 끝에 개의 트래커만 그리도록 수정해 보자. 트래커의 영역을 구하는 GetTrackerRect 함수를 수정하면 된다.

 

void GetTrackerRect(int idx,int nTrack,RECT *trt)

{

   ....

   if (arObj[idx]->Type == DT_LINE) {

      if ((arObj[idx]->Flag & 03) == DS_LT || (arObj[idx]->Flag & 03) == DS_RB) {

          if (nTrack != 1 && nTrack != 8) {

             SetRect(trt,0,0,0,0);

          }

      }

      if ((arObj[idx]->Flag & 03) == DS_RT || (arObj[idx]->Flag & 03) == DS_LB) {

          if (nTrack != 3 && nTrack != 6) {

             SetRect(trt,0,0,0,0);

          }

      }

   }

}

 

직선인 경우는 방향에 따라 개의 트래커만 남기고 나머지 트래커의 영역은 모두 0 되도록 했다. 트래커의 크기가 0이므로 트래커는 그려 봐야 화면에 보이지 않으며 히트 테스트도 항상 실패한다. 따라서 OnPaint TrackerHitTest 함수의 코드는 수정하지 않아도 된다.

다음은 선택의 정확도를 조금 높여 보자. 사각이나 비트맵은 직사각형 모양을 하고 있으므로 도형이 차지하고 있는 영역안의 아무 곳이나 클릭해도 선택되는 것이 옳지만 타원이나 직선은 그렇지 않다. 타원의 바깥쪽이라도 외접 사각형을 클릭하면 선택되는데 타원만 있을 때는 이런 선택이 이상하게 보이지 않지만 타원 뒤쪽의 다른 도형을 선택하고자 때는 다소 불편할 수도 있다. 정확하게 타원 안쪽에 있을 때만 선택하도록 FindObject 함수를 수정해 보자.

 

struct ddapack {

   int x,y;

   int offset;

};

void CALLBACK lineproc(int x,int y,LPARAM lParam)

{

   ddapack *pack=(ddapack *)lParam;

   int offset;

 

   offset=(int)hypot(x-pack->x,y-pack->y);

   pack->offset=min(pack->offset,offset);

}

 

int FindObject(int x, int y)

{

   int idx;

   POINT pt;

   HRGN hRgn;

   BOOL bIn;

   ddapack pack;

   RECT temprt;

 

   pt.x=x;

   pt.y=y;

   for (idx=arNum-1;idx>=0;idx--) {

      temprt=arObj[idx]->rt;

      InflateRect(&temprt,3,3);

      if (PtInRect(&temprt,pt)==TRUE) {

          switch (arObj[idx]->Type) {

          case DT_LINE:

             pack.x=x;

             pack.y=y;

             pack.offset=100;

             if ((arObj[idx]->Flag & 03) == DS_LT || (arObj[idx]->Flag & 03) == DS_RB) {

                LineDDA(arObj[idx]->rt.left, arObj[idx]->rt.top,arObj[idx]->rt.right,

                   arObj[idx]->rt.bottom,lineproc,(LPARAM)&pack);

             } else {

                LineDDA(arObj[idx]->rt.right, arObj[idx]->rt.top,arObj[idx]->rt.left,

                   arObj[idx]->rt.bottom,lineproc,(LPARAM)&pack);

             }

             if (pack.offset <= 4) {

                return idx;

             }

             break;

          case DT_ELLIPSE:

             hRgn=CreateEllipticRgn(arObj[idx]->rt.left, arObj[idx]->rt.top,

                arObj[idx]->rt.right,arObj[idx]->rt.bottom);

             bIn=PtInRegion(hRgn,x,y);

             DeleteObject(hRgn);

             if (bIn) return idx;

             break;

          default:

             if (PtInRect(&arObj[idx]->rt,pt)==TRUE) {

                return idx;

             }

             break;

          }

      }

   }

   return -1;

}

 

타원인 경우는 타원 영역과 일치하는 리전을 생성한 PtInRegion 함수로 클릭한 지점이 타원의 안쪽인지 아닌지를 점검했다. 리전은 복잡한 모양에 대해서도 히트 테스트를 정확하게 수행한다. 직선의 경우는 다른 방식을 도입했는데 직선과 클릭한 점의 좌표간의 근접 거리를 구해 거리가 4이하일 선택한 것으로 인정한다. 정확하게 직선 위를 클릭하기 어려우므로 약간의 여유분을 두어 직선과 가까운 거리를 찍을 때도 선택하는 것이 편리하다.

직선과 클릭한 좌표와의 거리를 구하는 방법은 여러 가지가 있는데 예제는 LineDDA 함수로 직선을 구성하는 모든 점과의 거리를 구한 가장 가까운 거리를 취하는 방법을 선택했다. 방법보다 수학적으로 간단하고 쉬운 방법도 있지만 LineDDA 함수를 이런 식으로 사용할 수도 있다는 것을 보여주기 위해 일부러 API 함수를 사용해 보았다.

직선과 타원의 경우 선택 점검에 다소 시간이 소모되므로 모든 직선과 타원에 대해 코드를 실행하지 않고 일단 PtInRect 영역 안인지를 먼저 점검한 세부적인 점검 루틴을 실행하도록 했다. 그래서 영역의 안쪽이 아닌 도형에 대해서는 리전을 만들거나 콜백 함수를 호출하는 불필요한 처리를 하지 않는다. 이렇게 하지 않으면 문서에 직선이 아주 많을 경우 선택에 시간이 너무 오래 걸릴 것이다.

그래서 도형에 대해 리전 점검을 하기 전에 대충의 검사를 먼저 하는데 도형의 사각 영역을 3픽셀씩 바깥으로 확장하여 영역 안에 있을 때만 리전 점검을 하도록 했다. 3픽셀씩 확장한 점검하는 이유는 수평, 수직선의 경우 폭과 높이가 0이기 때문에 어떤 점도 안쪽에 존재할 없기 때문이다. 점검은 어디까지나 대충의 점검일 뿐이며 조건이 만족할 아래쪽의 switch 문에서 다시 한번 정밀 점검을 하도록 했다.