.정규화

여기까지 코드를 작성한 테스트해 보면 ApiDraw 선택 코드에 약간의 문제가 있음을 발견할 있다. 도형을 그릴 흔히 마우스를 좌상단에서 우하단으로 드래그하는데 경우는 도형의 영역이 정규화되어 있으므로 아무 문제가 없다. 그러나 드래그 방향을 바꾸면 도형은 선택되지 않는다. 그런지 그림으로 설명해 보자.

ApiDraw 마우스 핸들러들은 최초 클릭한 지점을 좌상단으로 하고 마우스 버튼을 놓은 지점을 우하단으로 기록하는데 좌상단에서 우하단으로 드래그한 경우는 사각형의 모양이 정상적이지만 반대로 우하단에서 좌상단으로 드래그를 경우는 left right보다 오른쪽에 있고 top bottom보다 아래에 있는 비정상적인 사각형이 만들어진다. 이런 경우라도 Rectangle, Ellipse 함수들은 좌표를 조정한 그리도록 되어 있어 도형은 제대로 출력된다. 그러나 사각 영역안에 점이 포함되어 있는지를 조사하는 PtInRect 함수는 그렇지 못해 정규화된 사각형만을 인수로 요구한다. 함수의 내부 코드는 아마 다음과 같을 것이다.

 

if (prt->left < pt.x && prt->right > pt.x && prt->top < pt.y && prt->bottom > pt.y)

   return TRUE;

else

   return FALSE;

 

pt.x left right 사이에 있고 pt.y top bottom 사이에 있을 때만 점이 사각 영역 내부에 있다고 판단한다. 조건을 만족하려면 left right보다 왼쪽에 있고 top bottom보다는 위쪽에 있어야 한다. 그런데 드래그 방향을 바꾸어 버리면 조건을 절대로 만족할 없다. left보다 x left보다 작은 right보다 결코 작을 없기 때문이다.

PtInRect 함수는 인수로 전달된 사각형이 정규화되어 있다고 가정하기 때문에 함수를 계속 사용하려면 정규화 조건을 만족시켜야 한다. 도형의 영역이 결정되는 시점은 AppendObject 함수에서이므로 함수에서 전달된 좌표를 바로 대입하지 말고 정규화한 대입하면 일단 문제가 해결된다.

 

BOOL AppendObject(DTool Type,int x1,int y1,int x2,int y2)

{

   ....

   arObj[arNum]=(DObject *)malloc(sizeof(DObject));

   arObj[arNum]->Type=Type;

   SetRect(&arObj[arNum]->rt,x1,y1,x2,y2);

   arObj[arNum]->rt.left=min(x1,x2);

   arObj[arNum]->rt.right=max(x1,x2);

   arObj[arNum]->rt.top=min(y1,y2);

   arObj[arNum]->rt.bottom=max(y1,y2);

   arNum++;

   return TRUE;

}

 

left x1, x2 작은 값을 취하고 right x1, x2 값을 취하면 항상 left right보다 왼쪽에 있게 것이다. 수직쪽으로도 마찬가지로 top bottom보다 항상 위쪽에 있도록 조정했다. 이렇게 하면 드래그한 방향에 상관없이 사각형은 항상 정규화되어 좌상단에서 우하단으로 드래그한 것처럼 기록된다. 다양한 형태로 존재하는 데이터를 다루기 쉬운 일정한 형태로 변환하는 것을 정규화(Normalization) 하는데 한가지 모양의 사각형만 존재하도록 함으로써 다른 부분의 코드가 훨씬 단순해진다.

방법 대신 FindObject에서 도형의 영역을 사용할 arObj rt 대한 사본을 복사한 사본을 정규화하여 비교할 수도 있다. AppendObject 코드를 잠시 주석 처리해 놓고 다음 테스트 코드를 작성해 보자.

 

int FindObject(int x, int y)

