diff options
Diffstat (limited to 'src/path')
-rw-r--r-- | src/path/CMakeLists.txt | 21 | ||||
-rw-r--r-- | src/path/README | 11 | ||||
-rw-r--r-- | src/path/path-boolop.cpp | 956 | ||||
-rw-r--r-- | src/path/path-boolop.h | 34 | ||||
-rw-r--r-- | src/path/path-object-set.cpp | 166 | ||||
-rw-r--r-- | src/path/path-offset.cpp | 475 | ||||
-rw-r--r-- | src/path/path-offset.h | 41 | ||||
-rw-r--r-- | src/path/path-outline.cpp | 795 | ||||
-rw-r--r-- | src/path/path-outline.h | 60 | ||||
-rw-r--r-- | src/path/path-simplify.cpp | 123 | ||||
-rw-r--r-- | src/path/path-simplify.h | 28 | ||||
-rw-r--r-- | src/path/path-util.cpp | 199 | ||||
-rw-r--r-- | src/path/path-util.h | 43 |
13 files changed, 2952 insertions, 0 deletions
diff --git a/src/path/CMakeLists.txt b/src/path/CMakeLists.txt new file mode 100644 index 0000000..1b08e3f --- /dev/null +++ b/src/path/CMakeLists.txt @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + + +set(path_SRC + path-boolop.cpp + path-object-set.cpp + path-offset.cpp + path-outline.cpp + path-simplify.cpp + path-util.cpp + + # ------- + # Headers + path-boolop.h + path-offset.h + path-outline.h + path-simplify.h + path-util.h +) + +add_inkscape_source("${path_SRC}") diff --git a/src/path/README b/src/path/README new file mode 100644 index 0000000..309e2d6 --- /dev/null +++ b/src/path/README @@ -0,0 +1,11 @@ + + +This directory contains code related to path manipulations. + +To do: + +* Move all path manipulation related code here (livarot, etc.). +* Merge code into lib2geom if possible. +* Get rid of livarot +* Remove GUI dependencies. +* Find a better place to put directory. diff --git a/src/path/path-boolop.cpp b/src/path/path-boolop.cpp new file mode 100644 index 0000000..78f7281 --- /dev/null +++ b/src/path/path-boolop.cpp @@ -0,0 +1,956 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Boolean operations. + *//* + * Authors: + * see git history + * Created by fred on Fri Dec 05 2003. + * tweaked endlessly by bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> + +#include <glibmm/i18n.h> +#include <2geom/intersection-graph.h> +#include <2geom/svg-path-parser.h> // to get from SVG on boolean to Geom::Path +#include <2geom/utils.h> + +#include "path-boolop.h" +#include "path-util.h" + +#include "message-stack.h" +#include "path-chemistry.h" // copy_object_properties() + +#include "helper/geom.h" // pathv_to_linear_and_cubic_beziers() + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-flowtext.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "display/curve.h" + +#include "ui/icon-names.h" +#include "ui/widget/canvas.h" // Disable drawing during op + +#include "svg/svg.h" + +#include "xml/repr-sorting.h" + +using Inkscape::DocumentUndo; + +// fonctions utilitaires +bool +Ancetre(Inkscape::XML::Node *a, Inkscape::XML::Node *who) +{ + if (who == nullptr || a == nullptr) + return false; + if (who == a) + return true; + return Ancetre(a->parent(), who); +} + + +bool Inkscape::ObjectSet::pathUnion(const bool skip_undo, bool silent) { + BoolOpErrors result = pathBoolOp(bool_op_union, skip_undo, false, INKSCAPE_ICON("path-union"), + _("Union"), silent); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathIntersect(const bool skip_undo, bool silent) +{ + BoolOpErrors result = pathBoolOp(bool_op_inters, skip_undo, false, INKSCAPE_ICON("path-intersection"), + _("Intersection"), silent); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathDiff(const bool skip_undo, bool silent) +{ + BoolOpErrors result = pathBoolOp(bool_op_diff, skip_undo, false, INKSCAPE_ICON("path-difference"), + _("Difference"), silent); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathSymDiff(const bool skip_undo, bool silent) +{ + BoolOpErrors result = pathBoolOp(bool_op_symdiff, skip_undo, false, INKSCAPE_ICON("path-exclusion"), + _("Exclusion"), silent); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathCut(const bool skip_undo, bool silent) +{ + BoolOpErrors result = pathBoolOp(bool_op_cut, skip_undo, false, INKSCAPE_ICON("path-division"), + _("Division"), silent); + return DONE == result; +} + +bool +Inkscape::ObjectSet::pathSlice(const bool skip_undo, bool silent) +{ + BoolOpErrors result = pathBoolOp(bool_op_slice, skip_undo, false, INKSCAPE_ICON("path-cut"), + _("Cut path"), silent); + return DONE == result; +} + +// helper for printing error messages, regardless of whether we have a GUI or not +// If desktop == NULL, errors will be shown on stderr +static void +boolop_display_error_message(SPDesktop *desktop, Glib::ustring const &msg) +{ + if (desktop) { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, msg); + } else { + g_printerr("%s\n", msg.c_str()); + } +} + +/** + * Calculate the threshold for the given PathVector based + * on it's bounding box. + * + * @param path - The PathVector to calculate the threshold for. + * @param threshold - The starting threshold, usually 0.1 + */ +double get_threshold(Geom::PathVector const &path, double threshold) +{ + auto maybe_box = path.boundsFast(); + if (!maybe_box) + return threshold; + Geom::Rect box = *maybe_box; + double diagonal = Geom::distance( + Geom::Point(box[Geom::X].min(), box[Geom::Y].min()), + Geom::Point(box[Geom::X].max(), box[Geom::Y].max()) + ); + return threshold * (diagonal / 100); +} + +/** + * Calculate the threshold for the given SPItem/SPShape based + * on it's bounding box (see PathVector get_threshold above) + * + * @param item - The SPItem to calculate the threshold for. + * @param threshold - The starting threshold, usually 0.1 + */ +double get_threshold(SPItem const *item, double threshold) +{ + auto shape = dynamic_cast<SPShape const *>(item); + if (shape && shape->curve()) { + return get_threshold(shape->curve()->get_pathvector(), threshold); + } + return threshold; +} + +void +sp_flatten(Geom::PathVector &pathvector, FillRule fillkind) +{ + Path *orig = new Path; + orig->LoadPathVector(pathvector); + Shape *theShape = new Shape; + Shape *theRes = new Shape; + orig->ConvertWithBackData(1.0); + orig->Fill(theShape, 0); + theRes->ConvertToShape(theShape, fillkind); + Path *originaux[1]; + originaux[0] = orig; + Path *res = new Path; + theRes->ConvertToForme(res, 1, originaux, true); + + delete theShape; + delete theRes; + char *res_d = res->svg_dump_path(); + delete res; + delete orig; + pathvector = sp_svg_read_pathv(res_d); +} + +// boolean operations PathVectors A,B -> PathVector result. +// This is derived from sp_selected_path_boolop +// take the source paths from the file, do the operation, delete the originals and add the results +// fra,fra are fill_rules for PathVectors a,b +Geom::PathVector +sp_pathvector_boolop(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool_op bop, + fill_typ fra, fill_typ frb, bool livarotonly, bool flattenbefore) +{ + int error = 0; + return sp_pathvector_boolop(pathva, pathvb, bop, fra, frb, livarotonly, flattenbefore, error); +} + +Geom::PathVector +sp_pathvector_boolop(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool_op bop, + fill_typ fra, fill_typ frb, bool livarotonly, bool flattenbefore, int &error) +{ + if (!livarotonly) { + try { + Geom::PathVector a = pathv_to_linear_and_cubic_beziers(pathva); + Geom::PathVector b = pathv_to_linear_and_cubic_beziers(pathvb); + if (flattenbefore) { + sp_flatten(a, fra); + sp_flatten(b, frb); + } + Geom::PathVector out; + // dont change tolerande give errors on boolops + auto pig = Geom::PathIntersectionGraph(a, b, Geom::EPSILON); + if (bop == bool_op_inters) { + out = pig.getIntersection(); + } else if (bop == bool_op_union) { + out = pig.getUnion(); + + } else if (bop == bool_op_symdiff) { + out = pig.getXOR(); + } else if (bop == bool_op_diff) { + out = pig.getBminusA(); //livarot order... + } else if (bop == bool_op_cut) { + out = pig.getBminusA(); + auto tmp = pig.getIntersection(); + out.insert(out.end(), tmp.begin(), tmp.end()); + } else if (bop == bool_op_slice) { + // go to livarot + livarotonly = true; + } + if (!livarotonly) { + return out; + } + } catch (...) { + g_debug("Path Intersection Graph failed boolops, fallback to livarot"); + } + } + error = 1; + // extract the livarot Paths from the source objects + // also get the winding rule specified in the style + int nbOriginaux = 2; + std::vector<Path *> originaux(nbOriginaux); + std::vector<FillRule> origWind(nbOriginaux); + origWind[0]=fra; + origWind[1]=frb; + Geom::PathVector patht; + // Livarot's outline of arcs is broken. So convert the path to linear and cubics only, for which the outline is created correctly. + originaux[0] = Path_for_pathvector(pathv_to_linear_and_cubic_beziers( pathva)); + originaux[1] = Path_for_pathvector(pathv_to_linear_and_cubic_beziers( pathvb)); + + // some temporary instances, first + Shape *theShapeA = new Shape; + Shape *theShapeB = new Shape; + Shape *theShape = new Shape; + Path *res = new Path; + res->SetBackData(false); + + Path::cut_position *toCut=nullptr; + int nbToCut = 0; + + if ( bop == bool_op_inters || bop == bool_op_union || bop == bool_op_diff || bop == bool_op_symdiff ) { + // true boolean op + // get the polygons of each path, with the winding rule specified, and apply the operation iteratively + originaux[0]->ConvertWithBackData(get_threshold(pathva, 0.1)); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + originaux[1]->ConvertWithBackData(get_threshold(pathvb, 0.1)); + + originaux[1]->Fill(theShape, 1); + + theShapeB->ConvertToShape(theShape, origWind[1]); + + theShape->Booleen(theShapeB, theShapeA, bop); + + } else if ( bop == bool_op_cut ) { + // cuts= sort of a bastard boolean operation, thus not the axact same modus operandi + // technically, the cut path is not necessarily a polygon (thus has no winding rule) + // it is just uncrossed, and cleaned from duplicate edges and points + // then it's fed to Booleen() which will uncross it against the other path + // then comes the trick: each edge of the cut path is duplicated (one in each direction), + // thus making a polygon. the weight of the edges of the cut are all 0, but + // the Booleen need to invert the ones inside the source polygon (for the subsequent + // ConvertToForme) + + // the cut path needs to have the highest pathID in the back data + // that's how the Booleen() function knows it's an edge of the cut + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(get_threshold(pathva, 0.1)); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + originaux[1]->ConvertWithBackData(get_threshold(pathvb, 0.1)); + + originaux[1]->Fill(theShape, 1,false,false,false); //do not closeIfNeeded + + theShapeB->ConvertToShape(theShape, fill_justDont); // fill_justDont doesn't computes winding numbers + + // les elements arrivent en ordre inverse dans la liste + theShape->Booleen(theShapeB, theShapeA, bool_op_cut, 1); + + } else if ( bop == bool_op_slice ) { + // slice is not really a boolean operation + // you just put the 2 shapes in a single polygon, uncross it + // the points where the degree is > 2 are intersections + // just check it's an intersection on the path you want to cut, and keep it + // the intersections you have found are then fed to ConvertPositionsToMoveTo() which will + // make new subpath at each one of these positions + // inversion pour l'opération + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(get_threshold(pathva, 0.1)); + + originaux[0]->Fill(theShapeA, 0,false,false,false); // don't closeIfNeeded + + originaux[1]->ConvertWithBackData(get_threshold(pathvb, 0.1)); + + originaux[1]->Fill(theShapeA, 1,true,false,false);// don't closeIfNeeded and just dump in the shape, don't reset it + + theShape->ConvertToShape(theShapeA, fill_justDont); + + if ( theShape->hasBackData() ) { + // should always be the case, but ya never know + { + for (int i = 0; i < theShape->numberOfPoints(); i++) { + if ( theShape->getPoint(i).totalDegree() > 2 ) { + // possibly an intersection + // we need to check that at least one edge from the source path is incident to it + // before we declare it's an intersection + int cb = theShape->getPoint(i).incidentEdge[FIRST]; + int nbOrig=0; + int nbOther=0; + int piece=-1; + float t=0.0; + while ( cb >= 0 && cb < theShape->numberOfEdges() ) { + if ( theShape->ebData[cb].pathID == 0 ) { + // the source has an edge incident to the point, get its position on the path + piece=theShape->ebData[cb].pieceID; + if ( theShape->getEdge(cb).st == i ) { + t=theShape->ebData[cb].tSt; + } else { + t=theShape->ebData[cb].tEn; + } + nbOrig++; + } + if ( theShape->ebData[cb].pathID == 1 ) nbOther++; // the cut is incident to this point + cb=theShape->NextAt(i, cb); + } + if ( nbOrig > 0 && nbOther > 0 ) { + // point incident to both path and cut: an intersection + // note that you only keep one position on the source; you could have degenerate + // cases where the source crosses itself at this point, and you wouyld miss an intersection + toCut=(Path::cut_position*)realloc(toCut, (nbToCut+1)*sizeof(Path::cut_position)); + toCut[nbToCut].piece=piece; + toCut[nbToCut].t=t; + nbToCut++; + } + } + } + } + { + // i think it's useless now + int i = theShape->numberOfEdges() - 1; + for (;i>=0;i--) { + if ( theShape->ebData[i].pathID == 1 ) { + theShape->SubEdge(i); + } + } + } + + } + } + + int* nesting=nullptr; + int* conts=nullptr; + int nbNest=0; + // pour compenser le swap juste avant + if ( bop == bool_op_slice ) { +// theShape->ConvertToForme(res, nbOriginaux, originaux, true); +// res->ConvertForcedToMoveTo(); + res->Copy(originaux[0]); + res->ConvertPositionsToMoveTo(nbToCut, toCut); // cut where you found intersections + free(toCut); + } else if ( bop == bool_op_cut ) { + // il faut appeler pour desallouer PointData (pas vital, mais bon) + // the Booleen() function did not deallocate the point_data array in theShape, because this + // function needs it. + // this function uses the point_data to get the winding number of each path (ie: is a hole or not) + // for later reconstruction in objects, you also need to extract which path is parent of holes (nesting info) + theShape->ConvertToFormeNested(res, nbOriginaux, &originaux[0], 1, nbNest, nesting, conts); + } else { + theShape->ConvertToForme(res, nbOriginaux, &originaux[0]); + } + + delete theShape; + delete theShapeA; + delete theShapeB; + delete originaux[0]; + delete originaux[1]; + + gchar *result_str = res->svg_dump_path(); + Geom::PathVector outres = Geom::parse_svg_path(result_str); + g_free(result_str); + + delete res; + return outres; +} + +/** + * Workaround for buggy Path::Transform() which incorrectly transforms arc commands. + * + * TODO: Fix PathDescrArcTo::transform() and then remove this workaround. + */ +static void transformLivarotPath(Path *res, Geom::Affine const &affine) +{ + res->LoadPathVector(res->MakePathVector() * affine); +} + +// boolean operations on the desktop +// take the source paths from the file, do the operation, delete the originals and add the results +BoolOpErrors Inkscape::ObjectSet::pathBoolOp(bool_op bop, const bool skip_undo, const bool checked, + const Glib::ustring icon_name, const Glib::ustring description, + bool const silent) +{ + if (nullptr != desktop() && !checked) { + SPDocument *doc = desktop()->getDocument(); + // don't redraw the canvas during the operation as that can remarkably slow down the progress + desktop()->getCanvas()->set_drawing_disabled(true); + BoolOpErrors returnCode = ObjectSet::pathBoolOp(bop, true, true,icon_name); + desktop()->getCanvas()->set_drawing_disabled(false); + + switch(returnCode) { + case ERR_TOO_LESS_PATHS_1: + if (!silent) { + boolop_display_error_message(desktop(), + _("Select <b>at least 1 path</b> to perform a boolean union.")); + } + break; + case ERR_TOO_LESS_PATHS_2: + if (!silent) { + boolop_display_error_message(desktop(), + _("Select <b>at least 2 paths</b> to perform a boolean operation.")); + } + break; + case ERR_NO_PATHS: + if (!silent) { + boolop_display_error_message(desktop(), + _("One of the objects is <b>not a path</b>, cannot perform boolean operation.")); + } + break; + case ERR_Z_ORDER: + if (!silent) { + boolop_display_error_message(desktop(), + _("Unable to determine the <b>z-order</b> of the objects selected for difference, XOR, division, or path cut.")); + } + break; + case DONE_NO_PATH: + if (!skip_undo) { + DocumentUndo::done(doc, description, ""); + } + break; + case DONE: + if (!skip_undo) { + DocumentUndo::done(doc, description, icon_name); + } + break; + case DONE_NO_ACTION: + // Do nothing (?) + break; + } + return returnCode; + } + + SPDocument *doc = document(); + std::vector<SPItem*> il(items().begin(), items().end()); + + // allow union on a single object for the purpose of removing self overlapse (svn log, revision 13334) + if (il.size() < 2 && bop != bool_op_union) { + return ERR_TOO_LESS_PATHS_2; + } + else if (il.size() < 1) { + return ERR_TOO_LESS_PATHS_1; + } + + g_assert(!il.empty()); + + // reverseOrderForOp marks whether the order of the list is the top->down order + // it's only used when there are 2 objects, and for operations who need to know the + // topmost object (differences, cuts) + bool reverseOrderForOp = false; + + if (bop == bool_op_diff || bop == bool_op_cut || bop == bool_op_slice) { + // check in the tree to find which element of the selection list is topmost (for 2-operand commands only) + Inkscape::XML::Node *a = il.front()->getRepr(); + Inkscape::XML::Node *b = il.back()->getRepr(); + + if (a == nullptr || b == nullptr) { + return ERR_Z_ORDER; + } + + if (Ancetre(a, b)) { + // a is the parent of b, already in the proper order + } else if (Ancetre(b, a)) { + // reverse order + reverseOrderForOp = true; + } else { + + // objects are not in parent/child relationship; + // find their lowest common ancestor + Inkscape::XML::Node *parent = LCA(a, b); + if (parent == nullptr) { + return ERR_Z_ORDER; + } + + // find the children of the LCA that lead from it to the a and b + Inkscape::XML::Node *as = AncetreFils(a, parent); + Inkscape::XML::Node *bs = AncetreFils(b, parent); + + // find out which comes first + for (Inkscape::XML::Node *child = parent->firstChild(); child; child = child->next()) { + if (child == as) { + /* a first, so reverse. */ + reverseOrderForOp = true; + break; + } + if (child == bs) + break; + } + } + } + + g_assert(!il.empty()); + + // first check if all the input objects have shapes + // otherwise bail out + for (auto item : il) + { + if (!SP_IS_SHAPE(item) && !SP_IS_TEXT(item) && !SP_IS_FLOWTEXT(item)) + { + return ERR_NO_PATHS; + } + } + + // extract the livarot Paths from the source objects + // also get the winding rule specified in the style + int nbOriginaux = il.size(); + std::vector<Path *> originaux(nbOriginaux); + std::vector<FillRule> origWind(nbOriginaux); + int curOrig; + { + curOrig = 0; + for (auto item : il) + { + // apply live path effects prior to performing boolean operation + char const *id = item->getAttribute("id"); + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + SPDocument * document = item->document; + lpeitem->removeAllPathEffects(true); + SPObject *elemref = document->getObjectById(id); + if (elemref && elemref != item) { + // If the LPE item is a shape, it is converted to a path + // so we need to reupdate the item + item = dynamic_cast<SPItem *>(elemref); + } + } + SPCSSAttr *css = sp_repr_css_attr(reinterpret_cast<SPObject *>(il[0])->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) { + origWind[curOrig]= fill_nonZero; + } else if (val && strcmp(val, "evenodd") == 0) { + origWind[curOrig]= fill_oddEven; + } else { + origWind[curOrig]= fill_nonZero; + } + + originaux[curOrig] = Path_for_item(item, true, true); + if (originaux[curOrig] == nullptr || originaux[curOrig]->descr_cmd.size() <= 1) + { + for (int i = curOrig; i >= 0; i--) delete originaux[i]; + return DONE_NO_ACTION; + } + curOrig++; + } + } + // reverse if needed + // note that the selection list keeps its order + if ( reverseOrderForOp ) { + std::swap(originaux[0], originaux[1]); + std::swap(origWind[0], origWind[1]); + } + + // and work + // some temporary instances, first + Shape *theShapeA = new Shape; + Shape *theShapeB = new Shape; + Shape *theShape = new Shape; + Path *res = new Path; + res->SetBackData(false); + Path::cut_position *toCut=nullptr; + int nbToCut=0; + + if ( bop == bool_op_inters || bop == bool_op_union || bop == bool_op_diff || bop == bool_op_symdiff ) { + // true boolean op + // get the polygons of each path, with the winding rule specified, and apply the operation iteratively + originaux[0]->ConvertWithBackData(get_threshold(il[0], 0.1)); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + curOrig = 1; + for (auto item : il){ + if(item==il[0])continue; + originaux[curOrig]->ConvertWithBackData(get_threshold(item, 0.1)); + + originaux[curOrig]->Fill(theShape, curOrig); + + theShapeB->ConvertToShape(theShape, origWind[curOrig]); + + /* Due to quantization of the input shape coordinates, we may end up with A or B being empty. + * If this is a union or symdiff operation, we just use the non-empty shape as the result: + * A=0 => (0 or B) == B + * B=0 => (A or 0) == A + * A=0 => (0 xor B) == B + * B=0 => (A xor 0) == A + * If this is an intersection operation, we just use the empty shape as the result: + * A=0 => (0 and B) == 0 == A + * B=0 => (A and 0) == 0 == B + * If this a difference operation, and the upper shape (A) is empty, we keep B. + * If the lower shape (B) is empty, we still keep B, as it's empty: + * A=0 => (B - 0) == B + * B=0 => (0 - A) == 0 == B + * + * In any case, the output from this operation is stored in shape A, so we may apply + * the above rules simply by judicious use of swapping A and B where necessary. + */ + bool zeroA = theShapeA->numberOfEdges() == 0; + bool zeroB = theShapeB->numberOfEdges() == 0; + if (zeroA || zeroB) { + // We might need to do a swap. Apply the above rules depending on operation type. + bool resultIsB = ((bop == bool_op_union || bop == bool_op_symdiff) && zeroA) + || ((bop == bool_op_inters) && zeroB) + || (bop == bool_op_diff); + if (resultIsB) { + // Swap A and B to use B as the result + Shape *swap = theShapeB; + theShapeB = theShapeA; + theShapeA = swap; + } + } else { + // Just do the Boolean operation as usual + // les elements arrivent en ordre inverse dans la liste + theShape->Booleen(theShapeB, theShapeA, bop); + Shape *swap = theShape; + theShape = theShapeA; + theShapeA = swap; + } + curOrig++; + } + + { + Shape *swap = theShape; + theShape = theShapeA; + theShapeA = swap; + } + + } else if ( bop == bool_op_cut ) { + // cuts= sort of a bastard boolean operation, thus not the axact same modus operandi + // technically, the cut path is not necessarily a polygon (thus has no winding rule) + // it is just uncrossed, and cleaned from duplicate edges and points + // then it's fed to Booleen() which will uncross it against the other path + // then comes the trick: each edge of the cut path is duplicated (one in each direction), + // thus making a polygon. the weight of the edges of the cut are all 0, but + // the Booleen need to invert the ones inside the source polygon (for the subsequent + // ConvertToForme) + + // the cut path needs to have the highest pathID in the back data + // that's how the Booleen() function knows it's an edge of the cut + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(get_threshold(il[0], 0.1)); + + originaux[0]->Fill(theShape, 0); + + theShapeA->ConvertToShape(theShape, origWind[0]); + + originaux[1]->ConvertWithBackData(get_threshold(il[1], 0.1)); + + if ((originaux[1]->pts.size() == 2) && originaux[1]->pts[0].isMoveTo && !originaux[1]->pts[1].isMoveTo) + originaux[1]->Fill(theShape, 1,false,true,false); // see LP Bug 177956 + else + originaux[1]->Fill(theShape, 1,false,false,false); //do not closeIfNeeded + + theShapeB->ConvertToShape(theShape, fill_justDont); // fill_justDont doesn't computes winding numbers + + // les elements arrivent en ordre inverse dans la liste + theShape->Booleen(theShapeB, theShapeA, bool_op_cut, 1); + + } else if ( bop == bool_op_slice ) { + // slice is not really a boolean operation + // you just put the 2 shapes in a single polygon, uncross it + // the points where the degree is > 2 are intersections + // just check it's an intersection on the path you want to cut, and keep it + // the intersections you have found are then fed to ConvertPositionsToMoveTo() which will + // make new subpath at each one of these positions + // inversion pour l'opération + { + Path* swap=originaux[0];originaux[0]=originaux[1];originaux[1]=swap; + int swai=origWind[0];origWind[0]=origWind[1];origWind[1]=(fill_typ)swai; + } + originaux[0]->ConvertWithBackData(get_threshold(il[0], 0.1)); + + originaux[0]->Fill(theShapeA, 0,false,false,false); // don't closeIfNeeded + + originaux[1]->ConvertWithBackData(get_threshold(il[1], 0.1)); + + originaux[1]->Fill(theShapeA, 1,true,false,false);// don't closeIfNeeded and just dump in the shape, don't reset it + + theShape->ConvertToShape(theShapeA, fill_justDont); + + if ( theShape->hasBackData() ) { + // should always be the case, but ya never know + { + for (int i = 0; i < theShape->numberOfPoints(); i++) { + if ( theShape->getPoint(i).totalDegree() > 2 ) { + // possibly an intersection + // we need to check that at least one edge from the source path is incident to it + // before we declare it's an intersection + int cb = theShape->getPoint(i).incidentEdge[FIRST]; + int nbOrig=0; + int nbOther=0; + int piece=-1; + float t=0.0; + while ( cb >= 0 && cb < theShape->numberOfEdges() ) { + if ( theShape->ebData[cb].pathID == 0 ) { + // the source has an edge incident to the point, get its position on the path + piece=theShape->ebData[cb].pieceID; + if ( theShape->getEdge(cb).st == i ) { + t=theShape->ebData[cb].tSt; + } else { + t=theShape->ebData[cb].tEn; + } + nbOrig++; + } + if ( theShape->ebData[cb].pathID == 1 ) nbOther++; // the cut is incident to this point + cb=theShape->NextAt(i, cb); + } + if ( nbOrig > 0 && nbOther > 0 ) { + // point incident to both path and cut: an intersection + // note that you only keep one position on the source; you could have degenerate + // cases where the source crosses itself at this point, and you wouyld miss an intersection + toCut=(Path::cut_position*)realloc(toCut, (nbToCut+1)*sizeof(Path::cut_position)); + toCut[nbToCut].piece=piece; + toCut[nbToCut].t=t; + nbToCut++; + } + } + } + } + { + // i think it's useless now + int i = theShape->numberOfEdges() - 1; + for (;i>=0;i--) { + if ( theShape->ebData[i].pathID == 1 ) { + theShape->SubEdge(i); + } + } + } + + } + } + + int* nesting=nullptr; + int* conts=nullptr; + int nbNest=0; + // pour compenser le swap juste avant + if ( bop == bool_op_slice ) { +// theShape->ConvertToForme(res, nbOriginaux, originaux, true); +// res->ConvertForcedToMoveTo(); + res->Copy(originaux[0]); + res->ConvertPositionsToMoveTo(nbToCut, toCut); // cut where you found intersections + free(toCut); + } else if ( bop == bool_op_cut ) { + // il faut appeler pour desallouer PointData (pas vital, mais bon) + // the Booleen() function did not deallocate the point_data array in theShape, because this + // function needs it. + // this function uses the point_data to get the winding number of each path (ie: is a hole or not) + // for later reconstruction in objects, you also need to extract which path is parent of holes (nesting info) + theShape->ConvertToFormeNested(res, nbOriginaux, &originaux[0], 1, nbNest, nesting, conts); + } else { + theShape->ConvertToForme(res, nbOriginaux, &originaux[0]); + } + + delete theShape; + delete theShapeA; + delete theShapeB; + for (int i = 0; i < nbOriginaux; i++) delete originaux[i]; + + if (res->descr_cmd.size() <= 1) + { + // only one command, presumably a moveto: it isn't a path + for (auto l : il){ + l->deleteObject(); + } + clear(); + + delete res; + return DONE_NO_PATH; + } + + // get the source path object + SPObject *source; + if ( bop == bool_op_diff || bop == bool_op_cut || bop == bool_op_slice ) { + if (reverseOrderForOp) { + source = il[0]; + } else { + source = il.back(); + } + } else { + // find out the bottom object + std::vector<Inkscape::XML::Node*> sorted(xmlNodes().begin(), xmlNodes().end()); + + sort(sorted.begin(),sorted.end(),sp_repr_compare_position_bool); + + source = doc->getObjectByRepr(sorted.front()); + } + + // adjust style properties that depend on a possible transform in the source object in order + // to get a correct style attribute for the new path + SPItem* item_source = SP_ITEM(source); + Geom::Affine i2doc(item_source->i2doc_affine()); + + Inkscape::XML::Node *repr_source = source->getRepr(); + + // remember important aspects of the source path, to be restored + gint pos = repr_source->position(); + Inkscape::XML::Node *parent = repr_source->parent(); + // remove source paths + clear(); + for (auto l : il){ + if (l != item_source) { + // delete the object for real, so that its clones can take appropriate action + l->deleteObject(); + } + } + + auto const source2doc_inverse = i2doc.inverse(); + char const *const old_transform_attibute = repr_source->attribute("transform"); + + // now that we have the result, add it on the canvas + if ( bop == bool_op_cut || bop == bool_op_slice ) { + int nbRP=0; + Path** resPath; + if ( bop == bool_op_slice ) { + // there are moveto's at each intersection, but it's still one unique path + // so break it down and add each subpath independently + // we could call break_apart to do this, but while we have the description... + resPath=res->SubPaths(nbRP, false); + } else { + // cut operation is a bit wicked: you need to keep holes + // that's why you needed the nesting + // ConvertToFormeNested() dumped all the subpath in a single Path "res", so we need + // to get the path for each part of the polygon. that's why you need the nesting info: + // to know in which subpath to add a subpath + resPath=res->SubPathsWithNesting(nbRP, true, nbNest, nesting, conts); + + // cleaning + if ( conts ) free(conts); + if ( nesting ) free(nesting); + } + + // add all the pieces resulting from cut or slice + std::vector <Inkscape::XML::Node*> selection; + for (int i=0;i<nbRP;i++) { + transformLivarotPath(resPath[i], source2doc_inverse); + gchar *d = resPath[i]->svg_dump_path(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, repr_source); + + // Delete source on last iteration (after we don't need repr_source anymore). As a consequence, the last + // item will inherit the original's id. + if (i + 1 == nbRP) { + item_source->deleteObject(false); + } + + repr->setAttribute("d", d); + g_free(d); + + // for slice, remove fill + if (bop == bool_op_slice) { + SPCSSAttr *css; + + css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "fill", "none"); + + sp_repr_css_change(repr, css, "style"); + + sp_repr_css_attr_unref(css); + } + + repr->setAttributeOrRemoveIfEmpty("transform", old_transform_attibute); + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + selection.push_back(repr); + Inkscape::GC::release(repr); + + delete resPath[i]; + } + setReprList(selection); + if ( resPath ) free(resPath); + + } else { + transformLivarotPath(res, source2doc_inverse); + gchar *d = res->svg_dump_path(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, repr_source); + + // delete it so that its clones don't get alerted; this object will be restored shortly, with the same id + item_source->deleteObject(false); + + repr->setAttribute("d", d); + g_free(d); + + repr->setAttributeOrRemoveIfEmpty("transform", old_transform_attibute); + + parent->addChildAtPos(repr, pos); + + set(repr); + Inkscape::GC::release(repr); + } + + delete res; + + return DONE; +} + +/* + 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:textwidth=99 : diff --git a/src/path/path-boolop.h b/src/path/path-boolop.h new file mode 100644 index 0000000..f587590 --- /dev/null +++ b/src/path/path-boolop.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Boolean operations. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef PATH_BOOLOP_H +#define PATH_BOOLOP_H + +#include <2geom/path.h> +#include "livarot/Path.h" // FillRule +#include "object/object-set.h" // bool_op + +void sp_flatten(Geom::PathVector &pathvector, FillRule fillkind); +Geom::PathVector sp_pathvector_boolop(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool_op bop, + FillRule fra, FillRule frb, bool livarotonly, bool flattenbefore, int &error); +Geom::PathVector sp_pathvector_boolop(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool_op bop, + FillRule fra, FillRule frb, bool livarotonly = false, bool flattenbefore = true); + +#endif // PATH_BOOLOP_H + +/* + 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:textwidth=99 : diff --git a/src/path/path-object-set.cpp b/src/path/path-object-set.cpp new file mode 100644 index 0000000..f5d0770 --- /dev/null +++ b/src/path/path-object-set.cpp @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * + * Path related functions for ObjectSet. + * + * Copyright (C) 2020 Tavmjong Bah + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * TODO: Move related code from path-chemistry.cpp + * + */ + +#include <glibmm/i18n.h> + +#include "document-undo.h" +#include "message-stack.h" + +#include "attribute-rel-util.h" + +#include "object/object-set.h" +#include "path/path-outline.h" +#include "path/path-simplify.h" +#include "ui/icon-names.h" + +using Inkscape::ObjectSet; + +bool +ObjectSet::strokesToPaths(bool legacy, bool skip_undo) +{ + if (desktop() && isEmpty()) { + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>stroked path(s)</b> to convert stroke to path.")); + return false; + } + + bool did = false; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + if (prefs->getBool("/options/pathoperationsunlink/value", true)) { + did = unlinkRecursive(true); + } + + // Need to turn on stroke scaling to ensure stroke is scaled when transformed! + bool scale_stroke = prefs->getBool("/options/transform/stroke", true); + prefs->setBool("/options/transform/stroke", true); + + + std::vector<SPItem *> my_items(items().begin(), items().end()); + + for (auto item : my_items) { + // Do not remove the object from the selection here + // as we want to keep it selected if the whole operation fails + Inkscape::XML::Node *new_node = item_to_paths(item, legacy); + if (new_node) { + SPObject* new_item = document()->getObjectByRepr(new_node); + + // Markers don't inherit properties from outside the + // marker. When converted to paths objects they need to be + // protected from inheritance. This is why (probably) the stroke + // to path code uses SP_STYLE_FLAG_ALWAYS when defining the + // style of the fill and stroke during the conversion. This + // means the style contains every possible property. Once we've + // finished the stroke to path conversion, we can eliminate + // unneeded properties from the style element. + sp_attribute_clean_recursive(new_node, SP_ATTRCLEAN_STYLE_REMOVE | SP_ATTRCLEAN_DEFAULT_REMOVE); + + add(new_item); // Add to selection. + did = true; + } + } + + // Reset + prefs->setBool("/options/transform/stroke", scale_stroke); + + if (desktop() && !did) { + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No stroked paths</b> in the selection.")); + } + + if (did && !skip_undo) { + Inkscape::DocumentUndo::done(document(), _("Convert stroke to path"), ""); + } else if (!did && !skip_undo) { + Inkscape::DocumentUndo::cancel(document()); + } + + return did; +} + +bool +ObjectSet::simplifyPaths(bool skip_undo) +{ + if (desktop() && isEmpty()) { + desktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>path(s)</b> to simplify.")); + return false; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double threshold = prefs->getDouble("/options/simplifythreshold/value", 0.003); + bool justCoalesce = prefs->getBool( "/options/simplifyjustcoalesce/value", false); + + // Keep track of accelerated simplify + static gint64 previous_time = 0; + static gdouble multiply = 1.0; + + // Get the current time + gint64 current_time = g_get_monotonic_time(); + + // Was the previous call to this function recent? (<0.5 sec) + if (previous_time > 0 && current_time - previous_time < 500000) { + + // add to the threshold 1/2 of its original value + multiply += 0.5; + threshold *= multiply; + + } else { + // reset to the default + multiply = 1; + } + + // Remember time for next call + previous_time = current_time; + + // set "busy" cursor + if (desktop()) { + desktop()->setWaitingCursor(); + } + + Geom::OptRect selectionBbox = visualBounds(); + if (!selectionBbox) { + std::cerr << "ObjectSet::: selection has no visual bounding box!" << std::endl; + return false; + } + double size = L2(selectionBbox->dimensions()); + + int pathsSimplified = 0; + std::vector<SPItem *> my_items(items().begin(), items().end()); + for (auto item : my_items) { + pathsSimplified += path_simplify(item, threshold, justCoalesce, size); + } + + if (pathsSimplified > 0 && !skip_undo) { + DocumentUndo::done(document(), _("Simplify"), INKSCAPE_ICON("path-simplify")); + } + + if (desktop()) { + desktop()->clearWaitingCursor(); + if (pathsSimplified > 0) { + desktop()->messageStack()->flashF(Inkscape::NORMAL_MESSAGE, _("<b>%d</b> paths simplified."), pathsSimplified); + } else { + desktop()->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No paths</b> to simplify in the selection.")); + } + } + + return (pathsSimplified > 0); +} + +/* + 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:textwidth=99 : diff --git a/src/path/path-offset.cpp b/src/path/path-offset.cpp new file mode 100644 index 0000000..6607bc9 --- /dev/null +++ b/src/path/path-offset.cpp @@ -0,0 +1,475 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path offsets. + *//* + * Authors: + * see git history + * Created by fred on Fri Dec 05 2003. + * tweaked endlessly by bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * contains lots of stitched pieces of path-chemistry.c + */ + +#include <vector> + +#include <glibmm/i18n.h> + +#include "path-offset.h" +#include "path-util.h" + +#include "message-stack.h" +#include "path-chemistry.h" // copy_object_properties() +#include "selection.h" + +#include "display/curve.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-flowtext.h" +#include "object/sp-path.h" +#include "object/sp-text.h" + +#include "ui/icon-names.h" + +#include "style.h" + +#define MIN_OFFSET 0.01 + +using Inkscape::DocumentUndo; + +void sp_selected_path_do_offset(SPDesktop *desktop, bool expand, double prefOffset); +void sp_selected_path_create_offset_object(SPDesktop *desktop, int expand, bool updating); + +void +sp_selected_path_offset(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double prefOffset = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + + sp_selected_path_do_offset(desktop, true, prefOffset); +} +void +sp_selected_path_inset(SPDesktop *desktop) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double prefOffset = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + + sp_selected_path_do_offset(desktop, false, prefOffset); +} + +void +sp_selected_path_offset_screen(SPDesktop *desktop, double pixels) +{ + sp_selected_path_do_offset(desktop, true, pixels / desktop->current_zoom()); +} + +void +sp_selected_path_inset_screen(SPDesktop *desktop, double pixels) +{ + sp_selected_path_do_offset(desktop, false, pixels / desktop->current_zoom()); +} + + +void sp_selected_path_create_offset_object_zero(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 0, false); +} + +void sp_selected_path_create_offset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 1, false); +} +void sp_selected_path_create_inset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, -1, false); +} + +void sp_selected_path_create_updating_offset_object_zero(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 0, true); +} + +void sp_selected_path_create_updating_offset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, 1, true); +} + +void sp_selected_path_create_updating_inset(SPDesktop *desktop) +{ + sp_selected_path_create_offset_object(desktop, -1, true); +} + +void sp_selected_path_create_offset_object(SPDesktop *desktop, int expand, bool updating) +{ + Inkscape::Selection *selection = desktop->getSelection(); + SPItem *item = selection->singleItem(); + + if (auto shape = dynamic_cast<SPShape const *>(item)) { + if (!shape->curve()) + return; + } else if (auto text = dynamic_cast<SPText const *>(item)) { + if (!text->getNormalizedBpath()) + return; + } else { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("Selected object is <b>not a path</b>, cannot inset/outset.")); + return; + } + + Geom::Affine const transform(item->transform); + auto scaling_factor = item->i2doc_affine().descrim(); + + item->doWriteTransform(Geom::identity()); + + // remember the position of the item + gint pos = item->getRepr()->position(); + + // remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + + float o_width = 0; + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + o_width = prefs->getDouble("/options/defaultoffsetwidth/value", 1.0, "px"); + o_width /= scaling_factor; + if (scaling_factor == 0 || o_width < MIN_OFFSET) { + o_width = MIN_OFFSET; + } + } + + Path *orig = Path_for_item(item, true, false); + if (orig == nullptr) + { + return; + } + + Path *res = new Path; + res->SetBackData(false); + + { + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + orig->ConvertWithBackData(1.0); + orig->Fill(theShape, 0); + + SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + else if (val && strcmp(val, "evenodd") == 0) + { + theRes->ConvertToShape(theShape, fill_oddEven); + } + else + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + + Path *originaux[1]; + originaux[0] = orig; + theRes->ConvertToForme(res, 1, originaux); + + delete theShape; + delete theRes; + } + + if (res->descr_cmd.size() <= 1) + { + // pas vraiment de points sur le resultat + // donc il ne reste rien + DocumentUndo::done(desktop->getDocument(), + (updating ? _("Create linked offset") + : _("Create dynamic offset")), + (updating ? INKSCAPE_ICON("path-offset-linked") + : INKSCAPE_ICON("path-offset-dynamic"))); + selection->clear(); + + delete res; + delete orig; + return; + } + + { + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("svg:path"); + + if (!updating) { + Inkscape::copy_object_properties(repr, item->getRepr()); + } else { + gchar const *style = item->getRepr()->attribute("style"); + repr->setAttribute("style", style); + } + + repr->setAttribute("sodipodi:type", "inkscape:offset"); + repr->setAttributeSvgDouble("inkscape:radius", ( expand > 0 + ? o_width + : expand < 0 + ? -o_width + : 0 )); + + gchar *str = res->svg_dump_path(); + repr->setAttribute("inkscape:original", str); + g_free(str); + str = nullptr; + + if ( updating ) { + + //XML Tree being used directly here while it shouldn't be + item->doWriteTransform(transform); + char const *id = item->getRepr()->attribute("id"); + char const *uri = g_strdup_printf("#%s", id); + repr->setAttribute("xlink:href", uri); + g_free((void *) uri); + } else { + repr->removeAttribute("inkscape:href"); + // delete original + item->deleteObject(false); + } + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + SPItem *nitem = reinterpret_cast<SPItem *>(desktop->getDocument()->getObjectByRepr(repr)); + + if ( !updating ) { + // apply the transform to the offset + nitem->doWriteTransform(transform); + } + + // The object just created from a temporary repr is only a seed. + // We need to invoke its write which will update its real repr (in particular adding d=) + nitem->updateRepr(); + + Inkscape::GC::release(repr); + + selection->set(nitem); + } + + DocumentUndo::done(desktop->getDocument(), + (updating ? _("Create linked offset") + : _("Create dynamic offset")), + (updating ? INKSCAPE_ICON("path-offset-linked") + : INKSCAPE_ICON("path-offset-dynamic"))); + + delete res; + delete orig; +} + +/** + * Apply offset to selected paths + * @param desktop Targeted desktop + * @param expand True if offset expands, False if it shrinks paths + * @param prefOffset Size of offset in pixels + */ +void +sp_selected_path_do_offset(SPDesktop *desktop, bool expand, double prefOffset) +{ + Inkscape::Selection *selection = desktop->getSelection(); + + if (selection->isEmpty()) { + desktop->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Select <b>path(s)</b> to inset/outset.")); + return; + } + + bool did = false; + std::vector<SPItem*> il(selection->items().begin(), selection->items().end()); + for (auto item : il){ + if (auto shape = dynamic_cast<SPShape const *>(item)) { + if (!shape->curve()) + continue; + } else if (auto text = dynamic_cast<SPText const *>(item)) { + if (!text->getNormalizedBpath()) + continue; + } else if (auto text = dynamic_cast<SPFlowtext const *>(item)) { + if (!text->getNormalizedBpath()) + continue; + } else { + continue; + } + + Geom::Affine const transform(item->transform); + auto scaling_factor = item->i2doc_affine().descrim(); + + item->doWriteTransform(Geom::identity()); + + float o_width = 0; + float o_miter = 0; + JoinType o_join = join_straight; + //ButtType o_butt = butt_straight; + + { + SPStyle *i_style = item->style; + int jointype = i_style->stroke_linejoin.value; + + switch (jointype) { + case SP_STROKE_LINEJOIN_MITER: + o_join = join_pointy; + break; + case SP_STROKE_LINEJOIN_ROUND: + o_join = join_round; + break; + default: + o_join = join_straight; + break; + } + + // scale to account for transforms and document units + o_width = prefOffset / scaling_factor; + + if (scaling_factor == 0 || o_width < MIN_OFFSET) { + o_width = MIN_OFFSET; + } + o_miter = i_style->stroke_miterlimit.value * o_width; + } + + Path *orig = Path_for_item(item, false); + if (orig == nullptr) { + continue; + } + + Path *res = new Path; + res->SetBackData(false); + + { + Shape *theShape = new Shape; + Shape *theRes = new Shape; + + orig->ConvertWithBackData(0.03); + orig->Fill(theShape, 0); + + SPCSSAttr *css = sp_repr_css_attr(item->getRepr(), "style"); + gchar const *val = sp_repr_css_property(css, "fill-rule", nullptr); + if (val && strcmp(val, "nonzero") == 0) + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + else if (val && strcmp(val, "evenodd") == 0) + { + theRes->ConvertToShape(theShape, fill_oddEven); + } + else + { + theRes->ConvertToShape(theShape, fill_nonZero); + } + + // et maintenant: offset + // methode inexacte +/* Path *originaux[1]; + originaux[0] = orig; + theRes->ConvertToForme(res, 1, originaux); + + if (expand) { + res->OutsideOutline(orig, 0.5 * o_width, o_join, o_butt, o_miter); + } else { + res->OutsideOutline(orig, -0.5 * o_width, o_join, o_butt, o_miter); + } + + orig->ConvertWithBackData(1.0); + orig->Fill(theShape, 0); + theRes->ConvertToShape(theShape, fill_positive); + originaux[0] = orig; + theRes->ConvertToForme(res, 1, originaux); + + if (o_width >= 0.5) { + // res->Coalesce(1.0); + res->ConvertEvenLines(1.0); + res->Simplify(1.0); + } else { + // res->Coalesce(o_width); + res->ConvertEvenLines(1.0*o_width); + res->Simplify(1.0 * o_width); + } */ + // methode par makeoffset + + if (expand) + { + theShape->MakeOffset(theRes, o_width, o_join, o_miter); + } + else + { + theShape->MakeOffset(theRes, -o_width, o_join, o_miter); + } + theRes->ConvertToShape(theShape, fill_positive); + + res->Reset(); + theRes->ConvertToForme(res); + + res->ConvertEvenLines(0.1); + res->Simplify(0.1); + + delete theShape; + delete theRes; + } + + did = true; + + // remember the position of the item + gint pos = item->getRepr()->position(); + // remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + + selection->remove(item); + + Inkscape::XML::Node *repr = nullptr; + + if (res->descr_cmd.size() > 1) { // if there's 0 or 1 node left, drop this path altogether + Inkscape::XML::Document *xml_doc = desktop->doc()->getReprDoc(); + repr = xml_doc->createElement("svg:path"); + + Inkscape::copy_object_properties(repr, item->getRepr()); + } + + item->deleteObject(false); + + if (repr) { + gchar *str = res->svg_dump_path(); + repr->setAttribute("d", str); + g_free(str); + + // add the new repr to the parent + // move to the saved position + parent->addChildAtPos(repr, pos); + + SPItem *newitem = (SPItem *) desktop->getDocument()->getObjectByRepr(repr); + + // reapply the transform + newitem->doWriteTransform(transform); + + selection->add(repr); + + Inkscape::GC::release(repr); + } + + delete orig; + delete res; + } + + if (did) { + DocumentUndo::done(desktop->getDocument(), + (expand ? _("Outset path") : _("Inset path")), + (expand ? INKSCAPE_ICON("path-outset") : INKSCAPE_ICON("path-inset"))); + } else { + desktop->messageStack()->flash(Inkscape::ERROR_MESSAGE, _("<b>No paths</b> to inset/outset in the selection.")); + return; + } +} + +/* + 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:textwidth=99 : diff --git a/src/path/path-offset.h b/src/path/path-offset.h new file mode 100644 index 0000000..d4996bc --- /dev/null +++ b/src/path/path-offset.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path offsets. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef PATH_OFFSET_H +#define PATH_OFFSET_H + +class SPDesktop; + +// offset/inset of a curve +// takes the fill-rule in consideration +// offset amount is the stroke-width of the curve +void sp_selected_path_offset (SPDesktop *desktop); +void sp_selected_path_offset_screen (SPDesktop *desktop, double pixels); +void sp_selected_path_inset (SPDesktop *desktop); +void sp_selected_path_inset_screen (SPDesktop *desktop, double pixels); +void sp_selected_path_create_offset (SPDesktop *desktop); +void sp_selected_path_create_inset (SPDesktop *desktop); +void sp_selected_path_create_updating_offset (SPDesktop *desktop); +void sp_selected_path_create_updating_inset (SPDesktop *desktop); + +void sp_selected_path_create_offset_object_zero (SPDesktop *desktop); +void sp_selected_path_create_updating_offset_object_zero (SPDesktop *desktop); + +#endif // PATH_OFFSET_H + +/* + 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:textwidth=99 : diff --git a/src/path/path-outline.cpp b/src/path/path-outline.cpp new file mode 100644 index 0000000..29913ec --- /dev/null +++ b/src/path/path-outline.cpp @@ -0,0 +1,795 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * + * Two related object to path operations: + * + * 1. Find a path that includes fill, stroke, and markers. Useful for finding a visual bounding box. + * 2. Take a set of objects and find an identical visual representation using only paths. + * + * Copyright (C) 2020 Tavmjong Bah + * Copyright (C) 2018 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * Code moved from splivarot.cpp + * + */ + +#include "path-outline.h" + +#include <vector> + +#include "path-chemistry.h" // Should be moved to path directory +#include "message-stack.h" // Should be removed. +#include "selection.h" +#include "style.h" + +#include "display/curve.h" // Should be moved to path directory + +#include "helper/geom.h" // pathv_to_linear_and_cubic() + +#include "livarot/LivarotDefs.h" +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/object-set.h" +#include "object/box3d.h" +#include "object/sp-item.h" +#include "object/sp-marker.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-flowtext.h" + +#include "svg/svg.h" + +/** + * Given an item, find a path representing the fill and a path representing the stroke. + * Returns true if fill path found. Item may not have a stroke in which case stroke path is empty. + * bbox_only==true skips cleaning up the stroke path. + * Encapsulates use of livarot. + */ +bool +item_find_paths(const SPItem *item, Geom::PathVector& fill, Geom::PathVector& stroke, bool bbox_only = false) +{ + const SPShape *shape = dynamic_cast<const SPShape*>(item); + const SPText *text = dynamic_cast<const SPText*>(item); + + if (!shape && !text) { + return false; + } + + std::unique_ptr<SPCurve> curve; + if (shape) { + curve = SPCurve::copy(shape->curve()); + } else if (text) { + curve = text->getNormalizedBpath(); + } else { + std::cerr << "item_find_paths: item not shape or text!" << std::endl; + return false; + } + + if (!curve) { + std::cerr << "item_find_paths: no curve!" << std::endl; + return false; + } + + if (curve->get_pathvector().empty()) { + std::cerr << "item_find_paths: curve empty!" << std::endl; + return false; + } + + fill = curve->get_pathvector(); + + if (!item->style) { + // Should never happen + std::cerr << "item_find_paths: item with no style!" << std::endl; + return false; + } + + if (item->style->stroke.isNone()) { + // No stroke, no chocolate! + return true; + } + + // Now that we have a valid curve with stroke, do offset. We use Livarot for this as + // lib2geom does not yet handle offsets correctly. + + // Livarot's outline of arcs is broken. So convert the path to linear and cubics only, for + // which the outline is created correctly. + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers( fill ); + + SPStyle *style = item->style; + + double stroke_width = style->stroke_width.computed; + if (stroke_width < Geom::EPSILON) { + // https://bugs.launchpad.net/inkscape/+bug/1244861 + stroke_width = Geom::EPSILON; + } + double miter = style->stroke_miterlimit.value * stroke_width; + + JoinType join; + switch (style->stroke_linejoin.computed) { + case SP_STROKE_LINEJOIN_MITER: + join = join_pointy; + break; + case SP_STROKE_LINEJOIN_ROUND: + join = join_round; + break; + default: + join = join_straight; + break; + } + + ButtType butt; + switch (style->stroke_linecap.computed) { + case SP_STROKE_LINECAP_SQUARE: + butt = butt_square; + break; + case SP_STROKE_LINECAP_ROUND: + butt = butt_round; + break; + default: + butt = butt_straight; + break; + } + + Path *origin = new Path; // Fill + Path *offset = new Path; + + Geom::Affine const transform(item->transform); + double const scale = transform.descrim(); + + origin->LoadPathVector(pathv); + offset->SetBackData(false); + + if (!style->stroke_dasharray.values.empty()) { + // We have dashes! + origin->ConvertWithBackData(0.005); // Approximate by polyline + origin->DashPolylineFromStyle(style, scale, 0); + auto bounds = Geom::bounds_fast(pathv); + if (bounds) { + double size = Geom::L2(bounds->dimensions()); + origin->Simplify(size * 0.000005); // Polylines to Beziers + } + } + + // Finally do offset! + origin->Outline(offset, 0.5 * stroke_width, join, butt, 0.5 * miter); + + if (bbox_only) { + stroke = offset->MakePathVector(); + } else { + // Clean-up shape + + offset->ConvertWithBackData(1.0); // Approximate by polyline + + Shape *theShape = new Shape; + offset->Fill(theShape, 0); // Convert polyline to shape, step 1. + + Shape *theOffset = new Shape; + theOffset->ConvertToShape(theShape, fill_positive); // Create an intersection free polygon (theOffset), step2. + theOffset->ConvertToForme(origin, 1, &offset); // Turn shape into contour (stored in origin). + + stroke = origin->MakePathVector(); // Note origin was replaced above by stroke! + } + + delete origin; + delete offset; + + // std::cout << " fill: " << sp_svg_write_path(fill) << " count: " << fill.curveCount() << std::endl; + // std::cout << " stroke: " << sp_svg_write_path(stroke) << " count: " << stroke.curveCount() << std::endl; + return true; +} + + +// ======================== Item to Outline ===================== // + +static +void item_to_outline_add_marker_child( SPItem const *item, Geom::Affine marker_transform, Geom::PathVector* pathv_in ) +{ + Geom::Affine tr(marker_transform); + tr = item->transform * tr; + + // note: a marker child item can be an item group! + if (SP_IS_GROUP(item)) { + // recurse through all childs: + for (auto& o: item->children) { + if (auto childitem = dynamic_cast<SPItem const *>(&o)) { + item_to_outline_add_marker_child(childitem, tr, pathv_in); + } + } + } else { + Geom::PathVector* marker_pathv = item_to_outline(item); + + if (marker_pathv) { + for (const auto & j : *marker_pathv) { + pathv_in->push_back(j * tr); + } + delete marker_pathv; + } + } +} + +static +void item_to_outline_add_marker( SPObject const *marker_object, Geom::Affine marker_transform, + Geom::Scale stroke_scale, Geom::PathVector* pathv_in ) +{ + SPMarker const * marker = SP_MARKER(marker_object); + + Geom::Affine tr(marker_transform); + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + tr = stroke_scale * tr; + } + // total marker transform + tr = marker->c2p * tr; + + SPItem const * marker_item = sp_item_first_item_child(marker_object); // why only consider the first item? can a marker only consist of a single item (that may be a group)? + if (marker_item) { + item_to_outline_add_marker_child(marker_item, tr, pathv_in); + } +} + + +/** + * Returns a pathvector that is the outline of the stroked item, with markers. + * item must be an SPShape or an SPText. + * The only current use of this function has exclude_markers true! (SPShape::either_bbox). + * TODO: See if SPShape::either_bbox's union with markers is the same as one would get + * with bbox_only false. + */ +Geom::PathVector* item_to_outline(SPItem const *item, bool exclude_markers) +{ + Geom::PathVector fill; // Used for locating markers. + Geom::PathVector stroke; // Used for creating outline (and finding bbox). + item_find_paths(item, fill, stroke, true); // Skip cleaning up stroke shape. + + Geom::PathVector *ret_pathv = nullptr; + + if (fill.curveCount() == 0) { + std::cerr << "item_to_outline: fill path has no segments!" << std::endl; + return ret_pathv; + } + + if (stroke.size() > 0) { + ret_pathv = new Geom::PathVector(stroke); + } else { + // No stroke, use fill path. + ret_pathv = new Geom::PathVector(fill); + } + + if (exclude_markers) { + return ret_pathv; + } + + const SPShape *shape = dynamic_cast<const SPShape *>(item); + if (shape && shape->hasMarkers()) { + + SPStyle *style = shape->style; + Geom::Scale scale(style->stroke_width.computed); + + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( SPObject *marker_obj = shape->_marker[i] ) { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(fill.front().front())); + item_to_outline_add_marker( marker_obj, m, scale, ret_pathv ); + } + } + + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + SPObject *midmarker_obj = shape->_marker[i]; + if (!midmarker_obj) continue; + for(Geom::PathVector::const_iterator path_it = fill.begin(); path_it != fill.end(); ++path_it) { + + // START position + if ( path_it != fill.begin() && + ! ((path_it == (fill.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(path_it->front())); + item_to_outline_add_marker( midmarker_obj, m, scale, ret_pathv); + } + + // MID position + if (path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment + */ + Geom::Affine const m (sp_shape_marker_get_transform(*curve_it1, *curve_it2)); + item_to_outline_add_marker( midmarker_obj, m, scale, ret_pathv); + + ++curve_it1; + ++curve_it2; + } + } + + // END position + if ( path_it != (fill.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + item_to_outline_add_marker( midmarker_obj, m, scale, ret_pathv ); + } + } + } + + // END marker + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( SPObject *marker_obj = shape->_marker[i] ) { + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = fill.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + item_to_outline_add_marker( marker_obj, m, scale, ret_pathv ); + } + } + } + + return ret_pathv; +} + + + +// ========================= Stroke to Path ====================== // + +static +void item_to_paths_add_marker( SPItem *context, + SPObject *marker_object, Geom::Affine marker_transform, + Geom::Scale stroke_scale, + Inkscape::XML::Node *g_repr, Inkscape::XML::Document *xml_doc, + SPDocument * doc, bool legacy) +{ + SPMarker* marker = SP_MARKER (marker_object); + SPItem* marker_item = sp_item_first_item_child(marker_object); + if (!marker_item) { + return; + } + + Geom::Affine tr(marker_transform); + + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + tr = stroke_scale * tr; + } + // total marker transform + tr = marker_item->transform * marker->c2p * tr; + + if (marker_item->getRepr()) { + Inkscape::XML::Node *m_repr = marker_item->getRepr()->duplicate(xml_doc); + g_repr->addChildAtPos(m_repr, 0); + SPItem *marker_item = (SPItem *) doc->getObjectByRepr(m_repr); + marker_item->doWriteTransform(tr); + if (!legacy) { + item_to_paths(marker_item, legacy, context); + } + } +} + + +/* + * Find an outline that represents an item. + * If legacy, text will not be handled as it is not a shape. + * If a new item is created it is returned. + * If the input item is a group and that group contains a changed item, the group node is returned + * (marking a change). + * + * The return value is used externally to update a selection. It is nullptr if no change is made. + */ +Inkscape::XML::Node* +item_to_paths(SPItem *item, bool legacy, SPItem *context) +{ + char const *id = item->getAttribute("id"); + SPDocument *doc = item->document; + bool flatten = false; + // flatten all paths effects + SPLPEItem *lpeitem = SP_LPE_ITEM(item); + if (lpeitem && lpeitem->hasPathEffect()) { + lpeitem->removeAllPathEffects(true); + SPObject *elemref = doc->getObjectById(id); + if (elemref && elemref != item) { + // If the LPE item is a shape, it is converted to a path + // so we need to reupdate the item + item = dynamic_cast<SPItem *>(elemref); + } + auto flat_item = dynamic_cast<SPLPEItem *>(elemref); + if (!flat_item || !flat_item->hasPathEffect()) { + flatten = true; + } + } + // convert text/3dbox to path + if (dynamic_cast<SPText *>(item) + || dynamic_cast<SPFlowtext *>(item) + || dynamic_cast<SPBox3D *>(item)) { + if (legacy) { + return nullptr; + } + + Inkscape::ObjectSet original_objects {doc}; // doc or desktop shouldn't be necessary + original_objects.add(dynamic_cast<SPObject *>(item)); + original_objects.toCurves(true); + SPItem * new_item = original_objects.singleItem(); + if (new_item && new_item != item) { + flatten = true; + item = new_item; + } else { + g_warning("item_to_paths: flattening text or 3D box failed."); + return nullptr; + } + } + // if group, recurse + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + if (legacy) { + return nullptr; + } + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + bool did = false; + for (auto subitem : item_list) { + if (item_to_paths(subitem, legacy)) { + did = true; + } + } + if (did || flatten) { + // This indicates that at least one thing was changed inside the group. + return group->getRepr(); + } else { + return nullptr; + } + } + + SPShape* shape = dynamic_cast<SPShape *>(item); + if (!shape) { + return nullptr; + } + + Geom::PathVector fill_path; + Geom::PathVector stroke_path; + bool status = item_find_paths(item, fill_path, stroke_path); + + if (!status) { + // Was not a well structured shape (or text). + return nullptr; + } + + // The styles ------------------------ + + // Copying stroke style to fill will fail for properties not defined by style attribute + // (i.e., properties defined in style sheet or by attributes). + SPStyle *style = item->style; + SPCSSAttr *ncss = sp_css_attr_from_style(style, SP_STYLE_FLAG_ALWAYS); + SPCSSAttr *ncsf = sp_css_attr_from_style(style, SP_STYLE_FLAG_ALWAYS); + if (context) { + SPStyle *context_style = context->style; + SPCSSAttr *ctxt_style = sp_css_attr_from_style(context_style, SP_STYLE_FLAG_ALWAYS); + // TODO: browsers have different behaviours with context on markers + // we need to revisit in the future for best matching + // also dont know if opacity is or should be included in context + gchar const *s_val = sp_repr_css_property(ctxt_style, "stroke", nullptr); + gchar const *f_val = sp_repr_css_property(ctxt_style, "fill", nullptr); + if (style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE || + style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) + { + gchar const *fill_value = (style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE) ? s_val : f_val; + sp_repr_css_set_property(ncss, "fill", fill_value); + sp_repr_css_set_property(ncsf, "fill", fill_value); + } + if (style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE || + style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) + { + gchar const *stroke_value = (style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL) ? f_val : s_val; + sp_repr_css_set_property(ncss, "stroke", stroke_value); + sp_repr_css_set_property(ncsf, "stroke", stroke_value); + } + } + // Stroke + + gchar const *s_val = sp_repr_css_property(ncss, "stroke", nullptr); + gchar const *s_opac = sp_repr_css_property(ncss, "stroke-opacity", nullptr); + gchar const *f_val = sp_repr_css_property(ncss, "fill", nullptr); + gchar const *opacity = sp_repr_css_property(ncss, "opacity", nullptr); // Also for markers + gchar const *filter = sp_repr_css_property(ncss, "filter", nullptr); // Also for markers + + sp_repr_css_set_property(ncss, "stroke", "none"); + sp_repr_css_set_property(ncss, "stroke-width", nullptr); + sp_repr_css_set_property(ncss, "stroke-opacity", "1.0"); + sp_repr_css_set_property(ncss, "filter", nullptr); + sp_repr_css_set_property(ncss, "opacity", nullptr); + sp_repr_css_unset_property(ncss, "marker-start"); + sp_repr_css_unset_property(ncss, "marker-mid"); + sp_repr_css_unset_property(ncss, "marker-end"); + + // we change the stroke to fill on ncss to create the filled stroke + sp_repr_css_set_property(ncss, "fill", s_val); + if ( s_opac ) { + sp_repr_css_set_property(ncss, "fill-opacity", s_opac); + } else { + sp_repr_css_set_property(ncss, "fill-opacity", "1.0"); + } + + + sp_repr_css_set_property(ncsf, "stroke", "none"); + sp_repr_css_set_property(ncsf, "stroke-opacity", "1.0"); + sp_repr_css_set_property(ncss, "stroke-width", nullptr); + sp_repr_css_set_property(ncsf, "filter", nullptr); + sp_repr_css_set_property(ncsf, "opacity", nullptr); + sp_repr_css_unset_property(ncsf, "marker-start"); + sp_repr_css_unset_property(ncsf, "marker-mid"); + sp_repr_css_unset_property(ncsf, "marker-end"); + + // The object tree ------------------- + + // Remember the position of the item + gint pos = item->getRepr()->position(); + + // Remember parent + Inkscape::XML::Node *parent = item->getRepr()->parent(); + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + + // Create a group to put everything in. + Inkscape::XML::Node *g_repr = xml_doc->createElement("svg:g"); + + Inkscape::copy_object_properties(g_repr, item->getRepr()); + // drop copied style, children will be re-styled (stroke becomes fill) + g_repr->removeAttribute("style"); + + // Add the group to the parent, move to the saved position + parent->addChildAtPos(g_repr, pos); + + // The stroke ------------------------ + Inkscape::XML::Node *stroke = nullptr; + if (s_val && g_strcmp0(s_val,"none") != 0 && stroke_path.size() > 0) { + stroke = xml_doc->createElement("svg:path"); + sp_repr_css_change(stroke, ncss, "style"); + + stroke->setAttribute("d", sp_svg_write_path(stroke_path)); + } + sp_repr_css_attr_unref(ncss); + + // The fill -------------------------- + Inkscape::XML::Node *fill = nullptr; + if (f_val && g_strcmp0(f_val,"none") != 0 && !legacy) { + fill = xml_doc->createElement("svg:path"); + sp_repr_css_change(fill, ncsf, "style"); + + fill->setAttribute("d", sp_svg_write_path(fill_path)); + } + sp_repr_css_attr_unref(ncsf); + + // The markers ----------------------- + Inkscape::XML::Node *markers = nullptr; + Geom::Scale scale(style->stroke_width.computed); + + if (shape->hasMarkers()) { + if (!legacy) { + markers = xml_doc->createElement("svg:g"); + g_repr->addChildAtPos(markers, pos); + } else { + markers = g_repr; + } + + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( SPObject *marker_obj = shape->_marker[i] ) { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(fill_path.front().front())); + item_to_paths_add_marker( item, marker_obj, m, scale, + markers, xml_doc, doc, legacy); + } + } + + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + SPObject *midmarker_obj = shape->_marker[i]; + if (!midmarker_obj) continue; // TODO use auto below + for(Geom::PathVector::const_iterator path_it = fill_path.begin(); path_it != fill_path.end(); ++path_it) { + + // START position + if ( path_it != fill_path.begin() && + ! ((path_it == (fill_path.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine const m (sp_shape_marker_get_transform_at_start(path_it->front())); + item_to_paths_add_marker( item, midmarker_obj, m, scale, + markers, xml_doc, doc, legacy); + } + + // MID position + if (path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment + */ + Geom::Affine const m (sp_shape_marker_get_transform(*curve_it1, *curve_it2)); + item_to_paths_add_marker( item, midmarker_obj, m, scale, + markers, xml_doc, doc, legacy); + + ++curve_it1; + ++curve_it2; + } + } + + // END position + if ( path_it != (fill_path.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + item_to_paths_add_marker( item, midmarker_obj, m, scale, + markers, xml_doc, doc, legacy); + } + } + } + + // END marker + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( SPObject *marker_obj = shape->_marker[i] ) { + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = fill_path.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine const m = sp_shape_marker_get_transform_at_end(lastcurve); + item_to_paths_add_marker( item, marker_obj, m, scale, + markers, xml_doc, doc, legacy); + } + } + } + + gchar const *paint_order = sp_repr_css_property(ncss, "paint-order", nullptr); + SPIPaintOrder temp; + temp.read( paint_order ); + bool unique = false; + if ((!fill && !markers) || (!fill && !stroke) || (!markers && !stroke)) { + unique = true; + } + if (temp.layer[0] != SP_CSS_PAINT_ORDER_NORMAL && !legacy && !unique) { + + if (temp.layer[0] == SP_CSS_PAINT_ORDER_FILL) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + if ( fill ) { + g_repr->appendChild(fill); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( markers ) { + markers->setPosition(2); + } + } else { + if ( fill ) { + g_repr->appendChild(fill); + } + if ( markers ) { + markers->setPosition(1); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + } + } else if (temp.layer[0] == SP_CSS_PAINT_ORDER_STROKE) { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_FILL) { + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( fill ) { + g_repr->appendChild(fill); + } + if ( markers ) { + markers->setPosition(2); + } + } else { + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( markers ) { + markers->setPosition(1); + } + if ( fill ) { + g_repr->appendChild(fill); + } + } + } else { + if (temp.layer[1] == SP_CSS_PAINT_ORDER_STROKE) { + if ( markers ) { + markers->setPosition(0); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( fill ) { + g_repr->appendChild(fill); + } + } else { + if ( markers ) { + markers->setPosition(0); + } + if ( fill ) { + g_repr->appendChild(fill); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + } + } + + } else if (!unique) { + if ( fill ) { + g_repr->appendChild(fill); + } + if ( stroke ) { + g_repr->appendChild(stroke); + } + if ( markers ) { + markers->setPosition(2); + } + } + + bool did = false; + // only consider it a change if more than a fill is created. + if (stroke || markers) { + did = true; + } + + Inkscape::XML::Node *out = nullptr; + + if (!fill && !markers && did) { + out = stroke; + } else if (!fill && !stroke && did) { + out = markers; + } else if(did) { + out = g_repr; + } else { + parent->removeChild(g_repr); + Inkscape::GC::release(g_repr); + if (fill) { + Inkscape::GC::release(fill); + } + return (flatten ? item->getRepr() : nullptr); + } + + SPCSSAttr *r_style = sp_repr_css_attr_new(); + sp_repr_css_set_property(r_style, "opacity", opacity); + sp_repr_css_set_property(r_style, "filter", filter); + sp_repr_css_change(out, r_style, "style"); + + sp_repr_css_attr_unref(r_style); + if (unique && out != markers) { // markers are already a child of g_repr + g_assert(out != g_repr); + parent->addChild(out, g_repr); + parent->removeChild(g_repr); + Inkscape::GC::release(g_repr); + } + out->setAttribute("transform", item->getRepr()->attribute("transform")); + + // We're replacing item, delete it. + item->deleteObject(false); + + out->setAttribute("id",id); + Inkscape::GC::release(out); + + return out; +} + +/* + 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:textwidth=99 : diff --git a/src/path/path-outline.h b/src/path/path-outline.h new file mode 100644 index 0000000..10df017 --- /dev/null +++ b/src/path/path-outline.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** @file + * + * Two related object to path operations: + * + * 1. Find a path that includes fill, stroke, and markers. Useful for finding a visual bounding box. + * 2. Take a set of objects and find an identical visual representation using only paths. + * + * Copyright (C) 2020 Tavmjong Bah + * Copyright (C) 2018 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_PATH_OUTLINE_H +#define SEEN_PATH_OUTLINE_H + +class SPDesktop; +class SPItem; + +namespace Geom { + class PathVector; +} + +namespace Inkscape { +namespace XML { + class Node; +} +} + +/** + * Find an outline that represents an item. + */ +Geom::PathVector* item_to_outline (SPItem const *item, bool exclude_markers = false); + +/** + * Replace item by path objects (a.k.a. stroke to path). + */ +Inkscape::XML::Node* item_to_paths(SPItem *item, bool legacy = false, SPItem *context = nullptr); + +/** + * Replace selected items by path objects (a.k.a. stroke to >path). + * TODO: remove desktop dependency. + */ +void selection_to_paths (SPDesktop *desktop, bool legacy = false); + + +#endif // SEEN_PATH_OUTLINE_H + +/* + 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:textwidth=99 : diff --git a/src/path/path-simplify.cpp b/src/path/path-simplify.cpp new file mode 100644 index 0000000..7bc7ddf --- /dev/null +++ b/src/path/path-simplify.cpp @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Simplify paths (reduce node count). + *//* + * Authors: + * see git history + * Created by fred on Fri Dec 05 2003. + * tweaked endlessly by bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#endif + +#include <vector> + +#include "path-simplify.h" +#include "path-util.h" + +#include "document-undo.h" +#include "preferences.h" + +#include "livarot/Path.h" + +#include "object/sp-item-group.h" +#include "object/sp-path.h" + +using Inkscape::DocumentUndo; + +// Return number of paths simplified (can be greater than one if group). +int +path_simplify(SPItem *item, float threshold, bool justCoalesce, double size) +{ + //If this is a group, do the children instead + SPGroup* group = dynamic_cast<SPGroup *>(item); + if (group) { + int pathsSimplified = 0; + std::vector<SPItem*> items = sp_item_group_item_list(group); + for (auto item : items) { + pathsSimplified += path_simplify(item, threshold, justCoalesce, size); + } + return pathsSimplified; + } + + SPPath* path = dynamic_cast<SPPath *>(item); + if (!path) { + return 0; + } + + // There is actually no option in the preferences dialog for this! + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool simplifyIndividualPaths = prefs->getBool("/options/simplifyindividualpaths/value"); + if (simplifyIndividualPaths) { + Geom::OptRect itemBbox = item->documentVisualBounds(); + if (itemBbox) { + size = L2(itemBbox->dimensions()); + } else { + size = 0; + } + } + + // Correct virtual size by full transform (bug #166937). + size /= item->i2doc_affine().descrim(); + + // Save the transform, to re-apply it after simplification. + Geom::Affine const transform(item->transform); + + /* + reset the transform, effectively transforming the item by transform.inverse(); + this is necessary so that the item is transformed twice back and forth, + allowing all compensations to cancel out regardless of the preferences + */ + item->doWriteTransform(Geom::identity()); + + // SPLivarot: Start ----------------- + + // Get path to simplify (note that the path *before* LPE calculation is needed) + Path *orig = Path_for_item_before_LPE(item, false); + if (orig == nullptr) { + return 0; + } + + if ( justCoalesce ) { + orig->Coalesce(threshold * size); + } else { + orig->ConvertEvenLines(threshold * size); + orig->Simplify(threshold * size); + } + + // Path + gchar *str = orig->svg_dump_path(); + + // SPLivarot: End ------------------- + + char const *patheffect = item->getRepr()->attribute("inkscape:path-effect"); + if (patheffect) { + item->setAttribute("inkscape:original-d", str); + } else { + item->setAttribute("d", str); + } + g_free(str); + + // reapply the transform + item->doWriteTransform(transform); + + // clean up + if (orig) delete orig; + + return 1; +} + +/* + 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:textwidth=99 : diff --git a/src/path/path-simplify.h b/src/path/path-simplify.h new file mode 100644 index 0000000..0beda44 --- /dev/null +++ b/src/path/path-simplify.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Simplify paths (reduce node count). + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef PATH_SIMPLIFY_H +#define PATH_SIMPLIFY_H + +class SPItem; + +int path_simplify(SPItem *item, float threshold, bool justCoalesce, double size); + +#endif // PATH_SIMPLIFY_H + +/* + 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:textwidth=99 : diff --git a/src/path/path-util.cpp b/src/path/path-util.cpp new file mode 100644 index 0000000..c8af3e0 --- /dev/null +++ b/src/path/path-util.cpp @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path utilities. + *//* + * Authors: + * see git history + * Created by fred on Fri Dec 05 2003. + * tweaked endlessly by bulia byak <buliabyak@users.sf.net> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +#endif + +#include <vector> + +#include "path-util.h" + +#include "text-editing.h" + +#include "livarot/Path.h" +#include "livarot/Shape.h" + +#include "object/sp-flowtext.h" +#include "object/sp-image.h" +#include "object/sp-marker.h" +#include "object/sp-path.h" +#include "object/sp-text.h" + +#include "display/curve.h" + +// derived from Path_for_item +Path * +Path_for_pathvector(Geom::PathVector const &epathv) +{ + /*std::cout << "converting to Livarot path" << std::endl; + + Geom::SVGPathWriter wr; + wr.feed(epathv); + std::cout << wr.str() << std::endl;*/ + + Path *dest = new Path; + dest->LoadPathVector(epathv); + return dest; +} + +Path * +Path_for_item(SPItem *item, bool doTransformation, bool transformFull) +{ + std::unique_ptr<SPCurve> curve = curve_for_item(item); + + if (curve == nullptr) + return nullptr; + + Geom::PathVector *pathv = + pathvector_for_curve(item, curve.get(), doTransformation, transformFull, Geom::identity(), Geom::identity()); + + /*std::cout << "converting to Livarot path" << std::endl; + + Geom::SVGPathWriter wr; + if (pathv) { + wr.feed(*pathv); + } + std::cout << wr.str() << std::endl;*/ + + Path *dest = new Path; + dest->LoadPathVector(*pathv); + delete pathv; + + /*gchar *str = dest->svg_dump_path(); + std::cout << "After conversion:\n" << str << std::endl; + g_free(str);*/ + + return dest; +} + +/** + * Obtains an item's Path before the LPE stack has been applied. + */ +Path * +Path_for_item_before_LPE(SPItem *item, bool doTransformation, bool transformFull) +{ + std::unique_ptr<SPCurve> curve = curve_for_item_before_LPE(item); + + if (curve == nullptr) + return nullptr; + + Geom::PathVector *pathv = + pathvector_for_curve(item, curve.get(), doTransformation, transformFull, Geom::identity(), Geom::identity()); + + Path *dest = new Path; + dest->LoadPathVector(*pathv); + delete pathv; + + return dest; +} + +/* + * NOTE: Returns empty pathvector if curve == NULL + * TODO: see if calling this method can be optimized. All the pathvector copying might be slow. + */ +Geom::PathVector* +pathvector_for_curve(SPItem *item, SPCurve *curve, bool doTransformation, bool transformFull, Geom::Affine extraPreAffine, Geom::Affine extraPostAffine) +{ + if (curve == nullptr) + return nullptr; + + Geom::PathVector *dest = new Geom::PathVector; + *dest = curve->get_pathvector(); // Make a copy; must be freed by the caller! + + if (doTransformation) { + if (transformFull) { + *dest *= extraPreAffine * item->i2doc_affine() * extraPostAffine; + } else { + *dest *= extraPreAffine * (Geom::Affine)item->transform * extraPostAffine; + } + } else { + *dest *= extraPreAffine * extraPostAffine; + } + + return dest; +} + +/** + * Obtains an item's curve. For SPPath, it is the path *before* LPE. For SPShapes other than path, it is the path *after* LPE. + * So the result is somewhat ill-defined, and probably this method should not be used... See curve_for_item_before_LPE. + */ +std::unique_ptr<SPCurve> curve_for_item(SPItem *item) +{ + if (!item) + return nullptr; + + std::unique_ptr<SPCurve> curve; + + if (auto path = dynamic_cast<SPPath const *>(item)) { + curve = SPCurve::copy(path->curveForEdit()); + } else if (auto shape = dynamic_cast<SPShape const *>(item)) { + curve = SPCurve::copy(shape->curve()); + } else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + curve = te_get_layout(item)->convertToCurves(); + } else if (auto image = dynamic_cast<SPImage const *>(item)) { + curve = image->get_curve(); + } + + return curve; +} + +/** + * Obtains an item's curve *before* LPE. + */ +std::unique_ptr<SPCurve> curve_for_item_before_LPE(SPItem *item) +{ + if (!item) + return nullptr; + + std::unique_ptr<SPCurve> curve; + + if (auto shape = dynamic_cast<SPShape const *>(item)) { + curve = SPCurve::copy(shape->curveForEdit()); + } else if (SP_IS_TEXT(item) || SP_IS_FLOWTEXT(item)) { + curve = te_get_layout(item)->convertToCurves(); + } else if (auto image = dynamic_cast<SPImage const *>(item)) { + curve = image->get_curve(); + } + + return curve; +} + +std::optional<Path::cut_position> get_nearest_position_on_Path(Path *path, Geom::Point p, unsigned seg) +{ + std::optional<Path::cut_position> result; + if (!path) { + return result; // returns empty std::optional + } + //get nearest position on path + result = path->PointToCurvilignPosition(p, seg); + return result; +} + +Geom::Point get_point_on_Path(Path *path, int piece, double t) +{ + Geom::Point p; + path->PointAt(piece, t, p); + return p; +} + + +/* + 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:textwidth=99 : diff --git a/src/path/path-util.h b/src/path/path-util.h new file mode 100644 index 0000000..6db5aec --- /dev/null +++ b/src/path/path-util.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Path utilities. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef PATH_UTIL_H +#define PATH_UTIL_H + +#include <2geom/forward.h> +#include <2geom/path.h> + +#include "livarot/Path.h" + +#include <memory> + +class SPCurve; +class SPItem; + +Path *Path_for_pathvector(Geom::PathVector const &pathv); +Path *Path_for_item(SPItem *item, bool doTransformation, bool transformFull = true); +Path *Path_for_item_before_LPE(SPItem *item, bool doTransformation, bool transformFull = true); +Geom::PathVector* pathvector_for_curve(SPItem *item, SPCurve *curve, bool doTransformation, bool transformFull, Geom::Affine extraPreAffine, Geom::Affine extraPostAffine); +std::unique_ptr<SPCurve> curve_for_item(SPItem *item); +std::unique_ptr<SPCurve> curve_for_item_before_LPE(SPItem *item); +std::optional<Path::cut_position> get_nearest_position_on_Path(Path *path, Geom::Point p, unsigned seg = 0); +Geom::Point get_point_on_Path(Path *path, int piece, double t); + +#endif // PATH_UTIL_H + +/* + 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:textwidth=99 : |