|
|
/******************************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(); }
|