PAINT.C
/******************************************************************************\ 
* 
*  MODULE:      PAINT.C 
* 
*  PURPOSE:     This is the module responsible for painting the SPINCUBE 
*               custom control. When Paint() is called we retrieve a 
*               pointer to a SPINCUBEINFO structure, and then use it's 
*               current rotation & translation values to transform the 
*               polyhedron described by gNormalizedVertices & gaiFacets. 
*               Once we've transformed the vertices, we draw the 
*               background, which consists of a grey rectangle and a few 
*               black lines (a crass attempt to render a perspective 
*               view into a "room"), on the offscreen bitmap associated 
*               with the control (i.e. pSCI->hbmCompat). Then we walk the 
*               facet list of the transformed polyhedron (gXformedVertices 
*               & gaiFacets), drawing only those facets whose outward 
*               normal faces us (again, drawing on pSCI->hbmCompat). 
*               Finally, we BitBlt the appropriate rectangle from our 
*               offscreen bitmap to the screen itself. 
* 
*               Drawing to the offscreen bitmap has two advantages over 
*               drawing straight to the screen: 
* 
*                 1. The actual drawing the user sees consists of only 
*                    a single BitBlt. Otherwise, the user would see us 
*                    both erase the polyhedron in it's old position and 
*                    draw it in it's new position (alot of flashing- not 
*                    very smooth animation). 
* 
*                 2. When a spincube control with the SS_ERASE style 
*                    is brought to the foreground, all it's contents 
*                    i.e. the cube trails) are saved & can be re-Blted 
*                    to the screen. Otherwise, all this info would be 
*                    lost & there'd be a big blank spot in the middle 
*                    of the control! 
* 
*               Interested persons should consult a text on 3 dimensional 
*               graphics for more information (i.e. "Computer Graphics: 
*               Principles and Practice", by Foley & van Dam). 
* 
*               Notes: 
* 
*               - A 3x2 tranformation matrix  is used instead of a  3x3 
*                 matrix, since the transformed z-values aren't needed. 
*                 (Normally these would be required for use in depth 
*                 sorting  [for hidden surface removal], but  since we 
*                 draw  only  a single convex polyhedron this  is not 
*                 necessary.) 
* 
*               - A simplified perspective viewing transformation 
*                 (which also  precludes the need for the transformed z 
*                 coordinates). In a nutshell, the perspective  scale 
*                 is as follows: 
* 
*                                    p' = S    x  p 
*                                          per 
* 
*                 where: 
*                        S    = WindowDepth / 
*                         per      (WindowDepth + fCurrentZTranslation) 
* 
*                 (WindowDepth is  the greater of the  control's window 
*                 height or window width.) 
* 
* 
*  FUNCTIONS:   Paint()                         - the paint routine 
*               TransformVertices()             - transforms vertices 
*               ComputeRotationTransformation() - computes xformation 
*                                                 based on current x, y 
*                                                 and z rotation angles 
* 
* 
*                           Microsoft Developer Support 
*                  Copyright 1992 - 1998 Microsoft Corporation 
* 
\******************************************************************************/ 
 
#include <windows.h> 
#include <math.h> 
#include <stdlib.h> 
#include "spincube.h" 
#include "paint.h" 
 
 
 
/******************************************************************************\ 
* 
*  FUNCTION:     Paint 
* 
*  INPUTS:       hwnd - Handle of the window to draw into. 
* 
*  COMMENTS:     Draws window background & a polyhedron in the window. 
* 
\******************************************************************************/ 
 
