/******************************Module*Header*******************************\ * Module Name: pathwide.cxx * * This module generates the outlines for geometric wide-lines. Given * a path for a line, it generates a new path that, when filled using * a winding mode fill, yields the resulting widened line. * * Geometric wide-lines may have different end-cap and line-join * attributes. They may also be styled. * * THINGS TO NOTE * * The path is already in device coordinates, so the resulting wide-line * is also calculated in device coordinates. * * The key thing to note is that the pen's shape is defined in world space, so * its shape in device space depends on the current world-to-device * transform (which may be an arbitrary 2 by 2 affine transform). Pens * are circular in world space, with their diameter there defined by the * pen width. However, in device space a pen shape is only guaranteed to be * an ellipse symmetrical about some arbitrary axis. And vectors that are * perpendicular in world space are not necessarily perpendicular in device * space (which is important for calculating flat and square end-caps, and * bevel joins). * * HOW WE DO IT * * We implement wide-lines by first generating in device space a convex * polygonal approximation to the pen's shape. We 'drag' this polygon * along the given path; the outline of a widened line is generated by * the point on the pen polygon that is furthest from the line's ray * (called the 'draw' vertex). This point may be found by a simple * binary search using entirely integer arithmetic. * * This approach makes round round joins and round caps very efficient: * the points for the resulting round part is simply the points between * draw vertices. * * To compute true perpendiculars (for flat and square end-caps, and * bevel joins) we interpolate between vertices on the pen polygon * (once again using only integer math). * * The lengths of the dashes and gaps for styled lines are defined in * world space; to do styling we computed every line segment's length * in world space, and divide it accordingly. * * HOBBY WIDE-LINES * * Rendering the wide-lines using a polygonized pen allows us to take * advantage of Hobby wide-lines. Hobby's wide-line algorithm * generates 'optimally shaped' pens that will yield lines of constant- * looking width, independent of the direction of the lines. (OS/2's * PM suffers from having its thin wide-lines appear not to be of * constant width). * * Unfortunately, Hobby's algorithm is fairly expensive to generate an * optimal pen shape polygon given its width and the current transform; * it is significantly faster for us to generate a quick-and-dirty * polygon approximation by creating the ellipse out of Bezier curves and * flattening it. * * But Hobby pens are most important for small, near circular pens, * where the eye can readily detect imperfections in the line. So we * support Hobby wide-lines only for small, near circular pens, and we * keep these pen shapes in a pre-computed table. * * See: * * John Hobby, "Digitized Brush Trajectories", PhD Dissertation, * Stanford University, August 1985 * * GENERATING BOTH SIDES AT THE SAME TIME * * We also generate both sides of the wide-line outline as we traverse * the path, keeping track of both sides in separate paths (the outlines * of the 'left' and 'right' sides of the wide-line). When the end of a * figure (that is, the end of a sub-path) is reached, we reverse all the * points in the 'left' path and append it to the 'right', and close the * result. * * This could have been implemented by traversing the path once to generate * one side, then traversing it again in the reverse direction to generate * the other side. However, most of the hard computations for perpendiculars * and such would have to be done twice. Our method is faster, although * there is more book-keeping. * * BEZIERS * * We really only know how to widen lines, so we convert Beziers as we * read them to their polyline approximations. We 'flatten' Beziers to * constant error, independent of pen width, and make sure we apply 'round' * joins to the interior polylines comprising the Bezier. The round join * handles extreme cases such as cusps, so there really is no need to * flatten to less error for larger pens. * * (This is in contrast to OS/2's PM which pre-flattens its curves prior * to widening, and applies the regular joins to the interior of the curve. * For wider pens, the allowed error in the flattening must be decreased * because sharp curves would not look 'round' at cusp-points when using * bevel or miter joins.) * * We flatten Bezier curves as we traverse path (instead of only widening * paths that are already flattened) because it is useful to know the * Bezier data. We use the control points to calculate true perpendiculars * at the end-points of curves, and we have to know when we're in the * interior of the Bezier to always apply round joins. * * Advantages: o We're much faster * * o Produce better looking curves * * Disadvantages: o More book-keeping * * o Have to be careful when mixing perpendiculars (such * as from miter joins or flat caps) with round joins * * o Perpendiculars on styled curves may not be as accurate * for wide-lines -- we use the approximating lines to * calculate the perp, and more lines would give better * perps. * * However, it can also be argued that due to the * snapping effect of the device-space grid, approximating * the curve into more small lines would actually make * the perps _less_ accurate. * * LAZY EVALUATION * * Computing draw vectors and perpendiculars are very expensive operations, * so we lazily evaluate them -- we compute them only when we need them. * With styling, it may happen we completely skip over some lines (so we * never do any evaluation), or that we break a line into many small pieces, * and so re-use perps and draw-vectors (saving us time and ensuring that * style ends are always perpendicular for styled segments). * * Created: 23-Sep-1991 * Author: J. Andrew Goossen [andrewgo] * * Copyright (c) 1991-1999 Microsoft Corporation * \**************************************************************************/ #include "precomp.hxx" #include "flhack.hxx" #include "pathwide.hxx" #ifdef DEBUG_WIDE #define INLINE #else #define INLINE inline #endif #ifdef DEBUG_WIDE LONG lConv(EFLOAT ef); #endif POINTFIX WIDEPENOBJ::aptfxHobby1[] = { {0, 8}, {8, 0}, {0, -8}, {-8, 0} }; POINTFIX WIDEPENOBJ::aptfxHobby2[] = { {8, 16}, {16, 0}, {8, -16}, {-8, -16}, {-16, 0} }; POINTFIX WIDEPENOBJ::aptfxHobby3[] = { {24, 8}, {24, -8}, {8, -24}, {-8, -24}, {-24, -8}, {-24, 8} }; POINTFIX WIDEPENOBJ::aptfxHobby4[] = { {32, 8}, {32, -8}, {24, -24}, {8, -32}, {-8, -32}, {-24, -24}, {-32, -8}, {-32, 8} }; POINTFIX WIDEPENOBJ::aptfxHobby5[] = { {40, 8}, {40, -8}, {32, -24}, {24, -32}, {8, -40}, {-8, -40}, {-24, -32}, {-32, -24}, {-40, -8}, {-40, 8} }; POINTFIX WIDEPENOBJ::aptfxHobby6[] = { {48, 8}, {48, -8}, {40, -24}, {24, -40}, {8, -48}, {-8, -48}, {-24, -40}, {-40, -24}, {-48, -8}, {-48, 8} }; HOBBY WIDEPENOBJ::ahob[HOBBY_TABLE_SIZE] = { { aptfxHobby1, sizeof(aptfxHobby1) / sizeof(POINTFIX) }, { aptfxHobby2, sizeof(aptfxHobby2) / sizeof(POINTFIX) }, { aptfxHobby3, sizeof(aptfxHobby3) / sizeof(POINTFIX) }, { aptfxHobby4, sizeof(aptfxHobby4) / sizeof(POINTFIX) }, { aptfxHobby5, sizeof(aptfxHobby5) / sizeof(POINTFIX) }, { aptfxHobby6, sizeof(aptfxHobby6) / sizeof(POINTFIX) } }; /******************************Public*Routine******************************\ * bTurnLeftRandom(pa, pb) * * Returns TRUE if the sign of the cross product of a X b is positive. * For simple minds like mine, it returns: * * TRUE if b 'turns left' from a (cross product is < 0) * FALSE if b is parallel to or 'turns right' from a (cross is >= 0) * * When the vectors are random, half the time we take a short-cut by * checking the signs. Remember that we're in device space, where * positive 'y' is down! * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE BOOL bTurnLeftRandom( PEVECTORFX pa, PEVECTORFX pb) { BOOL bRet; // If the signs are correct, we don't even have to multiply: if ((pa->x ^ pa->y ^ pb->x ^ pb->y) < 0) bRet = ((pa->x ^ pb->y) < 0); else { LONGLONG ad; LONGLONG bc; // Check sign of (pa->x * pb->y - pa->y * pb->x) = (ad - bc) ad = Int32x32To64(pa->x, pb->y); bc = Int32x32To64(pa->y, pb->x); bRet = (ad < bc); } return(bRet); } /******************************Public*Routine******************************\ * WIDEPATHOBJ::bGrowPath() * * Creates a new path record. Adds to the end of the path, and sets * 'pptfxPathRecCurrent' and 'pptfxPathRecEnd'. * * History: * 1-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Stole it from PaulB's pprFlattenRec. \**************************************************************************/ BOOL WIDEPATHOBJ::bGrowPath() { PPATHALLOC ppa = ppath->ppachain; COUNT cMax = 0; if (ppa != (PPATHALLOC) NULL) { // We have a current pathalloc, see how much will fit // computation done into temps to avoid compiler assertion! PPOINTFIX pptfxStart = &(ppa->pprfreestart->aptfx[0]); PPOINTFIX pptfxEnd = (PPOINTFIX) ((char *)ppa + ppa->siztPathAlloc); if (pptfxEnd > pptfxStart) { //Sundown truncation ASSERT4GB((ULONGLONG)(pptfxEnd - pptfxStart)); cMax = (COUNT)(pptfxEnd - pptfxStart); } } // Now we can decide if we need a new pathalloc if (cMax < PATHALLOCTHRESHOLD) { // allocate a new pathalloc, link it into path if ( (ppa = newpathalloc()) == (PPATHALLOC) NULL) return(FALSE); ppa->ppanext = ppath->ppachain; ppath->ppachain = ppa; // adjust maxadd // Sundown truncation ASSERT4GB((ULONGLONG)(((char *)ppa + ppa->siztPathAlloc) - (char *)ppa->pprfreestart)); ULONGSIZE_T numbytes = (ULONG)(((char *)ppa + ppa->siztPathAlloc) - (char *)ppa->pprfreestart); cMax = (numbytes - offsetof(PATHRECORD, aptfx))/sizeof(POINTFIX); } // Add pathrecord to end of path PPATHREC pprNew = ppa->pprfreestart; if (ppath->pprlast == (PPATHREC) NULL) { ppath->pprfirst = pprNew; pprNew->pprprev = (PPATHREC) NULL; } else { ppath->pprlast->pprnext = pprNew; pprNew->pprprev = ppath->pprlast; } ppath->pprlast = pprNew; pprNew->pprnext = (PPATHREC) NULL; pprNew->count = 0; // pptfxPathRecCurrent points to first available spot. pptfxPathRecEnd // points to first spot *after* the last available spot. pptfxPathRecCurrent = &pprNew->aptfx[0]; pptfxPathRecEnd = &pprNew->aptfx[cMax]; return(TRUE); } /******************************Public*Routine******************************\ * WIDEPATHOBJ::vGrowPathAndAddPoint(pptfx) * * Adds to the current path by creating a new record and adding the * specified point. If this fails, the path is marked as being out-of- * memory. * * History: * 1-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDEPATHOBJ::vGrowPathAndAddPoint( PPOINTFIX pptfx, PEVECTORFX pvec, BOOL bInvert) { ASSERT4GB((LONGLONG)(pptfxPathRecCurrent - ppath->pprlast->aptfx)); COUNT cpt = (COUNT)(pptfxPathRecCurrent - ppath->pprlast->aptfx); ppath->pprlast->count = cpt; ppath->ppachain->pprfreestart = NEXTPATHREC(ppath->pprlast); if (bValid()) { if (!bGrowPath()) vSetError(); else { ppath->pprlast->flags = 0; *pptfxPathRecCurrent = *pptfx; if (pvec != (PEVECTORFX) NULL) { if (bInvert) { pptfxPathRecCurrent->x -= pvec->x; pptfxPathRecCurrent->y -= pvec->y; } else { pptfxPathRecCurrent->x += pvec->x; pptfxPathRecCurrent->y += pvec->y; } } pptfxPathRecCurrent++; } } } /******************************Public*Routine******************************\ * WIDEPATHOBJ::bBeginFigure() * * Creates the start of a new figure (sub-path) in the path. Must be * called before vAddPoint(). * * History: * 1-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL WIDEPATHOBJ::bBeginFigure() { if (bValid()) { #ifdef DEBUG_WIDE ASSERTGDI(!bOpenPath, "BeginFigure on already open path!"); bOpenPath = TRUE; #endif if (bGrowPath()) { ppath->pprlast->flags = PD_BEGINSUBPATH; return(TRUE); } vSetError(); } return(FALSE); } /******************************Public*Routine******************************\ * WIDEPATHOBJ::vEndFigure() * * Ends the open figure. Must be called after all vAddPoint()'s are done * for the figure. * * History: * 1-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDEPATHOBJ::vEndFigure() { #ifdef DEBUG_WIDE ASSERTGDI(bOpenPath, "vEndFigure on unopen path!"); bOpenPath = FALSE; #endif //Sundown truncation ASSERT4GB((ULONGLONG)(pptfxPathRecCurrent - ppath->pprlast->aptfx)); COUNT cpt = (COUNT)(pptfxPathRecCurrent - ppath->pprlast->aptfx); ppath->pprlast->flags |= PD_ENDSUBPATH; ppath->pprlast->count = cpt; // Adjust the pathalloc record: ppath->ppachain->pprfreestart = NEXTPATHREC(ppath->pprlast); } /******************************Public*Routine******************************\ * WIDEPATHOBJ::vPrependBeforeFigure() * * Moves the last figure in path and prepends to the first figure. * * History: * 24-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID WIDEPATHOBJ::vPrependBeforeFigure() { #ifdef DEBUG_WIDE ASSERTGDI(!bOpenPath, "vPrependBeforeFigure on open path!"); #endif PPATHREC pprSrcStart; PPATHREC pprTargStart; // Find the start of the last figure: pprSrcStart = ppath->pprlast; while (!(pprSrcStart->flags & PD_BEGINSUBPATH)) { pprSrcStart = pprSrcStart->pprprev; ASSERTGDI(pprSrcStart != (PPATHREC) NULL, "Couldn't find start"); } // Find the start of the 2nd last figure: pprTargStart = pprFigureStart; ASSERTGDI(pprTargStart != (PPATHREC) NULL && (pprTargStart->flags & PD_BEGINSUBPATH), "Funky pprFigureStart"); PPATHREC pprTargEnd = pprSrcStart->pprprev; PPATHREC pprSrcEnd = ppath->pprlast; // 'targ' is currently before 'src', and 'src' is the last figure in the // path. We'll move 'src' to be before 'targ': ppath->pprlast = pprTargEnd; // Hmmm, seems to me we're guaranteed to have pprprev NULL here because // we're looking at the start of the path: if (pprTargStart->pprprev == (PPATHREC) NULL) ppath->pprfirst = pprSrcStart; else pprTargStart->pprprev->pprnext = pprSrcStart; pprSrcStart->pprprev = pprTargStart->pprprev; pprSrcEnd->pprnext = pprTargStart; pprTargStart->pprprev = pprSrcEnd; pprTargEnd->pprnext = (PPATHREC) NULL; // We're prepending this to the start of the first figure, so clean up // the flags: pprTargStart->flags &= ~PD_BEGINSUBPATH; pprSrcEnd->flags &= ~(PD_ENDSUBPATH | PD_CLOSEFIGURE); } /******************************Public*Routine******************************\ * WIDEPATHOBJ::vPrependBeforeSubpath() * * Moves the last figure in path and prepends it to the previous subpath. * * History: * 24-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID WIDEPATHOBJ::vPrependBeforeSubpath() { #ifdef DEBUG_WIDE ASSERTGDI(!bOpenPath, "vPrependBeforeSubpath on open path!"); #endif PPATHREC pprSrcStart; PPATHREC pprTargStart; // Find the start of the last figure: pprSrcStart = ppath->pprlast; while (!(pprSrcStart->flags & PD_BEGINSUBPATH)) { pprSrcStart = pprSrcStart->pprprev; ASSERTGDI(pprSrcStart != (PPATHREC) NULL, "Couldn't find start"); } // Find the start of the 2nd last figure: ASSERTGDI(pprSrcStart->pprprev != (PPATHREC) NULL, "Was no previous figure"); pprTargStart = pprSrcStart->pprprev; while (!(pprTargStart->flags & PD_BEGINSUBPATH)) { pprTargStart = pprTargStart->pprprev; ASSERTGDI(pprTargStart != (PPATHREC) NULL, "Couldn't find previous figure"); } PPATHREC pprTargEnd = pprSrcStart->pprprev; PPATHREC pprSrcEnd = ppath->pprlast; // 'targ' is currently before 'src', and 'src' is the last figure in the // path. We'll move 'src' to be before 'targ': ppath->pprlast = pprTargEnd; if (pprTargStart->pprprev == (PPATHREC) NULL) ppath->pprfirst = pprSrcStart; else pprTargStart->pprprev->pprnext = pprSrcStart; pprSrcStart->pprprev = pprTargStart->pprprev; pprSrcEnd->pprnext = pprTargStart; pprTargStart->pprprev = pprSrcEnd; pprTargEnd->pprnext = (PPATHREC) NULL; // We're prepending this to the start of the first figure, so clean up // the flags: pprTargStart->flags &= ~PD_BEGINSUBPATH; pprSrcEnd->flags &= ~(PD_ENDSUBPATH | PD_CLOSEFIGURE); } /******************************Public*Routine******************************\ * WIDEPATHOBJ::vReverseConcatenate(wpo) * * Reverses 'wpo' and adds all points to the end of this path. * bBeginFigure() must have been called previously for this path, and * 'wpo' must be ended by calling vEndFigure(). * * The path records of 'wpo' are freed as they are copied (in order to * decrease the total number of path records needed by both paths). As such, * 'wpo' must be entirely one figure! * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDEPATHOBJ::vReverseConcatenate(WIDEPATHOBJ& wpo) { #ifdef DEBUG_WIDE ASSERTGDI(bOpenPath, "vReverseConcatenate on closed target!"); ASSERTGDI(!wpo.bOpenPath, "vReverseConcatenate on open source!"); #endif ASSERTGDI(bValid(), "Reverse invalid path"); ASSERTGDI(wpo.ppath->pprfirst != (PPATHREC) NULL, "NULL path 1"); ASSERTGDI(wpo.ppath->ppachain != (PPATHALLOC) NULL, "NULL path 2"); ASSERTGDI(ppath->pprfirst != (PPATHREC) NULL, "NULL path 3"); ASSERTGDI(ppath->ppachain != (PPATHALLOC) NULL, "NULL path 4"); // Reverse the path: PPATHREC ppr = wpo.ppath->pprlast; while (ppr != (PPATHREC) NULL) { PPATHREC pprPrev = ppr->pprprev; PPOINTFIX pptfxStart; PPOINTFIX pptfxEnd; // Copy all the points in reverse order: pptfxStart = &ppr->aptfx[0]; pptfxEnd = &ppr->aptfx[ppr->count]; while (pptfxEnd > pptfxStart) vAddPoint(--pptfxEnd); PPATHALLOC ppaEnd = wpo.ppath->ppachain; ASSERTGDI(ppaEnd != (PPATHALLOC) NULL, "Null pathalloc"); wpo.ppath->ppachain = ppaEnd->ppanext; ASSERTGDI(((PBYTE) pptfxStart > (PBYTE) ppaEnd) && ((PBYTE) pptfxStart - (PBYTE) ppaEnd) < (LONG) ppaEnd->siztPathAlloc, "Last pathrec doesn't correspond to last pathalloc"); freepathalloc(ppaEnd); ppr = pprPrev; } ASSERTGDI(wpo.ppath->ppachain == (PPATHALLOC) NULL, "Didn't delete entire chain"); // Clean up old path: wpo.ppath->ppachain = (PPATHALLOC) NULL; wpo.ppath->pprlast = (PPATHREC) NULL; wpo.ppath->pprfirst = (PPATHREC) NULL; } /******************************Public*Routine******************************\ * vHalve(vec) * * Halves the vector, and always rounds 1/32 up. Symmetric about zero. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID vHalve(EVECTORFX& vec) { if (vec.x >= 0) vec.x++; if (vec.y >= 0) vec.y++; vec.x >>= 1; vec.y >>= 1; } /******************************Public*Routine******************************\ * vVecSymmetricRound(pvec) * * Always rounds 1/2 up. Symmetric about zero. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID vVecSymmetricRound(PEVECTORFX pvec) { // Round 1/2 up: if (pvec->x >= 0) pvec->x = (pvec->x + 4) & ~7L; else pvec->x = (pvec->x + 3) & ~7L; if (pvec->y >= 0) pvec->y = (pvec->y + 4) & ~7L; else pvec->y = (pvec->y + 3) & ~7L; } /******************************Public*Routine******************************\ * vVecRound(pvec) * * Always rounds 1/2 down for 'x'. Rounds 1/2 up for 'y'. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID vVecRound(PEVECTORFX pvec) { // Round 1/2 down: if (pvec->x >= 0) pvec->x = (pvec->x + 3) & ~7L; else pvec->x = (pvec->x + 4) & ~7L; // Round 1/2 up: if (pvec->y >= 0) pvec->y = (pvec->y + 4) & ~7L; else pvec->y = (pvec->y + 3) & ~7L; } /******************************Public*Routine******************************\ * WIDEPENOBJ::bHobbyize(avecAxis[]) * * Will copy a pre-computed Hobby pen polygon if the pen is small and * circular. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL WIDEPENOBJ::bHobbyize(EVECTORFX avecAxis[]) { ASSERTGDI(((avecAxis[0].x == avecAxis[1].y) && (avecAxis[0].y == -avecAxis[1].x)) || ((avecAxis[0].x == -avecAxis[1].y) && (avecAxis[0].y == avecAxis[1].x)), "Must be orthogonal and circular"); ASSERTGDI((MAX(ABS(avecAxis[0].x), ABS(avecAxis[0].y)) < LPLUSHALFTOFX(HOBBY_TABLE_SIZE)), "Must be Hobby sized"); // We now know that the transform was orthogonal, and that the pen is // circular in device space. // We now know that the vector coordinates have less than 15 bits // signficance, so we can safely compute its Euclidean length in // 32 bits: FIX fxPenWidthSquared = avecAxis[0].x * avecAxis[0].x + avecAxis[0].y * avecAxis[0].y; if (fxPenWidthSquared < SQUARE(LPLUSHALFTOFX(HOBBY_TABLE_SIZE))) { // Pen is small enough that it will be in our Hobby tables: LONG iHobbyPen; if (fxPenWidthSquared < SQUARE(LPLUSHALFTOFX(1))) iHobbyPen = 0; // Width less than 1.5 else if (fxPenWidthSquared < SQUARE(LPLUSHALFTOFX(2))) iHobbyPen = 1; // Width less than 2.5 else if (fxPenWidthSquared < SQUARE(LPLUSHALFTOFX(3))) iHobbyPen = 2; // Width less than 3.5 else if (fxPenWidthSquared < SQUARE(LPLUSHALFTOFX(4))) iHobbyPen = 3; // Width less than 4.5 else if (fxPenWidthSquared < SQUARE(LPLUSHALFTOFX(5))) iHobbyPen = 4; // Width less than 5.5 else { #if (HOBBY_TABLE_SIZE != 6L) #error "Table size changed, update routine" #endif iHobbyPen = 5; // Width less than 6.5 } // Copy the Hobby pen from the table to our path: if (!bBeginFigure()) return(FALSE); PPOINTFIX pptfx = ahob[iHobbyPen].pptfx; PPOINTFIX pptfxEnd = pptfx + ahob[iHobbyPen].cptfx; while (pptfx < pptfxEnd) vAddPoint(pptfx++); vEndFigure(); bIsHobby(TRUE); return(TRUE); } return(FALSE); } /******************************Public*Routine******************************\ * WIDEPENOBJ::bThicken(aptfxDiameter) * * Check if the pen is so thin in one dimension that it would be invisible * for some lines. If so, thicken it. * * History: * 31-Jan-1992 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ #define FX_HALF (LTOFX(1) >> 1) #define FX_HALF_SQUARED (FX_HALF * FX_HALF) BOOL WIDEPENOBJ::bThicken(PPOINTFIX aptfxDiameter) { // The aptfxDiameter vectors passed to us were constructed from the // diameter of the pen -- we actually want the vectors as based on // the radius, so halve them: EVECTORFX aevecTmp[2]; PPOINTFIX aptfx = (PPOINTFIX) aevecTmp; aevecTmp[0] = aptfxDiameter[0]; aevecTmp[1] = aptfxDiameter[1]; vHalve(aevecTmp[0]); vHalve(aevecTmp[1]); // What I would like to do is compute the length of the minor-axis of // the pen ellipse in device coordinates -- if its length is less than // half a pixel, we know we have to thicken the pen. // // But it's a lot of computation to do that (see Knuth, "Metafont", // p. 382). So, we take the two vectors that described the axis of // the ellipse in world space, and transform them to device space (call // them 'avec'). The perpendicular distance from the smaller vector // to the larger will be close to the minor-axis length. // Make sure we won't overflow: if (((ABS(aptfx[0].x) | ABS(aptfx[0].y) | ABS(aptfx[1].x) | ABS(aptfx[1].y)) & ~(0xfffL)) != 0) return(FALSE); FIX fxLengthSquared0 = aptfx[0].x * aptfx[0].x + aptfx[0].y * aptfx[0].y; FIX fxLengthSquared1 = aptfx[1].x * aptfx[1].x + aptfx[1].y * aptfx[1].y; POINTFIX ptfx; // Will be the largest vector FIX fxLength; // Length of largest vector // // We use the forumla to compute the distance from a point to a line // from the origin, where 'd' is the distance, 'B' is the vector, // 'P' is the point: // // 2 // 2 4 [ (P.y * B.x) - (P.x * B.y) ] // d = ------------------------------- // B.x * B.x + B.y * B.y // // We want to exit early when the distance is more than 1/2 pixel (8 in // FIX units), i.e. HALF < d or: // // 2 2 2 2 // HALF (B.x + B.y ) < 4 [ (P.y * B.x) - (P.x * B.y) ] // if (fxLengthSquared0 > fxLengthSquared1) { // Vector 0 is bigger than 1: FIX fxNum = aptfx[1].y * aptfx[0].x - aptfx[1].x * aptfx[0].y; LONGLONG eqLeft = (LONGLONG) fxLengthSquared0 * (FX_HALF_SQUARED >> 2); LONGLONG eqRight = Int32x32To64((LONG) fxNum, (LONG) fxNum); if (eqLeft < eqRight) return(FALSE); ptfx = aptfx[0]; fxLength = fxLengthSquared0; } else { // Vector 1 is bigger than (or equal to) 0: FIX fxNum = aptfx[0].y * aptfx[1].x - aptfx[0].x * aptfx[1].y; LONGLONG eqLeft = (LONGLONG) fxLengthSquared1 * (FX_HALF_SQUARED >> 2); LONGLONG eqRight = Int32x32To64((LONG) fxNum, (LONG) fxNum); if (eqLeft < eqRight) return(FALSE); ptfx = aptfx[1]; fxLength = fxLengthSquared1; } // Make sure that the largest vector extends outside the unit circle (if // it's not, we'll make it so that we end up with the entire Hobby pen // for width 1): if (fxLength < FX_HALF_SQUARED) { ptfx.x = FX_HALF; ptfx.y = 0; } // Okay, we know the larger vector, and we have to fix the smaller one. // Let's simply figure out the vertex on the Hobby pen of width 1 that // would be used if we drew a line in the same direction as our large // vector (which is when our given pen is its thinest). // // So let's compute the appropriate Hobby vector of size one (making // sure it's to the 'left' of the big vector, because the vertices have to // be in counter-clockwise order): POINTFIX ptfxHobby; if (ABS(ptfx.y) <= ptfx.x) { ptfxHobby.x = 0; ptfxHobby.y = -FX_HALF; } else if (ABS(ptfx.x) <= -ptfx.y) { ptfxHobby.x = -FX_HALF; ptfxHobby.y = 0; } else if (ABS(ptfx.y) <= -ptfx.x) { ptfxHobby.x = 0; ptfxHobby.y = FX_HALF; } else { ASSERTGDI(ABS(ptfx.x) <= ptfx.y, "Oops"); ptfxHobby.x = FX_HALF; ptfxHobby.y = 0; } // Put stuff into path, remembering that 'pptfx' points to either // ppath->pprlast->aptfx[0] or [1]. Also, we have to be in counter- // clockwise order. if (!bBeginFigure()) return(FALSE); vAddPoint(&ptfx); vAddPoint(&ptfxHobby); ptfx.x = -ptfx.x; ptfx.y = -ptfx.y; ptfxHobby.x = -ptfxHobby.x; ptfxHobby.y = -ptfxHobby.y; vAddPoint(&ptfx); vAddPoint(&ptfxHobby); vEndFigure(); return(TRUE); } /******************************Public*Routine******************************\ * WIDEPENOBJ::bPenFlatten(pptfxControl) * * Flattens a geometric pen object. pptfxControl points to the 7 control * points of a half-ellipse. * * History: * 1-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL WIDEPENOBJ::bPenFlatten(PPOINTFIX pptfxControl) { if (!bGrowPath()) return(FALSE); ASSERTGDI(pptfxPathRecEnd - pptfxPathRecCurrent > 2, "Path rec too small"); ppath->pprlast->flags = PD_BEGINSUBPATH; // Leave room to insert a point at the beginning of the path. Also leave // room for an additional point at the end of the path record: PPOINTFIX pptfxFirst = pptfxPathRecCurrent; pptfxPathRecCurrent++; pptfxPathRecEnd--; // Don't forget the very first point: *pptfxPathRecCurrent++ = *pptfxControl; // We'll crack 2 Beziers: for (LONG ll = 0; ll <= 1; ll++, pptfxControl += 3) { BOOL bMore; BEZIER bez; bez.vInit(pptfxControl); do { bMore = bez.bNext(pptfxPathRecCurrent++); if (!bMore) break; // Check if we have to create a new path record: if (pptfxPathRecCurrent > pptfxPathRecEnd) { // Wrap up old path record: ASSERTGDI(pptfxPathRecCurrent == pptfxPathRecEnd + 1, "Cur != End + 1"); // We want to repeat the last edge of this path record as the // first edge in the next. That means repeating two points // (remember that we left room for one more point in this record): PPOINTFIX pptfxPrev = pptfxPathRecCurrent - 2; // Sundown truncation ppath->pprlast->count = (ULONG)(pptfxPathRecCurrent - ppath->pprlast->aptfx); ASSERTGDI(ppath->pprlast->count >= 3, "Pen pathrecord doesn't have 3 vertices"); ppath->ppachain->pprfreestart = NEXTPATHREC(ppath->pprlast); // Create a new path record: if (!bGrowPath()) return(FALSE); ppath->pprlast->flags = 0; // Repeat those two points in the new path record: *pptfxPathRecCurrent++ = *(pptfxPrev); *pptfxPathRecCurrent++ = *(pptfxPrev + 1); // Don't forget to leave room for an additional point at the // end of the record: pptfxPathRecEnd--; } } while (1); } // The pen covers 180 degrees of the ellipse. The first and last edges // of the pen must be parallel, their vectors in opposite directions. // The last point in the path is already the opposite of the first. Now // make the zeroth point the opposite of the second last and voila, // parallel edges. pptfxFirst->x = - (pptfxPathRecCurrent - 2)->x; pptfxFirst->y = - (pptfxPathRecCurrent - 2)->y; ppath->pprlast->flags |= PD_ENDSUBPATH; // Sundown truncation ppath->pprlast->count = (ULONG)(pptfxPathRecCurrent - ppath->pprlast->aptfx); ASSERTGDI(ppath->pprlast->count >= 3, "Final pen pathrecord doesn't have 3 vertices"); return(TRUE); } /******************************Public*Routine******************************\ * WIDEPENOBJ::bPolygonizePen(exo, ulWidth) * * Creates a polyline approximation to the pen. * * If the pen is small and circular, we copy a pre-computed Hobby pen; * otherwise, we approximate the pen using Bezier curves and convert this * to lines. Remembering that the pen is defined in world space, we * construct the Bezier curves that approximate the pen circle in world * space. We then transform the coordinates to device space, and flatten. * * It makes life a lot easier if we can assume that the vertices of the pen * are stored in counter-clockwise order. So, we check the transform to * see if it is inverting, and adjust the Bezier control points accordingly. * * Due to the half-symmetry property of ellipses, we only have to store * the vertices for half the ellipse. For the binary search, it is useful * also to ensure that the last edge vector is the exact opposite of the * first. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL WIDEPENOBJ::bPolygonizePen( EXFORMOBJ& exo, // World to device transform LONG lWidth) // Pen width in world space { EVECTORFX avecAxis[2]; avecAxis[0].x = (FIX) lWidth; avecAxis[0].y = 0; avecAxis[1].x = 0; avecAxis[1].y = - (FIX) lWidth; exo.bXform((PVECTORL) avecAxis, avecAxis, 2); if (((avecAxis[0].x == avecAxis[1].y) && (avecAxis[0].y == -avecAxis[1].x)) || ((avecAxis[0].x == -avecAxis[1].y) && (avecAxis[0].y == avecAxis[1].x))) { // We now know that the transform was orthogonal, and that the pen is // circular in device space. if (MAX(ABS(avecAxis[0].x), ABS(avecAxis[0].y)) < LPLUSHALFTOFX(HOBBY_TABLE_SIZE)) { if (bHobbyize(avecAxis)) return(TRUE); else if (!bValid()) // See if path still okay return(FALSE); } } if (bThicken((PPOINTFIX) avecAxis)) return(TRUE); else if (!bValid()) // See if path still okay return(FALSE); // The points must be ordered in a counter-clockwise direction. // The half-ellipse we're creating starts at avecAxis[0], goes through // avecAxis[1], and ends at -avecAxis[0]. As such, avecAxis[1] must // lie to the left of avecAxis[0]: if (!bTurnLeftRandom(&avecAxis[0], &avecAxis[1])) { avecAxis[1].x = -avecAxis[1].x; avecAxis[1].y = -avecAxis[1].y; } vHalve(avecAxis[0]); vHalve(avecAxis[1]); VECTORFX vecRight; VECTORFX vecUp; LONGLONG eqTmp; vEllipseControlsOut(&avecAxis[0], &vecRight, &eqTmp); vEllipseControlsOut(&avecAxis[1], &vecUp, &eqTmp); // // 4 3 2 // +----x-------E-------x----+ // | | // | | // | | // 5 x x 1 ^ // | | | // | | | // | | | vecUp // | VecRight | | // 6 E O-------> E 0 | // // where 'O' is the origin, // 'x' is a control point of a Bezier curve // 'E' is an end point of a Bezier curve // EVECTORFX avec[7]; avec[6].x = -avecAxis[0].x; avec[6].y = -avecAxis[0].y; avec[5] = avec[6]; avec[5] += vecUp; avec[4] = avecAxis[1]; avec[4] -= vecRight; avec[3] = avecAxis[1]; avec[2] = avecAxis[1]; avec[2] += vecRight; avec[1] = avecAxis[0]; avec[1] += vecUp; avec[0] = avecAxis[0]; return(bPenFlatten((PPOINTFIX) avec)); } /******************************Public*Routine******************************\ * bLeft(pvec, pptfx, pfxCross) * * Returns TRUE if the vector to 'pptfx' turns left from 'pvec'. Returns * the cross product in pfxCross. * * NOTE: Since only the low DWORD of the cross product is returned, *pfxCross * is valid only when the vector '*(pptfx + 1) - *pptfx' is close to being * parallel to 'pvec'. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE BOOL bLeft( PEVECTORFX pvec, // First vector PPOINTFIX pptfx, // Second vector PLONGLONG pfxCross) // Result of pvec X pptfx { LONGLONG ad; LONGLONG bc; ad = Int32x32To64(pvec->x, (pptfx + 1)->y - pptfx->y); bc = Int32x32To64(pvec->y, (pptfx + 1)->x - pptfx->x); ad -= bc; *pfxCross = ad; return(ad < 0); } /******************************Public*Routine******************************\ * WIDEPENOBJ::vDetermineDrawVertex(vec, ld) * * The core routine of wide-lines. This routine determines the point * on the pen that draws the wide-line outline for the given line vector. * * Think of the line as a ray, and imagine dragging an elliptical pen * along it. The point on the ellipse that is furthest from the * ray defines the outline of the widened line. It's a bit of work to * determine the draw vertex on a true ellipse (involving a bunch of * floating point calculations). * * We take a different approach: we've already computed a convex polygonal * approximation of the true ellipse. We search the vertices of the * polygon to determine which is the farthest from the ray. * * We could do this by finding the maximum cross product of the line * vector and the vectors from the center of the pen to each of its * vertices, but this approach doesn't lend itself to a binary search. * * We accomplish a binary search by looking at the cross product of the * line vector and the vector difference between successive vertices of the * pen polygon -- we find the point on the pen polygon whose adjacent * edge vector on one side turns to the 'left' of the line vector, and * whose other edge vector turns to the 'right' of the line vector. * That is, the vertex whose cross products of its adjacent edge vectors * and the line vector have different signs. * * Our pen polygon is only half the ellipse -- under an affine transform, * ellipses are always symmetrical about some axis. Since we made sure * the first edge is exactly opposite the last edge when we built the * pen polygon, it follows that for any line vector, the cross product * with the first and last edges have different signs (unless the line * vector is parallel to that edge, which we sort of let fall through). * So we start the binary search by considering the first and last edges * of the half-ellipse. * * For the binary search, we maintain the invariant that the cross product * of the first and last edges with our line vector has different signs, and * we subdivide the interval between the first and last edge until they are * adjacent edges on the polygon; the shared vertex is then our draw vertex. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDEPENOBJ::vDetermineDrawVertex( EVECTORFX& vec, // Direction of line LINEDATA& ld) // Result goes here { PPOINTFIX pptfxStart; PPOINTFIX pptfxEnd; PPOINTFIX pptfxMid; LONGLONG fxCrossMid; BOOL bLeftStart; BOOL bLeftEnd; ld.ppr = ppath->pprfirst; if (ppath->pprfirst == ppath->pprlast) { bLeftStart = bLeft(&vec, &ld.ppr->aptfx[0], &ld.fxCrossStart); ld.fxCrossEnd = -ld.fxCrossStart; } else { // El yucko, the pen is bigger than one PathRecord. Scan through the // linked list until we find a PathRecord whose edges bracket our vector. // // NOTE: If our vector is parallel to and in the same direction as the // last edge in the last pathrecord, then // // bTurnLeft(vec, vecStart) == FALSE // and bTurnLeft(vec, vecEnd) == FALSE while (TRUE) { bLeftStart = bLeft(&vec, &ld.ppr->aptfx[0], &ld.fxCrossStart); bLeftEnd = bLeft(&vec, &ld.ppr->aptfx[ld.ppr->count - 2], &ld.fxCrossEnd); if ((bLeftStart ^ bLeftEnd) || ld.ppr->pprnext == (PPATHREC) NULL) break; ld.ppr = ld.ppr->pprnext; } } if (bLeftStart) ld.vSetInvert(); else ld.vClearInvert(); // 'pptfxStart' is a pointer to the first point of the first edge in // the path record. 'pptfxEnd' is a pointer to the first point of the // last edge. pptfxStart = &ld.ppr->aptfx[0]; pptfxEnd = &ld.ppr->aptfx[ld.ppr->count - 2]; while (TRUE) { pptfxMid = pptfxStart + ((pptfxEnd - pptfxStart) >> 1); if (bLeft(&vec, pptfxMid, &fxCrossMid) ^ bLeftStart) { // Signs are different, so the point we want must be between // 'start' and 'mid': ld.fxCrossEnd = fxCrossMid; if (pptfxMid == pptfxStart + 1) { ld.pptfx = pptfxMid; break; } pptfxEnd = pptfxMid; } else { // Signs are the same, so the point we want must be between // 'mid' and 'end': ld.fxCrossStart = fxCrossMid; if (pptfxEnd == pptfxMid + 1) { ld.pptfx = pptfxEnd; break; } pptfxStart = pptfxMid; } ASSERTGDI(pptfxStart < pptfxEnd, "Couldn't find draw vertex"); } // ld.pptfx is our draw vertex. #ifdef DEBUG_WIDE ASSERTGDI((ld.fxCrossEnd >= 0 && ld.fxCrossStart < 0) || (ld.fxCrossEnd <= 0 && ld.fxCrossStart >= 0), "Expected different cross products signs"); #endif ld.fxCrossStart = ABS(ld.fxCrossStart); ld.fxCrossEnd = ABS(ld.fxCrossEnd); } /******************************Public*Routine******************************\ * vAddNice(wpath, pptfx, pvec, bInvert) * * This routine is for adding round joins and end-caps to the given path. * * It does a trick to make rounds look nicer: when the given point lies on * the integer grid, it shrinks the pen vector by 1/16th of a pel in both * coordinates. * * This works on the assumption that most often the pens are constructed * from integer widths in device space -- when that happens, we will often * get assymetric filling because our fill convention breaks ties when a * scan-line has a line fall exactly on the integer grid by being lower-right * exclusive. * * Note: Remember that the bounds calculation has to take this possible * size increase into account. * * History: * 20-Nov-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID vAddNice( WIDEPATHOBJ& wpath, // Path to add to PPOINTFIX pptfx, // Spine point about which the round is added PEVECTORFX pvec, // The vector to add to pptfx BOOL bInvert) // TRUE if pvec is to be subtracted from pptfx { EVECTORFX vec; if (((pptfx->x | pptfx->y) & (LTOFX(1) - 1)) == 0) { if (bInvert) { vec.x = -pvec->x; vec.y = -pvec->y; bInvert = FALSE; } else { vec = *pvec; } if (vec.x > 0) vec.x -= 1; else if (vec.x < 0) vec.x += 1; if (vec.y > 0) vec.y -= 1; else if (vec.y < 0) vec.y += 1; pvec = &vec; } wpath.vAddPoint(pptfx, pvec, bInvert); } /******************************Public*Routine******************************\ * WIDEPENOBJ::cptAddRound(wid, ldIn, ldOut, bLeft, bInPerp, bOutPerp) * * Adds round joins to 'wid'. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ COUNT WIDEPENOBJ::cptAddRound( WIDENER& wid, // Path to which to add LINEDATA& ldIn, // Vector entering the point LINEDATA& ldOut, // Vector exiting the point BOOL bLeft, // ldOut turns left from ldIn? BOOL bInPerp, // BOOL bOutPerp) // { COUNT cpt = 0; ASSERTGDI(ldIn.bVecDrawComputed() && ldOut.bVecDrawComputed(), "Draw vertex not computed"); if (ldIn.pptfx == ldOut.pptfx && ldIn.bInvert() == ldOut.bInvert()) return(0); BOOL bInvert = ldIn.bInvert(); PPATHREC ppr = ldIn.ppr; PPOINTFIX pptfx = ldIn.pptfx; if (bLeft) { // Turns left, so add counter-clockwise to right side: if (bInPerp && !ldIn.bToLeftSide()) { wid.vAddRightNice((PEVECTORFX) pptfx, bInvert); } pptfx++; while (ppr != ldOut.ppr || pptfx > ldOut.pptfx || bInvert != ldOut.bInvert()) { PPOINTFIX pptfxEnd = &ppr->aptfx[ldIn.ppr->count - 1]; while (pptfx < pptfxEnd) { wid.vAddRightNice((PEVECTORFX) pptfx++, bInvert); cpt++; } if (ppr->pprnext != (PPATHREC) NULL) ppr = ppr->pprnext; else { ppr = ppath->pprfirst; bInvert = !bInvert; } pptfx = &ppr->aptfx[1]; } ASSERTGDI(ppr == ldOut.ppr, "Unmatched path record pointers"); ASSERTGDI(ldOut.pptfx - pptfx < (PATHALLOCSIZE >> 3), "Clockwise join unsynced"); while (pptfx < ldOut.pptfx) { wid.vAddRightNice((PEVECTORFX) pptfx++, bInvert); cpt++; } if (bOutPerp && ldOut.bToLeftSide()) { wid.vAddRightNice((PEVECTORFX) pptfx, bInvert); } } else { // Turns right, so add clockwise to left side: if (bInPerp && ldIn.bToLeftSide()) { wid.vAddLeftNice((PEVECTORFX) pptfx, bInvert); } pptfx--; while (ppr != ldOut.ppr || pptfx < ldOut.pptfx || bInvert != ldOut.bInvert()) { PPOINTFIX pptfxStart = &ppr->aptfx[1]; while (pptfx > pptfxStart) { wid.vAddLeftNice((PEVECTORFX) pptfx--, bInvert); cpt++; } if (ppr->pprprev != (PPATHREC) NULL) ppr = ppr->pprprev; else { ppr = ppath->pprlast; bInvert = !bInvert; } pptfx = &ppr->aptfx[ppr->count - 1]; } ASSERTGDI(ppr == ldOut.ppr, "Unmatched path record pointers"); ASSERTGDI(pptfx - ldOut.pptfx < (PATHALLOCSIZE >> 3), "Counterclockwise join unsynced"); while (pptfx > ldOut.pptfx) { wid.vAddLeftNice((PEVECTORFX) pptfx--, bInvert); cpt++; } if (bOutPerp && !ldOut.bToLeftSide()) { wid.vAddLeftNice((PEVECTORFX) pptfx, bInvert); } } return(cpt); } /******************************Public*Routine******************************\ * WIDEPENOBJ::vAddRoundEndCap(wid, ld, bStartCap, bRound) * * Adds round caps to the path. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDEPENOBJ::vAddRoundEndCap( WIDENER& wid, // Path to which to add LINEDATA& ld, // Vector entering the point BOOL bStartCap, // TRUE if Start-cap BOOL bRound) // TRUE if round joins { ASSERTGDI(ld.bVecDrawComputed(), "Draw vertex not computed"); BOOL bInvert = (bStartCap) ? !ld.bInvert() : ld.bInvert(); PPATHREC ppr = ld.ppr; PPOINTFIX pptfx = ld.pptfx; // Turns left, so add counter-clockwise to right side: if (!bRound && !ld.bToLeftSide()) wid.vAddRightNice((PEVECTORFX) pptfx, bInvert); pptfx++; while (ppr != ld.ppr || pptfx > ld.pptfx) { PPOINTFIX pptfxEnd = &ppr->aptfx[ld.ppr->count - 1]; while (pptfx < pptfxEnd) wid.vAddRightNice((PEVECTORFX) pptfx++, bInvert); if (ppr->pprnext != (PPATHREC) NULL) ppr = ppr->pprnext; else { ppr = ppath->pprfirst; bInvert = !bInvert; } pptfx = &ppr->aptfx[1]; } ASSERTGDI(ppr == ld.ppr, "Unmatched path record pointers"); ASSERTGDI(ld.pptfx - pptfx < (PATHALLOCSIZE >> 3), "Clockwise join unsynced"); while (pptfx < ld.pptfx) wid.vAddRightNice((PEVECTORFX) pptfx++, bInvert); if (!bRound && ld.bToLeftSide()) wid.vAddRightNice((PEVECTORFX) pptfx, bInvert); } /******************************Public*Routine******************************\ * READER::bNextPoint(ptfx) * * Reads the next point from the spine of the path. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL READER::bNextPoint(POINTFIX& ptfx) { BOOL bRet; if (pptfxRead < pptfxEnd) { ptfx = *pptfxRead++; bRet = TRUE; } else if (pd.flags & PD_ENDSUBPATH) bRet = FALSE; else { // Get next path record: vMoreToEnum(pepoSpine->bEnum(&pd)); ASSERTGDI(pd.count > 0, "Empty path record"); ASSERTGDI(!(pd.flags & PD_BEGINSUBPATH), "Unexpected begin subpath"); ptfx = pd.pptfx[0]; pptfxRead = &pd.pptfx[1]; pptfxEnd = &pd.pptfx[pd.count]; bRet = TRUE; } return(bRet); } /******************************Public*Routine******************************\ * READER::bNextFigure() * * Goes to the next figure in the path. Returns FALSE if there are none. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL READER::bNextFigure() { BOOL bRet = bMoreToEnum(); if (bRet) { vMoreToEnum(pepoSpine->bEnum(&pd)); if (pd.count == 0) { ASSERTGDI(!bMoreToEnum(), "Empty path record in non-empty path"); bRet = FALSE; } ASSERTGDI((pd.flags & PD_BEGINSUBPATH) || (pd.count == 0), "bNextFig: Not at start of fig"); pptfxRead = &pd.pptfx[0]; pptfxEnd = &pd.pptfx[pd.count]; } return(bRet); } /******************************Public*Routine******************************\ * LINER::vNextPoint() * * This function reads the next point in a path. It will break Bezier * curves into lines as it goes. * * CONSTRUCTING THE PATH BY VERTEX INSTEAD OF BY LINE * * The widening system is point based and not line based. That is, we * read the original path one vertex at-a-time and construct the wide-lines * by making the appropriate structures at each of those vertices (i.e., * at every vertex we either construct an end-cap or a join); we don't read * the path a line at-a-time and add both ends of the line then do the * join if there is one. * * This is a subtle but important distinction. * * RETURNING THE DATA * * To construct a join, we need 3 things from the original path: * 1) The point of the join; * 2) The vector of the line entering the join; * 3) The vector of the line exiting the join. * * A start cap requires: * 1) The point of the start cap; * 2) The vector of the line exiting the start cap. * * An end cap requires: * 1) The point of the end cap; * 2) The vector of the line entering the end cap. * * This data is returned by setting 'ptfxThis' as the point of interest, * 'pldIn' points to the entering vector, and 'pldOut' points to the * exiting vector. * * We don't incur the cost of re-computing the same perpendicular or draw * vector for both sides of a line because that's handled by the lazy * evaluation of 'pldIn' and 'pldOut'. * * VARIABLES * * Internal: * * ptfxNext - Next point after 'ptfxThis', used to compute 'ldOut' * ptfxStartFigure - Start point for current figure * ls - Internal state for reading path * ldStartFigure - Line vector data for first line in figure * ldStartTangent, ldEndTangent - Line vector data for start and end * tangents of current Bezier * * External: * * ptfxThis - Current point * pldIn, pldOut - Entering and exiting vectors from 'ptfxThis' * * pld->vecLine is the difference vector between 'ptfxThis' and 'ptfxNext' * (so its length is the length of that line; used by styler) * pld->vecTangent is the true tangent vector at the point * (this is used for calculating true perpendiculars, and * its length does NOT correspond to the line at that point) * * we - Returns point type at 'ptfxThis': * * WE_STARTFIGURE - Figure starts here * WE_JOIN - Do a normal join * WE_BEZIERJOIN - Do a Bezier join * WE_CLOSEFIGURE - Do the final CloseFigure join * WE_ENDFIGURE - Do the end-cap * WE_FINISHFIGURE - Do the start-cap for the figure we * just finished * WE_DONEPATH - All done! * * The resulting language is: * * {s{j|b}[c|ef]}d * * History: * 1-Dec-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID LINER::vNextPoint() { // Assign 'pldOut' a new spot in the LINEDATA buffer. Just make sure // the location isn't already being used by 'pldIn': pldOut = &ldBuf[ (pldIn != &ldBuf[0]) ? 0 : 1 ]; switch (ls) { case LS_READPATH: case LS_STARTFIGURE: if (ls == LS_READPATH) we = WE_JOIN; // Normal join else { ASSERTGDI(ls == LS_STARTFIGURE, "Not start figure"); we = WE_STARTFIGURE; // The point starts a figure ls = LS_READPATH; } if (bNextPoint(ptfxNext)) { // The next point in the path may be the second control point // of a Bezier curve. If it is, we have to set up to crack the // curve into lines: if (bIsBezier()) { POINTFIX aptfx[4]; aptfx[0] = ptfxThis; aptfx[1] = ptfxNext; bNextPoint(aptfx[2]); bNextPoint(aptfx[3]); bez.vInit(aptfx); if (!bez.bNext(&ptfxNext)) { // Okay, we have a real small Bezier curve (it got converted // into one line). Don't bother trying to use the Bezier's // control points to get accurate perps (that is a heuristic // that only works when you have lotsa lines in the // approximation): pldOut->vInit(ptfxNext, ptfxThis); ls = LS_READPATH; return; } pldOut->vInit(ptfxNext, ptfxThis); ldStartTangent = *pldOut; ldStartTangent.vecTangent = aptfx[1]; ldStartTangent.vecTangent -= aptfx[0]; // Initialize end-tangent vector with the true end-tangent // (we will change 'ldEndTangent.vecLine' later): ldEndTangent.vInit(aptfx[3], aptfx[2]); pldOut = &ldStartTangent; // Set up to get the next point from the current Bezier: ls = LS_READBEZIER; return; } } else { // Since there is no 'ptfxNext', it follows that 'ptfxThis' is the // last point in the figure. See if we have to close the figure. ls = LS_FINISHFIGURE; ptfxNext = ptfxStartFigure; if (bIsClosedFigure()) we = WE_JOIN; else { we = WE_ENDFIGURE; return; } } break; case LS_FINISHFIGURE: we = bIsClosedFigure() ? WE_CLOSEFIGURE : WE_FINISHFIGURE; pldOut = &ldStartFigure; if (!bNextFigure()) ls = LS_DONEPATH; else { bNextPoint(ptfxNext); ptfxStartFigure = ptfxNext; ls = LS_STARTFIGURE; } return; case LS_READBEZIER: we = WE_BEZIERJOIN; if (!bez.bNext(&ptfxNext)) { // Since this is the last line in the Bezier, make sure we use // the tangent vector computed from the last two control points // when calculating perpendicular for the line: ls = LS_READPATH; pldOut->vInit(ptfxNext, ptfxThis); pldOut->vecTangent = ldEndTangent.vecTangent; return; } break; case LS_DONEPATH: we = WE_DONEPATH; return; default: RIP("Unknown line state"); } // Finally, compute the exit vector for those cases that made it here: pldOut->vInit(ptfxNext, ptfxThis); } /******************************Public*Routine******************************\ * LINER::vZeroFigure() * * Sets pldOut and pldIn for a zero-length figure. * * History: * 31-Dec-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID LINER::vZeroFigure() { ldStartFigure.vInit(); ldStartFigure.vecLine.x = LTOFX(1); ldStartFigure.vecTangent.x = LTOFX(1); ldStartFigure.vecLine.y = 0; ldStartFigure.vecTangent.y = 0; pldIn = &ldStartFigure; pldOut = &ldStartFigure; } /******************************Public*Routine******************************\ * LINER::vNextEvent() * * This function reads the next event from the path. * * History: * 31-Dec-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID LINER::vNextEvent() { // Update some of our current point data: ptfxThis = ptfxNext; pldIn = pldOut; // Check out the next point: vNextPoint(); WIDENEVENT weOld = we; // Eat any zero-length lines. This will stop automatically while (pldOut->vecLine.x == 0 && pldOut->vecLine.y == 0) { if (we != WE_STARTFIGURE && we != WE_JOIN && we != WE_BEZIERJOIN) break; // Due to rounding on the integer grid, for small Bezier curves // the first or last line segments may wind up being zero length. // In that case, 'vecLine' will be a zero-vector. But we do know // the true tangents at that point. vNextPoint(); } // Have to watch for the case when the vecTagent is zero but vecLine is // not (such as when the first two or last two control points of the // Bezier are coincident): if (pldOut->vecTangent.x == 0 && pldOut->vecTangent.y == 0) pldOut->vecTangent = pldOut->vecLine; if (weOld == WE_STARTFIGURE) { if (we == WE_CLOSEFIGURE || we == WE_ENDFIGURE) { if (we == WE_ENDFIGURE) { vNextPoint(); ASSERTGDI(we == WE_FINISHFIGURE, "Expected finish figure"); } vZeroFigure(); we = WE_ZEROFIGURE; } else { ldStartFigure = *pldOut; pldOut = &ldStartFigure; we = WE_STARTFIGURE; } } } /******************************Public*Routine******************************\ * STYLER::STYLER(epo, dco) * * Constructor for the styler. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ STYLER::STYLER( EPATHOBJ& epo, // Path of spine PLINEATTRS pla) : LINER(epo), exoDeviceToWorld(&mxDeviceToWorld, DONT_COMPUTE_FLAGS) { vDoingStyles((pla->pstyle != (PFLOAT_LONG) NULL) && (pla->cstyle > 0)); if (bDoingStyles()) { pstyleStart = pla->pstyle; pstyleCurrent = pstyleStart; pstyleEnd = pstyleStart + pla->cstyle; // Next point comes from the path, not from a style: vStyleNext(FALSE); } } /******************************Public*Routine******************************\ * STYLER::efNextStyleLength() * * Reads next style array entry from user-supplied list. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE EFLOAT STYLER::efNextStyleLength() { EFLOATEXT efResult(pstyleCurrent->e); pstyleCurrent++; if (pstyleCurrent >= pstyleEnd) pstyleCurrent = pstyleStart; ASSERTGDI(!efResult.bIsNegative(), "Negative style length"); return(efResult); } /******************************Public*Routine******************************\ * STYLER::efWorldLength(vec) * * Computes the world space length of the vector. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ EFLOAT STYLER::efWorldLength(EVECTORFX vec) { // Transform the vector from device FIX format to world space LONG: BOOL bRet = exoDeviceToWorld.bXform(&vec, (PVECTORL) &vec, 1); ASSERTGDI(bRet, "XForm failed"); // Now compute the Cartesian length: EFLOAT x; EFLOAT y; // The Alpha computes an eensy-weensy length for zero-length vectors // which causes later math to underflow, so we simply check for zero // length here. The next smallest possible vector has a length of 1.0, // and that has no chance of underflow. if ((vec.x == 0) && (vec.y == 0)) { x = FP_0_0; } else { x = vec.x; y = vec.y; x *= x; y *= y; x += y; x.vSqrt(); } return(x); } /******************************Public*Routine******************************\ * ptfxFraction(ptfx, pld, efDistance, efLineLength) * * Returns ptfx + pld->vecLine * (efDistance / efLineLength). * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ POINTFIX ptfxFraction( POINTFIX ptfx, PLINEDATA pld, EFLOAT& efDistance, EFLOAT& efLineLength) { ASSERTGDI(efDistance <= efLineLength, "More than line length"); ASSERTGDI(efDistance >= FP_0_0, "Negative distance"); POINTFIX ptfxResult; if (efLineLength.bIsZero()) return(ptfx); if (!pld->bNormalizedComputed()) { pld->ptflNormalized.x = (LONG) pld->vecLine.x; pld->ptflNormalized.y = (LONG) pld->vecLine.y; // Assume a floating point multiply is twice as fast as a divide: EFLOAT efFactor; efFactor = FP_1_0; efFactor /= efLineLength; pld->ptflNormalized.x *= efFactor; pld->ptflNormalized.y *= efFactor; pld->vSetNormalizedComputed(); } POINTFL ptfl; ptfl = pld->ptflNormalized; ptfl.x *= efDistance; ptfl.y *= efDistance; LONG x; LONG y; // bEfToL has to take a LONG argument (can't be a FIX cast to a LONG): BOOL bRet1 = ptfl.x.bEfToL(x); BOOL bRet2 = ptfl.y.bEfToL(y); ASSERTGDI(bRet1 && bRet2, "Unexpected overflow"); ptfxResult = ptfx; ptfxResult.x += (FIX) x; ptfxResult.y += (FIX) y; return(ptfxResult); } /******************************Public*Routine******************************\ * STYLER::vNextStyleEvent() * * Gets next style event. Could be a join, end-cap, start-cap, etc. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID STYLER::vNextStyleEvent() { if (!bDoingStyles()) vNextEvent(); else { if (bStyleNext()) { if (we == WE_STOPDASH) { // Handle gaps: // This following test includes equality in order to eat zero // length lines when on a zero length style: while (efStyleLength >= efRemainingLength) { efStyleLength -= efRemainingLength; vNextEvent(); if (we != WE_JOIN && we != WE_BEZIERJOIN) { // We've come to the end of the figure, and it's covered // by a gap. Since we've effectively already put an end-cap // on the figure (we're in a gap, after all), we can eat // an end-cap message. ASSERTGDI(we == WE_CLOSEFIGURE || we == WE_ENDFIGURE, "Unexpected event"); if (we == WE_ENDFIGURE) { // We need the first point in the figure: vNextEvent(); ASSERTGDI(we == WE_FINISHFIGURE, "Expected finish fig"); } vStyleNext(FALSE); we = WE_FINISHFIGURE; return; } efDoneLength = FP_0_0; efLineLength = efWorldLength(pldOut->vecLine); efRemainingLength = efLineLength; ptfxLineStart = ptfxThis; } efRemainingLength -= efStyleLength; efDoneLength += efStyleLength; ptfxThis = ptfxFraction(ptfxLineStart, pldOut, efDoneLength, efLineLength); efStyleLength = efNextStyleLength(); we = WE_STARTDASH; return; } else { // Handle a dash: // If you do a Rectangle(100, 100, 200, 200) with flat caps // and a world transform set, with a style array of // {100, 30}, I don't want it to wrap around a corner -- // i.e., both perps for the start and end of the first dash // should be calculated from the vector (-100, 0), and NOT // have the start calculated from (-100, 0) and the end // from (0, 100).. if (efStyleLength > efRemainingLength) { efStyleLength -= efRemainingLength; } else { efRemainingLength -= efStyleLength; efDoneLength += efStyleLength; ptfxThis = ptfxFraction(ptfxLineStart, pldOut, efDoneLength, efLineLength); pldIn = pldOut; efStyleLength = efNextStyleLength(); we = WE_STOPDASH; return; } } } // Okay, we're done with the current line (we've completely styled it), // so get the next one from the path: vNextEvent(); switch(we) { case WE_STARTFIGURE: vResetStyles(); efStyleLength = efNextStyleLength(); efDoneLength = FP_0_0; efLineLength = efWorldLength(pldOut->vecLine); efRemainingLength = efLineLength; ptfxLineStart = ptfxThis; vStyleNext(TRUE); break; case WE_JOIN: case WE_BEZIERJOIN: efDoneLength = FP_0_0; efLineLength = efWorldLength(pldOut->vecLine); efRemainingLength = efLineLength; ptfxLineStart = ptfxThis; vStyleNext(TRUE); break; default: vStyleNext(FALSE); } } } /******************************Public*Routine******************************\ * WIDENER::vVecDrawCompute(ld) * * Computes the draw vector for 'ld'. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDENER::vVecDrawCompute(LINEDATA& ld) { #ifdef DEBUG_WIDE ASSERTGDI(ld.vecTangent.x != 0 || ld.vecTangent.y != 0, "Can't compute draw vertex for zero vector"); #endif wpen.vDetermineDrawVertex(ld.vecTangent, ld); if (!ld.bInvert()) ld.vecDraw = *ld.pptfx; else { ld.vecDraw.x = - ld.pptfx->x; ld.vecDraw.y = - ld.pptfx->y; } ld.vSetVecDrawComputed(); vVecSymmetricRound(&ld.vecDraw); } /******************************Public*Routine******************************\ * WIDENER::vVecPerpCompute(ld) * * Computes the perp vector for 'ld'. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDENER::vVecPerpCompute(LINEDATA& ld) { ASSERTGDI(ld.vecTangent.x != 0 || ld.vecTangent.y != 0, "Can't compute perp for zero vector"); if (!ld.bVecDrawComputed()) vVecDrawCompute(ld); ASSERTGDI(ld.fxCrossStart >= 0 && ld.fxCrossEnd >= 0, "-ve cross"); LONGLONG aa = ld.fxCrossStart; LONGLONG bb = ld.fxCrossEnd; EVECTORFX vec; if (aa <= bb) { vec.x = ld.pptfx->x - (ld.pptfx - 1)->x; vec.y = ld.pptfx->y - (ld.pptfx - 1)->y; } else { vec.x = (ld.pptfx + 1)->x - ld.pptfx->x; vec.y = (ld.pptfx + 1)->y - ld.pptfx->y; } POINTFIX ptfxMid; ptfxMid.x = ld.pptfx->x - (vec.x >> 1); ptfxMid.y = ld.pptfx->y - (vec.y >> 1); LONGLONG x; LONGLONG y; LONGLONG fxDivisor = aa + bb; x = aa * ABS(vec.x); y = aa * ABS(vec.y); // This is actually only an unsigned divide: ULONG ulXRemainder; ULONG ulYRemainder; if (fxDivisor != 0) { if (fxDivisor < ULONG_MAX) { VDIV(x, (ULONG) fxDivisor, &ulXRemainder); VDIV(y, (ULONG) fxDivisor, &ulYRemainder); } else { // When the numbers are this big, phooie on the rounding: ulXRemainder = 0; x /= fxDivisor; ulYRemainder = 0; y /= fxDivisor; } } else { ulXRemainder = 0; ulYRemainder = 0; } // Now adjust the signs: FIX xPrime = (LONG) x; FIX yPrime = (LONG) y; // This is a safe conversion because fxDivisor is always positive: ULONG ulHalfDivisor = (ULONG) fxDivisor; ulHalfDivisor >>= 1; // Round: if (ulXRemainder >= ulHalfDivisor) xPrime++; if (ulYRemainder >= ulHalfDivisor) yPrime++; // Give it the correct sign: if (vec.x < 0) xPrime = -xPrime; if (vec.y < 0) yPrime = -yPrime; ld.vecPerp.x = xPrime + ptfxMid.x; ld.vecPerp.y = yPrime + ptfxMid.y; if (ld.bInvert()) { ld.vecPerp.x = -ld.vecPerp.x; ld.vecPerp.y = -ld.vecPerp.y; } ld.vSetVecPerpComputed(); // Rounds to half integer: vVecRound(&ld.vecPerp); } /******************************Public*Routine******************************\ * WIDENER::vVecSquareCompute(ld) * * Computes the square cap vector for 'ld'. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDENER::vVecSquareCompute(LINEDATA& ld) { ASSERTGDI(ld.vecTangent.x != 0 || ld.vecTangent.y != 0, "Can't compute square vector for zero vector"); EFLOAT efAlpha; EFLOAT efLength; efAlpha = efHalfWidth; efLength = efWorldLength(ld.vecTangent); if (!efLength.bIsZero()) efAlpha /= efLength; else efAlpha = FP_0_0; EFLOATEXT x(ld.vecTangent.x); EFLOATEXT y(ld.vecTangent.y); x *= efAlpha; y *= efAlpha; // We can assert that these conversions won't fail because we already // did bounds checking on the entire path. BOOL bRet1 = x.bEfToL(ld.vecSquare.x); BOOL bRet2 = y.bEfToL(ld.vecSquare.y); ASSERTGDI(bRet1 && bRet2, "Square vector out of bounds"); ld.vSetVecSquareComputed(); } /******************************Public*Routine******************************\ * bIs31Bits(fx) * * Returns TRUE if 'fx' is a 31-bit signed number (i.e., the upper 2 bits * are the same). * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE BOOL bIs31Bits(FIX fx) { return(fx < ((LONG) LONG_MAX >> 1) && fx > ((LONG) LONG_MIN >> 1)); } /******************************Public*Routine******************************\ * bComputeIntersect(pvecA, pvecB, pvecC, pvecD, pvecIntersect) * * Computes the intersection of the rays defined by the given line segments. * Returns FALSE if the computation overflows or if the intersection is * outside the 31 bit space. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL bComputeIntersect( PEVECTORFX pvecA, // Start point of first line segment PEVECTORFX pvecB, // Direction of first line segment PEVECTORFX pvecC, // Start point of second line segment PEVECTORFX pvecD, // Direction of second line segment PEVECTORFX pvecIntersect) // Resulting point of intersection { // // The intersection is computed by: // // (Cx - Ax)(-Dy) + (Cy - Ay)(Dx) // lambda = ------------------------------ // (Bx)(-Dy) + (By)(Dx) // // intersect = A + lambda * B // EFLOAT efTerm1; EFLOAT efTerm2; EFLOAT efNum; EFLOAT efDenom; // (Cx - Ax)(-Dy) efNum = (pvecC->x - pvecA->x); efTerm2 = -pvecD->y; efNum *= efTerm2; // (Cy - Ay)(Dx) efTerm1 = (pvecC->y - pvecA->y); efTerm2 = pvecD->x; efTerm1 *= efTerm2; efNum += efTerm1; // (Bx)(-Dy) efDenom = pvecB->x; efTerm2 = -pvecD->y; efDenom *= efTerm2; // (By)(Dx) efTerm1 = pvecB->y; efTerm2 = pvecD->x; efTerm1 *= efTerm2; efDenom += efTerm1; if (efDenom.bIsZero()) return(FALSE); // lambda efNum /= efDenom; // lambda * B EVECTORFX lambdaB; efTerm1 = pvecB->x; efTerm2 = pvecB->y; efTerm1 *= efNum; efTerm2 *= efNum; if (!efTerm1.bEfToL(lambdaB.x) || !efTerm2.bEfToL(lambdaB.y) || !bIs31Bits(lambdaB.x) || !bIs31Bits(lambdaB.y)) { return(FALSE); } // A + lambda * B pvecIntersect->x = pvecA->x + lambdaB.x; pvecIntersect->y = pvecA->y + lambdaB.y; // Check for overflow: if (!bIs31Bits(pvecIntersect->x) || !bIs31Bits(pvecIntersect->y)) { WARNING("Miter would fall outside device space\n"); return(FALSE); } return(TRUE); } /******************************Public*Routine******************************\ * WIDENER::bMiterInLimit(vec) * * Determines if the resulting vector from the middle of the wideline to * the outer corner would be within the specified miter limit. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL WIDENER::bMiterInLimit(EVECTORFX vec) { // Note we change our local copy of 'vec': BOOL bRet = exoDeviceToWorld.bXform(&vec, (PVECTORL) &vec, 1); ASSERTGDI(bRet, "XForm failed"); // Now compute the square of the Cartesian length: EFLOAT x; EFLOAT y; x = vec.x; y = vec.y; x *= x; y *= y; x += y; return(x <= efHalfWidthMiterLimitSquared); } /******************************Public*Routine******************************\ * WIDENER:vAddRoundJoin(bBezierJoin) * * Adds a join. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDENER::vAddRoundJoin(BOOL bBezierJoin) { BOOL bTurnLeft = bTurnLeftRandom(&pldIn->vecTangent, &pldOut->vecTangent); BOOL bInPerp = FALSE; BOOL bOutPerp = FALSE; if (!bAllRound()) { if (!bBezierJoin) { bInPerp = TRUE; bOutPerp = TRUE; } // else // { // if (pldIn->bSamePenSection(ldStartTangent) // || pldIn->bSamePenSection(ldEndTangent)) // bInPerp = TRUE; // // if (pldOut->bSamePenSection(ldStartTangent) // || pldOut->bSamePenSection(ldEndTangent)) // bOutPerp = TRUE; // } } EVECTORFX vecIn = bInPerp ? vecInPerp() : vecInDraw(); EVECTORFX vecOut = bOutPerp ? vecOutPerp() : vecOutDraw(); vAddRight(vecIn); vAddLeft(vecIn); if (vecIn != vecOut) { COUNT cpt; if (bTurnLeft) { cpt = wpen.cptAddRound(*this, *pldIn, *pldOut, bTurnLeft, bInPerp, bOutPerp); vAddLeft(); if (!bAllRound()) // if ((cpt > 0 || bInPerp || bOutPerp) && bBezierJoin) // hmmm and not all round? { vAddLeft(vecOut); if (cpt > 0) wpen.cptAddRound(*this, *pldOut, *pldIn, !bTurnLeft, bOutPerp, bInPerp); vAddLeft(vecIn); vAddLeft(); } } else { cpt = wpen.cptAddRound(*this, *pldIn, *pldOut, bTurnLeft, bInPerp, bOutPerp); vAddRight(); if (!bAllRound()) // if ((cpt > 0 || bInPerp || bOutPerp) && bBezierJoin) // hmmm and not all round? { vAddRight(vecOut); if (cpt > 0) wpen.cptAddRound(*this, *pldOut, *pldIn, !bTurnLeft, bOutPerp, bInPerp); vAddRight(vecIn); vAddRight(); } } vAddRight(vecOut); vAddLeft(vecOut); } } /******************************Public*Routine******************************\ * WIDENER:vAddJoin(bBezierJoin) * * Adds a join. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDENER::vAddJoin(BOOL bBezierJoin) { BOOL bTurnLeft; if (iJoin == JOIN_ROUND || bBezierJoin) vAddRoundJoin(bBezierJoin); else if (iJoin == JOIN_BEVEL) { bTurnLeft = bTurnLeftRandom(&pldIn->vecTangent, &pldOut->vecTangent); EVECTORFX vecIn = vecInPerp(); EVECTORFX vecOut = vecOutPerp(); vAddLeft(vecIn); vAddRight(vecIn); if (vecIn != vecOut) { if (bTurnLeft) vAddLeft(); else vAddRight(); vAddRight(vecOut); vAddLeft(vecOut); } } else { ASSERTGDI(iJoin == JOIN_MITER, "Unexpected join type"); bTurnLeft = bTurnLeftRandom(&pldIn->vecTangent, &pldOut->vecTangent); // Use the 'perpendicular' vector rather than the 'draw' vector here for // computing the intersection point. This is done to be consistent: // if the same path is redrawn but with a different miter limit, we will // still light the same pels except on the joins that change between // mitered and beveled. EVECTORFX vecIn = vecInPerp(); EVECTORFX vecOut = vecOutPerp(); vAddLeft(vecIn); vAddRight(vecIn); if (vecIn != vecOut) { EVECTORFX vecIntersect; if (bComputeIntersect(&vecIn, &pldIn->vecLine, &vecOut, &pldOut->vecLine, &vecIntersect)) if (bMiterInLimit(vecIntersect)) { if (bTurnLeft) vAddRight(vecIntersect); else vAddLeft(vecIntersect); } if (bTurnLeft) vAddLeft(); else vAddRight(); vAddLeft(vecOut); vAddRight(vecOut); } } } /******************************Public*Routine******************************\ * WIDENER::vAddEndCap() * * Adds an end-cap. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDENER::vAddEndCap() { switch(iEndCap) { case ENDCAP_SQUARE: EVECTORFX vecRight; EVECTORFX vecLeft; vecRight = vecInSquare(); vecLeft = vecRight; vecRight += vecInPerp(); vecLeft -= vecInPerp(); vAddRight(vecRight); vAddRight(vecLeft); break; case ENDCAP_BUTT: EVECTORFX vecInP; vecInP = vecInPerp(); vAddRight(vecInP, FALSE); vAddRight(vecInP, TRUE); break; case ENDCAP_ROUND: EVECTORFX vec; if (bAllRound()) vec = vecInDraw(); else vec = vecInPerp(); vAddRight(vec, FALSE); wpen.vAddRoundEndCap(*this, *pldIn, FALSE, bAllRound()); vAddRight(vec, TRUE); break; default: RIP("Unknown cap type"); } } /******************************Public*Routine******************************\ * WIDENER::vAddStartCap() * * Adds a start cap. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID WIDENER::vAddStartCap() { switch(iEndCap) { case ENDCAP_SQUARE: { EVECTORFX vecRight = vecOutSquare(); EVECTORFX vecLeft; vecRight.x = -vecRight.x; vecRight.y = -vecRight.y; vecLeft = vecRight; vecRight += vecOutPerp(); vecLeft -= vecOutPerp(); vAddRight(vecLeft); vAddRight(vecRight); } break; case ENDCAP_BUTT: EVECTORFX vecOutP; vecOutP = vecOutPerp(); vAddRight(vecOutP, TRUE); vAddRight(vecOutP, FALSE); break; case ENDCAP_ROUND: { EVECTORFX vec; if (bAllRound()) vec = vecOutDraw(); else vec = vecOutPerp(); vAddRight(vec, TRUE); wpen.vAddRoundEndCap(*this, *pldOut, TRUE, bAllRound()); vAddRight(vec, FALSE); } break; default: RIP("Unknown cap type"); } } /******************************Public*Routine******************************\ * BOOL WIDENER::bWiden() * * Widens the path. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL WIDENER::bWiden() { while(TRUE) { vNextStyleEvent(); switch (we) { case WE_ZEROFIGURE: // If the entire figure is comprised of one point, output the // pen circle at that point if we have round caps (we can't handle // square caps because we have no idea of the intended direction): if (iEndCap == ENDCAP_ROUND) { if (!wpathRight.bBeginFigure()) return(FALSE); vAddStartCap(); vAddEndCap(); // Finish up: wpathRight.vEndFigure(); wpathRight.vCloseFigure(); } break; case WE_STARTFIGURE: if (!wpathLeft.bBeginFigure() || !wpathRight.bBeginFigure()) return(FALSE); vFigureStyled(FALSE); wpathRight.vMarkFigureStart(); break; case WE_JOIN: vAddJoin(FALSE); break; case WE_BEZIERJOIN: vAddJoin(TRUE); break; case WE_STARTDASH: if (!wpathLeft.bBeginFigure() || !wpathRight.bBeginFigure()) return(FALSE); vAddStartCap(); break; case WE_ENDFIGURE: case WE_STOPDASH: vAddEndCap(); // Finish up: wpathLeft.vEndFigure(); if (!bValid()) return(FALSE); wpathRight.vReverseConcatenate(wpathLeft); wpathRight.vEndFigure(); wpathRight.vCloseFigure(); vFigureStyled(TRUE); // We hit a style dash break; case WE_FINISHFIGURE: // Prepend the start cap to the beginning of the figure: if (!wpathRight.bBeginFigure()) return(FALSE); vAddStartCap(); wpathRight.vEndFigure(); wpathRight.vPrependBeforeFigure(); break; case WE_CLOSEFIGURE: vAddJoin(FALSE); wpathLeft.vEndFigure(); if (!bFigureStyled()) { wpathRight.vEndFigure(); wpathRight.vCloseFigure(); if (!wpathRight.bBeginFigure()) return(FALSE); wpathRight.vReverseConcatenate(wpathLeft); wpathRight.vEndFigure(); wpathRight.vCloseFigure(); } else { wpathRight.vEndFigure(); if (!wpathRight.bBeginFigure()) return(FALSE); wpathRight.vReverseConcatenate(wpathLeft); wpathRight.vEndFigure(); wpathRight.vPrependBeforeSubpath(); wpathRight.vPrependBeforeFigure(); wpathRight.vCloseFigure(); } break; case WE_DONEPATH: return(bValid()); default: RIP("Unknown widen event"); } } RIP("Shouldn't get here"); return(FALSE); } /******************************Public*Routine******************************\ * EPATHOBJ::vBecome(wpath) * * Free any data in our path, copy all the data from 'epo', and delete * 'epo'. * * Our path can be a PATHMEMOBJ or PATHSTACKOBJ. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID EPATHOBJ::vBecome(WIDEPATHOBJ& wpath) { ASSERTGDI(!(wpath.ppath->flType & PATHTYPE_STACK), "Can't do stacks"); // First, free all the path blocks in our destination path: vFreeBlocks(); // Now copy all the important stuff about the path from 'wpath', including // the pointers to all its path data: cCurves = wpath.cCurves; ppath->ppachain = wpath.ppath->ppachain; ppath->pprfirst = wpath.ppath->pprfirst; ppath->pprlast = wpath.ppath->pprlast; // The flags, ercfxBoundBox and ptfxCurrent fields should be the same for // the widened result as they were for the spine, so don't copy them. ppath->flags |= (PD_BEGINSUBPATH | PD_ENDSUBPATH); fl &= ~(PO_BEZIERS | PO_ELLIPSE); // Now delete the wpath path object: wpath.ppath->ppachain = (PATHALLOC*) NULL; wpath.vDelete(); } /******************************Public*Routine******************************\ * WIDENER::vMakeItWide(epo) * * Replaces the path 'epo' with its widened result. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ INLINE VOID WIDENER::vMakeItWide(EPATHOBJ& epo) { epo.vBecome(wpathRight); // Recompute the total number of lines in the path: epo.cCurves = epo.cTotalCurves(); } /******************************Public*Routine******************************\ * WIDENER::WIDENER(epo, exoWtoD, pla) * * Constructor for the widener. * * Coming in to here, we expect bounds checking to be already done via * bComputeWidenedBounds(), so that we don't have to worry about over- * flowing outside the 31 bit device space. * * History: * 23-Oct-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ WIDENER::WIDENER( EPATHOBJ& epo, EXFORMOBJ& exoWtoD, PLINEATTRS pla) : STYLER(epo, pla) { ASSERTGDI(pla->fl & LA_GEOMETRIC, "Not a geometric line"); ASSERTGDI(epo.bValid(), "Invalid path"); ASSERTGDI(exoWtoD.bValid(), "Invalid xform"); // Bail out here if we failed to allocate any objects. The caller must also // call WIDENER::bValid() immediately after invoking the WIDENER constructor: if (!bValid()) return; iEndCap = pla->iEndCap; iJoin = pla->iJoin; // Set 'AllRound' flag if we don't have to ever worry about perpendiculars // looking funny: vAllRound((iJoin == JOIN_ROUND) && (iEndCap == ENDCAP_ROUND || iEndCap == ENDCAP_SQUARE)); EFLOATEXT efWidth(pla->elWidth.e); LONG lWidth; BOOL bRet; bRet = efWidth.bEfToL(lWidth); ASSERTGDI(bRet, "Unexpected width overflow"); efHalfWidth = efWidth; efHalfWidth.vDivBy2(); // Set up for miter limit: if (iJoin == JOIN_MITER) { // Compute (PenWidth * MiterLimit / 2) ^ 2: efHalfWidthMiterLimitSquared = pla->eMiterLimit; efHalfWidthMiterLimitSquared *= efHalfWidth; // *= operator probably can't handle dest and src being the same: EFLOAT efTemp = efHalfWidthMiterLimitSquared; efHalfWidthMiterLimitSquared *= efTemp; } // Current transform has to be invertible for doing styling or mitering // (miter limit is specified in world space). Square caps too. if (pla->pstyle != (PFLOAT_LONG) NULL || iEndCap == ENDCAP_SQUARE || iJoin == JOIN_MITER) { // Compute inverse of the current transform: if (exoDeviceToWorld.bInverse(exoWtoD)) { // Ensure that every vector between every point in the path // could be transformed to world space: EVECTORFX avec[2]; avec[0].x = epo.rcfxBoundBox().xRight - epo.rcfxBoundBox().xLeft; avec[0].y = epo.rcfxBoundBox().yBottom - epo.rcfxBoundBox().yTop; avec[1].x = -avec[0].x; avec[1].y = avec[0].y; if (!exoDeviceToWorld.bXform(avec, (PVECTORL) avec, 2)) vSetError(); } else { SAVE_ERROR_CODE(ERROR_ARITHMETIC_OVERFLOW); vSetError(); } } if (!bValid()) return; // Do most of the work: if (!wpen.bPolygonizePen(exoWtoD, lWidth) || !bWiden()) { vSetError(); } } /******************************Public*Routine******************************\ * EPATHOBJ::bWiden(epoSpine, exo, pla) * * Widens the specified path, overwriting the results of '*this'. * * History: * 3-Feb-1992 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL EPATHOBJ::bWiden(EPATHOBJ& epoSpine, XFORMOBJ* pxo, LINEATTRS* pla) { WIDENER wid(epoSpine, *((EXFORMOBJ*) pxo), pla); BOOL bValid = wid.bValid(); if (bValid) wid.vMakeItWide(*this); return(bValid); } /******************************Public*Routine******************************\ * EPATHOBJ::bComputeWidenedBounds(exo, pla) * * Adjusts the bounds of the path to allow for joins for geometric * wide-lines. We do this by transforming the bound box of the pen * to device space and taking the corners as the extrema of the joins. * * For Miter joins, this is an even rougher guess since we assume we'll * have the longest possible join allowed by the Miter limit. * * The values returned are the upper bounds if the path is to be widened * (which is fine for pointer exclusion). * * FALSE is returned if any part of the widened result might fall outside * the 31 bit device space. * * History: * 3-Feb-1992 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ BOOL EPATHOBJ::bComputeWidenedBounds( EPATHOBJ& epoSpine, XFORMOBJ *pexo, LINEATTRS *pla) { ASSERTGDI(epoSpine.bValid(), "Not valid path"); ASSERTGDI(bValid(), "Not valid path"); ASSERTGDI(pla->fl & LA_GEOMETRIC, "Not geometric line"); EFLOATEXT efWidth(pla->elWidth.e); LONG lWidth; BOOL b = efWidth.bEfToL(lWidth); ASSERTGDI(b, "Width too large"); ASSERTGDI(lWidth >= 0, "Negative pen radius"); VECTORL avecl[2]; // We use the diameter and not the radius of the pen here because halving // it in world space as an integer could possibly lose a lot of precision, // and it's faster to call 'bXform' with an integer vector than an EFLOAT // one: avecl[0].x = lWidth; avecl[0].y = lWidth; avecl[1].x = lWidth; avecl[1].y = -lWidth; if (!((EXFORMOBJ*) pexo)->bXform(avecl, (PVECTORFX) avecl, 2)) return(FALSE); // Chop in half to get the maximums of the pen (we were using the diameter // instead of the radius, remember?). Add a pixel of slop too (remembering // that there is rounding error, and vAddNice can increase the dimensions // of the pen slightly): LONG xAdjust = (MAX(ABS(avecl[0].x), ABS(avecl[1].x)) >> 1) + LTOFX(1); LONG yAdjust = (MAX(ABS(avecl[0].y), ABS(avecl[1].y)) >> 1) + LTOFX(1); // Account for square caps by multiplying by a coarse approximation to the // square root of 2: if (pla->iEndCap == ENDCAP_SQUARE) { xAdjust += (xAdjust >> 1); yAdjust += (yAdjust >> 1); // Watch for overflow: if (!bIs31Bits(xAdjust) || !bIs31Bits(yAdjust)) return(FALSE); } // We know here that xAdjust and yAdjust have at most 31 bits significance. // (Ok, so maybe we overflowed by adding 1). if (pla->iJoin == JOIN_MITER) { EFLOATEXT efMiterLimit(pla->eMiterLimit); ASSERTGDI(FP_1_0 <= efMiterLimit, "Miter limit less than one"); EFLOATEXT ef(xAdjust); ef *= efMiterLimit; if (!ef.bEfToL(xAdjust)) return(FALSE); ef = yAdjust; ef *= efMiterLimit; if (!ef.bEfToL(yAdjust)) return(FALSE); if (!bIs31Bits(xAdjust) || !bIs31Bits(yAdjust)) return(FALSE); } PATH *ppNew = ppath; PATH *ppOld = epoSpine.ppath; LONG xLeft = ppOld->rcfxBoundBox.xLeft; LONG xRight = ppOld->rcfxBoundBox.xRight; LONG yTop = ppOld->rcfxBoundBox.yTop; LONG yBottom = ppOld->rcfxBoundBox.yBottom; // Our widen code expects the coordinates to fix in a 27.4 space in order // not to overflow (as guaranteed by the DDI spec, but not guaranteed by // the code): if (!bIs31Bits(xLeft) || !bIs31Bits(xRight) || !bIs31Bits(yTop) || !bIs31Bits(yBottom)) { return(FALSE); } // Copy the bounds from the 'old' spine to the 'new' widened result, // accounting for the resulting increase in size: xLeft -= xAdjust; xRight += xAdjust; yTop -= yAdjust; yBottom += yAdjust; // Make sure we the widened result doesn't fall outside the 31 bit space: if (!bIs31Bits(xLeft) || !bIs31Bits(xRight) || !bIs31Bits(yTop) || !bIs31Bits(yBottom)) { return(FALSE); } ppNew->rcfxBoundBox.xLeft = xLeft; ppNew->rcfxBoundBox.xRight = xRight; ppNew->rcfxBoundBox.yTop = yTop; ppNew->rcfxBoundBox.yBottom = yBottom; return(TRUE); } /******************************Public*Routine******************************\ * EPATHOBJ::vReComputeBounds() * * Updates the bounding rectangle, based on reading all the points in * the path. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID EPATHOBJ::vReComputeBounds() { ASSERTGDI(bValid(), "ReComputeWidenedBounds() in invalid path"); PPATHREC ppr; if (ppath != (PPATH) NULL) { if (ppath->pprfirst != (PPATHREC) NULL) { ASSERTGDI(ppath->pprfirst->count > 0, "Shouldn't have empty pathrec"); ppath->rcfxBoundBox.xLeft = ppath->pprfirst->aptfx->x; ppath->rcfxBoundBox.yTop = ppath->pprfirst->aptfx->y; ppath->rcfxBoundBox.xRight = ppath->rcfxBoundBox.xLeft; ppath->rcfxBoundBox.yBottom = ppath->rcfxBoundBox.yTop; for (ppr = ppath->pprfirst; ppr != (PPATHREC) NULL; ppr = ppr->pprnext) { PPOINTFIX pptfx = &ppr->aptfx[0]; PPOINTFIX pptfxEnd = &ppr->aptfx[ppr->count]; while (pptfx < pptfxEnd) ((ERECTFX*) &ppath->rcfxBoundBox)->vInclude(*pptfx++); } } else { ppath->rcfxBoundBox.xLeft = 0; ppath->rcfxBoundBox.yTop = 0; ppath->rcfxBoundBox.xRight = 0; ppath->rcfxBoundBox.yBottom = 0; } } } /******************************Public*Routine******************************\ * EPATHOBJ::vWidenSetupForFrameRgn(dco, cxPen, cyPen, pexo, pla) * * Initializes a LINEATTRS and EXFORMOBJ to be passed to bWiden to widen * the path appropriately. Used only for FrameRgn. * * The FrameRgn pen is elliptical in world space, whereas the Widen code * expects it to be circular. So we simply change the transform here. * * The FrameRgn ellipse may also have zero-length axis (unlike normal * widened paths), so a flag must be set for that. * * History: * 12-Sep-1991 -by- J. Andrew Goossen [andrewgo] * Wrote it. \**************************************************************************/ VOID EPATHOBJ::vWidenSetupForFrameRgn( XDCOBJ& dco, // Used to get current world transform LONG cxPen, // x dimension of pen in world space LONG cyPen, // y dimension of pen EXFORMOBJ* pexo, // Must be initialized via 'vInit(&mx, DONT_COMPUTE_FLAGS)' LINEATTRS* pla) { ASSERTGDI(pexo->bValid(), "Xform must be initialized"); // FrameRgn can have zero-width or zero-height brush dimensions: pla->fl = LA_GEOMETRIC | LA_ALLOW_ZERO_DIMENSIONS; pla->iJoin = JOIN_MITER; pla->iEndCap = ENDCAP_ROUND; pla->eMiterLimit = dco.pdc->l_eMiterLimit(); pla->pstyle = (FLOAT_LONG*) NULL; pla->cstyle = 0; // We need to double the pen dimensions because half of the pen // will draw outside the region, and will be clipped away: ASSERTGDI(cxPen >= 0 && cyPen >= 0, "Illegal pen dimension"); cxPen *= 2; cyPen *= 2; // Make our calculations a little more numerically stable: BOOL bAdjustX = FALSE; if (cxPen < cyPen) { register LONG lTmp; SWAPL(cxPen, cyPen, lTmp); bAdjustX = !bAdjustX; } // Figure out the y-value normalizing factor that will make a square // of dimensions [cxPen, cxPen] become a rectangle [cxPen, cyPen]: ASSERTGDI(cxPen >= cyPen, ""); EFLOATEXT efX(cxPen); efX.vEfToF(pla->elWidth.e); EFLOATEXT efNormalize(cyPen); if (!efX.bIsZero()) efNormalize /= efX; // Now copy the world transform and modify it: pexo->vSet(&dco.pdc->mxWorldToDevice()); if (bAdjustX) { pexo->efM11() *= efNormalize; pexo->efM12() *= efNormalize; } else { pexo->efM21() *= efNormalize; pexo->efM22() *= efNormalize; } pexo->vComputeAccelFlags(); }