{

   int idx;

   POINT pt;

   RECT trt;

 

   pt.x=x;

   pt.y=y;

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

      trt.left=min(arObj[idx]->rt.left,arObj[idx]->rt.right);

      trt.right=max(arObj[idx]->rt.left,arObj[idx]->rt.right);

      trt.top=min(arObj[idx]->rt.top,arObj[idx]->rt.bottom);

      trt.bottom=max(arObj[idx]->rt.top,arObj[idx]->rt.bottom);

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

          return idx;

      }

   }

   return -1;

}

 

이렇게 해도 일단은 동작하는데 어차피 검색할 정규화되므로 아무 문제가 없다. 도형을 기록할 아예 정규화를 해서 기록할 것인지 아니면 비교할 때만 잠시 정규화를 것인지가 다른데 비교할 때마다 정규화를 하려면 그만큼 속도의 낭비가 있으므로 기록할 정규화를 하는 것이 합리적이다. 기록은 어쩌다 한번이지만 검색은 선택, 커서 변경 수시로 해야 하므로 가급적이면 횟수가 작은 쪽에서 정규화를 하는 것이 좋다. ApiDraw AppendObject에서 정규화를 하는 방식을 채택하기로 한다.

이렇게 코드를 수정한 테스트해 보면 타원이나 사각형은 문제없이 그려지기도 하고 선택도 되지만 직선의 경우는 무조건 정규화되어 버리기 때문에 우하향의 직선만 그려진다. 직선은 드래그 방향에 따라 우하향, 좌하향 가지 모양이 있어 정규화에 따른 부작용이 생긴다. 그래서 직선의 경우는 사용자가 마우스를 드래그한 방향을 따로 기억해 두고 방향에 따라 도형을 다르게 그려야 한다.

 

#define DS_LT 0

#define DS_RT 1

#define DS_LB 2

#define DS_RB 3

....

struct DObject

{

   DTool Type;

   RECT rt;

   unsigned short Flag;

};

 

DS_* 매크로는 드래그를 시작한 시작점이며 도형에 정보를 기억하는 Flag라는 멤버를 추가한다. AppendObject에서 전달된 좌표의 대소를 평가하여 드래그한 방향을 기록하도록 한다.

 

BOOL AppendObject(DTool Type,int x1,int y1,int x2,int y2)

{

   ....

   arObj[arNum]->rt.left=min(x1,x2);

   arObj[arNum]->rt.right=max(x1,x2);

   arObj[arNum]->rt.top=min(y1,y2);

   arObj[arNum]->rt.bottom=max(y1,y2);

   if (x1<x2) {

      if (y1<y2) {

          arObj[arNum]->Flag=DS_LT;

      } else {

          arObj[arNum]->Flag=DS_LB;

      }

   } else {

      if (y1<y2) {

          arObj[arNum]->Flag=DS_RT;

      } else {

          arObj[arNum]->Flag=DS_RB;

      }

   }

   arNum++;

   return TRUE;

}

 

이렇게 하면 사각 영역도 정규화되고 드래그를 시작한 지점도 있다. OnPaint에서 직선을 그릴 때는 Flag 참조해서 방향을 결정한다.

 

      case DT_LINE:

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

             MoveToEx(hdc,arObj[idx]->rt.left,arObj[idx]->rt.top,NULL);

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

          } else {

             MoveToEx(hdc,arObj[idx]->rt.left,arObj[idx]->rt.bottom,NULL);

             LineTo(hdc,arObj[idx]->rt.right,arObj[idx]->rt.top);

          }

          break;

 

DS_LT, DS_RB 때는 우하향의 직선을 그리고 DS_LB, DS_RT 때는 우상향의 직선을 그렸다. 차후 직선끝에 화살표 장식을 달거나 정보가 유용하게 사용될 것이다. Flag 멤버는 16비트 길이를 가지며 드래그 시작점은 하위 2비트만을 사용하므로 나머지 14비트는 도형의 다른 속성을 기록하는 용도로 사용할 있다. 그래서 Flag로부터 방향을 읽을 & 03(이진수로 11) 연산하여 하위 2비트만 점검하여 차후에 상위 비트가 다른 용도로 사용되더라도 영향을 받지 않도록 했다. AppendObject에서 Flag 다른 값도 같이 기록하려면 이후부터 값에 원하는 값을 OR해야 한다.