// Toonz components includes #include "tcurveutil.h" #include "tinterval.h" #include "tellipticbrushP.h" #include "tstrokeoutline.h" using namespace tellipticbrush; //******************************************************************************** // EXPLANATION //******************************************************************************** /*! \file tellipticbrush.cpp This code deals with the "outlinization" process of a TStroke instance. The process of extracting the outline of a thick stroke can be resumed in 2 main steps: 1. Discretize the stroke centerline in the most appropriate centerline points, extracting infos about position and left/right derivatives at each. 2. Build the outline points associated to each individual centerline point; eventually including additional junction points and caps. The first major step has some sub-routines worth noting: 1.1 Isolate regions of the stroke where the thickness speed is greater than the gemoetrical speed of the centerline. These points are 'self-covered' by their immediate neighbourhood, and thus cannot be seen - or build outline directions. 1.2 Some procedural style need to sample the centerline at a given length step. 1.3 The centerline should be sampled so that the resulting polygonal outline approximation is tightly close to the theoretical outline, up to an error bound. The recursive approach is the simplest to deal with this issue. The second step implements different outline styles to extrude the centerline points. */ //******************************************************************************** // Geometric Helper Functions //******************************************************************************** //! Returns the distance between two points double tellipticbrush::dist(const TPointD &P1, const TPointD &P2) { return norm(P2 - P1); } //------------------------------------------------------------ //! Returns the distance between two points double tellipticbrush::dist(const TThickPoint &P1, const TThickPoint &P2) { return norm(P2 - P1); } //------------------------------------------------------------ //! Returns the angle between (unnormalized) vectors v1 and v2 double tellipticbrush::angle(const TPointD &v1, const TPointD &v2) { TPointD d1(v1 * (1.0 / norm(v1))), d2(v2 * (1.0 / norm(v2))); return atan2(cross(v1, v2), v1 * v2); } //------------------------------------------------------------ /*! Returns the intersection between two lines in the form of \b coordinates from a pair of the lines' starting points. Passed directions must have norm 1. If the system's determinant modulus is under the specified tolerance parameter, TConsts::napd is returned. */ TPointD tellipticbrush::intersectionCoords(const TPointD &P0, const TPointD &d0, const TPointD &P1, const TPointD &d1, double detTol) { // Solve P0 + x * d0 == P1 + y * d1 double det = d0.y * d1.x - d0.x * d1.y; if (fabs(det) < detTol) return TConsts::napd; TPointD P1_P0(P1 - P0); return TPointD((d1.x * P1_P0.y - d1.y * P1_P0.x) / det, (d0.x * P1_P0.y - d0.y * P1_P0.x) / det); } //------------------------------------------------------------ /*! Returns the left or right envelope direction of centerline point p against thick direction d. */ void tellipticbrush::buildEnvelopeDirection(const TThickPoint &p, const TThickPoint &d, bool left, TPointD &res) { double dNorm2 = sq(d.x) + sq(d.y); double a = -d.thick / dNorm2; double b = sqrt(dNorm2 - sq(d.thick)) / dNorm2; TPointD n(left ? TPointD(-d.y, d.x) : TPointD(d.y, -d.x)); res = a * TPointD(d.x, d.y) + b * n; } //------------------------------------------------------------ void tellipticbrush::buildEnvelopeDirections(const TThickPoint &p, const TThickPoint &d, TPointD &resL, TPointD &resR) { double dNorm2 = sq(d.x) + sq(d.y); double a = -d.thick / dNorm2; double b = sqrt(dNorm2 - sq(d.thick)) / dNorm2; TPointD n(-d.y, d.x); resL = a * TPointD(d.x, d.y) + b * n; resR = a * TPointD(d.x, d.y) - b * n; } //------------------------------------------------------------ /*! Extrudes centerline point p against thick direction d, returning its left or right envelope displacement vector. */ void tellipticbrush::buildEnvelopeVector(const TThickPoint &p, const TThickPoint &d, bool left, TPointD &res) { buildEnvelopeDirection(p, d, left, res); res.x = p.thick * res.x; res.y = p.thick * res.y; } //------------------------------------------------------------ void tellipticbrush::buildEnvelopeVectors(const TThickPoint &p, const TThickPoint &d, TPointD &resL, TPointD &resR) { buildEnvelopeDirections(p, d, resL, resR); resL.x = p.thick * resL.x; resL.y = p.thick * resL.y; resR.x = p.thick * resR.x; resR.y = p.thick * resR.y; } //------------------------------------------------------------ /*! Builds the angle that supports a *quality* discretization of the circle with maximal error < m_pixSize. */ void tellipticbrush::buildAngularSubdivision(double radius, double angle, double err, int &nAngles) { /* See "Graphic Gems", page 600. NOTE: maxAngle is not multiplied by 2.0 as the naive pythagorical argument would pretend. The 2.0 holds if we want to find the angle at which the distance of the circle from its approximation is always < error. But we want MORE. We want that to happen against the distance from EVERY TANGENT LINE of the arc - not the arc itself. This is coherent with the assumption that pixels orientation is not known. It's easy to see that maxAngle just has to be not multiplied by 2. */ double maxAngle = acos(1.0 - err / radius); //* 2.0; nAngles = tceil(fabs(angle) / maxAngle); } //------------------------------------------------------------ TRectD tellipticbrush::computeBBox(const TStroke &stroke) { TRectD bbox; int i, n = stroke.getChunkCount(); for (i = 0; i < n; i++) bbox += stroke.getChunk(i)->getBBox(); return bbox; } //******************************************************************************** // CenterlinePoint implementation //******************************************************************************** void tellipticbrush::CenterlinePoint::buildPos(const TStroke &stroke) { if (m_posBuilt) return; m_p = stroke.getChunk(m_chunkIdx)->getThickPoint(m_t); m_posBuilt = true; } //------------------------------------------------------------ void tellipticbrush::CenterlinePoint::buildDirs(const TStroke &stroke) { if (m_dirsBuilt) return; int chunkPrev, chunkNext; double tPrev, tNext; bool coveredPrev, coveredNext; // Discriminate the boundary cases bool quadBoundary; if (m_t == 0.0) { quadBoundary = true; chunkPrev = m_chunkIdx - 1, chunkNext = m_chunkIdx; tPrev = 1.0, tNext = 0.0; } else if (m_t == 1.0) { quadBoundary = true; chunkPrev = m_chunkIdx, chunkNext = m_chunkIdx + 1; tPrev = 1.0, tNext = 0.0; } else { quadBoundary = false; chunkPrev = chunkNext = m_chunkIdx; tPrev = tNext = m_t; } // Build the backward direction if (chunkPrev >= 0) { const TThickQuadratic *ttqPrev = stroke.getChunk(chunkPrev); const TThickPoint &P0 = ttqPrev->getThickP0(); const TThickPoint &P1 = ttqPrev->getThickP1(); const TThickPoint &P2 = ttqPrev->getThickP2(); if (quadBoundary && (P1 == P2)) m_prevD = P2 - P0; // Toonz 'Linear' CPs. Eliminating a perilous // singularity this way. else { m_prevD.x = 2.0 * ((P1.x - P0.x) + tPrev * (P0.x - 2.0 * P1.x + P2.x)); m_prevD.y = 2.0 * ((P1.y - P0.y) + tPrev * (P0.y - 2.0 * P1.y + P2.y)); m_prevD.thick = 2.0 * ((P1.thick - P0.thick) + tPrev * (P0.thick - 2.0 * P1.thick + P2.thick)); } // Points whose thickness derivative does exceeds the point speed // cannot project envelope directions for that direction. This needs to be // known. coveredPrev = (sq(m_prevD.x) + sq(m_prevD.y) < sq(m_prevD.thick) + tolPar); // Accept only uncovered derivatives m_hasPrevD = !coveredPrev; } else { m_hasPrevD = false; coveredPrev = true; // ie prev coverage must not affect next coverage m_prevD = TConsts::natp; } // Build the forward direction if (chunkPrev == chunkNext) { // If the quadratic is the same, no need to derive it twice m_hasNextD = m_hasPrevD; m_nextD = m_prevD; coveredNext = coveredPrev; } else if (chunkNext < stroke.getChunkCount()) { const TThickQuadratic *ttqNext = stroke.getChunk(chunkNext); const TThickPoint &P0 = ttqNext->getThickP0(); const TThickPoint &P1 = ttqNext->getThickP1(); const TThickPoint &P2 = ttqNext->getThickP2(); if (quadBoundary && (P0 == P1)) m_nextD = P2 - P0; else { m_nextD.x = 2.0 * ((P1.x - P0.x) + tNext * (P0.x - 2.0 * P1.x + P2.x)); m_nextD.y = 2.0 * ((P1.y - P0.y) + tNext * (P0.y - 2.0 * P1.y + P2.y)); m_nextD.thick = 2.0 * ((P1.thick - P0.thick) + tNext * (P0.thick - 2.0 * P1.thick + P2.thick)); } coveredNext = (sq(m_nextD.x) + sq(m_nextD.y) < sq(m_nextD.thick) + tolPar); m_hasNextD = !coveredNext; } else { m_hasNextD = false; coveredNext = true; // ie prev coverage must not affect next coverage m_nextD = TConsts::natp; } m_covered = (coveredPrev && coveredNext); m_dirsBuilt = true; } //******************************************************************************** // Specialized Linearizator for common stroke drawing //******************************************************************************** namespace { class LengthLinearizator final : public tellipticbrush::StrokeLinearizator { double m_lengthStep; int m_countIdx; public: LengthLinearizator(const TStroke *stroke, double lengthStep) : StrokeLinearizator(stroke), m_lengthStep(lengthStep), m_countIdx(0) {} void linearize(std::vector &cPoints, int chunk) override; }; //-------------------------------------------------------------------------------------------- void LengthLinearizator::linearize(std::vector &cPoints, int chunk) { if (m_lengthStep == 0.0) return; // Retrieve the stroke length at stroke start double startW = this->m_stroke->getW(chunk, 0.0); double startLength = this->m_stroke->getLength(startW); // Retrieve the quadratic's end length const TThickQuadratic *ttq = this->m_stroke->getChunk(chunk); double endLength = startLength + ttq->getLength(); // Build the step-length inside the chunk int n = tceil(startLength / m_lengthStep); double length; double t, w; int chk; for (length = n * m_lengthStep; length < endLength; length += m_lengthStep) { // Retrieve the new params at length. Need to use the sloppy TStroke // interface, // unfortunately... w = this->m_stroke->getParameterAtLength(length); // WARNING: TStroke's interface is COMPLETELY WRONG about what gets returned // by the following function. This is just *CRAZY* - however, let's take it // all right... bool ok = !this->m_stroke->getChunkAndT(w, chk, t); // In case something goes wrong, skip if (!ok || chk != chunk) continue; // Store the param, that NEEDS TO BE INCREMENTALLY COUNTED - as length // linearization // is typically used for special procedural vector styles that need this // info. CenterlinePoint cPoint(chk, t); cPoint.m_countIdx = m_countIdx += 2; //++m_countIdx; cPoints.push_back(cPoint); } } //============================================================================================ class RecursiveLinearizator final : public tellipticbrush::StrokeLinearizator { double m_pixSize; public: RecursiveLinearizator(const TStroke *stroke, double pixSize) : StrokeLinearizator(stroke), m_pixSize(pixSize) {} void linearize(std::vector &cPoints, int chunk) override; void subdivide(std::vector &cPoints, CenterlinePoint &cp0, CenterlinePoint &cp1); }; //-------------------------------------------------------------------------------------------- void RecursiveLinearizator::linearize(std::vector &cPoints, int chunk) { /* Recursively linearizes the centerline, in the following way: Take one point, together with the next. Add a point in the middle interval, until the next thick point is included (up to pixSize) in the 'forward-cast' envelope of current one. If the midpoint was added, repeat on the 2 sub-intervals. */ const TStroke &stroke = *this->m_stroke; const TThickQuadratic &ttq = *stroke.getChunk(chunk); // Sort the interval (SHOULD BE DONE OUTSIDE?) std::sort(cPoints.begin(), cPoints.end()); std::vector addedPoints; unsigned int i, size_1 = cPoints.size() - 1; for (i = 0; i < size_1; ++i) { cPoints[i].buildPos(stroke); cPoints[i].buildDirs(stroke); cPoints[i + 1].buildPos(stroke); cPoints[i + 1].buildDirs(stroke); subdivide(addedPoints, cPoints[i], cPoints[i + 1]); } cPoints[size_1].buildPos(stroke); cPoints[size_1].buildDirs(stroke); CenterlinePoint cpEnd(chunk, 1.0); { const TThickPoint &P1(ttq.getThickP1()); cpEnd.m_p = ttq.getThickP2(); cpEnd.m_prevD = TThickPoint(2.0 * (cpEnd.m_p.x - P1.x), 2.0 * (cpEnd.m_p.y - P1.y), 2.0 * (cpEnd.m_p.thick - P1.thick)); cpEnd.m_hasPrevD = true; // The effective false case should already be dealt by sqrt... } subdivide(addedPoints, cPoints[size_1], cpEnd); cPoints.insert(cPoints.end(), addedPoints.begin(), addedPoints.end()); } //-------------------------------------------------------------------------------------------- void RecursiveLinearizator::subdivide(std::vector &cPoints, CenterlinePoint &cp0, CenterlinePoint &cp1) { if (!(cp0.m_hasNextD && cp1.m_hasPrevD)) return; // Build the distance of next from the outline of cp's 'envelope extension' TPointD envDirL0, envDirR0, envDirL1, envDirR1; buildEnvelopeDirections(cp0.m_p, cp0.m_nextD, envDirL0, envDirR0); buildEnvelopeDirections(cp1.m_p, cp1.m_prevD, envDirL1, envDirR1); TPointD diff(convert(cp1.m_p) - convert(cp0.m_p)); double d = std::max(fabs(envDirL0 * (diff + cp1.m_p.thick * envDirL1 - cp0.m_p.thick * envDirL0)), fabs(envDirR0 * (diff + cp1.m_p.thick * envDirR1 - cp0.m_p.thick * envDirR0))); if (d > m_pixSize && cp1.m_t - cp0.m_t > 1e-4) { double midT = 0.5 * (cp0.m_t + cp1.m_t); CenterlinePoint midPoint(cp0.m_chunkIdx, midT); midPoint.buildPos(*this->m_stroke); midPoint.buildDirs(*this->m_stroke); subdivide(cPoints, cp0, midPoint); subdivide(cPoints, midPoint, cp1); cPoints.push_back(midPoint); } } //============================================================================================ class CoverageLinearizator final : public tellipticbrush::StrokeLinearizator { public: CoverageLinearizator(const TStroke *stroke) : StrokeLinearizator(stroke) {} void linearize(std::vector &cPoints, int chunk) override; }; //-------------------------------------------------------------------------------------------- void CoverageLinearizator::linearize(std::vector &cPoints, int chunk) { // Retrieve the at max 2 parameters for which: // sq(d.x) + sq(d.y) == sq(d.thick) + tolPar(*) (ie, "self-coverage" // critical points) // It can be rewritten in the canonical form: at^2 + bt + c == 0 const TThickQuadratic &ttq(*this->m_stroke->getChunk(chunk)); TThickPoint P0(ttq.getThickP0()), P1(ttq.getThickP1()), P2(ttq.getThickP2()); if ((P0 == P1) || (P1 == P2)) return; // Linear speed out/in case. Straightened up in the buildDirs() // Remember that d = 2 [P1 - P0 + t (P0 + P2 - 2 P1)] T3DPointD u(P1.x - P0.x, P1.y - P0.y, P1.thick - P0.thick); T3DPointD v(P0.x + P2.x - 2.0 * P1.x, P0.y + P2.y - 2.0 * P1.y, P0.thick + P2.thick - 2.0 * P1.thick); double a = sq(v.x) + sq(v.y) - sq(v.z); if (fabs(a) < 1e-4) return; // Little (acceleration) quadratics case //(*) Build tolerance - 2.0 since tolPar is already used to discriminate //'good' dirs. Ours must be. const double twiceTolPar = 2.0 * tolPar; double b = 2.0 * (u.x * v.x + u.y * v.y - u.z * v.z); double c = sq(u.x) + sq(u.y) - sq(u.z) - twiceTolPar; double delta = sq(b) - 4.0 * a * c; if (delta < 0) return; double sqrtDelta = sqrt(delta); double t0 = (-b - sqrtDelta) / (2.0 * a); double t1 = (-b + sqrtDelta) / (2.0 * a); if (t0 > 0 && t0 < 1) { CenterlinePoint cp(chunk, t0); cp.buildPos(*this->m_stroke); cp.buildDirs(*this->m_stroke); cp.m_hasNextD = false; cPoints.push_back(cp); } if (t1 > 0 && t1 < 1) { CenterlinePoint cp(chunk, t1); cp.buildPos(*this->m_stroke); cp.buildDirs(*this->m_stroke); cp.m_hasPrevD = false; cPoints.push_back(cp); } } } // namespace //******************************************************************************** // Outline Builder implementation //******************************************************************************** tellipticbrush::OutlineBuilder::OutlineBuilder(const OutlinizationData &data, const TStroke &stroke) : m_pixSize(data.m_pixSize) , m_oOptions(stroke.outlineOptions()) , m_lastChunk(stroke.getChunkCount() - 1) { typedef TStroke::OutlineOptions OutlineOptions; switch (m_oOptions.m_capStyle) { case OutlineOptions::PROJECTING_CAP: { m_addBeginCap = &OutlineBuilder::addProjectingBeginCap>; m_addEndCap = &OutlineBuilder::addProjectingEndCap>; m_addBeginCap_ext = &OutlineBuilder::addProjectingBeginCap; m_addEndCap_ext = &OutlineBuilder::addProjectingEndCap; break; } case OutlineOptions::BUTT_CAP: { m_addBeginCap = &OutlineBuilder::addButtBeginCap; m_addEndCap = &OutlineBuilder::addButtEndCap; m_addBeginCap_ext = 0; m_addEndCap_ext = 0; break; } case OutlineOptions::ROUND_CAP: default: m_addBeginCap = &OutlineBuilder::addRoundBeginCap; m_addEndCap = &OutlineBuilder::addRoundEndCap; m_addBeginCap_ext = 0; m_addEndCap_ext = 0; }; switch (m_oOptions.m_joinStyle) { case OutlineOptions::MITER_JOIN: { m_addSideCaps = &OutlineBuilder::addMiterSideCaps>; m_addSideCaps_ext = &OutlineBuilder::addMiterSideCaps; break; } case OutlineOptions::BEVEL_JOIN: { m_addSideCaps = &OutlineBuilder::addBevelSideCaps; m_addSideCaps_ext = 0; break; } case OutlineOptions::ROUND_JOIN: default: m_addSideCaps = &OutlineBuilder::addRoundSideCaps; m_addSideCaps_ext = 0; }; } //------------------------------------------------------------ /*! Translates a CenterlinePoint instance into OutlinePoints, and adds them to the supplied vector container. */ void tellipticbrush::OutlineBuilder::buildOutlinePoints( std::vector &oPoints, const CenterlinePoint &cPoint) { // If the centerline directions exist and match, just add their envelope // displacement directly if (cPoint.m_hasPrevD && cPoint.m_hasNextD && cPoint.m_prevD == cPoint.m_nextD) { TPointD leftD, rightD; buildEnvelopeVector(cPoint.m_p, cPoint.m_prevD, true, leftD); buildEnvelopeVector(cPoint.m_p, cPoint.m_prevD, false, rightD); oPoints.push_back( TOutlinePoint(convert(cPoint.m_p) + rightD, cPoint.m_countIdx)); oPoints.push_back( TOutlinePoint(convert(cPoint.m_p) + leftD, cPoint.m_countIdx)); } else { // We have to add caps/joins together with the envelope displacements // Caps which are not at stroke ends are always imposed to be round. if (cPoint.m_hasPrevD) { if (cPoint.m_hasNextD) (this->*m_addSideCaps)(oPoints, cPoint); else if (cPoint.m_chunkIdx == m_lastChunk && cPoint.m_t == 1.0) (this->*m_addEndCap)(oPoints, cPoint); else addRoundEndCap(oPoints, cPoint); } else { if (cPoint.m_hasNextD) if (cPoint.m_chunkIdx == 0 && cPoint.m_t == 0.0) (this->*m_addBeginCap)(oPoints, cPoint); else addRoundBeginCap(oPoints, cPoint); else addCircle(oPoints, cPoint); } } } //------------------------------------------------------------ /*! Translates a CenterlinePoint instance into bounding box points, and adds them to the supplied (bbox) rect. */ void tellipticbrush::OutlineBuilder::buildOutlineExtensions( TRectD &bbox, const CenterlinePoint &cPoint) { if (!(cPoint.m_hasPrevD && cPoint.m_hasNextD && cPoint.m_prevD == cPoint.m_nextD)) { // Only non-envelope points are interesting to the bbox builder procedure if (cPoint.m_hasPrevD) { if (cPoint.m_hasNextD && m_addSideCaps_ext) (this->*m_addSideCaps_ext)(bbox, cPoint); else if (cPoint.m_chunkIdx == m_lastChunk && cPoint.m_t == 1.0 && m_addEndCap_ext) (this->*m_addEndCap_ext)(bbox, cPoint); } else { if (cPoint.m_hasNextD) if (cPoint.m_chunkIdx == 0 && cPoint.m_t == 0.0 && m_addBeginCap_ext) (this->*m_addBeginCap_ext)(bbox, cPoint); } } } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addCircularArcPoints( int idx, std::vector &outPoints, const TPointD ¢er, const TPointD &ray, double angle, int nAngles, int countIdx) { TPointD rotRay(ray); // Push the initial point without rotation outPoints[idx] = TOutlinePoint(center + ray, countIdx); idx += 2; // Build the rotation double sin_a = sin(angle); // NOTE: The 'angle' input parameter CANNOT be // substituted with just cos, double cos_a = cos(angle); // while sin = sqrt(1.0 - sq(cos)), BECAUSE this // way sin is ALWAYS > 0 int i; for (i = 1; i <= nAngles; ++i, idx += 2) { rotRay = TPointD(rotRay.x * cos_a - rotRay.y * sin_a, rotRay.x * sin_a + rotRay.y * cos_a); outPoints[idx] = center + rotRay; } } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addCircle( std::vector &oPoints, const CenterlinePoint &cPoint) { // Build the angle step for (0, pi) int nAngles; double stepAngle, totAngle = angle(TPointD(1.0, 0.0), TPointD(-1.0, 0.0)); buildAngularSubdivision(cPoint.m_p.thick, totAngle, m_pixSize, nAngles); stepAngle = totAngle / (double)nAngles; // Resize the vector to store the required points int idx = oPoints.size(); oPoints.resize(oPoints.size() + 2 * (nAngles + 1), TOutlinePoint(TPointD())); // Add the circle points from each semi-circle addCircularArcPoints(idx, oPoints, convert(cPoint.m_p), TPointD(cPoint.m_p.thick, 0.0), -stepAngle, nAngles, cPoint.m_countIdx); addCircularArcPoints(idx + 1, oPoints, convert(cPoint.m_p), TPointD(cPoint.m_p.thick, 0.0), stepAngle, nAngles, cPoint.m_countIdx); } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addRoundBeginCap( std::vector &oPoints, const CenterlinePoint &cPoint) { TPointD rightD; buildEnvelopeVector(cPoint.m_p, cPoint.m_nextD, false, rightD); TPointD beginD(-convert(cPoint.m_nextD)); beginD = (cPoint.m_p.thick / norm(beginD)) * beginD; int nAngles; double stepAngle, totAngle = angle(beginD, rightD); buildAngularSubdivision(cPoint.m_p.thick, totAngle, m_pixSize, nAngles); stepAngle = totAngle / (double)nAngles; int idx = oPoints.size(); oPoints.resize(oPoints.size() + 2 * (nAngles + 1), TOutlinePoint(TPointD())); addCircularArcPoints(idx, oPoints, convert(cPoint.m_p), beginD, stepAngle, nAngles, cPoint.m_countIdx); addCircularArcPoints(idx + 1, oPoints, convert(cPoint.m_p), beginD, -stepAngle, nAngles, cPoint.m_countIdx); // we just need to take the opposite // angle to deal with left side } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addRoundEndCap( std::vector &oPoints, const CenterlinePoint &cPoint) { // Build the backward envelope directions // Note that the situation is specular on the left and right side... TPointD leftD, rightD; buildEnvelopeVector(cPoint.m_p, cPoint.m_prevD, true, leftD); buildEnvelopeVector(cPoint.m_p, cPoint.m_prevD, false, rightD); int nAngles; double stepAngle, totAngle = angle(rightD, convert(cPoint.m_prevD)); buildAngularSubdivision(cPoint.m_p.thick, totAngle, m_pixSize, nAngles); stepAngle = totAngle / (double)nAngles; int idx = oPoints.size(); oPoints.resize(oPoints.size() + 2 * (nAngles + 1), TOutlinePoint(TPointD())); addCircularArcPoints(idx, oPoints, convert(cPoint.m_p), rightD, stepAngle, nAngles, cPoint.m_countIdx); addCircularArcPoints(idx + 1, oPoints, convert(cPoint.m_p), leftD, -stepAngle, nAngles, cPoint.m_countIdx); // we just need to take the // opposite angle to deal // with left side } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addButtBeginCap( std::vector &oPoints, const CenterlinePoint &cPoint) { // Just add the 2 basic envelope points TPointD leftDNext, rightDNext; buildEnvelopeVectors(cPoint.m_p, cPoint.m_nextD, leftDNext, rightDNext); // PLUS, add their midpoint, since it generates this part of stroke // antialias... TPointD leftP(convert(cPoint.m_p) + leftDNext), rightP(convert(cPoint.m_p) + rightDNext); TPointD midP(0.5 * (leftP + rightP)); oPoints.push_back(midP); oPoints.push_back(midP); oPoints.push_back(TOutlinePoint(rightP, cPoint.m_countIdx)); oPoints.push_back(TOutlinePoint(leftP, cPoint.m_countIdx)); } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addButtEndCap( std::vector &oPoints, const CenterlinePoint &cPoint) { TPointD leftDPrev, rightDPrev; buildEnvelopeVectors(cPoint.m_p, cPoint.m_prevD, leftDPrev, rightDPrev); TPointD leftP(convert(cPoint.m_p) + leftDPrev), rightP(convert(cPoint.m_p) + rightDPrev); TPointD midP(0.5 * (leftP + rightP)); oPoints.push_back( TOutlinePoint(convert(cPoint.m_p) + rightDPrev, cPoint.m_countIdx)); oPoints.push_back( TOutlinePoint(convert(cPoint.m_p) + leftDPrev, cPoint.m_countIdx)); oPoints.push_back(midP); oPoints.push_back(midP); } //------------------------------------------------------------ template void tellipticbrush::OutlineBuilder::addProjectingBeginCap( T &oPoints, const CenterlinePoint &cPoint) { double thick = cPoint.m_p.thick; // Find the base points TPointD leftDNext, rightDNext; buildEnvelopeDirections(cPoint.m_p, cPoint.m_nextD, leftDNext, rightDNext); TPointD leftP(convert(cPoint.m_p) + thick * leftDNext); TPointD rightP(convert(cPoint.m_p) + thick * rightDNext); // Add the intersections between the envelope directions' orthogonals and the // direction orthogonals TPointD dir(normalize(-cPoint.m_nextD)); TPointD dirP(convert(cPoint.m_p) + thick * dir); TPointD cornerLCoords = intersectionCoords( dirP, TPointD(dir.y, -dir.x), leftP, TPointD(-leftDNext.y, leftDNext.x)); TPointD cornerRCoords = intersectionCoords(dirP, TPointD(-dir.y, dir.x), rightP, TPointD(rightDNext.y, -rightDNext.x)); if (cornerLCoords.x < 0 || cornerRCoords.y < 0) return; // As before, midPoints must be added due to antialias TPointD cornerL(dirP + cornerLCoords.x * TPointD(dir.y, -dir.x)); TPointD cornerR(dirP + cornerRCoords.x * TPointD(-dir.y, dir.x)); TPointD midP(0.5 * (cornerL + cornerR)); addEnvelopePoint(oPoints, midP); addEnvelopePoint(oPoints, midP); addExtensionPoint(oPoints, cornerR); addExtensionPoint(oPoints, cornerL); // Initial points must be added later, in the begin case addEnvelopePoint(oPoints, rightP, cPoint.m_countIdx); addEnvelopePoint(oPoints, leftP, cPoint.m_countIdx); } //------------------------------------------------------------ template void tellipticbrush::OutlineBuilder::addProjectingEndCap( T &oPoints, const CenterlinePoint &cPoint) { double thick = cPoint.m_p.thick; // Add the base points TPointD leftDPrev, rightDPrev; buildEnvelopeDirections(cPoint.m_p, cPoint.m_prevD, leftDPrev, rightDPrev); TPointD leftP(convert(cPoint.m_p) + thick * leftDPrev); TPointD rightP(convert(cPoint.m_p) + thick * rightDPrev); addEnvelopePoint(oPoints, rightP, cPoint.m_countIdx); addEnvelopePoint(oPoints, leftP, cPoint.m_countIdx); // Add the intersections between the envelope directions' orthogonals and the // direction orthogonals TPointD dir(normalize(cPoint.m_prevD)); TPointD dirP(convert(cPoint.m_p) + thick * dir); TPointD cornerLCoords = intersectionCoords( dirP, TPointD(-dir.y, dir.x), leftP, TPointD(leftDPrev.y, -leftDPrev.x)); TPointD cornerRCoords = intersectionCoords(dirP, TPointD(dir.y, -dir.x), rightP, TPointD(-rightDPrev.y, rightDPrev.x)); if (cornerLCoords.x < 0 || cornerRCoords.y < 0) return; TPointD cornerL(dirP + cornerLCoords.x * TPointD(-dir.y, dir.x)); TPointD cornerR(dirP + cornerRCoords.x * TPointD(dir.y, -dir.x)); TPointD midP(0.5 * (cornerL + cornerR)); addExtensionPoint(oPoints, cornerR); addExtensionPoint(oPoints, cornerL); addEnvelopePoint(oPoints, midP); addEnvelopePoint(oPoints, midP); } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addRoundSideCaps( std::vector &oPoints, const CenterlinePoint &cPoint) { // Side caps - this has only sense when the backward and forward // direction-derivatives // are different. This means that they build different envelope directions. // So, we add // side caps to cover the 'elbow fractures' TPointD leftDPrev, leftDNext, rightDPrev, rightDNext; buildEnvelopeVector(cPoint.m_p, cPoint.m_prevD, true, leftDPrev); buildEnvelopeVector(cPoint.m_p, cPoint.m_nextD, true, leftDNext); buildEnvelopeVector(cPoint.m_p, cPoint.m_prevD, false, rightDPrev); buildEnvelopeVector(cPoint.m_p, cPoint.m_nextD, false, rightDNext); // This time, angle step is NOT specular int nAnglesL, nAnglesR; double totAngleL = angle(leftDPrev, leftDNext); double totAngleR = angle(rightDPrev, rightDNext); // The common case is that these angles have the same sign - thus building // opposites arcs of a circle if (tsign(totAngleL) != tsign(totAngleR)) { // However, there may be exceptions. We must still impose // the constraint about 'covering opposite arcs of a circle' - // it is necessary to make the outline look consistently filled. TPointD prevD(convert(cPoint.m_prevD)), nextD(convert(cPoint.m_nextD)); // The only dangerous case is when the directions are near-opposed if (prevD * nextD < 0) { // Here, we must make one angle its (sign-opposite) 2*pi complement. // Keep the angle with the least fabs (smallest 'butterfly intersection') if (fabs(totAngleL) < fabs(totAngleR)) totAngleR = (totAngleR > 0) ? totAngleR - M_2PI : totAngleR + M_2PI; else totAngleL = (totAngleL > 0) ? totAngleL - M_2PI : totAngleL + M_2PI; } } buildAngularSubdivision(cPoint.m_p.thick, totAngleL, m_pixSize, nAnglesL); buildAngularSubdivision(cPoint.m_p.thick, totAngleR, m_pixSize, nAnglesR); int nAngles = std::max(nAnglesL, nAnglesR); double stepAngleL = totAngleL / (double)nAngles; double stepAngleR = totAngleR / (double)nAngles; if (nAnglesL == 1 && nAnglesR == 1 && fabs(totAngleL) < 0.525 && fabs(totAngleR) < 0.525) // angle < 30 degrees { // Simple case oPoints.push_back( TOutlinePoint(convert(cPoint.m_p) + rightDPrev, cPoint.m_countIdx)); oPoints.push_back( TOutlinePoint(convert(cPoint.m_p) + leftDPrev, cPoint.m_countIdx)); } else { int idx = oPoints.size(); oPoints.resize(oPoints.size() + 2 * (nAngles + 1), TOutlinePoint(TPointD())); addCircularArcPoints(idx, oPoints, convert(cPoint.m_p), rightDPrev, stepAngleR, nAngles, cPoint.m_countIdx); addCircularArcPoints( idx + 1, oPoints, convert(cPoint.m_p), leftDPrev, stepAngleL, nAngles, cPoint.m_countIdx); // same angle here, as this is just a stroke // direction rotation } } //------------------------------------------------------------ void tellipticbrush::OutlineBuilder::addBevelSideCaps( std::vector &oPoints, const CenterlinePoint &cPoint) { // Build the envelope directions TPointD leftDPrev, leftDNext, rightDPrev, rightDNext; buildEnvelopeDirections(cPoint.m_p, cPoint.m_prevD, leftDPrev, rightDPrev); buildEnvelopeDirections(cPoint.m_p, cPoint.m_nextD, leftDNext, rightDNext); // Add at least 2 outline points (the prevs) oPoints.push_back(TOutlinePoint( convert(cPoint.m_p) + cPoint.m_p.thick * rightDPrev, cPoint.m_countIdx)); oPoints.push_back(TOutlinePoint( convert(cPoint.m_p) + cPoint.m_p.thick * leftDPrev, cPoint.m_countIdx)); // Only add the additional points when at least one of the envelope // differences // passing from prev to next is above the pixel size if (2.0 * cPoint.m_p.thick < m_pixSize) return; double threshold = sq(m_pixSize / cPoint.m_p.thick); double bevelSizeL = norm2(leftDNext - leftDPrev); double bevelSizeR = norm2(rightDNext - rightDPrev); if (bevelSizeL > threshold || bevelSizeR > threshold) { oPoints.push_back(convert(cPoint.m_p) + cPoint.m_p.thick * rightDNext); oPoints.push_back(convert(cPoint.m_p) + cPoint.m_p.thick * leftDNext); } } //------------------------------------------------------------ template void tellipticbrush::OutlineBuilder::addMiterSideCaps( T &oPoints, const CenterlinePoint &cPoint) { // Build the elbow side TPointD prevD(cPoint.m_prevD); prevD = (1.0 / norm(prevD)) * prevD; TPointD nextD(cPoint.m_nextD); nextD = (1.0 / norm(nextD)) * nextD; double cross = prevD.x * nextD.y - prevD.y * nextD.x; bool leftSide = (cross < 0); // ie elbow on the left side when turning right // Add the intersection point of the outline's tangential extensions //'Tangential extensions' are just the orthogonals to envelope directions // Build envelope directions TPointD envPrevSide, envNextSide; buildEnvelopeDirection(cPoint.m_p, cPoint.m_prevD, leftSide, envPrevSide); buildEnvelopeDirection(cPoint.m_p, cPoint.m_nextD, leftSide, envNextSide); // Build tangential directions TPointD prevTangentialD, nextTangentialD; if (leftSide) { prevTangentialD = TPointD(envPrevSide.y, -envPrevSide.x); nextTangentialD = TPointD(-envNextSide.y, envNextSide.x); } else { prevTangentialD = TPointD(-envPrevSide.y, envPrevSide.x); nextTangentialD = TPointD(envNextSide.y, -envNextSide.x); } // Build the outline points in the envelope directions envPrevSide = cPoint.m_p.thick * envPrevSide; envNextSide = cPoint.m_p.thick * envNextSide; TPointD p0(convert(cPoint.m_p) + envPrevSide); TPointD p1(convert(cPoint.m_p) + envNextSide); // Set coordinates bounds double lowerBound = std::max(cPoint.m_p.thick * m_oOptions.m_miterLower, m_pixSize); double upperBound = cPoint.m_p.thick * m_oOptions.m_miterUpper; // Build the intersection between the 2 lines TPointD cornerCoords( intersectionCoords(p0, prevTangentialD, p1, nextTangentialD)); if (cornerCoords == TConsts::napd || cornerCoords.x < lowerBound || cornerCoords.y > upperBound || cornerCoords.y < lowerBound || cornerCoords.y > upperBound) { // Bevel caps addOutlineBuilderFunc(&OutlineBuilder::addBevelSideCaps, oPoints, cPoint); return; } TPointD corner(p0 + cornerCoords.x * prevTangentialD); TPointD envPrevNotSide, envNextNotSide; buildEnvelopeVector(cPoint.m_p, cPoint.m_prevD, !leftSide, envPrevNotSide); buildEnvelopeVector(cPoint.m_p, cPoint.m_nextD, !leftSide, envNextNotSide); TPointD notSidePrev(convert(cPoint.m_p) + envPrevNotSide); TPointD notSideNext(convert(cPoint.m_p) + envNextNotSide); if (leftSide) { addEnvelopePoint(oPoints, notSidePrev, cPoint.m_countIdx); addEnvelopePoint(oPoints, convert(cPoint.m_p) + envPrevSide, cPoint.m_countIdx); addExtensionPoint(oPoints, 0.5 * (notSidePrev + notSideNext)); addExtensionPoint(oPoints, corner); addEnvelopePoint(oPoints, notSideNext); addEnvelopePoint(oPoints, convert(cPoint.m_p) + envNextSide); } else { addEnvelopePoint(oPoints, convert(cPoint.m_p) + envPrevSide, cPoint.m_countIdx); addEnvelopePoint(oPoints, notSidePrev, cPoint.m_countIdx); addExtensionPoint(oPoints, corner); addExtensionPoint(oPoints, 0.5 * (notSidePrev + notSideNext)); addEnvelopePoint(oPoints, convert(cPoint.m_p) + envNextSide); addEnvelopePoint(oPoints, notSideNext); } } //******************************************************************************** // Outline Builder Function //******************************************************************************** namespace { int buildCPointsData(const TStroke &stroke, std::vector &cPoints) { // Build point positions unsigned int i, pointsCount = cPoints.size(); int validPointsCount = 0; for (i = 0; i < pointsCount; ++i) { CenterlinePoint &cPoint = cPoints[i]; cPoint.buildPos(stroke); cPoint.buildDirs(stroke); if (!cPoint.m_covered) // Covered points simply cannot build envelope directions (forward nor // backward). // So, don't consider them ++validPointsCount; } if (!validPointsCount) { // Only single points may end up here. We just solve the problem // uncovering the first point. cPoints[0].m_covered = false; validPointsCount = 1; } return validPointsCount; } } // namespace //------------------------------------------------------------ void tellipticbrush::buildOutline(const TStroke &stroke, std::vector &cPoints, TStrokeOutline &outline, const OutlinizationData &data) { // Build the centerline points associated with passed stroke parameters int outlineSize = buildCPointsData(stroke, cPoints); // Reserve the lower bound known to the outline points std::vector &oPoints = outline.getArray(); oPoints.reserve(2 * outlineSize); OutlineBuilder outBuilder(data, stroke); // Now, build the outline unsigned int i, cPointsCount = cPoints.size(); for (i = 0;; ++i) { // Search the next uncovered point for (; i < cPointsCount && cPoints[i].m_covered; ++i) ; if (i >= cPointsCount) break; // Build the associated outline points outBuilder.buildOutlinePoints(oPoints, cPoints[i]); } } //******************************************************************************** // Make Outline Implementation //******************************************************************************** namespace { /* Quick container to store all the linearization features to be supported. \note The set should be appropriately ordered so that linearizator dependence can be supported (linearizators may work depending on knowledge of the other linearized points) */ struct LinearizatorsSet { static const int nLinearizators = 3; LengthLinearizator m_lengthLinearizator; CoverageLinearizator m_coverageLinearizator; RecursiveLinearizator m_recursiveLinearizator; StrokeLinearizator *m_linearizatorPtrs[nLinearizators]; public: LinearizatorsSet(const TStroke &stroke, const OutlinizationData &data) : m_lengthLinearizator(&stroke, data.m_options.m_lengthStep) , m_coverageLinearizator(&stroke) , m_recursiveLinearizator(&stroke, data.m_pixSize) { m_linearizatorPtrs[0] = &m_lengthLinearizator; m_linearizatorPtrs[1] = &m_coverageLinearizator; m_linearizatorPtrs[2] = &m_recursiveLinearizator; } StrokeLinearizator *operator[](int i) { return m_linearizatorPtrs[i]; } const int size() const { return nLinearizators; } }; } // namespace //============================================================================================ void TOutlineUtil::makeOutline(const TStroke &stroke, TStrokeOutline &outline, const TOutlineUtil::OutlineParameter &options) { // Build outlinization data OutlinizationData data(options); // Build a set of linearizators for the specified stroke LinearizatorsSet linearizators(stroke, data); std::vector cPoints, chunkPoints; int i, chunksCount = stroke.getChunkCount(); for (i = 0; i < chunksCount; ++i) { chunkPoints.clear(); chunkPoints.push_back(CenterlinePoint(i, 0.0)); int j, linearsCount = linearizators.size(); for (j = 0; j < linearsCount; ++j) { StrokeLinearizator *linearizator = linearizators[j]; linearizator->linearize(chunkPoints, i); } // These points are just PUSH_BACK'd to the vector. A sorting must be // performed // before storing them in the overall centerline points vector std::sort(chunkPoints.begin(), chunkPoints.end()); cPoints.insert(cPoints.end(), chunkPoints.begin(), chunkPoints.end()); } // Build the final point. CenterlinePoint last(chunksCount - 1, 1.0); // In the selfLoop case, use its info to modify the initial point. if (stroke.isSelfLoop()) { CenterlinePoint &first = cPoints[0]; first.buildPos(stroke); first.buildDirs(stroke); last.buildPos(stroke); last.buildDirs(stroke); first.m_prevD = last.m_prevD; first.m_hasPrevD = last.m_hasPrevD; last.m_nextD = first.m_nextD; last.m_hasNextD = first.m_hasNextD; first.m_covered = last.m_covered = (first.m_covered && last.m_covered); } cPoints.push_back(last); // Now, build the outline associated to the linearized centerline // NOTE: It's NOT NECESSARY TO BUILD ALL THE CENTERLINE POINTS BEFORE THIS! // It's sufficient to build the outline TOGETHER with the centeraline, for // each quadratic! buildOutline(stroke, cPoints, outline, data); } //============================================================================================ TRectD TOutlineUtil::computeBBox(const TStroke &stroke) { typedef TStroke::OutlineOptions OOpts; // First, calculate the usual stroke bbox TRectD roundBBox(::computeBBox(stroke)); const OOpts &oOptions(stroke.outlineOptions()); if (oOptions.m_capStyle != OOpts::PROJECTING_CAP && oOptions.m_joinStyle != OOpts::MITER_JOIN) return roundBBox; // Build interesting centerline points (in this case, junction points) std::vector cPoints; int i, chunksCount = stroke.getChunkCount(); for (i = 0; i < chunksCount; ++i) { CenterlinePoint cPoint(i, 0.0); cPoint.buildPos(stroke); cPoint.buildDirs(stroke); cPoints.push_back(cPoint); } // Build the final point. CenterlinePoint last(chunksCount - 1, 1.0); last.buildPos(stroke); last.buildDirs(stroke); // In the selfLoop case, use its info to modify the initial point. if (stroke.isSelfLoop()) { CenterlinePoint &first = cPoints[0]; first.m_prevD = last.m_prevD; first.m_hasPrevD = last.m_hasPrevD; last.m_nextD = first.m_nextD; last.m_hasNextD = first.m_hasNextD; first.m_covered = last.m_covered = (first.m_covered && last.m_covered); } cPoints.push_back(last); // Now, add the associated 'extending' outline points OutlineBuilder outBuilder(OutlinizationData(), stroke); TRectD extensionBBox((std::numeric_limits::max)(), (std::numeric_limits::max)(), -(std::numeric_limits::max)(), -(std::numeric_limits::max)()); unsigned int j, cPointsCount = cPoints.size(); for (j = 0;; ++j) { // Search the next uncovered point for (; j < cPointsCount && cPoints[j].m_covered; ++j) ; if (j >= cPointsCount) break; // Build the associated outline points outBuilder.buildOutlineExtensions(extensionBBox, cPoints[j]); } // Finally, merge the 2 bboxes return roundBBox + extensionBBox; }