diff options
Diffstat (limited to 'src/display/curve.cpp')
-rw-r--r-- | src/display/curve.cpp | 720 |
1 files changed, 720 insertions, 0 deletions
diff --git a/src/display/curve.cpp b/src/display/curve.cpp new file mode 100644 index 0000000..64f8e63 --- /dev/null +++ b/src/display/curve.cpp @@ -0,0 +1,720 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Johan Engelen + * + * Copyright (C) 2000 Lauris Kaplinski + * Copyright (C) 2000-2001 Ximian, Inc. + * Copyright (C) 2002 Lauris Kaplinski + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "display/curve.h" + +#include <glib.h> +#include <2geom/pathvector.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/point.h> +#include <helper/geom.h> + +#include <utility> + +/** + * Routines for SPCurve and for its Geom::PathVector + */ + +SPCurve::smart_pointer // +SPCurve::new_from_rect(Geom::Rect const &rect, bool all_four_sides) +{ + auto c = std::make_unique<SPCurve>(); + + Geom::Point p = rect.corner(0); + c->moveto(p); + + for (int i=3; i>=1; --i) { + c->lineto(rect.corner(i)); + } + + if (all_four_sides) { + // When _constrained_ snapping to a path, the 2geom::SimpleCrosser will be invoked which doesn't consider the closing segment. + // of a path. Consequently, in case we want to snap to for example the page border, we must provide all four sides of the + // rectangle explicitly + c->lineto(rect.corner(0)); + } else { + // ... instead of just three plus a closing segment + c->closepath(); + } + + return c; +} + +SPCurve::~SPCurve() += default; + +/* Methods */ + +void +SPCurve::set_pathvector(Geom::PathVector const & new_pathv) +{ + _pathv = new_pathv; +} + +Geom::PathVector const & +SPCurve::get_pathvector() const +{ + return _pathv; +} + +/* + * Returns the number of segments of all paths summed + * This count includes the closing line segment of a closed path. + */ +size_t +SPCurve::get_segment_count() const +{ + return _pathv.curveCount(); +} + +/** + * Increase _refcount of curve. + */ +SPCurve::smart_pointer // +SPCurve::ref() +{ + _refcount += 1; + + return smart_pointer(this); +} + +/** + * Decrease refcount of curve, with possible destruction. + */ +void SPCurve::_unref() +{ + _refcount -= 1; + + if (_refcount < 1) { + delete this; + } +} + +/** + * Create new curve from this curve's pathvector array. + */ +SPCurve::smart_pointer // +SPCurve::copy() const +{ + return std::make_unique<SPCurve>(_pathv); +} + +/** + * Return a copy of `curve` or NULL if `curve` is NULL + */ +SPCurve::smart_pointer // +SPCurve::copy(SPCurve const *curve) +{ + return curve ? curve->copy() : nullptr; +} + +/** + * Returns a list of new curves corresponding to the subpaths in \a curve. + * 2geomified + */ +std::list<SPCurve::smart_pointer> // +SPCurve::split() const +{ + std::list<smart_pointer> l; + + for (const auto & path_it : _pathv) { + Geom::PathVector newpathv; + newpathv.push_back(path_it); + SPCurve * newcurve = new SPCurve(newpathv); + l.emplace_back(newcurve); + } + + return l; +} + +/** + * Returns a list of new curves of non-overlapping subpath in \a curve. + */ +std::list<SPCurve::smart_pointer> +SPCurve::split_non_overlapping() const +{ + std::list<smart_pointer> result; + + for (const auto & path_it : _pathv) { + Geom::PathVector newpathv; + newpathv.push_back(path_it); + + for (auto &existing : result) { + if (is_intersecting(existing->_pathv, newpathv)) { + existing->append(newpathv, false); + newpathv.clear(); + } + } + if (!newpathv.empty()) { + SPCurve * newcurve = new SPCurve(newpathv); + result.emplace_back(newcurve); + } + } + + return result; +} + +/** + * Transform all paths in curve using matrix. + */ +void +SPCurve::transform(Geom::Affine const &m) +{ + _pathv *= m; +} + +/** + * Set curve to empty curve. + * In more detail: this clears the internal pathvector from all its paths. + */ +void +SPCurve::reset() +{ + _pathv.clear(); +} + +/** Several consecutive movetos are ALLOWED + * Ref: http://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes + * (first subitem of the item about zero-length path segments) */ + +/** + * Calls SPCurve::moveto() with point made of given coordinates. + */ +void +SPCurve::moveto(double x, double y) +{ + moveto(Geom::Point(x, y)); +} +/** + * Perform a moveto to a point, thus starting a new subpath. + * Point p must be finite. + */ +void +SPCurve::moveto(Geom::Point const &p) +{ + Geom::Path path(p); + path.setStitching(true); + _pathv.push_back(path); +} + +/** + * Adds a line to the current subpath. + * Point p must be finite. + */ +void +SPCurve::lineto(Geom::Point const &p) +{ + if (_pathv.empty()) g_message("SPCurve::lineto - path is empty!"); + else _pathv.back().appendNew<Geom::LineSegment>( p ); +} +/** + * Calls SPCurve::lineto( Geom::Point(x,y) ) + */ +void +SPCurve::lineto(double x, double y) +{ + lineto(Geom::Point(x,y)); +} + +/** + * Adds a quadratic bezier segment to the current subpath. + * All points must be finite. + */ +void +SPCurve::quadto(Geom::Point const &p1, Geom::Point const &p2) +{ + if (_pathv.empty()) g_message("SPCurve::quadto - path is empty!"); + else _pathv.back().appendNew<Geom::QuadraticBezier>( p1, p2); +} +/** + * Calls SPCurve::quadto( Geom::Point(x1,y1), Geom::Point(x2,y2) ) + * All coordinates must be finite. + */ +void +SPCurve::quadto(double x1, double y1, double x2, double y2) +{ + quadto( Geom::Point(x1,y1), Geom::Point(x2,y2) ); +} + +/** + * Adds a bezier segment to the current subpath. + * All points must be finite. + */ +void +SPCurve::curveto(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2) +{ + if (_pathv.empty()) g_message("SPCurve::curveto - path is empty!"); + else _pathv.back().appendNew<Geom::CubicBezier>( p0, p1, p2 ); +} +/** + * Calls SPCurve::curveto( Geom::Point(x0,y0), Geom::Point(x1,y1), Geom::Point(x2,y2) ) + * All coordinates must be finite. + */ +void +SPCurve::curveto(double x0, double y0, double x1, double y1, double x2, double y2) +{ + curveto( Geom::Point(x0,y0), Geom::Point(x1,y1), Geom::Point(x2,y2) ); +} + +/** + * Close current subpath by possibly adding a line between start and end. + */ +void +SPCurve::closepath() +{ + _pathv.back().close(true); +} + +/** Like SPCurve::closepath() but sets the end point of the last subpath + to the subpath start point instead of adding a new lineto. + + Used for freehand drawing when the user draws back to the start point. +**/ +void +SPCurve::closepath_current() +{ + if (_pathv.back().size() > 0 && dynamic_cast<Geom::LineSegment const *>(&_pathv.back().back_open())) { + _pathv.back().erase_last(); + } else { + _pathv.back().setFinal(_pathv.back().initialPoint()); + } + _pathv.back().close(true); +} + +/** + * True if no paths are in curve. If it only contains a path with only a moveto, the path is considered NON-empty + */ +bool +SPCurve::is_empty() const +{ + return _pathv.empty(); +} + +/** + * True if paths are in curve. If it only contains a path with only a moveto, the path is considered as unset FALSE + */ +bool +SPCurve::is_unset() const +{ + if (get_segment_count()) { + return false; + } + return true; +} + +/** + * True iff all subpaths are closed. + * Returns false if the curve is empty. + */ +bool +SPCurve::is_closed() const +{ + if (is_empty()) { + return false; + } + + for (const auto & it : _pathv) { + if ( ! it.closed() ) { + return false; + } + } + + return true; +} + +/** + * True if both curves are equal + */ +bool SPCurve::is_equal(SPCurve const *other) const +{ + if(other == nullptr) { + return false; + } + + if(_pathv == other->get_pathvector()){ + return true; + } + + return false; +} + +/** + * True if both curves are near + */ +bool SPCurve::is_similar(SPCurve const *other, double precission) const +{ + if(other == nullptr) { + return false; + } + + return pathv_similar(_pathv, other->get_pathvector(), precission); +} + +/** + * Return last pathsegment (possibly the closing path segment) of the last path in PathVector or NULL. + * If the last path is empty (contains only a moveto), the function returns NULL + */ +Geom::Curve const * +SPCurve::last_segment() const +{ + if (is_empty()) { + return nullptr; + } + if (_pathv.back().empty()) { + return nullptr; + } + + return &_pathv.back().back_default(); +} + +/** + * Return last path in PathVector or NULL. + */ +Geom::Path const * +SPCurve::last_path() const +{ + if (is_empty()) { + return nullptr; + } + + return &_pathv.back(); +} + +/** + * Return first pathsegment in PathVector or NULL. + * equal in functionality to SPCurve::first_bpath() + */ +Geom::Curve const * +SPCurve::first_segment() const +{ + if (is_empty()) { + return nullptr; + } + if (_pathv.front().empty()) { + return nullptr; + } + + return &_pathv.front().front(); +} + +/** + * Return first path in PathVector or NULL. + */ +Geom::Path const * +SPCurve::first_path() const +{ + if (is_empty()) { + return nullptr; + } + + return &_pathv.front(); +} + +/** + * Return first point of first subpath or nothing when the path is empty. + */ +std::optional<Geom::Point> +SPCurve::first_point() const +{ + std::optional<Geom::Point> retval; + + if (!is_empty()) { + retval = _pathv.front().initialPoint(); + } + + return retval; +} + +/** + * Return the second point of first subpath or _movePos if curve too short. + * If the pathvector is empty, this returns nothing. If the first path is only a moveto, this method + * returns the first point of the second path, if it exists. If there is no 2nd path, it returns the + * first point of the first path. + */ +std::optional<Geom::Point> +SPCurve::second_point() const +{ + std::optional<Geom::Point> retval; + if (!is_empty()) { + if (_pathv.front().empty()) { + // first path is only a moveto + // check if there is second path + if (_pathv.size() > 1) { + retval = _pathv[1].initialPoint(); + } else { + retval = _pathv[0].initialPoint(); + } + } else { + retval = _pathv.front()[0].finalPoint(); + } + } + + return retval; +} + +/** + * Return the second-last point of last subpath or first point when that last subpath has only a moveto. + */ +std::optional<Geom::Point> +SPCurve::penultimate_point() const +{ + std::optional<Geom::Point> retval; + if (!is_empty()) { + Geom::Path const &lastpath = _pathv.back(); + if (!lastpath.empty()) { + Geom::Curve const &back = lastpath.back_default(); + retval = back.initialPoint(); + } else { + retval = lastpath.initialPoint(); + } + } + + return retval; +} + +/** + * Return last point of last subpath or nothing when the curve is empty. + * If the last path is only a moveto, then return that point. + */ +std::optional<Geom::Point> +SPCurve::last_point() const +{ + std::optional<Geom::Point> retval; + + if (!is_empty()) { + retval = _pathv.back().finalPoint(); + } + + return retval; +} + +/** + * Returns a *new* \a curve but drawn in the opposite direction. + * Should result in the same shape, but + * with all its markers drawn facing the other direction. + * Reverses the order of subpaths as well + **/ +SPCurve::smart_pointer // +SPCurve::create_reverse() const +{ + return std::make_unique<SPCurve>(_pathv.reversed()); +} + +/** + * Append \a curve2 to \a this. + * If \a use_lineto is false, simply add all paths in \a curve2 to \a this; + * if \a use_lineto is true, combine \a this's last path and \a curve2's first path and add the rest of the paths in \a curve2 to \a this. + */ +void +SPCurve::append(SPCurve const &curve2, + bool use_lineto) +{ + append(curve2._pathv, use_lineto); +} +void SPCurve::append(Geom::PathVector const &pathv, bool use_lineto) +{ + if (pathv.empty()) + return; + + if (use_lineto) { + Geom::PathVector::const_iterator it = pathv.begin(); + if ( ! _pathv.empty() ) { + Geom::Path & lastpath = _pathv.back(); + lastpath.appendNew<Geom::LineSegment>( (*it).initialPoint() ); + lastpath.append( (*it) ); + } else { + _pathv.push_back( (*it) ); + } + + for (++it; it != pathv.end(); ++it) { + _pathv.push_back( (*it) ); + } + } else { + for (const auto &it : pathv) { + _pathv.push_back( it ); + } + } +} + +/** + * Append \a c1 to \a this with possible fusing of close endpoints. If the end of this curve and the start of c1 are within tolerance distance, + * then the startpoint of c1 is moved to the end of this curve and the first subpath of c1 is appended to the last subpath of this curve. + * When one of the curves is empty, this curves path becomes the non-empty path. + * + * @param tolerance Tolerance for endpoint fusion (applied to x and y separately) + * @return False if one of the curves (this curve or the argument curve) is closed, true otherwise. + */ +bool SPCurve::append_continuous(SPCurve const &c1, double tolerance) +{ + if (is_closed() || c1.is_closed()) { + return false; + } + + if (c1.is_empty()) { + return true; + } + + if (this->is_empty()) { + _pathv = c1._pathv; + return true; + } + + if ((fabs(last_point()->x() - c1.first_point()->x()) <= tolerance) && + (fabs(last_point()->y() - c1.first_point()->y()) <= tolerance)) { + // c1's first subpath can be appended to this curve's last subpath + Geom::PathVector::const_iterator path_it = c1._pathv.begin(); + Geom::Path & lastpath = _pathv.back(); + + Geom::Path newfirstpath(*path_it); + newfirstpath.setInitial(lastpath.finalPoint()); + lastpath.append( newfirstpath ); + + for (++path_it; path_it != c1._pathv.end(); ++path_it) { + _pathv.push_back( (*path_it) ); + } + + } else { + append(c1, true); + } + + return true; +} + +/** + * Remove last segment of curve. + * (Only used once in /src/pen-context.cpp) + */ +void +SPCurve::backspace() +{ + if ( is_empty() ) + return; + + if ( !_pathv.back().empty() ) { + _pathv.back().erase_last(); + _pathv.back().close(false); + } +} + +/** + * TODO: add comments about what this method does and what assumptions are made and requirements are put on SPCurve + (2:08:18 AM) Johan: basically, i convert the path to pw<d2> +(2:08:27 AM) Johan: then i calculate an offset path +(2:08:29 AM) Johan: to move the knots +(2:08:36 AM) Johan: then i add it +(2:08:40 AM) Johan: then convert back to path +If I remember correctly, this moves the firstpoint to new_p0, and the lastpoint to new_p1, and moves all nodes in between according to their arclength (interpolates the movement amount) + */ +void +SPCurve::stretch_endpoints(Geom::Point const &new_p0, Geom::Point const &new_p1) +{ + if (is_empty()) { + return; + } + + Geom::Point const offset0( new_p0 - *first_point() ); + Geom::Point const offset1( new_p1 - *last_point() ); + + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = _pathv.front().toPwSb(); + Geom::Piecewise<Geom::SBasis> arclength = Geom::arcLengthSb(pwd2); + if ( arclength.lastValue() <= 0 ) { + g_error("SPCurve::stretch_endpoints - arclength <= 0"); + throw; + } + arclength *= 1./arclength.lastValue(); + Geom::Point const A( offset0 ); + Geom::Point const B( offset1 ); + Geom::Piecewise<Geom::SBasis> offsetx = (arclength*-1.+1)*A[0] + arclength*B[0]; + Geom::Piecewise<Geom::SBasis> offsety = (arclength*-1.+1)*A[1] + arclength*B[1]; + Geom::Piecewise<Geom::D2<Geom::SBasis> > offsetpath = Geom::sectionize( Geom::D2<Geom::Piecewise<Geom::SBasis> >(offsetx, offsety) ); + pwd2 += offsetpath; + _pathv = Geom::path_from_piecewise( pwd2, 0.001 ); +} + +/** + * sets start of first path to new_p0, and end of first path to new_p1 + */ +void +SPCurve::move_endpoints(Geom::Point const &new_p0, Geom::Point const &new_p1) +{ + if (is_empty()) { + return; + } + _pathv.front().setInitial(new_p0); + _pathv.front().setFinal(new_p1); +} + +/** + * returns the number of nodes in a path, used for statusbar text when selecting an spcurve. + * Sum of nodes in all the paths. When a path is closed, and its closing line segment is of zero-length, + * this function will not count the closing knot double (so basically ignores the closing line segment when it has zero length) + */ +size_t +SPCurve::nodes_in_path() const +{ + size_t nr = 0; + for(const auto & it : _pathv) { + // if the path does not have any segments, it is a naked moveto, + // and therefore any path has at least one valid node + size_t psize = std::max<size_t>(1, it.size_closed()); + nr += psize; + if (it.closed() && it.size_closed() > 0) { + const Geom::Curve &closingline = it.back_closed(); + // the closing line segment is always of type + // Geom::LineSegment. + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + // closingline.isDegenerate() did not work, because it only checks for + // *exact* zero length, which goes wrong for relative coordinates and + // rounding errors... + // the closing line segment has zero-length. So stop before that one! + nr -= 1; + } + } + } + + return nr; +} + +/** + * Adds p to the last point (and last handle if present) of the last path + */ +void +SPCurve::last_point_additive_move(Geom::Point const & p) +{ + if (is_empty()) { + return; + } + + _pathv.back().setFinal( _pathv.back().finalPoint() + p ); + + // Move handle as well when the last segment is a cubic bezier segment: + // TODO: what to do for quadratic beziers? + if ( Geom::CubicBezier const *lastcube = dynamic_cast<Geom::CubicBezier const *>(&_pathv.back().back()) ) { + Geom::CubicBezier newcube( *lastcube ); + newcube.setPoint(2, newcube[2] + p); + _pathv.back().replace( --_pathv.back().end(), newcube ); + } +} + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8: |