void Paint (HWND hwnd) 
{ 
  PSPINCUBEINFO  pSCI; 
  RECT           rect; 
  int            i; 
  LONG           lScaleFactor; 
  PAINTSTRUCT    ps; 
  HRGN           hrgnClip; 
  HBRUSH         hBrush, hBrushSave; 
  int            iX, iY, iCX, iCY; 
  int            facetIndex, numPoints; 
  POINT          polygn[MAX_POINTS_PER_FACET]; 
  POINT          vector1, vector2; 
  COLORREF       acrColor[6] = { 0x0000ff, 0x00ff00, 0xff0000, 
                                 0x00ffff, 0xff00ff, 0xffff00 }; 
 
  pSCI = (PSPINCUBEINFO) GetWindowLong (hwnd, GWL_SPINCUBEDATA); 
 
  BeginPaint (hwnd, &ps); 
 
  if (memcmp((void *)&ps.rcPaint, (void *)&pSCI->rcCubeBoundary, sizeof(RECT)) 
      & !REPAINT_BKGND(pSCI)) 
 
  { 
    // 
    // We're not here because it's time to animate (i.e. this paint isn't 
    //   the result of a WM_TIMER), so just do the Blt & blow out of here... 
    // 
 
    BitBlt (ps.hdc, 
            ps.rcPaint.left, 
            ps.rcPaint.top, 
            ps.rcPaint.right - ps.rcPaint.left, 
            ps.rcPaint.bottom - ps.rcPaint.top, 
            pSCI->hdcCompat, ps.rcPaint.left, 
            ps.rcPaint.top, SRCCOPY); 
 
    EndPaint (hwnd, &ps); 
    return; 
  } 
 
 
  // 
  // The rectangle we get back is in Desktop coordinates, so we need to 
  //   modify it to reflect coordinates relative to this window. 
  // 
 
  GetWindowRect (hwnd, &rect); 
 
  rect.right  -= rect.left; 
  rect.bottom -= rect.top; 
  rect.left = rect.top = 0; 
 
 
  // 
  // Determine a "best fit" scale factor for our polyhedron 
  // 
 
  if (!(lScaleFactor = rect.right > rect.bottom ? 
                       rect.bottom/12 : rect.right/12)) 
 
    lScaleFactor = 1; 
 
 
  TransformVertices (hwnd, &rect, pSCI, lScaleFactor); 
 
 
  // 
  // Draw the window frame & background 
  // 
  // Note: The chances are that we are coming through here because we 
  //   got a WM_TIMER message & it's time to redraw the cube to simulate 
  //   animation. In that case all we want to erase/redraw is that small 
  //   rectangle which bounded the polyhedron the last time. The less 
  //   drawing that actually gets done the better, since we wnat to 
  //   minimize the flicker on the screen. __BeginPaint__ is perfect for 
  //   this because it causes all drawing outside of the invalid region 
  //   to be "clipped" (no drawing is performed outside of the invalid 
  //   region), and it also validates the invalid region. 
  // 
 
  if (DO_ERASE(hwnd) || REPAINT_BKGND(pSCI)) 
  { 
    hrgnClip = CreateRectRgnIndirect (&ps.rcPaint); 
    SelectClipRgn (pSCI->hdcCompat, hrgnClip); 
    DeleteObject (hrgnClip); 
 
    SelectObject (pSCI->hdcCompat, GetStockObject (GRAY_BRUSH)); 
    Rectangle    (pSCI->hdcCompat, (int)rect.left, (int)rect.top, 
                  (int)rect.right, (int)rect.bottom); 
 
    iX = (rect.right  - rect.left) / 4; 
    iY = (rect.bottom - rect.top ) / 4; 
 
    MoveToEx (pSCI->hdcCompat, (int)rect.left, (int)rect.top, NULL); 
    LineTo   (pSCI->hdcCompat, (int)rect.left + iX, (int)rect.top + iY); 
    LineTo   (pSCI->hdcCompat, (int)rect.left + iX, (int)rect.bottom - iY); 
    LineTo   (pSCI->hdcCompat, (int)rect.left,      (int)rect.bottom); 
 
    MoveToEx (pSCI->hdcCompat, (int)rect.right, (int)rect.top, NULL); 
    LineTo   (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.top + iY); 
    LineTo   (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.bottom- iY); 
    LineTo   (pSCI->hdcCompat, (int)rect.right,      (int)rect.bottom); 
 
    MoveToEx (pSCI->hdcCompat, (int)rect.left + iX,  (int)rect.top + iY, NULL); 
    LineTo   (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.top + iY); 
 
    MoveToEx (pSCI->hdcCompat, (int)rect.left + iX,  (int)rect.bottom - iY, NULL); 
    LineTo   (pSCI->hdcCompat, (int)rect.right - iX, (int)rect.bottom - iY); 
 
    SelectClipRgn (pSCI->hdcCompat, NULL); 
 
    pSCI->iOptions &= ~SPINCUBE_REPAINT_BKGND; 
  } 
 
 
  // 
  // Draw the polyhedron. We'll walk through the facets list and compute 
  //   the normal for each facet- if the normal has z > 0, then the facet 
  //   faces us and we'll draw it. Note that this algorithim is ONLY valid 
  //   for scenes with a single, convex polyhedron. 
  // 
  // Note: Use GetDC here because the above call to BeginPaint will 
  //   probably not give us a DC with access to as much real estate as 
  //   we'd like (we wouldn't be able to draw outside of the invalid 
  //   region). We can party on the entire control window with the DC 
  //   returned by GetDC. 
  // 
 
  for (i = 0, facetIndex = 0; i < NUMFACETS; i++) 
  { 
    vector1.x = gXformedVertices[gaiFacets[facetIndex + 1]].x - 
                gXformedVertices[gaiFacets[facetIndex]].x; 
    vector1.y = gXformedVertices[gaiFacets[facetIndex + 1]].y - 
                gXformedVertices[gaiFacets[facetIndex]].y; 
    vector2.x = gXformedVertices[gaiFacets[facetIndex + 2]].x - 
                gXformedVertices[gaiFacets[facetIndex + 1]].x; 
    vector2.y = gXformedVertices[gaiFacets[facetIndex + 2]].y - 
                gXformedVertices[gaiFacets[facetIndex + 1]].y; 
 
    for (numPoints = 0; gaiFacets[facetIndex] != -1; numPoints++, facetIndex++) 
    { 
      polygn[numPoints].x = gXformedVertices[gaiFacets[facetIndex]].x; 
      polygn[numPoints].y = gXformedVertices[gaiFacets[facetIndex]].y; 
    } 
 
    facetIndex++; /* skip over the -1's in the facets list */ 
    if ((vector1.x*vector2.y - vector1.y*vector2.x) > 0) 
    { 
      hBrush     = CreateSolidBrush (acrColor[i]); 
      hBrushSave = (HBRUSH) SelectObject (pSCI->hdcCompat, hBrush); 
 
      Polygon (pSCI->hdcCompat, &polygn[0], numPoints); 
 
      SelectObject (pSCI->hdcCompat, hBrushSave); 
      DeleteObject (hBrush); 
    } 
  } 
 
  iX  = pSCI->rcCubeBoundary.left < ps.rcPaint.left ? 
        pSCI->rcCubeBoundary.left : ps.rcPaint.left; 
  iY  = pSCI->rcCubeBoundary.top  < ps.rcPaint.top  ? 
        pSCI->rcCubeBoundary.top  : ps.rcPaint.top; 
 
  iCX = (pSCI->rcCubeBoundary.right > ps.rcPaint.right ? 
         pSCI->rcCubeBoundary.right : ps.rcPaint.right) - iX; 
 
  iCY = (pSCI->rcCubeBoundary.bottom > ps.rcPaint.bottom ? 
         pSCI->rcCubeBoundary.bottom : ps.rcPaint.bottom) - iY; 
 
  EndPaint (hwnd, &ps); 
 
  ps.hdc = GetDC (hwnd); 
 
  BitBlt (ps.hdc, iX, iY, iCX, iCY, pSCI->hdcCompat, iX, iY, SRCCOPY); 
 
  ReleaseDC (hwnd, ps.hdc); 
 
} 
 
 
 
