From a70e977b735ca493910984a56723280b66385059 Mon Sep 17 00:00:00 2001 From: Paul Houx Date: Thu, 1 Feb 2024 03:58:16 +0100 Subject: [PATCH] Path2d and Shape2d additions: basic shapes and commands. (#2325) * Added convenience functions to Path2d and Shape2d, allowing easy creation of several shapes and smooth curves. * Added relative functions to Path2d and refactored things. * Closed the rounded rectangle in Path2d. * Tweaked the Path2d::spiral to get rid of a wobble. * Path2d::angleHelper() is now an anonymous namespace function. --------- Co-authored-by: paulhoux --- include/cinder/Path2d.h | 72 ++++++- include/cinder/Shape2d.h | 28 +++ src/cinder/Path2d.cpp | 420 +++++++++++++++++++++++++++++++++++++-- src/cinder/Shape2d.cpp | 87 ++++++++ 4 files changed, 586 insertions(+), 21 deletions(-) diff --git a/include/cinder/Path2d.h b/include/cinder/Path2d.h index 3ebb9f6f22..5efb2cb601 100644 --- a/include/cinder/Path2d.h +++ b/include/cinder/Path2d.h @@ -43,20 +43,75 @@ class CI_API Path2d { void moveTo( float x, float y ) { moveTo( vec2( x, y ) ); } void lineTo( const vec2 &p ); void lineTo( float x, float y ) { lineTo( vec2( x, y ) ); } + void horizontalLineTo( float x ); + void verticalLineTo( float y ); void quadTo( const vec2 &p1, const vec2 &p2 ); void quadTo( float x1, float y1, float x2, float y2 ) { quadTo( vec2( x1, y1 ), vec2( x2, y2 ) ); } + void smoothQuadTo( const vec2 &p2 ); + void smoothQuadTo( float x2, float y2 ) { smoothQuadTo( vec2( x2, y2 ) ); } void curveTo( const vec2 &p1, const vec2 &p2, const vec2 &p3 ); void curveTo( float x1, float y1, float x2, float y2, float x3, float y3 ) { curveTo( vec2( x1, y1 ), vec2( x2, y2 ), vec2( x3, y3 ) ); } + void smoothCurveTo( const vec2 &p2, const vec2 &p3 ); + void smoothCurveTo( float x2, float y2, float x3, float y3 ) { smoothCurveTo( vec2( x2, y2 ), vec2( x3, y3 ) ); } void arc( const vec2 ¢er, float radius, float startRadians, float endRadians, bool forward = true ); void arc( float centerX, float centerY, float radius, float startRadians, float endRadians, bool forward = true ) { arc( vec2( centerX, centerY ), radius, startRadians, endRadians, forward ); } void arcTo( const vec2 &p, const vec2 &t, float radius ); void arcTo( float x, float y, float tanX, float tanY, float radius) { arcTo( vec2( x, y ), vec2( tanX, tanY ), radius ); } + void arcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &p2 ); + + //! + void relativeMoveTo( float dx, float dy ) { relativeMoveTo( vec2( dx, dy ) ); } + void relativeMoveTo( const vec2 &delta ); + void relativeLineTo( float dx, float dy ) { relativeLineTo( vec2( dx, dy ) ); } + void relativeLineTo( const vec2 &delta ); + void relativeHorizontalLineTo( float dx ); + void relativeVerticalLineTo( float dy ); + void relativeQuadTo( float dx1, float dy1, float dx2, float dy2 ) { relativeQuadTo( vec2( dx1, dy1 ), vec2( dx2, dy2 ) ); } + void relativeQuadTo( const vec2 &delta1, const vec2 &delta2 ); + void relativeSmoothQuadTo( float dx, float dy ) { relativeSmoothQuadTo( vec2( dx, dy ) ); } + void relativeSmoothQuadTo( const vec2 &delta ); + void relativeCurveTo( float dx1, float dy1, float dx2, float dy2, float dx3, float dy3 ) { relativeCurveTo( vec2( dx1, dy1 ), vec2( dx2, dy2 ), vec2( dx3, dy3 ) ); } + void relativeCurveTo( const vec2 &delta1, const vec2 &delta2, const vec2 &delta3 ); + void relativeSmoothCurveTo( float dx2, float dy2, float dx3, float dy3 ) { relativeSmoothCurveTo( vec2( dx2, dy2 ), vec2( dx3, dy3 ) ); } + void relativeSmoothCurveTo( const vec2 &delta2, const vec2 &delta3 ); + void relativeArcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, float dx, float dy ) { relativeArcTo( rx, ry, phi, largeArcFlag, sweepFlag, vec2( dx, dy ) ); } + void relativeArcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &delta ); //! Closes the path, by drawing a straight line from the first to the last point. This is only legal as the last command. void close() { mSegments.push_back( CLOSE ); } - bool isClosed() const { return ( mSegments.size() > 1 ) && mSegments.back() == CLOSE; } + bool isClosed() const { return ! mSegments.empty() && mSegments.back() == CLOSE; } + + //! Creates a circle with given \a center and \a radius and returns it as a Path2d. + static Path2d circle( const vec2 ¢er, float radius ); + //! Creates an ellipse with given \a center and \a radiusX and \a radiusY and returns it as a Path2d. + static Path2d ellipse( const vec2 ¢er, float radiusX, float radiusY ); + //! Creates a line with given start point \a p0 and end point \a p1 and returns it as a Path2d. + static Path2d line( const vec2 &p0, const vec2 &p1 ); + //! Creates a polygon from the given \a points and returns it as a Path2d. + static Path2d polygon( const std::vector &points, bool closed = true ); + //! Creates a rectangle with given \a bounds and returns it as a Path2d. + static Path2d rectangle( const Rectf &bounds ) { return rectangle( bounds.x1, bounds.y1, bounds.getWidth(), bounds.getHeight() ); } + //! Creates a rectangle with given origin \a x, \a y and size \a width, \a height and returns it as a Path2d. + static Path2d rectangle( float x, float y, float width, float height ); + //! Creates a rounded rectangle with given \a bounds and corner radius \a r and returns it as a Path2d. + static Path2d roundedRectangle( const Rectf &bounds, float r ) { return roundedRectangle( bounds, r, r ); } + //! Creates a rounded rectangle with given \a bounds and corner radii \a rx and \a ry and returns it as a Path2d. + static Path2d roundedRectangle( const Rectf &bounds, float rx, float ry ) { return roundedRectangle( bounds.x1, bounds.y1, bounds.getWidth(), bounds.getHeight(), rx, ry ); } + //! Creates a rounded rectangle with given origin \a x, \a y and size \a width, \a height and corner radius \a r returns it as a Path2d. + static Path2d roundedRectangle( float x, float y, float width, float height, float r ) { return roundedRectangle( x, y, width, height, r, r ); } + //! Creates a rounded rectangle with given origin \a x, \a y and size \a width, \a height and corner radii \a rx and \a ry and returns it as a Path2d. + static Path2d roundedRectangle( float x, float y, float width, float height, float rx, float ry ); + //! Creates a star with the given \a center and number of \a points and returns it as a Path2d. + static Path2d star( const vec2 ¢er, int points, float largeRadius, float smallRadius, float rotation = 0 ); + //! Creates an arrow from start point \a p0 to end point \a p1 and returns it as a Path2d. The arrow head can be shaped with parameters \a thickness, \a width, \a length and \a concavity. + static Path2d arrow( const vec2 &p0, const vec2 &p1, float thickness, float width = 4, float length = 4, float concavity = 0 ); + //! Creates an arrow from start point \a x0, \a y0 to end point \a x1, \a y1 and returns it as a Path2d. The arrow head can be shaped with parameters \a thickness, \a width, \a length and \a concavity. + static Path2d arrow( float x0, float y0, float x1, float y1, float thickness, float width = 4, float length = 4, float concavity = 0 ) { return arrow( vec2( x0, y0 ), vec2( x1, y1 ), thickness, width, length, concavity ); } + //! Creates an Archimedean spiral at \a center and returns it as a Path2d. The spiral runs from \a innerRadius to \a outerRadius and the radius will increase by \a spacing every full revolution. + //! You can provide an optional radial \a offset. + static Path2d spiral( const vec2 ¢er, float innerRadius, float outerRadius, float spacing, float offset = 0 ); - //! Reverses the order of the path's points, inverting its winding order + //! Reverses the orientation of the path, changing CW to CCW and vice versa. void reverse(); bool empty() const { return mPoints.empty(); } @@ -94,8 +149,12 @@ class CI_API Path2d { const std::vector& getPoints() const { return mPoints; } std::vector& getPoints() { return mPoints; } - const vec2& getPoint( size_t point ) const { return mPoints[point]; } - vec2& getPoint( size_t point ) { return mPoints[point]; } + const vec2& getPoint( size_t point ) const { return mPoints[ point % mPoints.size() ]; } + vec2& getPoint( size_t point ) { return mPoints[ point % mPoints.size() ]; } + const vec2& getPointBefore( size_t point ) const { return getPoint( point + mPoints.size() - 1 ); } + vec2& getPointBefore( size_t point ) { return getPoint( point + mPoints.size() - 1 ); } + const vec2& getPointAfter( size_t point ) const { return getPoint( point + 1 ); } + vec2& getPointAfter( size_t point ) { return getPoint( point + 1 ); } const vec2& getCurrentPoint() const { return mPoints.back(); } void setPoint( size_t index, const vec2 &p ) { mPoints[index] = p; } @@ -115,6 +174,11 @@ class CI_API Path2d { //! Returns the precise bounding box around the curve itself. Slower to calculate than calcBoundingBox(). Rectf calcPreciseBoundingBox() const; + //! Returns whether the path is defined in clockwise order. + bool calcClockwise() const; + //! Returns whether the path is defined in counter-clockwise order. + bool calcCounterClockwise() const { return !calcClockwise(); } + //! Returns whether the point \a pt is contained within the boundaries of the Path2d. If \a evenOddFill is \c true (the default) then Even-Odd fill rule is used, otherwise, the Winding fill rule is applied. bool contains( const vec2 &pt, bool evenOddFill = true ) const; diff --git a/include/cinder/Shape2d.h b/include/cinder/Shape2d.h index 8b79957ecc..f26f07e91a 100644 --- a/include/cinder/Shape2d.h +++ b/include/cinder/Shape2d.h @@ -37,15 +37,43 @@ class CI_API Shape2d { void moveTo( float x, float y ) { moveTo( vec2( x, y ) ); } void lineTo( const vec2 &p ); void lineTo( float x, float y ) { lineTo( vec2( x, y ) ); } + void horizontalLineTo( float x ); + void verticalLineTo( float y ); void quadTo( const vec2 &p1, const vec2 &p2 ); void quadTo( float x1, float y1, float x2, float y2 ) { quadTo( vec2( x1, y1 ), vec2( x2, y2 ) ); } + void smoothQuadTo( const vec2 &p2 ); + void smoothQuadTo( float x2, float y2 ) { smoothQuadTo( vec2( x2, y2 ) ); } void curveTo( const vec2 &p1, const vec2 &p2, const vec2 &p3 ); void curveTo( float x1, float y1, float x2, float y2, float x3, float y3 ) { curveTo( vec2( x1, y1 ), vec2( x2, y2 ), vec2( x3, y3 ) ); } + void smoothCurveTo( const vec2 &p2, const vec2 &p3 ); + void smoothCurveTo( float x2, float y2, float x3, float y3 ) { smoothCurveTo( vec2( x2, y2 ), vec2( x3, y3 ) ); } void arc( const vec2 ¢er, float radius, float startRadians, float endRadians, bool forward = true ); void arc( float centerX, float centerY, float radius, float startRadians, float endRadians, bool forward = true ) { arc( vec2( centerX, centerY ), radius, startRadians, endRadians, forward ); } void arcTo( const vec2 &p, const vec2 &t, float radius ); void arcTo( float x, float y, float tanX, float tanY, float radius) { arcTo( vec2( x, y ), vec2( tanX, tanY ), radius ); } + void arcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, float px, float py ) { arcTo( rx, ry, phi, largeArcFlag, sweepFlag, vec2( px, py ) ); } + void arcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &p2 ); void close(); + + void relativeMoveTo( const vec2 &p ); + void relativeMoveTo( float dx, float dy ) { relativeMoveTo( vec2( dx, dy ) ); } + void relativeLineTo( const vec2 &delta ); + void relativeLineTo( float dx, float dy ) { relativeLineTo( vec2( dx, dy ) ); } + void relativeHorizontalLineTo( float dx ); + void relativeVerticalLineTo( float dy ); + void relativeQuadTo( const vec2 &delta1, const vec2 &delta2 ); + void relativeQuadTo( float x1, float y1, float x2, float y2 ) { relativeQuadTo( vec2( x1, y1 ), vec2( x2, y2 ) ); } + void relativeSmoothQuadTo( const vec2 &delta ); + void relativeSmoothQuadTo( float x2, float y2 ) { relativeSmoothQuadTo( vec2( x2, y2 ) ); } + void relativeCurveTo( const vec2 &delta1, const vec2 &delta2, const vec2 &delta3 ); + void relativeCurveTo( float x1, float y1, float x2, float y2, float x3, float y3 ) { relativeCurveTo( vec2( x1, y1 ), vec2( x2, y2 ), vec2( x3, y3 ) ); } + void relativeSmoothCurveTo( const vec2 &delta2, const vec2 &delta3 ); + void relativeSmoothCurveTo( float x2, float y2, float x3, float y3 ) { relativeSmoothCurveTo( vec2( x2, y2 ), vec2( x3, y3 ) ); } + void relativeArcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, float px, float py ) { relativeArcTo( rx, ry, phi, largeArcFlag, sweepFlag, vec2( px, py ) ); } + void relativeArcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &delta ); + + //! Reverses the orientation of the shape's contours, changing CW to CCW and vice versa. + void reverse(); bool empty() const { return mContours.empty(); } void clear() { mContours.clear(); } diff --git a/src/cinder/Path2d.cpp b/src/cinder/Path2d.cpp index 1df6515042..4f231db7ca 100644 --- a/src/cinder/Path2d.cpp +++ b/src/cinder/Path2d.cpp @@ -168,6 +168,18 @@ void Path2d::lineTo( const vec2 &p ) mSegments.push_back( LINETO ); } +void Path2d::horizontalLineTo( float x ) +{ + const vec2 &pt = getCurrentPoint(); + lineTo( x, pt.y ); +} + +void Path2d::verticalLineTo( float y ) +{ + const vec2 &pt = getCurrentPoint(); + lineTo( pt.x, y ); +} + void Path2d::quadTo( const vec2 &p1, const vec2 &p2 ) { if( mPoints.empty() ) @@ -178,6 +190,24 @@ void Path2d::quadTo( const vec2 &p1, const vec2 &p2 ) mSegments.push_back( QUADTO ); } +void Path2d::smoothQuadTo( const vec2 &p2 ) +{ + if( mPoints.empty() ) + throw Path2dExc(); // can only smoothQuadTo as non-first point + + vec2 p1 = getCurrentPoint(); + + if( ! mSegments.empty() && mSegments.back() == QUADTO ) { + const vec2 &c = getPointBefore( mPoints.size() - 1 ); + p1.x = 2 * p1.x - c.x; + p1.y = 2 * p1.y - c.y; + } + + mSegments.emplace_back( QUADTO ); + mPoints.emplace_back( p1.x, p1.y ); + mPoints.emplace_back( p2.x, p2.y ); +} + void Path2d::curveTo( const vec2 &p1, const vec2 &p2, const vec2 &p3 ) { if( mPoints.empty() ) @@ -189,6 +219,25 @@ void Path2d::curveTo( const vec2 &p1, const vec2 &p2, const vec2 &p3 ) mSegments.push_back( CUBICTO ); } +void Path2d::smoothCurveTo( const vec2 &p2, const vec2 &p3 ) +{ + if( mPoints.empty() ) + throw Path2dExc(); // can only smoothCurveTo as non-first point + + vec2 p1 = getCurrentPoint(); + + if( ! mSegments.empty() && mSegments.back() == CUBICTO ) { + const vec2 &c = getPointBefore( mPoints.size() - 1 ); + p1.x = 2 * p1.x - c.x; + p1.y = 2 * p1.y - c.y; + } + + mSegments.emplace_back( CUBICTO ); + mPoints.emplace_back( p1.x, p1.y ); + mPoints.emplace_back( p2.x, p2.y ); + mPoints.emplace_back( p3.x, p3.y ); +} + void Path2d::arc( const vec2 ¢er, float radius, float startRadians, float endRadians, bool forward ) { if( forward ) { @@ -338,26 +387,346 @@ void Path2d::arcTo( const vec2 &p1, const vec2 &t, float radius ) } } -void Path2d::reverse() +namespace { + +float angleHelper( const vec2 &u, const vec2 &v ) { - // The path is empty: nothing to do. - if( empty() ) - return; + // See: equation 5.4 of https://www.w3.org/TR/SVG/implnote.html + const float c = u.x * v.y - u.y * v.x; + const float d = glm::dot( glm::normalize( u ), glm::normalize( v ) ); + return c < 0 ? -math::acos( d ) : math::acos( d ); +} - // Reverse all points. - std::reverse( mPoints.begin(), mPoints.end() ); +} // namespace - // Reverse the segments, but skip the "moveto" and "close": - if( isClosed() ) { - // There should be at least 4 segments: "moveto", "close" and two other segments. - if( mSegments.size() > 3 ) - std::reverse( mSegments.begin() + 1, mSegments.end() - 1 ); - } - else { - // There should be at least 3 segments: "moveto" and two other segments. - if( mSegments.size() > 2 ) - std::reverse( mSegments.begin() + 1, mSegments.end() ); - } +void Path2d::arcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &p2 ) +{ + // See: https://www.w3.org/TR/SVG/implnote.html + + if( approxZero( rx ) || approxZero( ry ) ) { + return lineTo( p2 ); + } + + const vec2 p1 = mPoints.back(); + + const float sinPhi = math::sin( phi ); + const float cosPhi = math::cos( phi ); + + // Step 1: move ellipse so origin will be the midpoint between p1 and p2. + vec2 mid = ( p1 - p2 ) * 0.5f; // midpoint + + const float x1p = mid.x * cosPhi + mid.y * sinPhi; // equation 5.1 + const float y1p = mid.y * cosPhi - mid.x * sinPhi; + if( approxZero( x1p ) && approxZero( y1p ) ) + return lineTo( p2 ); + + const float x1pSquared = x1p * x1p; + const float y1pSquared = y1p * y1p; + + float rxSquared = rx * rx; + float rySquared = ry * ry; + + float lambda = x1pSquared / rxSquared + y1pSquared / rySquared; // equation 6.2 + if( lambda > 1.0f ) { // equation 6.3 + lambda = math::sqrt( lambda ); + rx *= lambda; + ry *= lambda; + rxSquared = rx * rx; + rySquared = ry * ry; + } + + // Step 2: compute coordinates of the center of the ellipse. + const float x = rySquared * x1pSquared; + const float y = rxSquared * y1pSquared; + + float r = roundToZero( ( rxSquared * rySquared - y - x ) / ( y + x ) ); + r = largeArcFlag == sweepFlag ? -sqrtf( r ) : sqrtf( r ); + + float cxp = r * ( rx * y1p ) / ry; // equation 5.2 + float cyp = r * -( ry * x1p ) / rx; + + // Step 3: transform back to original coordinate system. + mid = ( p1 + p2 ) * 0.5f; + vec2 c{ cxp * cosPhi - cyp * sinPhi + mid.x, cyp * cosPhi + cxp * sinPhi + mid.y }; // equation 5.3 + + // Step 4: compute angles and number of segments. + vec2 v1{ ( x1p - cxp ) / rx, ( y1p - cyp ) / ry }; + vec2 v2{ ( -x1p - cxp ) / rx, ( -y1p - cyp ) / ry }; + + float theta = angleHelper( { 1, 0 }, v1 ); + float deltaTheta = angleHelper( v1, v2 ); + + if( !sweepFlag && deltaTheta > 0 ) + deltaTheta -= float( 2 * M_PI ); + else if( sweepFlag && deltaTheta < 0 ) + deltaTheta += float( 2 * M_PI ); + + float segments = glm::max( 1.0f, math::ceil( math::abs( deltaTheta ) / float( M_PI / 2 ) ) ); + deltaTheta /= segments; + + float h = 4.0f / 3.0f * math::tan( deltaTheta / 4 ); + + // Step 5: generate cubic bezier curve segments. + for( int i = 0; i < int( segments ); ++i ) { + float x1 = roundToZero( math::cos( theta ) ); + float y1 = roundToZero( math::sin( theta ) ); + + theta += deltaTheta; + + float x2 = roundToZero( math::cos( theta ) ); + float y2 = roundToZero( math::sin( theta ) ); + + vec2 c1{ rx * ( x1 - y1 * h ), ry * ( y1 + x1 * h ) }; + vec2 c2{ rx * ( x2 + y2 * h ), ry * ( y2 - x2 * h ) }; + vec2 c3{ rx * x2, ry * y2 }; + + c1 = glm::rotate( c1, phi ) + c; + c2 = glm::rotate( c2, phi ) + c; + c3 = glm::rotate( c3, phi ) + c; + + curveTo( c1, c2, c3 ); + } +} + +void Path2d::relativeMoveTo( const vec2 &delta ) +{ + const auto &pt = getCurrentPoint(); + moveTo( pt + delta ); +} + +void Path2d::relativeLineTo( const vec2 &delta ) +{ + const auto &pt = getCurrentPoint(); + lineTo( pt + delta ); +} + +void Path2d::relativeHorizontalLineTo( float dx ) +{ + const auto &pt = getCurrentPoint(); + horizontalLineTo( pt.x + dx ); +} + +void Path2d::relativeVerticalLineTo( float dy ) +{ + const auto &pt = getCurrentPoint(); + verticalLineTo( pt.y + dy ); +} + +void Path2d::relativeQuadTo( const vec2 &delta1, const vec2 &delta2 ) +{ + const auto &pt = getCurrentPoint(); + quadTo( pt + delta1, pt + delta2 ); +} + +void Path2d::relativeSmoothQuadTo( const vec2 &delta ) +{ + const auto &pt = getCurrentPoint(); + smoothQuadTo( pt + delta ); +} + +void Path2d::relativeCurveTo( const vec2 &delta1, const vec2 &delta2, const vec2 &delta3 ) +{ + const auto &pt = getCurrentPoint(); + curveTo( pt + delta1, pt + delta2, pt + delta3 ); +} + +void Path2d::relativeSmoothCurveTo( const vec2 &delta2, const vec2 &delta3 ) +{ + const auto &pt = getCurrentPoint(); + smoothCurveTo( pt + delta2, pt + delta3 ); +} + +void Path2d::relativeArcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &delta ) +{ + const auto &pt = getCurrentPoint(); + arcTo( rx, ry, phi, largeArcFlag, sweepFlag, pt + delta ); +} + +Path2d Path2d::circle( const vec2 ¢er, float radius ) +{ + Path2d shape; + shape.moveTo( center.x + radius, center.y ); + shape.relativeArcTo( radius, radius, 0, false, true, vec2( -( radius + radius ), 0 ) ); + shape.relativeArcTo( radius, radius, 0, false, true, vec2( +( radius + radius ), 0 ) ); + return shape; +} + +Path2d Path2d::ellipse( const vec2 ¢er, float radiusX, float radiusY ) +{ + Path2d shape; + shape.moveTo( center.x + radiusX, center.y ); + shape.relativeArcTo( radiusX, radiusY, 0, false, true, vec2( -( radiusX + radiusX ), 0 ) ); + shape.relativeArcTo( radiusX, radiusY, 0, false, true, vec2( +( radiusX + radiusX ), 0 ) ); + return shape; +} + +Path2d Path2d::line( const vec2 &p0, const vec2 &p1 ) +{ + Path2d shape; + shape.moveTo( p0 ); + shape.lineTo( p1 ); + return shape; +} + +Path2d Path2d::polygon( const std::vector &points, bool closed ) +{ + if( points.size() < 2 ) + throw Path2dExc(); // + + Path2d shape; + + auto itr = points.begin(); + shape.moveTo( *itr++ ); + while( itr != points.end() ) + shape.lineTo( *itr++ ); + + if( closed ) + shape.close(); + + return shape; +} + +Path2d Path2d::rectangle( float x, float y, float width, float height ) +{ + Path2d shape; + shape.moveTo( x, y ); + shape.lineTo( x + width, y ); + shape.lineTo( x + width, y + height ); + shape.lineTo( x, y + height ); + shape.close(); + return shape; +} + +Path2d Path2d::roundedRectangle( float x, float y, float width, float height, float rx, float ry ) +{ + if( approxZero( rx ) || approxZero( ry ) ) + return rectangle( x, y, width, height ); + + Path2d shape; + shape.moveTo( x + rx, y ); + shape.lineTo( x + width - rx, y ); + shape.arcTo( rx, ry, 0, false, true, vec2( x + width, y + ry ) ); + shape.lineTo( x + width, y + height - ry ); + shape.arcTo( rx, ry, 0, false, true, vec2( x + width - rx, y + height ) ); + shape.lineTo( x + rx, y + height ); + shape.arcTo( rx, ry, 0, false, true, vec2( x, y + height - ry ) ); + shape.lineTo( x, y + ry ); + shape.arcTo( rx, ry, 0, false, true, vec2( x + rx, y ) ); + shape.close(); + return shape; +} + +Path2d Path2d::star( const vec2 ¢er, int points, float largeRadius, float smallRadius, float rotation ) +{ + const float step = glm::radians( 180.0f / float( points ) ); + + Path2d shape; + for( int i = 0; i < 2 * points; i += 2 ) { + float x = center.x + largeRadius * glm::sin( rotation + float( i + 0 ) * step ); + float y = center.y - largeRadius * glm::cos( rotation + float( i + 0 ) * step ); + if( i == 0 ) + shape.moveTo( x, y ); + else + shape.lineTo( x, y ); + x = center.x + smallRadius * glm::sin( rotation + float( i + 1 ) * step ); + y = center.y - smallRadius * glm::cos( rotation + float( i + 1 ) * step ); + shape.lineTo( x, y ); + } + shape.close(); + return shape; +} + +Path2d Path2d::arrow( const vec2 &p0, const vec2 &p1, float thickness, float width, float length, float concavity ) +{ + const float distance = glm::distance( p1, p0 ); + const vec2 direction = ( p1 - p0 ) / distance; + const vec2 normal{ 0.5f * thickness * direction.y, -0.5f * thickness * direction.x }; + + vec2 base = p0 + direction * glm::max( 0.0f, distance - thickness * length ); + + Path2d shape; + shape.moveTo( p0 - normal ); + shape.lineTo( base - normal + direction * thickness * length * concavity ); + shape.lineTo( base - normal * width ); + shape.lineTo( p1 ); + shape.lineTo( base + normal * width ); + shape.lineTo( base + normal + direction * thickness * length * concavity ); + shape.lineTo( p0 + normal ); + shape.close(); + return shape; +} + +Path2d Path2d::spiral( const vec2 ¢er, float innerRadius, float outerRadius, float spacing, float offset ) +{ + // Helper struct + struct Point { + float x; + float y; + float theta; + float tangent; + + explicit Point( float theta, float offset = 0 ) + : theta( theta ) + { + float c = math::cos( theta + offset ); + float s = math::sin( theta + offset ); + x = theta * c; + y = theta * s; + tangent = math::atan2( s + x, c - y ); + } + + std::pair generate( const Point &previous ) const + { + const auto offset = 4 * math::tan( ( theta - previous.theta ) / 4 ) / 3; + const auto p1 = vec2( math::cos( previous.tangent ) * offset * previous.theta + previous.x, math::sin( previous.tangent ) * offset * previous.theta + previous.y ); + const auto p2 = vec2( math::cos( tangent - float( M_PI ) ) * offset * theta + x, math::sin( tangent - float( M_PI ) ) * offset * theta + y ); + return std::make_pair( p1, p2 ); + } + }; + + const auto step = spacing / ( 2.0f * float( M_PI ) ); + const auto radiansStart = glm::radians( 360 * innerRadius / spacing ); + const auto radiansEnd = glm::radians( 360 * outerRadius / spacing ); + + Point p0( radiansStart, offset - radiansStart ); + + Path2d shape; + shape.moveTo( center.x + p0.x * step, center.y + p0.y * step ); + + float radians = radiansStart + glm::radians( clamp( radiansStart * step, 3.0f, 60.0f ) ); // Adaptive step size. + while( radians < radiansEnd ) { + const auto p3 = Point( radians, offset - radiansStart ); + const auto controls = p3.generate( p0 ); + shape.curveTo( center.x + controls.first.x * step, center.y + controls.first.y * step, center.x + controls.second.x * step, center.y + controls.second.y * step, center.x + p3.x * step, center.y + p3.y * step ); + + p0 = p3; + + radians += glm::radians( glm::clamp( radians * step, 3.0f, 60.0f ) ); // Adaptive step size. + } + + const auto p3 = Point( radiansEnd, offset - radiansStart ); + const auto controls = p3.generate( p0 ); + + shape.curveTo( center.x + controls.first.x * step, center.y + controls.first.y * step, center.x + controls.second.x * step, center.y + controls.second.y * step, center.x + p3.x * step, center.y + p3.y * step ); + + return shape; +} + +void Path2d::reverse() +{ + // The path is empty: nothing to do. + if( empty() ) + return; + + // Reverse all points. + std::reverse( mPoints.begin(), mPoints.end() ); + + if( isClosed() && mSegments.size() > 2 ) { + std::reverse( mSegments.begin(), mSegments.end() - 1 ); + } + else if( ! isClosed() && mSegments.size() > 1 ) { + std::reverse( mSegments.begin(), mSegments.end() ); + } } void Path2d::appendSegment( SegmentType segmentType, const vec2 *points ) @@ -1004,6 +1373,23 @@ Rectf Path2d::calcPreciseBoundingBox() const return result; } +bool Path2d::calcClockwise() const +{ + // See: https://en.wikipedia.org/wiki/Curve_orientation + size_t index = 0; + for( size_t i = 1; i < mPoints.size(); ++i ) { + if( mPoints.at( i ).x < mPoints.at( index ).x || ( approxEqual( mPoints.at( i ).x, mPoints.at( index ).x ) && mPoints.at( i ).y < mPoints.at( index ).y ) ) + index = i; + } + + const auto &a = getPoint(index); + const auto &b = getPointBefore(index); + const auto &c = getPointAfter(index); + const auto sign = glm::sign( (b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y) ); + + return sign < 0; +} + namespace { float calcCubicBezierSpeed( const vec2 p[3], float t ) { diff --git a/src/cinder/Shape2d.cpp b/src/cinder/Shape2d.cpp index cf10fa6f9d..47601860ef 100644 --- a/src/cinder/Shape2d.cpp +++ b/src/cinder/Shape2d.cpp @@ -39,16 +39,38 @@ void Shape2d::lineTo( const vec2 &p ) mContours.back().lineTo( p ); } +void Shape2d::horizontalLineTo( float x ) +{ + const auto &pt = getCurrentPoint(); + lineTo( x, pt.y ); +} + +void Shape2d::verticalLineTo( float y ) +{ + const auto &pt = getCurrentPoint(); + lineTo( pt.x, y ); +} + void Shape2d::quadTo( const vec2 &p1, const vec2 &p2 ) { mContours.back().quadTo( p1, p2 ); } +void Shape2d::smoothQuadTo( const vec2 &p2 ) +{ + mContours.back().smoothQuadTo( p2 ); +} + void Shape2d::curveTo( const vec2 &p1, const vec2 &p2, const vec2 &p3 ) { mContours.back().curveTo( p1, p2, p3 ); } +void Shape2d::smoothCurveTo( const vec2 &p2, const vec2 &p3 ) +{ + mContours.back().smoothCurveTo( p2, p3 ); +} + void Shape2d::arc( const vec2 ¢er, float radius, float startRadians, float endRadians, bool forward ) { if( mContours.empty() ) @@ -61,12 +83,77 @@ void Shape2d::arcTo( const vec2 &p, const vec2 &t, float radius ) mContours.back().arcTo( p, t, radius ); } +void Shape2d::arcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &p2 ) +{ + mContours.back().arcTo( rx, ry, phi, largeArcFlag, sweepFlag, p2 ); +} + +void Shape2d::relativeMoveTo( const vec2 &p ) +{ + const auto &pt = getCurrentPoint(); + moveTo( pt + p ); +} + +void Shape2d::relativeLineTo( const vec2 &delta ) +{ + const auto &pt = getCurrentPoint(); + lineTo( pt + delta ); +} + +void Shape2d::relativeHorizontalLineTo( float dx ) +{ + const auto &pt = getCurrentPoint(); + lineTo( pt.x + dx, pt.y ); +} + +void Shape2d::relativeVerticalLineTo( float dy ) +{ + const auto &pt = getCurrentPoint(); + lineTo( pt.x, pt.y + dy ); +} + +void Shape2d::relativeQuadTo( const vec2 &delta1, const vec2 &delta2 ) +{ + const auto &pt = getCurrentPoint(); + quadTo( pt + delta1, pt + delta2 ); +} + +void Shape2d::relativeSmoothQuadTo( const vec2 &delta ) +{ + const auto &pt = getCurrentPoint(); + smoothQuadTo( pt + delta ); +} + +void Shape2d::relativeCurveTo( const vec2 &delta1, const vec2 &delta2, const vec2 &delta3 ) +{ + const auto &pt = getCurrentPoint(); + curveTo( pt + delta1, pt + delta2, pt + delta3 ); +} + +void Shape2d::relativeSmoothCurveTo( const vec2 &delta2, const vec2 &delta3 ) +{ + const auto &pt = getCurrentPoint(); + smoothCurveTo( pt + delta2, pt + delta3 ); +} + +void Shape2d::relativeArcTo( float rx, float ry, float phi, bool largeArcFlag, bool sweepFlag, const vec2 &delta ) +{ + const auto &pt = getCurrentPoint(); + arcTo( rx, ry, phi, largeArcFlag, sweepFlag, pt + delta ); +} + void Shape2d::close() { if( ! mContours.empty() ) mContours.back().close(); } +void Shape2d::reverse() +{ + for( auto &contour : mContours ) + contour.reverse(); +} + void Shape2d::append( const Shape2d &shape ) { for( vector::const_iterator pathIt = shape.getContours().begin(); pathIt != shape.getContours().end(); ++pathIt )