/******************************************************************************\ 
* 
*  FUNCTION:     TransformVertices 
* 
*  INPUTS:       hwnd         - control window handle 
*                pWindowRect  - pointer to RECT describing control's dimensions 
*                pSCI         - pointer to control's SPINCUBEINFO structure 
*                fScaleFactor - scale factor for use in this window 
* 
\******************************************************************************/ 
 
void TransformVertices (HWND hwnd,          RECT  *pWindowRect, 
                        PSPINCUBEINFO pSCI, LONG  lScaleFactor) 
{ 
  int    i; 
  int    iWindowDepth = pWindowRect->right > pWindowRect->bottom ? 
                        pWindowRect->right : pWindowRect->bottom; 
  RECT   WindowRect; 
  float  fDepthScale; 
  int    iNewTranslationInc = (rand() % 10) + 2; 
  float  fNewRotationInc    = (float) 0.01 * ((rand() % 30) + 2); 
 
  WindowRect.left = - (WindowRect.right  = pWindowRect->right  >> 1); 
  WindowRect.top  = - (WindowRect.bottom = pWindowRect->bottom >> 1); 
 
  // 
  // Initiailize the bounding rectangle with max/min vals 
  // 
 
  pSCI->rcCubeBoundary.left   = 
  pSCI->rcCubeBoundary.top    = 100000; // big positive value 
  pSCI->rcCubeBoundary.right  = 
  pSCI->rcCubeBoundary.bottom = -100000; // small negative value 
 
 
  // 
  // First scale, then rotate, then translate each vertex. 
  //   Keep track of the maximum & minimum values bounding the 
  //   vertices in the x,y plane for use later in bounds checking. 
  // 
  // Note: we don't bother computing z values after the scale, 
  //   as they are only really necessary for the rotation. If we 
  //   were doing real bounds checking we'd need it, but this code 
  //   simply uses the pSCI->iCurrentZTranslation to determine 
  //   the z-boundaries. 
  // 
 
  for (i = 0; i < NUMVERTICES; i++) 
  { 
    LONG tempX; 
 
    // 
    // Copy the static vertices into a temp array 
    // 
 
    gXformedVertices[i] = gNormalizedVertices[i]; 
 
    // 
    // The scale... 
    // 
 
    gXformedVertices[i].x *= lScaleFactor; 
    gXformedVertices[i].y *= lScaleFactor; 
    gXformedVertices[i].z *= lScaleFactor; 
 
    // 
    // The rotation... 
    // 
 
    ComputeRotationTransformation (pSCI->fCurrentXRotation, 
                                   pSCI->fCurrentYRotation, 
                                   pSCI->fCurrentZRotation); 
 
    tempX   =               (LONG) (gM[0][0] * gXformedVertices[i].x + 
                                    gM[0][1] * gXformedVertices[i].y + 
                                    gM[0][2] * gXformedVertices[i].z); 
 
    gXformedVertices[i].y = (LONG) (gM[1][0] * gXformedVertices[i].x + 
                                    gM[1][1] * gXformedVertices[i].y + 
                                    gM[1][2] * gXformedVertices[i].z); 
    gXformedVertices[i].x = tempX; 
 
    // 
    // The translation... 
    // 
 
    gXformedVertices[i].x += pSCI->iCurrentXTranslation; 
    gXformedVertices[i].y += pSCI->iCurrentYTranslation; 
 
    // 
    // Check if we have new max or min vals 
    // 
 
    if (pSCI->rcCubeBoundary.left > gXformedVertices[i].x) 
 
      pSCI->rcCubeBoundary.left = gXformedVertices[i].x; 
 
    if (pSCI->rcCubeBoundary.right < gXformedVertices[i].x) 
 
      pSCI->rcCubeBoundary.right = gXformedVertices[i].x; 
 
    if (pSCI->rcCubeBoundary.top > gXformedVertices[i].y) 
 
      pSCI->rcCubeBoundary.top = gXformedVertices[i].y; 
 
    if (pSCI->rcCubeBoundary.bottom < gXformedVertices[i].y) 
 
      pSCI->rcCubeBoundary.bottom = gXformedVertices[i].y; 
  } 
 
 
  // 
  // Now for some bounds checking, change translation & rotation increments 
  //   if we hit a "wall". Also so the gbHitBoundary flag so we remember 
  //   to flash the cube when we draw it. 
  // 
 
  if (pSCI->rcCubeBoundary.left < WindowRect.left) 
  { 
    pSCI->iCurrentXTranslationInc = iNewTranslationInc; 
    pSCI->fCurrentZRotationInc    = fNewRotationInc; 
  } 
 
  else if (pSCI->rcCubeBoundary.right > WindowRect.right) 
  { 
    pSCI->iCurrentXTranslationInc = -iNewTranslationInc; 
    pSCI->fCurrentZRotationInc    = -fNewRotationInc; 
  } 
 
  if (pSCI->rcCubeBoundary.top < WindowRect.top) 
   { 
    pSCI->iCurrentYTranslationInc = iNewTranslationInc; 
    pSCI->fCurrentXRotationInc    = fNewRotationInc; 
  } 
 
  else if (pSCI->rcCubeBoundary.bottom > WindowRect.bottom) 
  { 
    pSCI->iCurrentYTranslationInc = -iNewTranslationInc; 
    pSCI->fCurrentXRotationInc    = -fNewRotationInc; 
  } 
 
  if (pSCI->iCurrentZTranslation < (int) lScaleFactor<<1) 
  { 
    pSCI->iCurrentZTranslationInc = iNewTranslationInc; 
    pSCI->fCurrentYRotationInc    = fNewRotationInc; 
  } 
 
  else if (pSCI->iCurrentZTranslation > (iWindowDepth - (int) lScaleFactor)) 
  { 
    pSCI->iCurrentZTranslationInc = -iNewTranslationInc; 
    pSCI->fCurrentYRotationInc    = -fNewRotationInc; 
  } 
 
 
  // 
  // Now a kludgy scale based on depth (iCurrentZTranslation) of the center 
  //   point of the polyhedron 
  // 
 
  fDepthScale =  ((float) iWindowDepth) / 
                 ((float) (iWindowDepth + pSCI->iCurrentZTranslation)); 
 
  pSCI->rcCubeBoundary.left  = (LONG)(fDepthScale* pSCI->rcCubeBoundary.left  ); 
  pSCI->rcCubeBoundary.right = (LONG)(fDepthScale* pSCI->rcCubeBoundary.right ); 
  pSCI->rcCubeBoundary.top   = (LONG)(fDepthScale* pSCI->rcCubeBoundary.top   ); 
  pSCI->rcCubeBoundary.bottom= (LONG)(fDepthScale* pSCI->rcCubeBoundary.bottom); 
 
  for (i = 0; i < NUMVERTICES; i++) 
  { 
    gXformedVertices[i].x = (LONG) (fDepthScale * gXformedVertices[i].x); 
    gXformedVertices[i].y = (LONG) (fDepthScale * gXformedVertices[i].y); 
  } 
 
 
  // 
  // If currently in motion then increment the current rotation & tranlation 
  // 
 
  if (IN_MOTION(hwnd)) 
  { 
    pSCI->fCurrentXRotation += pSCI->fCurrentXRotationInc; 
    pSCI->fCurrentYRotation += pSCI->fCurrentYRotationInc; 
    pSCI->fCurrentZRotation += pSCI->fCurrentZRotationInc; 
 
    pSCI->iCurrentXTranslation += pSCI->iCurrentXTranslationInc; 
    pSCI->iCurrentYTranslation += pSCI->iCurrentYTranslationInc; 
    pSCI->iCurrentZTranslation += pSCI->iCurrentZTranslationInc; 
  } 
 
 
  // 
  // Up to this point all coordinates are relative to a window whose 
  //   center is at (0,0). Now we'll translate appropriately... 
  // 
 
  pSCI->rcCubeBoundary.left   += pWindowRect->right  >> 1; 
  pSCI->rcCubeBoundary.right  += pWindowRect->right  >> 1; 
  pSCI->rcCubeBoundary.top    += pWindowRect->bottom >> 1; 
  pSCI->rcCubeBoundary.bottom += pWindowRect->bottom >> 1; 
 
  for (i = 0; i < NUMVERTICES; i++) 
  { 
    gXformedVertices[i].x += pWindowRect->right  >> 1; 
    gXformedVertices[i].y += pWindowRect->bottom >> 1; 
  } 
 
 
  // 
  // Since FillRect's are inclusive-exclusive (there'll be leftovers 
  //   from the last cube we drew otherwise)... 
  // 
 
  pSCI->rcCubeBoundary.right++; 
  pSCI->rcCubeBoundary.bottom++; 
 
 
  // 
  // Finally, adjust the rcCubeBoundary such that it fits entirely within 
  //   the acutal control window. The reason for this is that when calling 
  //   InvalidateRect from SpincubeWndProc\case_WM_TIMER we may get 
  //   a different PAINSTRUCT.rcPaint (since InvalidateRect clips the passed 
  //   in rect to the window bounds) and our abouve test to memcmp() will 
  //   fail. 
  // 
 
  if (pSCI->rcCubeBoundary.left < 0) 
 
    pSCI->rcCubeBoundary.left = 0; 
 
  if (pSCI->rcCubeBoundary.top < 0) 
 
    pSCI->rcCubeBoundary.top = 0; 
 
  if (pSCI->rcCubeBoundary.right > pWindowRect->right) 
 
    pSCI->rcCubeBoundary.right = pWindowRect->right; 
 
  if (pSCI->rcCubeBoundary.bottom > pWindowRect->bottom) 
 
    pSCI->rcCubeBoundary.bottom = pWindowRect->bottom; 
} 
 
 
/******************************************************************************\ 
* 
*  FUNCTION:    ComputeRotationTransformation 
* 
*  INPUTS:      fRotationX - Angle to rotate about X axis. 
*               fRotationY - Angle to rotate about Y axis. 
*               fRotationZ - Angle to rotate about Z axis. 
* 
*  COMMENTS:    Computes a 3x2 tranformation matrix which rotates about 
*               the Z axis, the Y axis, and the X axis, respectively. 
* 
\******************************************************************************/ 
 
void ComputeRotationTransformation (float fRotationX, 
                                    float fRotationY, 
                                    float fRotationZ) 
{ 
  float sinX, cosX, sinY, cosY, sinZ, cosZ; 
 
  sinX = (float) sin ((double) fRotationX); 
  cosX = (float) cos ((double) fRotationX); 
  sinY = (float) sin ((double) fRotationY); 
  cosY = (float) cos ((double) fRotationY); 
  sinZ = (float) sin ((double) fRotationZ); 
  cosZ = (float) cos ((double) fRotationZ); 
 
  gM[0][0] =  cosY*cosZ; 
  gM[0][1] = -cosY*sinZ; 
  gM[0][2] =  sinY; 
  gM[1][0] =  sinX*sinY*cosZ + cosX*sinZ; 
  gM[1][1] = -sinX*sinY*sinZ + cosX*cosZ; 
  gM[1][2] = -sinX*cosY; 
}