diff options
Diffstat (limited to '')
186 files changed, 41869 insertions, 0 deletions
diff --git a/src/live_effects/CMakeLists.txt b/src/live_effects/CMakeLists.txt new file mode 100644 index 0000000..3acdd22 --- /dev/null +++ b/src/live_effects/CMakeLists.txt @@ -0,0 +1,195 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(live_effects_SRC + effect.cpp + fill-conversion.cpp + lpe-angle_bisector.cpp + lpe-attach-path.cpp + lpe-bendpath.cpp + lpe-bool.cpp + lpe-bounding-box.cpp + lpe-bspline.cpp + lpe-circle_3pts.cpp + lpe-circle_with_radius.cpp + lpe-clone-original.cpp + lpe-constructgrid.cpp + lpe-copy_rotate.cpp + lpe-curvestitch.cpp + lpe-dashed-stroke.cpp + lpe-dynastroke.cpp + lpe-ellipse_5pts.cpp + lpe-embrodery-stitch.cpp + lpe-embrodery-stitch-ordering.cpp + lpe-envelope.cpp + lpe-extrude.cpp + lpe-fill-between-many.cpp + lpe-fill-between-strokes.cpp + lpe-fillet-chamfer.cpp + lpe-gears.cpp + lpe-interpolate.cpp + lpe-interpolate_points.cpp + lpe-jointype.cpp + lpe-knot.cpp + lpe-lattice.cpp + lpe-lattice2.cpp + lpe-line_segment.cpp + lpe-measure-segments.cpp + lpe-mirror_symmetry.cpp + lpe-offset.cpp + lpe-parallel.cpp + lpe-path_length.cpp + lpe-patternalongpath.cpp + lpe-perp_bisector.cpp + lpe-perspective-envelope.cpp + lpe-powerclip.cpp + lpe-powermask.cpp + lpe-powerstroke.cpp + lpe-pts2ellipse.cpp + lpe-recursiveskeleton.cpp + lpe-rough-hatches.cpp + lpe-roughen.cpp + lpe-ruler.cpp + lpe-show_handles.cpp + lpe-simplify.cpp + lpe-skeleton.cpp + lpe-sketch.cpp + lpe-slice.cpp + lpe-spiro.cpp + lpe-tangent_to_curve.cpp + lpe-taperstroke.cpp + lpe-test-doEffect-stack.cpp + lpe-text_label.cpp + lpe-tiling.cpp + lpe-transform_2pts.cpp + lpegroupbbox.cpp + lpeobject-reference.cpp + lpe-vonkoch.cpp + lpeobject.cpp + spiro-converters.cpp + spiro.cpp + + parameter/array.cpp + parameter/bool.cpp + parameter/colorpicker.cpp + parameter/hidden.cpp + parameter/satellite.cpp + parameter/satellitearray.cpp + parameter/satellite-reference.cpp + parameter/message.cpp + parameter/originalsatellite.cpp + parameter/originalpath.cpp + parameter/patharray.cpp + parameter/parameter.cpp + parameter/path-reference.cpp + parameter/path.cpp + parameter/point.cpp + parameter/powerstrokepointarray.cpp + parameter/random.cpp + parameter/nodesatellitesarray.cpp + parameter/text.cpp + parameter/fontbutton.cpp + parameter/togglebutton.cpp + parameter/transformedpoint.cpp + parameter/unit.cpp + parameter/vector.cpp + + + # ------- + # Headers + effect-enum.h + effect.h + fill-conversion.h + lpe-angle_bisector.h + lpe-attach-path.h + lpe-bendpath.h + lpe-bool.h + lpe-bounding-box.h + lpe-bspline.h + lpe-circle_3pts.h + lpe-circle_with_radius.h + lpe-clone-original.h + lpe-constructgrid.h + lpe-copy_rotate.h + lpe-curvestitch.h + lpe-dashed-stroke.h + lpe-dynastroke.h + lpe-ellipse_5pts.h + lpe-embrodery-stitch.h + lpe-embrodery-stitch-ordering.h + lpe-envelope.h + lpe-extrude.h + lpe-fill-between-many.h + lpe-fill-between-strokes.h + lpe-fillet-chamfer.h + lpe-gears.h + lpe-interpolate.h + lpe-interpolate_points.h + lpe-jointype.h + lpe-knot.h + lpe-lattice.h + lpe-lattice2.h + lpe-line_segment.h + lpe-measure-segments.h + lpe-mirror_symmetry.h + lpe-offset.h + lpe-parallel.h + lpe-path_length.h + lpe-patternalongpath.h + lpe-perp_bisector.h + lpe-perspective-envelope.h + lpe-powerstroke-interpolators.h + lpe-powerclip.h + lpe-powermask.h + lpe-powerstroke.h + lpe-pts2ellipse.h + lpe-recursiveskeleton.h + lpe-rough-hatches.h + lpe-roughen.h + lpe-ruler.h + lpe-show_handles.h + lpe-simplify.h + lpe-skeleton.h + lpe-sketch.h + lpe-slice.h + lpe-spiro.h + lpe-tangent_to_curve.h + lpe-taperstroke.h + lpe-test-doEffect-stack.h + lpe-text_label.h + lpe-tiling.h + lpe-transform_2pts.h + lpe-vonkoch.h + lpegroupbbox.h + lpeobject-reference.h + lpeobject.h + spiro-converters.h + spiro.h + + parameter/array.h + parameter/bool.h + parameter/colorpicker.h + parameter/hidden.h + parameter/enum.h + parameter/satellite.h + parameter/satellitearray.h + parameter/satellite-reference.h + parameter/message.h + parameter/originalsatellite.h + parameter/originalpath.h + parameter/patharray.h + parameter/parameter.h + parameter/path-reference.h + parameter/path.h + parameter/point.h + parameter/powerstrokepointarray.h + parameter/random.h + parameter/nodesatellitesarray.h + parameter/text.h + parameter/fontbutton.h + parameter/togglebutton.h + parameter/transformedpoint.h + parameter/unit.h + parameter/vector.h +) + +# add_inkscape_lib(live_effects_LIB "${live_effects_SRC}") +add_inkscape_source("${live_effects_SRC}") diff --git a/src/live_effects/README b/src/live_effects/README new file mode 100644 index 0000000..827a62e --- /dev/null +++ b/src/live_effects/README @@ -0,0 +1,8 @@ + + +This directory contains our "Live Path Effects" which create new paths from an existing path, storing the original path for reuse. + +To do: + +* Move to a suitable subdirectory. + diff --git a/src/live_effects/effect-enum.h b/src/live_effects/effect-enum.h new file mode 100644 index 0000000..9e2140d --- /dev/null +++ b/src/live_effects/effect-enum.h @@ -0,0 +1,309 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_ENUM_H +#define INKSCAPE_LIVEPATHEFFECT_ENUM_H + +/* + * Inkscape::LivePathEffect::EffectType + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "util/enums.h" + +namespace Inkscape { +namespace LivePathEffect { + +//Please fill in the same order than in effect.cpp:98 +enum EffectType { + BEND_PATH = 0, + GEARS, + PATTERN_ALONG_PATH, + CURVE_STITCH, + VONKOCH, + KNOT, + CONSTRUCT_GRID, + SPIRO, + ENVELOPE, + INTERPOLATE, + ROUGH_HATCHES, + SKETCH, + RULER, + POWERSTROKE, + CLONE_ORIGINAL, + SIMPLIFY, + LATTICE2, + PERSPECTIVE_ENVELOPE, + INTERPOLATE_POINTS, + TRANSFORM_2PTS, + SHOW_HANDLES, + ROUGHEN, + BSPLINE, + JOIN_TYPE, + TAPER_STROKE, + MIRROR_SYMMETRY, + COPY_ROTATE, + ATTACH_PATH, + FILL_BETWEEN_STROKES, + FILL_BETWEEN_MANY, + ELLIPSE_5PTS, + BOUNDING_BOX, + MEASURE_SEGMENTS, + FILLET_CHAMFER, + POWERCLIP, + POWERMASK, + PTS2ELLIPSE, + OFFSET, + DASHED_STROKE, + BOOL_OP, + SLICE, + TILING, + // PUT NEW LPE BEFORE EXPERIMENTAL IN THE SAME ORDER AS IN effect.cpp + // Visible Experimental LPE's + ANGLE_BISECTOR, + CIRCLE_WITH_RADIUS, + CIRCLE_3PTS, + EXTRUDE, + LINE_SEGMENT, + PARALLEL, + PERP_BISECTOR, + TANGENT_TO_CURVE, + // Hidden Experimental LPE's + DOEFFECTSTACK_TEST, + DYNASTROKE, + LATTICE, + PATH_LENGTH, + RECURSIVE_SKELETON, + TEXT_LABEL, + EMBRODERY_STITCH, + INVALID_LPE // This must be last (I made it such that it is not needed anymore I think..., Don't trust on it being + // last. - johan) +}; +//ALPHABETIC +enum ParamType { + ARRAY = 0, + BOOL, + COLOR_PICKER, + ENUM, + FONT_BUTTON, + HIDDEN, + MESSAGE, + NODE_SATELLITE_ARRAY, + ORIGINAL_PATH, + ORIGINAL_SATELLITE, + PATH_REFERENCE, + PATH, + PATH_ARRAY, + POINT, + POWERSTROKE_POINT_ARRAY, + RANDOM, + SATELLITE, + SATELLITE_ARRAY, + TEXT, + TOGGLE_BUTTON, + TRANSFORMED_POINT, + UNIT, + VECTOR, + INVALID_PARAM // This must be last +}; + +template <typename E> +struct EnumEffectData { + E id; + const Glib::ustring label; + const Glib::ustring key; + const Glib::ustring icon; + const Glib::ustring description; + const bool on_path; + const bool on_shape; + const bool on_group; + const bool on_image; + const bool on_text; + const bool experimental; +}; + +const Glib::ustring empty_string(""); + +/** + * Simplified management of enumerations of LPE items with UI labels. + * + * @note that get_id_from_key and get_id_from_label return 0 if it cannot find an entry for that key string. + * @note that get_label and get_key return an empty string when the requested id is not in the list. + */ +template <typename E> +class EnumEffectDataConverter { + public: + typedef EnumEffectData<E> Data; + + EnumEffectDataConverter(const EnumEffectData<E> *cd, const unsigned int length) + : _length(length) + , _data(cd) + { + } + + E get_id_from_label(const Glib::ustring &label) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].label == label) + return _data[i].id; + } + + return (E)0; + } + + E get_id_from_key(const Glib::ustring &key) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].key == key) + return _data[i].id; + } + + return (E)0; + } + + bool is_valid_key(const Glib::ustring &key) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].key == key) + return true; + } + + return false; + } + + bool is_valid_id(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return true; + } + return false; + } + + const Glib::ustring &get_label(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].label; + } + + return empty_string; + } + + const Glib::ustring &get_key(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].key; + } + + return empty_string; + } + + const Glib::ustring &get_icon(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].icon; + } + + return empty_string; + } + + const Glib::ustring &get_description(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].description; + } + + return empty_string; + } + + bool get_on_path(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_path; + } + + return false; + } + + bool get_on_shape(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_shape; + } + + return false; + } + + bool get_on_group(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_group; + } + + return false; + } + + bool get_on_image(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_image; + } + + return false; + } + + bool get_on_text(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].on_text; + } + + return false; + } + + bool get_experimental(const E id) const + { + for (unsigned int i = 0; i < _length; ++i) { + if (_data[i].id == id) + return _data[i].experimental; + } + + return false; + } + + const EnumEffectData<E> &data(const unsigned int i) const { return _data[i]; } + + const unsigned int _length; + + private: + const EnumEffectData<E> *_data; +}; + +extern const EnumEffectData<EffectType> LPETypeData[]; /// defined in effect.cpp +extern const EnumEffectDataConverter<EffectType> LPETypeConverter; /// defined in effect.cpp + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/effect.cpp b/src/live_effects/effect.cpp new file mode 100644 index 0000000..795b73e --- /dev/null +++ b/src/live_effects/effect.cpp @@ -0,0 +1,2034 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +//#define LPE_ENABLE_TEST_EFFECTS //uncomment for toy effects + +// include effects: +#include <cstdio> +#include <cstring> +#include <gtkmm/expander.h> +#include <pangomm/layout.h> + +#include "display/curve.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-angle_bisector.h" +#include "live_effects/lpe-attach-path.h" +#include "live_effects/lpe-bendpath.h" +#include "live_effects/lpe-bool.h" +#include "live_effects/lpe-bounding-box.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpe-circle_3pts.h" +#include "live_effects/lpe-circle_with_radius.h" +#include "live_effects/lpe-clone-original.h" +#include "live_effects/lpe-constructgrid.h" +#include "live_effects/lpe-copy_rotate.h" +#include "live_effects/lpe-curvestitch.h" +#include "live_effects/lpe-dashed-stroke.h" +#include "live_effects/lpe-dynastroke.h" +#include "live_effects/lpe-ellipse_5pts.h" +#include "live_effects/lpe-embrodery-stitch.h" +#include "live_effects/lpe-envelope.h" +#include "live_effects/lpe-extrude.h" +#include "live_effects/lpe-fill-between-many.h" +#include "live_effects/lpe-fill-between-strokes.h" +#include "live_effects/lpe-fillet-chamfer.h" +#include "live_effects/lpe-gears.h" +#include "live_effects/lpe-interpolate.h" +#include "live_effects/lpe-interpolate_points.h" +#include "live_effects/lpe-jointype.h" +#include "live_effects/lpe-knot.h" +#include "live_effects/lpe-lattice.h" +#include "live_effects/lpe-lattice2.h" +#include "live_effects/lpe-line_segment.h" +#include "live_effects/lpe-measure-segments.h" +#include "live_effects/lpe-mirror_symmetry.h" +#include "live_effects/lpe-offset.h" +#include "live_effects/lpe-parallel.h" +#include "live_effects/lpe-path_length.h" +#include "live_effects/lpe-patternalongpath.h" +#include "live_effects/lpe-perp_bisector.h" +#include "live_effects/lpe-perspective-envelope.h" +#include "live_effects/lpe-powerclip.h" +#include "live_effects/lpe-powermask.h" +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-pts2ellipse.h" +#include "live_effects/lpe-recursiveskeleton.h" +#include "live_effects/lpe-rough-hatches.h" +#include "live_effects/lpe-roughen.h" +#include "live_effects/lpe-ruler.h" +#include "live_effects/lpe-show_handles.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpe-sketch.h" +#include "live_effects/lpe-slice.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpe-tangent_to_curve.h" +#include "live_effects/lpe-taperstroke.h" +#include "live_effects/lpe-test-doEffect-stack.h" +#include "live_effects/lpe-text_label.h" +#include "live_effects/lpe-tiling.h" +#include "live_effects/lpe-transform_2pts.h" +#include "live_effects/lpe-vonkoch.h" +#include "live_effects/lpeobject.h" +#include "message-stack.h" +#include "object/sp-defs.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "path-chemistry.h" +#include "ui/icon-loader.h" +#include "ui/tools/node-tool.h" +#include "ui/tools/pen-tool.h" +#include "xml/node-event-vector.h" +#include "xml/sp-css-attr.h" + +namespace Inkscape { + +namespace LivePathEffect { + +const EnumEffectData<EffectType> LPETypeData[] = { + // {constant defined in effect-enum.h, N_("name of your effect"), "name of your effect in SVG"} + // please sync order with effect-enum.h +/* 0.46 */ + { + BEND_PATH, + NC_("path effect", "Bend") ,//label + "bend_path" ,//key + "bend-path" ,//icon + N_("Bend an object along the curvature of another path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + GEARS, + NC_("path effect", "Gears") ,//label + "gears" ,//key + "gears" ,//icon + N_("Create interlocking, configurable gears based on the nodes of a path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + PATTERN_ALONG_PATH, + NC_("path effect", "Pattern Along Path") ,//label + "skeletal" ,//key + "skeletal" ,//icon + N_("Place one or more copies of another path along the path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, // for historic reasons, this effect is called skeletal(strokes) in Inkscape:SVG + { + CURVE_STITCH, + NC_("path effect", "Stitch Sub-Paths") ,//label + "curvestitching" ,//key + "curvestitching" ,//icon + N_("Draw perpendicular lines between subpaths of a path, like rungs of a ladder") ,//description + true ,//on_path + false ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, +/* 0.47 */ + { + VONKOCH, + NC_("path effect", "VonKoch") ,//label + "vonkoch" ,//key + "vonkoch" ,//icon + N_("Create VonKoch fractal") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + KNOT, + NC_("path effect", "Knot") ,//label + "knot" ,//key + "knot" ,//icon + N_("Create gaps in self-intersections, as in Celtic knots") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + CONSTRUCT_GRID, + NC_("path effect", "Construct grid") ,//label + "construct_grid" ,//key + "construct-grid" ,//icon + N_("Create a (perspective) grid from a 3-node path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + SPIRO, + NC_("path effect", "Spiro spline") ,//label + "spiro" ,//key + "spiro" ,//icon + N_("Make the path curl like wire, using Spiro B-Splines. This effect is usually used directly on the canvas with the Spiro mode of the drawing tools.") ,//description + true ,//on_path + false ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + ENVELOPE, + NC_("path effect", "Envelope Deformation") ,//label + "envelope" ,//key + "envelope" ,//icon + N_("Adjust the shape of an object by transforming paths on its four sides") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + INTERPOLATE, + NC_("path effect", "Interpolate Sub-Paths") ,//label + "interpolate" ,//key + "interpolate" ,//icon + N_("Create a stepwise transition between the 2 subpaths of a path") ,//description + true ,//on_path + false ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + ROUGH_HATCHES, + NC_("path effect", "Hatches (rough)") ,//label + "rough_hatches" ,//key + "rough-hatches" ,//icon + N_("Fill the object with adjustable hatching") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + SKETCH, + NC_("path effect", "Sketch") ,//label + "sketch" ,//key + "sketch" ,//icon + N_("Draw multiple short strokes along the path, as in a pencil sketch") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + RULER, + NC_("path effect", "Ruler") ,//label + "ruler" ,//key + "ruler" ,//icon + N_("Add ruler marks to the object in adjustable intervals, using the object's stroke style.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, +/* 0.91 */ + { + POWERSTROKE, + NC_("path effect", "Power stroke") ,//label + "powerstroke" ,//key + "powerstroke" ,//icon + N_("Create calligraphic strokes and control their variable width and curvature. This effect can also be used directly on the canvas with a pressure sensitive stylus and the Pencil tool.") ,//description + true ,//on_path + true ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + CLONE_ORIGINAL, + NC_("path effect", "Clone original") ,//label + "clone_original" ,//key + "clone-original" ,//icon + N_("Let an object take on the shape, fill, stroke and/or other attributes of another object.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, +/* 0.92 */ + { + SIMPLIFY, + NC_("path effect", "Simplify") ,//label + "simplify" ,//key + "simplify" ,//icon + N_("Smoothen and simplify a object. This effect is also available in the Pencil tool's tool controls.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + LATTICE2, + NC_("path effect", "Lattice Deformation 2") ,//label + "lattice2" ,//key + "lattice2" ,//icon + N_("Warp an object's shape based on a 5x5 grid") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + PERSPECTIVE_ENVELOPE, + NC_("path effect", "Perspective/Envelope") ,//label + "perspective-envelope" ,//key wrong key with "-" retain because historic + "perspective-envelope" ,//icon + N_("Transform the object to fit into a shape with four corners, either by stretching it or creating the illusion of a 3D-perspective") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + INTERPOLATE_POINTS, + NC_("path effect", "Interpolate points") ,//label + "interpolate_points" ,//key + "interpolate-points" ,//icon + N_("Connect the nodes of the object (e.g. corresponding to data points) by different types of lines.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + TRANSFORM_2PTS, + NC_("path effect", "Transform by 2 points") ,//label + "transform_2pts" ,//key + "transform-2pts" ,//icon + N_("Scale, stretch and rotate an object by two handles") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + SHOW_HANDLES, + NC_("path effect", "Show handles") ,//label + "show_handles" ,//key + "show-handles" ,//icon + N_("Draw the handles and nodes of objects (replaces the original styling with a black stroke)") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + ROUGHEN, + NC_("path effect", "Roughen") ,//label + "roughen" ,//key + "roughen" ,//icon + N_("Roughen an object by adding and randomly shifting new nodes") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + BSPLINE, + NC_("path effect", "BSpline") ,//label + "bspline" ,//key + "bspline" ,//icon + N_("Create a BSpline that molds into the path's corners. This effect is usually used directly on the canvas with the BSpline mode of the drawing tools.") ,//description + true ,//on_path + false ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + JOIN_TYPE, + NC_("path effect", "Join type") ,//label + "join_type" ,//key + "join-type" ,//icon + N_("Select among various join types for a object's corner nodes (mitre, rounded, extrapolated arc, ...)") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + TAPER_STROKE, + NC_("path effect", "Taper stroke") ,//label + "taper_stroke" ,//key + "taper-stroke" ,//icon + N_("Let the path's ends narrow down to a tip") ,//description + true ,//on_path + true ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + MIRROR_SYMMETRY, + NC_("path effect", "Mirror symmetry") ,//label + "mirror_symmetry" ,//key + "mirror-symmetry" ,//icon + N_("Mirror an object along a movable axis, or around the page center. The mirrored copy can be styled independently.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + COPY_ROTATE, + NC_("path effect", "Rotate copies") ,//label + "copy_rotate" ,//key + "copy-rotate" ,//icon + N_("Create multiple rotated copies of an object, as in a kaleidoscope. The copies can be styled independently.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, +/* Ponyscape -> Inkscape 0.92*/ + { + ATTACH_PATH, + NC_("path effect", "Attach path") ,//label + "attach_path" ,//key + "attach-path" ,//icon + N_("Glue the current path's ends to a specific position on one or two other paths") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + FILL_BETWEEN_STROKES, + NC_("path effect", "Fill between strokes") ,//label + "fill_between_strokes" ,//key + "fill-between-strokes" ,//icon + N_("Turn the path into a fill between two other open paths (e.g. between two paths with PowerStroke applied to them)") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + FILL_BETWEEN_MANY, + NC_("path effect", "Fill between many") ,//label + "fill_between_many" ,//key + "fill-between-many" ,//icon + N_("Turn the path into a fill between multiple other open paths (e.g. between paths with PowerStroke applied to them)") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + ELLIPSE_5PTS, + NC_("path effect", "Ellipse by 5 points") ,//label + "ellipse_5pts" ,//key + "ellipse-5pts" ,//icon + N_("Create an ellipse from 5 nodes on its circumference") ,//description + true ,//on_path + true ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + BOUNDING_BOX, + NC_("path effect", "Bounding Box") ,//label + "bounding_box" ,//key + "bounding-box" ,//icon + N_("Turn the path into a bounding box that entirely encompasses another path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, +/* 1.0 */ + { + MEASURE_SEGMENTS, + NC_("path effect", "Measure Segments") ,//label + "measure_segments" ,//key + "measure-segments" ,//icon + N_("Add dimensioning for distances between nodes, optionally with projection and many other configuration options") ,//description + true ,//on_path + true ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + FILLET_CHAMFER, + NC_("path effect", "Corners (Fillet/Chamfer)") ,//label + "fillet_chamfer" ,//key + "fillet-chamfer" ,//icon + N_("Adjust the shape of a path's corners, rounding them to a specified radius, or cutting them off") ,//description + true ,//on_path + true ,//on_shape + false ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + POWERCLIP, + NC_("path effect", "Power clip") ,//label + "powerclip" ,//key + "powerclip" ,//icon + N_("Invert, hide or flatten a clip (apply like a Boolean operation)") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + POWERMASK, + NC_("path effect", "Power mask") ,//label + "powermask" ,//key + "powermask" ,//icon + N_("Invert or hide a mask, or use its negative") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + PTS2ELLIPSE, + NC_("path effect", "Ellipse from points") ,//label + "pts2ellipse" ,//key + "pts2ellipse" ,//icon + N_("Draw a circle, ellipse, arc or slice based on the nodes of a path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + OFFSET, + NC_("path effect", "Offset") ,//label + "offset" ,//key + "offset" ,//icon + N_("Offset the path, optionally keeping cusp corners cusp") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + DASHED_STROKE, + NC_("path effect", "Dashed Stroke") ,//label + "dashed_stroke" ,//key + "dashed-stroke" ,//icon + N_("Add a dashed stroke whose dashes end exactly on a node, optionally with the same number of dashes per path segment") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + /* 1.1 */ + { + BOOL_OP, + NC_("path effect", "Boolean operation") ,//label + "bool_op" ,//key + "bool-op" ,//icon + N_("Cut, union, subtract, intersect and divide a path non-destructively with another path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + { + SLICE, + NC_("path effect", "Slice") ,//label + "slice" ,//key + "slice" ,//icon + N_("Slices the item into parts. It can also be applied multiple times.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + /* 1.2 */ + { + TILING, + NC_("path effect", "Tiling") ,//label + "tiling" ,//key + "tiling" ,//icon + N_("Create multiple copies of an object following a grid layout. Customize size, rotation, distances, style and tiling symmetry.") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, + // VISIBLE experimental LPEs + { + ANGLE_BISECTOR, + NC_("path effect", "Angle bisector") ,//label + "angle_bisector" ,//key + "experimental" ,//icon + N_("Draw a line that halves the angle between the first three nodes of the path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + CIRCLE_WITH_RADIUS, + NC_("path effect", "Circle (by center and radius)") ,//label + "circle_with_radius" ,//key + "experimental" ,//icon + N_("Draw a circle, where the first node of the path is the center, and the last determines its radius") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + CIRCLE_3PTS, + NC_("path effect", "Circle by 3 points") ,//label + "circle_3pts" ,//key + "experimental" ,//icon + N_("Draw a circle whose circumference passes through the first three nodes of the path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + EXTRUDE, + NC_("path effect", "Extrude") ,//label + "extrude" ,//key + "experimental" ,//icon + N_("Extrude the path, creating a face for each path segment") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + LINE_SEGMENT, + NC_("path effect", "Line Segment") ,//label + "line_segment" ,//key + "experimental" ,//icon + N_("Draw a straight line that connects the first and last node of a path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + PARALLEL, + NC_("path effect", "Parallel") ,//label + "parallel" ,//key + "experimental" ,//icon + N_("Create a draggable line that will always be parallel to a two-node path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + PERP_BISECTOR, + NC_("path effect", "Perpendicular bisector") ,//label + "perp_bisector" ,//key + "experimental" ,//icon + N_("Draw a perpendicular line in the middle of the (imaginary) line that connects the start and end nodes") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + TANGENT_TO_CURVE, + NC_("path effect", "Tangent to curve") ,//label + "tangent_to_curve" ,//key + "experimental" ,//icon + N_("Draw a tangent with variable length and additional angle that can be moved along the path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, +#ifdef LPE_ENABLE_TEST_EFFECTS + { + DOEFFECTSTACK_TEST, + NC_("path effect", "doEffect stack test") ,//label + "doeffectstacktest" ,//key + "experimental" ,//icon + N_("Test LPE") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + DYNASTROKE, + NC_("path effect", "Dynamic stroke") ,//label + "dynastroke" ,//key + "experimental" ,//icon + N_("Create calligraphic strokes with variably shaped ends, making use of a parameter for the brush angle") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + LATTICE, + NC_("path effect", "Lattice Deformation") ,//label + "lattice" ,//key + "experimental" ,//icon + N_("Deform an object using a 4x4 grid") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + PATH_LENGTH, + NC_("path effect", "Path length") ,//label + "path_length" ,//key + "experimental" ,//icon + N_("Display the total length of a (curved) path") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + RECURSIVE_SKELETON, + NC_("path effect", "Recursive skeleton") ,//label + "recursive_skeleton" ,//key + "experimental" ,//icon + N_("Draw a path recursively") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + TEXT_LABEL, + NC_("path effect", "Text label") ,//label + "text_label" ,//key + "experimental" ,//icon + N_("Add a label for the object") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + true ,//experimental + }, + { + EMBRODERY_STITCH, + NC_("path effect", "Embroidery stitch") ,//label + "embrodery_stitch" ,//key + "embrodery-stitch" ,//icon + N_("Embroidery stitch") ,//description + true ,//on_path + true ,//on_shape + true ,//on_group + false ,//on_image + false ,//on_text + false ,//experimental + }, +#endif + +}; + +const EnumEffectDataConverter<EffectType> LPETypeConverter(LPETypeData, sizeof(LPETypeData) / sizeof(*LPETypeData)); + +int +Effect::acceptsNumClicks(EffectType type) { + switch (type) { + case INVALID_LPE: return -1; // in case we want to distinguish between invalid LPE and valid ones that expect zero clicks + case ANGLE_BISECTOR: return 3; + case CIRCLE_3PTS: return 3; + case CIRCLE_WITH_RADIUS: return 2; + case LINE_SEGMENT: return 2; + case PERP_BISECTOR: return 2; + default: return 0; + } +} + +Effect* +Effect::New(EffectType lpenr, LivePathEffectObject *lpeobj) +{ + Effect* neweffect = nullptr; + switch (lpenr) { + case EMBRODERY_STITCH: + neweffect = static_cast<Effect*> ( new LPEEmbroderyStitch(lpeobj) ); + break; + case BOOL_OP: + neweffect = static_cast<Effect*> ( new LPEBool(lpeobj) ); + break; + case PATTERN_ALONG_PATH: + neweffect = static_cast<Effect*> ( new LPEPatternAlongPath(lpeobj) ); + break; + case BEND_PATH: + neweffect = static_cast<Effect*> ( new LPEBendPath(lpeobj) ); + break; + case SKETCH: + neweffect = static_cast<Effect*> ( new LPESketch(lpeobj) ); + break; + case ROUGH_HATCHES: + neweffect = static_cast<Effect*> ( new LPERoughHatches(lpeobj) ); + break; + case VONKOCH: + neweffect = static_cast<Effect*> ( new LPEVonKoch(lpeobj) ); + break; + case KNOT: + neweffect = static_cast<Effect*> ( new LPEKnot(lpeobj) ); + break; + case GEARS: + neweffect = static_cast<Effect*> ( new LPEGears(lpeobj) ); + break; + case CURVE_STITCH: + neweffect = static_cast<Effect*> ( new LPECurveStitch(lpeobj) ); + break; + case LATTICE: + neweffect = static_cast<Effect*> ( new LPELattice(lpeobj) ); + break; + case ENVELOPE: + neweffect = static_cast<Effect*> ( new LPEEnvelope(lpeobj) ); + break; + case CIRCLE_WITH_RADIUS: + neweffect = static_cast<Effect*> ( new LPECircleWithRadius(lpeobj) ); + break; + case SPIRO: + neweffect = static_cast<Effect*> ( new LPESpiro(lpeobj) ); + break; + case CONSTRUCT_GRID: + neweffect = static_cast<Effect*> ( new LPEConstructGrid(lpeobj) ); + break; + case PERP_BISECTOR: + neweffect = static_cast<Effect*> ( new LPEPerpBisector(lpeobj) ); + break; + case TANGENT_TO_CURVE: + neweffect = static_cast<Effect*> ( new LPETangentToCurve(lpeobj) ); + break; + case MIRROR_SYMMETRY: + neweffect = static_cast<Effect*> ( new LPEMirrorSymmetry(lpeobj) ); + break; + case CIRCLE_3PTS: + neweffect = static_cast<Effect*> ( new LPECircle3Pts(lpeobj) ); + break; + case ANGLE_BISECTOR: + neweffect = static_cast<Effect*> ( new LPEAngleBisector(lpeobj) ); + break; + case PARALLEL: + neweffect = static_cast<Effect*> ( new LPEParallel(lpeobj) ); + break; + case COPY_ROTATE: + neweffect = static_cast<Effect*> ( new LPECopyRotate(lpeobj) ); + break; + case OFFSET: + neweffect = static_cast<Effect*> ( new LPEOffset(lpeobj) ); + break; + case RULER: + neweffect = static_cast<Effect*> ( new LPERuler(lpeobj) ); + break; + case INTERPOLATE: + neweffect = static_cast<Effect*> ( new LPEInterpolate(lpeobj) ); + break; + case INTERPOLATE_POINTS: + neweffect = static_cast<Effect*> ( new LPEInterpolatePoints(lpeobj) ); + break; + case TEXT_LABEL: + neweffect = static_cast<Effect*> ( new LPETextLabel(lpeobj) ); + break; + case PATH_LENGTH: + neweffect = static_cast<Effect*> ( new LPEPathLength(lpeobj) ); + break; + case LINE_SEGMENT: + neweffect = static_cast<Effect*> ( new LPELineSegment(lpeobj) ); + break; + case DOEFFECTSTACK_TEST: + neweffect = static_cast<Effect*> ( new LPEdoEffectStackTest(lpeobj) ); + break; + case BSPLINE: + neweffect = static_cast<Effect*> ( new LPEBSpline(lpeobj) ); + break; + case DYNASTROKE: + neweffect = static_cast<Effect*> ( new LPEDynastroke(lpeobj) ); + break; + case RECURSIVE_SKELETON: + neweffect = static_cast<Effect*> ( new LPERecursiveSkeleton(lpeobj) ); + break; + case EXTRUDE: + neweffect = static_cast<Effect*> ( new LPEExtrude(lpeobj) ); + break; + case POWERSTROKE: + neweffect = static_cast<Effect*> ( new LPEPowerStroke(lpeobj) ); + break; + case CLONE_ORIGINAL: + neweffect = static_cast<Effect*> ( new LPECloneOriginal(lpeobj) ); + break; + case ATTACH_PATH: + neweffect = static_cast<Effect*> ( new LPEAttachPath(lpeobj) ); + break; + case FILL_BETWEEN_STROKES: + neweffect = static_cast<Effect*> ( new LPEFillBetweenStrokes(lpeobj) ); + break; + case FILL_BETWEEN_MANY: + neweffect = static_cast<Effect*> ( new LPEFillBetweenMany(lpeobj) ); + break; + case ELLIPSE_5PTS: + neweffect = static_cast<Effect*> ( new LPEEllipse5Pts(lpeobj) ); + break; + case BOUNDING_BOX: + neweffect = static_cast<Effect*> ( new LPEBoundingBox(lpeobj) ); + break; + case JOIN_TYPE: + neweffect = static_cast<Effect*> ( new LPEJoinType(lpeobj) ); + break; + case TAPER_STROKE: + neweffect = static_cast<Effect*> ( new LPETaperStroke(lpeobj) ); + break; + case SIMPLIFY: + neweffect = static_cast<Effect*> ( new LPESimplify(lpeobj) ); + break; + case LATTICE2: + neweffect = static_cast<Effect*> ( new LPELattice2(lpeobj) ); + break; + case PERSPECTIVE_ENVELOPE: + neweffect = static_cast<Effect*> ( new LPEPerspectiveEnvelope(lpeobj) ); + break; + case FILLET_CHAMFER: + neweffect = static_cast<Effect*> ( new LPEFilletChamfer(lpeobj) ); + break; + case POWERCLIP: + neweffect = static_cast<Effect*> ( new LPEPowerClip(lpeobj) ); + break; + case POWERMASK: + neweffect = static_cast<Effect*> ( new LPEPowerMask(lpeobj) ); + break; + case ROUGHEN: + neweffect = static_cast<Effect*> ( new LPERoughen(lpeobj) ); + break; + case SHOW_HANDLES: + neweffect = static_cast<Effect*> ( new LPEShowHandles(lpeobj) ); + break; + case TRANSFORM_2PTS: + neweffect = static_cast<Effect*> ( new LPETransform2Pts(lpeobj) ); + break; + case MEASURE_SEGMENTS: + neweffect = static_cast<Effect*> ( new LPEMeasureSegments(lpeobj) ); + break; + case PTS2ELLIPSE: + neweffect = static_cast<Effect*> ( new LPEPts2Ellipse(lpeobj) ); + break; + case DASHED_STROKE: + neweffect = static_cast<Effect *>(new LPEDashedStroke(lpeobj)); + break; + case SLICE: + neweffect = static_cast<Effect *>(new LPESlice(lpeobj)); + break; + case TILING: + neweffect = static_cast<Effect*> ( new LPETiling(lpeobj) ); + break; + default: + g_warning("LivePathEffect::Effect::New called with invalid patheffect type (%d)", lpenr); + neweffect = nullptr; + break; + } + + if (neweffect) { + neweffect->readallParameters(lpeobj->getRepr()); + } + + return neweffect; +} + +void Effect::createAndApply(const char* name, SPDocument *doc, SPItem *item) +{ + // Path effect definition + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *repr = xml_doc->createElement("inkscape:path-effect"); + repr->setAttribute("effect", name); + + doc->getDefs()->getRepr()->addChild(repr, nullptr); // adds to <defs> and assigns the 'id' attribute + const gchar * repr_id = repr->attribute("id"); + Inkscape::GC::release(repr); + + gchar *href = g_strdup_printf("#%s", repr_id); + SP_LPE_ITEM(item)->addPathEffect(href, true); + g_free(href); +} + +void +Effect::createAndApply(EffectType type, SPDocument *doc, SPItem *item) +{ + createAndApply(LPETypeConverter.get_key(type).c_str(), doc, item); +} + +Effect::Effect(LivePathEffectObject *lpeobject) + : apply_to_clippath_and_mask(false), + _provides_knotholder_entities(false), + oncanvasedit_it(0), + is_visible(_("Is visible?"), _("If unchecked, the effect remains applied to the object but is temporarily disabled on canvas"), "is_visible", &wr, this, true), + lpeversion(_("Version"), _("LPE version"), "lpeversion", &wr, this, "0", true), + show_orig_path(false), + keep_paths(false), + is_load(true), + on_remove_all(false), + lpeobj(lpeobject), + concatenate_before_pwd2(false), + sp_lpe_item(nullptr), + current_zoom(0), + refresh_widgets(false), + current_shape(nullptr), + provides_own_flash_paths(true), // is automatically set to false if providesOwnFlashPaths() is not overridden + defaultsopen(false), + is_ready(false), + is_applied(false) +{ + registerParameter(&is_visible); + registerParameter(&lpeversion); + is_visible.widget_is_visible = false; + _before_commit_connection = lpeobj->document->connectBeforeCommit(sigc::mem_fun(*this, &Effect::doOnBeforeCommit)); +} + +Effect::~Effect() +{ + _before_commit_connection.disconnect(); +} + +Glib::ustring +Effect::getName() const +{ + if (lpeobj->effecttype_set && LPETypeConverter.is_valid_id(lpeobj->effecttype) ) + return Glib::ustring( _(LPETypeConverter.get_label(lpeobj->effecttype).c_str()) ); + else + return Glib::ustring( _("No effect") ); +} + +EffectType +Effect::effectType() const { + return lpeobj->effecttype; +} + +std::vector<SPLPEItem *> +Effect::getCurrrentLPEItems() const { + std::vector<SPLPEItem *> result; + auto hreflist = getLPEObj()->hrefList; + for (auto item : hreflist) { + SPLPEItem * lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + result.push_back(lpeitem); + } + } + return result; +} + +/** + * Is performed a single time when the effect is freshly applied to a path + */ +void +Effect::doOnApply (SPLPEItem const*/*lpeitem*/) +{ +} + +void +Effect::setCurrentZoom(double cZ) +{ + current_zoom = cZ; +} + +/** + * Overridden function to apply transforms for example to powerstroke, jointtype or tapperstroke + */ +void Effect::transform_multiply(Geom::Affine const &postmul, bool /*set*/) {} + +/** + * @param lpeitem The item being transformed + * + * @pre effect is referenced by lpeitem + * + * FIXME Probably only makes sense if this effect is referenced by exactly one + * item (`this->lpeobj->hrefList` contains exactly one element)? + */ +void Effect::transform_multiply_impl(Geom::Affine const &postmul, SPLPEItem *lpeitem) +{ + assert("pre: effect is referenced by lpeitem" && + std::any_of(lpeobj->hrefList.begin(), lpeobj->hrefList.end(), + [lpeitem](SPObject *obj) { return lpeitem == dynamic_cast<SPLPEItem *>(obj); })); + + // FIXME Is there a way to eliminate the raw Effect::sp_lpe_item pointer? + sp_lpe_item = lpeitem; + + transform_multiply(postmul, false); +} + +void +Effect::setSelectedNodePoints(std::vector<Geom::Point> sNP) +{ + selectedNodesPoints = sNP; +} + +/** + * The lpe is on clipboard + */ +bool Effect::isOnClipboard() +{ + SPDocument *document = getSPDoc(); + if (!document) { + return false; + } + Inkscape::XML::Node *root = document->getReprRoot(); + Inkscape::XML::Node *clipnode = sp_repr_lookup_name(root, "inkscape:clipboard", 1); + return clipnode != nullptr; +} + +bool +Effect::isNodePointSelected(Geom::Point const &nodePoint) const +{ + if (selectedNodesPoints.size() > 0) { + using Geom::X; + using Geom::Y; + for (auto p : selectedNodesPoints) { + Geom::Affine transformCoordinate = sp_lpe_item->i2dt_affine(); + Geom::Point p2(nodePoint[X],nodePoint[Y]); + p2 *= transformCoordinate; + if (Geom::are_near(p, p2, 0.01)) { + return true; + } + } + } + return false; +} + +// this is done in each action committed to undo and allow do things when all operations pending are done in this undo +// stack +void Effect::doOnBeforeCommit() +{ + if (_lpe_action == LPE_NONE) { + return; + } + sp_lpe_item = dynamic_cast<SPLPEItem *>(*getLPEObj()->hrefList.begin()); + if (sp_lpe_item && _lpe_action == LPE_UPDATE) { + if (sp_lpe_item->getCurrentLPE() == this) { + DocumentUndo::ScopedInsensitive _no_undo(sp_lpe_item->document); + sp_lpe_item_update_patheffect(sp_lpe_item, false, true); + } + _lpe_action = LPE_NONE; + return; + } + Inkscape::LivePathEffect::SatelliteArrayParam *lpesatellites = nullptr; + Inkscape::LivePathEffect::OriginalSatelliteParam *lpesatellite = nullptr; + std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + lpesatellites = dynamic_cast<SatelliteArrayParam *>(*p); + lpesatellite = dynamic_cast<OriginalSatelliteParam *>(*p); + if (lpesatellites || lpesatellite) { + break; + } + } + if (!lpesatellites && !lpesatellite) { + return; + } + LPEAction lpe_action = _lpe_action; + _lpe_action = LPE_NONE; + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if (sp_lpe_item) { + sp_lpe_item_enable_path_effects(sp_lpe_item, false); + } + std::vector<std::shared_ptr<SatelliteReference> > satelltelist; + if (lpesatellites) { + lpesatellites->read_from_SVG(); + satelltelist = lpesatellites->data(); + } else { + lpesatellite->read_from_SVG(); + satelltelist.push_back(lpesatellite->lperef); + } + for (auto &iter : satelltelist) { + SPObject *elemref; + if (iter && iter->isAttached() && (elemref = iter->getObject())) { + if (auto *item = dynamic_cast<SPItem *>(elemref)) { + Inkscape::XML::Node *elemnode = elemref->getRepr(); + SPCSSAttr *css; + Glib::ustring css_str; + switch (lpe_action) { + case LPE_TO_OBJECTS: + if (item->isHidden()) { + // We set updating because item signal fire a deletion that reset whole parameter satellites + if (lpesatellites) { + lpesatellites->setUpdating(true); + item->deleteObject(true); + lpesatellites->setUpdating(false); + } else { + lpesatellite->setUpdating(true); + item->deleteObject(true); + lpesatellite->setUpdating(false); + } + } else { + elemnode->removeAttribute("sodipodi:insensitive"); + SPDefs *defs = dynamic_cast<SPDefs *>(elemref->parent); + if (!defs && sp_lpe_item) { + item->moveTo(sp_lpe_item, false); + } + } + break; + + case LPE_ERASE: + // We set updating because item signal fire a deletion that reset whole parameter satellites + if (lpesatellites) { + lpesatellites->setUpdating(true); + item->deleteObject(true); + lpesatellites->setUpdating(false); + } else { + lpesatellite->setUpdating(true); + item->deleteObject(true); + lpesatellite->setUpdating(false); + } + break; + + case LPE_VISIBILITY: + css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, elemref->getRepr()->attribute("style")); + if (!isVisible() /* && std::strcmp(elemref->getId(),sp_lpe_item->getId()) != 0*/) { + css->setAttribute("display", "none"); + } else { + css->removeAttribute("display"); + } + sp_repr_css_write_string(css, css_str); + elemnode->setAttributeOrRemoveIfEmpty("style", css_str); + if (sp_lpe_item) { + sp_lpe_item_enable_path_effects(sp_lpe_item, true); + sp_lpe_item_update_patheffect(sp_lpe_item, false, false); + sp_lpe_item_enable_path_effects(sp_lpe_item, false); + } + sp_repr_css_attr_unref( css ); + break; + default: + break; + } + } + } + } + if (lpe_action == LPE_ERASE || lpe_action == LPE_TO_OBJECTS) { + Inkscape::LivePathEffect::SatelliteArrayParam *lpesatellites = nullptr; + Inkscape::LivePathEffect::OriginalSatelliteParam *lpesatellite = nullptr; + std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + lpesatellites = dynamic_cast<SatelliteArrayParam *>(*p); + lpesatellite = dynamic_cast<OriginalSatelliteParam *>(*p); + if (lpesatellites) { + lpesatellites->clear(); + lpesatellites->write_to_SVG(); + } + if (lpesatellite) { + lpesatellite->unlink(); + lpesatellite->write_to_SVG(); + } + } + } + if (sp_lpe_item) { + sp_lpe_item_enable_path_effects(sp_lpe_item, true); + } +} + +// we delay till current operation is done to aboid deleted items crashes +void Effect::processObjects(LPEAction lpe_action) +{ + if (lpe_action == LPE_UPDATE && _lpe_action == LPE_NONE) { + _lpe_action = lpe_action; + return; + } + _lpe_action = lpe_action; + Inkscape::LivePathEffect::SatelliteArrayParam *lpesatellites = nullptr; + Inkscape::LivePathEffect::OriginalSatelliteParam *lpesatellite = nullptr; + std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + lpesatellites = dynamic_cast<SatelliteArrayParam *>(*p); + lpesatellite = dynamic_cast<OriginalSatelliteParam *>(*p); + if (lpesatellites || lpesatellite) { + break; + } + } + if (!lpesatellites && !lpesatellite) { + return; + } + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + sp_lpe_item = dynamic_cast<SPLPEItem *>(*getLPEObj()->hrefList.begin()); + if (!document || !sp_lpe_item) { + return; + } + std::vector<std::shared_ptr<SatelliteReference> > satelltelist; + if (lpesatellites) { + lpesatellites->read_from_SVG(); + satelltelist = lpesatellites->data(); + } else { + lpesatellite->read_from_SVG(); + satelltelist.push_back(lpesatellite->lperef); + } + for (auto &iter : satelltelist) { + SPObject *elemref; + if (iter && iter->isAttached() && (elemref = iter->getObject())) { + if (auto *item = dynamic_cast<SPItem *>(elemref)) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + switch (lpe_action) { + case LPE_TO_OBJECTS: + if (lpeitem && item->isHidden()) { + lpeitem->removeAllPathEffects(false); + } + break; + case LPE_ERASE: + if (lpeitem) { + lpeitem->removeAllPathEffects(false); + } + break; + default: + break; + } + } + } + } +} + +/** + * Is performed on load document or revert + * If the item is fixed legacy return true + */ +bool Effect::doOnOpen(SPLPEItem const * /*lpeitem*/) +{ + // Do nothing for simple effects + update_satellites(); + return false; +} + +void +Effect::update_satellites(bool updatelpe) { + std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + (*p)->update_satellites(updatelpe); + } +} + + +/** + * Is performed each time before the effect is updated. + */ +void +Effect::doBeforeEffect (SPLPEItem const*/*lpeitem*/) +{ + //Do nothing for simple effects +} +/** + * Is performed at the end of the LPE only one time per "lpeitem" + * in paths/shapes is called in middle of the effect so we add the + * "curve" param to allow updates in the LPE results at this stage + * for groups dont need to send "curve" parameter because is applied + * when the LPE process finish, we can update LPE results without "curve" parameter + * @param lpeitem the element that has this LPE + * @param curve the curve to pass when in mode path or shape + */ +void Effect::doAfterEffect (SPLPEItem const* /*lpeitem*/, SPCurve *curve) +{ + //Do nothing for simple effects + update_satellites(); +} + +void Effect::doOnException(SPLPEItem const * /*lpeitem*/) +{ + has_exception = true; + pathvector_after_effect = pathvector_before_effect; +} + + +void Effect::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ +} +void Effect::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ +} +//secret impl methods (shhhh!) +void Effect::doAfterEffect_impl(SPLPEItem const *lpeitem, SPCurve *curve) +{ + doAfterEffect(lpeitem, curve); + is_load = false; + is_applied = false; +} + +void Effect::doOnRemove_impl(SPLPEItem const* lpeitem) +{ + SPDocument *document = getSPDoc(); + sp_lpe_item = dynamic_cast<SPLPEItem *>(*getLPEObj()->hrefList.begin()); + if (!document || !sp_lpe_item) { + return; + } + std::vector<SPObject *> satellites = effect_get_satellites(true); + satellites.insert(satellites.begin(), sp_lpe_item); + doOnRemove(lpeitem); + for (auto obj:satellites) { + if (obj->getAttribute("class")){ + Glib::ustring newclass = obj->getAttribute("class"); + size_t pos = newclass.find("UnoptimicedTransforms"); + if (pos != std::string::npos) { + newclass.erase(pos, 21); + obj->setAttribute("class",newclass.empty() ? nullptr : newclass.c_str()); + } + } + } +} + +/** + * Is performed on document open allow things like fix legacy LPE in a undo insensitive way + */ +void Effect::doOnOpen_impl() +{ + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + is_load = true; + doOnOpen(lpeitems[0]); + } +} + +void Effect::doOnApply_impl(SPLPEItem const* lpeitem) +{ + sp_lpe_item = const_cast<SPLPEItem *>(lpeitem); + is_applied = true; + // we can override "lpeversion" value in each LPE using doOnApply + // this allow to handle legacy LPE and some times update to newest definitions + // In BBB Martin, Mc and Jabiertxof make decision + // to only update this value per each LPE when changes. + // and use the Inkscape release version that has this new LPE change + // LPE without lpeversion are created in an inkscape lower than 1.0 + lpeversion.param_setValue("1", true); + doOnApply(lpeitem); + setReady(); + has_exception = false; +} + +void Effect::doBeforeEffect_impl(SPLPEItem const* lpeitem) +{ + sp_lpe_item = const_cast<SPLPEItem *>(lpeitem); + doBeforeEffect(lpeitem); + if (is_load) { + update_satellites(false); + } + update_helperpath(); +} + +void +Effect::writeParamsToSVG() { + std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + (*p)->write_to_SVG(); + } +} + +std::vector<SPObject *> Effect::effect_get_satellites(bool force) +{ + std::vector<SPObject *> satellites; + if (!force && !satellitestoclipboard) { + return satellites; + } + std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + std::vector<SPObject *> tmp = (*p)->param_get_satellites(); + satellites.insert(satellites.begin(), tmp.begin(), tmp.end()); + } + return satellites; +} + +/** + * If the effect expects a path parameter (specified by a number of mouse clicks) before it is + * applied, this is the method that processes the resulting path. Override it to customize it for + * your LPE. But don't forget to call the parent method so that is_ready is set to true! + */ +void +Effect::acceptParamPath (SPPath const*/*param_path*/) { + setReady(); +} + +/* + * Here be the doEffect function chain: + */ +void +Effect::doEffect (SPCurve * curve) +{ + Geom::PathVector orig_pathv = curve->get_pathvector(); + + Geom::PathVector result_pathv = doEffect_path(orig_pathv); + + curve->set_pathvector(result_pathv); +} + +Geom::PathVector +Effect::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + + if ( !concatenate_before_pwd2 ) { + // default behavior + for (const auto & i : path_in) { + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_in = i.toPwSb(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_out = doEffect_pwd2(pwd2_in); + Geom::PathVector path = Geom::path_from_piecewise( pwd2_out, LPE_CONVERSION_TOLERANCE); + // add the output path vector to the already accumulated vector: + for (const auto & j : path) { + path_out.push_back(j); + } + } + } else { + // concatenate the path into possibly discontinuous pwd2 + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_in; + for (const auto & i : path_in) { + pwd2_in.concat( i.toPwSb() ); + } + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_out = doEffect_pwd2(pwd2_in); + path_out = Geom::path_from_piecewise( pwd2_out, LPE_CONVERSION_TOLERANCE); + } + + return path_out; +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +Effect::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + g_warning("Effect has no doEffect implementation"); + return pwd2_in; +} + +void +Effect::readallParameters(Inkscape::XML::Node const* repr) +{ + std::vector<Parameter *>::iterator it = param_vector.begin(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + while (it != param_vector.end()) { + Parameter * param = *it; + const gchar * key = param->param_key.c_str(); + const gchar * value = repr->attribute(key); + if (value) { + bool accepted = param->param_readSVGValue(value); + if (!accepted) { + g_warning("Effect::readallParameters - '%s' not accepted for %s", value, key); + } + } else { + Glib::ustring pref_path = (Glib::ustring)"/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring)"/" + + (Glib::ustring)key; + bool valid = prefs->getEntry(pref_path).isValid(); + if(valid){ + param->param_update_default(prefs->getString(pref_path).c_str()); + } else { + param->param_set_default(); + } + } + ++it; + } +} + +/* This function does not and SHOULD NOT write to XML */ +void +Effect::setParameter(const gchar * key, const gchar * new_value) +{ + Parameter * param = getParameter(key); + if (param) { + if (new_value) { + bool accepted = param->param_readSVGValue(new_value); + if (!accepted) { + g_warning("Effect::setParameter - '%s' not accepted for %s", new_value, key); + } + } else { + // set default value + param->param_set_default(); + } + } +} + +void +Effect::registerParameter(Parameter * param) +{ + param_vector.push_back(param); +} + + +/** + * Add all registered LPE knotholder handles to the knotholder + */ +void +Effect::addHandles(KnotHolder *knotholder, SPItem *item) { + using namespace Inkscape::LivePathEffect; + + // add handles provided by the effect itself + addKnotHolderEntities(knotholder, item); + + if (is_load) { + // needed by fillet and powerstroke LPEs + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (lpeitem) { + sp_lpe_item_update_patheffect(lpeitem, false, false); + } + } + + // add handles provided by the effect's parameters (if any) + for (auto & p : param_vector) { + p->addKnotHolderEntities(knotholder, item); + } +} + +/** + * Return a vector of PathVectors which contain all canvas indicators for this effect. + * This is the function called by external code to get all canvas indicators (effect and its parameters) + * lpeitem = the item onto which this effect is applied + * @todo change return type to one pathvector, add all paths to one pathvector instead of maintaining a vector of pathvectors + */ +std::vector<Geom::PathVector> +Effect::getCanvasIndicators(SPLPEItem const* lpeitem) +{ + std::vector<Geom::PathVector> hp_vec; + + // add indicators provided by the effect itself + addCanvasIndicators(lpeitem, hp_vec); + + // add indicators provided by the effect's parameters + for (auto & p : param_vector) { + p->addCanvasIndicators(lpeitem, hp_vec); + } + Geom::Affine scale = lpeitem->i2doc_affine(); + for (auto &path : hp_vec) { + path *= scale; + } + return hp_vec; +} + +/** + * Add possible canvas indicators (i.e., helperpaths other than the original path) to \a hp_vec + * This function should be overwritten by derived effects if they want to provide their own helperpaths. + */ +void +Effect::addCanvasIndicators(SPLPEItem const*/*lpeitem*/, std::vector<Geom::PathVector> &/*hp_vec*/) +{ +} + +/** + * Call to a method on nodetool to update the helper path from the effect + */ +void +Effect::update_helperpath() { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context); + if (nt) { + Inkscape::UI::Tools::sp_update_helperpath(desktop); + } + } +} + +/** + * This *creates* a new widget, management of deletion should be done by the caller + */ +Gtk::Widget * +Effect::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::Box * vbox = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_VERTICAL) ); + + vbox->set_border_width(5); + + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = param->param_newWidget(); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if (param->widget_is_enabled) { + widg->set_sensitive(true); + } else { + widg->set_sensitive(false); + } + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +bool sp_enter_tooltip(GdkEventCrossing *evt, Gtk::Widget *widg) +{ + widg->trigger_tooltip_query(); + return true; +} + +/** + * This *creates* a new widget, with default values setter + */ +Gtk::Widget * +Effect::defaultParamSet() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Gtk::Box * vbox_expander = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_VERTICAL) ); + Glib::ustring effectname = _(Inkscape::LivePathEffect::LPETypeConverter.get_label(effectType()).c_str()); + Glib::ustring effectkey = (Glib::ustring)Inkscape::LivePathEffect::LPETypeConverter.get_key(effectType()); + std::vector<Parameter *>::iterator it = param_vector.begin(); + bool has_params = false; + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + has_params = true; + Parameter * param = *it; + const gchar * key = param->param_key.c_str(); + if (g_strcmp0(key, "lpeversion") == 0) { + ++it; + continue; + } + const gchar * label = param->param_label.c_str(); + Glib::ustring value = param->param_getSVGValue(); + Glib::ustring defvalue = param->param_getDefaultSVGValue(); + Glib::ustring pref_path = "/live_effects/"; + pref_path += effectkey; + pref_path +="/"; + pref_path += key; + bool valid = prefs->getEntry(pref_path).isValid(); + const gchar * set_or_upd; + Glib::ustring def = Glib::ustring(_("<b>Default value:</b> ")) + defvalue; + Glib::ustring ove = Glib::ustring(_("<b>Default value overridden:</b> ")); + if (valid) { + set_or_upd = _("Update"); + def = ""; + } else { + set_or_upd = _("Set"); + ove = ""; + } + Gtk::Box * vbox_param = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) ); + Gtk::Box *namedicon = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + Gtk::Label *parameter_label = Gtk::manage(new Gtk::Label(label, Gtk::ALIGN_START)); + parameter_label->set_use_markup(true); + parameter_label->set_use_underline(true); + parameter_label->set_ellipsize(Pango::ELLIPSIZE_END); + Glib::ustring tooltip = Glib::ustring("<b>") + parameter_label->get_text() + Glib::ustring("</b>\n") + + param->param_tooltip + Glib::ustring("\n"); + Gtk::Image *info = sp_get_icon_image("info", 20); + Gtk::EventBox *infoeventbox = Gtk::manage(new Gtk::EventBox()); + infoeventbox->add(*info); + infoeventbox->set_tooltip_markup((tooltip + def + ove).c_str()); + namedicon->pack_start(*infoeventbox, false, false, 2); + namedicon->pack_start(*parameter_label, true, true, 2); + namedicon->set_homogeneous(false); + vbox_param->pack_start(*namedicon, true, true, 2); + Gtk::Button *set = Gtk::manage(new Gtk::Button((Glib::ustring)set_or_upd)); + Gtk::Button *unset = Gtk::manage(new Gtk::Button(Glib::ustring(_("Unset")))); + unset->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Effect::unsetDefaultParam), pref_path, + tooltip, param, info, set, unset)); + set->signal_clicked().connect(sigc::bind(sigc::mem_fun(*this, &Effect::setDefaultParam), pref_path, tooltip, + param, info, set, unset)); + if (!valid) { + unset->set_sensitive(false); + } + unset->set_size_request (90, -1); + set->set_size_request (90, -1); + vbox_param->pack_end(*unset, false, true, 2); + vbox_param->pack_end(*set, false, true, 2); + + vbox_expander->pack_start(*vbox_param, true, true, 2); + } + ++it; + } + Glib::ustring tip = "<b>" + effectname + (Glib::ustring)_("</b>: Set default parameters"); + Gtk::Expander * expander = Gtk::manage(new Gtk::Expander(tip)); + expander->set_use_markup(true); + expander->add(*vbox_expander); + expander->set_expanded(defaultsopen); + expander->property_expanded().signal_changed().connect(sigc::bind<0>(sigc::mem_fun(*this, &Effect::onDefaultsExpanderChanged), expander )); + if (has_params) { + Gtk::Widget *vboxwidg = dynamic_cast<Gtk::Widget *>(expander); + vboxwidg->set_margin_bottom(5); + vboxwidg->set_margin_top(5); + return vboxwidg; + } else { + return nullptr; + } +} + +void +Effect::onDefaultsExpanderChanged(Gtk::Expander * expander) +{ + defaultsopen = expander->get_expanded(); +} + +void Effect::setDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset) +{ + Glib::ustring value = param->param_getSVGValue(); + Glib::ustring defvalue = param->param_getDefaultSVGValue(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setString(pref_path, value); + gchar * label = _("Update"); + set->set_label((Glib::ustring)label); + unset->set_sensitive(true); + Glib::ustring ove = Glib::ustring(_("<b>Default value overridden:</b> ")) + value; + info->set_tooltip_markup((tooltip + ove).c_str()); +} + +void Effect::unsetDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset) +{ + Glib::ustring value = param->param_getSVGValue(); + Glib::ustring defvalue = param->param_getDefaultSVGValue(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->remove(pref_path); + gchar * label = _("Set"); + set->set_label((Glib::ustring)label); + unset->set_sensitive(false); + Glib::ustring def = Glib::ustring(_("<b>Default value:</b> Default")); + info->set_tooltip_markup((tooltip + def).c_str()); +} + +Inkscape::XML::Node *Effect::getRepr() +{ + return lpeobj->getRepr(); +} + +SPDocument *Effect::getSPDoc() +{ + if (lpeobj->document == nullptr) { + g_message("Effect::getSPDoc() returns NULL"); + } + return lpeobj->document; +} + +Parameter * +Effect::getParameter(const char * key) +{ + Glib::ustring stringkey(key); + + if (param_vector.empty()) return nullptr; + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + Parameter * param = *it; + if ( param->param_key == key) { + return param; + } + + ++it; + } + + return nullptr; +} + +Parameter * +Effect::getNextOncanvasEditableParam() +{ + if (param_vector.size() == 0) // no parameters + return nullptr; + + oncanvasedit_it++; + if (oncanvasedit_it >= static_cast<int>(param_vector.size())) { + oncanvasedit_it = 0; + } + int old_it = oncanvasedit_it; + + do { + Parameter * param = param_vector[oncanvasedit_it]; + if(param && param->oncanvas_editable) { + return param; + } else { + oncanvasedit_it++; + if (oncanvasedit_it == static_cast<int>(param_vector.size())) { // loop round the map + oncanvasedit_it = 0; + } + } + } while (oncanvasedit_it != old_it); // iterate until complete loop through map has been made + + return nullptr; +} + +void +Effect::editNextParamOncanvas(SPItem * item, SPDesktop * desktop) +{ + if (!desktop) return; + + Parameter * param = getNextOncanvasEditableParam(); + if (param) { + param->param_editOncanvas(item, desktop); + gchar *message = g_strdup_printf(_("Editing parameter <b>%s</b>."), param->param_label.c_str()); + desktop->messageStack()->flash(Inkscape::NORMAL_MESSAGE, message); + g_free(message); + } else { + desktop->messageStack()->flash( Inkscape::WARNING_MESSAGE, + _("None of the applied path effect's parameters can be edited on-canvas.") ); + } +} + +/* This function should reset the defaults and is used for example to initialize an effect right after it has been applied to a path +* The nice thing about this is that this function can use knowledge of the original path and set things accordingly for example to the size or origin of the original path! +*/ +void +Effect::resetDefaults(SPItem const* /*item*/) +{ + std::vector<Inkscape::LivePathEffect::Parameter *>::iterator p; + for (p = param_vector.begin(); p != param_vector.end(); ++p) { + (*p)->param_set_default(); + (*p)->write_to_SVG(); + } +} + +bool +Effect::providesKnotholder() const +{ + // does the effect actively provide any knotholder entities of its own? + if (_provides_knotholder_entities) { + return true; + } + + // otherwise: are there any parameters that have knotholderentities? + for (auto p : param_vector) { + if (p->providesKnotHolderEntities()) { + return true; + } + } + + return false; +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/effect.h b/src/live_effects/effect.h new file mode 100644 index 0000000..701297f --- /dev/null +++ b/src/live_effects/effect.h @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_H +#define INKSCAPE_LIVEPATHEFFECT_H + +/* + * Copyright (C) Johan Engelen 2007-2012 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "effect-enum.h" +#include "parameter/bool.h" +#include "parameter/hidden.h" +#include "ui/widget/registry.h" +#include <2geom/forward.h> +#include <glibmm/ustring.h> +#include <gtkmm/eventbox.h> +#include <gtkmm/expander.h> + + +#define LPE_CONVERSION_TOLERANCE 0.01 // FIXME: find good solution for this. + +class SPDocument; +class SPDesktop; +class SPItem; +class LivePathEffectObject; +class SPLPEItem; +class KnotHolder; +class KnotHolderEntity; +class SPPath; +class SPCurve; + +namespace Gtk { + class Widget; +} + +namespace Inkscape { + +namespace XML { + class Node; +} + +namespace LivePathEffect { + +enum LPEPathFlashType { + SUPPRESS_FLASH, +// PERMANENT_FLASH, + DEFAULT +}; + +enum LPEAction +{ + LPE_NONE = 0, + LPE_ERASE, + LPE_TO_OBJECTS, + LPE_VISIBILITY, + LPE_UPDATE +}; + +class Effect { +public: + static Effect* New(EffectType lpenr, LivePathEffectObject *lpeobj); + static void createAndApply(const char* name, SPDocument *doc, SPItem *item); + static void createAndApply(EffectType type, SPDocument *doc, SPItem *item); + + virtual ~Effect(); + Effect(const Effect&) = delete; + Effect& operator=(const Effect&) = delete; + + EffectType effectType() const; + + //basically, to get this method called before the derived classes, a bit + //of indirection is needed. We first call these methods, then the below. + void doAfterEffect_impl(SPLPEItem const *lpeitem, SPCurve *curve); + void doOnApply_impl(SPLPEItem const* lpeitem); + void doBeforeEffect_impl(SPLPEItem const* lpeitem); + void doOnOpen_impl(); + void doOnRemove_impl(SPLPEItem const* lpeitem); + void transform_multiply_impl(Geom::Affine const &postmul, SPLPEItem *); + void doOnBeforeCommit(); + void doOnUndo(); + void setCurrentZoom(double cZ); + void setSelectedNodePoints(std::vector<Geom::Point> sNP); + bool isNodePointSelected(Geom::Point const &nodePoint) const; + bool isOnClipboard(); + std::vector<SPLPEItem *> getCurrrentLPEItems() const; + void update_satellites(bool updatelpe = false); + virtual void doOnException(SPLPEItem const *lpeitem); + virtual void doOnVisibilityToggled(SPLPEItem const* lpeitem); + void writeParamsToSVG(); + std::vector<SPObject *> effect_get_satellites(bool force = true); + virtual void acceptParamPath (SPPath const* param_path); + static int acceptsNumClicks(EffectType type); + int acceptsNumClicks() const { return acceptsNumClicks(effectType()); } + SPShape * getCurrentShape() const { return current_shape; }; + void setCurrentShape(SPShape * shape) { current_shape = shape; } + virtual void processObjects(LPEAction lpe_action); + + /* + * isReady() indicates whether all preparations which are necessary to apply the LPE are done, + * e.g., waiting for a parameter path either before the effect is created or when it needs a + * path as argument. This is set in SPLPEItem::addPathEffect(). + */ + inline bool isReady() const { return is_ready; } + inline void setReady(bool ready = true) { is_ready = ready; } + + virtual void doEffect (SPCurve * curve); + + virtual Gtk::Widget * newWidget(); + virtual Gtk::Widget * defaultParamSet(); + /** + * Sets all parameters to their default values and writes them to SVG. + */ + virtual void resetDefaults(SPItem const* item); + + // /TODO: providesKnotholder() is currently used as an indicator of whether a nodepath is + // created for an item or not. When we allow both at the same time, this needs rethinking! + bool providesKnotholder() const; + // /TODO: in view of providesOwnFlashPaths() below, this is somewhat redundant + // (but spiro lpe still needs it!) + virtual LPEPathFlashType pathFlashType() const { return DEFAULT; } + void addHandles(KnotHolder *knotholder, SPItem *item); + std::vector<Geom::PathVector> getCanvasIndicators(SPLPEItem const* lpeitem); + void update_helperpath(); + bool has_exception; + + inline bool providesOwnFlashPaths() const { + return provides_own_flash_paths || show_orig_path; + } + inline bool showOrigPath() const { return show_orig_path; } + + Glib::ustring getName() const; + Inkscape::XML::Node * getRepr(); + SPDocument * getSPDoc(); + LivePathEffectObject * getLPEObj() {return lpeobj;}; + LivePathEffectObject const * getLPEObj() const {return lpeobj;}; + Parameter * getParameter(const char * key); + + void readallParameters(Inkscape::XML::Node const* repr); + void setParameter(const gchar * key, const gchar * new_value); + + inline bool isVisible() const { return is_visible; } + + void editNextParamOncanvas(SPItem * item, SPDesktop * desktop); + bool apply_to_clippath_and_mask; + bool keep_paths; // set this to false allow retain extra generated objects, see measure line LPE + bool is_load; + bool is_applied; + bool on_remove_all; + bool refresh_widgets; + bool finishiddle = false; + bool satellitestoclipboard = false; + bool helperLineSatellites = false; + BoolParam is_visible; + HiddenParam lpeversion; + Geom::PathVector pathvector_before_effect; + Geom::PathVector pathvector_after_effect; + SPLPEItem *sp_lpe_item; // these get stored in doBeforeEffect_impl, and derived classes may do as they please with + // them. + SPShape *current_shape; // these get stored in performPathEffects. + std::vector<Parameter *> param_vector; + +protected: + Effect(LivePathEffectObject *lpeobject); + friend class SatelliteArrayParam; + friend class LPEMeasureSegments; + // provide a set of doEffect functions so the developer has a choice + // of what kind of input/output parameters he desires. + // the order in which they appear is the order in which they are + // called by this base class. (i.e. doEffect(SPCurve * curve) defaults to calling + // doEffect(Geom::PathVector ) + virtual Geom::PathVector + doEffect_path (Geom::PathVector const & path_in); + virtual Geom::Piecewise<Geom::D2<Geom::SBasis> > + doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in); + + void registerParameter(Parameter * param); + Parameter * getNextOncanvasEditableParam(); + + virtual void addKnotHolderEntities(KnotHolder * /*knotholder*/, SPItem * /*item*/) {}; + + virtual void addCanvasIndicators(SPLPEItem const* lpeitem, std::vector<Geom::PathVector> &hp_vec); + + bool _provides_knotholder_entities; + LPEAction _lpe_action = LPE_NONE; + int oncanvasedit_it; + bool show_orig_path; // set this to true in derived effects to automatically have the original + // path displayed as helperpath + // this boolean defaults to false, it concatenates the input path to one pwd2, + // instead of normally 'splitting' the path into continuous pwd2 paths and calling doEffect_pwd2 for each. + bool concatenate_before_pwd2; + double current_zoom; + std::vector<Geom::Point> selectedNodesPoints; + Inkscape::UI::Widget::Registry wr; + +private: + LivePathEffectObject *lpeobj; + virtual void transform_multiply(Geom::Affine const &postmul, bool set); + virtual bool doOnOpen(SPLPEItem const *lpeitem); + virtual void doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve); + virtual void doOnRemove (SPLPEItem const* lpeitem); + virtual void doOnApply (SPLPEItem const* lpeitem); + virtual void doBeforeEffect (SPLPEItem const* lpeitem); + void onDefaultsExpanderChanged(Gtk::Expander * expander); + void setDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset); + void unsetDefaultParam(Glib::ustring pref_path, Glib::ustring tooltip, Parameter *param, Gtk::Image *info, + Gtk::Button *set, Gtk::Button *unset); + bool provides_own_flash_paths; // if true, the standard flash path is suppressed + sigc::connection _before_commit_connection; + + bool is_ready; + bool defaultsopen; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/fill-conversion.cpp b/src/live_effects/fill-conversion.cpp new file mode 100644 index 0000000..b019855 --- /dev/null +++ b/src/live_effects/fill-conversion.cpp @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Fill/stroke conversion routines for LPEs which draw a stroke + * + * Authors: + * Liam P White + * + * Copyright (C) 2020 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "fill-conversion.h" + +#include "document.h" +#include "desktop-style.h" +#include "object/sp-shape.h" +#include "object/sp-defs.h" +#include "object/sp-paint-server.h" +#include "svg/svg-color.h" +#include "svg/css-ostringstream.h" +#include "style.h" + +static SPObject *generate_linked_fill(SPShape *source) +{ + // Create new fill and effect object + SPDocument *doc = source->document; + SPObject *defs = doc->getDefs(); + + Inkscape::XML::Node *effectRepr = doc->getReprDoc()->createElement("inkscape:path-effect"); + SPObject *effectObj = nullptr; + Inkscape::XML::Node *pathRepr = doc->getReprDoc()->createElement("svg:path"); + SPObject *pathObj = nullptr; + + gchar *effectTarget = nullptr; + gchar *pathTarget = nullptr; + + effectTarget = g_strdup_printf("#%s,0,1", source->getId()); + effectRepr->setAttribute("effect", "fill_between_many"); + effectRepr->setAttribute("method", "bsplinespiro"); + effectRepr->setAttribute("linkedpaths", effectTarget); + defs->appendChild(effectRepr); + Inkscape::GC::release(effectRepr); + + effectObj = doc->getObjectByRepr(effectRepr); + + pathTarget = g_strdup_printf("#%s", effectObj->getId()); + pathRepr->setAttribute("inkscape:original-d", "M 0,0"); + pathRepr->setAttribute("inkscape:path-effect", pathTarget); + pathRepr->setAttribute("d", "M 0,0"); + + SPObject *prev = source->getPrev(); + source->parent->addChild(pathRepr, prev ? prev->getRepr() : nullptr); + Inkscape::GC::release(pathRepr); + pathObj = doc->getObjectByRepr(pathRepr); + source->setAttribute("inkscape:linked-fill", pathObj->getId()); + + g_free(effectTarget); + g_free(pathTarget); + + return pathObj; +} + +static SPObject *get_linked_fill(SPObject *source) +{ + SPDocument *doc = source->document; + + char const *linked_fill_id = source->getAttribute("inkscape:linked-fill"); + if (!linked_fill_id) { + return nullptr; + } + + return doc->getObjectById(linked_fill_id); +} + +static void convert_fill_color(SPCSSAttr *css, SPObject *source) +{ + char c[64]; + + sp_svg_write_color(c, sizeof(c), source->style->fill.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(source->style->fill_opacity.value))); + sp_repr_css_set_property(css, "fill", c); +} + +static void convert_stroke_color(SPCSSAttr *css, SPObject *source) +{ + char c[64]; + + sp_svg_write_color(c, sizeof(c), source->style->stroke.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(source->style->stroke_opacity.value))); + sp_repr_css_set_property(css, "fill", c); +} + +static void revert_stroke_color(SPCSSAttr *css, SPObject *source) +{ + char c[64]; + + sp_svg_write_color(c, sizeof(c), source->style->fill.value.color.toRGBA32(SP_SCALE24_TO_FLOAT(source->style->fill_opacity.value))); + sp_repr_css_set_property(css, "stroke", c); +} + +static void convert_fill_server(SPCSSAttr *css, SPObject *source) +{ + SPPaintServer *server = source->style->getFillPaintServer(); + + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property(css, "fill", str.c_str()); + } +} + +static void convert_stroke_server(SPCSSAttr *css, SPObject *source) +{ + SPPaintServer *server = source->style->getStrokePaintServer(); + + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property(css, "fill", str.c_str()); + } +} + +static void revert_stroke_server(SPCSSAttr *css, SPObject *source) +{ + SPPaintServer *server = source->style->getFillPaintServer(); + + if (server) { + Glib::ustring str; + str += "url(#"; + str += server->getId(); + str += ")"; + sp_repr_css_set_property(css, "stroke", str.c_str()); + } +} + +static bool has_fill(SPObject *source) +{ + return source->style->fill.isColor() || source->style->fill.isPaintserver(); +} + +static bool has_stroke(SPObject *source) +{ + return source->style->stroke.isColor() || source->style->stroke.isPaintserver(); +} + +namespace Inkscape { +namespace LivePathEffect { + +void lpe_shape_convert_stroke_and_fill(SPShape *shape) +{ + if (has_fill(shape)) { + SPCSSAttr *fill_css = sp_repr_css_attr_new(); + SPObject *linked = generate_linked_fill(shape); + + if (shape->style->fill.isColor()) { + convert_fill_color(fill_css, shape); + } else { + convert_fill_server(fill_css, shape); + } + + sp_desktop_apply_css_recursive(linked, fill_css, true); + sp_repr_css_attr_unref(fill_css); + } + + SPCSSAttr *stroke_css = sp_repr_css_attr_new(); + + if (has_stroke(shape)) { + if (shape->style->stroke.isColor()) { + convert_stroke_color(stroke_css, shape); + } else { + convert_stroke_server(stroke_css, shape); + } + } + + sp_repr_css_set_property(stroke_css, "fill-rule", "nonzero"); + sp_repr_css_set_property(stroke_css, "stroke", "none"); + sp_desktop_apply_css_recursive(shape, stroke_css, true); + sp_repr_css_attr_unref(stroke_css); +} + +void lpe_shape_revert_stroke_and_fill(SPShape *shape, double width) +{ + SPObject *linked = get_linked_fill(shape); + SPCSSAttr *css = sp_repr_css_attr_new(); + + if (has_fill(shape)) { + if (shape->style->fill.isColor()) { + revert_stroke_color(css, shape); + } else { + revert_stroke_server(css, shape); + } + } + + if (linked != nullptr) { + if (linked->style->fill.isColor()) { + convert_fill_color(css, linked); + } else { + convert_fill_server(css, linked); + } + + linked->deleteObject(); + } else { + sp_repr_css_set_property(css, "fill", "none"); + } + + Inkscape::CSSOStringStream os; + os << fabs(width); + sp_repr_css_set_property(css, "stroke-width", os.str().c_str()); + + sp_desktop_apply_css_recursive(shape, css, true); + sp_repr_css_attr_unref(css); +} + +} +} + +/* + 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 : diff --git a/src/live_effects/fill-conversion.h b/src/live_effects/fill-conversion.h new file mode 100644 index 0000000..e588019 --- /dev/null +++ b/src/live_effects/fill-conversion.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_FILLCONVERSION_H +#define INKSCAPE_FILLCONVERSION_H + +/* + * Copyright (C) Liam P White 2020 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +class SPShape; + +namespace Inkscape { +namespace LivePathEffect { + +/** + * Prepares a SPShape's fill and stroke for use in a path effect by setting + * the existing stroke properties into the shape's fill, and setting the + * existing fill properties into a new linked fill item which follows the + * old shape. + */ +void lpe_shape_convert_stroke_and_fill(SPShape *shape); + +/** + * Applies the fill of the SPShape to its stroke, sets the stroke width to the + * provided parameter, and potentially removes a linked fill, copying its + * properties to the fill of the shape. Essentially undoes the result of the + * above function. + */ +void lpe_shape_revert_stroke_and_fill(SPShape *shape, double width); + +} +} + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-angle_bisector.cpp b/src/live_effects/lpe-angle_bisector.cpp new file mode 100644 index 0000000..28afe20 --- /dev/null +++ b/src/live_effects/lpe-angle_bisector.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-angle_bisector.h" +#include "2geom/sbasis-to-bezier.h" + +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +namespace AB { + +class KnotHolderEntityLeftEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityLeftEnd(LPEAngleBisector* effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityRightEnd(LPEAngleBisector* effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +} // namespace AB + +LPEAngleBisector::LPEAngleBisector(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + length_left(_("Length left:"), _("Specifies the left end of the bisector"), "length-left", &wr, this, 0), + length_right(_("Length right:"), _("Specifies the right end of the bisector"), "length-right", &wr, this, 250) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + registerParameter( dynamic_cast<Parameter *>(&length_left) ); + registerParameter( dynamic_cast<Parameter *>(&length_right) ); +} + +LPEAngleBisector::~LPEAngleBisector() += default; + +Geom::PathVector +LPEAngleBisector::doEffect_path (Geom::PathVector const & path_in) +{ + using namespace Geom; + + // we assume that the path has >= 3 nodes + ptA = path_in[0].pointAt(1); + Point B = path_in[0].initialPoint(); + Point C = path_in[0].pointAt(2); + + double angle = angle_between(B - ptA, C - ptA); + + dir = unit_vector(B - ptA) * Rotate(angle/2); + + Geom::Point D = ptA - dir * length_left; + Geom::Point E = ptA + dir * length_right; + + Piecewise<D2<SBasis> > output = Piecewise<D2<SBasis> >(D2<SBasis>(SBasis(D[X], E[X]), SBasis(D[Y], E[Y]))); + + return path_from_piecewise(output, LPE_CONVERSION_TOLERANCE); +} + +void +LPEAngleBisector::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) { + { + KnotHolderEntity *e = new AB::KnotHolderEntityLeftEnd(this); + e->create(desktop, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:LeftEnd", + _("Adjust the \"left\" end of the bisector")); + knotholder->add(e); + } + { + KnotHolderEntity *e = new AB::KnotHolderEntityRightEnd(this); + e->create(desktop, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:RightEnd", + _("Adjust the \"right\" end of the bisector")); + knotholder->add(e); + } +}; + +namespace AB { + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPEAngleBisector *lpe = dynamic_cast<LPEAngleBisector *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->dir); + lpe->length_left.param_set_value(-lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPEAngleBisector *lpe = dynamic_cast<LPEAngleBisector *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->dir); + lpe->length_right.param_set_value(lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const +{ + LPEAngleBisector const* lpe = dynamic_cast<LPEAngleBisector const*>(_effect); + return lpe->ptA - lpe->dir * lpe->length_left; +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const +{ + LPEAngleBisector const* lpe = dynamic_cast<LPEAngleBisector const*>(_effect); + return lpe->ptA + lpe->dir * lpe->length_right; +} + +} // namespace AB + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-angle_bisector.h b/src/live_effects/lpe-angle_bisector.h new file mode 100644 index 0000000..b780fce --- /dev/null +++ b/src/live_effects/lpe-angle_bisector.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ANGLE_BISECTOR_H +#define INKSCAPE_LPE_ANGLE_BISECTOR_H + +/* + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace AB { + // we use a separate namespace to avoid clashes with other LPEs + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; +} + +class LPEAngleBisector : public Effect { +public: + LPEAngleBisector(LivePathEffectObject *lpeobject); + ~LPEAngleBisector() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + + friend class AB::KnotHolderEntityLeftEnd; + friend class AB::KnotHolderEntityRightEnd; + void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + +//private: + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point ptA; + Geom::Point dir; + + LPEAngleBisector(const LPEAngleBisector&); + LPEAngleBisector& operator=(const LPEAngleBisector&); +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-attach-path.cpp b/src/live_effects/lpe-attach-path.cpp new file mode 100644 index 0000000..a6b4b03 --- /dev/null +++ b/src/live_effects/lpe-attach-path.cpp @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include "live_effects/lpe-attach-path.h" +#include "display/curve.h" +#include "2geom/path-sink.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEAttachPath::LPEAttachPath(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + start_path(_("Start path:"), _("Path to attach to the start of this path"), "startpath", &wr, this), + start_path_position(_("Start path position:"), _("Position to attach path start to"), "startposition", &wr, this, 0.0), + start_path_curve_start(_("Start path curve start:"), _("Starting curve"), "startcurvestart", &wr, this, Geom::Point(20,0)/*, true*/), + start_path_curve_end(_("Start path curve end:"), _("Ending curve"), "startcurveend", &wr, this, Geom::Point(20,0)/*, true*/), + end_path(_("End path:"), _("Path to attach to the end of this path"), "endpath", &wr, this), + end_path_position(_("End path position:"), _("Position to attach path end to"), "endposition", &wr, this, 0.0), + end_path_curve_start(_("End path curve start:"), _("Starting curve"), "endcurvestart", &wr, this, Geom::Point(20,0)/*, true*/), + end_path_curve_end(_("End path curve end:"), _("Ending curve"), "endcurveend", &wr, this, Geom::Point(20,0)/*, true*/) +{ + registerParameter(&start_path); + registerParameter(&start_path_position); + registerParameter(&start_path_curve_start); + registerParameter(&start_path_curve_end); + + registerParameter(&end_path); + registerParameter(&end_path_position); + registerParameter(&end_path_curve_start); + registerParameter(&end_path_curve_end); + + //perceived_path = true; + show_orig_path = true; + curve_start_previous_origin = start_path_curve_end.getOrigin(); + curve_end_previous_origin = end_path_curve_end.getOrigin(); + start_path.setUpdating(true); + end_path.setUpdating(true); +} + +LPEAttachPath::~LPEAttachPath() += default; + +void LPEAttachPath::resetDefaults(SPItem const * /*item*/) +{ + curve_start_previous_origin = start_path_curve_end.getOrigin(); + curve_end_previous_origin = end_path_curve_end.getOrigin(); +} + +void +LPEAttachPath::doBeforeEffect (SPLPEItem const* lpeitem) +{ + if (is_load) { + start_path.setUpdating(false); + start_path.start_listening(start_path.getObject()); + start_path.connect_selection_changed(); + end_path.setUpdating(false); + end_path.start_listening(end_path.getObject()); + end_path.connect_selection_changed(); + SPItem * item = nullptr; + if (( item = dynamic_cast<SPItem *>(end_path.getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + if (( item = dynamic_cast<SPItem *>(start_path.getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } +} + + +bool +LPEAttachPath::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + start_path.setUpdating(false); + start_path.start_listening(start_path.getObject()); + start_path.connect_selection_changed(); + end_path.setUpdating(false); + end_path.start_listening(end_path.getObject()); + end_path.connect_selection_changed(); + return false; +} + +void LPEAttachPath::doEffect (SPCurve * curve) +{ + Geom::PathVector this_pathv = curve->get_pathvector(); + if (sp_lpe_item && !this_pathv.empty()) { + Geom::Path p = Geom::Path(this_pathv.front().initialPoint()); + + bool set_start_end = start_path_curve_end.getOrigin() != curve_start_previous_origin; + bool set_end_end = end_path_curve_end.getOrigin() != curve_end_previous_origin; + + if (start_path.linksToPath() && start_path.getObject()) { + + Geom::PathVector linked_pathv = start_path.get_pathvector(); + Geom::Affine linkedtransform = start_path.getObject()->getRelativeTransform(sp_lpe_item); + + if ( !linked_pathv.empty() ) + { + Geom::Path transformedpath = linked_pathv.front() * linkedtransform; + start_path_curve_start.setOrigin(this_pathv.front().initialPoint()); + + std::vector<Geom::Point> derivs = this_pathv.front().front().pointAndDerivatives(0, 3); + + for (unsigned deriv_n = 1; deriv_n < derivs.size(); deriv_n++) { + Geom::Coord length = derivs[deriv_n].length(); + if ( ! Geom::are_near(length, 0) && !start_path.getObject()->_successor) { + if (set_start_end) { + start_path_position.param_set_value(transformedpath.nearestTime(start_path_curve_end.getOrigin()).asFlatTime()); + } + if (start_path_position > transformedpath.size()) { + start_path_position.param_set_value(transformedpath.size()); + } else if (start_path_position < 0) { + start_path_position.param_set_value(0); + } + Geom::Curve const *c = start_path_position >= transformedpath.size() ? + &transformedpath.back() : &transformedpath.at((int)start_path_position); + + std::vector<Geom::Point> derivs_2 = c->pointAndDerivatives(start_path_position >= transformedpath.size() ? 1 : (start_path_position - (int)start_path_position), 3); + for (unsigned deriv_n_2 = 1; deriv_n_2 < derivs_2.size(); deriv_n_2++) { + Geom::Coord length_2 = derivs[deriv_n_2].length(); + if ( ! Geom::are_near(length_2, 0) ) { + start_path_curve_end.setOrigin(derivs_2[0]); + curve_start_previous_origin = start_path_curve_end.getOrigin(); + + double startangle = atan2(start_path_curve_start.getVector().y(), start_path_curve_start.getVector().x()); + double endangle = atan2(start_path_curve_end.getVector().y(), start_path_curve_end.getVector().x()); + double startderiv = atan2(derivs[deriv_n].y(), derivs[deriv_n].x()); + double endderiv = atan2(derivs_2[deriv_n_2].y(), derivs_2[deriv_n_2].x()); + Geom::Point pt1 = Geom::Point(start_path_curve_start.getVector().length() * cos(startangle + startderiv), start_path_curve_start.getVector().length() * sin(startangle + startderiv)); + Geom::Point pt2 = Geom::Point(start_path_curve_end.getVector().length() * cos(endangle + endderiv), start_path_curve_end.getVector().length() * sin(endangle + endderiv)); + p = Geom::Path(derivs_2[0]); + p.appendNew<Geom::CubicBezier>(-pt2 + derivs_2[0], -pt1 + this_pathv.front().initialPoint(), this_pathv.front().initialPoint()); + break; + + } + } + break; + } + } + } + } + + p.append(this_pathv.front()); + + if (end_path.linksToPath() && end_path.getObject()) { + + Geom::PathVector linked_pathv = end_path.get_pathvector(); + Geom::Affine linkedtransform = end_path.getObject()->getRelativeTransform(sp_lpe_item); + + if ( !linked_pathv.empty() ) + { + Geom::Path transformedpath = linked_pathv.front() * linkedtransform; + Geom::Curve * last_seg_reverse = this_pathv.front().back().reverse(); + + end_path_curve_start.setOrigin(last_seg_reverse->initialPoint()); + + std::vector<Geom::Point> derivs = last_seg_reverse->pointAndDerivatives(0, 3); + for (unsigned deriv_n = 1; deriv_n < derivs.size(); deriv_n++) { + Geom::Coord length = derivs[deriv_n].length(); + if ( ! Geom::are_near(length, 0) && !end_path.getObject()->_successor) { + if (set_end_end) { + end_path_position.param_set_value(transformedpath.nearestTime(end_path_curve_end.getOrigin()).asFlatTime()); + } + + if (end_path_position > transformedpath.size()) { + end_path_position.param_set_value(transformedpath.size()); + } else if (end_path_position < 0) { + end_path_position.param_set_value(0); + } + const Geom::Curve *c = end_path_position >= transformedpath.size() ? + &transformedpath.back() : &transformedpath.at((int)end_path_position); + + std::vector<Geom::Point> derivs_2 = c->pointAndDerivatives(end_path_position >= transformedpath.size() ? 1 : (end_path_position - (int)end_path_position), 3); + for (unsigned deriv_n_2 = 1; deriv_n_2 < derivs_2.size(); deriv_n_2++) { + Geom::Coord length_2 = derivs[deriv_n_2].length(); + if ( ! Geom::are_near(length_2, 0) ) { + + end_path_curve_end.setOrigin(derivs_2[0]); + curve_end_previous_origin = end_path_curve_end.getOrigin(); + + double startangle = atan2(end_path_curve_start.getVector().y(), end_path_curve_start.getVector().x()); + double endangle = atan2(end_path_curve_end.getVector().y(), end_path_curve_end.getVector().x()); + double startderiv = atan2(derivs[deriv_n].y(), derivs[deriv_n].x()); + double endderiv = atan2(derivs_2[deriv_n_2].y(), derivs_2[deriv_n_2].x()); + Geom::Point pt1 = Geom::Point(end_path_curve_start.getVector().length() * cos(startangle + startderiv), end_path_curve_start.getVector().length() * sin(startangle + startderiv)); + Geom::Point pt2 = Geom::Point(end_path_curve_end.getVector().length() * cos(endangle + endderiv), end_path_curve_end.getVector().length() * sin(endangle + endderiv)); + p.appendNew<Geom::CubicBezier>(-pt1 + this_pathv.front().finalPoint(), -pt2 + derivs_2[0], derivs_2[0]); + + break; + + } + } + break; + } + } + delete last_seg_reverse; + } + } + Geom::PathVector outvector; + outvector.push_back(p); + curve->set_pathvector(outvector); + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-attach-path.h b/src/live_effects/lpe-attach-path.h new file mode 100644 index 0000000..ce995a5 --- /dev/null +++ b/src/live_effects/lpe-attach-path.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ATTACH_PATH_H +#define INKSCAPE_LPE_ATTACH_PATH_H + +/* + * Inkscape::LPEAttachPath + * + * Copyright (C) Ted Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/originalpath.h" +#include "live_effects/parameter/vector.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/transformedpoint.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEAttachPath : public Effect { +public: + LPEAttachPath(LivePathEffectObject *lpeobject); + ~LPEAttachPath() override; + + void doEffect (SPCurve * curve) override; + void resetDefaults(SPItem const * item) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + +private: + LPEAttachPath(const LPEAttachPath&) = delete; + LPEAttachPath& operator=(const LPEAttachPath&) = delete; + + Geom::Point curve_start_previous_origin; + Geom::Point curve_end_previous_origin; + + OriginalPathParam start_path; + ScalarParam start_path_position; + TransformedPointParam start_path_curve_start; + VectorParam start_path_curve_end; + + OriginalPathParam end_path; + ScalarParam end_path_position; + TransformedPointParam end_path_curve_start; + VectorParam end_path_curve_end; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bendpath.cpp b/src/live_effects/lpe-bendpath.cpp new file mode 100644 index 0000000..9aaeeff --- /dev/null +++ b/src/live_effects/lpe-bendpath.cpp @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Steren Giannini 2008 <steren.giannini@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> + +#include "display/curve.h" +#include "live_effects/lpe-bendpath.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +/* Theory in e-mail from J.F. Barraud +Let B be the skeleton path, and P the pattern (the path to be deformed). + +P is a map t --> P(t) = ( x(t), y(t) ). +B is a map t --> B(t) = ( a(t), b(t) ). + +The first step is to re-parametrize B by its arc length: this is the parametrization in which a point p on B is located by its distance s from start. One obtains a new map s --> U(s) = (a'(s),b'(s)), that still describes the same path B, but where the distance along B from start to +U(s) is s itself. + +We also need a unit normal to the path. This can be obtained by computing a unit tangent vector, and rotate it by 90�. Call this normal vector N(s). + +The basic deformation associated to B is then given by: + + (x,y) --> U(x)+y*N(x) + +(i.e. we go for distance x along the path, and then for distance y along the normal) + +Of course this formula needs some minor adaptations (as is it depends on the absolute position of P for instance, so a little translation is needed +first) but I think we can first forget about them. +*/ + +namespace Inkscape { +namespace LivePathEffect { + +namespace BeP { +class KnotHolderEntityWidthBendPath : public LPEKnotHolderEntity { + public: + KnotHolderEntityWidthBendPath(LPEBendPath * effect) : LPEKnotHolderEntity(effect) {} + ~KnotHolderEntityWidthBendPath() override + { + LPEBendPath *lpe = dynamic_cast<LPEBendPath *> (_effect); + lpe->_knot_entity = nullptr; + } + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + }; +} // BeP + +LPEBendPath::LPEBendPath(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + bend_path(_("Bend path:"), _("Path along which to bend the original path"), "bendpath", &wr, this, "M0,0 L1,0"), + original_height(0.0), + prop_scale(_("_Width:"), _("Width of the path"), "prop_scale", &wr, this, 1.0), + scale_y_rel(_("W_idth in units of length"), _("Scale the width of the path in units of its length"), "scale_y_rel", &wr, this, false), + vertical_pattern(_("_Original path is vertical"), _("Rotates the original 90 degrees, before bending it along the bend path"), "vertical", &wr, this, false), + hide_knot(_("Hide width knot"), _("Hide width knot"),"hide_knot", &wr, this, false) +{ + registerParameter( &bend_path ); + registerParameter( &prop_scale); + registerParameter( &scale_y_rel); + registerParameter( &vertical_pattern); + registerParameter(&hide_knot); + + prop_scale.param_set_digits(3); + prop_scale.param_set_increments(0.01, 0.10); + + _knot_entity = nullptr; + _provides_knotholder_entities = true; + apply_to_clippath_and_mask = true; + concatenate_before_pwd2 = true; +} + +LPEBendPath::~LPEBendPath() += default; + + +bool +LPEBendPath::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + bend_path.reload(); + return false; +} + + +void +LPEBendPath::doBeforeEffect (SPLPEItem const* lpeitem) +{ + // get the item bounding box + original_bbox(lpeitem, false, true); + original_height = boundingbox_Y.max() - boundingbox_Y.min(); + if (is_load) { + bend_path.reload(); + } + if (_knot_entity) { + if (hide_knot) { + helper_path.clear(); + _knot_entity->knot->hide(); + } else { + _knot_entity->knot->show(); + } + _knot_entity->update_knot(); + } +} + +void LPEBendPath::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Selection * selection = nullptr; + SPItem *linked = nullptr; + if (SP_ACTIVE_DESKTOP) { + selection = SP_ACTIVE_DESKTOP->getSelection(); + linked = dynamic_cast<SPItem *>(bend_path.getObject()); + } + if (linked) { + linked->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + return; + } + if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) { + bend_path.param_transform_multiply(postmul, false); + } else if( //this allow transform user spected way when lpeitem and pathparamenter are both selected + sp_lpe_item && + sp_lpe_item->pathEffectsEnabled() && + linked && + (selection->includes(linked))) + { + Geom::Affine transformlpeitem = sp_item_transform_repr(sp_lpe_item).inverse() * postmul; + sp_lpe_item->transform *= transformlpeitem.inverse(); + sp_lpe_item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEBendPath::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + /* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */ + Geom::Affine affine = bend_path.get_relative_affine(); + + if (bend_path.changed) { + uskeleton = arc_length_parametrization(Piecewise<D2<SBasis> >(bend_path.get_pwd2() * affine),2,.1); + uskeleton = remove_short_cuts(uskeleton,.01); + n = rot90(derivative(uskeleton)); + n = force_continuity(remove_short_cuts(n,.01)); + + bend_path.changed = false; + } + + if (uskeleton.empty()) { + return pwd2_in; /// \todo or throw an exception instead? might be better to throw an exception so that the UI can display an error message or smth + } + + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pwd2_in); + Piecewise<SBasis> x = vertical_pattern.get_value() ? Piecewise<SBasis>(patternd2[1]) : Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y = vertical_pattern.get_value() ? Piecewise<SBasis>(patternd2[0]) : Piecewise<SBasis>(patternd2[1]); + + Interval bboxHorizontal = vertical_pattern.get_value() ? boundingbox_Y : boundingbox_X; + Interval bboxVertical = vertical_pattern.get_value() ? boundingbox_X : boundingbox_Y; + + //+0.1 in x fix bug #1658855 + //We use the group bounding box size or the path bbox size to translate well x and y + x-= bboxHorizontal.min() + 0.1; + y-= bboxVertical.middle(); + + double scaling = uskeleton.cuts.back()/bboxHorizontal.extent(); + + if (scaling != 1.0) { + x*=scaling; + } + + if ( scale_y_rel.get_value() ) { + y*=(scaling*prop_scale); + } else { + if (prop_scale != 1.0) y *= prop_scale; + } + + Piecewise<D2<SBasis> > output = compose(uskeleton,x) + y*compose(n,x); + return output; +} + +void +LPEBendPath::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); + + Geom::Point start(boundingbox_X.min(), (boundingbox_Y.max()+boundingbox_Y.min())/2); + Geom::Point end(boundingbox_X.max(), (boundingbox_Y.max()+boundingbox_Y.min())/2); + + if ( Geom::are_near(start,end) ) { + end += Geom::Point(1.,0.); + } + + Geom::Path path; + path.start( start ); + path.appendNew<Geom::LineSegment>( end ); + bend_path.set_new_value( path.toPwSb(), true ); +} + +void +LPEBendPath::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(helper_path); +} + +void +LPEBendPath::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new BeP::KnotHolderEntityWidthBendPath(this); + _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:WidthBend", + _("Change the width")); + knotholder->add(_knot_entity); + if (hide_knot) { + _knot_entity->knot->hide(); + _knot_entity->update_knot(); + } +} + +namespace BeP { + +void +KnotHolderEntityWidthBendPath::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + LPEBendPath *lpe = dynamic_cast<LPEBendPath *> (_effect); + + Geom::Point const s = snap_knot_position(p, state); + Geom::Path path_in = lpe->bend_path.get_pathvector().pathAt(Geom::PathVectorTime(0, 0, 0.0)); + Geom::Point ptA = path_in.pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in.pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in.curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point knot_pos = this->knot->pos * item->i2dt_affine().inverse(); + Geom::Coord nearest_to_ray = ray.nearestTime(knot_pos); + if(nearest_to_ray == 0){ + lpe->prop_scale.param_set_value(-Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } else { + lpe->prop_scale.param_set_value(Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } + if (!lpe->original_height) { + lpe->prop_scale.param_set_value(0); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/live_effects/bend_path/width", lpe->prop_scale); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityWidthBendPath::knot_get() const +{ + LPEBendPath *lpe = dynamic_cast<LPEBendPath *> (_effect); + Geom::Path path_in = lpe->bend_path.get_pathvector().pathAt(Geom::PathVectorTime(0, 0, 0.0)); + Geom::Point ptA = path_in.pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in.pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in.curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point result_point = Geom::Point::polar(ray.angle(), (lpe->original_height/2.0) * lpe->prop_scale) + ptA; + lpe->helper_path.clear(); + if (!lpe->hide_knot) { + Geom::Path hp(result_point); + hp.appendNew<Geom::LineSegment>(ptA); + lpe->helper_path.push_back(hp); + hp.clear(); + } + return result_point; +} +} // namespace BeP +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-bendpath.h b/src/live_effects/lpe-bendpath.h new file mode 100644 index 0000000..ec5196e --- /dev/null +++ b/src/live_effects/lpe-bendpath.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_BENDPATH_H +#define INKSCAPE_LPE_BENDPATH_H + +/* + * Inkscape::LPEPathAlongPath + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Steren Giannini 2008 <steren.giannini@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" + +#include <2geom/sbasis.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/d2.h> +#include <2geom/piecewise.h> + +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { +class PencilToolbar; +} +} // namespace UI +namespace LivePathEffect { + +namespace BeP { +class KnotHolderEntityWidthBendPath; +} + +//for Bend path on group : we need information concerning the group Bounding box +class LPEBendPath : public Effect, GroupBBoxEffect { +public: + LPEBendPath(LivePathEffectObject *lpeobject); + ~LPEBendPath() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + void resetDefaults(SPItem const* item) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) override; + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + + PathParam bend_path; + + friend class BeP::KnotHolderEntityWidthBendPath; + friend class Inkscape::UI::Toolbar::PencilToolbar; + +protected: + double original_height; + ScalarParam prop_scale; +private: + BoolParam scale_y_rel; + BoolParam vertical_pattern; + BoolParam hide_knot; + KnotHolderEntity * _knot_entity; + Geom::PathVector helper_path; + Geom::Piecewise<Geom::D2<Geom::SBasis> > uskeleton; + Geom::Piecewise<Geom::D2<Geom::SBasis> > n; + + void on_pattern_pasted(); + + LPEBendPath(const LPEBendPath&); + LPEBendPath& operator=(const LPEBendPath&); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bool.cpp b/src/live_effects/lpe-bool.cpp new file mode 100644 index 0000000..bd37b7a --- /dev/null +++ b/src/live_effects/lpe-bool.cpp @@ -0,0 +1,960 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Boolean operation live path effect + * + * Copyright (C) 2016-2017 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-bool.h" + +#include <algorithm> +#include <cmath> +#include <cstring> +#include <glibmm/i18n.h> + +#include "2geom/affine.h" +#include "2geom/bezier-curve.h" +#include "2geom/path-sink.h" +#include "2geom/path.h" +#include "2geom/svg-path-parser.h" +#include "display/curve.h" +#include "helper/geom.h" +#include "inkscape.h" +#include "selection-chemistry.h" +#include "livarot/Path.h" +#include "livarot/Shape.h" +#include "livarot/path-description.h" +#include "live_effects/lpeobject.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-root.h" +#include "path/path-boolop.h" +#include "path/path-util.h" +#include "snap.h" +#include "style.h" +#include "svg/svg.h" +#include "ui/tools/tool-base.h" + +namespace Inkscape { +namespace LivePathEffect { + +// Define an extended boolean operation type + +static const Util::EnumData<LPEBool::bool_op_ex> BoolOpData[LPEBool::bool_op_ex_count] = { + {LPEBool::bool_op_ex_union, N_("union"), "union"}, + {LPEBool::bool_op_ex_inters, N_("intersection"), "inters"}, + {LPEBool::bool_op_ex_diff, N_("difference"), "diff"}, + {LPEBool::bool_op_ex_symdiff, N_("symmetric difference"), "symdiff"}, + {LPEBool::bool_op_ex_cut, N_("division"), "cut"}, + {LPEBool::bool_op_ex_cut_both, N_("division both"), "cut-both"}, + // Note on naming of operations: + // bool_op_cut is called "Division" in the manu, see sp_selected_path_cut + // bool_op_slice is called "Cut path" in the menu, see sp_selected_path_slice + // TODO: this 3 options are commented because dont work properly + // maybe in 1.2 can be fixed but need libarot base to do + // {LPEBool::bool_op_ex_slice, N_("cut"), "slice"}, + // {LPEBool::bool_op_ex_slice_inside, N_("cut inside"), "slice-inside"}, + // {LPEBool::bool_op_ex_slice_outside, N_("cut outside"), "slice-outside"}, +}; + +static const Util::EnumDataConverter<LPEBool::bool_op_ex> BoolOpConverter(BoolOpData, sizeof(BoolOpData) / sizeof(*BoolOpData)); + +static const Util::EnumData<fill_typ> FillTypeData[] = { + { fill_oddEven, N_("even-odd"), "oddeven" }, + { fill_nonZero, N_("non-zero"), "nonzero" }, + { fill_positive, N_("positive"), "positive" }, + { fill_justDont, N_("take from object"), "from-curve" } +}; + +static const Util::EnumDataConverter<fill_typ> FillTypeConverter(FillTypeData, sizeof(FillTypeData) / sizeof(*FillTypeData)); + +LPEBool::LPEBool(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , operand_item(_("Operand path:"), _("Operand for the boolean operation"), "operand-path", &wr, this) + , bool_operation(_("Operation:"), _("Boolean Operation"), "operation", BoolOpConverter, &wr, this, bool_op_ex_union) + , swap_operands(_("Swap operands"), _("Swap operands (useful e.g. for difference)"), "swap-operands", &wr, this) + , rmv_inner( + _("Remove inner"), + _("For cut operations: remove inner (non-contour) lines of cutting path to avoid invisible extra points"), + "rmv-inner", &wr, this) + , fill_type_this(_("Fill type this:"), _("Fill type (winding mode) for this path"), "filltype-this", + FillTypeConverter, &wr, this, fill_justDont) + , fill_type_operand(_("Fill type operand:"), _("Fill type (winding mode) for operand path"), "filltype-operand", + FillTypeConverter, &wr, this, fill_justDont) + , filter("Filter", "Previous filter", "filter", &wr, this, "", true) +{ + registerParameter(&operand_item); + registerParameter(&bool_operation); + registerParameter(&swap_operands); + //registerParameter(&rmv_inner); + registerParameter(&fill_type_this); + registerParameter(&filter); + registerParameter(&fill_type_operand); + show_orig_path = true; + satellitestoclipboard = true; + prev_affine = Geom::identity(); + operand = dynamic_cast<SPItem *>(operand_item.getObject()); + if (operand) { + operand_id = operand->getId(); + } +} + +LPEBool::~LPEBool() { + keep_paths = false; + doOnRemove(nullptr); +} + +bool LPEBool::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + legacytest_livarotonly = false; + Glib::ustring version = lpeversion.param_getSVGValue(); + if (version < "1.2") { + if (!SP_ACTIVE_DESKTOP) { + legacytest_livarotonly = true; + } + lpeversion.param_setValue("1.2", true); + } + operand_item.start_listening(operand_item.getObject()); + operand_item.connect_selection_changed(); + return false; +} + +bool cmp_cut_position(const Path::cut_position &a, const Path::cut_position &b) +{ + return a.piece == b.piece ? a.t < b.t : a.piece < b.piece; +} + +Geom::PathVector +sp_pathvector_boolop_slice_intersect(Geom::PathVector const &pathva, Geom::PathVector const &pathvb, bool inside, fill_typ fra, fill_typ frb) +{ + // This is similar to sp_pathvector_boolop/bool_op_slice, but keeps only edges inside the cutter area. + // The code is also based on sp_pathvector_boolop_slice. + // + // We have two paths on input + // - a closed area which is used to cut out pieces from a contour (called area below) + // - a contour which is cut into pieces by the border of thr area (called contour below) + // + // The code below works in the following steps + // (a) Convert the area to a shape, so that we can ask the winding number for any point + // (b) Add both, the contour and the area to a single shape and intersect them + // (c) Find the intersection points between area border and contour (vector toCut) + // (d) Split the original contour at the intersection points + // (e) check for each contour edge in combined shape if its center is inside the area - if not discard it + // (f) create a vector of all inside edges + // (g) convert the piece numbers to the piece numbers after applying the cuts + // (h) fill a bool vector with information which pieces are in + // (i) filter the descr_cmd of the result path with this bool vector + // + // The main inefficiency here is step (e) because I use a winding function of the area-shape which goes + // through the complete edge list for each point I ask for, so effort is n-edges-contour * n-edges-area. + // It is tricky to improve this without building into the livarot code. + // One way might be to decide at the intersection points which edges touching the intersection points are + // in by making a loop through all edges on the intersection vertex. Since this is a directed non intersecting + // graph, this should provide sufficient information. + // But since I anyway will change this to the new mechanism some time speed is fairly ok, I didn't look into this. + + + // extract the livarot Paths from the source objects + // also get the winding rule specified in the style + // Livarot's outline of arcs is broken. So convert the path to linear and cubics only, for which the outline is created correctly. + Path *contour_path = Path_for_pathvector(pathv_to_linear_and_cubic_beziers(pathva)); + Path *area_path = Path_for_pathvector(pathv_to_linear_and_cubic_beziers(pathvb)); + + // Shapes from above paths + Shape *area_shape = new Shape; + Shape *combined_shape = new Shape; + Shape *combined_inters = new Shape; + + // Add the area (process to intersection free shape) + area_path->ConvertWithBackData(1.0); + area_path->Fill(combined_shape, 1); + + // Convert this to a shape with full winding information + area_shape->ConvertToShape(combined_shape, frb); + + // Add the contour to the combined path (just add, no winding processing) + contour_path->ConvertWithBackData(1.0); + contour_path->Fill(combined_shape, 0, true, false, false); + + // Intersect the area and the contour - no fill processing + combined_inters->ConvertToShape(combined_shape, fill_justDont); + + // Result path + Path *result_path = new Path; + result_path->SetBackData(false); + + // Cutting positions for contour + std::vector<Path::cut_position> toCut; + + if (combined_inters->hasBackData()) { + // should always be the case, but ya never know + { + for (int i = 0; i < combined_inters->numberOfPoints(); i++) { + if (combined_inters->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 = combined_inters->getPoint(i).incidentEdge[FIRST]; + int nbOrig = 0; + int nbOther = 0; + int piece = -1; + float t = 0.0; + while (cb >= 0 && cb < combined_inters->numberOfEdges()) { + if (combined_inters->ebData[cb].pathID == 0) { + // the source has an edge incident to the point, get its position on the path + piece = combined_inters->ebData[cb].pieceID; + if (combined_inters->getEdge(cb).st == i) { + t = combined_inters->ebData[cb].tSt; + } else { + t = combined_inters->ebData[cb].tEn; + } + nbOrig++; + } + if (combined_inters->ebData[cb].pathID == 1) { + nbOther++; // the cut is incident to this point + } + cb = combined_inters->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 + Path::cut_position cutpos; + cutpos.piece = piece; + cutpos.t = t; + toCut.push_back(cutpos); + } + } + } + } + { + // remove the edges from the intersection polygon + int i = combined_inters->numberOfEdges() - 1; + for (; i >= 0; i--) { + if (combined_inters->ebData[i].pathID == 1) { + combined_inters->SubEdge(i); + } else { + const Shape::dg_arete &edge = combined_inters->getEdge(i); + const Shape::dg_point &start = combined_inters->getPoint(edge.st); + const Shape::dg_point &end = combined_inters->getPoint(edge.en); + Geom::Point mid = 0.5 * (start.x + end.x); + int wind = area_shape->PtWinding(mid); + if (wind == 0) { + combined_inters->SubEdge(i); + } + } + } + } + } + + // create a vector of pieces, which are in the intersection + std::vector<Path::cut_position> inside_pieces(combined_inters->numberOfEdges()); + for (int i = 0; i < combined_inters->numberOfEdges(); i++) { + inside_pieces[i].piece = combined_inters->ebData[i].pieceID; + // Use the t middle point, this is safe to compare with values from toCut in the presence of roundoff errors + inside_pieces[i].t = 0.5 * (combined_inters->ebData[i].tSt + combined_inters->ebData[i].tEn); + } + std::sort(inside_pieces.begin(), inside_pieces.end(), cmp_cut_position); + + // sort cut positions + std::sort(toCut.begin(), toCut.end(), cmp_cut_position); + + // Compute piece ids after ConvertPositionsToMoveTo + { + int idIncr = 0; + std::vector<Path::cut_position>::iterator itPiece = inside_pieces.begin(); + std::vector<Path::cut_position>::iterator itCut = toCut.begin(); + while (itPiece != inside_pieces.end()) { + while (itCut != toCut.end() && cmp_cut_position(*itCut, *itPiece)) { + ++itCut; + idIncr += 2; + } + itPiece->piece += idIncr; + ++itPiece; + } + } + + // Copy the original path to result and cut at the intersection points + result_path->Copy(contour_path); + result_path->ConvertPositionsToMoveTo(toCut.size(), toCut.data()); // cut where you found intersections + + // Create an array of bools which states which pieces are in + std::vector<bool> inside_flags(result_path->descr_cmd.size(), false); + for (auto & inside_piece : inside_pieces) { + inside_flags[ inside_piece.piece ] = true; + // also enable the element -1 to get the MoveTo + if (inside_piece.piece >= 1) { + inside_flags[ inside_piece.piece - 1 ] = true; + } + } + +#if 0 // CONCEPT TESTING + //Check if the inside/outside verdict is consistent - just for testing the concept + // Retrieve the pieces + int nParts = 0; + Path **parts = result_path->SubPaths(nParts, false); + + // Each piece should be either fully in or fully out + int iPiece = 0; + for (int iPart = 0; iPart < nParts; iPart++) { + bool andsum = true; + bool orsum = false; + for (int iCmd = 0; iCmd < parts[iPart]->descr_cmd.size(); iCmd++, iPiece++) { + andsum = andsum && inside_flags[ iPiece ]; + orsum = andsum || inside_flags[ iPiece ]; + } + + if (andsum != orsum) { + g_warning("Inconsistent inside/outside verdict for part=%d", iPart); + } + } + g_free(parts); +#endif + + // iterate over the commands of a path and keep those which are inside + int iDest = 0; + for (int iSrc = 0; iSrc < result_path->descr_cmd.size(); iSrc++) { + if (inside_flags[iSrc] == inside) { + result_path->descr_cmd[iDest++] = result_path->descr_cmd[iSrc]; + } else { + delete result_path->descr_cmd[iSrc]; + } + } + result_path->descr_cmd.resize(iDest); + + delete combined_inters; + delete combined_shape; + delete area_shape; + delete contour_path; + delete area_path; + + gchar *result_str = result_path->svg_dump_path(); + Geom::PathVector outres = Geom::parse_svg_path(result_str); + // CONCEPT TESTING g_warning( "%s", result_str ); + g_free(result_str); + delete result_path; + + return outres; +} + +// remove inner contours +Geom::PathVector +sp_pathvector_boolop_remove_inner(Geom::PathVector const &pathva, fill_typ fra) +{ + Geom::PathVector patht; + Path *patha = Path_for_pathvector(pathv_to_linear_and_cubic_beziers(pathva)); + + Shape *shape = new Shape; + Shape *shapeshape = new Shape; + Path *resultp = new Path; + resultp->SetBackData(false); + + patha->ConvertWithBackData(0.1); + patha->Fill(shape, 0); + shapeshape->ConvertToShape(shape, fra); + shapeshape->ConvertToForme(resultp, 1, &patha); + + delete shape; + delete shapeshape; + delete patha; + + gchar *result_str = resultp->svg_dump_path(); + Geom::PathVector resultpv = Geom::parse_svg_path(result_str); + g_free(result_str); + + delete resultp; + return resultpv; +} + +static fill_typ GetFillTyp(SPItem *item) +{ + 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) { + return fill_nonZero; + } else if (val && strcmp(val, "evenodd") == 0) { + return fill_oddEven; + } else { + return fill_nonZero; + } +} + +void LPEBool::add_filter() +{ + SPItem *operand = dynamic_cast<SPItem *>(operand_item.getObject()); + if (operand) { + Inkscape::XML::Node *repr = operand->getRepr(); + if (!repr) { + return; + } + SPFilter *filt = operand->style->getFilter(); + if (filt && filt->getId() && strcmp(filt->getId(), "selectable_hidder_filter") != 0) { + filter.param_setValue(filt->getId(), true); + } + if (!filt || (filt->getId() && strcmp(filt->getId(), "selectable_hidder_filter") != 0)) { + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "filter", "url(#selectable_hidder_filter)"); + sp_repr_css_change(repr, css, "style"); + sp_repr_css_attr_unref(css); + } + } +} + +void LPEBool::remove_filter(SPObject *operand) +{ + if (operand) { + Inkscape::XML::Node *repr = operand->getRepr(); + if (!repr) { + return; + } + SPFilter *filt = operand->style->getFilter(); + if (filt && (filt->getId() && strcmp(filt->getId(), "selectable_hidder_filter") == 0)) { + SPCSSAttr *css = sp_repr_css_attr_new(); + Glib::ustring filtstr = filter.param_getSVGValue(); + if (filtstr != "") { + Glib::ustring url = "url(#"; + url += filtstr; + url += ")"; + sp_repr_css_set_property(css, "filter", url.c_str()); + // blur is removed when no item using it + /*SPDocument *document = getSPDoc(); + SPObject * filterobj = nullptr; + if((filterobj = document->getObjectById(filtstr))) { + for (auto obj:filterobj->childList(false)) { + if (obj) { + obj->deleteObject(false); + break; + } + } + } */ + filter.param_setValue(""); + } else { + sp_repr_css_unset_property(css, "filter"); + } + sp_repr_css_change(repr, css, "style"); + sp_repr_css_attr_unref(css); + } + } +} + +void +LPEBool::doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) +{ + if (onremove) { + onremove = false; + } +} + +void LPEBool::doBeforeEffect(SPLPEItem const *lpeitem) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + + _hp.clear(); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPObject *elemref = nullptr; + Inkscape::XML::Node *boolfilter = nullptr; + if (!(elemref = document->getObjectById("selectable_hidder_filter"))) { + boolfilter = xml_doc->createElement("svg:filter"); + boolfilter->setAttribute("id", "selectable_hidder_filter"); + boolfilter->setAttribute("width", "1"); + boolfilter->setAttribute("height", "1"); + boolfilter->setAttribute("x", "0"); + boolfilter->setAttribute("y", "0"); + boolfilter->setAttribute("style", "color-interpolation-filters:sRGB;"); + boolfilter->setAttribute("inkscape:label", "LPE boolean visibility"); + /* Create <path> */ + Inkscape::XML::Node *primitive = xml_doc->createElement("svg:feComposite"); + primitive->setAttribute("id", "boolops_hidder_primitive"); + primitive->setAttribute("result", "composite1"); + primitive->setAttribute("operator", "arithmetic"); + primitive->setAttribute("in2", "SourceGraphic"); + primitive->setAttribute("in", "BackgroundImage"); + Inkscape::XML::Node *defs = document->getDefs()->getRepr(); + defs->addChild(boolfilter, nullptr); + Inkscape::GC::release(boolfilter); + boolfilter->addChild(primitive, nullptr); + Inkscape::GC::release(primitive); + } else { + for (auto obj : elemref->childList(false)) { + if (obj && strcmp(obj->getId(), "boolops_hidder_primitive") != 0) { + obj->deleteObject(true); + } + } + } + bool active = true; + if (operand_item.lperef && operand_item.lperef->isAttached() && operand_item.lperef.get()->getObject() == nullptr) { + active = false; + } + if (!active && !is_load) { + operand_item.unlink(); + return; + } + SPItem *current_operand = dynamic_cast<SPItem *>(operand_item.getObject()); + if (onremove && current_operand) { + operand_id = current_operand->getId(); + return; + } + operand = dynamic_cast<SPItem *>(getSPDoc()->getObjectById(operand_id)); + if (!operand_item.linksToItem()) { + operand_item.read_from_SVG(); + current_operand = dynamic_cast<SPItem *>(operand_item.getObject()); + } + if (!current_operand && !operand) { + return; + } + if (!current_operand) { + operand_item.unlink(); + } + if (current_operand && !operand ) { + operand_id = current_operand->getId(); + operand_item.update_satellites(true); + return; + } + if (current_operand && !operand_item.isConnected()) { + operand_item.start_listening(current_operand); + operand_item.update_satellites(true); + return; + } + + if (current_operand) { + if (!(document->getObjectById(current_operand->getId()))) { + operand_item.unlink(); + operand = nullptr; + operand_id = ""; + current_operand = nullptr; + } else { + operand_id = current_operand->getId(); + } + } + SPLPEItem *operandlpe = dynamic_cast<SPLPEItem *>(operand_item.getObject()); + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Inkscape::Selection *selection = desktop->getSelection(); + if (selection && operand && sp_lpe_item && selection->includes(operand) && selection->includes(sp_lpe_item)) { + if (operandlpe && operandlpe->hasPathEffectOfType(Inkscape::LivePathEffect::EffectType::BOOL_OP)) { + sp_lpe_item_update_patheffect(operandlpe, false, false); + } + } + } + if (!current_operand) { + if (operand) { + remove_filter(operand); + } + operand = nullptr; + operand_id = ""; + } + + if (current_operand && operand != current_operand) { + if (operand) { + remove_filter(operand); + } + operand = current_operand; + remove_filter(operand); + if (is_load && sp_lpe_item) { + sp_lpe_item_update_patheffect(sp_lpe_item, true, true); + } + } + if (current_operand) { + bool_op_ex op = bool_operation.get_value(); + if (is_visible && op != bool_op_ex_cut_both) { + add_filter(); + } else { + remove_filter(current_operand); + } + } +} + +void LPEBool::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + operand = dynamic_cast<SPItem *>(sp_lpe_item->document->getObjectById(operand_id)); + if (is_visible && sp_lpe_item->pathEffectsEnabled() && operand && !isOnClipboard()) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && !desktop->getSelection()->includes(operand)) { + prev_affine = operand->transform * sp_item_transform_repr(sp_lpe_item).inverse() * postmul; + operand->doWriteTransform(prev_affine); + } + } +} + +Geom::PathVector LPEBool::get_union(SPObject *root, SPObject *object, bool _from_original_d) +{ + Geom::PathVector res; + Geom::PathVector clippv; + SPItem *objitem = dynamic_cast<SPItem *>(object); + if (objitem) { + SPObject *clip_path = objitem->getClipObject(); + if (clip_path) { + std::vector<SPObject *> clip_path_list = clip_path->childList(true); + if (clip_path_list.size()) { + for (auto clip : clip_path_list) { + SPShape *clipshape = dynamic_cast<SPShape *>(clip); + if (clipshape) { + std::unique_ptr<SPCurve> curve; + if (_from_original_d) { + curve = SPCurve::copy(clipshape->curveForEdit()); + } else { + curve = SPCurve::copy(clipshape->curve()); + } + if (curve) { + clippv = curve->get_pathvector(); + curve->transform(clipshape->transform); + } + } + } + } + } + } + SPGroup *group = dynamic_cast<SPGroup *>(object); + if (group) { + std::vector<SPItem *> item_list = sp_item_group_item_list(group); + for (auto iter : item_list) { + Geom::PathVector tmp = get_union(root, iter, _from_original_d); + if (res.empty()) { + res = tmp; + } else { + res = sp_pathvector_boolop(res, tmp, to_bool_op(bool_op_ex_union), fill_oddEven, + fill_oddEven, legacytest_livarotonly); + } + } + } + SPShape *shape = dynamic_cast<SPShape *>(object); + if (shape) { + fill_typ originfill = fill_oddEven; + std::unique_ptr<SPCurve> curve; + if (_from_original_d) { + curve = SPCurve::copy(shape->curveForEdit()); + } else { + curve = SPCurve::copy(shape->curve()); + } + if (curve) { + curve->transform(i2anc_affine(shape, root->parent)); + Geom::PathVector tmp = curve->get_pathvector(); + if (res.empty()) { + res = tmp; + } else { + res = sp_pathvector_boolop(res, tmp, to_bool_op(bool_op_ex_union), originfill, + GetFillTyp(shape), legacytest_livarotonly); + } + } + originfill = GetFillTyp(shape); + } + SPText *text = dynamic_cast<SPText *>(object); + if (text) { + std::unique_ptr<SPCurve> curve = text->getNormalizedBpath(); + if (curve) { + curve->transform(i2anc_affine(text, root->parent)); + Geom::PathVector tmp = curve->get_pathvector(); + if (res.empty()) { + res = tmp; + } else { + res = sp_pathvector_boolop(res, tmp, to_bool_op(bool_op_ex_union), fill_oddEven, + fill_oddEven, legacytest_livarotonly); + } + } + } + if (!clippv.empty()) { + res = sp_pathvector_boolop(clippv, res, to_bool_op(bool_op_ex_diff), fill_oddEven, fill_oddEven, legacytest_livarotonly); + } + return res; +} + +void LPEBool::doEffect(SPCurve *curve) +{ + Geom::PathVector path_in = curve->get_pathvector(); + SPItem *current_operand = dynamic_cast<SPItem *>(operand_item.getObject()); + if (current_operand == current_shape) { + g_warning("operand and current shape are the same"); + operand_item.param_set_default(); + return; + } + if (onremove) { + current_operand = dynamic_cast<SPItem *>(getSPDoc()->getObjectById(operand_id)); + } + if (current_operand) { + bool_op_ex op = bool_operation.get_value(); + bool swap = swap_operands.get_value(); + if (op == bool_op_ex_cut_both) { + swap = false; + } + + Geom::Affine current_affine = sp_lpe_item->transform; + Geom::PathVector operand_pv = get_union(current_operand, current_operand); + if (operand_pv.empty()) { + return; + } + path_in *= current_affine; + + Geom::PathVector path_a = swap ? path_in : operand_pv; + Geom::PathVector path_b = swap ? operand_pv : path_in; + _hp = path_a; + _hp.insert(_hp.end(), path_b.begin(), path_b.end()); + _hp *= current_affine.inverse(); + auto item = dynamic_cast<SPItem *>(operand_item.getObject()); + fill_typ fill_this = fill_type_this.get_value() != fill_justDont ? fill_type_this.get_value() : GetFillTyp(current_shape); + fill_typ fill_operand = + fill_type_operand.get_value() != fill_justDont ? fill_type_operand.get_value() : GetFillTyp(item); + + fill_typ fill_a = swap ? fill_this : fill_operand; + fill_typ fill_b = swap ? fill_operand : fill_this; + + if (rmv_inner.get_value()) { + path_b = sp_pathvector_boolop_remove_inner(path_b, fill_b); + } + Geom::PathVector path_out; + helperLineSatellites = false; + if (op == bool_op_ex_cut) { + if (onremove) { + path_out = sp_pathvector_boolop(path_a, path_b, to_bool_op(bool_op_ex_diff), fill_a, fill_b, legacytest_livarotonly); + } else { + int error = 0; + Geom::PathVector path_tmp = sp_pathvector_boolop(path_a, path_b, to_bool_op(op), fill_a, fill_b, legacytest_livarotonly, true, error); + for (auto pathit : path_tmp) { + if (pathit.size() != 2 || !error) { + path_out.push_back(pathit); + } + } + } + /* } else if (op == bool_op_ex_slice) { + path_out = sp_pathvector_boolop_slice_intersect(path_a, path_b, true, fill_a, fill_b); + Geom::PathVector path_tmp = sp_pathvector_boolop_slice_intersect(path_a, path_b, false, fill_a, fill_b); + for (auto pathit : path_tmp) { + path_out.push_back(pathit); + } + } else if (op == bool_op_ex_slice_inside) { + path_out = sp_pathvector_boolop_slice_intersect(path_a, path_b, true, fill_a, fill_b); + } else if (op == bool_op_ex_slice_outside) { + path_out = sp_pathvector_boolop_slice_intersect(path_a, path_b, false, fill_a, fill_b); + */ + } else if (op == bool_op_ex_cut_both){ + if (onremove) { + path_out = sp_pathvector_boolop(path_a, path_b, to_bool_op(bool_op_ex_diff), fill_a, fill_b, legacytest_livarotonly); + } else { + helperLineSatellites = true; + path_out = sp_pathvector_boolop(path_a, path_b, (bool_op) bool_op_diff, fill_a, fill_b, legacytest_livarotonly); + auto tmp = sp_pathvector_boolop(path_a, path_b, (bool_op) bool_op_inters, fill_a, fill_b, legacytest_livarotonly); + path_out.insert(path_out.end(),tmp.begin(),tmp.end()); + /* auto tmp2 = sp_pathvector_boolop(path_a, path_b, (bool_op) bool_op_diff, fill_a, fill_b); + path_out.insert(path_out.end(),tmp2.begin(),tmp2.end()); */ + } + } else { + path_out = sp_pathvector_boolop(path_a, path_b, (bool_op) op, fill_a, fill_b, legacytest_livarotonly); + } + curve->set_pathvector(path_out * current_affine.inverse()); + } +} + +void LPEBool::addCanvasIndicators(SPLPEItem const * /*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(_hp); +} + +Inkscape::XML::Node * +LPEBool::dupleNode(SPObject * origin, Glib::ustring element_type) +{ + Inkscape::XML::Document *xml_doc = getSPDoc()->getReprDoc(); + Inkscape::XML::Node *dest = xml_doc->createElement(element_type.c_str()); + dest->setAttribute("transform", origin->getAttribute("transform")); + dest->setAttribute("d", origin->getAttribute("d")); + dest->setAttribute("style", origin->getAttribute("style")); + dest->setAttribute("mask", origin->getAttribute("mask")); + dest->setAttribute("clip-path", origin->getAttribute("clip-path")); + dest->setAttribute("class", origin->getAttribute("class")); + dest->setAttribute("style", origin->getAttribute("style")); + for (auto iter : origin->style->properties()) { + if (iter->style_src != SPStyleSrc::UNSET) { + auto key = iter->id(); + if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) { + const gchar *attr = origin->getAttribute(iter->name().c_str()); + if (attr) { + dest->setAttribute(iter->name(), attr); + } + } + } + } + return dest; +} + +void LPEBool::fractureit(SPObject * operandit, Geom::PathVector unionpv) +{ + // 1.2 feature no need to legacy bool + SPItem *operandit_item = dynamic_cast<SPItem *>(operandit); + SPGroup *operandit_g = dynamic_cast<SPGroup *>(operandit); + SPShape *operandit_shape = dynamic_cast<SPShape *>(operandit); + fill_typ fill_a = fill_type_this.get_value() != fill_justDont ? fill_type_this.get_value() : GetFillTyp(operandit_item); + fill_typ fill_b = fill_type_operand.get_value() != fill_justDont ? fill_type_operand.get_value() : GetFillTyp(operandit_item); + //unionpv *= sp_lpe_item->transform; + auto divisionit = dynamic_cast<SPItem *>(getSPDoc()->getObjectById(division_id)); + if (operandit_g) { + Inkscape::XML::Node *dest = dupleNode(operandit, "svg:g"); + dest->setAttribute("transform", nullptr); + if (!division_other) { + division_other = dynamic_cast<SPGroup *>(sp_lpe_item->parent->appendChildRepr(dest)); + Inkscape::GC::release(dest); + division_other_id = division_other->getId(); + division_other->parent->reorder(division_other, divisionit); + } else { + division_other = dynamic_cast<SPGroup *>(division_other->appendChildRepr(dest)); + } + Inkscape::XML::Node *dest2 = dupleNode(operandit, "svg:g"); + dest2->setAttribute("transform", nullptr); + if (!division_both) { + division_both = dynamic_cast<SPGroup *>(sp_lpe_item->parent->appendChildRepr(dest2)); + Inkscape::GC::release(dest2); + division_both->parent->reorder(division_both, division_other); + } else { + division_both = dynamic_cast<SPGroup *>(division_both->appendChildRepr(dest2)); + } + + for (auto& child: operandit_g->children) { + SPItem *item = dynamic_cast<SPItem *>(&child); + if (item) { + fractureit(item, unionpv); + } + } + } + if (operandit_shape) { + std::unique_ptr<SPCurve> curve = SPCurve::copy(operandit_shape->curve()); + if (curve) { + curve->transform(i2anc_affine(operandit_shape, sp_lpe_item->parent)); + auto intesect = sp_pathvector_boolop(unionpv, curve->get_pathvector(), (bool_op) bool_op_inters, fill_a, fill_b); + Inkscape::XML::Node *dest = dupleNode(operandit_shape, "svg:path"); + dest->setAttribute("d", sp_svg_write_path(intesect)); + dest->setAttribute("transform", nullptr); + if (!division_other) { + division_other = dynamic_cast<SPGroup *>(sp_lpe_item->parent); + } + SPItem *divisionitem = dynamic_cast<SPItem *>(division_other->appendChildRepr(dest)); + Inkscape::GC::release(dest); + if (division_other_id.empty()) { + division_other->reorder(divisionitem, divisionit); + division_other_id = Glib::ustring(dest->attribute("id")); + } + auto operandit_pathv = sp_pathvector_boolop(unionpv, curve->get_pathvector(), (bool_op) bool_op_diff, fill_a, fill_b); + Inkscape::XML::Node *dest2 = dupleNode(operandit_shape, "svg:path"); + dest2->setAttribute("transform", nullptr); + dest2->setAttribute("d", sp_svg_write_path(operandit_pathv)); + if (!division_both) { + division_both = dynamic_cast<SPGroup *>(sp_lpe_item->parent); + SPItem *divisionitem2 = dynamic_cast<SPItem *>(division_both->appendChildRepr(dest2)); + division_both->reorder(divisionitem2, divisionitem); + } else { + division_both->appendChildRepr(dest2); + } + Inkscape::GC::release(dest2); + } + } +} + +void LPEBool::divisionit(SPObject * operand_a, SPObject * operand_b, Geom::PathVector unionpv) +{ + SPItem *operand_a_item = dynamic_cast<SPItem *>(operand_a); + SPItem *operand_b_item = dynamic_cast<SPItem *>(operand_b); + SPGroup *operand_b_g = dynamic_cast<SPGroup *>(operand_b); + SPShape *operand_b_shape = dynamic_cast<SPShape *>(operand_b); + fill_typ fill_a = fill_type_this.get_value() != fill_justDont ? fill_type_this.get_value() : GetFillTyp(operand_a_item); + fill_typ fill_b = fill_type_operand.get_value() != fill_justDont ? fill_type_operand.get_value() : GetFillTyp(operand_b_item); + if (operand_b_g) { + Inkscape::XML::Node *dest = dupleNode(operand_b, "svg:g"); + dest->setAttribute("transform", nullptr); + if (!division) { + division = dynamic_cast<SPGroup *>(sp_lpe_item->parent->appendChildRepr(dest)); + Inkscape::GC::release(dest); + division_id = division->getId(); + division->parent->reorder(division, sp_lpe_item); + } else { + division = dynamic_cast<SPGroup *>(division->appendChildRepr(dest)); + } + for (auto& child: operand_b_g->children) { + SPItem *item = dynamic_cast<SPItem *>(&child); + if (item) { + divisionit(operand_a, item, unionpv); + } + } + } + if (operand_b_shape) { + if (!division) { + division = dynamic_cast<SPGroup *>(sp_lpe_item->parent); + } + std::unique_ptr<SPCurve> curve = SPCurve::copy(operand_b_shape->curveForEdit()); + if (curve) { + curve->transform(i2anc_affine(operand_b_shape, sp_lpe_item->parent)); + auto intesect = sp_pathvector_boolop(unionpv, curve->get_pathvector(), (bool_op) bool_op_inters, fill_a, fill_b); + Inkscape::XML::Node *dest = dupleNode(operand_b_shape, "svg:path"); + dest->setAttribute("d", sp_svg_write_path(intesect)); + dest->setAttribute("transform", nullptr); + SPItem *item = dynamic_cast<SPItem *>(division->appendChildRepr(dest)); + Inkscape::GC::release(dest); + if (item && division_id.empty()) { + division_id = item->getId(); + } + } + } +} + +void LPEBool::doOnRemove(SPLPEItem const * lpeitem) +{ + // set "keep paths" hook on sp-lpe-item.cpp + remove_filter(operand_item.getObject()); + SPItem *operand = dynamic_cast<SPItem *>(getSPDoc()->getObjectById(operand_id)); + if (operand) { + if (keep_paths) { + bool_op_ex op = bool_operation.get_value(); + if (op == bool_op_ex_cut || op == bool_op_ex_cut_both) { + reverse = lpeitem->pos_in_parent() > operand->pos_in_parent(); + division = nullptr; + Geom::PathVector unionpv = get_union(operand, operand); + divisionit(operand, sp_lpe_item, unionpv); + onremove = true; + sp_lpe_item_update_patheffect(sp_lpe_item, false, true); + if (op == bool_op_ex_cut_both) { + auto * a = dynamic_cast<SPItem *>(getSPDoc()->getObjectById(division_id)); + if (a) { + unionpv = get_union(sp_lpe_item, sp_lpe_item, true); + fractureit(operand, unionpv); + auto * b = dynamic_cast<SPItem *>(getSPDoc()->getObjectById(division_other_id)); + if (a && b) { + if (reverse) { + b->lowerOne(); + } + } + } + } + // we reset variables because LPE is not removed on undo so I need to get clean on redo + division = nullptr; + division_both = nullptr; + division_other = nullptr; + operand_id = ""; + division_id = ""; + division_other_id = ""; + onremove = false; + } + if (is_visible) { + processObjects(LPE_ERASE); + } + } + } +} + +// TODO: Migrate the tree next function to effect.cpp/h to avoid duplication +void LPEBool::doOnVisibilityToggled(SPLPEItem const * /*lpeitem*/) +{ + SPItem *operand = dynamic_cast<SPItem *>(operand_item.getObject()); + if (operand) { + if (!is_visible) { + remove_filter(operand); + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ diff --git a/src/live_effects/lpe-bool.h b/src/live_effects/lpe-bool.h new file mode 100644 index 0000000..9cd2ef2 --- /dev/null +++ b/src/live_effects/lpe-bool.h @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Boolean operation live path effect + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_BOOL_H +#define INKSCAPE_LPE_BOOL_H + +#include "livarot/LivarotDefs.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/originalsatellite.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEBool : public Effect { +public: + LPEBool(LivePathEffectObject *lpeobject); + ~LPEBool() override; + + void doEffect(SPCurve *curve) override; + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + void doBeforeEffect(SPLPEItem const *lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + void doAfterEffect(SPLPEItem const* lpeitem, SPCurve *curve) override; + void doOnVisibilityToggled(SPLPEItem const * /*lpeitem*/) override; + void doOnRemove(SPLPEItem const * /*lpeitem*/) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void add_filter(); + void fractureit(SPObject * operandit, Geom::PathVector unionpv); + void divisionit(SPObject * operand_a, SPObject * operand_b, Geom::PathVector unionpv); + Geom::PathVector get_union(SPObject *root, SPObject *object, bool prefear_original = false); + Inkscape::XML::Node *dupleNode(SPObject * origin, Glib::ustring element_type); + void remove_filter(SPObject *object); + enum bool_op_ex + { + bool_op_ex_union = bool_op_union, + bool_op_ex_inters = bool_op_inters, + bool_op_ex_diff = bool_op_diff, + bool_op_ex_symdiff = bool_op_symdiff, + bool_op_ex_cut = bool_op_cut, + bool_op_ex_cut_both, + // bool_op_ex_slice = bool_op_slice, + // bool_op_ex_slice_inside, // like bool_op_slice, but leaves only the contour pieces inside of the cut path + // bool_op_ex_slice_outside, // like bool_op_slice, but leaves only the contour pieces outside of the cut path + bool_op_ex_count + }; + + inline friend bool_op to_bool_op(bool_op_ex val) + { + //assert(val <= bool_op_ex_slice); + assert(val <= bool_op_ex_cut); + return (bool_op) val; + } + +private: + LPEBool(const LPEBool &) = delete; + LPEBool &operator=(const LPEBool &) = delete; + + OriginalSatelliteParam operand_item; + EnumParam<bool_op_ex> bool_operation; + EnumParam<fill_typ> fill_type_this; + EnumParam<fill_typ> fill_type_operand; + BoolParam swap_operands; + BoolParam rmv_inner; + bool legacytest_livarotonly = false; + bool onremove = false; + SPItem *operand = nullptr; + SPObject *parentlpe = nullptr; + SPGroup *division = nullptr; + SPGroup *division_both = nullptr; + SPGroup *division_other = nullptr; + Glib::ustring operand_id = ""; + Glib::ustring division_id = ""; + Glib::ustring division_other_id = ""; + HiddenParam filter; + Geom::PathVector _hp; + Geom::Affine prev_affine; + bool reverse = false; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bounding-box.cpp b/src/live_effects/lpe-bounding-box.cpp new file mode 100644 index 0000000..f826ff1 --- /dev/null +++ b/src/live_effects/lpe-bounding-box.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-bounding-box.h" + +#include "display/curve.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + + +namespace Inkscape { +namespace LivePathEffect { + +LPEBoundingBox::LPEBoundingBox(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + linked_path(_("Linked path:"), _("Path from which to take the original path data"), "linkedpath", &wr, this), + visual_bounds(_("Visual Bounds"), _("Uses the visual bounding box"), "visualbounds", &wr, this) +{ + registerParameter(&linked_path); + registerParameter(&visual_bounds); + //perceived_path = true; + linked_path.setUpdating(true); +} + +LPEBoundingBox::~LPEBoundingBox() += default; + +bool +LPEBoundingBox::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + linked_path.setUpdating(false); + linked_path.start_listening(linked_path.getObject()); + linked_path.connect_selection_changed(); + return false; +} + +void +LPEBoundingBox::doBeforeEffect (SPLPEItem const* lpeitem) +{ + if (is_load) { + linked_path.setUpdating(false); + linked_path.start_listening(linked_path.getObject()); + linked_path.connect_selection_changed(); + SPItem * item = nullptr; + if (( item = dynamic_cast<SPItem *>(linked_path.getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } +} + + +void LPEBoundingBox::doEffect (SPCurve * curve) +{ + if (curve) { + if ( linked_path.linksToPath() && linked_path.getObject() ) { + SPItem * item = linked_path.getObject(); + Geom::OptRect bbox = visual_bounds.get_value() ? item->visualBounds() : item->geometricBounds(); + Geom::Path p; + Geom::PathVector out; + if (bbox) { + p = Geom::Path(*bbox); + out.push_back(p); + } + + curve->set_pathvector(out); + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-bounding-box.h b/src/live_effects/lpe-bounding-box.h new file mode 100644 index 0000000..86d81fe --- /dev/null +++ b/src/live_effects/lpe-bounding-box.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_BOUNDING_BOX_H +#define INKSCAPE_LPE_BOUNDING_BOX_H + +/* + * Inkscape::LPEFillBetweenStrokes + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/originalpath.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEBoundingBox : public Effect { +public: + LPEBoundingBox(LivePathEffectObject *lpeobject); + ~LPEBoundingBox() override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void doEffect (SPCurve * curve) override; + +private: + OriginalPathParam linked_path; + BoolParam visual_bounds; + +private: + LPEBoundingBox(const LPEBoundingBox&) = delete; + LPEBoundingBox& operator=(const LPEBoundingBox&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-bspline.cpp b/src/live_effects/lpe-bspline.cpp new file mode 100644 index 0000000..d470e70 --- /dev/null +++ b/src/live_effects/lpe-bspline.cpp @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <gtkmm.h> + +#include "document-undo.h" +#include "preferences.h" + +#include "display/curve.h" +#include "helper/geom-curves.h" +#include "live_effects/lpe-bspline.h" +#include "object/sp-path.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "ui/widget/scalar.h" +#include "xml/repr.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +const double HANDLE_CUBIC_GAP = 0.001; +const double NO_POWER = 0.0; +const double DEFAULT_START_POWER = 1.0/3.0; +const double DEFAULT_END_POWER = 2.0/3.0; +Geom::Path sp_bspline_drawHandle(Geom::Point p, double helper_size); + +LPEBSpline::LPEBSpline(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + steps(_("Steps with CTRL:"), _("Change number of steps with CTRL pressed"), "steps", &wr, this, 2), + helper_size(_("Helper size:"), _("Helper size"), "helper_size", &wr, this, 0), + apply_no_weight(_("Apply changes if weight = 0%"), _("Apply changes if weight = 0%"), "apply_no_weight", &wr, this, true), + apply_with_weight(_("Apply changes if weight > 0%"), _("Apply changes if weight > 0%"), "apply_with_weight", &wr, this, true), + only_selected(_("Change only selected nodes"), _("Change only selected nodes"), "only_selected", &wr, this, false), + weight(_("Change weight %:"), _("Change weight percent of the effect"), "weight", &wr, this, DEFAULT_START_POWER * 100) +{ + registerParameter(&weight); + registerParameter(&steps); + registerParameter(&helper_size); + registerParameter(&apply_no_weight); + registerParameter(&apply_with_weight); + registerParameter(&only_selected); + + weight.param_set_range(NO_POWER, 100.0); + weight.param_set_increments(0.1, 0.1); + weight.param_set_digits(4); + weight.param_set_undo(false); + + steps.param_set_range(1, 10); + steps.param_set_increments(1, 1); + steps.param_set_digits(0); + steps.param_set_undo(false); + + helper_size.param_set_range(0.0, 999.0); + helper_size.param_set_increments(1, 1); + helper_size.param_set_digits(2); +} + +LPEBSpline::~LPEBSpline() = default; + +void LPEBSpline::doBeforeEffect (SPLPEItem const* /*lpeitem*/) +{ + if(!hp.empty()) { + hp.clear(); + } +} + + +void LPEBSpline::doOnApply(SPLPEItem const* lpeitem) +{ + if (!SP_IS_SHAPE(lpeitem)) { + g_warning("LPE BSpline can only be applied to shapes (not groups)."); + SPLPEItem * item = const_cast<SPLPEItem*>(lpeitem); + item->removeCurrentPathEffect(false); + } +} + +void +LPEBSpline::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(hp); +} + +Gtk::Widget *LPEBSpline::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox->set_homogeneous(false); + vbox->set_border_width(5); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + if (param->param_key == "weight") { + Gtk::Box * buttons = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Button *default_weight = + Gtk::manage(new Gtk::Button(Glib::ustring(_("Default weight")))); + default_weight->signal_clicked() + .connect(sigc::mem_fun(*this, &LPEBSpline::toDefaultWeight)); + buttons->pack_start(*default_weight, true, true, 2); + Gtk::Button *make_cusp = + Gtk::manage(new Gtk::Button(Glib::ustring(_("Make cusp")))); + make_cusp->signal_clicked() + .connect(sigc::mem_fun(*this, &LPEBSpline::toMakeCusp)); + buttons->pack_start(*make_cusp, true, true, 2); + vbox->pack_start(*buttons, true, true, 2); + } + if (param->param_key == "weight" || param->param_key == "steps") { + Inkscape::UI::Widget::Scalar *widg_registered = + Gtk::manage(dynamic_cast<Inkscape::UI::Widget::Scalar *>(widg)); + widg_registered->signal_value_changed() + .connect(sigc::mem_fun(*this, &LPEBSpline::toWeight)); + widg = dynamic_cast<Gtk::Widget *>(widg_registered); + if (widg) { + Gtk::Box * hbox_weight_steps = dynamic_cast<Gtk::Box *>(widg); + std::vector< Gtk::Widget* > childList = hbox_weight_steps->get_children(); + Gtk::Entry* entry_widget = dynamic_cast<Gtk::Entry *>(childList[1]); + entry_widget->set_width_chars(9); + } + } + if (param->param_key == "only_selected" || param->param_key == "apply_no_weight" || param->param_key == "apply_with_weight") { + Gtk::CheckButton *widg_registered = + Gtk::manage(dynamic_cast<Gtk::CheckButton *>(widg)); + widg = dynamic_cast<Gtk::Widget *>(widg_registered); + } + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void LPEBSpline::toDefaultWeight() +{ + changeWeight(DEFAULT_START_POWER * 100); + DocumentUndo::done(getSPDoc(), _("Change to default weight"), INKSCAPE_ICON("dialog-path-effects")); +} + +void LPEBSpline::toMakeCusp() +{ + changeWeight(NO_POWER); + DocumentUndo::done(getSPDoc(), _("Change to 0 weight"), INKSCAPE_ICON("dialog-path-effects")); +} + +void LPEBSpline::toWeight() +{ + changeWeight(weight); + DocumentUndo::done(getSPDoc(), _("Change scalar parameter"), INKSCAPE_ICON("dialog-path-effects")); +} + +void LPEBSpline::changeWeight(double weight_ammount) +{ + SPPath *path = dynamic_cast<SPPath *>(sp_lpe_item); + if(path) { + auto curve = SPCurve::copy(path->curveForEdit()); + doBSplineFromWidget(curve.get(), weight_ammount / 100.0); + path->setAttribute("inkscape:original-d", sp_svg_write_path(curve->get_pathvector())); + } +} + +void LPEBSpline::doEffect(SPCurve *curve) +{ + sp_bspline_do_effect(curve, helper_size, hp); +} + +void sp_bspline_do_effect(SPCurve *curve, double helper_size, Geom::PathVector &hp) +{ + if (curve->get_segment_count() < 1) { + return; + } + Geom::PathVector const original_pathv = curve->get_pathvector(); + curve->reset(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + for (const auto & path_it : original_pathv) { + if (path_it.empty()) { + continue; + } + if (!prefs->getBool("/tools/nodes/show_outline", true)){ + hp.push_back(path_it); + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + auto curve_n = std::make_unique<SPCurve>(); + Geom::Point previousNode(0, 0); + Geom::Point node(0, 0); + Geom::Point point_at1(0, 0); + Geom::Point point_at2(0, 0); + Geom::Point next_point_at1(0, 0); + Geom::D2<Geom::SBasis> sbasis_in; + Geom::D2<Geom::SBasis> sbasis_out; + Geom::D2<Geom::SBasis> sbasis_helper; + Geom::CubicBezier const *cubic = nullptr; + curve_n->moveto(curve_it1->initialPoint()); + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + auto in = std::make_unique<SPCurve>(); + in->moveto(curve_it1->initialPoint()); + in->lineto(curve_it1->finalPoint()); + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + if (cubic) { + sbasis_in = in->first_segment()->toSBasis(); + if(are_near((*cubic)[1],(*cubic)[0]) && !are_near((*cubic)[2],(*cubic)[3])) { + point_at1 = sbasis_in.valueAt(DEFAULT_START_POWER); + } else { + point_at1 = sbasis_in.valueAt(Geom::nearest_time((*cubic)[1], *in->first_segment())); + } + if(are_near((*cubic)[2],(*cubic)[3]) && !are_near((*cubic)[1],(*cubic)[0])) { + point_at2 = sbasis_in.valueAt(DEFAULT_END_POWER); + } else { + point_at2 = sbasis_in.valueAt(Geom::nearest_time((*cubic)[2], *in->first_segment())); + } + } else { + point_at1 = in->first_segment()->initialPoint(); + point_at2 = in->first_segment()->finalPoint(); + } + if ( curve_it2 != curve_endit ) { + auto out = std::make_unique<SPCurve>(); + out->moveto(curve_it2->initialPoint()); + out->lineto(curve_it2->finalPoint()); + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it2); + if (cubic) { + sbasis_out = out->first_segment()->toSBasis(); + if(are_near((*cubic)[1],(*cubic)[0]) && !are_near((*cubic)[2],(*cubic)[3])) { + next_point_at1 = sbasis_in.valueAt(DEFAULT_START_POWER); + } else { + next_point_at1 = sbasis_out.valueAt(Geom::nearest_time((*cubic)[1], *out->first_segment())); + } + } else { + next_point_at1 = out->first_segment()->initialPoint(); + } + } + if (path_it.closed() && curve_it2 == curve_endit) { + auto start = std::make_unique<SPCurve>(); + start->moveto(path_it.begin()->initialPoint()); + start->lineto(path_it.begin()->finalPoint()); + Geom::D2<Geom::SBasis> sbasis_start = start->first_segment()->toSBasis(); + auto line_helper = std::make_unique<SPCurve>(); + cubic = dynamic_cast<Geom::CubicBezier const *>(&*path_it.begin()); + if (cubic) { + line_helper->moveto(sbasis_start.valueAt( + Geom::nearest_time((*cubic)[1], *start->first_segment()))); + } else { + line_helper->moveto(start->first_segment()->initialPoint()); + } + + auto end = std::make_unique<SPCurve>(); + end->moveto(curve_it1->initialPoint()); + end->lineto(curve_it1->finalPoint()); + Geom::D2<Geom::SBasis> sbasis_end = end->first_segment()->toSBasis(); + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + if (cubic) { + line_helper->lineto(sbasis_end.valueAt( + Geom::nearest_time((*cubic)[2], *end->first_segment()))); + } else { + line_helper->lineto(end->first_segment()->finalPoint()); + } + sbasis_helper = line_helper->first_segment()->toSBasis(); + node = sbasis_helper.valueAt(0.5); + curve_n->curveto(point_at1, point_at2, node); + curve_n->move_endpoints(node, node); + } else if ( curve_it2 == curve_endit) { + curve_n->curveto(point_at1, point_at2, curve_it1->finalPoint()); + curve_n->move_endpoints(path_it.begin()->initialPoint(), curve_it1->finalPoint()); + } else { + auto line_helper = std::make_unique<SPCurve>(); + line_helper->moveto(point_at2); + line_helper->lineto(next_point_at1); + sbasis_helper = line_helper->first_segment()->toSBasis(); + previousNode = node; + node = sbasis_helper.valueAt(0.5); + Geom::CubicBezier const *cubic2 = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + if((cubic && are_near((*cubic)[0],(*cubic)[1])) || (cubic2 && are_near((*cubic2)[2],(*cubic2)[3]))) { + node = curve_it1->finalPoint(); + } + curve_n->curveto(point_at1, point_at2, node); + } + if(!are_near(node,curve_it1->finalPoint()) && helper_size > 0.0) { + hp.push_back(sp_bspline_drawHandle(node, helper_size)); + } + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + curve_n->closepath_current(); + } + curve->append(*curve_n, false); + } + if(helper_size > 0.0) { + Geom::PathVector const pathv = curve->get_pathvector(); + hp.push_back(pathv[0]); + } +} + +Geom::Path sp_bspline_drawHandle(Geom::Point p, double helper_size) +{ + char const * svgd = "M 1,0.5 A 0.5,0.5 0 0 1 0.5,1 0.5,0.5 0 0 1 0,0.5 0.5,0.5 0 0 1 0.5,0 0.5,0.5 0 0 1 1,0.5 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + Geom::Affine aff = Geom::Affine(); + aff *= Geom::Scale(helper_size); + pathv *= aff; + pathv *= Geom::Translate(p - Geom::Point(0.5*helper_size, 0.5*helper_size)); + return pathv[0]; +} + +void LPEBSpline::doBSplineFromWidget(SPCurve *curve, double weight_ammount) +{ + using Geom::X; + using Geom::Y; + + if (curve->get_segment_count() < 1) + return; + // Make copy of old path as it is changed during processing + Geom::PathVector const original_pathv = curve->get_pathvector(); + curve->reset(); + + for (const auto & path_it : original_pathv) { + + if (path_it.empty()) { + continue; + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + + auto curve_n = std::make_unique<SPCurve>(); + Geom::Point point_at0(0, 0); + Geom::Point point_at1(0, 0); + Geom::Point point_at2(0, 0); + Geom::Point point_at3(0, 0); + Geom::D2<Geom::SBasis> sbasis_in; + Geom::D2<Geom::SBasis> sbasis_out; + Geom::CubicBezier const *cubic = nullptr; + curve_n->moveto(curve_it1->initialPoint()); + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + auto in = std::make_unique<SPCurve>(); + in->moveto(curve_it1->initialPoint()); + in->lineto(curve_it1->finalPoint()); + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + point_at0 = in->first_segment()->initialPoint(); + point_at3 = in->first_segment()->finalPoint(); + sbasis_in = in->first_segment()->toSBasis(); + if (cubic) { + if ((apply_no_weight && apply_with_weight) || + (apply_no_weight && Geom::are_near((*cubic)[1], point_at0)) || + (apply_with_weight && !Geom::are_near((*cubic)[1], point_at0))) + { + if (isNodePointSelected(point_at0) || !only_selected) { + point_at1 = sbasis_in.valueAt(weight_ammount); + if (weight_ammount != NO_POWER) { + point_at1 = + Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + } + } else { + point_at1 = (*cubic)[1]; + } + } else { + point_at1 = (*cubic)[1]; + } + if ((apply_no_weight && apply_with_weight) || + (apply_no_weight && Geom::are_near((*cubic)[2], point_at3)) || + (apply_with_weight && !Geom::are_near((*cubic)[2], point_at3))) + { + if (isNodePointSelected(point_at3) || !only_selected) { + point_at2 = sbasis_in.valueAt(1 - weight_ammount); + if (weight_ammount != NO_POWER) { + point_at2 = + Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + } + } else { + point_at2 = (*cubic)[2]; + } + } else { + point_at2 = (*cubic)[2]; + } + } else { + if ((apply_no_weight && apply_with_weight) || + (apply_no_weight && weight_ammount == NO_POWER) || + (apply_with_weight && weight_ammount != NO_POWER)) + { + if (isNodePointSelected(point_at0) || !only_selected) { + point_at1 = sbasis_in.valueAt(weight_ammount); + point_at1 = + Geom::Point(point_at1[X] + HANDLE_CUBIC_GAP, point_at1[Y] + HANDLE_CUBIC_GAP); + } else { + point_at1 = in->first_segment()->initialPoint(); + } + if (isNodePointSelected(point_at3) || !only_selected) { + point_at2 = sbasis_in.valueAt(1 - weight_ammount); + point_at2 = + Geom::Point(point_at2[X] + HANDLE_CUBIC_GAP, point_at2[Y] + HANDLE_CUBIC_GAP); + } else { + point_at2 = in->first_segment()->finalPoint(); + } + } else { + point_at1 = in->first_segment()->initialPoint(); + point_at2 = in->first_segment()->finalPoint(); + } + } + curve_n->curveto(point_at1, point_at2, point_at3); + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + curve_n->move_endpoints(path_it.begin()->initialPoint(), + path_it.begin()->initialPoint()); + } else { + curve_n->move_endpoints(path_it.begin()->initialPoint(), point_at3); + } + if (path_it.closed()) { + curve_n->closepath_current(); + } + curve->append(*curve_n, false); + } +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-bspline.h b/src/live_effects/lpe-bspline.h new file mode 100644 index 0000000..0c3a065 --- /dev/null +++ b/src/live_effects/lpe-bspline.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_BSPLINE_H +#define INKSCAPE_LPE_BSPLINE_H + +/* + * Inkscape::LPEBSpline + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include <vector> + +namespace Inkscape { +namespace LivePathEffect { + +class LPEBSpline : public Effect { +public: + LPEBSpline(LivePathEffectObject *lpeobject); + ~LPEBSpline() override; + LPEBSpline(const LPEBSpline &) = delete; + LPEBSpline &operator=(const LPEBSpline &) = delete; + + LPEPathFlashType pathFlashType() const override + { + return SUPPRESS_FLASH; + } + void doOnApply(SPLPEItem const* lpeitem) override; + void doEffect(SPCurve *curve) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doBSplineFromWidget(SPCurve *curve, double value); + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) override; + Gtk::Widget *newWidget() override; + void changeWeight(double weightValue); + void toDefaultWeight(); + void toMakeCusp(); + void toWeight(); + + // TODO make this private + ScalarParam steps; + +private: + ScalarParam helper_size; + BoolParam apply_no_weight; + BoolParam apply_with_weight; + BoolParam only_selected; + ScalarParam weight; + Geom::PathVector hp; +}; +void sp_bspline_do_effect(SPCurve *curve, double helper_size, Geom::PathVector &hp); + +} //namespace LivePathEffect +} //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-circle_3pts.cpp b/src/live_effects/lpe-circle_3pts.cpp new file mode 100644 index 0000000..7982abd --- /dev/null +++ b/src/live_effects/lpe-circle_3pts.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Circle through 3 points" implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-circle_3pts.h" + +// You might need to include other 2geom files. You can add them here: +#include <2geom/circle.h> +#include <2geom/path-sink.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPECircle3Pts::LPECircle3Pts(LivePathEffectObject *lpeobject) : + Effect(lpeobject) +{ +} + +LPECircle3Pts::~LPECircle3Pts() += default; + +static void _circle3(Geom::Point const &A, Geom::Point const &B, Geom::Point const &C, Geom::PathVector &path_out) { + using namespace Geom; + + Point D = (A + B)/2; + Point E = (B + C)/2; + + Point v = (B - A).ccw(); + Point w = (C - B).ccw(); + + double det = -v[0] * w[1] + v[1] * w[0]; + + Point M; + if (!v.isZero()) { + Point F = E - D; + double lambda = det == 0 ? 0 : (-w[1] * F[0] + w[0] * F[1]) / det; + M = D + v * lambda; + } else { + M = E; + } + + double radius = L2(M - A); + + Geom::Circle c(M, radius); + path_out = Geom::Path(c); +} + +Geom::PathVector +LPECircle3Pts::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out = Geom::PathVector(); + + // we assume that the path has >= 3 nodes + Geom::Point A = path_in[0].initialPoint(); + Geom::Point B = path_in[0].pointAt(1); + Geom::Point C = path_in[0].pointAt(2); + + _circle3(A, B, C, path_out); + + return path_out; +} + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-circle_3pts.h b/src/live_effects/lpe-circle_3pts.h new file mode 100644 index 0000000..bf89b9f --- /dev/null +++ b/src/live_effects/lpe-circle_3pts.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CIRCLE_3PTS_H +#define INKSCAPE_LPE_CIRCLE_3PTS_H + +/** \file + * LPE "Circle through 3 points" implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPECircle3Pts : public Effect { +public: + LPECircle3Pts(LivePathEffectObject *lpeobject); + ~LPECircle3Pts() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + LPECircle3Pts(const LPECircle3Pts&) = delete; + LPECircle3Pts& operator=(const LPECircle3Pts&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-circle_with_radius.cpp b/src/live_effects/lpe-circle_with_radius.cpp new file mode 100644 index 0000000..1a6b3a8 --- /dev/null +++ b/src/live_effects/lpe-circle_with_radius.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE effect that draws a circle based on two points and a radius. + * - implementation + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-circle_with_radius.h" +#include "display/curve.h" + +// You might need to include other 2geom files. You can add them here: +#include <2geom/circle.h> +#include <2geom/path-sink.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +LPECircleWithRadius::LPECircleWithRadius(LivePathEffectObject *lpeobject) : + Effect(lpeobject)//, + // initialise your parameters here: + //radius(_("Float parameter"), _("just a real number like 1.4!"), "svgname", &wr, this, 50) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + //registerParameter( dynamic_cast<Parameter *>(&radius) ); +} + +LPECircleWithRadius::~LPECircleWithRadius() += default; + +Geom::PathVector +LPECircleWithRadius::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out = Geom::PathVector(); + + Geom::Point center = path_in[0].initialPoint(); + Geom::Point pt = path_in[0].finalPoint(); + + double radius = Geom::L2(pt - center); + + Geom::Circle c(center, radius); + return Geom::Path(c); +} + +/* + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPECircleWithRadius::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > & pwd2_in) +{ + Geom::Piecewise<Geom::D2<Geom::SBasis> > output; + + output = pwd2_in; // spice this up to make the effect actually *do* something! + + return output; +} + +*/ + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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/live_effects/lpe-circle_with_radius.h b/src/live_effects/lpe-circle_with_radius.h new file mode 100644 index 0000000..dc9a8b9 --- /dev/null +++ b/src/live_effects/lpe-circle_with_radius.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief LPE effect that draws a circle based on two points and a radius + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_CIRCLE_WITH_RADIUS_H +#define INKSCAPE_LPE_CIRCLE_WITH_RADIUS_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPECircleWithRadius : public Effect { +public: + LPECircleWithRadius(LivePathEffectObject *lpeobject); + ~LPECircleWithRadius() override; + +// Choose to implement one of the doEffect functions. You can delete or comment out the others. + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + // add the parameters for your effect here: + //ScalarParam radius; + // there are all kinds of parameters. Check the /live_effects/parameter directory which types exist! + + LPECircleWithRadius(const LPECircleWithRadius&) = delete; + LPECircleWithRadius& operator=(const LPECircleWithRadius&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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/live_effects/lpe-clone-original.cpp b/src/live_effects/lpe-clone-original.cpp new file mode 100644 index 0000000..fd920cf --- /dev/null +++ b/src/live_effects/lpe-clone-original.cpp @@ -0,0 +1,433 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2012 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-clone-original.h" + +#include "actions/actions-tools.h" +#include "display/curve.h" +#include "live_effects/parameter/satellite-reference.h" +#include "lpe-bspline.h" +#include "lpe-spiro.h" +#include "lpeobject-reference.h" +#include "lpeobject.h" +#include "object/sp-clippath.h" +#include "object/sp-mask.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/sp-use.h" +#include "svg/path-string.h" +#include "svg/svg.h" +#include "ui/tools/node-tool.h" +#include "xml/sp-css-attr.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<Clonelpemethod> ClonelpemethodData[] = { + { CLM_NONE, N_("No Shape"), "none" }, + { CLM_D, N_("With LPE's"), "d" }, + { CLM_ORIGINALD, N_("Without LPE's"), "originald" }, + { CLM_BSPLINESPIRO, N_("Spiro or BSpline Only"), "bsplinespiro" }, +}; +static const Util::EnumDataConverter<Clonelpemethod> CLMConverter(ClonelpemethodData, CLM_END); + +LPECloneOriginal::LPECloneOriginal(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , linkeditem(_("Linked Item:"), _("Item from which to take the original data"), "linkeditem", &wr, this) + , method(_("Shape"), _("Linked shape"), "method", CLMConverter, &wr, this, CLM_D) + , attributes(_("Attributes"), _("Attributes of the original that the clone should copy, written as a comma-separated list; e.g. 'transform, X, Y'."), + "attributes", &wr, this, "") + , css_properties(_("CSS Properties"), + _("CSS properties of the original that the clone should copy, written as a comma-separated list; e.g. 'fill, filter, opacity'."), + "css_properties", &wr, this, "") + , allow_transforms(_("Allow Transforms"), _("Allow transforms"), "allow_transforms", &wr, this, true) +{ + //0.92 compatibility + const gchar *linkedpath = getLPEObj()->getAttribute("linkedpath"); + if (linkedpath && strcmp(linkedpath, "") != 0){ + getLPEObj()->setAttribute("linkeditem", linkedpath); + getLPEObj()->removeAttribute("linkedpath"); + getLPEObj()->setAttribute("method", "bsplinespiro"); + getLPEObj()->setAttribute("allow_transforms", "false"); + }; + + sync = false; + linked = ""; + if (getLPEObj()->getAttribute("linkeditem")) { + linked = getLPEObj()->getAttribute("linkeditem"); + } + registerParameter(&linkeditem); + registerParameter(&method); + registerParameter(&attributes); + registerParameter(&css_properties); + registerParameter(&allow_transforms); + attributes.param_hide_canvas_text(); + css_properties.param_hide_canvas_text(); +} + +LPECloneOriginal::~LPECloneOriginal() +{ + doOnRemove(nullptr); +} + +bool LPECloneOriginal::doOnOpen(SPLPEItem const *lpeitem) +{ + // we need to inform when all items are ready to read svg relink clones + // previously couldn't be because clones are not ready (load later) + linkeditem.start_listening(linkeditem.getObject()); + linkeditem.connect_selection_changed(); + return false; +} + +void +LPECloneOriginal::syncOriginal() +{ + if (method != CLM_NONE) { + sync = true; + sp_lpe_item_update_patheffect (sp_lpe_item, false, true); + method.param_set_value(CLM_NONE); + refresh_widgets = true; + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + sp_lpe_item_update_patheffect (sp_lpe_item, false, true); + if (desktop && dynamic_cast<Inkscape::UI::Tools::NodeTool *>(desktop->event_context)) { + // Why is this switching tools twice? Probably to reinitialize Node Tool. + set_active_tool(desktop, "Select"); + set_active_tool(desktop, "Node"); + } + } +} + +Gtk::Widget * +LPECloneOriginal::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + ++it; + } + Gtk::Button * sync_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("No Shape Sync to Current")))); + sync_button->signal_clicked().connect(sigc::mem_fun (*this,&LPECloneOriginal::syncOriginal)); + vbox->pack_start(*sync_button, true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void +LPECloneOriginal::cloneAttributes(SPObject *origin, SPObject *dest, const gchar * attributes, const gchar * css_properties, bool init) +{ + SPDocument *document = getSPDoc(); + if (!document || !origin || !dest) { + return; + } + bool root = dest == sp_lpe_item; + SPGroup * group_origin = dynamic_cast<SPGroup *>(origin); + SPGroup * group_dest = dynamic_cast<SPGroup *>(dest); + if (group_origin && group_dest && group_origin->getItemCount() == group_dest->getItemCount()) { + std::vector< SPObject * > childs = group_origin->childList(true); + size_t index = 0; + for (auto & child : childs) { + SPObject *dest_child = group_dest->nthChild(index); + cloneAttributes(child, dest_child, attributes, css_properties, init); + index++; + } + } else if ((!group_origin && group_dest) || + ( group_origin && !group_dest)) + { + g_warning("LPE Clone Original: for this path effect to work properly, the same type and the same number of children are required"); + return; + } + //Attributes + SPShape * shape_origin = dynamic_cast<SPShape *>(origin); + SPShape * shape_dest = dynamic_cast<SPShape *>(dest); + SPItem * item_origin = dynamic_cast<SPItem *>(origin); + SPItem * item_dest = dynamic_cast<SPItem *>(dest); + SPMask * mask_origin = dynamic_cast<SPMask *>(item_origin->getMaskObject()); + SPMask * mask_dest = dynamic_cast<SPMask *>(item_dest->getMaskObject()); + if(mask_origin && mask_dest) { + std::vector<SPObject*> mask_list = mask_origin->childList(true); + std::vector<SPObject*> mask_list_dest = mask_dest->childList(true); + if (mask_list.size() == mask_list_dest.size()) { + size_t i = 0; + for (auto mask_data : mask_list) { + SPObject * mask_dest_data = mask_list_dest[i]; + cloneAttributes(mask_data, mask_dest_data, attributes, css_properties, init); + i++; + } + } + } + + SPClipPath *clippath_origin = SP_ITEM(origin)->getClipObject(); + SPClipPath *clippath_dest = SP_ITEM(dest)->getClipObject(); + if(clippath_origin && clippath_dest) { + std::vector<SPObject*> clippath_list = clippath_origin->childList(true); + std::vector<SPObject*> clippath_list_dest = clippath_dest->childList(true); + if (clippath_list.size() == clippath_list_dest.size()) { + size_t i = 0; + for (auto clippath_data : clippath_list) { + SPObject * clippath_dest_data = clippath_list_dest[i]; + cloneAttributes(clippath_data, clippath_dest_data, attributes, css_properties, init); + i++; + } + } + } + + gchar ** attarray = g_strsplit(old_attributes.c_str(), ",", 0); + gchar ** iter = attarray; + while (*iter) { + const char *attribute = g_strstrip(*iter); + if (strlen(attribute)) { + dest->removeAttribute(attribute); + } + iter++; + } + g_strfreev(attarray); + + attarray = g_strsplit(attributes, ",", 0); + iter = attarray; + while (*iter) { + const char *attribute = g_strstrip(*iter); + if (strlen(attribute) && shape_dest && shape_origin) { + if (std::strcmp(attribute, "d") == 0) { + std::unique_ptr<SPCurve> c; + if (method == CLM_BSPLINESPIRO) { + c = SPCurve::copy(shape_origin->curveForEdit()); + SPLPEItem * lpe_item = SP_LPE_ITEM(origin); + if (lpe_item) { + PathEffectList lpelist = lpe_item->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpe)) { + Geom::PathVector hp; + LivePathEffect::sp_bspline_do_effect(c.get(), 0, hp); + } else if (dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpe)) { + LivePathEffect::sp_spiro_do_effect(c.get()); + } + } + } + } + } else if (method == CLM_ORIGINALD) { + c = SPCurve::copy(shape_origin->curveForEdit()); + } else if(method == CLM_D){ + c = SPCurve::copy(shape_origin->curve()); + } + if (c && method != CLM_NONE) { + Geom::PathVector c_pv = c->get_pathvector(); + c->set_pathvector(c_pv); + auto str = sp_svg_write_path(c_pv); + if (sync){ + dest->setAttribute("inkscape:original-d", str); + } + shape_dest->setCurveInsync(std::move(c)); + dest->setAttribute("d", str); + } else if (method != CLM_NONE) { + dest->removeAttribute(attribute); + } + } else { + dest->setAttribute(attribute, origin->getAttribute(attribute)); + } + } + iter++; + } + if (!allow_transforms || !root) { + dest->setAttribute("transform", origin->getAttribute("transform")); + dest->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + g_strfreev(attarray); + + SPCSSAttr *css_origin = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css_origin, origin->getAttribute("style")); + SPCSSAttr *css_dest = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css_dest, dest->getAttribute("style")); + if (init) { + css_dest = css_origin; + } + gchar ** styleattarray = g_strsplit(old_css_properties.c_str(), ",", 0); + gchar ** styleiter = styleattarray; + while (*styleiter) { + const char *attribute = g_strstrip(*styleiter); + if (strlen(attribute)) { + sp_repr_css_set_property (css_dest, attribute, nullptr); + } + styleiter++; + } + g_strfreev(styleattarray); + + styleattarray = g_strsplit(css_properties, ",", 0); + styleiter = styleattarray; + while (*styleiter) { + const char *attribute = g_strstrip(*styleiter); + if (strlen(attribute)) { + const char* origin_attribute = sp_repr_css_property(css_origin, attribute, ""); + if (!strlen(origin_attribute)) { //==0 + sp_repr_css_set_property (css_dest, attribute, nullptr); + } else { + sp_repr_css_set_property (css_dest, attribute, origin_attribute); + } + } + styleiter++; + } + g_strfreev(styleattarray); + + Glib::ustring css_str; + sp_repr_css_write_string(css_dest,css_str); + dest->setAttributeOrRemoveIfEmpty("style", css_str); +} + +void +LPECloneOriginal::doBeforeEffect (SPLPEItem const* lpeitem){ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + + + bool active = true; + if (linkeditem.lperef && linkeditem.lperef->isAttached() && linkeditem.lperef.get()->getObject() == nullptr) { + active = false; + } + if (!active) { + linkeditem.unlink(); + return; + } + + if (linkeditem.linksToItem()) { + if (!linkeditem.isConnected() && linkeditem.getObject()) { + linkeditem.start_listening(linkeditem.getObject()); + linkeditem.update_satellites(true); + return; + } + sp_lpe_item = nullptr; + auto lpeitems = getCurrrentLPEItems(); + if (lpeitems.size()) { + sp_lpe_item = lpeitems[0]; + } + SPItem *orig = dynamic_cast<SPItem *>(linkeditem.getObject()); + if(!orig) { + return; + } + SPText *text_origin = dynamic_cast<SPText *>(orig); + SPItem *dest = dynamic_cast<SPItem *>(sp_lpe_item); + const gchar * id = orig->getId(); + bool init = !is_load && g_strcmp0(id, linked.c_str()) != 0; + /* if (sp_lpe_item->getRepr()->attribute("style")) { + init = false; + } */ + Glib::ustring attr = "d,"; + if (text_origin) { + std::unique_ptr<SPCurve> curve = text_origin->getNormalizedBpath(); + dest->setAttribute("inkscape:original-d", sp_svg_write_path(curve->get_pathvector())); + attr = ""; + } + if (g_strcmp0(linked.c_str(), id) && !is_load) { + dest->setAttribute("transform", nullptr); + } + original_bbox(lpeitem, false, true); + auto attributes_str = attributes.param_getSVGValue(); + attr += attributes_str + ","; + if (attr.size() && attributes_str.empty()) { + attr.erase (attr.size()-1, 1); + } + auto css_properties_str = css_properties.param_getSVGValue(); + Glib::ustring style_attr = ""; + if (style_attr.size() && css_properties_str.empty()) { + style_attr.erase (style_attr.size()-1, 1); + } + style_attr += css_properties_str + ","; + cloneAttributes(orig, dest, attr.c_str(), style_attr.c_str(), init); + old_css_properties = css_properties.param_getSVGValue(); + old_attributes = attributes.param_getSVGValue(); + sync = false; + linked = id; + } else { + linked = ""; + } +} + +void LPECloneOriginal::doOnRemove(SPLPEItem const *lpeitem) +{ + // this leave a empty path item but keep clone + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + if (sp_lpe_item && sp_lpe_item->getAttribute("class")) { + Glib::ustring fromclone = sp_lpe_item->getAttribute("class"); + size_t pos = fromclone.find("fromclone"); + if (pos != Glib::ustring::npos) { + gchar *transform = g_strdup(sp_lpe_item->getAttribute("transform")); + linkeditem.quit_listening(); + SPObject *owner = linkeditem.lperef->getObject(); + if (owner) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + desktop->selection->clone(); + SPUse *clone; + if (( clone = dynamic_cast<SPUse*>(desktop->selection->singleItem()))) { + gchar *href_str = g_strdup_printf("#%s", owner->getAttribute("id")); + clone->setAttribute("xlink:href", href_str); + clone->setAttribute("transform", transform); + g_free(href_str); + } + } + } + g_free(transform); + } + } + } + linkeditem.unlink(); +} + +void +LPECloneOriginal::doEffect (SPCurve * curve) +{ + if (method != CLM_NONE) { + SPCurve const *current_curve = current_shape->curve(); + if (current_curve != nullptr) { + curve->set_pathvector(current_curve->get_pathvector()); + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-clone-original.h b/src/live_effects/lpe-clone-original.h new file mode 100644 index 0000000..cb20e37 --- /dev/null +++ b/src/live_effects/lpe-clone-original.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CLONE_ORIGINAL_H +#define INKSCAPE_LPE_CLONE_ORIGINAL_H + +/* + * Inkscape::LPECloneOriginal + * + * Copyright (C) Johan Engelen 2012 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/originalsatellite.h" +#include "live_effects/parameter/text.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum Clonelpemethod { CLM_NONE, CLM_D, CLM_ORIGINALD, CLM_BSPLINESPIRO, CLM_END }; + +class LPECloneOriginal : public Effect, GroupBBoxEffect { +public: + LPECloneOriginal(LivePathEffectObject *lpeobject); + ~LPECloneOriginal() override; + void doEffect (SPCurve * curve) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void doOnRemove(SPLPEItem const * /*lpeitem*/) override; + Gtk::Widget *newWidget() override; + OriginalSatelliteParam linkeditem; + +private: + EnumParam<Clonelpemethod> method; + TextParam attributes; + TextParam css_properties; + BoolParam allow_transforms; + Glib::ustring old_attributes; + Glib::ustring old_css_properties; + Glib::ustring linked; + void syncOriginal(); + void cloneAttributes(SPObject *origin, SPObject *dest, const gchar *attributes, const gchar *css_properties, + bool init); + bool sync; + LPECloneOriginal(const LPECloneOriginal&) = delete; + LPECloneOriginal& operator=(const LPECloneOriginal&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-constructgrid.cpp b/src/live_effects/lpe-constructgrid.cpp new file mode 100644 index 0000000..e758619 --- /dev/null +++ b/src/live_effects/lpe-constructgrid.cpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE Construct Grid implementation + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-constructgrid.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; + +LPEConstructGrid::LPEConstructGrid(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + nr_x(_("Size _X:"), _("The size of the grid in X direction."), "nr_x", &wr, this, 5), + nr_y(_("Size _Y:"), _("The size of the grid in Y direction."), "nr_y", &wr, this, 5) +{ + registerParameter(&nr_x); + registerParameter(&nr_y); + + nr_x.param_make_integer(); + nr_y.param_make_integer(); + nr_x.param_set_range(1, 1e10); + nr_y.param_set_range(1, 1e10); +} + +LPEConstructGrid::~LPEConstructGrid() += default; + +Geom::PathVector +LPEConstructGrid::doEffect_path (Geom::PathVector const & path_in) +{ + // Check that the path has at least 3 nodes (i.e. 2 segments), more nodes are ignored + if (path_in[0].size() >= 2) { + // read the first 3 nodes: + Geom::Path::const_iterator it ( path_in[0].begin() ); + Geom::Point first_p = (*it++).initialPoint(); + Geom::Point origin = (*it++).initialPoint(); + Geom::Point second_p = (*it++).initialPoint(); + // make first_p and second_p be the construction *vectors* of the grid: + first_p -= origin; + second_p -= origin; + Geom::Translate first_translation( first_p ); + Geom::Translate second_translation( second_p ); + + // create the gridpaths of the two directions + Geom::Path first_path( origin ); + first_path.appendNew<LineSegment>( origin + first_p*nr_y ); + Geom::Path second_path( origin ); + second_path.appendNew<LineSegment>( origin + second_p*nr_x ); + + // use the gridpaths and set them in the correct grid + Geom::PathVector path_out; + path_out.push_back(first_path); + for (int ix = 0; ix < nr_x; ix++) { + path_out.push_back(path_out.back() * second_translation ); + } + path_out.push_back(second_path); + for (int iy = 0; iy < nr_y; iy++) { + path_out.push_back(path_out.back() * first_translation ); + } + + return path_out; + } else { + return path_in; + } +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-constructgrid.h b/src/live_effects/lpe-constructgrid.h new file mode 100644 index 0000000..5865402 --- /dev/null +++ b/src/live_effects/lpe-constructgrid.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CONSTRUCTGRID_H +#define INKSCAPE_LPE_CONSTRUCTGRID_H + +/** \file + * Implementation of the construct grid LPE, see lpe-constructgrid.cpp + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEConstructGrid : public Effect { +public: + LPEConstructGrid(LivePathEffectObject *lpeobject); + ~LPEConstructGrid() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + ScalarParam nr_x; + ScalarParam nr_y; + + LPEConstructGrid(const LPEConstructGrid&) = delete; + LPEConstructGrid& operator=(const LPEConstructGrid&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif // INKSCAPE_LPE_CONSTRUCTGRID_H diff --git a/src/live_effects/lpe-copy_rotate.cpp b/src/live_effects/lpe-copy_rotate.cpp new file mode 100644 index 0000000..2c937b0 --- /dev/null +++ b/src/live_effects/lpe-copy_rotate.cpp @@ -0,0 +1,754 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <copy_rotate> implementation + */ +/* + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-copy_rotate.h" + +#include <2geom/intersection-graph.h> +#include <2geom/path-intersection.h> +#include <2geom/sbasis-to-bezier.h> +#include <gdk/gdk.h> +#include <gtkmm.h> + +#include "display/curve.h" +#include "helper/geom.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/satellite-reference.h" +#include "object/sp-object.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "path-chemistry.h" +#include "path/path-boolop.h" +#include "style.h" +#include "svg/path-string.h" +#include "svg/svg.h" +#include "xml/sp-css-attr.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<RotateMethod> RotateMethodData[RM_END] = { + { RM_NORMAL, N_("Normal"), "normal" }, + { RM_KALEIDOSCOPE, N_("Kaleidoscope"), "kaleidoskope" }, + { RM_FUSE, N_("Fuse paths"), "fuse_paths" } +}; +static const Util::EnumDataConverter<RotateMethod> +RMConverter(RotateMethodData, RM_END); + +LPECopyRotate::LPECopyRotate(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // do not change name of this parameter us used in oncommit + lpesatellites(_("lpesatellites"), _("Items satellites"), "lpesatellites", &wr, this, false), + method(_("Method:"), _("Rotate methods"), "method", RMConverter, &wr, this, RM_NORMAL), + origin(_("Origin"), _("Adjust origin of the rotation"), "origin", &wr, this, _("Adjust origin of the rotation")), + starting_point(_("Start point"), _("Starting point to define start angle"), "starting_point", &wr, this, _("Adjust starting point to define start angle")), + starting_angle(_("Starting angle"), _("Angle of the first copy"), "starting_angle", &wr, this, 0.0), + rotation_angle(_("Rotation angle"), _("Angle between two successive copies"), "rotation_angle", &wr, this, 60.0), + num_copies(_("Number of copies"), _("Number of copies of the original path"), "num_copies", &wr, this, 6), + gap(_("Gap"), _("Gap space between copies, use small negative gaps to fix some joins"), "gap", &wr, this, -0.01), + copies_to_360(_("Distribute evenly"), _("Angle between copies is 360°/number of copies (ignores rotation angle setting)"), "copies_to_360", &wr, this, true), + mirror_copies(_("Mirror copies"), _("Mirror between copies"), "mirror_copies", &wr, this, false), + split_items(_("Split elements"), _("Split elements, so each can have its own style"), "split_items", &wr, this, false), + link_styles(_("Link styles"), _("Link styles on split mode"), "link_styles", &wr, this, false), + dist_angle_handle(100.0) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + //0.92 compatibility + if (this->getRepr()->attribute("fuse_paths") && strcmp(this->getRepr()->attribute("fuse_paths"), "true") == 0){ + this->getRepr()->removeAttribute("fuse_paths"); + this->getRepr()->setAttribute("method", "kaleidoskope"); + this->getRepr()->setAttribute("mirror_copies", "true"); + }; + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&lpesatellites); + registerParameter(&method); + registerParameter(&num_copies); + registerParameter(&starting_angle); + registerParameter(&starting_point); + registerParameter(&rotation_angle); + registerParameter(&gap); + registerParameter(&origin); + registerParameter(&copies_to_360); + registerParameter(&mirror_copies); + registerParameter(&split_items); + registerParameter(&link_styles); + gap.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + gap.param_set_increments(0.01, 0.01); + gap.param_set_digits(5); + rotation_angle.param_set_digits(4); + num_copies.param_set_range(1, std::numeric_limits<gint>::max()); + num_copies.param_make_integer(); + apply_to_clippath_and_mask = true; + previous_num_copies = num_copies; + previous_origin = Geom::Point(0,0); + previous_start_point = Geom::Point(0,0); + starting_point.param_widget_is_visible(false); + reset = link_styles; +} + +LPECopyRotate::~LPECopyRotate() +{ + keep_paths = false; + doOnRemove(nullptr); +}; + +bool LPECopyRotate::doOnOpen(SPLPEItem const *lpeitem) +{ + bool fixed = false; + if (!is_load || is_applied) { + return fixed; + } + legacytest_livarotonly = false; + Glib::ustring version = lpeversion.param_getSVGValue(); + if (version < "1.2") { + if (!SP_ACTIVE_DESKTOP) { + legacytest_livarotonly = true; + } + if (!split_items) { + return fixed; + } + lpesatellites.clear(); + for (size_t i = 0; i < num_copies - 1; i++) { + Glib::ustring id = Glib::ustring("rotated-"); + id += std::to_string(i); + id += "-"; + id += getLPEObj()->getId(); + SPObject *elemref = getSPDoc()->getObjectById(id.c_str()); + if (elemref) { + lpesatellites.link(elemref, i); + } + } + lpeversion.param_setValue("1.2", true); + fixed = true; + lpesatellites.write_to_SVG(); + } + if (!split_items) { + return fixed; + } + lpesatellites.start_listening(); + lpesatellites.connect_selection_changed(); + container = lpeitem->parent; + return fixed; +} + +void +LPECopyRotate::doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) +{ + if (split_items) { + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + bool write = false; + bool active = !lpesatellites.data().size(); + for (auto lpereference : lpesatellites.data()) { + if (lpereference && lpereference->isAttached() && lpereference.get()->getObject() != nullptr) { + active = true; + } + } + if (!active && !is_load && previous_split) { + lpesatellites.clear(); + previous_num_copies = 0; + return; + } + + container = sp_lpe_item->parent; + if (previous_num_copies != num_copies) { + write = true; + size_t pos = 0; + for (auto lpereference : lpesatellites.data()) { + if (lpereference && lpereference->isAttached()) { + SPItem *copies = dynamic_cast<SPItem *>(lpereference->getObject()); + if (copies) { + if (pos > num_copies - 2) { + copies->setHidden(true); + } else if (copies->isHidden()) { + copies->setHidden(false); + } + } + } + pos++; + } + previous_num_copies = num_copies; + } + bool forcewrite = write; + Geom::Affine m = Geom::Translate(-origin) * Geom::Rotate(-(Geom::rad_from_deg(starting_angle))); + for (size_t i = 1; i < num_copies; ++i) { + Geom::Affine r = Geom::identity(); + if(mirror_copies && i%2 != 0) { + r *= Geom::Rotate(Geom::Angle(half_dir)).inverse(); + r *= Geom::Scale(1, -1); + r *= Geom::Rotate(Geom::Angle(half_dir)); + } + + Geom::Rotate rot(-(Geom::rad_from_deg(rotation_angle * i))); + Geom::Affine t = m * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + if (method != RM_NORMAL) { + if(mirror_copies && i%2 != 0) { + t = m * r * rot * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + } + } else { + if(mirror_copies && i%2 != 0) { + t = m * Geom::Rotate(Geom::rad_from_deg(-rotation_angle)) * r * rot * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + } + } + t *= sp_lpe_item->transform; + toItem(t, i-1, reset, write); + forcewrite = forcewrite || write; + } + //we keep satellites connected and active if write needed + bool connected = lpesatellites.is_connected(); + if (forcewrite || !connected) { + lpesatellites.write_to_SVG(); + lpesatellites.start_listening(); + lpesatellites.update_satellites(!connected); + } + reset = link_styles; + } + previous_split = split_items; +} + +void LPECopyRotate::cloneStyle(SPObject *orig, SPObject *dest) +{ + dest->setAttribute("transform", orig->getAttribute("transform")); + dest->setAttribute("style", orig->getAttribute("style")); + dest->setAttribute("mask", orig->getAttribute("mask")); + dest->setAttribute("clip-path", orig->getAttribute("clip-path")); + dest->setAttribute("class", orig->getAttribute("class")); + for (auto iter : orig->style->properties()) { + if (iter->style_src != SPStyleSrc::UNSET) { + auto key = iter->id(); + if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) { + const gchar *attr = orig->getAttribute(iter->name().c_str()); + if (attr) { + dest->setAttribute(iter->name(), attr); + } + } + } + } +} + +void LPECopyRotate::cloneD(SPObject *orig, SPObject *dest, Geom::Affine transform) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if ( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() == SP_GROUP(dest)->getItemCount() ) { + if (reset) { + cloneStyle(orig, dest); + } + std::vector< SPObject * > childs = orig->childList(true); + size_t index = 0; + for (auto & child : childs) { + SPObject *dest_child = dest->nthChild(index); + cloneD(child, dest_child, transform); + index++; + } + return; + } else if( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() != SP_GROUP(dest)->getItemCount()) { + split_items.param_setValue(false); + return; + } + + if ( SP_IS_TEXT(orig) && SP_IS_TEXT(dest) && SP_TEXT(orig)->children.size() == SP_TEXT(dest)->children.size()) { + if (reset) { + cloneStyle(orig, dest); + } + size_t index = 0; + for (auto & child : SP_TEXT(orig)->children) { + SPObject *dest_child = dest->nthChild(index); + cloneD(&child, dest_child, transform); + index++; + } + } + + SPShape * shape = SP_SHAPE(orig); + SPPath * path = SP_PATH(dest); + if (shape) { + SPCurve const *c = shape->curve(); + if (c) { + auto str = sp_svg_write_path(c->get_pathvector()); + if (shape && !path) { + const char *id = dest->getAttribute("id"); + const char *style = dest->getAttribute("style"); + Inkscape::XML::Document *xml_doc = dest->document->getReprDoc(); + Inkscape::XML::Node *dest_node = xml_doc->createElement("svg:path"); + dest_node->setAttribute("id", id); + dest_node->setAttribute("style", style); + dest->updateRepr(xml_doc, dest_node, SP_OBJECT_WRITE_ALL); + path = SP_PATH(dest); + } + path->setAttribute("d", str); + } else { + path->removeAttribute("d"); + } + + } + if (reset) { + cloneStyle(orig, dest); + } +} + +Inkscape::XML::Node * +LPECopyRotate::createPathBase(SPObject *elemref) { + SPDocument *document = getSPDoc(); + if (!document) { + return nullptr; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *prev = elemref->getRepr(); + SPGroup *group = dynamic_cast<SPGroup *>(elemref); + if (group) { + Inkscape::XML::Node *container = xml_doc->createElement("svg:g"); + container->setAttribute("transform", prev->attribute("transform")); + container->setAttribute("mask", prev->attribute("mask")); + container->setAttribute("clip-path", prev->attribute("clip-path")); + container->setAttribute("class", prev->attribute("class")); + container->setAttribute("style", prev->attribute("style")); + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + Inkscape::XML::Node *previous = nullptr; + for (auto sub_item : item_list) { + Inkscape::XML::Node *resultnode = createPathBase(sub_item); + + container->addChild(resultnode, previous); + previous = resultnode; + } + return container; + } + Inkscape::XML::Node *resultnode = xml_doc->createElement("svg:path"); + resultnode->setAttribute("transform", prev->attribute("transform")); + resultnode->setAttribute("style", prev->attribute("style")); + resultnode->setAttribute("mask", prev->attribute("mask")); + resultnode->setAttribute("clip-path", prev->attribute("clip-path")); + resultnode->setAttribute("class", prev->attribute("class")); + return resultnode; +} + +void +LPECopyRotate::toItem(Geom::Affine transform, size_t i, bool reset, bool &write) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + //Inkscape::XML::Document *xml_doc = document->getReprDoc(); + + SPObject *elemref = nullptr; + if (container != sp_lpe_item->parent) { + lpesatellites.read_from_SVG(); + return; + } + if (lpesatellites.data().size() > i && lpesatellites.data()[i]) { + elemref = lpesatellites.data()[i]->getObject(); + } + Inkscape::XML::Node *phantom = nullptr; + bool creation = false; + if (elemref) { + phantom = elemref->getRepr(); + } else { + creation = true; + phantom = createPathBase(sp_lpe_item); + reset = true; + elemref = container->appendChildRepr(phantom); + + Inkscape::GC::release(phantom); + } + cloneD(sp_lpe_item, elemref, transform); + elemref->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(transform)); + reset = link_styles; + // allow use on clones even in different parent + /* if (elemref->parent != container) { + if (!creation) { + lpesatellites.unlink(elemref); + } + Inkscape::XML::Node *copy = phantom->duplicate(xml_doc); + copy->setAttribute("id", elemref->getId()); + lpesatellites.link(container->appendChildRepr(copy), i); + Inkscape::GC::release(copy); + elemref->deleteObject(); + } else */ + if (creation) { + write = true; + lpesatellites.link(elemref, i); + } +} + +Gtk::Widget * LPECopyRotate::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + + +void +LPECopyRotate::doOnApply(SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + + A = Point(boundingbox_X.min(), boundingbox_Y.middle()); + B = Point(boundingbox_X.middle(), boundingbox_Y.middle()); + origin.param_setValue(A, true); + origin.param_update_default(A); + dist_angle_handle = L2(B - A); + dir = unit_vector(B - A); + lpeversion.param_setValue("1.2", true); +} + +void +LPECopyRotate::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + if (!split_items && lpesatellites.data().size()) { + processObjects(LPE_ERASE); + } + if (link_styles) { + reset = true; + } + if (split_items && !lpesatellites.data().size()) { + lpesatellites.read_from_SVG(); + if (lpesatellites.data().size()) { + lpesatellites.update_satellites(); + } + } + original_bbox(lpeitem, false, true); + if (copies_to_360 && num_copies > 2) { + rotation_angle.param_set_value(360.0/(double)num_copies); + } + if (method != RM_NORMAL && rotation_angle * num_copies > 360 && rotation_angle > 0 && copies_to_360) { + num_copies.param_set_value(floor(360/rotation_angle)); + } + if (method != RM_NORMAL && mirror_copies && copies_to_360) { + num_copies.param_set_increments(2.0,10.0); + if ((int)num_copies%2 !=0) { + num_copies.param_set_value(num_copies+1); + rotation_angle.param_set_value(360.0/(double)num_copies); + } + } else { + num_copies.param_set_increments(1.0, 10.0); + } + + A = Point(boundingbox_X.min(), boundingbox_Y.middle()); + B = Point(boundingbox_X.middle(), boundingbox_Y.middle()); + if (Geom::are_near(A, B, 0.01)) { + B += Geom::Point(1.0, 0.0); + } + dir = unit_vector(B - A); + // I first suspected the minus sign to be a bug in 2geom but it is + // likely due to SVG's choice of coordinate system orientation (max) + bool near_start_point = Geom::are_near(previous_start_point, (Geom::Point)starting_point, 0.01); + bool near_origin = Geom::are_near(previous_origin, (Geom::Point)origin, 0.01); + if (!near_start_point && !is_load) { + if (lpeitem->document->isSensitive()) { + starting_angle.param_set_value(deg_from_rad(-angle_between(dir, starting_point - origin))); + } + if (GDK_SHIFT_MASK) { + dist_angle_handle = L2(B - A); + } else { + dist_angle_handle = L2(starting_point - origin); + } + } + if (dist_angle_handle < 1.0) { + dist_angle_handle = 1.0; + } + double distance = dist_angle_handle; + if (previous_start_point != Geom::Point(0,0) || previous_origin != Geom::Point(0,0)) { + distance = Geom::distance(previous_origin, starting_point); + } + start_pos = origin + dir * Rotate(-rad_from_deg(starting_angle)) * distance; + if (!near_start_point || !near_origin || split_items) { + starting_point.param_setValue(start_pos); + } + + previous_origin = (Geom::Point)origin; + previous_start_point = (Geom::Point)starting_point; +} + +void +LPECopyRotate::split(Geom::PathVector &path_on, Geom::Path const ÷r) +{ + Geom::PathVector tmp_path; + double time_start = 0.0; + Geom::Path original = path_on[0]; + int position = 0; + Geom::Crossings cs = crossings(original,divider); + std::vector<double> crossed; + for(auto & c : cs) { + crossed.push_back(c.ta); + } + std::sort(crossed.begin(), crossed.end()); + for (double time_end : crossed) { + if (time_start == time_end || time_end - time_start < Geom::EPSILON) { + continue; + } + Geom::Path portion_original = original.portion(time_start,time_end); + if (!portion_original.empty()) { + Geom::Point side_checker = portion_original.pointAt(0.0001); + position = Geom::sgn(Geom::cross(divider[1].finalPoint() - divider[0].finalPoint(), side_checker - divider[0].finalPoint())); + if (rotation_angle != 180) { + position = pointInTriangle(side_checker, divider.initialPoint(), divider[0].finalPoint(), divider[1].finalPoint()); + } + if (position == 1) { + tmp_path.push_back(portion_original); + } + portion_original.clear(); + time_start = time_end; + } + } + position = Geom::sgn(Geom::cross(divider[1].finalPoint() - divider[0].finalPoint(), original.finalPoint() - divider[0].finalPoint())); + if (rotation_angle != 180) { + position = pointInTriangle(original.finalPoint(), divider.initialPoint(), divider[0].finalPoint(), divider[1].finalPoint()); + } + if (cs.size() > 0 && position == 1) { + Geom::Path portion_original = original.portion(time_start, original.size()); + if(!portion_original.empty()){ + if (!original.closed()) { + tmp_path.push_back(portion_original); + } else { + if (tmp_path.size() > 0 && tmp_path[0].size() > 0 ) { + portion_original.setFinal(tmp_path[0].initialPoint()); + portion_original.append(tmp_path[0]); + tmp_path[0] = portion_original; + } else { + tmp_path.push_back(portion_original); + } + } + portion_original.clear(); + } + } + if (cs.size()==0 && position == 1) { + tmp_path.push_back(original); + } + path_on = tmp_path; +} + +Geom::PathVector +LPECopyRotate::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + double diagonal = Geom::distance(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + Geom::OptRect bbox = sp_lpe_item->geometricBounds(); + size_divider = Geom::distance(origin,bbox) + (diagonal * 6); + Geom::Point line_start = origin + dir * Geom::Rotate(-(Geom::rad_from_deg(starting_angle))) * size_divider; + Geom::Point line_end = origin + dir * Geom::Rotate(-(Geom::rad_from_deg(rotation_angle + starting_angle))) * size_divider; + divider = Geom::Path(line_start); + divider.appendNew<Geom::LineSegment>((Geom::Point)origin); + divider.appendNew<Geom::LineSegment>(line_end); + divider.close(); + half_dir = unit_vector(Geom::middle_point(line_start,line_end) - (Geom::Point)origin); + FillRuleBool fillrule = fill_nonZero; + if (current_shape->style && + current_shape->style->fill_rule.set && + current_shape->style->fill_rule.computed == SP_WIND_RULE_EVENODD) + { + fillrule = (FillRuleBool)fill_oddEven; + } + if (method != RM_NORMAL) { + if (method != RM_KALEIDOSCOPE) { + path_out = doEffect_path_post(path_in, fillrule); + } else { + path_out = pathv_to_linear_and_cubic_beziers(path_in); + } + if (num_copies == 0) { + return path_out; + } + Geom::PathVector triangle; + triangle.push_back(divider); + path_out = sp_pathvector_boolop(path_out, triangle, bool_op_inters, fillrule, fillrule, legacytest_livarotonly); + if ( !split_items ) { + path_out = doEffect_path_post(path_out, fillrule); + } else { + path_out *= Geom::Translate(half_dir * gap); + } + } else { + path_out = doEffect_path_post(path_in, fillrule); + } + if (!split_items && method != RM_NORMAL) { + Geom::PathVector path_out_tmp; + for (const auto & path_it : path_out) { + if (path_it.empty()) { + continue; + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + Geom::Path res; + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + if (!Geom::are_near(curve_it1->initialPoint(), curve_it1->pointAt(0.5), 0.05)) { + if (!res.empty()) { + res.setFinal(curve_it1->initialPoint()); + } + Geom::Curve *c = curve_it1->duplicate(); + res.append(c); + } + ++curve_it1; + } + if (path_it.closed()) { + res.close(); + } + path_out_tmp.push_back(res); + } + path_out = path_out_tmp; + } + return pathv_to_linear_and_cubic_beziers(path_out); +} + +Geom::PathVector +LPECopyRotate::doEffect_path_post (Geom::PathVector const & path_in, FillRuleBool fillrule) +{ + if ((split_items || num_copies == 1) && method == RM_NORMAL) { + if (split_items) { + Geom::PathVector path_out = pathv_to_linear_and_cubic_beziers(path_in); + Geom::Affine m = Geom::Translate(-origin) * Geom::Rotate(-(Geom::rad_from_deg(starting_angle))); + Geom::Affine t = m * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * + Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + return path_out * t; + } + return path_in; + } + + Geom::Affine pre = Geom::Translate(-origin) * Geom::Rotate(-Geom::rad_from_deg(starting_angle)); + Geom::PathVector original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::PathVector output_pv; + Geom::PathVector output; + for (int i = 0; i < num_copies; ++i) { + Geom::Rotate rot(-Geom::rad_from_deg(rotation_angle * i)); + Geom::Affine r = Geom::identity(); + if( i%2 != 0 && mirror_copies) { + r *= Geom::Rotate(Geom::Angle(half_dir)).inverse(); + r *= Geom::Scale(1, -1); + r *= Geom::Rotate(Geom::Angle(half_dir)); + } + Geom::Affine t = pre * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + if(mirror_copies && i%2 != 0) { + t = pre * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)).inverse() * Geom::Translate(origin); + } + if (method != RM_NORMAL) { + //we use safest way to union + Geom::PathVector join_pv = original_pathv * t; + join_pv *= Geom::Translate(half_dir * rot * gap); + if (!output_pv.empty()) { + output_pv = sp_pathvector_boolop(output_pv, join_pv, bool_op_union, fillrule, fillrule, legacytest_livarotonly); + } else { + output_pv = join_pv; + } + } else { + t = pre * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * r * rot * Geom::Rotate(Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + if(mirror_copies && i%2 != 0) { + t = pre * Geom::Rotate(Geom::rad_from_deg(-starting_angle-rotation_angle)) * r * rot * Geom::Rotate(-Geom::rad_from_deg(starting_angle)) * Geom::Translate(origin); + } + output_pv = path_in * t; + output.insert(output.end(), output_pv.begin(), output_pv.end()); + } + } + if (method != RM_NORMAL) { + output = output_pv; + } + return output; +} + +void +LPECopyRotate::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + using namespace Geom; + hp_vec.clear(); + Geom::Path hp; + hp.start(start_pos); + hp.appendNew<Geom::LineSegment>((Geom::Point)origin); + hp.appendNew<Geom::LineSegment>(origin + dir * Rotate(-rad_from_deg(rotation_angle+starting_angle)) * Geom::distance(origin,starting_point)); + Geom::PathVector pathv; + pathv.push_back(hp); + hp_vec.push_back(pathv); +} + +void +LPECopyRotate::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); +} + +void +LPECopyRotate::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ + processObjects(LPE_VISIBILITY); +} + +void +LPECopyRotate::doOnRemove (SPLPEItem const* lpeitem) +{ + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + return; + } + processObjects(LPE_ERASE); +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-copy_rotate.h b/src/live_effects/lpe-copy_rotate.h new file mode 100644 index 0000000..3479794 --- /dev/null +++ b/src/live_effects/lpe-copy_rotate.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_COPY_ROTATE_H +#define INKSCAPE_LPE_COPY_ROTATE_H + +/** \file + * LPE <copy_rotate> implementation, see lpe-copy_rotate.cpp. + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/satellitearray.h" +#include "live_effects/parameter/text.h" +// this is only to fillrule +#include "livarot/Shape.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum RotateMethod { + RM_NORMAL, + RM_KALEIDOSCOPE, + RM_FUSE, + RM_END +}; + +typedef FillRule FillRuleBool; + +class LPECopyRotate : public Effect, GroupBBoxEffect { +public: + LPECopyRotate(LivePathEffectObject *lpeobject); + ~LPECopyRotate() override; + void doOnApply (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) override; + void split(Geom::PathVector &path_in, Geom::Path const ÷r); + void resetDefaults(SPItem const* item) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + bool doOnOpen(SPLPEItem const * /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + Gtk::Widget * newWidget() override; + void cloneStyle(SPObject *orig, SPObject *dest); + Geom::PathVector doEffect_path_post (Geom::PathVector const & path_in, FillRuleBool fillrule); + void toItem(Geom::Affine transform, size_t i, bool reset, bool &write); + void cloneD(SPObject *orig, SPObject *dest, Geom::Affine transform); + Inkscape::XML::Node * createPathBase(SPObject *elemref); + //virtual void setFusion(Geom::PathVector &path_in, Geom::Path divider, double sizeDivider); + BoolParam split_items; +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + +private: + SatelliteArrayParam lpesatellites; + EnumParam<RotateMethod> method; + PointParam origin; + PointParam starting_point; + ScalarParam starting_angle; + ScalarParam rotation_angle; + ScalarParam num_copies; + ScalarParam gap; + BoolParam copies_to_360; + BoolParam mirror_copies; + BoolParam link_styles; + Geom::Point A; + Geom::Point B; + Geom::Point dir; + Geom::Point half_dir; + Geom::Point start_pos; + Geom::Point previous_origin; + Geom::Point previous_start_point; + double dist_angle_handle; + double size_divider; + Geom::Path divider; + double previous_num_copies; + bool reset; + bool legacytest_livarotonly = false; + bool previous_split = false; + SPObject *container; + LPECopyRotate(const LPECopyRotate&) = delete; + LPECopyRotate& operator=(const LPECopyRotate&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-curvestitch.cpp b/src/live_effects/lpe-curvestitch.cpp new file mode 100644 index 0000000..f1f74dd --- /dev/null +++ b/src/live_effects/lpe-curvestitch.cpp @@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE Curve Stitching implementation, used as an example for a base starting class + * when implementing new LivePathEffects. + * + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/scalar.h" +#include "live_effects/lpe-curvestitch.h" + +#include "object/sp-path.h" + +#include "svg/svg.h" +#include "xml/repr.h" + +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; + +LPECurveStitch::LPECurveStitch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + strokepath(_("Stitch path:"), _("The path that will be used as stitch."), "strokepath", &wr, this, "M0,0 L1,0"), + nrofpaths(_("N_umber of paths:"), _("The number of paths that will be generated."), "count", &wr, this, 5), + startpoint_edge_variation(_("Sta_rt edge variance:"), _("The amount of random jitter to move the start points of the stitches inside & outside the guide path"), "startpoint_edge_variation", &wr, this, 0), + startpoint_spacing_variation(_("Sta_rt spacing variance:"), _("The amount of random shifting to move the start points of the stitches back & forth along the guide path"), "startpoint_spacing_variation", &wr, this, 0), + endpoint_edge_variation(_("End ed_ge variance:"), _("The amount of randomness that moves the end points of the stitches inside & outside the guide path"), "endpoint_edge_variation", &wr, this, 0), + endpoint_spacing_variation(_("End spa_cing variance:"), _("The amount of random shifting to move the end points of the stitches back & forth along the guide path"), "endpoint_spacing_variation", &wr, this, 0), + prop_scale(_("Scale _width:"), _("Scale the width of the stitch path"), "prop_scale", &wr, this, 1), + scale_y_rel(_("Scale _width relative to length"), _("Scale the width of the stitch path relative to its length"), "scale_y_rel", &wr, this, false) +{ + registerParameter(&nrofpaths); + registerParameter(&startpoint_edge_variation); + registerParameter(&startpoint_spacing_variation); + registerParameter(&endpoint_edge_variation); + registerParameter(&endpoint_spacing_variation); + registerParameter(&strokepath ); + registerParameter(&prop_scale); + registerParameter(&scale_y_rel); + + nrofpaths.param_make_integer(); + nrofpaths.param_set_range(2, Geom::infinity()); + + prop_scale.param_set_digits(3); + prop_scale.param_set_increments(0.01, 0.10); + transformed = false; +} + +LPECurveStitch::~LPECurveStitch() += default; + +bool +LPECurveStitch::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + strokepath.reload(); + return false; +} + + +Geom::PathVector +LPECurveStitch::doEffect_path (Geom::PathVector const & path_in) +{ + + if (is_load) { + strokepath.reload(); + } + + if (path_in.size() >= 2) { + startpoint_edge_variation.resetRandomizer(); + endpoint_edge_variation.resetRandomizer(); + startpoint_spacing_variation.resetRandomizer(); + endpoint_spacing_variation.resetRandomizer(); + Geom::Affine affine = strokepath.get_relative_affine().withoutTranslation(); + + D2<Piecewise<SBasis> > stroke = make_cuts_independent(strokepath.get_pwd2() * affine); + OptInterval bndsStroke = bounds_exact(stroke[0]); + OptInterval bndsStrokeY = bounds_exact(stroke[1]); + if (!bndsStroke && !bndsStrokeY) { + return path_in; + } + gdouble scaling = bndsStroke->max() - bndsStroke->min(); + Point stroke_origin(bndsStroke->min(), (bndsStrokeY->max()+bndsStrokeY->min())/2); + + Geom::PathVector path_out; + + // do this for all permutations (ii,jj) if there are more than 2 paths? realllly cool! + for (unsigned ii = 0 ; ii < path_in.size() - 1; ii++) + for (unsigned jj = ii+1; jj < path_in.size(); jj++) + { + Piecewise<D2<SBasis> > A = arc_length_parametrization(Piecewise<D2<SBasis> >(path_in[ii].toPwSb()),2,.1); + Piecewise<D2<SBasis> > B = arc_length_parametrization(Piecewise<D2<SBasis> >(path_in[jj].toPwSb()),2,.1); + Interval bndsA = A.domain(); + Interval bndsB = B.domain(); + gdouble incrementA = (bndsA.max()-bndsA.min()) / (nrofpaths-1); + gdouble incrementB = (bndsB.max()-bndsB.min()) / (nrofpaths-1); + gdouble tA = bndsA.min(); + gdouble tB = bndsB.min(); + gdouble tAclean = tA; // the tA without spacing_variation + gdouble tBclean = tB; // the tB without spacing_variation + + for (int i = 0; i < nrofpaths; i++) { + Point start = A(tA); + Point end = B(tB); + if (startpoint_edge_variation.get_value() != 0) + start = start + (startpoint_edge_variation - startpoint_edge_variation.get_value()/2) * (end - start); + if (endpoint_edge_variation.get_value() != 0) + end = end + (endpoint_edge_variation - endpoint_edge_variation.get_value()/2)* (end - start); + + if (!Geom::are_near(start,end)) { + gdouble scaling_y = 1.0; + if (scale_y_rel.get_value() || transformed) { + scaling_y = (L2(end-start)/scaling)*prop_scale; + transformed = false; + } else { + scaling_y = prop_scale; + } + + Affine transform; + transform.setXAxis( (end-start) / scaling ); + transform.setYAxis( rot90(unit_vector(end-start)) * scaling_y); + transform.setTranslation( start ); + Piecewise<D2<SBasis> > pwd2_out = (strokepath.get_pwd2()-stroke_origin) * transform; + + // add stuff to one big pw<d2<sbasis> > and then outside the loop convert to path? + // No: this way, the separate result paths are kept separate which might come in handy some time! + Geom::PathVector result = Geom::path_from_piecewise(pwd2_out, LPE_CONVERSION_TOLERANCE); + path_out.push_back(result[0]); + } + gdouble svA = startpoint_spacing_variation - startpoint_spacing_variation.get_value()/2; + gdouble svB = endpoint_spacing_variation - endpoint_spacing_variation.get_value()/2; + tAclean += incrementA; + tBclean += incrementB; + tA = tAclean + incrementA * svA; + tB = tBclean + incrementB * svB; + if (tA > bndsA.max()) + tA = bndsA.max(); + if (tB > bndsB.max()) + tB = bndsB.max(); + } + } + + return path_out; + } else { + return path_in; + } +} + +void +LPECurveStitch::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + if (!SP_IS_PATH(item)) return; + + using namespace Geom; + + // set the stroke path to run horizontally in the middle of the bounding box of the original path + + // calculate bounding box: (isn't there a simpler way?) + Piecewise<D2<SBasis> > pwd2; + Geom::PathVector temppath = sp_svg_read_pathv( item->getRepr()->attribute("inkscape:original-d")); + for (const auto & i : temppath) { + pwd2.concat( i.toPwSb() ); + } + D2<Piecewise<SBasis> > d2pw = make_cuts_independent(pwd2); + OptInterval bndsX = bounds_exact(d2pw[0]); + OptInterval bndsY = bounds_exact(d2pw[1]); + if (bndsX && bndsY) { + Point start(bndsX->min(), (bndsY->max()+bndsY->min())/2); + Point end(bndsX->max(), (bndsY->max()+bndsY->min())/2); + if ( !Geom::are_near(start,end) ) { + Geom::Path path; + path.start( start ); + path.appendNew<Geom::LineSegment>( end ); + strokepath.set_new_value( path.toPwSb(), true ); + } else { + // bounding box is too small to make decent path. set to default default. :-) + strokepath.param_set_and_write_default(); + } + } else { + // bounding box is non-existent. set to default default. :-) + strokepath.param_set_and_write_default(); + } +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-curvestitch.h b/src/live_effects/lpe-curvestitch.h new file mode 100644 index 0000000..929da4a --- /dev/null +++ b/src/live_effects/lpe-curvestitch.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_CURVESTITCH_H +#define INKSCAPE_LPE_CURVESTITCH_H + +/** \file + * Implementation of the curve stitch effect, see lpe-curvestitch.cpp + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/random.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPECurveStitch : public Effect { +public: + LPECurveStitch(LivePathEffectObject *lpeobject); + ~LPECurveStitch() override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + + void resetDefaults(SPItem const* item) override; + +private: + PathParam strokepath; + ScalarParam nrofpaths; + RandomParam startpoint_edge_variation; + RandomParam startpoint_spacing_variation; + RandomParam endpoint_edge_variation; + RandomParam endpoint_spacing_variation; + ScalarParam prop_scale; + BoolParam scale_y_rel; + bool transformed; + + LPECurveStitch(const LPECurveStitch&) = delete; + LPECurveStitch& operator=(const LPECurveStitch&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-dashed-stroke.cpp b/src/live_effects/lpe-dashed-stroke.cpp new file mode 100644 index 0000000..7edbeac --- /dev/null +++ b/src/live_effects/lpe-dashed-stroke.cpp @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-dashed-stroke.h" +#include "2geom/path.h" +#include "2geom/pathvector.h" +#include "helper/geom.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEDashedStroke::LPEDashedStroke(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , numberdashes(_("Number of dashes"), _("Number of dashes"), "numberdashes", &wr, this, 3) + , holefactor(_("Hole factor"), _("Hole factor"), "holefactor", &wr, this, 0.0) + , splitsegments(_("Use segments"), _("Use segments"), "splitsegments", &wr, this, true) + , halfextreme(_("Half start/end"), _("Start and end of each segment has half size"), "halfextreme", &wr, this, true) + , unifysegment(_("Equalize dashes"), _("Global dash length is approximately the length of the dashes in the shortest path segment"), + "unifysegment", &wr, this, true) + , message(_("Note"), _("Important messages"), "message", &wr, this, + _("Add <b>\"Fill Between Many LPE\"</b> to add fill.")) +{ + registerParameter(&numberdashes); + registerParameter(&holefactor); + registerParameter(&splitsegments); + registerParameter(&halfextreme); + registerParameter(&unifysegment); + registerParameter(&message); + numberdashes.param_set_range(2, 999999999); + numberdashes.param_set_increments(1, 1); + numberdashes.param_set_digits(0); + holefactor.param_set_range(-0.99999, 0.99999); + holefactor.param_set_increments(0.01, 0.01); + holefactor.param_set_digits(5); + message.param_set_min_height(30); +} + +LPEDashedStroke::~LPEDashedStroke() = default; + +void LPEDashedStroke::doBeforeEffect(SPLPEItem const *lpeitem) {} + +///Calculate the time in curve_in with a real time of A +//TODO: find a better place to it +double LPEDashedStroke::timeAtLength(double const A, Geom::Path const &segment) +{ + if ( A == 0 || segment[0].isDegenerate()) { + return 0; + } + double t = 1; + t = timeAtLength(A, segment.toPwSb()); + return t; +} + +///Calculate the time in curve_in with a real time of A +//TODO: find a better place to it +double LPEDashedStroke::timeAtLength(double const A, Geom::Piecewise<Geom::D2<Geom::SBasis>> pwd2) +{ + if ( A == 0 || pwd2.size() == 0) { + return 0; + } + + double t = pwd2.size(); + std::vector<double> t_roots = roots(Geom::arcLengthSb(pwd2) - A); + if (!t_roots.empty()) { + t = t_roots[0]; + } + return t; +} + +Geom::PathVector LPEDashedStroke::doEffect_path(Geom::PathVector const &path_in) +{ + Geom::PathVector const pv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::PathVector result; + for (const auto & path_it : pv) { + if (path_it.empty()) { + continue; + } + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + size_t numberdashes_fixed = numberdashes; + if(!splitsegments) { + numberdashes_fixed++; + } + size_t numberholes = numberdashes_fixed - 1; + size_t ammount = numberdashes_fixed + numberholes; + if (halfextreme) { + ammount--; + } + double base = 1/(double)ammount; + double globaldash = base * numberdashes_fixed * (1 + holefactor); + if (halfextreme) { + globaldash = base * (numberdashes_fixed - 1) * (1 + holefactor); + } + double globalhole = 1-globaldash; + double dashpercent = globaldash/numberdashes_fixed; + if (halfextreme) { + dashpercent = globaldash/(numberdashes_fixed -1); + } + double holepercent = globalhole/numberholes; + double dashsize_fixed = 0; + double holesize_fixed = 0; + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2 = path_it.toPwSb(); + double length_pwd2 = length (pwd2); + double minlength = length_pwd2; + if(unifysegment) { + while (curve_it1 != curve_endit) { + double length_segment = (*curve_it1).length(); + if (length_segment < minlength) { + minlength = length_segment; + dashsize_fixed = (*curve_it1).length() * dashpercent; + holesize_fixed = (*curve_it1).length() * holepercent; + } + ++curve_it1; + ++curve_it2; + } + curve_it1 = path_it.begin(); + curve_it2 = ++(path_it.begin()); + curve_endit = path_it.end_default(); + } + size_t p_index = 0; + size_t start_index = result.size(); + if(splitsegments) { + while (curve_it1 != curve_endit) { + Geom::Path segment = path_it.portion(p_index, p_index + 1); + if(unifysegment) { + double integral; + modf((*curve_it1).length()/(dashsize_fixed + holesize_fixed), &integral); + numberdashes_fixed = (size_t)integral + 1; + numberholes = numberdashes_fixed - 1; + ammount = numberdashes_fixed + numberholes; + if (halfextreme) { + ammount--; + } + base = 1/(double)ammount; + globaldash = base * numberdashes_fixed * (1 + holefactor); + if (halfextreme) { + globaldash = base * (numberdashes_fixed - 1) * (1 + holefactor); + } + globalhole = 1-globaldash; + dashpercent = globaldash/numberdashes_fixed; + if (halfextreme) { + dashpercent = globaldash/(numberdashes_fixed -1); + } + holepercent = globalhole/numberholes; + } + double dashsize = (*curve_it1).length() * dashpercent; + double holesize = (*curve_it1).length() * holepercent; + if ((*curve_it1).isLineSegment()) { + if (result.size() && Geom::are_near(segment.initialPoint(),result[result.size()-1].finalPoint())) { + result[result.size()-1].setFinal(segment.initialPoint()); + if (halfextreme) { + result[result.size()-1].append(segment.portion(0.0, dashpercent/2.0)); + } else { + result[result.size()-1].append(segment.portion(0.0, dashpercent)); + } + } else { + if (halfextreme) { + result.push_back(segment.portion(0.0, dashpercent/2.0)); + } else { + result.push_back(segment.portion(0.0, dashpercent)); + } + } + + double start = dashpercent + holepercent; + if (halfextreme) { + start = (dashpercent/2.0) + holepercent; + } + while (start < 1) { + if (start + dashpercent > 1) { + result.push_back(segment.portion(start, 1)); + } else { + result.push_back(segment.portion(start, start + dashpercent)); + } + start += dashpercent + holepercent; + } + } else if (!(*curve_it1).isLineSegment()) { + double start = 0.0; + double end = 0.0; + if (halfextreme) { + end = timeAtLength(dashsize/2.0,segment); + } else { + end = timeAtLength(dashsize,segment); + } + if (result.size() && Geom::are_near(segment.initialPoint(),result[result.size()-1].finalPoint())) { + result[result.size()-1].setFinal(segment.initialPoint()); + result[result.size()-1].append(segment.portion(start, end)); + } else { + result.push_back(segment.portion(start, end)); + } + double startsize = dashsize + holesize; + if (halfextreme) { + startsize = (dashsize/2.0) + holesize; + } + double endsize = startsize + dashsize; + start = timeAtLength(startsize,segment); + end = timeAtLength(endsize,segment); + while (start < 1 && start > 0) { + result.push_back(segment.portion(start, end)); + startsize = endsize + holesize; + endsize = startsize + dashsize; + start = timeAtLength(startsize,segment); + end = timeAtLength(endsize,segment); + } + } + if (curve_it2 == curve_endit) { + if (path_it.closed()) { + Geom::Path end = result[result.size()-1]; + end.setFinal(result[start_index].initialPoint()); + end.append(result[start_index]); + result[start_index] = end; + } + } + p_index ++; + ++curve_it1; + ++curve_it2; + } + } else { + double start = 0.0; + double end = 0.0; + double dashsize = length_pwd2 * dashpercent; + double holesize = length_pwd2 * holepercent; + if (halfextreme) { + end = timeAtLength(dashsize/2.0,pwd2); + } else { + end = timeAtLength(dashsize,pwd2); + } + result.push_back(path_it.portion(start, end)); + double startsize = dashsize + holesize; + if (halfextreme) { + startsize = (dashsize/2.0) + holesize; + } + double endsize = startsize + dashsize; + start = timeAtLength(startsize,pwd2); + end = timeAtLength(endsize,pwd2); + while (start < path_it.size() && start > 0) { + result.push_back(path_it.portion(start, end)); + startsize = endsize + holesize; + endsize = startsize + dashsize; + start = timeAtLength(startsize,pwd2); + end = timeAtLength(endsize,pwd2); + } + if (path_it.closed()) { + Geom::Path end = result[result.size()-1]; + end.setFinal(result[start_index].initialPoint()); + end.append(result[start_index]); + result[start_index] = end; + } + } + } + return result; +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-dashed-stroke.h b/src/live_effects/lpe-dashed-stroke.h new file mode 100644 index 0000000..d001eaf --- /dev/null +++ b/src/live_effects/lpe-dashed-stroke.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_DASHED_STROKE_H +#define INKSCAPE_LPE_DASHED_STROKE_H + +/* + * Inkscape::LPEDashedStroke + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/message.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEDashedStroke : public Effect { + public: + LPEDashedStroke(LivePathEffectObject *lpeobject); + ~LPEDashedStroke() override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + double timeAtLength(double const A, Geom::Path const &segment); + double timeAtLength(double const A, Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2); +private: + ScalarParam numberdashes; + ScalarParam holefactor; + BoolParam splitsegments; + BoolParam halfextreme; + BoolParam unifysegment; + MessageParam message; +}; + +} //namespace LivePathEffect +} //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-dynastroke.cpp b/src/live_effects/lpe-dynastroke.cpp new file mode 100644 index 0000000..b9fb7a4 --- /dev/null +++ b/src/live_effects/lpe-dynastroke.cpp @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <dynastroke> implementation + */ +/* + * Authors: + * JF Barraud + * + * Copyright (C) JF Barraud 2007 <jf.barraud@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-dynastroke.h" +#include "display/curve.h" +//# include <libnr/n-art-bpath.h> + +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-math.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { +//TODO: growfor/fadefor can be expressed in unit of width. +//TODO: make round/sharp end choices independent for start and end. +//TODO: define more styles like in calligtool. +//TODO: allow fancy ends. + +static const Util::EnumData<DynastrokeMethod> DynastrokeMethodData[DSM_END] = { + {DSM_ELLIPTIC_PEN, N_("Elliptic Pen"), "elliptic_pen"}, + {DSM_THICKTHIN_FAST, N_("Thick-Thin strokes (fast)"), "thickthin_fast"}, + {DSM_THICKTHIN_SLOW, N_("Thick-Thin strokes (slow)"), "thickthin_slow"} +}; +static const Util::EnumDataConverter<DynastrokeMethod> DSMethodConverter(DynastrokeMethodData, DSM_END); + +static const Util::EnumData<DynastrokeCappingType> DynastrokeCappingTypeData[DSCT_END] = { + {DSCT_SHARP, N_("Sharp"), "sharp"}, + {DSCT_ROUND, N_("Round"), "round"}, +}; +static const Util::EnumDataConverter<DynastrokeCappingType> DSCTConverter(DynastrokeCappingTypeData, DSCT_END); + +LPEDynastroke::LPEDynastroke(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + method(_("Method:"), _("Choose pen type"), "method", DSMethodConverter, &wr, this, DSM_THICKTHIN_FAST), + width(_("Pen width:"), _("Maximal stroke width"), "width", &wr, this, 25), + roundness(_("Pen roundness:"), _("Min/Max width ratio"), "roundness", &wr, this, .2), + angle(_("Angle:"), _("direction of thickest strokes (opposite = thinnest)"), "angle", &wr, this, 45), +// modulo_pi(_("modulo pi"), _("Give forward and backward moves in one direction the same thickness "), "modulo_pi", &wr, this, false), + start_cap(_("Start:"), _("Choose start capping type"), "start_cap", DSCTConverter, &wr, this, DSCT_SHARP), + end_cap(_("End:"), _("Choose end capping type"), "end_cap", DSCTConverter, &wr, this, DSCT_SHARP), + growfor(_("Grow for:"), _("Make the stroke thinner near it's start"), "growfor", &wr, this, 100), + fadefor(_("Fade for:"), _("Make the stroke thinner near it's end"), "fadefor", &wr, this, 100), + round_ends(_("Round ends"), _("Strokes end with a round end"), "round_ends", &wr, this, false), + capping(_("Capping:"), _("left capping"), "capping", &wr, this, "M 100,5 C 50,5 0,0 0,0 0,0 50,-5 100,-5") +{ + + registerParameter(&method); + registerParameter(&width); + registerParameter(&roundness); + registerParameter(&angle); + //registerParameter(&modulo_pi) ); + registerParameter(&start_cap); + registerParameter(&growfor); + registerParameter(&end_cap); + registerParameter(&fadefor); + registerParameter(&round_ends); + registerParameter(&capping); + + width.param_set_range(0,std::numeric_limits<double>::max()); + roundness.param_set_range(0.01, 1); + angle.param_set_range(-360, 360); + growfor.param_set_range(0, std::numeric_limits<double>::max()); + fadefor.param_set_range(0, std::numeric_limits<double>::max()); + + show_orig_path = true; +} + +LPEDynastroke::~LPEDynastroke() += default; + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEDynastroke::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + +// std::cout<<"do effect: debut\n"; + + Piecewise<D2<SBasis> > output; + Piecewise<D2<SBasis> > m = pwd2_in; + Piecewise<D2<SBasis> > v = derivative(m);; + Piecewise<D2<SBasis> > n = unitVector(v); + n = rot90(n); + Piecewise<D2<SBasis> > n1,n2; + +// for (unsigned i=0; i<n.size(); i++){ +// std::cout<<n[i][X]<<"\n"; +// } +// return m + unitVector(v); + +#if 0 + Piecewise<SBasis> k = curvature(m); + OptInterval mag = bounds_exact(k); + //TODO test if mag is non empty... + k = (k-mag->min())*width/mag->extent() + (roundness*width); + Piecewise<D2<SBasis> > left = m + k*n; + Piecewise<D2<SBasis> > right = m - k*n; + right = compose(right,Linear(right.cuts.back(),right.cuts.front())); + D2<SBasis> line; + line[X] = Linear(left.lastValue()[X],right.firstValue()[X]); + line[Y] = Linear(left.lastValue()[Y],right.firstValue()[Y]); + output = left; + output.concat(Piecewise<D2<SBasis> >(line)); + output.concat(right); + line[X] = Linear(right.lastValue()[X],left.firstValue()[X]); + line[Y] = Linear(right.lastValue()[Y],left.firstValue()[Y]); + output.concat(Piecewise<D2<SBasis> >(line)); + return output; +#else + + double angle_rad = angle*M_PI/180.;//TODO: revert orientation?... + Piecewise<SBasis> w; + + // std::vector<double> corners = find_corners(m); + + DynastrokeMethod stroke_method = method.get_value(); + if (roundness==1.) { +// std::cout<<"round pen.\n"; + n1 = n*double(width); + n2 =-n1; + }else{ + switch(stroke_method) { + case DSM_ELLIPTIC_PEN:{ +// std::cout<<"ellptic pen\n"; + //FIXME: roundness=0??? + double c = cos(angle_rad), s = sin(angle_rad); + Affine rot,slant; + rot = Affine(c, -s, s, c, 0, 0 ); + slant = Affine(double(width)*roundness, 0, 0, double(width), 0, 0 ); + Piecewise<D2<SBasis> > nn = unitVector(v * ( rot * slant ) ); + slant = Affine( 0,-roundness, 1, 0, 0, 0 ); + rot = Affine(-s, -c, c, -s, 0, 0 ); + nn = nn * (slant * rot ); + + n1 = nn*double(width); + n2 =-n1; + break; + } + case DSM_THICKTHIN_FAST:{ +// std::cout<<"fast thick thin pen\n"; + D2<Piecewise<SBasis> > n_xy = make_cuts_independent(n); + w = n_xy[X]*sin(angle_rad) - n_xy[Y]*cos(angle_rad); + w = w * ((1 - roundness)*width/2.) + ((1 + roundness)*width/2.); + n1 = w*n; + n2 = -n1; + break; + } + case DSM_THICKTHIN_SLOW:{ +// std::cout<<"slow thick thin pen\n"; + D2<Piecewise<SBasis> > n_xy = make_cuts_independent(n); + w = n_xy[X]*cos(angle_rad)+ n_xy[Y]*sin(angle_rad); + w = w * ((1 - roundness)*width/2.) + ((1 + roundness)*width/2.); + //->Slower and less stable, but more accurate . + // General formula: n1 = w*u with ||u||=1 and u.v = -dw/dt + Piecewise<SBasis> dw = derivative(w); + Piecewise<SBasis> ncomp = sqrt(dot(v,v)-dw*dw,.1,3); + //FIXME: is force continuity useful? compatible with corners? +// std::cout<<"ici\n"; + n1 = -dw*v + ncomp*rot90(v); + n1 = w*force_continuity(unitVector(n1),.1); + n2 = -dw*v - ncomp*rot90(v); + n2 = w*force_continuity(unitVector(n2),.1); +// std::cout<<"ici2\n"; + break; + } + default:{ + n1 = n*double(width); + n2 = n1*(-.5); + break; + } + }//case + }//if/else + + // + //TODO: insert relevant stitch at each corner!! + // + + Piecewise<D2<SBasis> > left, right; + if ( m.segs.front().at0() == m.segs.back().at1()){ + // if closed: +// std::cout<<"closed input.\n"; + left = m + n1;//+ n; + right = m + n2;//- n; + } else { + //if not closed, shape the ends: + //TODO: allow fancy ends... +// std::cout<<"shaping the ends\n"; + double grow_length = growfor;// * width; + double fade_length = fadefor;// * width; + Piecewise<SBasis > s = arcLengthSb(m); + double totlength = s.segs.back().at1(); + + //scale factor for a sharp start + SBasis join = SBasis(2,Linear(0,1)); + join[1] = Linear(1,1); + Piecewise<SBasis > factor_in = Piecewise<SBasis >(join); + factor_in.cuts[1]=grow_length; + if (grow_length < totlength){ + factor_in.concat(Piecewise<SBasis >(Linear(1))); + factor_in.cuts[2]=totlength; + } +// std::cout<<"shaping the ends ici\n"; + //scale factor for a sharp end + join[0] = Linear(1,0); + join[1] = Linear(1,1); + Piecewise<SBasis > factor_out; + if (fade_length < totlength){ + factor_out = Piecewise<SBasis >(Linear(1)); + factor_out.cuts[1] = totlength-fade_length; + factor_out.concat(Piecewise<SBasis >(join)); + factor_out.cuts[2] = totlength; + }else{ + factor_out = Piecewise<SBasis >(join); + factor_out.setDomain(Interval(totlength-fade_length,totlength)); + } +// std::cout<<"shaping the ends ici ici\n"; + + Piecewise<SBasis > factor = factor_in*factor_out; + n1 = compose(factor,s)*n1; + n2 = compose(factor,s)*n2; + + left = m + n1; + right = m + n2; +// std::cout<<"shaping the ends ici ici ici\n"; + + if (start_cap.get_value() == DSCT_ROUND){ +// std::cout<<"shaping round start\n"; + SBasis tau(2,Linear(0)); + tau[1] = Linear(-1,0); + Piecewise<SBasis > hbump; + hbump.concat(Piecewise<SBasis >(tau*grow_length)); + hbump.concat(Piecewise<SBasis >(Linear(0))); + hbump.cuts[0]=0; + hbump.cuts[1]=fmin(grow_length,totlength*grow_length/(grow_length+fade_length)); + hbump.cuts[2]=totlength; + hbump = compose(hbump,s); + + left += - hbump * rot90(n); + right += - hbump * rot90(n); + } + if (end_cap.get_value() == DSCT_ROUND){ +// std::cout<<"shaping round end\n"; + SBasis tau(2,Linear(0)); + tau[1] = Linear(0,1); + Piecewise<SBasis > hbump; + hbump.concat(Piecewise<SBasis >(Linear(0))); + hbump.concat(Piecewise<SBasis >(tau*fade_length)); + hbump.cuts[0]=0; + hbump.cuts[1]=fmax(totlength-fade_length, totlength*grow_length/(grow_length+fade_length)); + hbump.cuts[2]=totlength; + hbump = compose(hbump,s); + + left += - hbump * rot90(n); + right += - hbump * rot90(n); + } + } + + left = force_continuity(left); + right = force_continuity(right); + +// std::cout<<"gathering result: left"; + output = left; +// std::cout<<" + reverse(right)"; + output.concat(reverse(right)); +// std::cout<<". done\n"; + +//----------- + return output; +#endif +} + + +/* ######################## */ + +} //namespace LivePathEffect (setq default-directory "c:/Documents And Settings/jf/Mes Documents/InkscapeSVN") +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-dynastroke.h b/src/live_effects/lpe-dynastroke.h new file mode 100644 index 0000000..ae1c511 --- /dev/null +++ b/src/live_effects/lpe-dynastroke.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_DYNASTROKE_H +#define INKSCAPE_LPE_DYNASTROKE_H + +/** \file + * LPE <dynastroke> implementation, see lpe-dynastroke.cpp. + */ + +/* + * Authors: + * JFB, but derived from Johan Engelen! + * + * Copyright (C) JF Barraud 2008 <jf.barraud@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum DynastrokeMethod { + DSM_ELLIPTIC_PEN = 0, + DSM_THICKTHIN_FAST, + DSM_THICKTHIN_SLOW, + DSM_END // This must be last +}; +enum DynastrokeCappingType { + DSCT_SHARP = 0, + DSCT_ROUND, + //DSCT_CUSTOM, + DSCT_END // This must be last +}; + + +class LPEDynastroke : public Effect { +public: + LPEDynastroke(LivePathEffectObject *lpeobject); + ~LPEDynastroke() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + +private: + EnumParam<DynastrokeMethod> method; + ScalarParam width; + ScalarParam roundness; + ScalarParam angle; + //BoolParam modulo_pi; + EnumParam<DynastrokeCappingType> start_cap; + EnumParam<DynastrokeCappingType> end_cap; + ScalarParam growfor; + ScalarParam fadefor; + BoolParam round_ends; + PathParam capping; + + LPEDynastroke(const LPEDynastroke&) = delete; + LPEDynastroke& operator=(const LPEDynastroke&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-ellipse_5pts.cpp b/src/live_effects/lpe-ellipse_5pts.cpp new file mode 100644 index 0000000..29288f8 --- /dev/null +++ b/src/live_effects/lpe-ellipse_5pts.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Ellipse through 5 points" implementation + */ + +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-ellipse_5pts.h" +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/path-sink.h> +#include "inkscape.h" +#include "desktop.h" +#include "message-stack.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEEllipse5Pts::LPEEllipse5Pts(LivePathEffectObject *lpeobject) : + Effect(lpeobject) +{ + //perceived_path = true; +} + +LPEEllipse5Pts::~LPEEllipse5Pts() += default; + +static double _det3(double (*mat)[3]) +{ + for (int i = 0; i < 2; i++) + { + for (int j = i + 1; j < 3; j++) + { + for (int k = i + 1; k < 3; k++) + { + mat[j][k] = (mat[j][k] * mat[i][i] - mat[j][i] * mat[i][k]); + if (i) mat[j][k] /= mat[i-1][i-1]; + } + } + } + return mat[2][2]; +} +static double _det5(double (*mat)[5]) +{ + for (int i = 0; i < 4; i++) + { + for (int j = i + 1; j < 5; j++) + { + for (int k = i + 1; k < 5; k++) + { + mat[j][k] = (mat[j][k] * mat[i][i] - mat[j][i] * mat[i][k]); + if (i) mat[j][k] /= mat[i-1][i-1]; + } + } + } + return mat[4][4]; +} + +Geom::PathVector +LPEEllipse5Pts::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out = Geom::PathVector(); + + if (path_in[0].size() < 4) { + + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Five points required for constructing an ellipse")); + return path_in; + } + // we assume that the path has >= 3 nodes + Geom::Point A = path_in[0].initialPoint(); + Geom::Point B = path_in[0].pointAt(1); + Geom::Point C = path_in[0].pointAt(2); + Geom::Point D = path_in[0].pointAt(3); + Geom::Point E = path_in[0].pointAt(4); + + using namespace Geom; + + double rowmajor_matrix[5][6] = + { + {A.x()*A.x(), A.x()*A.y(), A.y()*A.y(), A.x(), A.y(), 1}, + {B.x()*B.x(), B.x()*B.y(), B.y()*B.y(), B.x(), B.y(), 1}, + {C.x()*C.x(), C.x()*C.y(), C.y()*C.y(), C.x(), C.y(), 1}, + {D.x()*D.x(), D.x()*D.y(), D.y()*D.y(), D.x(), D.y(), 1}, + {E.x()*E.x(), E.x()*E.y(), E.y()*E.y(), E.x(), E.y(), 1} + }; + + double mat_a[5][5] = + { + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_b[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_c[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_d[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_e[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][5], rowmajor_matrix[1][5], rowmajor_matrix[2][5], rowmajor_matrix[3][5], rowmajor_matrix[4][5]} + }; + double mat_f[5][5] = + { + {rowmajor_matrix[0][0], rowmajor_matrix[1][0], rowmajor_matrix[2][0], rowmajor_matrix[3][0], rowmajor_matrix[4][0]}, + {rowmajor_matrix[0][1], rowmajor_matrix[1][1], rowmajor_matrix[2][1], rowmajor_matrix[3][1], rowmajor_matrix[4][1]}, + {rowmajor_matrix[0][2], rowmajor_matrix[1][2], rowmajor_matrix[2][2], rowmajor_matrix[3][2], rowmajor_matrix[4][2]}, + {rowmajor_matrix[0][3], rowmajor_matrix[1][3], rowmajor_matrix[2][3], rowmajor_matrix[3][3], rowmajor_matrix[4][3]}, + {rowmajor_matrix[0][4], rowmajor_matrix[1][4], rowmajor_matrix[2][4], rowmajor_matrix[3][4], rowmajor_matrix[4][4]} + }; + + double a1 = _det5(mat_a); + double b1 = -_det5(mat_b); + double c1 = _det5(mat_c); + double d1 = -_det5(mat_d); + double e1 = _det5(mat_e); + double f1 = -_det5(mat_f); + + double mat_check[][3] = + { + {a1, b1/2, d1/2}, + {b1/2, c1, e1/2}, + {d1/2, e1/2, f1} + }; + + if (_det3(mat_check) == 0 || a1*c1 - b1*b1/4 <= 0) { + SP_ACTIVE_DESKTOP->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("No ellipse found for specified points")); + return path_in; + } + + Geom::Ellipse el(a1, b1, c1, d1, e1, f1); + + double s, e; + double x0, y0, x1, y1, x2, y2, x3, y3; + double len; + + // figure out if we have a slice, guarding against rounding errors + + Geom::Path p(Geom::Point(cos(0), sin(0))); + + double end = 2 * M_PI; + for (s = 0; s < end; s += M_PI_2) { + e = s + M_PI_2; + if (e > end) + e = end; + len = 4*tan((e - s)/4)/3; + x0 = cos(s); + y0 = sin(s); + x1 = x0 + len * cos(s + M_PI_2); + y1 = y0 + len * sin(s + M_PI_2); + x3 = cos(e); + y3 = sin(e); + x2 = x3 + len * cos(e - M_PI_2); + y2 = y3 + len * sin(e - M_PI_2); + p.appendNew<Geom::CubicBezier>(Geom::Point(x1,y1), Geom::Point(x2,y2), Geom::Point(x3,y3)); + } + + Geom::Affine aff = Geom::Scale(el.rays()) * Geom::Rotate(el.rotationAngle()) * Geom::Translate(el.center()); + + path_out.push_back(p * aff); + + return path_out; +} + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-ellipse_5pts.h b/src/live_effects/lpe-ellipse_5pts.h new file mode 100644 index 0000000..843a9c4 --- /dev/null +++ b/src/live_effects/lpe-ellipse_5pts.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ELLIPSE_5PTS_H +#define INKSCAPE_LPE_ELLIPSE_5PTS_H + +/** \file + * LPE "Ellipse through 5 points" implementation + */ + +/* + * Authors: + * Theodore Janeczko + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEEllipse5Pts : public Effect { +public: + LPEEllipse5Pts(LivePathEffectObject *lpeobject); + ~LPEEllipse5Pts() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + LPEEllipse5Pts(const LPEEllipse5Pts&) = delete; + LPEEllipse5Pts& operator=(const LPEEllipse5Pts&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-embrodery-stitch-ordering.cpp b/src/live_effects/lpe-embrodery-stitch-ordering.cpp new file mode 100644 index 0000000..06129b2 --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch-ordering.cpp @@ -0,0 +1,1141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Sub-path Ordering functions for embroidery stitch LPE (Implementation) + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-embrodery-stitch-ordering.h" + +#include <numeric> + +namespace Inkscape { +namespace LivePathEffect { +namespace LPEEmbroderyStitchOrdering { + +using namespace Geom; + +// ==================== Debug Trace Macros ==================== + +// ATTENTION: both level and area macros must be enabled for tracing + +// These macros are for enabling certain levels of tracing +#define DebugTrace1(list) // g_warning list +#define DebugTrace2(list) // g_warning list + +// These macros are for enabling certain areas of tracing +#define DebugTraceGrouping(list) // list +#define DebugTraceTSP(list) // list + +// Combinations of above +#define DebugTrace1TSP(list) DebugTraceTSP( DebugTrace1(list) ) +#define DebugTrace2TSP(list) DebugTraceTSP( DebugTrace2(list) ) + +// ==================== Template Utilities ==================== + +// Delete all objects pointed to by a vector and clear the vector + +template< typename T > void delete_and_clear(std::vector<T> &vector) +{ + for (typename std::vector<T>::iterator it = vector.begin(); it != vector.end(); ++it) { + delete *it; + } + vector.clear(); +} + +// Assert that there are no duplicates in a vector + +template< typename T > void assert_unique(std::vector<T> &vector) +{ + typename std::vector<T> copy = vector; + std::sort(copy.begin(), copy.end()); + assert(std::unique(copy.begin(), copy.end()) == copy.end()); +} + +// remove element(s) by value + +template< typename T > void remove_by_value(std::vector<T> *vector, const T &value) +{ + vector->erase(std::remove(vector->begin(), vector->end(), value), vector->end()); +} + +// fill a vector with increasing elements (similar to C++11 iota) +// iota is included in some C++ libraries, not in other (it is always included for C++11) +// To avoid issues, use our own name (not iota) + +template<class OutputIterator, class Counter> +void fill_increasing(OutputIterator begin, OutputIterator end, Counter counter) +{ + while (begin != end) { + *begin++ = counter++; + } +} + +// check if an iterable sequence contains an element + +template<class InputIterator, class Element> +bool contains(InputIterator begin, InputIterator end, const Element &elem) +{ + while (begin != end) { + if (*begin == elem) { + return true; + } + ++begin; + } + return false; +} + +// Check if a vector contains an element + +template<class Element> +bool contains(std::vector<Element> const &vector, const Element &elem) +{ + return contains(vector.begin(), vector.end(), elem); +} + +// ==================== Multi-dimensional iterator functions ==================== + +// Below are 3 simple template functions to do triangle/pyramid iteration (without diagonal). +// Here is a sample of iterating over 5 elements in 3 dimensions: +// +// 0 1 2 +// 0 1 3 +// 0 1 4 +// 0 2 3 +// 0 2 4 +// 1 2 4 +// 1 3 4 +// 2 3 4 +// end end end +// +// If the number of elements is less then the number of dimensions, the number of dimensions is reduced automatically. +// +// I thought about creating an iterator class for this, but it doesn't match that well, so I used functions on iterator vectors. + +// Initialize a vector of iterators + +template<class Iterator> +void triangleit_begin(std::vector<Iterator> &iterators, Iterator const &begin, Iterator const &end, size_t n) +{ + iterators.clear(); + // limit number of dimensions to number of elements + size_t n1 = end - begin; + n = std::min(n, n1); + if (n) { + iterators.push_back(begin); + for (int i = 1; i < n; i++) { + iterators.push_back(iterators.back() + 1); + } + } +} + +// Increment a vector of iterators + +template<class Iterator> +void triangleit_incr(std::vector<Iterator> &iterators, Iterator const &end) +{ + size_t n = iterators.size(); + + for (int i = 0; i < n; i++) { + iterators[n - 1 - i]++; + // Each dimension ends at end-i, so that there are elements left for the i higher dimensions + if (iterators[n - 1 - i] != end - i) { + // Assign increasing numbers to the higher dimension + for (int j = n - i; j < n; j++) { + iterators[j] = iterators[j - 1] + 1; + } + return; + } + } +} + +// Check if a vector of iterators is at the end + +template<class Iterator> +bool triangleit_test(std::vector<Iterator> &iterators, Iterator const &end) +{ + if (iterators.empty()) { + return false; + } else { + return iterators.back() != end; + } +} + +// ==================== Trivial Ordering Functions ==================== + +// Sub-path reordering: do nothing - keep original order + +void OrderingOriginal(std::vector<OrderingInfo> &infos) +{ +} + +// Sub-path reordering: reverse every other sub path + +void OrderingZigZag(std::vector<OrderingInfo> &infos, bool revfirst) +{ + for (auto & info : infos) { + info.reverse = (info.index & 1) == (revfirst ? 0 : 1); + } +} + +// Sub-path reordering: continue with the neartest start or end point of yet unused sub paths + +void OrderingClosest(std::vector<OrderingInfo> &infos, bool revfirst) +{ + std::vector<OrderingInfo> result; + result.reserve(infos.size()); + + result.push_back(infos[0]); + result.back().reverse = revfirst; + Point p = result.back().GetEndRev(); + + infos[0].used = true; + + + for (unsigned int iRnd = 1; iRnd < infos.size(); iRnd++) { + // find closest point to p + unsigned iBest = 0; + bool revBest = false; + Coord distBest = Geom::infinity(); + + for (std::vector<OrderingInfo>::iterator it = infos.begin(); it != infos.end(); ++it) { + it->index = it - infos.begin(); + it->reverse = (it->index & 1) != 0; + + if (!it->used) { + Coord dist = distance(p, it->GetBegOrig()); + if (dist < distBest) { + distBest = dist; + iBest = it - infos.begin(); + revBest = false; + } + + dist = distance(p, it->GetEndOrig()); + if (dist < distBest) { + distBest = dist; + iBest = it - infos.begin(); + revBest = true; + } + } + } + + result.push_back(infos[iBest]); + result.back().reverse = revBest; + p = result.back().GetEndRev(); + infos[iBest].used = true; + } + + infos = result; +} + +// ==================== Traveling Salesman k-opt Ordering Function and Utilities ==================== + +// A Few notes on this: +// - This is a relatively simple Lin-type k-opt algorithm, but the grouping optimizations done make it already quite complex. +// - The main Ordering Function is OrderingAdvanced +// - Lines which start at the end of another line are connected and treated as one (struct OrderingInfoEx) +// - Groups of zig-zag OrderingInfoEx are grouped (struct OrderingGroup) if both ends of the segment mutually agree with a next neighbor. +// These groups are treated as a unit in the TSP algorithm. +// The only option is to reverse the first segment, so that a group has 4 end points, 2 of which are used externally. +// - Run a k-opt (k=2..5) Lin like Traveling Salesman Problem algorithm on the groups as a unit and the remaining edges. +// See https://en.wikipedia.org/wiki/Travelling_salesman_problem#Iterative_improvement +// The algorithm uses a greedy nearest neighbor as start configuration and does not use repeated random starts. +// - The algorithm searches an open tour (rather than a closed one), so the longest segment in the closed path is ignored. +// - TODO: it might be faster to use k=3 with a few random starting patterns instead of k=5 +// - TODO: it is surely wiser to implement e.g. Lin-Kenrighan TSP, but the simple k-opt works ok. +// - TODO(EASY): add a jump distance, above which threads are removed and make the length of this jump distance constant and large, +// so that mostly the number of jumps is optimized + +// Find 2 nearest points to given point + +void OrderingPoint::FindNearest2(const std::vector<OrderingInfoEx *> &infos) +{ + // This implementation is not terribly elegant (unSTLish). + // But for the first 2 elements using e.g. partial_sort is not simpler. + + Coord dist0 = Geom::infinity(); + Coord dist1 = Geom::infinity(); + nearest[0] = nullptr; + nearest[1] = nullptr; + + for (auto info : infos) { + Coord dist = distance(point, info->beg.point); + if (dist < dist1) { + if (&info->beg != this && &info->end != this) { + if (dist < dist0) { + nearest[1] = nearest[0]; + nearest[0] = &info->beg; + dist1 = dist0; + dist0 = dist; + } else { + nearest[1] = &info->beg; + dist1 = dist; + } + } + } + + dist = distance(point, info->end.point); + if (dist < dist1) { + if (&info->beg != this && &info->end != this) { + if (dist < dist0) { + nearest[1] = nearest[0]; + nearest[0] = &info->end; + dist1 = dist0; + dist0 = dist; + } else { + nearest[1] = &info->end; + dist1 = dist; + } + } + } + } +} + +// Check if "this" is among the nearest of its nearest + +void OrderingPoint::EnforceMutual() +{ + if (nearest[0] && !(this == nearest[0]->nearest[0] || this == nearest[0]->nearest[1])) { + nearest[0] = nullptr; + } + + if (nearest[1] && !(this == nearest[1]->nearest[0] || this == nearest[1]->nearest[1])) { + nearest[1] = nullptr; + } + + if (nearest[1] && !nearest[0]) { + nearest[0] = nearest[1]; + nearest[1] = nullptr; + } +} + +// Check if the subpath indices of this and other are the same, otherwise zero both nearest + +void OrderingPoint::EnforceSymmetric(const OrderingPoint &other) +{ + if (nearest[0] && !( + (other.nearest[0] && nearest[0]->infoex == other.nearest[0]->infoex) || + (other.nearest[1] && nearest[0]->infoex == other.nearest[1]->infoex) + )) { + nearest[0] = nullptr; + } + + if (nearest[1] && !( + (other.nearest[0] && nearest[1]->infoex == other.nearest[0]->infoex) || + (other.nearest[1] && nearest[1]->infoex == other.nearest[1]->infoex) + )) { + nearest[1] = nullptr; + } + + if (nearest[1] && !nearest[0]) { + nearest[0] = nearest[1]; + nearest[1] = nullptr; + } +} + +void OrderingPoint::Dump() +{ + // COMMENTED TO SUPPRESS WARNING UNUSED AUTHOR TAKE IT UNCOMMENTED + // Coord dist0 = nearest[0] ? distance(point, nearest[0]->point) : -1.0; + // Coord dist1 = nearest[1] ? distance(point, nearest[1]->point) : -1.0; + // int idx0 = nearest[0] ? nearest[0]->infoex->idx : -1; + // int idx1 = nearest[1] ? nearest[1]->infoex->idx : -1; + DebugTrace2(("I=%d X=%.5lf Y=%.5lf d1=%.3lf d2=%.3lf i1=%d i2=%d", infoex->idx, point.x(), 297.0 - point.y(), dist0, dist1, idx0, idx1)); +} + + +// If this element can be grouped (has neighbours) but is not yet grouped, create a new group + +void OrderingInfoEx::MakeGroup(std::vector<OrderingInfoEx *> &infos, std::vector<OrderingGroup *> *groups) +{ + if (grouped || !beg.HasNearest() || !end.HasNearest()) { + return; + } + + groups->push_back(new OrderingGroup(groups->size())); + + // Add neighbors recursively + AddToGroup(infos, groups->back()); +} + +// Add this and all connected elements to the group + +void OrderingInfoEx::AddToGroup(std::vector<OrderingInfoEx *> &infos, OrderingGroup *group) +{ + if (grouped || !beg.HasNearest() || !end.HasNearest()) { + return; + } + + group->items.push_back(this); + grouped = true; + // Note: beg and end neighbors have been checked to be symmetric + if (beg.nearest[0]) { + beg.nearest[0]->infoex->AddToGroup(infos, group); + } + if (beg.nearest[1]) { + beg.nearest[1]->infoex->AddToGroup(infos, group); + } + if (end.nearest[0]) { + end.nearest[0]->infoex->AddToGroup(infos, group); + } + if (end.nearest[1]) { + end.nearest[1]->infoex->AddToGroup(infos, group); + } +} + +// Constructor + +OrderingGroupNeighbor::OrderingGroupNeighbor(OrderingGroupPoint *me, OrderingGroupPoint *other) : + point(other), + distance(Geom::distance(me->point, other->point)) +{ +} + +// Comparison function for sorting by distance + +bool OrderingGroupNeighbor::Compare(const OrderingGroupNeighbor &a, const OrderingGroupNeighbor &b) +{ + return a.distance < b.distance; +} + +// Find the nearest unused neighbor point + +OrderingGroupNeighbor *OrderingGroupPoint::FindNearestUnused() +{ + for (auto & it : nearest) { + if (!it.point->used) { + DebugTrace1TSP(("Nearest: group %d, size %d, point %d, nghb %d, xFrom %.4lf, yFrom %.4lf, xTo %.4lf, yTo %.4lf, dist %.4lf", + it->point->group->index, it->point->group->items.size(), it->point->indexInGroup, it - nearest.begin(), + point.x(), 297 - point.y(), + it->point->point.x(), 297 - it->point->point.y(), + it->distance)); + return ⁢ + } + } + + // it shouldn't happen that we can't find any point at all + assert(0); + return nullptr; +} + +// Return the other end in the group of the point + +OrderingGroupPoint *OrderingGroupPoint::GetOtherEndGroup() +{ + return group->endpoints[ indexInGroup ^ 1 ]; +} + +// Return the alternate point (if one exists), 0 otherwise + +OrderingGroupPoint *OrderingGroupPoint::GetAltPointGroup() +{ + if (group->nEndPoints < 4) { + return nullptr; + } + + OrderingGroupPoint *alt = group->endpoints[ indexInGroup ^ 2 ]; + return alt->used ? nullptr : alt; +} + + +// Sets the rev flags in the group assuming that the group starts with this point + +void OrderingGroupPoint::SetRevInGroup() +{ + // If this is not a front point, the item list needs to be reversed + group->revItemList = !front; + + // If this is not a begin point, the items need to be reversed + group->revItems = !begin; +} + +// Mark an end point as used and also mark the two other alternating points as used +// Returns the used point + +void OrderingGroupPoint::UsePoint() +{ + group->UsePoint(indexInGroup); +} + +// Mark an end point as unused and possibly also mark the two other alternating points as unused +// Returns the used point + +void OrderingGroupPoint::UnusePoint() +{ + group->UnusePoint(indexInGroup); +} + +// Return the other end in the connection +OrderingGroupPoint *OrderingGroupPoint::GetOtherEndConnection() +{ + assert(connection); + assert(connection->points[ indexInConnection ] == this); + assert(connection->points[ indexInConnection ^ 1 ]); + + return connection->points[ indexInConnection ^ 1 ]; +} + + +// Set the end points of a group from the items + +void OrderingGroup::SetEndpoints() +{ + assert(items.size() >= 1); + + if (items.size() == 1) { + // A simple line: + // + // b0-front--e1 + + nEndPoints = 2; + endpoints[0] = new OrderingGroupPoint(items.front()->beg.point, this, 0, true, true); + endpoints[1] = new OrderingGroupPoint(items.front()->end.point, this, 1, false, true); + } else { + // If the number of elements is even, the group is + // either from items.front().beg to items.back().beg + // or from items.front().end to items.back().end: + // Below: b=beg, e=end, numbers are end point indices + // + // b0-front--e b0-front--e2 + // | | + // b---------e b---------e + // | | + // b---------e b---------e + // | | + // b1-back---e b1-back---e3 + // + // + // if the number of elements is odd, it is crossed: + // + // b0-front--e b--front--e2 + // | | + // b---------e b---------e + // | | + // b--back---e1 b3-back---e + // + // TODO: this is not true with the following kind of pattern + // + // b--front--e + // b---------e + // b--------e + // b--back--e + // + // Here only one connection is possible, from front.end to back.beg + // + // TODO: also this is not true if segment direction is alternating + // + // TOTO: => Just see where you end up from front().begin and front().end + // + // the numbering is such that either end points 0 and 1 are used or 2 and 3. + int cross = items.size() & 1 ? 2 : 0; + nEndPoints = 4; + + endpoints[0 ] = new OrderingGroupPoint(items.front()->beg.point, this, 0, true, true); + endpoints[1 ^ cross] = new OrderingGroupPoint(items.back() ->beg.point, this, 1 ^ cross, true, false); + endpoints[2 ] = new OrderingGroupPoint(items.front()->end.point, this, 2, false, true); + endpoints[3 ^ cross] = new OrderingGroupPoint(items.back() ->end.point, this, 3 ^ cross, false, false); + } +} + +// Add all points from another group as neighbors + +void OrderingGroup::AddNeighbors(OrderingGroup *nghb) +{ + for (int iThis = 0; iThis < nEndPoints; iThis++) { + for (int iNghb = 0; iNghb < nghb->nEndPoints; iNghb++) { + endpoints[iThis]->nearest.emplace_back(endpoints[iThis], nghb->endpoints[iNghb]); + } + } +} + +// Mark an end point as used and also mark the two other alternating points as used +// Returns the used point + +OrderingGroupPoint *OrderingGroup::UsePoint(int index) +{ + assert(index < nEndPoints); + assert(!endpoints[index]->used); + endpoints[index]->used = true; + if (nEndPoints == 4) { + int offs = index < 2 ? 2 : 0; + endpoints[0 + offs]->used = true; + endpoints[1 + offs]->used = true; + } + + return endpoints[index]; +} + +// Mark an end point as unused and possibly also mark the two other alternating points as unused +// Returns the used point + +void OrderingGroup::UnusePoint(int index) +{ + assert(index < nEndPoints); + assert(endpoints[index]->used); + endpoints[index]->used = false; + + if (nEndPoints == 4 && !endpoints[index ^ 1]->used) { + int offs = index < 2 ? 2 : 0; + endpoints[0 + offs]->used = false; + endpoints[1 + offs]->used = false; + } +} + +// Add an end point +void OrderingSegment::AddPoint(OrderingGroupPoint *point) +{ + assert(point); + assert(nEndPoints < 4); + endpoints[ nEndPoints++ ] = point; + + // If both ends of a group are added and the group has 4 points, add the other two as well + if (nEndPoints == 2 && endpoints[0]->group == endpoints[1]->group) { + OrderingGroup *group = endpoints[0]->group; + if (group->nEndPoints == 4) { + for (int i = 0; i < 4; i++) { + endpoints[i] = group->endpoints[i]; + } + nEndPoints = 4; + } + } +} + +// Get begin point (taking swap and end bit into account +OrderingGroupPoint *OrderingSegment::GetBeginPoint(unsigned int iSwap, unsigned int iEnd) +{ + int iPoint = ((iEnd >> endbit) & 1) + (((iSwap >> swapbit) & 1) << 1); + assert(iPoint < nEndPoints); + return endpoints[iPoint]; +} + +// Get end point (taking swap and end bit into account +OrderingGroupPoint *OrderingSegment::GetEndPoint(unsigned int iSwap, unsigned int iEnd) +{ + int iPoint = (((iEnd >> endbit) & 1) ^ 1) + (((iSwap >> swapbit) & 1) << 1); + assert(iPoint < nEndPoints); + return endpoints[iPoint]; +} + + +// Find the next unused point in list +std::vector<OrderingGroupPoint *>::iterator FindUnusedAndUse(std::vector<OrderingGroupPoint *> *unusedPoints, std::vector<OrderingGroupPoint *>::iterator const from) +{ + for (std::vector<OrderingGroupPoint *>::iterator it = from; it != unusedPoints->end(); ++it) { + if (!(*it)->used) { + (*it)->UsePoint(); + return it; + } + } + return unusedPoints->end(); +} + +// Find the shortest reconnect between the given points + +bool FindShortestReconnect(std::vector<OrderingSegment> &segments, std::vector<OrderingGroupConnection *> &connections, std::vector<OrderingGroupConnection *> &allconnections, OrderingGroupConnection **longestConnect, Coord *total, Coord olddist) +{ + // Find the longest connection outside of the active set + // The longest segment is then the longest of this longest outside segment and all inside segments + OrderingGroupConnection *longestOutside = nullptr; + + if (contains(connections, *longestConnect)) { + // The longest connection is inside the active set, so we need to search for the longest outside + Coord length = 0.0; + for (auto & allconnection : allconnections) { + if (allconnection->Distance() > length) { + if (!contains(connections, allconnection)) { + longestOutside = allconnection; + length = allconnection->Distance(); + } + } + } + } else { + longestOutside = *longestConnect; + } + + // length of longestConnect outside + Coord longestOutsideLength = longestOutside ? longestOutside->Distance() : 0.0; + + // We measure length without the longest, so subtract the longest length from the old distance + olddist -= (*longestConnect)->Distance(); + + // Assign a swap bit and end bit to each active connection + int nEndBits = 0; + int nSwapBits = 0; + for (auto & segment : segments) { + segment.endbit = nEndBits++; + if (segment.nEndPoints == 4) { + segment.swapbit = nSwapBits++; + } else { + // bit 32 should always be 0 + segment.swapbit = 31; + } + } + + unsigned int swapMask = (1U << nSwapBits) - 1; + unsigned int endMask = (1U << nEndBits) - 1; + + // Create a permutation vector + std::vector<int> permutation(segments.size()); + fill_increasing(permutation.begin(), permutation.end(), 0); + + // best improvement + bool improved = false; + Coord distBest = olddist; + std::vector<int> permutationBest; + unsigned int iSwapBest; + unsigned int iEndBest; + int nTrials = 0; + + // Loop over the permutations + do { + // Loop over the swap bits + unsigned int iSwap = 0; + do { + // Loop over the end bits + unsigned int iEnd = 0; + do { + // Length of all active connections + Coord lengthTotal = 0; + // Length of longest connection (active or inactive) + Coord lengthLongest = longestOutsideLength; + + // Close the loop with the end point of the last segment + OrderingGroupPoint *prevend = segments[permutation.back()].GetEndPoint(iSwap, iEnd); + for (int & it : permutation) { + OrderingGroupPoint *thisbeg = segments[it].GetBeginPoint(iSwap, iEnd); + Coord length = Geom::distance(thisbeg->point, prevend->point); + lengthTotal += length; + if (length > lengthLongest) { + lengthLongest = length; + } + prevend = segments[it].GetEndPoint(iSwap, iEnd); + } + lengthTotal -= lengthLongest; + + // If there is an improvement, remember the best selection + if (lengthTotal + 1e-6 < distBest) { + improved = true; + distBest = lengthTotal; + permutationBest = permutation; + iSwapBest = iSwap; + iEndBest = iEnd; + + // Just debug printing + OrderingGroupPoint *prevend = segments[permutation.back()].GetEndPoint(iSwap, iEnd); + for (int & it : permutation) { + // COMMENTED TO SUPPRESS WARNING UNUSED AUTHOR TAKE IT UNCOMMENTED + //OrderingGroupPoint *thisbeg = segments[it].GetBeginPoint(iSwap, iEnd); + DebugTrace2TSP(("IMP 0F=%d %d %.6lf", thisbeg->group->index, thisbeg->indexInGroup, Geom::distance(thisbeg->point, prevend->point))); + DebugTrace2TSP(("IMP 0T=%d %d %.6lf", prevend->group->index, prevend->indexInGroup, Geom::distance(thisbeg->point, prevend->point))); + prevend = segments[it].GetEndPoint(iSwap, iEnd); + } + } + + nTrials++; + + // bit 0 is always 0, because the first segment is kept fixed + iEnd += 2; + } while (iEnd & endMask); + iSwap++; + } while (iSwap & swapMask); + // first segment is kept fixed + } while (std::next_permutation(permutation.begin() + 1, permutation.end())); + + if (improved) { + DebugTrace2TSP(("Improvement %lf->%lf in %d", olddist, distBest, nTrials)); + // change the connections + + for (std::vector<OrderingGroupConnection *>::iterator it = connections.begin(); it != connections.end(); ++it) { + DebugTrace2TSP(("WAS 0F=%d %d %.6lf", (*it)->points[0]->group->index, (*it)->points[0]->indexInGroup, (*it)->Distance())); + DebugTrace2TSP(("WAS 0T=%d %d %.6lf", (*it)->points[1]->group->index, (*it)->points[1]->indexInGroup, (*it)->Distance())); + } + DebugTrace2TSP(("OLDDIST %.6lf delta %.6lf", olddist, olddist - (*longestConnect)->Distance())); + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[0]->group->index, (*longestConnect)->points[0]->indexInGroup, (*longestConnect)->Distance())); + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[1]->group->index, (*longestConnect)->points[1]->indexInGroup, (*longestConnect)->Distance())); + + int perm = permutationBest.back(); + + for (std::vector<OrderingGroupConnection *>::iterator it = connections.begin(); it != connections.end(); ++it) { + (*it)->Connect(1, segments[ perm ].GetEndPoint(iSwapBest, iEndBest)); + perm = permutationBest[ it - connections.begin() ]; + (*it)->Connect(0, segments[ perm ].GetBeginPoint(iSwapBest, iEndBest)); + + } + + for (std::vector<OrderingGroupConnection *>::iterator it = connections.begin(); it != connections.end(); ++it) { + DebugTrace2TSP(("IS 0F=%d %d %.6lf", (*it)->points[0]->group->index, (*it)->points[0]->indexInGroup, (*it)->Distance())); + DebugTrace2TSP(("IS 0T=%d %d %.6lf", (*it)->points[1]->group->index, (*it)->points[1]->indexInGroup, (*it)->Distance())); + } + + (*longestConnect) = longestOutside; + for (auto & connection : connections) { + if (connection->Distance() > (*longestConnect)->Distance()) { + *longestConnect = connection; + } + } + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[0]->group->index, (*longestConnect)->points[0]->indexInGroup, (*longestConnect)->Distance())); + DebugTrace2TSP(("LONG =%d %d %.6lf", (*longestConnect)->points[1]->group->index, (*longestConnect)->points[1]->indexInGroup, (*longestConnect)->Distance())); + } + + return improved; +} + +// Check if connections form a tour +void AssertIsTour(std::vector<OrderingGroup *> &groups, std::vector<OrderingGroupConnection *> &connections, OrderingGroupConnection *longestConnection) +{ + for (auto & connection : connections) { + for (auto pnt : connection->points) { + assert(pnt->connection == connection); + assert(pnt->connection->points[pnt->indexInConnection] == pnt); + assert(pnt->group->endpoints[pnt->indexInGroup] == pnt); + } + } + + Coord length1 = 0; + Coord longest1 = 0; + OrderingGroupPoint *current = connections.front()->points[0]; + + for (unsigned int n = 0; n < connections.size(); n++) { + DebugTrace2TSP(("Tour test 1 %p g=%d/%d c=%d/%d %p %p %.6lf %.3lf %.3lf %d %d %d", current, current->group->index, current->indexInGroup, current->connection->index, current->indexInConnection, current->connection->points[0], current->connection->points[1], current->connection->Distance(), current->point.x(), 297 - current->point.y(), current->begin, current->front, current->group->items.size())); + Coord length = current->connection->Distance(); + length1 += length; + longest1 = std::max(length, longest1); + current = current->GetOtherEndConnection(); + + DebugTrace2TSP(("Tour test 2 %p g=%d/%d c=%d/%d %p %p %.6lf %.3lf %.3lf %d %d %d", current, current->group->index, current->indexInGroup, current->connection->index, current->indexInConnection, current->connection->points[0], current->connection->points[1], current->connection->Distance(), current->point.x(), 297 - current->point.y(), current->begin, current->front, current->group->items.size())); + current = current->GetOtherEndGroup(); + } + DebugTrace2TSP(("Tour test 3 %p g=%d/%d c=%d/%d %p %p", current, current->group->index, current->indexInGroup, current->connection->index, current->indexInConnection, current->connection->points[0], current->connection->points[1])); + assert(current == connections.front()->points[0]); + + // The other direction + Coord length2 = 0; + Coord longest2 = 0; + current = connections.front()->points[0]; + for (unsigned int n = 0; n < connections.size(); n++) { + current = current->GetOtherEndGroup(); + Coord length = current->connection->Distance(); + length2 += length; + longest2 = std::max(length, longest2); + current = current->GetOtherEndConnection(); + } + assert(current == connections.front()->points[0]); + + DebugTrace1TSP(("Tour length %.6lf(%.6lf) longest %.6lf(%.6lf) remaining %.6lf(%.6lf)", length1, length2, longest1, longest2, length1 - longest1, length2 - longest2)); +} + +// Bring a tour into linear order after a modification + +/* I would like to avoid this. + * It is no problem to travel a tour with changing directions using the GetOtherEnd functions, + * but it is difficult to know the segments, that is which endpoint of a connection is connected to which by the unmodified pieces of the tour. + * In the end it is probably better to implement the Lin-Kernighan algorithm which avoids this problem by creating connected changes. */ + +void LinearizeTour(std::vector<OrderingGroupConnection *> &connections) +{ + OrderingGroupPoint *current = connections.front()->points[0]; + + for (unsigned int iNew = 0; iNew < connections.size(); iNew++) { + // swap the connection at location n with the current connection + OrderingGroupConnection *connection = current->connection; + unsigned int iOld = connection->index; + assert(connections[iOld] == connection); + + connections[iOld] = connections[iNew]; + connections[iNew] = connection; + connections[iOld]->index = iOld; + connections[iNew]->index = iNew; + + // swap the points of a connection + assert(current == connection->points[0] || current == connection->points[1]); + if (current != connection->points[0]) { + connection->points[1] = connection->points[0]; + connection->points[0] = current; + connection->points[1]->indexInConnection = 1; + connection->points[0]->indexInConnection = 0; + } + + current = current->GetOtherEndConnection(); + current = current->GetOtherEndGroup(); + } +} + +// Use some Traveling Salesman Problem (TSP) like heuristics to bring several groups into a +// order with as short as possible interconnection paths + +void OrderGroups(std::vector<OrderingGroup *> *groups, const int nDims) +{ + // There is no point in ordering just one group + if (groups->size() <= 1) { + return; + } + + // Initialize the endpoints for all groups + for (auto & group : *groups) { + group->SetEndpoints(); + } + + // Find the neighboring points for all end points of all groups and sort by distance + for (std::vector<OrderingGroup *>::iterator itThis = groups->begin(); itThis != groups->end(); ++itThis) { + for (int i = 0; i < (*itThis)->nEndPoints; i++) { + // This can be up to 2x too large, but still better than incrementing the size + (*itThis)->endpoints[i]->nearest.reserve(4 * groups->size()); + } + + for (std::vector<OrderingGroup *>::iterator itNghb = groups->begin(); itNghb != groups->end(); ++itNghb) { + if (itThis != itNghb) { + (*itThis)->AddNeighbors(*itNghb); + } + } + + for (int i = 0; i < (*itThis)->nEndPoints; i++) { + std::sort((*itThis)->endpoints[i]->nearest.begin(), (*itThis)->endpoints[i]->nearest.end(), OrderingGroupNeighbor::Compare); + } + } + + // =========== Step 1: Create a simple nearest neighbor chain =========== + + // Vector of connection points + std::vector<OrderingGroupConnection *> connections; + connections.reserve(groups->size()); + // Total Jump Distance + Coord total = 0.0; + + // Start with the first group and connect always with nearest unused point + OrderingGroupPoint *crnt = groups->front()->endpoints[0]; + + // The longest connection is ignored (we don't want cycles) + OrderingGroupConnection *longestConnect = nullptr; + + for (unsigned int nConnected = 0; nConnected < groups->size(); nConnected++) { + // Mark both end points of the current segment as used + crnt->UsePoint(); + crnt = crnt->GetOtherEndGroup(); + crnt->UsePoint(); + + // if this is the last segment, Mark start point of first segment as unused, + // so that the end can connect to it + if (nConnected == groups->size() - 1) { + groups->front()->endpoints[0]->UnusePoint(); + } + + // connect to next segment + OrderingGroupNeighbor *nghb = crnt->FindNearestUnused(); + connections.push_back(new OrderingGroupConnection(crnt, nghb->point, connections.size())); + total += nghb->distance; + crnt = nghb->point; + + if (!longestConnect || nghb->distance > longestConnect->Distance()) { + longestConnect = connections.back(); + } + } + + DebugTrace1TSP(("Total jump distance %.3lf (closed)", total)); + DebugTrace1TSP(("Total jump distance %.3lf (open)", total - longestConnect->Distance())); + + AssertIsTour(*groups, connections, longestConnect); + + // =========== Step 2: Choose nDims segments to clear and reconnect =========== + + bool improvement; + int nRuns = 0; + int nTrials = 0; + int nImprovements = 0; + + do { + improvement = false; + nRuns ++; + std::vector< std::vector<OrderingGroupConnection *>::iterator > iterators; + + for ( + triangleit_begin(iterators, connections.begin(), connections.end(), nDims); + triangleit_test(iterators, connections.end()); + triangleit_incr(iterators, connections.end()) + ) { + nTrials ++; + + Coord dist = 0; + + std::vector<OrderingSegment> segments(iterators.size()); + std::vector<OrderingGroupConnection *> changedconnections; + changedconnections.reserve(3); + OrderingGroupConnection *prev = *iterators.back(); + + + for (size_t i = 0; i < iterators.size(); i++) { + dist += (*iterators[i])->Distance(); + segments[i].AddPoint(prev->points[1]); + segments[i].AddPoint((*iterators[i])->points[0]); + prev = *iterators[i]; + changedconnections.push_back(*iterators[i]); + } + + if (FindShortestReconnect(segments, changedconnections, connections, &longestConnect, &total, dist)) { + nImprovements ++; + + AssertIsTour(*groups, connections, longestConnect); + LinearizeTour(connections); + AssertIsTour(*groups, connections, longestConnect); + improvement = true; + } + } + } while (improvement && nRuns < 10); + + DebugTrace1TSP(("Finished after %d rounds, %d trials, %d improvements", nRuns, nTrials, nImprovements)); + + // =========== Step N: Create vector of groups from vector of connection points =========== + + std::vector<OrderingGroup *> result; + result.reserve(groups->size()); + + // Go through the groups starting with the longest connection (which is this way left out) + { + OrderingGroupPoint *current = longestConnect->points[1]; + + for (unsigned int n = 0; n < connections.size(); n++) { + result.push_back(current->group); + current->SetRevInGroup(); + current = current->GetOtherEndGroup(); + current = current->GetOtherEndConnection(); + } + } + + assert(result.size() == groups->size()); + assert_unique(result); + + delete_and_clear(connections); + + *groups = result; +} + +// Global optimization of path length + +void OrderingAdvanced(std::vector<OrderingInfo> &infos, int nDims) +{ + if (infos.size() < 3) { + return; + } + + // Create extended ordering info vector and copy data from normal ordering info + std::vector<OrderingInfoEx *> infoex; + infoex.reserve(infos.size()); + + for (std::vector<OrderingInfo>::const_iterator it = infos.begin(); it != infos.end();) { + // Note: This assumes that the index in the OrderingInfo matches the vector index! + infoex.push_back(new OrderingInfoEx(*it, infoex.size())); + ++it; + while (it != infos.end() && it->begOrig == infoex.back()->end.point) { + infoex.back()->end.point = it->endOrig; + infoex.back()->origIndices.push_back(it->index); + ++it; + } + } + + // Find closest 2 points for each point and enforce that 2nd nearest is not further away than 1.8xthe nearest + // If this is not the case, clear nearest and 2nd nearest point + for (std::vector<OrderingInfoEx *>::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.FindNearest2(infoex); + (*it)->end.FindNearest2(infoex); + } + + DebugTraceGrouping( + DebugTrace2(("STEP1")); + for (std::vector<OrderingInfoEx *>::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.Dump(); + (*it)->end.Dump(); + } + ) + + // Make sure the nearest points are mutual + for (auto & it : infoex) { + it->beg.EnforceMutual(); + it->end.EnforceMutual(); + } + + DebugTraceGrouping( + DebugTrace2(("STEP2")); + for (std::vector<OrderingInfoEx *>::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.Dump(); + (*it)->end.Dump(); + } + ) + + // Make sure the nearest points for begin and end lead to the same sub-path (same index) + for (auto & it : infoex) { + it->beg.EnforceSymmetric(it->end); + it->end.EnforceSymmetric(it->beg); + } + + DebugTraceGrouping( + DebugTrace2(("STEP3")); + for (std::vector<OrderingInfoEx *>::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->beg.Dump(); + (*it)->end.Dump(); + } + ) + + // The remaining nearest neighbors should be 100% non ambiguous, so group them + std::vector<OrderingGroup *> groups; + for (std::vector<OrderingInfoEx *>::iterator it = infoex.begin(); it != infoex.end(); ++it) { + (*it)->MakeGroup(infoex, &groups); + } + + // Create single groups for ungrouped lines + std::vector<OrderingInfo> result; + result.reserve(infos.size()); + int nUngrouped = 0; + for (auto & it : infoex) { + if (!it->grouped) { + groups.push_back(new OrderingGroup(groups.size())); + groups.back()->items.push_back(it); + nUngrouped++; + } + } + + DebugTraceGrouping( + DebugTrace2(("Ungrouped lines = %d", nUngrouped)); + DebugTrace2(("%d Groups found", groups.size())); + for (std::vector<OrderingGroup *>::iterator it = groups.begin(); it != groups.end(); ++it) { + DebugTrace2(("Group size %d", (*it)->items.size())); + } + ) + + // Order groups, so that the connection path gets shortest + OrderGroups(&groups, nDims); + + // Copy grouped lines to output + for (auto & group : groups) { + for (unsigned int iItem = 0; iItem < group->items.size(); iItem++) { + unsigned int iItemRev = group->revItemList ? group->items.size() - 1 - iItem : iItem; + OrderingInfoEx *item = group->items[iItemRev]; + + // If revItems is false, even items shall have reverse=false + // In this case ( ( iItem & 1 ) == 0 )== true, revItems=false, (true==false) == false + bool reverse = ((iItem & 1) == 0) == group->revItems; + if (!reverse) { + for (int & origIndice : item->origIndices) { + result.push_back(infos[origIndice]); + result.back().reverse = false; + } + } else { + for (std::vector<int>::reverse_iterator itOrig = item->origIndices.rbegin(); itOrig != item->origIndices.rend(); ++itOrig) { + result.push_back(infos[*itOrig]); + result.back().reverse = true; + } + } + } + result.back().connect = true; + } + + + delete_and_clear(groups); + delete_and_clear(infoex); + + infos = result; +} + +} // namespace LPEEmbroderyStitchOrdering +} // namespace LivePathEffect +} // namespace Inkscape diff --git a/src/live_effects/lpe-embrodery-stitch-ordering.h b/src/live_effects/lpe-embrodery-stitch-ordering.h new file mode 100644 index 0000000..9899f66 --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch-ordering.h @@ -0,0 +1,314 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Sub-path Ordering functions for embroidery stitch LPE + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_EMBRODERY_STITCH_ORDERING_H +#define INKSCAPE_LPE_EMBRODERY_STITCH_ORDERING_H + +#include "live_effects/effect.h" + +namespace Inkscape { +namespace LivePathEffect { +namespace LPEEmbroderyStitchOrdering { + +// Structure keeping information on the ordering and reversing of sub paths +// Used for simple ordering functions like zig-zag + +struct OrderingInfo { + int index; + bool reverse; + bool used; + bool connect; + Geom::Point begOrig; // begin point in original orientation + Geom::Point endOrig; // end point in original orientation + + Geom::Point GetBegOrig() const + { + return begOrig; + } + Geom::Point GetEndOrig() const + { + return endOrig; + } + Geom::Point GetBegRev() const + { + return reverse ? endOrig : begOrig; + } + Geom::Point GetEndRev() const + { + return reverse ? begOrig : endOrig; + } +}; + +// Structure for a path end-point in OrderingInfoEx. +// This keeps information about the two nearest neighbor points. + +struct OrderingInfoEx; + +struct OrderingPoint { + OrderingPoint(const Geom::Point &pointIn, OrderingInfoEx *infoexIn, bool beginIn) : + point(pointIn), + infoex(infoexIn), + begin(beginIn) + { + nearest[0] = nearest[1] = nullptr; + } + + // Check if both nearest values are valid + bool IsNearestValid() const + { + return nearest[0] && nearest[1]; + } + // Check if at least one nearest values are valid + bool HasNearest() const + { + return nearest[0] || nearest[1]; + } + // Find 2 nearest points to given point + void FindNearest2(const std::vector<OrderingInfoEx *> &infos); + // Check if "this" is among the nearest of its nearest + void EnforceMutual(); + // Check if the subpath indices of this and other are the same, otherwise zero both nearest + void EnforceSymmetric(const OrderingPoint &other); + // Dump point information + void Dump(); + + Geom::Point point; + OrderingInfoEx *infoex; + bool begin; + const OrderingPoint *nearest[2]; +}; + +// Structure keeping information on the ordering and reversing of sub paths +// Used for advanced ordering functions with block creation and Traveling Salesman Problem Optimization +// A OrderingInfoEx may contain several original sub-paths in case sub-paths are directly connected. +// Directly connected sub-paths happen e.g. after a slice boolean operation. + +struct OrderingGroup; + +struct OrderingInfoEx { + OrderingInfoEx(const OrderingInfo &infoIn, int idxIn) : + beg(infoIn.begOrig, this, true), + end(infoIn.endOrig, this, false), + idx(idxIn), + grouped(false) + { + origIndices.push_back(infoIn.index); + } + + // If this element can be grouped (has neighbours) but is not yet grouped, create a new group + void MakeGroup(std::vector<OrderingInfoEx *> &infos, std::vector<OrderingGroup *> *groups); + // Add this and all connected elements to the group + void AddToGroup(std::vector<OrderingInfoEx *> &infos, OrderingGroup *group); + + int idx; + bool grouped; // true if this element has been grouped + OrderingPoint beg; // begin point in original orientation + OrderingPoint end; // end point in original orientation + std::vector<int> origIndices; // Indices of the original OrderInfos (more than 1 if directly connected +}; + +// Neighbor information for OrderingGroupPoint - a distance and a OrderingGroupPoint + +struct OrderingGroupPoint; + +struct OrderingGroupNeighbor { + OrderingGroupNeighbor(OrderingGroupPoint *me, OrderingGroupPoint *other); + + // Distance from owner of this neighbor info + Geom::Coord distance; + // Neighbor point (which in turn contains a pointer to the neighbor group + OrderingGroupPoint *point; + + // Comparison function for sorting by distance + static bool Compare(const OrderingGroupNeighbor &a, const OrderingGroupNeighbor &b); +}; + +// An end point in an OrderingGroup, with nearest neighbor information + +struct OrderingGroupConnection; + +struct OrderingGroupPoint { + OrderingGroupPoint(const Geom::Point &pointIn, OrderingGroup *groupIn, int indexIn, bool beginIn, bool frontIn) : + point(pointIn), + group(groupIn), + indexInGroup(indexIn), + connection(nullptr), + indexInConnection(0), + begin(beginIn), + front(frontIn), + used(false) + { + } + + // Find the nearest unused neighbor point + OrderingGroupNeighbor *FindNearestUnused(); + // Return the other end in the group of the point + OrderingGroupPoint *GetOtherEndGroup(); + // Return the alternate point (if one exists), 0 otherwise + OrderingGroupPoint *GetAltPointGroup(); + // Sets the rev flags in the group assuming that the group starts with this point + void SetRevInGroup(); + // Mark an end point as used and also mark the two other alternating points as used + // Returns the used point + void UsePoint(); + // Mark an end point as unused and possibly also mark the two other alternating points as unused + // Returns the used point + void UnusePoint(); + // Return the other end in the connection + OrderingGroupPoint *GetOtherEndConnection(); + + // The coordinates of the point + Geom::Point point; + // The group to which the point belongs + OrderingGroup *group; + // The end-point index within the group + int indexInGroup; + // The connection, which connects this point + OrderingGroupConnection *connection; + // The end point index in the connection + int indexInConnection; + // True if this is a begin point (rather than an end point) + bool begin; + // True if this is a front point (rather than a back point) + bool front; + // True if the point is used/connected to another point + bool used; + // The nearest neighbors, to which this group end point may connect + std::vector<OrderingGroupNeighbor> nearest; +}; + +// A connection between two points/groups +struct OrderingGroupConnection { + OrderingGroupConnection(OrderingGroupPoint *fromIn, OrderingGroupPoint *toIn, int indexIn) : + index(indexIn) + { + assert(fromIn->connection == 0); + assert(toIn->connection == 0); + points[0] = nullptr; + points[1] = nullptr; + Connect(0, fromIn); + Connect(1, toIn); + } + + // Connect one of the connection endpoints to the given point + void Connect(int index, OrderingGroupPoint *point) + { + assert(point); + points[index] = point; + point->connection = this; + point->indexInConnection = index; + } + + // Get length of connection + Geom::Coord Distance() + { + return Geom::distance(points[0]->point, points[1]->point); + } + + OrderingGroupPoint *points[2]; + // index of connection in the connections vector (just for debugging) + int index; +}; + +// A group of OrderingInfoEx, which build a block in path interconnect length optimization. +// A block can have two sets of endpoints. +// If a block has 2 sets of endpoints, one can swap between the two sets. + +struct OrderingGroup { + OrderingGroup(int indexIn) : + nEndPoints(0), + revItemList(false), + revItems(false), + index(indexIn) + { + for (auto & endpoint : endpoints) { + endpoint = nullptr; + } + } + + ~OrderingGroup() + { + for (int i = 0; i < nEndPoints; i++) { + delete endpoints[i]; + } + } + + // Set the endpoints of a group from the items + void SetEndpoints(); + // Add all points from another group as neighbors + void AddNeighbors(OrderingGroup *nghb); + // Mark an end point as used and also mark the two other alternating points as used + // Returns the used point + OrderingGroupPoint *UsePoint(int index); + // Mark an end point as unused and possibly also mark the two other alternating points as unused + // Returns the used point + void UnusePoint(int index); + + // Items on the group + std::vector<OrderingInfoEx *> items; + // End points of the group + OrderingGroupPoint *endpoints[4]; + // Number of endpoints used (either 2 or 4) + int nEndPoints; + // Index of the group (just for debugging purposes) + int index; + // If true, the items in the group shall be output from back to front. + bool revItemList; + // If false, the individual items are output alternatingly normal-reversed + // If true, the individual items are output alternatingly reversed-normal + bool revItems; +}; + +// A segment is either a OrderingGroup or a series of groups and connections. +// Usually a segment has just 2 end points. +// If a segment is just one ordering group, it has the same number of end points as the ordering group +// A main difference between a segment and a group is that the segment does not own the end points. + +struct OrderingSegment { + OrderingSegment() : + nEndPoints(0), + endbit(0), + swapbit(0) + {} + + // Add an end point + void AddPoint(OrderingGroupPoint *point); + // Get begin point (taking swap and end bit into account + OrderingGroupPoint *GetBeginPoint(unsigned int iSwap, unsigned int iEnd); + // Get end point (taking swap and end bit into account + OrderingGroupPoint *GetEndPoint(unsigned int iSwap, unsigned int iEnd); + + // End points of the group + OrderingGroupPoint *endpoints[4]; + // Number of endpoints used (either 2 or 4) + int nEndPoints; + // bit index in the end counter + int endbit; + // bit index in the swap counter + int swapbit; +}; + + +// Sub-path reordering: do nothing - keep original order +void OrderingOriginal(std::vector<OrderingInfo> &infos); + +// Sub-path reordering: reverse every other sub path +void OrderingZigZag(std::vector<OrderingInfo> &infos, bool revfirst); + +// Sub-path reordering: continue with the neartest start or end point of yet unused sub paths +void OrderingClosest(std::vector<OrderingInfo> &infos, bool revfirst); + +// Global optimization of path length +void OrderingAdvanced(std::vector<OrderingInfo> &infos, int nDims); + +} //LPEEmbroderyStitchOrdering +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-embrodery-stitch.cpp b/src/live_effects/lpe-embrodery-stitch.cpp new file mode 100644 index 0000000..0ba5060 --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch.cpp @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Embroidery stitch live path effect (Implementation) + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/scalar.h" +#include <glibmm/i18n.h> + +#include "live_effects/lpe-embrodery-stitch.h" +#include "live_effects/lpe-embrodery-stitch-ordering.h" + +#include <2geom/path.h> +#include <2geom/piecewise.h> +#include <2geom/sbasis.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-to-bezier.h> + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; +using namespace LPEEmbroderyStitchOrdering; + +static const Util::EnumData<LPEEmbroderyStitch::order_method> OrderMethodData[LPEEmbroderyStitch::order_method_count] = { + // clang-format off + { LPEEmbroderyStitch::order_method_no_reorder, N_("no reordering"), "no-reorder" }, + { LPEEmbroderyStitch::order_method_zigzag, N_("zig-zag"), "zig-zag" }, + { LPEEmbroderyStitch::order_method_zigzag_rev_first, N_("zig-zag, reverse first"), "zig-zag-rev-first" }, + { LPEEmbroderyStitch::order_method_closest, N_("closest"), "closest" }, + { LPEEmbroderyStitch::order_method_closest_rev_first, N_("closest, reverse first"), "closest-rev-first" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_2, N_("traveling salesman 2-opt (fast, bad)"), "tsp-2opt" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_3, N_("traveling salesman 3-opt (fast, ok)"), "tsp-3opt" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_4, N_("traveling salesman 4-opt (seconds)"), "tsp-4opt" }, + { LPEEmbroderyStitch::order_method_tsp_kopt_5, N_("traveling salesman 5-opt (minutes)"), "tsp-5opt" } + // clang-format on +}; + +static const Util::EnumDataConverter<LPEEmbroderyStitch::order_method> +OrderMethodConverter(OrderMethodData, sizeof(OrderMethodData) / sizeof(*OrderMethodData)); + +static const Util::EnumData<LPEEmbroderyStitch::connect_method> ConnectMethodData[LPEEmbroderyStitch::connect_method_count] = { + { LPEEmbroderyStitch::connect_method_line, N_("straight line"), "line" }, + { LPEEmbroderyStitch::connect_method_move_point_from, N_("move to begin"), "move-begin" }, + { LPEEmbroderyStitch::connect_method_move_point_mid, N_("move to middle"), "move-middle" }, + { LPEEmbroderyStitch::connect_method_move_point_to, N_("move to end"), "move-end" } +}; + +static const Util::EnumDataConverter<LPEEmbroderyStitch::connect_method> +ConnectMethodConverter(ConnectMethodData, sizeof(ConnectMethodData) / sizeof(*ConnectMethodData)); + +LPEEmbroderyStitch::LPEEmbroderyStitch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + ordering(_("Ordering method"), _("Method used to order sub paths"), "ordering", OrderMethodConverter, &wr, this, order_method_no_reorder), + connection(_("Connection method"), _("Method to connect end points of sub paths"), "connection", ConnectMethodConverter, &wr, this, connect_method_line), + stitch_length(_("Stitch length"), _("Divide path into straight segments of given length (in user units)"), "stitch-length", &wr, this, 10.0), + stitch_min_length(_("Minimum stitch length [%]"), _("Merge stitches that are shorter than this percentage of the stitch length"), "stitch-min-length", &wr, this, 25.0), + stitch_pattern(_("Stitch pattern"), _("Select between different stitch patterns"), "stitch_pattern", &wr, this, 0), + show_stitches(_("Show stitches"), _("Creates gaps between stitches (use only for preview, deactivate for use with embroidery machines)"), "show-stitches", &wr, this, false), + show_stitch_gap(_("Show stitch gap"), _("Length of the gap between stitches when showing stitches"), "show-stitch-gap", &wr, this, 0.5), + jump_if_longer(_("Jump if longer"), _("Jump connection if longer than"), "jump-if-longer", &wr, this, 100) +{ + registerParameter(dynamic_cast<Parameter *>(&ordering)); + registerParameter(dynamic_cast<Parameter *>(&connection)); + registerParameter(dynamic_cast<Parameter *>(&stitch_length)); + registerParameter(dynamic_cast<Parameter *>(&stitch_min_length)); + registerParameter(dynamic_cast<Parameter *>(&stitch_pattern)); + registerParameter(dynamic_cast<Parameter *>(&show_stitches)); + registerParameter(dynamic_cast<Parameter *>(&show_stitch_gap)); + registerParameter(dynamic_cast<Parameter *>(&jump_if_longer)); + + stitch_length.param_set_digits(1); + stitch_length.param_set_range(1, 10000); + stitch_min_length.param_set_digits(1); + stitch_min_length.param_set_range(0, 100); + stitch_pattern.param_make_integer(); + stitch_pattern.param_set_range(0, 2); + show_stitch_gap.param_set_range(0.001, 10); + jump_if_longer.param_set_range(0.0, 1000000); +} + +LPEEmbroderyStitch::~LPEEmbroderyStitch() += default; + +double LPEEmbroderyStitch::GetPatternInitialStep(int pattern, int line) +{ + switch (pattern) { + case 0: + return 0.0; + + case 1: + switch (line % 4) { + case 0: + return 0.0; + case 1: + return 0.25; + case 2: + return 0.50; + case 3: + return 0.75; + } + return 0.0; + + case 2: + switch (line % 4) { + case 0: + return 0.0; + case 1: + return 0.5; + case 2: + return 0.75; + case 3: + return 0.25; + } + return 0.0; + + default: + return 0.0; + } + +} + +Point LPEEmbroderyStitch::GetStartPointInterpolAfterRev(std::vector<OrderingInfo> const &info, unsigned i) +{ + Point start_this = info[i].GetBegRev(); + + if (i == 0) { + return start_this; + } + + if (!info[i - 1].connect) { + return start_this; + } + + Point end_prev = info[i - 1].GetEndRev(); + + switch (connection.get_value()) { + case connect_method_line: + return start_this; + case connect_method_move_point_from: + return end_prev; + case connect_method_move_point_mid: + return 0.5 * start_this + 0.5 * end_prev; + case connect_method_move_point_to: + return start_this; + default: + return start_this; + } +} +Point LPEEmbroderyStitch::GetEndPointInterpolAfterRev(std::vector<OrderingInfo> const &info, unsigned i) +{ + Point end_this = info[i].GetEndRev(); + + if (i + 1 == info.size()) { + return end_this; + } + + if (!info[i].connect) { + return end_this; + } + + Point start_next = info[i + 1].GetBegRev(); + + switch (connection.get_value()) { + case connect_method_line: + return end_this; + case connect_method_move_point_from: + return end_this; + case connect_method_move_point_mid: + return 0.5 * start_next + 0.5 * end_this; + case connect_method_move_point_to: + return start_next; + default: + return end_this; + } +} + +Point LPEEmbroderyStitch::GetStartPointInterpolBeforeRev(std::vector<OrderingInfo> const &info, unsigned i) +{ + if (info[i].reverse) { + return GetEndPointInterpolAfterRev(info, i); + } else { + return GetStartPointInterpolAfterRev(info, i); + } +} + +Point LPEEmbroderyStitch::GetEndPointInterpolBeforeRev(std::vector<OrderingInfo> const &info, unsigned i) +{ + if (info[i].reverse) { + return GetStartPointInterpolAfterRev(info, i); + } else { + return GetEndPointInterpolAfterRev(info, i); + } +} + +PathVector LPEEmbroderyStitch::doEffect_path(PathVector const &path_in) +{ + if (path_in.size() >= 2) { + PathVector path_out; + + // Create vectors with start and end points + std::vector<OrderingInfo> orderinginfos(path_in.size()); + // connect next path to this one + bool connect_with_previous = false; + + for (PathVector::const_iterator it = path_in.begin(); it != path_in.end(); ++it) { + OrderingInfo &info = orderinginfos[ it - path_in.begin() ]; + info.index = it - path_in.begin(); + info.reverse = false; + info.used = false; + info.connect = true; + info.begOrig = it->front().initialPoint(); + info.endOrig = it->back().finalPoint(); + } + + // Compute sub-path ordering + switch (ordering.get_value()) { + case order_method_no_reorder: + OrderingOriginal(orderinginfos); + break; + + case order_method_zigzag: + OrderingZigZag(orderinginfos, false); + break; + + case order_method_zigzag_rev_first: + OrderingZigZag(orderinginfos, true); + break; + + case order_method_closest: + OrderingClosest(orderinginfos, false); + break; + + case order_method_closest_rev_first: + OrderingClosest(orderinginfos, true); + break; + + case order_method_tsp_kopt_2: + OrderingAdvanced(orderinginfos, 2); + break; + + case order_method_tsp_kopt_3: + OrderingAdvanced(orderinginfos, 3); + break; + + case order_method_tsp_kopt_4: + OrderingAdvanced(orderinginfos, 4); + break; + + case order_method_tsp_kopt_5: + OrderingAdvanced(orderinginfos, 5); + break; + + } + + // Iterate over sub-paths in order found above + // Divide paths into stitches (currently always equidistant) + // Interpolate between neighboring paths, so that their ends coincide + for (std::vector<OrderingInfo>::const_iterator it = orderinginfos.begin(); it != orderinginfos.end(); ++it) { + // info index + unsigned iInfo = it - orderinginfos.begin(); + // subpath index + unsigned iPath = it->index; + // decide of path shall be reversed + bool reverse = it->reverse; + // minimum stitch length in absolute measure + double stitch_min_length_abs = stitch_min_length * 0.01 * stitch_length; + + // convert path to piecewise + Piecewise<D2<SBasis> > pwOrig = path_in[iPath].toPwSb(); + // make piecewise equidistant in time + Piecewise<D2<SBasis> > pwEqdist = arc_length_parametrization(pwOrig); + Piecewise<D2<SBasis> > pwStitch; + + // cut into stitches + double cutpos = 0.0; + Interval pwdomain = pwEqdist.domain(); + + // step length of first stitch + double step = GetPatternInitialStep(stitch_pattern, iInfo) * stitch_length; + if (step < stitch_min_length_abs) { + step += stitch_length; + } + + bool last = false; + bool first = true; + double posnext; + for (double pos = pwdomain.min(); !last; pos = posnext, cutpos += 1.0) { + // start point + Point p1; + if (first) { + p1 = GetStartPointInterpolBeforeRev(orderinginfos, iInfo); + first = false; + } else { + p1 = pwEqdist.valueAt(pos); + } + + // end point of this stitch + Point p2; + posnext = pos + step; + // last stitch is to end + if (posnext >= pwdomain.max() - stitch_min_length_abs) { + p2 = GetEndPointInterpolBeforeRev(orderinginfos, iInfo); + last = true; + } else { + p2 = pwEqdist.valueAt(posnext); + } + + pwStitch.push_cut(cutpos); + pwStitch.push_seg(D2<SBasis>(SBasis(Linear(p1[X], p2[X])), SBasis(Linear(p1[Y], p2[Y])))); + + // stitch length for all except first step + step = stitch_length; + } + pwStitch.push_cut(cutpos); + + if (reverse) { + pwStitch = Geom::reverse(pwStitch); + } + + if (it->connect && iInfo != orderinginfos.size() - 1) { + // Connect this segment with the previous segment by a straight line + Point end = pwStitch.lastValue(); + Point start_next = GetStartPointInterpolAfterRev(orderinginfos, iInfo + 1); + // connect end and start point + if (end != start_next && distance(end, start_next) <= jump_if_longer) { + cutpos += 1.0; + pwStitch.push_seg(D2<SBasis>(SBasis(Linear(end[X], start_next[X])), SBasis(Linear(end[Y], start_next[Y])))); + pwStitch.push_cut(cutpos); + } + } + + if (show_stitches) { + for (auto & seg : pwStitch.segs) { + // Create anew piecewise with just one segment + Piecewise<D2<SBasis> > pwOne; + pwOne.push_cut(0); + pwOne.push_seg(seg); + pwOne.push_cut(1); + + // make piecewise equidistant in time + Piecewise<D2<SBasis> > pwOneEqdist = arc_length_parametrization(pwOne); + Interval pwdomain = pwOneEqdist.domain(); + + // Compute the points of the shortened piece + Coord len = pwdomain.max() - pwdomain.min(); + Coord offs = 0.5 * (show_stitch_gap < 0.5 * len ? show_stitch_gap : 0.5 * len); + Point p1 = pwOneEqdist.valueAt(pwdomain.min() + offs); + Point p2 = pwOneEqdist.valueAt(pwdomain.max() - offs); + Piecewise<D2<SBasis> > pwOneGap; + + // Create Linear SBasis + D2<SBasis> sbasis = D2<SBasis>(SBasis(Linear(p1[X], p2[X])), SBasis(Linear(p1[Y], p2[Y]))); + + // Convert to path and add to path list + Geom::Path path = path_from_sbasis(sbasis , LPE_CONVERSION_TOLERANCE, false); + path_out.push_back(path); + } + } else { + PathVector pathv = path_from_piecewise(pwStitch, LPE_CONVERSION_TOLERANCE); + for (const auto & ipv : pathv) { + if (connect_with_previous) { + path_out.back().append(ipv); + } else { + path_out.push_back(ipv); + } + } + } + + connect_with_previous = it->connect; + } + + return path_out; + } else { + return path_in; + } +} + +void +LPEEmbroderyStitch::resetDefaults(SPItem const *item) +{ + Effect::resetDefaults(item); +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ diff --git a/src/live_effects/lpe-embrodery-stitch.h b/src/live_effects/lpe-embrodery-stitch.h new file mode 100644 index 0000000..6bbd931 --- /dev/null +++ b/src/live_effects/lpe-embrodery-stitch.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Embroidery stitch live path effect + * + * Copyright (C) 2016 Michael Soegtrop + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_EMBRODERY_STITCH_H +#define INKSCAPE_LPE_EMBRODERY_STITCH_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/lpe-embrodery-stitch-ordering.h" + +namespace Inkscape { +namespace LivePathEffect { + +using namespace LPEEmbroderyStitchOrdering; + +class LPEEmbroderyStitch : public Effect { +public: + + LPEEmbroderyStitch(LivePathEffectObject *lpeobject); + ~LPEEmbroderyStitch() override; + + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + + void resetDefaults(SPItem const *item) override; + + enum order_method { + order_method_no_reorder, + order_method_zigzag, + order_method_zigzag_rev_first, + order_method_closest, + order_method_closest_rev_first, + order_method_tsp_kopt_2, + order_method_tsp_kopt_3, + order_method_tsp_kopt_4, + order_method_tsp_kopt_5, + order_method_count + }; + enum connect_method { + connect_method_line, + connect_method_move_point_from, + connect_method_move_point_mid, + connect_method_move_point_to, + connect_method_count + }; + +private: + EnumParam<order_method> ordering; + EnumParam<connect_method> connection; + ScalarParam stitch_length; + ScalarParam stitch_min_length; + ScalarParam stitch_pattern; + BoolParam show_stitches; + ScalarParam show_stitch_gap; + ScalarParam jump_if_longer; + + LPEEmbroderyStitch(const LPEEmbroderyStitch &) = delete; + LPEEmbroderyStitch &operator=(const LPEEmbroderyStitch &) = delete; + + double GetPatternInitialStep(int pattern, int line); + Geom::Point GetStartPointInterpolAfterRev(std::vector<OrderingInfo> const &info, unsigned i); + Geom::Point GetEndPointInterpolAfterRev(std::vector<OrderingInfo> const &info, unsigned i); + Geom::Point GetStartPointInterpolBeforeRev(std::vector<OrderingInfo> const &info, unsigned i); + Geom::Point GetEndPointInterpolBeforeRev(std::vector<OrderingInfo> const &info, unsigned i); +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-envelope.cpp b/src/live_effects/lpe-envelope.cpp new file mode 100644 index 0000000..fc17120 --- /dev/null +++ b/src/live_effects/lpe-envelope.cpp @@ -0,0 +1,284 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Steren Giannini 2008 <steren.giannini@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-envelope.h" +#include "display/curve.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEEnvelope::LPEEnvelope(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + bend_path1(_("Top bend path:"), _("Top path along which to bend the original path"), "bendpath1", &wr, this, "M0,0 L1,0"), + bend_path2(_("Right bend path:"), _("Right path along which to bend the original path"), "bendpath2", &wr, this, "M0,0 L1,0"), + bend_path3(_("Bottom bend path:"), _("Bottom path along which to bend the original path"), "bendpath3", &wr, this, "M0,0 L1,0"), + bend_path4(_("Left bend path:"), _("Left path along which to bend the original path"), "bendpath4", &wr, this, "M0,0 L1,0"), + xx(_("_Enable left & right paths"), _("Enable the left and right deformation paths"), "xx", &wr, this, true), + yy(_("_Enable top & bottom paths"), _("Enable the top and bottom deformation paths"), "yy", &wr, this, true) +{ + registerParameter(&yy); + registerParameter(&xx); + registerParameter(&bend_path1); + registerParameter(&bend_path2); + registerParameter(&bend_path3); + registerParameter(&bend_path4); + concatenate_before_pwd2 = true; + apply_to_clippath_and_mask = true; +} + +LPEEnvelope::~LPEEnvelope() += default; + +bool +LPEEnvelope::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + bend_path1.reload(); + bend_path2.reload(); + bend_path3.reload(); + bend_path4.reload(); + return false; +} + +void LPEEnvelope::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) { + bend_path1.param_transform_multiply(postmul, false); + bend_path2.param_transform_multiply(postmul, false); + bend_path3.param_transform_multiply(postmul, false); + bend_path4.param_transform_multiply(postmul, false); + } +} + +void +LPEEnvelope::doBeforeEffect (SPLPEItem const* lpeitem) +{ + // get the item bounding box + original_bbox(lpeitem, false, true); + if (is_load) { + bend_path1.reload(); + bend_path2.reload(); + bend_path3.reload(); + bend_path4.reload(); + } + + +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEEnvelope::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + + if(!xx.get_value() && !yy.get_value()) + { + return pwd2_in; + } + + using namespace Geom; + + // Don't allow empty path parameters: + if ( bend_path1.get_pathvector().empty() + || bend_path2.get_pathvector().empty() + || bend_path3.get_pathvector().empty() + || bend_path4.get_pathvector().empty() ) + { + return pwd2_in; + } + + /* + The code below is inspired from the Bend Path code developed by jfb and mgsloan + Please, read it before trying to understand this one + */ + + Geom::Affine affine = bend_path1.get_relative_affine(); + Piecewise<D2<SBasis> > uskeleton1 = arc_length_parametrization(bend_path1.get_pwd2() * affine,2,.1); + uskeleton1 = remove_short_cuts(uskeleton1,.01); + Piecewise<D2<SBasis> > n1 = rot90(derivative(uskeleton1)); + n1 = force_continuity(remove_short_cuts(n1,.1)); + + affine = bend_path2.get_relative_affine(); + + Piecewise<D2<SBasis> > uskeleton2 = arc_length_parametrization(bend_path2.get_pwd2() * affine,2,.1); + uskeleton2 = remove_short_cuts(uskeleton2,.01); + Piecewise<D2<SBasis> > n2 = rot90(derivative(uskeleton2)); + n2 = force_continuity(remove_short_cuts(n2,.1)); + + affine = bend_path3.get_relative_affine(); + Piecewise<D2<SBasis> > uskeleton3 = arc_length_parametrization(bend_path3.get_pwd2() * affine,2,.1); + uskeleton3 = remove_short_cuts(uskeleton3,.01); + Piecewise<D2<SBasis> > n3 = rot90(derivative(uskeleton3)); + n3 = force_continuity(remove_short_cuts(n3,.1)); + + affine = bend_path4.get_relative_affine(); + Piecewise<D2<SBasis> > uskeleton4 = arc_length_parametrization(bend_path4.get_pwd2() * affine,2,.1); + uskeleton4 = remove_short_cuts(uskeleton4,.01); + Piecewise<D2<SBasis> > n4 = rot90(derivative(uskeleton4)); + n4 = force_continuity(remove_short_cuts(n4,.1)); + + + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pwd2_in); + Piecewise<SBasis> x = Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y = Piecewise<SBasis>(patternd2[1]); + + /*The *1.001 is a hack to avoid a small bug : path at x=0 and y=0 don't work well. */ + x-= boundingbox_X.min()*1.001; + y-= boundingbox_Y.min()*1.001; + + Piecewise<SBasis> x1 = x ; + Piecewise<SBasis> y1 = y ; + + Piecewise<SBasis> x2 = x ; + Piecewise<SBasis> y2 = y ; + x2 -= boundingbox_X.extent(); + + Piecewise<SBasis> x3 = x ; + Piecewise<SBasis> y3 = y ; + y3 -= boundingbox_Y.extent(); + + Piecewise<SBasis> x4 = x ; + Piecewise<SBasis> y4 = y ; + + + /*Scaling to the Bend Path length*/ + double scaling1 = uskeleton1.cuts.back()/boundingbox_X.extent(); + if (scaling1 != 1.0) { + x1*=scaling1; + } + + double scaling2 = uskeleton2.cuts.back()/boundingbox_Y.extent(); + if (scaling2 != 1.0) { + y2*=scaling2; + } + + double scaling3 = uskeleton3.cuts.back()/boundingbox_X.extent(); + if (scaling3 != 1.0) { + x3*=scaling3; + } + + double scaling4 = uskeleton4.cuts.back()/boundingbox_Y.extent(); + if (scaling4 != 1.0) { + y4*=scaling4; + } + + + + Piecewise<SBasis> xbis = x; + Piecewise<SBasis> ybis = y; + xbis *= -1.0; + xbis += boundingbox_X.extent(); + ybis *= -1.0; + ybis += boundingbox_Y.extent(); + /* This is important : y + ybis = constant and x +xbis = constant */ + + Piecewise<D2<SBasis> > output; + Piecewise<D2<SBasis> > output1; + Piecewise<D2<SBasis> > output2; + Piecewise<D2<SBasis> > output_x; + Piecewise<D2<SBasis> > output_y; + + /* + output_y : Deformation by Up and Down Bend Paths + We use weighting : The closer a point is to a Band Path, the more it will be affected by this Bend Path. + This is done by the line "ybis*Derformation1 + y*Deformation2" + The result is a mix between the 2 deformed paths + */ + output_y = ybis*(compose((uskeleton1),x1) + y1*compose(n1,x1) ) + + y*(compose((uskeleton3),x3) + y3*compose(n3,x3) ); + output_y /= (boundingbox_Y.extent()); + if(!xx.get_value() && yy.get_value()) + { + return output_y; + } + + /*output_x : Deformation by Left and Right Bend Paths*/ + output_x = x*(compose((uskeleton2),y2) + -x2*compose(n2,y2) ) + + xbis*(compose((uskeleton4),y4) + -x4*compose(n4,y4) ); + output_x /= (boundingbox_X.extent()); + if(xx.get_value() && !yy.get_value()) + { + return output_x; + } + + /*output : Deformation by Up, Left, Right and Down Bend Paths*/ + if(xx.get_value() && yy.get_value()) + { + Piecewise<SBasis> xsqr = x*xbis; /* xsqr = x * (BBox_X - x) */ + Piecewise<SBasis> ysqr = y*ybis; /* xsqr = y * (BBox_Y - y) */ + Piecewise<SBasis> xsqrbis = xsqr; + Piecewise<SBasis> ysqrbis = ysqr; + xsqrbis *= -1; + xsqrbis += boundingbox_X.extent()*boundingbox_X.extent()/4.; + ysqrbis *= -1; + ysqrbis += boundingbox_Y.extent()*boundingbox_Y.extent()/4.; + /*This is important : xsqr + xsqrbis = constant*/ + + + /* + Here we mix the last two results : output_x and output_y + output1 : The more a point is close to Up and Down, the less it will be affected by output_x. + (This is done with the polynomial function) + output2 : The more a point is close to Left and Right, the less it will be affected by output_y. + output : we do the mean between output1 and output2 for all points. + */ + output1 = (ysqrbis*output_y) + (ysqr*output_x); + output1 /= (boundingbox_Y.extent()*boundingbox_Y.extent()/4.); + + output2 = (xsqrbis*output_x) + (xsqr*output_y); + output2 /= (boundingbox_X.extent()*boundingbox_X.extent()/4.); + + output = output1 + output2; + output /= 2.; + + return output; + /*Of course, the result is not perfect, but on a graphical point of view, this is sufficient.*/ + + } + + // do nothing when xx and yy are both false + return pwd2_in; +} + +void +LPEEnvelope::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + original_bbox(SP_LPE_ITEM(item), false, true); + + Geom::Point Up_Left(boundingbox_X.min(), boundingbox_Y.min()); + Geom::Point Up_Right(boundingbox_X.max(), boundingbox_Y.min()); + Geom::Point Down_Left(boundingbox_X.min(), boundingbox_Y.max()); + Geom::Point Down_Right(boundingbox_X.max(), boundingbox_Y.max()); + + Geom::Path path1; + path1.start( Up_Left ); + path1.appendNew<Geom::LineSegment>( Up_Right ); + bend_path1.set_new_value( path1.toPwSb(), true ); + + Geom::Path path2; + path2.start( Up_Right ); + path2.appendNew<Geom::LineSegment>( Down_Right ); + bend_path2.set_new_value( path2.toPwSb(), true ); + + Geom::Path path3; + path3.start( Down_Left ); + path3.appendNew<Geom::LineSegment>( Down_Right ); + bend_path3.set_new_value( path3.toPwSb(), true ); + + Geom::Path path4; + path4.start( Up_Left ); + path4.appendNew<Geom::LineSegment>( Down_Left ); + bend_path4.set_new_value( path4.toPwSb(), true ); +} + + +} // namespace LivePathEffect +} /* namespace Inkscape */ diff --git a/src/live_effects/lpe-envelope.h b/src/live_effects/lpe-envelope.h new file mode 100644 index 0000000..3e906e5 --- /dev/null +++ b/src/live_effects/lpe-envelope.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ENVELOPE_H +#define INKSCAPE_LPE_ENVELOPE_H + +/* + * Inkscape::LPEEnvelope + * + * Copyright (C) Steren Giannini 2008 <steren.giannini@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" + +#include <2geom/sbasis.h> +#include <2geom/sbasis-geometric.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/d2.h> +#include <2geom/piecewise.h> + +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEEnvelope : public Effect, GroupBBoxEffect { +public: + LPEEnvelope(LivePathEffectObject *lpeobject); + ~LPEEnvelope() override; + + bool doOnOpen(SPLPEItem const *lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + +private: + PathParam bend_path1; + PathParam bend_path2; + PathParam bend_path3; + PathParam bend_path4; + BoolParam xx; + BoolParam yy; + + void on_pattern_pasted(); + + LPEEnvelope(const LPEEnvelope&); + LPEEnvelope& operator=(const LPEEnvelope&); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-extrude.cpp b/src/live_effects/lpe-extrude.cpp new file mode 100644 index 0000000..57e5ca2 --- /dev/null +++ b/src/live_effects/lpe-extrude.cpp @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE effect for extruding paths (making them "3D"). + * + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-extrude.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + + +namespace Inkscape { +namespace LivePathEffect { + +LPEExtrude::LPEExtrude(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + extrude_vector(_("Direction"), _("Defines the direction and magnitude of the extrusion"), "extrude_vector", &wr, this, Geom::Point(-10,10)) +{ + show_orig_path = true; + concatenate_before_pwd2 = false; + + registerParameter(&extrude_vector); +} + +LPEExtrude::~LPEExtrude() += default; + +static bool are_colinear(Geom::Point a, Geom::Point b) { + return Geom::are_near(cross(a,b), 0., 0.5); +} + +// find cusps, except at start/end for closed paths. +// this should be factored out later. +static std::vector<double> find_cusps( Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in ) { + using namespace Geom; + Piecewise<D2<SBasis> > deriv = derivative(pwd2_in); + std::vector<double> cusps; + // cusps are spots where the derivative jumps. + for (unsigned i = 1 ; i < deriv.size() ; ++i) { + if ( ! are_colinear(deriv[i-1].at1(), deriv[i].at0()) ) { + // there is a jump in the derivative, so add it to the cusps list + cusps.push_back(deriv.cuts[i]); + } + } + return cusps; +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEExtrude::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + // generate connecting lines (the 'sides' of the extrusion) + Geom::Path path(Point(0.,0.)); + path.appendNew<Geom::LineSegment>( extrude_vector.getVector() ); + Piecewise<D2<SBasis> > connector = path.toPwSb(); + + switch( 1 ) { + case 0: { + /* This one results in the following subpaths: the original, a displaced copy, and connector lines between the two + */ + + Piecewise<D2<SBasis> > pwd2_out = pwd2_in; + // generate extrusion bottom: (just a copy of original path, displaced a bit) + pwd2_out.concat( pwd2_in + extrude_vector.getVector() ); + + // connecting lines should be put at start and end of path if it is not closed + // it is not possible to check whether a piecewise<T> path is closed, + // so we check whether start and end are close + if ( ! are_near(pwd2_in.firstValue(), pwd2_in.lastValue()) ) { + pwd2_out.concat( connector + pwd2_in.firstValue() ); + pwd2_out.concat( connector + pwd2_in.lastValue() ); + } + // connecting lines should be put at cusps + Piecewise<D2<SBasis> > deriv = derivative(pwd2_in); + std::vector<double> cusps; // = roots(deriv); + for (double cusp : cusps) { + pwd2_out.concat( connector + pwd2_in.valueAt(cusp) ); + } + // connecting lines should be put where the tangent of the path equals the extrude_vector in direction + std::vector<double> rts = roots(dot(deriv, rot90(extrude_vector.getVector()))); + for (double rt : rts) { + pwd2_out.concat( connector + pwd2_in.valueAt(rt) ); + } + return pwd2_out; + } + + default: + case 1: { + /* This one creates separate closed subpaths that correspond to the faces of the extruded shape. + * When the LPE is complete, one can convert the shape to a normal path, then break subpaths apart and start coloring them. + */ + + Piecewise<D2<SBasis> > pwd2_out; + // split input path in pieces between points where deriv == vector + Piecewise<D2<SBasis> > deriv = derivative(pwd2_in); + std::vector<double> rts = roots(dot(deriv, rot90(extrude_vector.getVector()))); + + std::vector<double> cusps = find_cusps(pwd2_in); + + // see if we should treat the path as being closed. + bool closed_path = false; + if ( are_near(pwd2_in.firstValue(), pwd2_in.lastValue()) ) { + // the path is closed, however if there is a cusp at the closing point, we should treat it as being an open path. + if ( are_colinear(deriv.firstValue(), deriv.lastValue()) ) { + // there is no jump in the derivative, so treat path as being closed + closed_path = true; + } + } + + std::vector<double> connector_pts; + if (rts.empty()) { + connector_pts = cusps; + } else if (cusps.empty()) { + connector_pts = rts; + } else { + connector_pts = rts; + connector_pts.insert(connector_pts.begin(), cusps.begin(), cusps.end()); + sort(connector_pts.begin(), connector_pts.end()); + } + + double portion_t = 0.; + for (unsigned i = 0; i < connector_pts.size() ; ++i) { + Piecewise<D2<SBasis> > cut = portion(pwd2_in, portion_t, connector_pts[i] ); + portion_t = connector_pts[i]; + if (closed_path && i == 0) { + // if the path is closed, skip the first cut and add it to the last cut later + continue; + } + Piecewise<D2<SBasis> > part = cut; + part.continuousConcat(connector + cut.lastValue()); + part.continuousConcat(reverse(cut) + extrude_vector.getVector()); + part.continuousConcat(reverse(connector) + cut.firstValue()); + pwd2_out.concat( part ); + } + if (closed_path) { + Piecewise<D2<SBasis> > cut = portion(pwd2_in, portion_t, pwd2_in.domain().max() ); + cut.continuousConcat(portion(pwd2_in, pwd2_in.domain().min(), connector_pts[0] )); + Piecewise<D2<SBasis> > part = cut; + part.continuousConcat(connector + cut.lastValue()); + part.continuousConcat(reverse(cut) + extrude_vector.getVector()); + part.continuousConcat(reverse(connector) + cut.firstValue()); + pwd2_out.concat( part ); + } else if (!are_near(portion_t, pwd2_in.domain().max())) { + Piecewise<D2<SBasis> > cut = portion(pwd2_in, portion_t, pwd2_in.domain().max() ); + Piecewise<D2<SBasis> > part = cut; + part.continuousConcat(connector + cut.lastValue()); + part.continuousConcat(reverse(cut) + extrude_vector.getVector()); + part.continuousConcat(reverse(connector) + cut.firstValue()); + pwd2_out.concat( part ); + } + return pwd2_out; + } + } +} + +void +LPEExtrude::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + using namespace Geom; + + Geom::OptRect bbox = item->geometricBounds(); + if (bbox) { + Interval const &boundingbox_X = (*bbox)[Geom::X]; + Interval const &boundingbox_Y = (*bbox)[Geom::Y]; + extrude_vector.set_and_write_new_values( Geom::Point(boundingbox_X.middle(), boundingbox_Y.middle()), + (boundingbox_X.extent() + boundingbox_Y.extent())*Geom::Point(-0.05,0.2) ); + } +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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/live_effects/lpe-extrude.h b/src/live_effects/lpe-extrude.h new file mode 100644 index 0000000..d666138 --- /dev/null +++ b/src/live_effects/lpe-extrude.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief LPE effect for extruding paths (making them "3D"). + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_EXTRUDE_H +#define INKSCAPE_LPE_EXTRUDE_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/vector.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEExtrude : public Effect { +public: + LPEExtrude(LivePathEffectObject *lpeobject); + ~LPEExtrude() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + +private: + VectorParam extrude_vector; + + LPEExtrude(const LPEExtrude&) = delete; + LPEExtrude& operator=(const LPEExtrude&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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/live_effects/lpe-fill-between-many.cpp b/src/live_effects/lpe-fill-between-many.cpp new file mode 100644 index 0000000..cc44464 --- /dev/null +++ b/src/live_effects/lpe-fill-between-many.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "live_effects/lpe-fill-between-many.h" +#include "live_effects/lpeobject.h" +#include "xml/node.h" +#include "display/curve.h" +#include "inkscape.h" +#include "selection.h" + +#include "object/sp-defs.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "svg/svg.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<Filllpemethod> FilllpemethodData[] = { + { FLM_ORIGINALD, N_("Without LPEs"), "originald" }, + { FLM_BSPLINESPIRO, N_("With Spiro or BSpline"), "bsplinespiro" }, + { FLM_D, N_("With all LPEs"), "d" } +}; +static const Util::EnumDataConverter<Filllpemethod> FLMConverter(FilllpemethodData, FLM_END); + +LPEFillBetweenMany::LPEFillBetweenMany(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , linked_paths(_("Linked path:"), _("Paths from which to take the original path data"), "linkedpaths", &wr, this) + , method(_("LPEs:"), _("Which LPEs of the linked paths should be considered"), "method", FLMConverter, &wr, this, FLM_BSPLINESPIRO) + , join(_("Join subpaths"), _("Join subpaths"), "join", &wr, this, true) + , close(_("Close"), _("Close path"), "close", &wr, this, true) + , autoreverse(_("Autoreverse"), _("Autoreverse"), "autoreverse", &wr, this, true) +{ + registerParameter(&linked_paths); + registerParameter(&method); + registerParameter(&join); + registerParameter(&close); + registerParameter(&autoreverse); + previous_method = FLM_END; + linked_paths.setUpdating(true); +} + +LPEFillBetweenMany::~LPEFillBetweenMany() += default; + +void +LPEFillBetweenMany::doOnApply(SPLPEItem const* lpeitem) +{ + lpeversion.param_setValue("1.2", true); +} + +bool +LPEFillBetweenMany::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + + linked_paths.setUpdating(false); + linked_paths.start_listening(); + linked_paths.connect_selection_changed(); + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + prevaffine = i2anc_affine(sp_lpe_item, sp_lpe_item->document->getRoot()); + } + return false; +} + +void +LPEFillBetweenMany::doBeforeEffect (SPLPEItem const* lpeitem) +{ + legacytest = false; + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + } + if (!is_load) { + transform_multiply_nested(i2anc_affine(sp_lpe_item, sp_lpe_item->document->getRoot()).inverse() * prevaffine); + prevaffine = i2anc_affine(sp_lpe_item, sp_lpe_item->document->getRoot()); + } else { + linked_paths.setUpdating(false); + linked_paths.start_listening(); + linked_paths.connect_selection_changed(); + } + Glib::ustring version = lpeversion.param_getSVGValue(); + if (version < "1.2") { + legacytest = true; + } +} + +void +LPEFillBetweenMany::transform_multiply_nested(Geom::Affine const &postmul) +{ + if (is_visible && sp_lpe_item->pathEffectsEnabled() && !isOnClipboard() && !postmul.isIdentity()) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = nullptr; + if (desktop) { + selection = desktop->selection; + } + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + } + for (auto & iter : linked_paths._vector) { + SPItem *item; + if (iter->ref.isAttached() && (( item = dynamic_cast<SPItem*>(iter->ref.getObject()) )) && + !iter->_pathvector.empty() && iter->visibled) { + if (iter->_pathvector.front().closed() && linked_paths._vector.size() > 1) { + continue; + } + if (selection && !selection->includes(item, true) && selection->includes(sp_lpe_item, true)) { + item->transform *= i2anc_affine(item->parent, item->document->getRoot()); + item->transform *= postmul.inverse(); + item->transform *= i2anc_affine(item->parent, item->document->getRoot()).inverse(); + item->doWriteTransform(item->transform, nullptr, false); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + } + } +} + +void +LPEFillBetweenMany::doEffect (SPCurve * curve) +{ + if (previous_method != method) { + if (method == FLM_BSPLINESPIRO) { + linked_paths.allowOnlyBsplineSpiro(true); + linked_paths.setFromOriginalD(false); + } else if(method == FLM_ORIGINALD) { + linked_paths.allowOnlyBsplineSpiro(false); + linked_paths.setFromOriginalD(true); + } else { + linked_paths.allowOnlyBsplineSpiro(false); + linked_paths.setFromOriginalD(false); + } + previous_method = method; + } + Geom::PathVector res_pathv; + if (!autoreverse) { + for (auto & iter : linked_paths._vector) { + SPItem *item; + if (iter->ref.isAttached() && (( item = dynamic_cast<SPItem*>(iter->ref.getObject()) )) && + !iter->_pathvector.empty() && iter->visibled) { + Geom::Path linked_path; + if (iter->_pathvector.front().closed() && linked_paths._vector.size() > 1) { + continue; + } + if (iter->reversed) { + linked_path = iter->_pathvector.front().reversed(); + } else { + linked_path = iter->_pathvector.front(); + } + linked_path *= item->getRelativeTransform(sp_lpe_item); + if (!res_pathv.empty() && join) { + if (!are_near(res_pathv.front().finalPoint(), linked_path.initialPoint(), 0.1)) { + res_pathv.front().appendNew<Geom::LineSegment>(linked_path.initialPoint()); + } else { + linked_path.setInitial(res_pathv.front().finalPoint()); + } + res_pathv.front().append(linked_path); + } else { + if (close && !join) { + linked_path.close(); + } + res_pathv.push_back(linked_path); + } + } + } + } else { + unsigned int counter = 0; + Geom::Point current = Geom::Point(); + counter = 0; + std::vector<unsigned int> done; + for (auto & iter : linked_paths._vector) { + SPItem *item; + if (iter->ref.isAttached() && (( item = dynamic_cast<SPItem*>(iter->ref.getObject()) )) && + !iter->_pathvector.empty() && iter->visibled) { + Geom::Path linked_path; + if (iter->_pathvector.front().closed() && linked_paths._vector.size() > 1) { + counter++; + continue; + } + if (counter == 0) { + Geom::Path initial_path = iter->_pathvector.front(); + if (!legacytest && iter->reversed) { + initial_path = initial_path.reversed(); + } + done.push_back(0); + if (close && !join) { + initial_path.close(); + } + initial_path *= item->getRelativeTransform(sp_lpe_item); + res_pathv.push_back(initial_path); + current = res_pathv.front().finalPoint(); + } + Geom::Coord distance = Geom::infinity(); + unsigned int counter2 = 0; + unsigned int added = 0; + PathAndDirectionAndVisible *nearest = nullptr; + for (auto & iter2 : linked_paths._vector) { + SPItem *item2; + if (iter2->ref.isAttached() && (( item2 = dynamic_cast<SPItem*>(iter2->ref.getObject()) )) && + !iter2->_pathvector.empty() && iter2->visibled) { + if (item == item2 || std::find(done.begin(), done.end(), counter2) != done.end()) { + counter2++; + continue; + } + if (iter2->_pathvector.front().closed() && linked_paths._vector.size() > 1) { + counter2++; + continue; + } + Geom::Point start = iter2->_pathvector.front().initialPoint(); + Geom::Point end = iter2->_pathvector.front().finalPoint(); + if (!legacytest && iter2->reversed) { + std::swap(start,end); + } + if (!legacytest) { + current = res_pathv.finalPoint(); + } + Geom::Coord distance_iter = + std::min(Geom::distance(current, end), Geom::distance(current, start)); + if (distance > distance_iter) { + distance = distance_iter; + nearest = iter2; + added = counter2; + } + counter2++; + } + } + if (nearest != nullptr) { + done.push_back(added); + Geom::Point start = nearest->_pathvector.front().initialPoint(); + Geom::Point end = nearest->_pathvector.front().finalPoint(); + if (!legacytest && nearest->reversed) { + linked_path = iter->_pathvector.front().reversed(); + std::swap(start,end); + } else { + linked_path = iter->_pathvector.front(); + } + if (Geom::distance(current, end) > Geom::distance(current, start)) { + linked_path = nearest->_pathvector.front(); + } else { + linked_path = nearest->_pathvector.front().reversed(); + } + + if (legacytest) { + current = end; + } + SPItem *itemnear; + if (nearest->ref.isAttached() && (( itemnear = dynamic_cast<SPItem*>(nearest->ref.getObject()) ))) { + linked_path *= itemnear->getRelativeTransform(sp_lpe_item); + } + if (!res_pathv.empty() && join) { + if (!are_near(res_pathv.front().finalPoint(), linked_path.initialPoint(), 0.1)) { + res_pathv.front().appendNew<Geom::LineSegment>(linked_path.initialPoint()); + } else { + linked_path.setInitial(res_pathv.front().finalPoint()); + } + res_pathv.front().append(linked_path); + } else { + if (close && !join) { + linked_path.close(); + } + res_pathv.push_back(linked_path); + } + } + counter++; + } + } + } + if (!res_pathv.empty() && close) { + res_pathv.front().close(); + res_pathv.front().snapEnds(0.1); + } + if (res_pathv.empty()) { + res_pathv = curve->get_pathvector(); + } + curve->set_pathvector(res_pathv); +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-fill-between-many.h b/src/live_effects/lpe-fill-between-many.h new file mode 100644 index 0000000..b306782 --- /dev/null +++ b/src/live_effects/lpe-fill-between-many.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_FILL_BETWEEN_MANY_H +#define INKSCAPE_LPE_FILL_BETWEEN_MANY_H + +/* + * Inkscape::LPEFillBetweenStrokes + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/patharray.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum Filllpemethod { + FLM_ORIGINALD, + FLM_BSPLINESPIRO, + FLM_D, + FLM_END +}; + +class LPEFillBetweenMany : public Effect { +public: + LPEFillBetweenMany(LivePathEffectObject *lpeobject); + ~LPEFillBetweenMany() override; + void doEffect (SPCurve * curve) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doOnApply (SPLPEItem const* lpeitem) override; + void transform_multiply_nested(Geom::Affine const &postmul); +private: + PathArrayParam linked_paths; + EnumParam<Filllpemethod> method; + BoolParam join; + BoolParam close; + BoolParam autoreverse; + bool legacytest = false; + bool fixreverseend = false; + Geom::Affine prevaffine = Geom::identity(); + Filllpemethod previous_method; + LPEFillBetweenMany(const LPEFillBetweenMany&) = delete; + LPEFillBetweenMany& operator=(const LPEFillBetweenMany&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-fill-between-strokes.cpp b/src/live_effects/lpe-fill-between-strokes.cpp new file mode 100644 index 0000000..3dffa18 --- /dev/null +++ b/src/live_effects/lpe-fill-between-strokes.cpp @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-fill-between-strokes.h" + +#include "display/curve.h" +#include "svg/svg.h" + +#include "object/sp-root.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEFillBetweenStrokes::LPEFillBetweenStrokes(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + linked_path(_("Linked path:"), _("Path from which to take the original path data"), "linkedpath", &wr, this), + second_path(_("Second path:"), _("Second path from which to take the original path data"), "secondpath", &wr, this), + reverse_second(_("Reverse Second"), _("Reverses the second path order"), "reversesecond", &wr, this), + join(_("Join subpaths"), _("Join subpaths"), "join", &wr, this, true), + close(_("Close"), _("Close path"), "close", &wr, this, true) +{ + registerParameter(&linked_path); + registerParameter(&second_path); + registerParameter(&reverse_second); + registerParameter(&join); + registerParameter(&close); + linked_path.setUpdating(true); + second_path.setUpdating(true); +} + +LPEFillBetweenStrokes::~LPEFillBetweenStrokes() += default; + +void +LPEFillBetweenStrokes::doOnApply(SPLPEItem const* lpeitem) +{ + lpeversion.param_setValue("1.2", true); +} + +bool +LPEFillBetweenStrokes::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + linked_path.setUpdating(false); + second_path.setUpdating(false); + linked_path.start_listening(linked_path.getObject()); + linked_path.connect_selection_changed(); + second_path.start_listening(second_path.getObject()); + second_path.connect_selection_changed(); + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + prevaffine = i2anc_affine(sp_lpe_item, sp_lpe_item->document->getRoot()); + } + SPItem * item = nullptr; + if (( item = dynamic_cast<SPItem *>(linked_path.getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + if (( item = dynamic_cast<SPItem *>(second_path.getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + return false; +} + +void +LPEFillBetweenStrokes::doBeforeEffect (SPLPEItem const* lpeitem) +{ + legacytest = false; + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + } + if (!is_load) { + transform_multiply_nested(i2anc_affine(sp_lpe_item, sp_lpe_item->document->getRoot()).inverse() * prevaffine); + prevaffine = i2anc_affine(sp_lpe_item, sp_lpe_item->document->getRoot()); + } else { + linked_path.setUpdating(false); + second_path.setUpdating(false); + linked_path.start_listening(linked_path.getObject()); + linked_path.connect_selection_changed(); + second_path.start_listening(second_path.getObject()); + second_path.connect_selection_changed(); + SPItem * item = nullptr; + if (( item = dynamic_cast<SPItem *>(linked_path.getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + if (( item = dynamic_cast<SPItem *>(second_path.getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + Glib::ustring version = lpeversion.param_getSVGValue(); + if (version < "1.2") { + legacytest = true; + } +} +void +LPEFillBetweenStrokes::transform_multiply_nested(Geom::Affine const &postmul) +{ + if (is_visible && sp_lpe_item->pathEffectsEnabled() && !isOnClipboard() && !postmul.isIdentity()) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + Inkscape::Selection *selection = nullptr; + if (desktop) { + selection = desktop->selection; + } + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + } + SPItem *item; + if (( item = dynamic_cast<SPItem*>(linked_path.getObject()) )) { + if (selection && !selection->includes(item, true) && selection->includes(sp_lpe_item, true)) { + item->transform *= i2anc_affine(item->parent, item->document->getRoot()); + item->transform *= postmul.inverse(); + item->transform *= i2anc_affine(item->parent, item->document->getRoot()).inverse(); + item->doWriteTransform(item->transform, nullptr, false); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + SPItem *item2; + if (( item2 = dynamic_cast<SPItem*>(second_path.getObject()) )) { + if (selection && !selection->includes(item2, true) && selection->includes(sp_lpe_item, true)) { + item2->transform *= i2anc_affine(item2->parent, item2->document->getRoot()); + item2->transform *= postmul.inverse(); + item2->transform *= i2anc_affine(item2->parent, item2->document->getRoot()).inverse(); + item2->doWriteTransform(item2->transform, nullptr, false); + item2->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + } + + } +} + +void LPEFillBetweenStrokes::doEffect (SPCurve * curve) +{ + if (curve) { + if ( linked_path.linksToPath() && second_path.linksToPath() && linked_path.getObject() && second_path.getObject() ) { + SPItem * linked1 = linked_path.getObject(); + if (is_load) { + linked1->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + Geom::PathVector linked_pathv = linked_path.get_pathvector(); + SPItem * linked2 = second_path.getObject(); + if (is_load) { + linked2->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + Geom::PathVector second_pathv = second_path.get_pathvector(); + Geom::PathVector result_linked_pathv; + Geom::PathVector result_second_pathv; + linked_pathv *= linked1->getRelativeTransform(sp_lpe_item); + second_pathv *= linked2->getRelativeTransform(sp_lpe_item); + for (auto & iter : linked_pathv) + { + result_linked_pathv.push_back(iter); + } + + for (auto & iter : second_pathv) + { + result_second_pathv.push_back(iter); + } + + if ( !result_linked_pathv.empty() && !result_second_pathv.empty() && !result_linked_pathv.front().closed() ) { + if (reverse_second.get_value()) { + result_second_pathv.front() = result_second_pathv.front().reversed(); + } + if (join) { + if (!are_near(result_linked_pathv.front().finalPoint(), result_second_pathv.front().initialPoint(),0.1)) { + result_linked_pathv.front().appendNew<Geom::LineSegment>(result_second_pathv.front().initialPoint()); + } else { + result_second_pathv.front().setInitial(result_linked_pathv.front().finalPoint()); + } + result_linked_pathv.front().append(result_second_pathv.front()); + if (close) { + result_linked_pathv.front().close(); + } + } else { + if (close) { + result_linked_pathv.front().close(); + result_second_pathv.front().close(); + } + result_linked_pathv.push_back(result_second_pathv.front()); + } + curve->set_pathvector(result_linked_pathv); + } else if ( !result_linked_pathv.empty() ) { + curve->set_pathvector(result_linked_pathv); + } else if ( !result_second_pathv.empty() ) { + curve->set_pathvector(result_second_pathv); + } + } + else if ( linked_path.linksToPath() && linked_path.getObject() ) { + SPItem *linked1 = linked_path.getObject(); + if (is_load) { + linked1->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + Geom::PathVector linked_pathv = linked_path.get_pathvector(); + linked_pathv *= linked1->getRelativeTransform(sp_lpe_item); + Geom::PathVector result_pathv; + for (auto & iter : linked_pathv) + { + result_pathv.push_back(iter); + } + if ( !result_pathv.empty() ) { + if (close) { + result_pathv.front().close(); + } + curve->set_pathvector(result_pathv); + } + } + else if ( second_path.linksToPath() && second_path.getObject() ) { + SPItem *linked2 = second_path.getObject(); + if (is_load) { + linked2->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } + Geom::PathVector second_pathv = second_path.get_pathvector(); + second_pathv *= linked2->getRelativeTransform(sp_lpe_item); + Geom::PathVector result_pathv; + for (auto & iter : second_pathv) + { + result_pathv.push_back(iter); + } + if ( !result_pathv.empty() ) { + if (close) { + result_pathv.front().close(); + result_pathv.front().snapEnds(0.1); + } + curve->set_pathvector(result_pathv); + } + } + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-fill-between-strokes.h b/src/live_effects/lpe-fill-between-strokes.h new file mode 100644 index 0000000..4d7997d --- /dev/null +++ b/src/live_effects/lpe-fill-between-strokes.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_FILL_BETWEEN_STROKES_H +#define INKSCAPE_LPE_FILL_BETWEEN_STROKES_H + +/* + * Inkscape::LPEFillBetweenStrokes + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/originalpath.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEFillBetweenStrokes : public Effect { +public: + LPEFillBetweenStrokes(LivePathEffectObject *lpeobject); + ~LPEFillBetweenStrokes() override; + void doEffect (SPCurve * curve) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doOnApply (SPLPEItem const* lpeitem) override; + void transform_multiply_nested(Geom::Affine const &postmul); + +private: + OriginalPathParam linked_path; + OriginalPathParam second_path; + BoolParam reverse_second; + BoolParam join; + BoolParam close; + Geom::Affine prevaffine = Geom::identity(); + bool legacytest = false; + +private: + LPEFillBetweenStrokes(const LPEFillBetweenStrokes&) = delete; + LPEFillBetweenStrokes& operator=(const LPEFillBetweenStrokes&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-fillet-chamfer.cpp b/src/live_effects/lpe-fillet-chamfer.cpp new file mode 100644 index 0000000..2aa63fd --- /dev/null +++ b/src/live_effects/lpe-fillet-chamfer.cpp @@ -0,0 +1,723 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author(s): + * Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * + * Copyright (C) 2014 Author(s) + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-fillet-chamfer.h" + +#include <2geom/elliptical-arc.h> +#include <boost/optional.hpp> + +#include "display/curve.h" +#include "helper/geom-curves.h" +#include "helper/geom-nodesatellite.h" +#include "helper/geom.h" +#include "object/sp-shape.h" +#include "ui/knot/knot-holder.h" +#include "ui/tools/tool-base.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<Filletmethod> FilletmethodData[] = { + { FM_AUTO, N_("Auto"), "auto" }, + { FM_ARC, N_("Force arc"), "arc" }, + { FM_BEZIER, N_("Force bezier"), "bezier" } +}; +static const Util::EnumDataConverter<Filletmethod> FMConverter(FilletmethodData, FM_END); + +LPEFilletChamfer::LPEFilletChamfer(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + unit(_("Unit:"), _("Unit"), "unit", &wr, this, "px"), + nodesatellites_param("NodeSatellite_param", "NodeSatellite_param", + "nodesatellites_param", &wr, this), + method(_("Method:"), _("Method to calculate the fillet or chamfer"), + "method", FMConverter, &wr, this, FM_AUTO), + mode(_("Mode:"), _("Mode, e.g. fillet or chamfer"), + "mode", &wr, this, "F", true), + radius(_("Radius:"), _("Radius, in unit or %"), "radius", &wr, + this, 0.0), + chamfer_steps(_("Chamfer steps:"), _("Chamfer steps"), "chamfer_steps", + &wr, this, 1), + flexible(_("Radius in %"), _("Flexible radius size (%)"), + "flexible", &wr, this, false), + only_selected(_("Change only selected nodes"), + _("Change only selected nodes"), "only_selected", &wr, this, + false), + use_knot_distance(_("Use knots distance instead radius"), + _("Use knots distance instead radius"), + "use_knot_distance", &wr, this, true), + hide_knots(_("Hide knots"), _("Hide knots"), "hide_knots", &wr, this, + false), + apply_no_radius(_("Apply changes if radius = 0"), _("Apply changes if radius = 0"), "apply_no_radius", &wr, this, true), + apply_with_radius(_("Apply changes if radius > 0"), _("Apply changes if radius > 0"), "apply_with_radius", &wr, this, true), + _pathvector_nodesatellites(nullptr), + _degenerate_hide(false) +{ + // fix legacy < 1.2: + const gchar * satellites_param = getLPEObj()->getAttribute("satellites_param"); + if (satellites_param){ + getLPEObj()->setAttribute("nodesatellites_param", satellites_param); + }; + registerParameter(&nodesatellites_param); + registerParameter(&unit); + registerParameter(&method); + registerParameter(&mode); + registerParameter(&radius); + registerParameter(&chamfer_steps); + registerParameter(&flexible); + registerParameter(&use_knot_distance); + registerParameter(&apply_no_radius); + registerParameter(&apply_with_radius); + registerParameter(&only_selected); + registerParameter(&hide_knots); + + radius.param_set_range(0.0, std::numeric_limits<double>::max()); + radius.param_set_increments(1, 1); + radius.param_set_digits(4); + radius.param_set_undo(false); + chamfer_steps.param_set_range(1, std::numeric_limits<gint>::max()); + chamfer_steps.param_set_increments(1, 1); + chamfer_steps.param_make_integer(); + _provides_knotholder_entities = true; + helperpath = false; + previous_unit = Glib::ustring(""); +} + +void LPEFilletChamfer::doOnApply(SPLPEItem const *lpeItem) +{ + SPLPEItem *splpeitem = const_cast<SPLPEItem *>(lpeItem); + SPShape *shape = dynamic_cast<SPShape *>(splpeitem); + if (shape) { + Geom::PathVector const pathv = pathv_to_linear_and_cubic_beziers(shape->curve()->get_pathvector()); + NodeSatellites nodesatellites; + double power = radius; + if (!flexible) { + SPDocument *document = getSPDoc(); + Glib::ustring display_unit = document->getDisplayUnit()->abbr.c_str(); + power = Inkscape::Util::Quantity::convert(power, unit.get_abbreviation(), display_unit.c_str()); + } + NodeSatelliteType nodesatellite_type = FILLET; + std::map<std::string, NodeSatelliteType> gchar_map_to_nodesatellite_type = boost::assign::map_list_of( + "F", FILLET)("IF", INVERSE_FILLET)("C", CHAMFER)("IC", INVERSE_CHAMFER)("KO", INVALID_SATELLITE); + auto mode_str = mode.param_getSVGValue(); + std::map<std::string, NodeSatelliteType>::iterator it = gchar_map_to_nodesatellite_type.find(mode_str.raw()); + if (it != gchar_map_to_nodesatellite_type.end()) { + nodesatellite_type = it->second; + } + Geom::PathVector pathvres; + for (const auto & path_it : pathv) { + if (path_it.empty() || count_path_nodes(path_it) < 2) { + continue; + } + std::vector<NodeSatellite> subpath_nodesatellites; + Geom::Path::const_iterator curve_it = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + Geom::Path pathresult(curve_it->initialPoint()); + while (curve_it != curve_endit) { + if (pathresult.size()) { + pathresult.setFinal(curve_it->initialPoint()); + } + pathresult.append(*curve_it); + ++curve_it; + NodeSatellite nodesatellite(nodesatellite_type); + nodesatellite.setSteps(chamfer_steps); + nodesatellite.setAmount(power); + nodesatellite.setIsTime(flexible); + nodesatellite.setHasMirror(true); + nodesatellite.setHidden(hide_knots); + subpath_nodesatellites.push_back(nodesatellite); + } + + // we add the last nodesatellite on open path because _pathvector_nodesatellites is related to nodes, not + // curves so maybe in the future we can need this last nodesatellite in other effects don't remove for this + // effect because _pathvector_nodesatellites class has methods when the path is modified and we want one + // method for all uses + if (!path_it.closed()) { + NodeSatellite nodesatellite(nodesatellite_type); + nodesatellite.setSteps(chamfer_steps); + nodesatellite.setAmount(power); + nodesatellite.setIsTime(flexible); + nodesatellite.setHasMirror(true); + nodesatellite.setHidden(hide_knots); + subpath_nodesatellites.push_back(nodesatellite); + } + pathresult.close(path_it.closed()); + pathvres.push_back(pathresult); + pathresult.clear(); + nodesatellites.push_back(subpath_nodesatellites); + } + _pathvector_nodesatellites = new PathVectorNodeSatellites(); + _pathvector_nodesatellites->setPathVector(pathvres); + _pathvector_nodesatellites->setNodeSatellites(nodesatellites); + nodesatellites_param.setPathVectorNodeSatellites(_pathvector_nodesatellites); + } else { + g_warning("LPE Fillet/Chamfer can only be applied to shapes (not groups)."); + SPLPEItem *item = const_cast<SPLPEItem *>(lpeItem); + item->removeCurrentPathEffect(false); + } +} + +Gtk::Widget *LPEFilletChamfer::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = param->param_newWidget(); + if (param->param_key == "radius") { + Inkscape::UI::Widget::Scalar *widg_registered = + Gtk::manage(dynamic_cast<Inkscape::UI::Widget::Scalar *>(widg)); + widg_registered->signal_value_changed().connect( + sigc::mem_fun(*this, &LPEFilletChamfer::updateAmount)); + widg = widg_registered; + if (widg) { + Gtk::Box *scalar_parameter = dynamic_cast<Gtk::Box *>(widg); + std::vector<Gtk::Widget *> childList = scalar_parameter->get_children(); + Gtk::Entry *entry_widget = dynamic_cast<Gtk::Entry *>(childList[1]); + entry_widget->set_width_chars(6); + } + } else if (param->param_key == "chamfer_steps") { + Inkscape::UI::Widget::Scalar *widg_registered = + Gtk::manage(dynamic_cast<Inkscape::UI::Widget::Scalar *>(widg)); + widg_registered->signal_value_changed().connect( + sigc::mem_fun(*this, &LPEFilletChamfer::updateChamferSteps)); + widg = widg_registered; + if (widg) { + Gtk::Box *scalar_parameter = dynamic_cast<Gtk::Box *>(widg); + std::vector<Gtk::Widget *> childList = scalar_parameter->get_children(); + Gtk::Entry *entry_widget = dynamic_cast<Gtk::Entry *>(childList[1]); + entry_widget->set_width_chars(3); + } + } else if (param->param_key == "only_selected") { + Gtk::manage(widg); + } + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + ++it; + } + + Gtk::Box *fillet_container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + Gtk::Button *fillet = Gtk::manage(new Gtk::Button(Glib::ustring(_("Fillet")))); + fillet->signal_clicked().connect( + sigc::bind<NodeSatelliteType>(sigc::mem_fun(*this, &LPEFilletChamfer::updateNodeSatelliteType), FILLET)); + + fillet_container->pack_start(*fillet, true, true, 2); + Gtk::Button *inverse_fillet = Gtk::manage(new Gtk::Button(Glib::ustring(_("Inverse fillet")))); + inverse_fillet->signal_clicked().connect(sigc::bind<NodeSatelliteType>( + sigc::mem_fun(*this, &LPEFilletChamfer::updateNodeSatelliteType), INVERSE_FILLET)); + fillet_container->pack_start(*inverse_fillet, true, true, 2); + + Gtk::Box *chamfer_container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + Gtk::Button *chamfer = Gtk::manage(new Gtk::Button(Glib::ustring(_("Chamfer")))); + chamfer->signal_clicked().connect( + sigc::bind<NodeSatelliteType>(sigc::mem_fun(*this, &LPEFilletChamfer::updateNodeSatelliteType), CHAMFER)); + + chamfer_container->pack_start(*chamfer, true, true, 2); + Gtk::Button *inverse_chamfer = Gtk::manage(new Gtk::Button(Glib::ustring(_("Inverse chamfer")))); + inverse_chamfer->signal_clicked().connect(sigc::bind<NodeSatelliteType>( + sigc::mem_fun(*this, &LPEFilletChamfer::updateNodeSatelliteType), INVERSE_CHAMFER)); + chamfer_container->pack_start(*inverse_chamfer, true, true, 2); + + vbox->pack_start(*fillet_container, true, true, 2); + vbox->pack_start(*chamfer_container, true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return vbox; +} + +void LPEFilletChamfer::refreshKnots() +{ + if (nodesatellites_param._knoth) { + nodesatellites_param._knoth->update_knots(); + } +} + +void LPEFilletChamfer::updateAmount() +{ + setSelected(_pathvector_nodesatellites); + double power = radius; + if (!flexible) { + SPDocument *document = getSPDoc(); + Glib::ustring display_unit = document->getDisplayUnit()->abbr.c_str(); + power = Inkscape::Util::Quantity::convert(power, unit.get_abbreviation(), display_unit.c_str()); + } + _pathvector_nodesatellites->updateAmount(power, apply_no_radius, apply_with_radius, only_selected, + use_knot_distance, flexible); + nodesatellites_param.setPathVectorNodeSatellites(_pathvector_nodesatellites); +} + +void LPEFilletChamfer::updateChamferSteps() +{ + setSelected(_pathvector_nodesatellites); + _pathvector_nodesatellites->updateSteps(chamfer_steps, apply_no_radius, apply_with_radius, only_selected); + nodesatellites_param.setPathVectorNodeSatellites(_pathvector_nodesatellites); +} + +void LPEFilletChamfer::updateNodeSatelliteType(NodeSatelliteType nodesatellitetype) +{ + std::map<NodeSatelliteType, gchar const *> nodesatellite_type_to_gchar_map = boost::assign::map_list_of( + FILLET, "F")(INVERSE_FILLET, "IF")(CHAMFER, "C")(INVERSE_CHAMFER, "IC")(INVALID_SATELLITE, "KO"); + mode.param_setValue((Glib::ustring)nodesatellite_type_to_gchar_map.at(nodesatellitetype)); + setSelected(_pathvector_nodesatellites); + _pathvector_nodesatellites->updateNodeSatelliteType(nodesatellitetype, apply_no_radius, apply_with_radius, + only_selected); + nodesatellites_param.setPathVectorNodeSatellites(_pathvector_nodesatellites); +} + +void LPEFilletChamfer::setSelected(PathVectorNodeSatellites *_pathvector_nodesatellites) +{ + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + if (!_pathvector_nodesatellites) { + sp_lpe_item_update_patheffect(sp_lpe_item, false, false); + } else { + Geom::PathVector const pathv = _pathvector_nodesatellites->getPathVector(); + NodeSatellites nodesatellites = _pathvector_nodesatellites->getNodeSatellites(); + for (size_t i = 0; i < nodesatellites.size(); ++i) { + for (size_t j = 0; j < nodesatellites[i].size(); ++j) { + Geom::Curve const &curve_in = pathv[i][j]; + if (only_selected && isNodePointSelected(curve_in.initialPoint()) ){ + nodesatellites[i][j].setSelected(true); + } else { + nodesatellites[i][j].setSelected(false); + } + } + } + _pathvector_nodesatellites->setNodeSatellites(nodesatellites); + } + } +} + +void LPEFilletChamfer::doBeforeEffect(SPLPEItem const *lpeItem) +{ + if (!pathvector_before_effect.empty()) { + //fillet chamfer specific calls + nodesatellites_param.setUseDistance(use_knot_distance); + nodesatellites_param.setCurrentZoom(current_zoom); + //mandatory call + nodesatellites_param.setEffectType(effectType()); + Geom::PathVector const pathv = pathv_to_linear_and_cubic_beziers(pathvector_before_effect); + Geom::PathVector pathvres; + for (const auto &path_it : pathv) { + if (path_it.empty() || count_path_nodes(path_it) < 2) { + continue; + } + Geom::Path::const_iterator curve_it = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + Geom::Path pathresult(curve_it->initialPoint()); + while (curve_it != curve_endit) { + if (pathresult.size()) { + pathresult.setFinal(curve_it->initialPoint()); + } + if (Geom::are_near((*curve_it).initialPoint(), (*curve_it).finalPoint())) { + return; + } + pathresult.append(*curve_it); + ++curve_it; + } + pathresult.close(path_it.closed()); + pathvres.push_back(pathresult); + pathresult.clear(); + } // if are different sizes call to recalculate + NodeSatellites nodesatellites = nodesatellites_param.data(); + if (nodesatellites.empty()) { + doOnApply(lpeItem); // dont want _impl to not update versioning + nodesatellites = nodesatellites_param.data(); + } + bool write = false; + if (_pathvector_nodesatellites) { + size_t number_nodes = count_pathvector_nodes(pathvres); + size_t previous_number_nodes = _pathvector_nodesatellites->getTotalNodeSatellites(); + if (number_nodes != previous_number_nodes) { + double power = radius; + if (!flexible) { + SPDocument *document = getSPDoc(); + Glib::ustring display_unit = document->getDisplayUnit()->abbr.c_str(); + power = Inkscape::Util::Quantity::convert(power, unit.get_abbreviation(), display_unit.c_str()); + } + NodeSatelliteType nodesatellite_type = FILLET; + std::map<std::string, NodeSatelliteType> gchar_map_to_nodesatellite_type = boost::assign::map_list_of( + "F", FILLET)("IF", INVERSE_FILLET)("C", CHAMFER)("IC", INVERSE_CHAMFER)("KO", INVALID_SATELLITE); + auto mode_str = mode.param_getSVGValue(); + std::map<std::string, NodeSatelliteType>::iterator it = + gchar_map_to_nodesatellite_type.find(mode_str.raw()); + if (it != gchar_map_to_nodesatellite_type.end()) { + nodesatellite_type = it->second; + } + NodeSatellite nodesatellite(nodesatellite_type); + nodesatellite.setSteps(chamfer_steps); + nodesatellite.setAmount(power); + nodesatellite.setIsTime(flexible); + nodesatellite.setHasMirror(true); + nodesatellite.setHidden(hide_knots); + _pathvector_nodesatellites->recalculateForNewPathVector(pathvres, nodesatellite); + nodesatellites = _pathvector_nodesatellites->getNodeSatellites(); + write = true; + } + } + + if (_degenerate_hide) { + nodesatellites_param.setGlobalKnotHide(true); + } else { + nodesatellites_param.setGlobalKnotHide(false); + } + for (size_t i = 0; i < nodesatellites.size(); ++i) { + for (size_t j = 0; j < nodesatellites[i].size(); ++j) { + if (pathvres.size() <= i || j >= count_path_nodes(pathvres[i])) { + // we are on the end of a open path + // for the moment we dont want to use + // this nodesatellite so simplest do nothing with it + continue; + } + Geom::Curve const &curve_in = pathvres[i][j]; + if (nodesatellites[i][j].is_time != flexible) { + nodesatellites[i][j].is_time = flexible; + double amount = nodesatellites[i][j].amount; + if (nodesatellites[i][j].is_time) { + double time = timeAtArcLength(amount, curve_in); + nodesatellites[i][j].amount = time; + } else { + double size = arcLengthAt(amount, curve_in); + nodesatellites[i][j].amount = size; + } + } + nodesatellites[i][j].hidden = hide_knots; + if (only_selected && isNodePointSelected(curve_in.initialPoint()) ){ + nodesatellites[i][j].setSelected(true); + } + } + if (pathvres.size() > i && !pathvres[i].closed()) { + nodesatellites[i][0].amount = 0; + nodesatellites[i][count_path_nodes(pathvres[i]) - 1].amount = 0; + } + } + if (!_pathvector_nodesatellites) { + _pathvector_nodesatellites = new PathVectorNodeSatellites(); + } + _pathvector_nodesatellites->setPathVector(pathvres); + _pathvector_nodesatellites->setNodeSatellites(nodesatellites); + nodesatellites_param.setPathVectorNodeSatellites(_pathvector_nodesatellites, write); + size_t number_nodes = count_pathvector_nodes(pathvres); + size_t previous_number_nodes = _pathvector_nodesatellites->getTotalNodeSatellites(); + if (number_nodes != previous_number_nodes) { + doOnApply(lpeItem); // dont want _impl to not update versioning + nodesatellites = nodesatellites_param.data(); + nodesatellites_param.setPathVectorNodeSatellites(_pathvector_nodesatellites, write); + } + Glib::ustring current_unit = Glib::ustring(unit.get_abbreviation()); + if (previous_unit != current_unit && previous_unit != "") { + updateAmount(); + } + if (write) { + nodesatellites_param.reloadKnots(); + } else { + refreshKnots(); + } + previous_unit = current_unit; + } else { + g_warning("LPE Fillet can only be applied to shapes (not groups)."); + } +} + +void +LPEFilletChamfer::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(_hp); +} + +void +LPEFilletChamfer::addChamferSteps(Geom::Path &tmp_path, Geom::Path path_chamfer, Geom::Point end_arc_point, size_t steps) +{ + setSelected(_pathvector_nodesatellites); + double path_subdivision = 1.0 / steps; + for (size_t i = 1; i < steps; i++) { + Geom::Point chamfer_step = path_chamfer.pointAt(path_subdivision * i); + tmp_path.appendNew<Geom::LineSegment>(chamfer_step); + } + tmp_path.appendNew<Geom::LineSegment>(end_arc_point); +} + +Geom::PathVector +LPEFilletChamfer::doEffect_path(Geom::PathVector const &path_in) +{ + const double GAP_HELPER = 0.00001; + Geom::PathVector path_out; + size_t path = 0; + const double K = (4.0 / 3.0) * (sqrt(2.0) - 1.0); + _degenerate_hide = false; + Geom::PathVector const pathv = _pathvector_nodesatellites->getPathVector(); + NodeSatellites nodesatellites = _pathvector_nodesatellites->getNodeSatellites(); + for (const auto &path_it : pathv) { + Geom::Path tmp_path; + + double time0 = 0; + size_t curve = 0; + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + size_t next_index = curve + 1; + if (curve == count_path_nodes(pathv[path]) - 1 && pathv[path].closed()) { + next_index = 0; + } + //append last extreme of paths on open paths + if (curve == count_path_nodes(pathv[path]) - 1 && !pathv[path].closed()) { // the path is open and we are at + // end of path + if (time0 != 1) { // Previous nodesatellite not at 100% amount + Geom::Curve *last_curve = curve_it1->portion(time0, 1); + last_curve->setInitial(tmp_path.finalPoint()); + tmp_path.append(*last_curve); + } + ++curve_it1; + continue; + } + Geom::Curve const &curve_it2 = pathv[path][next_index]; + NodeSatellite nodesatellite = nodesatellites[path][next_index]; + if (Geom::are_near((*curve_it1).initialPoint(), (*curve_it1).finalPoint())) { + _degenerate_hide = true; + g_warning("Knots hidden if consecutive nodes has the same position."); + return path_in; + } + if (!curve) { //curve == 0 + if (!path_it.closed()) { + time0 = 0; + } else { + time0 = nodesatellites[path][0].time(*curve_it1); + } + } + double s = nodesatellite.arcDistance(curve_it2); + double time1 = nodesatellite.time(s, true, (*curve_it1)); + double time2 = nodesatellite.time(curve_it2); + if (time1 <= time0) { + time1 = time0; + } + if (time2 > 1) { + time2 = 1; + } + Geom::Curve *knot_curve_1 = curve_it1->portion(time0, time1); + Geom::Curve *knot_curve_2 = curve_it2.portion(time2, 1); + if (curve > 0) { + knot_curve_1->setInitial(tmp_path.finalPoint()); + } else { + tmp_path.start((*curve_it1).pointAt(time0)); + } + + Geom::Point start_arc_point = knot_curve_1->finalPoint(); + Geom::Point end_arc_point = curve_it2.pointAt(time2); + //add a gap helper + if (time2 == 1) { + end_arc_point = curve_it2.pointAt(time2 - GAP_HELPER); + } + if (time1 == time0) { + start_arc_point = curve_it1->pointAt(time1 + GAP_HELPER); + } + + double k1 = distance(start_arc_point, curve_it1->finalPoint()) * K; + double k2 = distance(curve_it2.initialPoint(), end_arc_point) * K; + Geom::CubicBezier const *cubic_1 = dynamic_cast<Geom::CubicBezier const *>(&*knot_curve_1); + Geom::CubicBezier const *cubic_2 = dynamic_cast<Geom::CubicBezier const *>(&*knot_curve_2); + Geom::Ray ray_1(start_arc_point, curve_it1->finalPoint()); + Geom::Ray ray_2(curve_it2.initialPoint(), end_arc_point); + if (cubic_1) { + ray_1.setPoints((*cubic_1)[2], start_arc_point); + } + if (cubic_2) { + ray_2.setPoints(end_arc_point, (*cubic_2)[1]); + } + bool ccw_toggle = cross(curve_it1->finalPoint() - start_arc_point, end_arc_point - start_arc_point) < 0; + double angle = angle_between(ray_1, ray_2, ccw_toggle); + double handle_angle_1 = ray_1.angle() - angle; + double handle_angle_2 = ray_2.angle() + angle; + if (ccw_toggle) { + handle_angle_1 = ray_1.angle() + angle; + handle_angle_2 = ray_2.angle() - angle; + } + Geom::Point handle_1 = Geom::Point::polar(ray_1.angle(), k1) + start_arc_point; + Geom::Point handle_2 = end_arc_point - Geom::Point::polar(ray_2.angle(), k2); + Geom::Point inverse_handle_1 = Geom::Point::polar(handle_angle_1, k1) + start_arc_point; + Geom::Point inverse_handle_2 = end_arc_point - Geom::Point::polar(handle_angle_2, k2); + if (time0 == 1) { + handle_1 = start_arc_point; + inverse_handle_1 = start_arc_point; + } + //remove gap helper + if (time2 == 1) { + end_arc_point = curve_it2.pointAt(time2); + } + if (time1 == time0) { + start_arc_point = curve_it1->pointAt(time0); + } + if (time1 != 1 && !Geom::are_near(angle,Geom::rad_from_deg(360))) { + if (time1 != time0 || (time1 == 1 && time0 == 1)) { + if (!knot_curve_1->isDegenerate()) { + tmp_path.append(*knot_curve_1); + } + } + NodeSatelliteType type = nodesatellite.nodesatellite_type; + size_t steps = nodesatellite.steps; + if (!steps) steps = 1; + Geom::Line const x_line(Geom::Point(0, 0), Geom::Point(1, 0)); + Geom::Line const angled_line(start_arc_point, end_arc_point); + double arc_angle = Geom::angle_between(x_line, angled_line); + double radius = Geom::distance(start_arc_point, middle_point(start_arc_point, end_arc_point)) / + sin(angle / 2.0); + Geom::Coord rx = radius; + Geom::Coord ry = rx; + bool eliptical = (is_straight_curve(*curve_it1) && + is_straight_curve(curve_it2) && method != FM_BEZIER) || + method == FM_ARC; + switch (type) { + case CHAMFER: + { + Geom::Path path_chamfer; + path_chamfer.start(tmp_path.finalPoint()); + if (eliptical) { + ccw_toggle = ccw_toggle ? false : true; + path_chamfer.appendNew<Geom::EllipticalArc>(rx, ry, arc_angle, false, ccw_toggle, end_arc_point); + } else { + path_chamfer.appendNew<Geom::CubicBezier>(handle_1, handle_2, end_arc_point); + } + addChamferSteps(tmp_path, path_chamfer, end_arc_point, steps); + } + break; + case INVERSE_CHAMFER: + { + Geom::Path path_chamfer; + path_chamfer.start(tmp_path.finalPoint()); + if (eliptical) { + path_chamfer.appendNew<Geom::EllipticalArc>(rx, ry, arc_angle, false, ccw_toggle, end_arc_point); + } else { + path_chamfer.appendNew<Geom::CubicBezier>(inverse_handle_1, inverse_handle_2, end_arc_point); + } + addChamferSteps(tmp_path, path_chamfer, end_arc_point, steps); + } + break; + case INVERSE_FILLET: + { + if (eliptical) { + bool side = false; + if (helperpath && !getSPDoc()->is_yaxisdown()) { + side = true; + ccw_toggle = ccw_toggle ? false : true; + } + tmp_path.appendNew<Geom::EllipticalArc>(rx, ry, arc_angle, side, ccw_toggle, end_arc_point); + } else { + tmp_path.appendNew<Geom::CubicBezier>(inverse_handle_1, inverse_handle_2, end_arc_point); + } + } + break; + default: //fillet + { + if (eliptical) { + bool side = false; + if (helperpath && !getSPDoc()->is_yaxisdown()) { + side = true; + } else { + ccw_toggle = ccw_toggle ? false : true; + } + tmp_path.appendNew<Geom::EllipticalArc>(rx, ry, arc_angle, side, ccw_toggle, end_arc_point); + } else { + tmp_path.appendNew<Geom::CubicBezier>(handle_1, handle_2, end_arc_point); + } + } + break; + } + } else { + if (!knot_curve_1->isDegenerate()) { + tmp_path.append(*knot_curve_1); + } + } + curve++; + ++curve_it1; + time0 = time2; + } + if (path_it.closed()) { + tmp_path.close(); + } + path++; + path_out.push_back(tmp_path); + } + if (helperpath) { + _hp = path_out; + return pathvector_after_effect; + } + _hp.clear(); + return path_out; +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offset:((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 : diff --git a/src/live_effects/lpe-fillet-chamfer.h b/src/live_effects/lpe-fillet-chamfer.h new file mode 100644 index 0000000..603b3aa --- /dev/null +++ b/src/live_effects/lpe-fillet-chamfer.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_FILLET_CHAMFER_H +#define INKSCAPE_LPE_FILLET_CHAMFER_H + +/* + * Author(s): + * Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * + * Copyright (C) 2014 Author(s) + * + * Jabiertxof:Thanks to all people help me + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "helper/geom-nodesatellite.h" +#include "helper/geom-pathvector_nodesatellites.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/nodesatellitesarray.h" +#include "live_effects/parameter/unit.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum Filletmethod { + FM_AUTO, + FM_ARC, + FM_BEZIER, + FM_END +}; + +class LPEFilletChamfer : public Effect { +public: + LPEFilletChamfer(LivePathEffectObject *lpeobject); + void doBeforeEffect(SPLPEItem const *lpeItem) override; + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + void doOnApply(SPLPEItem const *lpeItem) override; + Gtk::Widget *newWidget() override; + Geom::Ray getRay(Geom::Point start, Geom::Point end, Geom::Curve *curve, bool reverse); + void addChamferSteps(Geom::Path &tmp_path, Geom::Path path_chamfer, Geom::Point end_arc_point, size_t steps); + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) override; + void updateNodeSatelliteType(NodeSatelliteType nodesatellitetype); + void setSelected(PathVectorNodeSatellites *_pathvector_nodesatellites); + //void convertUnit(); + void updateChamferSteps(); + void updateAmount(); + void refreshKnots(); + bool helperpath; + NodeSatelliteArrayParam nodesatellites_param; + +private: + UnitParam unit; + EnumParam<Filletmethod> method; + ScalarParam radius; + ScalarParam chamfer_steps; + BoolParam flexible; + HiddenParam mode; + BoolParam only_selected; + BoolParam use_knot_distance; + BoolParam hide_knots; + BoolParam apply_no_radius; + BoolParam apply_with_radius; + bool _degenerate_hide; + PathVectorNodeSatellites *_pathvector_nodesatellites; + Geom::PathVector _hp; + Glib::ustring previous_unit; + LPEFilletChamfer(const LPEFilletChamfer &); + LPEFilletChamfer &operator=(const LPEFilletChamfer &); + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-gears.cpp b/src/live_effects/lpe-gears.cpp new file mode 100644 index 0000000..d636654 --- /dev/null +++ b/src/live_effects/lpe-gears.cpp @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright 2006 Michael G. Sloan <mgsloan@gmail.com> + * Copyright 2006 Aaron Spike <aaron@ekips.org> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-gears.h" +#include <2geom/bezier-to-sbasis.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +using namespace Geom; + +class Gear { +public: + // pitch circles touch on two properly meshed gears + // all measurements are taken from the pitch circle + double pitch_diameter() {return (_number_of_teeth * _module) / M_PI;} + double pitch_radius() {return pitch_diameter() / 2.0;} + void pitch_radius(double R) {_module = (2 * M_PI * R) / _number_of_teeth;} + + // base circle serves as the basis for the involute toothe profile + double base_diameter() {return pitch_diameter() * cos(_pressure_angle);} + double base_radius() {return base_diameter() / 2.0;} + + // diametrical pitch + double diametrical_pitch() {return _number_of_teeth / pitch_diameter();} + + // height of the tooth above the pitch circle + double addendum() {return 1.0 / diametrical_pitch();} + // depth of the tooth below the pitch circle + double dedendum() {return addendum() + _clearance;} + + // root circle specifies the bottom of the fillet between teeth + double root_radius() {return pitch_radius() - dedendum();} + double root_diameter() {return root_radius() * 2.0;} + + // outer circle is the outside diameter of the gear + double outer_radius() {return pitch_radius() + addendum();} + double outer_diameter() {return outer_radius() * 2.0;} + + // angle covered by the tooth on the pitch circle + double tooth_thickness_angle() {return M_PI / _number_of_teeth;} + + Geom::Point centre() {return _centre;} + void centre(Geom::Point c) {_centre = c;} + + double angle() {return _angle;} + void angle(double a) {_angle = a;} + + int number_of_teeth() {return _number_of_teeth;} + + Geom::Path path(); + Gear spawn(Geom::Point p); + + Gear(int n, double m, double phi) + : _number_of_teeth(n) + , _pressure_angle(phi) + , _module(m) + { + } +private: + int _number_of_teeth; + double _pressure_angle; + double _module; + double _clearance = 0.0; + double _angle = 0.0; + Geom::Point _centre; + D2<SBasis> _involute(double start, double stop) { + D2<SBasis> B; + D2<SBasis> I; + Linear bo = Linear(start,stop); + + B[0] = cos(bo,2); + B[1] = sin(bo,2); + + I = B - Linear(0,1) * derivative(B); + I = I*base_radius() + _centre; + return I; + } + D2<SBasis> _arc(double start, double stop, double R) { + D2<SBasis> B; + Linear bo = Linear(start,stop); + + B[0] = cos(bo,2); + B[1] = sin(bo,2); + + B = B*R + _centre; + return B; + } + // angle of the base circle used to create the involute to a certain radius + double involute_swath_angle(double R) { + if (R <= base_radius()) return 0.0; + return sqrt(R*R - base_radius()*base_radius())/base_radius(); + } + + // angle of the base circle between the origin of the involute and the intersection on another radius + double involute_intersect_angle(double R) { + if (R <= base_radius()) return 0.0; + return (sqrt(R*R - base_radius()*base_radius())/base_radius()) - acos(base_radius()/R); + } +}; + +static void +makeContinuous(D2<SBasis> &a, Point const b) { + for(unsigned d=0;d<2;d++) + a[d][0][0] = b[d]; +} + +Geom::Path Gear::path() { + Geom::Path pb; + + // angle covered by a full tooth and fillet + double tooth_rotation = 2.0 * tooth_thickness_angle(); + // angle covered by an involute + double involute_advance = involute_intersect_angle(outer_radius()) - involute_intersect_angle(root_radius()); + // angle covered by the tooth tip + double tip_advance = tooth_thickness_angle() - (2 * (involute_intersect_angle(outer_radius()) - involute_intersect_angle(pitch_radius()))); + // angle covered by the toothe root + double root_advance = (tooth_rotation - tip_advance) - (2.0 * involute_advance); + // begin drawing the involute at t if the root circle is larger than the base circle + double involute_t = involute_swath_angle(root_radius())/involute_swath_angle(outer_radius()); + + //rewind angle to start drawing from the leading edge of the tooth + double first_tooth_angle = _angle - ((0.5 * tip_advance) + involute_advance); + + Geom::Point prev; + for (int i=0; i < _number_of_teeth; i++) + { + double cursor = first_tooth_angle + (i * tooth_rotation); + + D2<SBasis> leading_I = compose(_involute(cursor, cursor + involute_swath_angle(outer_radius())), Linear(involute_t,1)); + if(i != 0) makeContinuous(leading_I, prev); + pb.append(SBasisCurve(leading_I)); + cursor += involute_advance; + prev = leading_I.at1(); + + D2<SBasis> tip = _arc(cursor, cursor+tip_advance, outer_radius()); + makeContinuous(tip, prev); + pb.append(SBasisCurve(tip)); + cursor += tip_advance; + prev = tip.at1(); + + cursor += involute_advance; + D2<SBasis> trailing_I = compose(_involute(cursor, cursor - involute_swath_angle(outer_radius())), Linear(1,involute_t)); + makeContinuous(trailing_I, prev); + pb.append(SBasisCurve(trailing_I)); + prev = trailing_I.at1(); + + if (base_radius() > root_radius()) { + Geom::Point leading_start = trailing_I.at1(); + Geom::Point leading_end = (root_radius() * unit_vector(leading_start - _centre)) + _centre; + prev = leading_end; + pb.appendNew<LineSegment>(leading_end); + } + + D2<SBasis> root = _arc(cursor, cursor+root_advance, root_radius()); + makeContinuous(root, prev); + pb.append(SBasisCurve(root)); + //cursor += root_advance; + prev = root.at1(); + + if (base_radius() > root_radius()) { + Geom::Point trailing_start = root.at1(); + Geom::Point trailing_end = (base_radius() * unit_vector(trailing_start - _centre)) + _centre; + pb.appendNew<LineSegment>(trailing_end); + prev = trailing_end; + } + } + + return pb; +} + +Gear Gear::spawn(Geom::Point p) { + double radius = Geom::distance(this->centre(), p) - this->pitch_radius(); + int N = (int) floor( (radius / this->pitch_radius()) * this->number_of_teeth() ); + + Gear gear(N, _module, _pressure_angle); + gear.centre(p); + + double a = atan2(p - this->centre()); + double new_angle = 0.0; + if (gear.number_of_teeth() % 2 == 0) + new_angle -= gear.tooth_thickness_angle(); + new_angle -= (_angle) * (pitch_radius() / gear.pitch_radius()); + new_angle += (a) * (pitch_radius() / gear.pitch_radius()); + gear.angle(new_angle + a); + return gear; +} + + + +// ################################################################# + + + +namespace Inkscape { +namespace LivePathEffect { + + +LPEGears::LPEGears(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + teeth(_("_Teeth:"), _("The number of teeth"), "teeth", &wr, this, 10), + phi(_("_Phi:"), _("Tooth pressure angle (typically 20-25 deg). The ratio of teeth not in contact."), "phi", &wr, this, 5), + min_radius(_("Min Radius:"), _("Minimum radius, low values can be slow"), "min_radius", &wr, this, 5.0) +{ + /* Tooth pressure angle: The angle between the tooth profile and a perpendicular to the pitch + * circle, usually at the point where the pitch circle meets the tooth profile. Standard angles + * are 20 and 25 degrees. The pressure angle affects the force that tends to separate mating + * gears. A high pressure angle means that higher ratio of teeth not in contact. However, this + * allows the teeth to have higher capacity and also allows fewer teeth without undercutting. + */ + + teeth.param_make_integer(); + teeth.param_set_range(3, 1e10); + min_radius.param_set_range(0.01, std::numeric_limits<double>::max()); + registerParameter(&teeth); + registerParameter(&phi); + registerParameter(&min_radius); +} + +LPEGears::~LPEGears() += default; + +Geom::PathVector +LPEGears::doEffect_path (Geom::PathVector const &path_in) +{ + Geom::PathVector path_out; + Geom::Path gearpath = path_in[0]; + + Geom::Path::iterator it(gearpath.begin()); + if ( it == gearpath.end() ) return path_out; + + Gear * gear = new Gear(teeth, 200.0, phi * M_PI / 180); + Geom::Point gear_centre = (*it).finalPoint(); + gear->centre(gear_centre); + gear->angle(atan2((*it).initialPoint() - gear_centre)); + + ++it; + if ( it == gearpath.end() ) return path_out; + double radius = Geom::distance(gear_centre, (*it).finalPoint()); + radius = radius < min_radius?min_radius:radius; + gear->pitch_radius(radius); + + path_out.push_back( gear->path()); + + for (++it; it != gearpath.end() ; ++it) { + if (are_near((*it).initialPoint(), (*it).finalPoint())) { + continue; + } + // iterate through Geom::Curve in path_in + Gear* gearnew = new Gear(gear->spawn( (*it).finalPoint() )); + path_out.push_back( gearnew->path() ); + delete gear; + gear = gearnew; + } + delete gear; + + return path_out; +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-gears.h b/src/live_effects/lpe-gears.h new file mode 100644 index 0000000..eb5ec8a --- /dev/null +++ b/src/live_effects/lpe-gears.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_GEARS_H +#define INKSCAPE_LPE_GEARS_H + +/* + * Inkscape::LPEGears + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEGears : public Effect { +public: + LPEGears(LivePathEffectObject *lpeobject); + ~LPEGears() override; + + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + +private: + ScalarParam teeth; + ScalarParam phi; + ScalarParam min_radius; + + LPEGears(const LPEGears&) = delete; + LPEGears& operator=(const LPEGears&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-interpolate.cpp b/src/live_effects/lpe-interpolate.cpp new file mode 100644 index 0000000..7456c16 --- /dev/null +++ b/src/live_effects/lpe-interpolate.cpp @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE interpolate implementation + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007-2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-interpolate.h" +#include <2geom/sbasis-to-bezier.h> +#include "display/curve.h" +#include "object/sp-path.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + + +namespace Inkscape { +namespace LivePathEffect { + +LPEInterpolate::LPEInterpolate(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , trajectory_path(_("Trajectory:"), _("Path along which intermediate steps are created."), "trajectory", &wr, this, + "M0,0 L0,0") + , number_of_steps(_("Steps_:"), _("Determines the number of steps from start to end path."), "steps", &wr, this, 5) + , equidistant_spacing(_("E_quidistant spacing"), + _("If true, the spacing between intermediates is constant along the length of the path. If " + "false, the distance depends on the location of the nodes of the trajectory path."), + "equidistant_spacing", &wr, this, true) +{ + show_orig_path = true; + + registerParameter(&trajectory_path); + registerParameter(&equidistant_spacing); + registerParameter(&number_of_steps); + + number_of_steps.param_make_integer(); + number_of_steps.param_set_range(2, std::numeric_limits<gint>::max()); +} + +LPEInterpolate::~LPEInterpolate() = default; + + +bool +LPEInterpolate::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + trajectory_path.reload(); + return false; +} + + +void LPEInterpolate::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) { + trajectory_path.param_transform_multiply(postmul, false); + } +} + +/* + * interpolate path_in[0] to path_in[1] + */ +Geom::PathVector LPEInterpolate::doEffect_path(Geom::PathVector const &path_in) +{ + if ((path_in.size() < 2) || (number_of_steps < 2)) { + return path_in; + } + // Don't allow empty path parameter: + if (trajectory_path.get_pathvector().empty()) { + return path_in; + } + if (is_load) { + trajectory_path.reload(); + } + + Geom::PathVector path_out; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_A = path_in[0].toPwSb(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_B = path_in[1].toPwSb(); + + // Transform both paths to (0,0) midpoint, so they can easily be positioned along interpolate_path + Geom::OptRect bounds_A = Geom::bounds_exact(pwd2_A); + if (bounds_A) { + pwd2_A -= bounds_A->midpoint(); + } + Geom::OptRect bounds_B = Geom::bounds_exact(pwd2_B); + if (bounds_B) { + pwd2_B -= bounds_B->midpoint(); + } + + // Make sure both paths have the same number of segments and cuts at the same locations + pwd2_B.setDomain(pwd2_A.domain()); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pA = Geom::partition(pwd2_A, pwd2_B.cuts); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pB = Geom::partition(pwd2_B, pwd2_A.cuts); + + auto trajectory = calculate_trajectory(bounds_A, bounds_B); + + Geom::Interval trajectory_domain = trajectory.domain(); + + for (int i = 0; i < number_of_steps; ++i) { + double fraction = i / (number_of_steps - 1); + + Geom::Piecewise<Geom::D2<Geom::SBasis> > pResult = pA * (1 - fraction) + pB * fraction; + pResult += trajectory.valueAt(trajectory_domain.min() + fraction * trajectory_domain.extent()); + + Geom::PathVector pathv = Geom::path_from_piecewise(pResult, LPE_CONVERSION_TOLERANCE); + path_out.push_back(pathv[0]); + } + + return path_out; +} + + +// returns the lpe parameter trajectory_path, transformed so that it starts at the +// bounding box center of the first path and ends at the bounding box center of the +// second path +Geom::Piecewise<Geom::D2<Geom::SBasis> > LPEInterpolate::calculate_trajectory(Geom::OptRect bounds_A, + Geom::OptRect bounds_B) +{ + Geom::Affine affine = trajectory_path.get_relative_affine(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > trajectory = trajectory_path.get_pathvector()[0].toPwSb() * affine; + + if (equidistant_spacing) { + trajectory = Geom::arc_length_parametrization(trajectory); + } + + if (!bounds_A || !bounds_B) { + return trajectory; + } + + auto trajectory_start = trajectory.firstValue(); + auto trajectory_end = trajectory.lastValue(); + + auto midpoint_A = bounds_A->midpoint(); + auto midpoint_B = bounds_B->midpoint(); + + Geom::Ray original(trajectory_start, trajectory_end); + Geom::Ray transformed(midpoint_A, midpoint_B); + + double rotation = transformed.angle() - original.angle(); + double scale = Geom::distance(midpoint_A, midpoint_B) / Geom::distance(trajectory_start, trajectory_end); + + Geom::Affine transformation; + + transformation *= Geom::Translate(-trajectory_start); + transformation *= Geom::Scale(scale, scale); + transformation *= Geom::Rotate(rotation); + + transformation *= Geom::Translate(midpoint_A); + + return trajectory * transformation; +} + +void LPEInterpolate::resetDefaults(SPItem const *item) +{ + Effect::resetDefaults(item); + + if (!SP_IS_PATH(item)) + return; + + SPCurve const *crv = SP_PATH(item)->curveForEdit(); + Geom::PathVector const &pathv = crv->get_pathvector(); + if ((pathv.size() < 2)) + return; + + Geom::OptRect bounds_A = pathv[0].boundsExact(); + Geom::OptRect bounds_B = pathv[1].boundsExact(); + + if (bounds_A && bounds_B) { + Geom::PathVector traj_pathv; + traj_pathv.push_back(Geom::Path()); + traj_pathv[0].start(bounds_A->midpoint()); + traj_pathv[0].appendNew<Geom::LineSegment>(bounds_B->midpoint()); + trajectory_path.set_new_value(traj_pathv, true); + } + else { + trajectory_path.param_set_and_write_default(); + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-interpolate.h b/src/live_effects/lpe-interpolate.h new file mode 100644 index 0000000..613c7cb --- /dev/null +++ b/src/live_effects/lpe-interpolate.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_INTERPOLATE_H +#define INKSCAPE_LPE_INTERPOLATE_H + +/** \file + * LPE interpolate implementation, see lpe-interpolate.cpp. + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007-2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEInterpolate : public Effect { + public: + LPEInterpolate(LivePathEffectObject *lpeobject); + ~LPEInterpolate() override; + + bool doOnOpen(SPLPEItem const *lpeitem) override; + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + void resetDefaults(SPItem const *item) override; + + private: + PathParam trajectory_path; + ScalarParam number_of_steps; + BoolParam equidistant_spacing; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > calculate_trajectory(Geom::OptRect bounds_A, Geom::OptRect bounds_B); + + LPEInterpolate(const LPEInterpolate &) = delete; + LPEInterpolate &operator=(const LPEInterpolate &) = delete; +}; + +} // namespace LivePathEffect +} // namespace Inkscape + +#endif // INKSCAPE_LPE_INTERPOLATE_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 : diff --git a/src/live_effects/lpe-interpolate_points.cpp b/src/live_effects/lpe-interpolate_points.cpp new file mode 100644 index 0000000..1d84811 --- /dev/null +++ b/src/live_effects/lpe-interpolate_points.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE interpolate_points implementation + * Interpolates between knots of the input path. + */ +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2014 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-interpolate_points.h" +#include "live_effects/lpe-powerstroke-interpolators.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + + +static const Util::EnumData<unsigned> InterpolatorTypeData[] = { + {Geom::Interpolate::INTERP_LINEAR , N_("Linear"), "Linear"}, + {Geom::Interpolate::INTERP_CUBICBEZIER , N_("CubicBezierFit"), "CubicBezierFit"}, + {Geom::Interpolate::INTERP_CUBICBEZIER_JOHAN , N_("CubicBezierJohan"), "CubicBezierJohan"}, + {Geom::Interpolate::INTERP_SPIRO , N_("SpiroInterpolator"), "SpiroInterpolator"}, + {Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM, N_("Centripetal Catmull-Rom"), "CentripetalCatmullRom"} +}; +static const Util::EnumDataConverter<unsigned> InterpolatorTypeConverter(InterpolatorTypeData, sizeof(InterpolatorTypeData)/sizeof(*InterpolatorTypeData)); + +LPEInterpolatePoints::LPEInterpolatePoints(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , interpolator_type( + _("Interpolator type:"), + _("Determines which kind of interpolator will be used to interpolate between stroke width along the path"), + "interpolator_type", InterpolatorTypeConverter, &wr, this, Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM) +{ + show_orig_path = false; + + registerParameter( &interpolator_type ); +} + +LPEInterpolatePoints::~LPEInterpolatePoints() += default; + +Geom::PathVector +LPEInterpolatePoints::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + std::unique_ptr<Geom::Interpolate::Interpolator> interpolator( Geom::Interpolate::Interpolator::create(static_cast<Geom::Interpolate::InterpolatorType>(interpolator_type.get_value())) ); + + for(const auto & path_it : path_in) { + if (path_it.empty()) + continue; + + if (path_it.closed()) { + g_warning("Interpolate points LPE currently ignores whether path is closed or not."); + } + + std::vector<Geom::Point> pts; + pts.push_back(path_it.initialPoint()); + + for (Geom::Path::const_iterator it = path_it.begin(), e = path_it.end_default(); it != e; ++it) { + pts.push_back((*it).finalPoint()); + } + + Geom::Path path = interpolator->interpolateToPath(pts); + + path_out.push_back(path); + } + + return path_out; +} + + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-interpolate_points.h b/src/live_effects/lpe-interpolate_points.h new file mode 100644 index 0000000..9b563cc --- /dev/null +++ b/src/live_effects/lpe-interpolate_points.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_INTERPOLATEPOINTS_H +#define INKSCAPE_LPE_INTERPOLATEPOINTS_H + +/** \file + * LPE interpolate_points implementation, see lpe-interpolate_points.cpp. + */ + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2014 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEInterpolatePoints : public Effect { +public: + LPEInterpolatePoints(LivePathEffectObject *lpeobject); + ~LPEInterpolatePoints() override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + EnumParam<unsigned> interpolator_type; + + LPEInterpolatePoints(const LPEInterpolatePoints&) = delete; + LPEInterpolatePoints& operator=(const LPEInterpolatePoints&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif // INKSCAPE_LPE_INTERPOLATEPOINTS_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 : diff --git a/src/live_effects/lpe-jointype.cpp b/src/live_effects/lpe-jointype.cpp new file mode 100644 index 0000000..cd241a5 --- /dev/null +++ b/src/live_effects/lpe-jointype.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * + * Liam P White + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/fill-conversion.h" +#include "helper/geom-pathstroke.h" + +#include "desktop-style.h" + +#include "display/curve.h" + +#include "object/sp-item-group.h" +#include "object/sp-shape.h" +#include "style.h" + +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +#include <2geom/elliptical-arc.h> + +#include "lpe-jointype.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<unsigned> JoinTypeData[] = { + // clang-format off + {JOIN_BEVEL, N_("Beveled"), "bevel"}, + {JOIN_ROUND, N_("Rounded"), "round"}, + {JOIN_MITER, N_("Miter"), "miter"}, + {JOIN_MITER_CLIP, N_("Miter Clip"), "miter-clip"}, + {JOIN_EXTRAPOLATE, N_("Extrapolated arc"), "extrp_arc"}, + {JOIN_EXTRAPOLATE1, N_("Extrapolated arc Alt1"), "extrp_arc1"}, + {JOIN_EXTRAPOLATE2, N_("Extrapolated arc Alt2"), "extrp_arc2"}, + {JOIN_EXTRAPOLATE3, N_("Extrapolated arc Alt3"), "extrp_arc3"}, + // clang-format on +}; + +static const Util::EnumData<unsigned> CapTypeData[] = { + {BUTT_FLAT, N_("Butt"), "butt"}, + {BUTT_ROUND, N_("Rounded"), "round"}, + {BUTT_SQUARE, N_("Square"), "square"}, + {BUTT_PEAK, N_("Peak"), "peak"}, + //{BUTT_LEANED, N_("Leaned"), "leaned"} +}; + +static const Util::EnumDataConverter<unsigned> CapTypeConverter(CapTypeData, sizeof(CapTypeData)/sizeof(*CapTypeData)); +static const Util::EnumDataConverter<unsigned> JoinTypeConverter(JoinTypeData, sizeof(JoinTypeData)/sizeof(*JoinTypeData)); + +LPEJoinType::LPEJoinType(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + line_width(_("Line width"), _("Thickness of the stroke"), "line_width", &wr, this, 1.), + linecap_type(_("Line cap"), _("The end shape of the stroke"), "linecap_type", CapTypeConverter, &wr, this, BUTT_FLAT), + linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", JoinTypeConverter, &wr, this, JOIN_EXTRAPOLATE), + //start_lean(_("Start path lean"), _("Start path lean"), "start_lean", &wr, this, 0.), + //end_lean(_("End path lean"), _("End path lean"), "end_lean", &wr, this, 0.), + miter_limit(_("Miter limit:"), _("Maximum length of the miter join (in units of stroke width)"), "miter_limit", &wr, this, 100.), + attempt_force_join(_("Force miter"), _("Overrides the miter limit and forces a join."), "attempt_force_join", &wr, this, true) +{ + show_orig_path = true; + registerParameter(&linecap_type); + registerParameter(&line_width); + registerParameter(&linejoin_type); + //registerParameter(&start_lean); + //registerParameter(&end_lean); + registerParameter(&miter_limit); + registerParameter(&attempt_force_join); + //start_lean.param_set_range(-1,1); + //start_lean.param_set_increments(0.1, 0.1); + //start_lean.param_set_digits(4); + //end_lean.param_set_range(-1,1); + //end_lean.param_set_increments(0.1, 0.1); + //end_lean.param_set_digits(4); +} + +LPEJoinType::~LPEJoinType() += default; + +void LPEJoinType::doOnApply(SPLPEItem const* lpeitem) +{ + if (!SP_IS_SHAPE(lpeitem)) { + return; + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + auto item = dynamic_cast<SPShape *>(lpeitem_mutable); + double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed : 1.; + + lpe_shape_convert_stroke_and_fill(item); + + Glib::ustring pref_path = (Glib::ustring)"/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring)"/" + + (Glib::ustring)"line_width"; + + bool valid = prefs->getEntry(pref_path).isValid(); + + if (!valid) { + line_width.param_set_value(width); + } + + line_width.write_to_SVG(); +} + +void LPEJoinType::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke) { + line_width.param_transform_multiply(postmul, false); + } +} + +//from LPEPowerStroke -- sets stroke color from existing fill color + +void LPEJoinType::doOnRemove(SPLPEItem const* lpeitem) +{ + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + auto shape = dynamic_cast<SPShape *>(lpeitem_mutable); + + if (!shape) { + return; + } + + lpe_shape_revert_stroke_and_fill(shape, line_width); +} + +Geom::PathVector LPEJoinType::doEffect_path(Geom::PathVector const & path_in) +{ + Geom::PathVector ret; + for (const auto & i : path_in) { + Geom::PathVector tmp = Inkscape::outline(i, line_width, + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), + static_cast<LineJoinType>(linejoin_type.get_value()), + static_cast<LineCapType>(linecap_type.get_value())); + ret.insert(ret.begin(), tmp.begin(), tmp.end()); + } + + return ret; +} + +} // namespace LivePathEffect +} // namespace Inkscape + +/* + 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 : diff --git a/src/live_effects/lpe-jointype.h b/src/live_effects/lpe-jointype.h new file mode 100644 index 0000000..5d5e20c --- /dev/null +++ b/src/live_effects/lpe-jointype.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Liam P White + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file COPYING for more information + */ + +#ifndef INKSCAPE_LPE_JOINTYPE_H +#define INKSCAPE_LPE_JOINTYPE_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/enum.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEJoinType : public Effect { +public: + LPEJoinType(LivePathEffectObject *lpeobject); + ~LPEJoinType() override; + + void doOnApply(SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + LPEJoinType(const LPEJoinType&) = delete; + LPEJoinType& operator=(const LPEJoinType&) = delete; + + ScalarParam line_width; + EnumParam<unsigned> linecap_type; + EnumParam<unsigned> linejoin_type; + //ScalarParam start_lean; + //ScalarParam end_lean; + ScalarParam miter_limit; + BoolParam attempt_force_join; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-knot.cpp b/src/live_effects/lpe-knot.cpp new file mode 100644 index 0000000..602b857 --- /dev/null +++ b/src/live_effects/lpe-knot.cpp @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE knot effect implementation. + */ +/* Authors: + * Jean-Francois Barraud <jf.barraud@gmail.com> + * Abhishek Sharma + * Johan Engelen + * + * Copyright (C) 2007-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gdk/gdk.h> +#include <optional> + +#include <2geom/sbasis-to-bezier.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/basic-intersection.h> + +#include "lpe-knot.h" + +// for change crossing undo +#include "document.h" +#include "document-undo.h" + +#include "style.h" + +#include "display/curve.h" + +#include "helper/geom.h" + +#include "object/sp-path.h" +#include "object/sp-shape.h" + +#include "ui/icon-names.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +class KnotHolderEntityCrossingSwitcher : public LPEKnotHolderEntity { +public: + KnotHolderEntityCrossingSwitcher(LPEKnot *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + void knot_click(guint state) override; +}; + + +static Geom::Path::size_type size_nondegenerate(Geom::Path const &path) { + Geom::Path::size_type retval = path.size_default(); + const Geom::Curve &closingline = path.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! + retval = path.size_open(); + } + return retval; +} + +//--------------------------------------------------------------------------- +//LPEKnot specific Interval manipulation. +//--------------------------------------------------------------------------- + +//remove an interval from an union of intervals. +//TODO: is it worth moving it to 2Geom? +static +std::vector<Geom::Interval> complementOf(Geom::Interval I, std::vector<Geom::Interval> domain){ + std::vector<Geom::Interval> ret; + if (!domain.empty()) { + double min = domain.front().min(); + double max = domain.back().max(); + Geom::Interval I1 = Geom::Interval(min,I.min()); + Geom::Interval I2 = Geom::Interval(I.max(),max); + + for (auto i : domain){ + std::optional<Geom::Interval> I1i = intersect(i,I1); + if (I1i && !I1i->isSingular()) ret.push_back(*I1i); + std::optional<Geom::Interval> I2i = intersect(i,I2); + if (I2i && !I2i->isSingular()) ret.push_back(*I2i); + } + } + return ret; +} + +//find the time interval during which patha is hidden by pathb near a given crossing. +// Warning: not accurate! +static +Geom::Interval +findShadowedTime(Geom::Path const &patha, std::vector<Geom::Point> const &pt_and_dir, + double const ta, double const width){ + using namespace Geom; + Point T = unit_vector(pt_and_dir[1]); + Point N = T.cw(); + //Point A = pt_and_dir[0] - 3 * width * T; + //Point B = A+6*width*T; + + Affine mat = from_basis( T, N, pt_and_dir[0] ); + mat = mat.inverse(); + Geom::Path p = patha * mat; + + std::vector<double> times; + + //TODO: explore the path fwd/backward from ta (worth?) + for (unsigned i = 0; i < size_nondegenerate(patha); i++){ + D2<SBasis> f = p[i].toSBasis(); + std::vector<double> times_i, temptimes; + temptimes = roots(f[Y]-width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + temptimes = roots(f[Y]+width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + temptimes = roots(f[X]-3*width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + temptimes = roots(f[X]+3*width); + times_i.insert(times_i.end(), temptimes.begin(), temptimes.end() ); + for (double & k : times_i){ + k+=i; + } + times.insert(times.end(), times_i.begin(), times_i.end() ); + } + std::sort( times.begin(), times.end() ); + std::vector<double>::iterator new_end = std::unique( times.begin(), times.end() ); + times.resize( new_end - times.begin() ); + + double tmin = 0, tmax = size_nondegenerate(patha); + double period = size_nondegenerate(patha); + if (!times.empty()){ + unsigned rk = upper_bound( times.begin(), times.end(), ta ) - times.begin(); + if ( rk < times.size() ) + tmax = times[rk]; + else if ( patha.closed() ) + tmax = times[0]+period; + + if ( rk > 0 ) + tmin = times[rk-1]; + else if ( patha.closed() ) + tmin = times.back()-period; + } + return Interval(tmin,tmax); +} + +//--------------------------------------------------------------------------- +//LPEKnot specific Crossing Data manipulation. +//--------------------------------------------------------------------------- + +//Yet another crossing data representation. +// an CrossingPoint stores +// -an intersection point +// -the involved path components +// -for each component, the time at which this crossing occurs + the order of this crossing along the component (when starting from 0). + +namespace LPEKnotNS {//just in case... +CrossingPoints::CrossingPoints(Geom::PathVector const &paths) : std::vector<CrossingPoint>(){ +// std::cout<<"\nCrossingPoints creation from path vector\n"; + for( unsigned i=0; i<paths.size(); i++){ + for( unsigned ii=0; ii < size_nondegenerate(paths[i]); ii++){ + for( unsigned j=i; j<paths.size(); j++){ + for( unsigned jj=(i==j?ii:0); jj < size_nondegenerate(paths[j]); jj++){ + std::vector<std::pair<double,double> > times; + if ( (i==j) && (ii==jj) ) { + +// std::cout<<"--(self int)\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::X] <<"\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::Y] <<"\n"; + + find_self_intersections( times, paths[i][ii].toSBasis() ); + } else { +// std::cout<<"--(pair int)\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::X] <<"\n"; +// std::cout << paths[i][ii].toSBasis()[Geom::Y] <<"\n"; +// std::cout<<"with\n"; +// std::cout << paths[j][jj].toSBasis()[Geom::X] <<"\n"; +// std::cout << paths[j][jj].toSBasis()[Geom::Y] <<"\n"; + + find_intersections( times, paths[i][ii].toSBasis(), paths[j][jj].toSBasis() ); + } + for (auto & time : times){ + //std::cout<<"intersection "<<i<<"["<<ii<<"]("<<times[k].first<<")= "<<j<<"["<<jj<<"]("<<times[k].second<<")\n"; + if ( !std::isnan(time.first) && !std::isnan(time.second) ){ + double zero = 1e-4; + if ( (i==j) && (fabs(time.first+ii - time.second-jj) <= zero) ) + { //this is just end=start of successive curves in a path. + continue; + } + if ( (i==j) && (ii == 0) && (jj == size_nondegenerate(paths[i])-1) + && paths[i].closed() + && (fabs(time.first) <= zero) + && (fabs(time.second - 1) <= zero) ) + {//this is just end=start of a closed path. + continue; + } + CrossingPoint cp; + cp.pt = paths[i][ii].pointAt(time.first); + cp.sign = 1; + cp.i = i; + cp.j = j; + cp.ni = 0; cp.nj=0;//not set yet + cp.ti = time.first + ii; + cp.tj = time.second + jj; + push_back(cp); + }else{ + std::cout<<"ooops: find_(self)_intersections returned NaN:" << std::endl; + //std::cout<<"intersection "<<i<<"["<<ii<<"](NaN)= "<<j<<"["<<jj<<"](NaN)\n"; + } + } + } + } + } + } + for( unsigned i=0; i<paths.size(); i++){ + std::map < double, unsigned > cuts; + for( unsigned k=0; k<size(); k++){ + CrossingPoint cp = (*this)[k]; + if (cp.i == i) cuts[cp.ti] = k; + if (cp.j == i) cuts[cp.tj] = k; + } + unsigned count = 0; + for (auto & cut : cuts){ + if ( ((*this)[cut.second].i == i) && ((*this)[cut.second].ti == cut.first) ){ + (*this)[cut.second].ni = count; + }else{ + (*this)[cut.second].nj = count; + } + count++; + } + } +} + +CrossingPoints::CrossingPoints(std::vector<double> const &input) : std::vector<CrossingPoint>() +{ + if ( (input.size() > 0) && (input.size()%9 == 0) ){ + using namespace Geom; + for( unsigned n=0; n<input.size(); ){ + CrossingPoint cp; + cp.pt[X] = input[n++]; + cp.pt[Y] = input[n++]; + cp.i = input[n++]; + cp.j = input[n++]; + cp.ni = input[n++]; + cp.nj = input[n++]; + cp.ti = input[n++]; + cp.tj = input[n++]; + cp.sign = input[n++]; + push_back(cp); + } + } +} + +std::vector<double> +CrossingPoints::to_vector() +{ + using namespace Geom; + std::vector<double> result; + for( unsigned n=0; n<size(); n++){ + CrossingPoint cp = (*this)[n]; + result.push_back(cp.pt[X]); + result.push_back(cp.pt[Y]); + result.push_back(double(cp.i)); + result.push_back(double(cp.j)); + result.push_back(double(cp.ni)); + result.push_back(double(cp.nj)); + result.push_back(double(cp.ti)); + result.push_back(double(cp.tj)); + result.push_back(double(cp.sign)); + } + return result; +} + +//FIXME: rewrite to check success: return bool, put result in arg. +CrossingPoint +CrossingPoints::get(unsigned const i, unsigned const ni) +{ + for (unsigned k=0; k<size(); k++){ + if ( ( ((*this)[k].i==i) && ((*this)[k].ni==ni) ) + || ( ((*this)[k].j==i) && ((*this)[k].nj==ni) ) ) + { + return (*this)[k]; + } + } + g_warning("LPEKnotNS::CrossingPoints::get error. %uth crossing along string %u not found.",ni,i); + assert(false);//debug purpose... + return CrossingPoint(); +} + +static unsigned +idx_of_nearest(CrossingPoints const &cpts, Geom::Point const &p) +{ + double dist=-1; + unsigned result = cpts.size(); + for (unsigned k=0; k<cpts.size(); k++){ + double dist_k = Geom::L2(p-cpts[k].pt); + if ( (dist < 0) || (dist > dist_k) ) { + result = k; + dist = dist_k; + } + } + return result; +} + +//TODO: Find a way to warn the user when the topology changes. +//TODO: be smarter at guessing the signs when the topology changed? +void +CrossingPoints::inherit_signs(CrossingPoints const &other, int default_value) +{ + bool topo_changed = false; + for (unsigned n=0; n < size(); n++){ + if ( (n < other.size()) + && (other[n].i == (*this)[n].i) + && (other[n].j == (*this)[n].j) + && (other[n].ni == (*this)[n].ni) + && (other[n].nj == (*this)[n].nj) ) + { + (*this)[n].sign = other[n].sign; + } else { + topo_changed = true; + break; + } + } + if (topo_changed) { + //TODO: Find a way to warn the user!! +// std::cout<<"knot topolgy changed!\n"; + for (unsigned n=0; n < size(); n++){ + Geom::Point p = (*this)[n].pt; + unsigned idx = idx_of_nearest(other,p); + if (idx < other.size()) { + (*this)[n].sign = other[idx].sign; + } else { + (*this)[n].sign = default_value; + } + } + } +} + +} + +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- +//LPEKnot effect. +//--------------------------------------------------------------------------- +//--------------------------------------------------------------------------- + + +LPEKnot::LPEKnot(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , + // initialise your parameters here: + interruption_width(_("_Gap length:"), _("Size of hidden region of lower string"), "interruption_width", &wr, this, + 3) + , prop_to_stroke_width( + _("_In units of stroke width"), + _("Gap width is given in multiples of stroke width. When unchecked, document units are used."), + "prop_to_stroke_width", &wr, this, true) + , both(_("_Gaps in both"), _("At path intersections, both parts will have a gap"), "both", &wr, this, false) + , inverse_width(_("_Groups: Inverse"), _("Use other stroke width, useful in groups with different stroke widths"), + "inverse_width", &wr, this, false) + , add_stroke_width("St_roke width", "Add the stroke width to the gap size", "add_stroke_width", &wr, this, + "inkscape_1.0_and_up", true) + , add_other_stroke_width("_Crossing path stroke width", "Add crossed stroke width to the gap size", + "add_other_stroke_width", &wr, this, "inkscape_1.0_and_up", true) + , switcher_size(_("S_witcher size:"), _("Orientation indicator/switcher size"), "switcher_size", &wr, this, 15) + , crossing_points_vector(_("Crossing Signs"), _("Crossing signs"), "crossing_points_vector", &wr, this) + , crossing_points() + , gpaths() + , gstroke_widths() + , selectedCrossing(0) + , switcher(0., 0.) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&switcher_size); + registerParameter(&interruption_width); + registerParameter(&prop_to_stroke_width); + registerParameter(&add_stroke_width); + registerParameter(&both); + registerParameter(&inverse_width); + registerParameter(&add_other_stroke_width); + registerParameter(&crossing_points_vector); + + _provides_knotholder_entities = true; +} + +LPEKnot::~LPEKnot() += default; + +void +LPEKnot::updateSwitcher(){ + if (selectedCrossing < crossing_points.size()){ + switcher = crossing_points[selectedCrossing].pt; + //std::cout<<"placing switcher at "<<switcher<<" \n"; + }else if (crossing_points.size()>0){ + selectedCrossing = 0; + switcher = crossing_points[selectedCrossing].pt; + //std::cout<<"placing switcher at "<<switcher<<" \n"; + }else{ + //std::cout<<"hiding switcher!\n"; + switcher = Geom::Point(Geom::infinity(),Geom::infinity()); + } +} + +Geom::PathVector +LPEKnot::doEffect_path (Geom::PathVector const &path_in) +{ + using namespace Geom; + Geom::PathVector path_out; + + if (gpaths.size()==0){ + return path_in; + } + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + for (const auto & comp : original_pathv){ + + //find the relevant path component in gpaths (required to allow groups!) + //Q: do we always receive the group members in the same order? can we rest on that? + unsigned i0 = 0; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gint precision = prefs->getInt("/options/svgoutput/numericprecision"); + prefs->setInt("/options/svgoutput/numericprecision", 4); // I think this is enough for minor differences + for (i0=0; i0<gpaths.size(); i0++){ + if (sp_svg_write_path(comp) == sp_svg_write_path(gpaths[i0])) + break; + } + prefs->setInt("/options/svgoutput/numericprecision", precision); + if (i0 == gpaths.size() ) {THROW_EXCEPTION("lpe-knot error: group member not recognized");}// this should not happen... + + std::vector<Interval> dom; + dom.emplace_back(0., size_nondegenerate(gpaths[i0])); + for (unsigned p = 0; p < crossing_points.size(); p++){ + if ( (crossing_points[p].i == i0) || (crossing_points[p].j == i0) ) { + unsigned i = crossing_points[p].i; + unsigned j = crossing_points[p].j; + double ti = crossing_points[p].ti; + double tj = crossing_points[p].tj; + + double curveidx, t; + + t = modf(ti, &curveidx); + if(curveidx == size_nondegenerate(gpaths[i]) ) { curveidx--; t = 1.;} + assert(curveidx >= 0 && curveidx < size_nondegenerate(gpaths[i])); + std::vector<Point> flag_i = gpaths[i][curveidx].pointAndDerivatives(t,1); + + t = modf(tj, &curveidx); + if(curveidx == size_nondegenerate(gpaths[j]) ) { curveidx--; t = 1.;} + assert(curveidx >= 0 && curveidx < size_nondegenerate(gpaths[j])); + std::vector<Point> flag_j = gpaths[j][curveidx].pointAndDerivatives(t,1); + + + int geom_sign = ( cross(flag_i[1], flag_j[1]) < 0 ? 1 : -1); + bool i0_is_under = false; + double width = interruption_width; + if ( crossing_points[p].sign * geom_sign > 0 ){ + i0_is_under = ( i == i0 ); + } + else if (crossing_points[p].sign * geom_sign < 0) { + if (j == i0){ + i0_is_under = true; + } + } + i0_is_under = crossing_points[p].sign != 0 && both ? true : i0_is_under; + if (i0_is_under && j == i0) { + // last check of sign makes sure we get different outputs when + // path components are part of the same subpath (i == j) + if (!(i == j && !both && crossing_points[p].sign * geom_sign > 0)) { + std::swap(i, j); + std::swap(ti, tj); + std::swap(flag_i, flag_j); + } + } + if (i0_is_under){ + if ( prop_to_stroke_width.get_value() ) { + if (inverse_width) { + width *= gstroke_widths[j]; + } + else { + width *= gstroke_widths[i]; + } + } + if (add_stroke_width.get_value() == "true") { + width += gstroke_widths[i]; + } + if (add_other_stroke_width.get_value() == "true") { + width += gstroke_widths[j]; + } + Interval hidden = findShadowedTime(gpaths[i0], flag_j, ti, width/2); + double period = size_nondegenerate(gpaths[i0]); + if (hidden.max() > period ) hidden -= period; + if (hidden.min()<0){ + dom = complementOf( Interval(0,hidden.max()) ,dom); + dom = complementOf( Interval(hidden.min()+period, period) ,dom); + }else{ + dom = complementOf(hidden,dom); + } + if (crossing_points[p].i == i0 && crossing_points[p].j == i0 && crossing_points[p].sign != 0 && + both) { + hidden = findShadowedTime(gpaths[i0], flag_i, tj, width / 2); + period = size_nondegenerate(gpaths[i0]); + if (hidden.max() > period) + hidden -= period; + if (hidden.min() < 0) { + dom = complementOf(Interval(0, hidden.max()), dom); + dom = complementOf(Interval(hidden.min() + period, period), dom); + } + else { + dom = complementOf(hidden, dom); + } + } + } + } + } + + //If the all component is hidden, continue. + if (dom.empty()){ + continue; + } + + //If the current path is closed and the last/first point is still there, glue first and last piece. + unsigned beg_comp = 0, end_comp = dom.size(); + if ( gpaths[i0].closed() && (dom.front().min() == 0) && (dom.back().max() == size_nondegenerate(gpaths[i0])) ) { + if ( dom.size() == 1){ + path_out.push_back(gpaths[i0]); + continue; + }else{ + // std::cout<<"fusing first and last component\n"; + ++beg_comp; + --end_comp; + Geom::Path first = gpaths[i0].portion(dom.back()); + //FIXME: stitching should not be necessary (?!?) + first.setStitching(true); + first.append(gpaths[i0].portion(dom.front())); + path_out.push_back(first); + } + } + for (unsigned comp = beg_comp; comp < end_comp; comp++){ + assert(dom.at(comp).min() >=0 && dom.at(comp).max() <= size_nondegenerate(gpaths.at(i0))); + path_out.push_back(gpaths[i0].portion(dom.at(comp))); + } + } + return path_out; +} + + + +//recursively collect gpaths and stroke widths (stolen from "sp-lpe_item.cpp"). +static void +collectPathsAndWidths (SPLPEItem const *lpeitem, Geom::PathVector &paths, std::vector<double> &stroke_widths){ + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + + if (auto group = dynamic_cast<SPGroup *>(lpeitem_mutable)) { + std::vector<SPItem*> item_list = sp_item_group_item_list(group); + for (auto subitem : item_list) { + if (SP_IS_LPE_ITEM(subitem)) { + collectPathsAndWidths(SP_LPE_ITEM(subitem), paths, stroke_widths); + } + } + } else if (auto shape = dynamic_cast<SPShape const *>(lpeitem)) { + SPCurve const *c = shape->curve(); + if (c) { + Geom::PathVector subpaths = pathv_to_linear_and_cubic_beziers(c->get_pathvector()); + for (const auto & subpath : subpaths){ + paths.push_back(subpath); + //FIXME: do we have to be more careful when trying to access stroke width? + stroke_widths.push_back(lpeitem->style->stroke_width.computed); + } + } + } +} + + +void +LPEKnot::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem); + + if (SP_IS_PATH(lpeitem)) { + supplied_path = SP_PATH(lpeitem)->curve()->get_pathvector(); + } + + gpaths.clear(); + gstroke_widths.clear(); + + collectPathsAndWidths(lpeitem, gpaths, gstroke_widths); + +// std::cout<<"\nPaths on input:\n"; +// for (unsigned i=0; i<gpaths.size(); i++){ +// for (unsigned ii=0; ii<gpaths[i].size(); ii++){ +// std::cout << gpaths[i][ii].toSBasis()[Geom::X] <<"\n"; +// std::cout << gpaths[i][ii].toSBasis()[Geom::Y] <<"\n"; +// std::cout<<"--\n"; +// } +// } + + //std::cout<<"crossing_pts_vect: "<<crossing_points_vector.param_getSVGValue()<<".\n"; + //std::cout<<"prop_to_stroke_width: "<<prop_to_stroke_width.param_getSVGValue()<<".\n"; + + LPEKnotNS::CrossingPoints old_crdata(crossing_points_vector.data()); + +// std::cout<<"\nVectorParam size:"<<crossing_points_vector.data().size()<<"\n"; + +// std::cout<<"\nOld crdata ("<<old_crdata.size()<<"): \n"; +// for (unsigned toto=0; toto<old_crdata.size(); toto++){ +// std::cout<<"("; +// std::cout<<old_crdata[toto].i<<","; +// std::cout<<old_crdata[toto].j<<","; +// std::cout<<old_crdata[toto].ni<<","; +// std::cout<<old_crdata[toto].nj<<","; +// std::cout<<old_crdata[toto].ti<<","; +// std::cout<<old_crdata[toto].tj<<","; +// std::cout<<old_crdata[toto].sign<<"),"; +// } + + //if ( old_crdata.size() > 0 ) std::cout<<"first crossing sign = "<<old_crdata[0].sign<<".\n"; + //else std::cout<<"old data is empty!!\n"; + crossing_points = LPEKnotNS::CrossingPoints(gpaths); +// std::cout<<"\nNew crdata ("<<crossing_points.size()<<"): \n"; +// for (unsigned toto=0; toto<crossing_points.size(); toto++){ +// std::cout<<"("; +// std::cout<<crossing_points[toto].i<<","; +// std::cout<<crossing_points[toto].j<<","; +// std::cout<<crossing_points[toto].ni<<","; +// std::cout<<crossing_points[toto].nj<<","; +// std::cout<<crossing_points[toto].ti<<","; +// std::cout<<crossing_points[toto].tj<<","; +// std::cout<<crossing_points[toto].sign<<"),"; +// } + crossing_points.inherit_signs(old_crdata); + + // Don't write to XML here, only store it in the param itself. Will be written to SVG later + crossing_points_vector.param_setValue(crossing_points.to_vector()); + + updateSwitcher(); +} + +void +LPEKnot::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + using namespace Geom; + double r = switcher_size*.1; + char const * svgd; + //TODO: use a nice path! + if ( (selectedCrossing >= crossing_points.size()) || (crossing_points[selectedCrossing].sign > 0) ) { + //svgd = "M -10,0 A 10 10 0 1 0 0,-10 l 5,-1 -1,2"; + svgd = "m -7.07,7.07 c 3.9,3.91 10.24,3.91 14.14,0 3.91,-3.9 3.91,-10.24 0,-14.14 -3.9,-3.91 -10.24,-3.91 -14.14,0 l 2.83,-4.24 0.7,2.12"; + } else if (crossing_points[selectedCrossing].sign < 0) { + //svgd = "M 10,0 A 10 10 0 1 1 0,-10 l -5,-1 1,2"; + svgd = "m 7.07,7.07 c -3.9,3.91 -10.24,3.91 -14.14,0 -3.91,-3.9 -3.91,-10.24 0,-14.14 3.9,-3.91 10.24,-3.91 14.14,0 l -2.83,-4.24 -0.7,2.12"; + } else { + //svgd = "M 10,0 A 10 10 0 1 0 -10,0 A 10 10 0 1 0 10,0 "; + svgd = "M 10,0 C 10,5.52 5.52,10 0,10 -5.52,10 -10,5.52 -10,0 c 0,-5.52 4.48,-10 10,-10 5.52,0 10,4.48 10,10 z"; + } + PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Affine(r,0,0,r,0,0) * Translate(switcher); + hp_vec.push_back(pathv); +} + +void LPEKnot::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + KnotHolderEntity *e = new KnotHolderEntityCrossingSwitcher(this); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:CrossingSwitcher", + _("Drag to select a crossing, click to flip it, Shift + click to change all crossings, Ctrl + click to " + "reset and change all crossings")); + knotholder->add(e); +}; + + +void +KnotHolderEntityCrossingSwitcher::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint /*state*/) +{ + LPEKnot* lpe = dynamic_cast<LPEKnot *>(_effect); + + lpe->selectedCrossing = idx_of_nearest(lpe->crossing_points,p); + lpe->updateSwitcher(); + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityCrossingSwitcher::knot_get() const +{ + LPEKnot const *lpe = dynamic_cast<LPEKnot const*>(_effect); + return lpe->switcher; +} + +void +KnotHolderEntityCrossingSwitcher::knot_click(guint state) +{ + LPEKnot* lpe = dynamic_cast<LPEKnot *>(_effect); + unsigned s = lpe->selectedCrossing; + if (s < lpe->crossing_points.size()){ + if (state & GDK_SHIFT_MASK){ + for (unsigned p = 0; p < lpe->crossing_points.size(); p++) { + lpe->crossing_points[p].sign = ((lpe->crossing_points[p].sign + 2) % 3) - 1; + } + } + else if (state & GDK_CONTROL_MASK) { + int sign = lpe->crossing_points[s].sign; + for (unsigned p = 0; p < lpe->crossing_points.size(); p++) { + lpe->crossing_points[p].sign = ((sign + 2) % 3) - 1; + } + }else{ + int sign = lpe->crossing_points[s].sign; + lpe->crossing_points[s].sign = ((sign+2)%3)-1; + //std::cout<<"crossing set to"<<lpe->crossing_points[s].sign<<".\n"; + } + lpe->crossing_points_vector.param_set_and_write_new_value(lpe->crossing_points.to_vector()); + DocumentUndo::done(lpe->getSPDoc(), _("Change knot crossing"), INKSCAPE_ICON("dialog-path-effects")); + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. +// sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); + } +} + + +/* ######################## */ + +} // namespace LivePathEffect +} // namespace Inkscape + +/* + 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/live_effects/lpe-knot.h b/src/live_effects/lpe-knot.h new file mode 100644 index 0000000..6768d79 --- /dev/null +++ b/src/live_effects/lpe-knot.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE knot effect implementation, see lpe-knot.cpp. + */ +/* Authors: + * Jean-Francois Barraud <jf.barraud@gmail.com> + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) Authors 2007-2012 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_KNOT_H +#define INKSCAPE_LPE_KNOT_H + + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/array.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/parameter.h" +//#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" +#include "2geom/crossing.h" + +class SPLPEItem; + +namespace Inkscape { +namespace LivePathEffect { + +class KnotHolderEntityCrossingSwitcher; + +// CrossingPoint, CrossingPoints: +// "point oriented" storage of crossing data (needed to find crossing nearest to a click, etc...) +//TODO: evaluate how lpeknot-specific that is? Should something like this exist in 2geom? +namespace LPEKnotNS {//just in case... +struct CrossingPoint { + Geom::Point pt; + int sign; //+/-1 = positive or neg crossing, 0 = flat. + unsigned i, j; //paths components meeting in this point. + unsigned ni, nj; //this crossing is the ni-th along i, nj-th along j. + double ti, tj; //time along paths. +}; + +class CrossingPoints : public std::vector<CrossingPoint>{ +public: + CrossingPoints() : std::vector<CrossingPoint>() {} + CrossingPoints(Geom::CrossingSet const &cs, Geom::PathVector const &path);//for self crossings only! + CrossingPoints(Geom::PathVector const &paths); + CrossingPoints(std::vector<double> const &input); + std::vector<double> to_vector(); + CrossingPoint get(unsigned const i, unsigned const ni); + void inherit_signs(CrossingPoints const &from_other, int default_value = 1); +}; +} + +class LPEKnot : public Effect, GroupBBoxEffect { +public: + LPEKnot(LivePathEffectObject *lpeobject); + ~LPEKnot() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & input_path) override; + + /* the knotholder entity classes must be declared friends */ + friend class KnotHolderEntityCrossingSwitcher; + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + Geom::PathVector supplied_path; //for knotholder business + +private: + void updateSwitcher(); + + ScalarParam interruption_width; + BoolParam prop_to_stroke_width; + BoolParam both; + BoolParam inverse_width; + // "add_stroke_width" and "add_other_stroke_width" parameters are not used since Inkscape 1.0, + // but changed from bool to hidden parameter to retain backward compatibility and dont show in the UI + HiddenParam add_stroke_width; + HiddenParam add_other_stroke_width; + ScalarParam switcher_size; + ArrayParam<double> crossing_points_vector;//svg storage of crossing_points + + LPEKnotNS::CrossingPoints crossing_points;//topology representation of the knot. + + Geom::PathVector gpaths;//the collection of all the paths in the object or group. + std::vector<double> gstroke_widths;//the collection of all the stroke widths in the object or group. + + //UI: please, someone, help me to improve this!! + unsigned selectedCrossing;//the selected crossing + Geom::Point switcher;//where to put the "switcher" helper + + LPEKnot(const LPEKnot&) = delete; + LPEKnot& operator=(const LPEKnot&) = delete; + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-lattice.cpp b/src/live_effects/lpe-lattice.cpp new file mode 100644 index 0000000..3dcc703 --- /dev/null +++ b/src/live_effects/lpe-lattice.cpp @@ -0,0 +1,310 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <lattice> implementation + + */ +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Steren Giannini + * No� Falzon + * Victor Navez + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-lattice.h" + +#include "display/curve.h" + +#include <2geom/sbasis-2d.h> +#include <2geom/bezier-to-sbasis.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +LPELattice::LPELattice(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + + // initialise your parameters here: + grid_point0(_("Control handle 0:"), _("Control handle 0"), "gridpoint0", &wr, this), + grid_point1(_("Control handle 1:"), _("Control handle 1"), "gridpoint1", &wr, this), + grid_point2(_("Control handle 2:"), _("Control handle 2"), "gridpoint2", &wr, this), + grid_point3(_("Control handle 3:"), _("Control handle 3"), "gridpoint3", &wr, this), + grid_point4(_("Control handle 4:"), _("Control handle 4"), "gridpoint4", &wr, this), + grid_point5(_("Control handle 5:"), _("Control handle 5"), "gridpoint5", &wr, this), + grid_point6(_("Control handle 6:"), _("Control handle 6"), "gridpoint6", &wr, this), + grid_point7(_("Control handle 7:"), _("Control handle 7"), "gridpoint7", &wr, this), + grid_point8(_("Control handle 8:"), _("Control handle 8"), "gridpoint8", &wr, this), + grid_point9(_("Control handle 9:"), _("Control handle 9"), "gridpoint9", &wr, this), + grid_point10(_("Control handle 10:"), _("Control handle 10"), "gridpoint10", &wr, this), + grid_point11(_("Control handle 11:"), _("Control handle 11"), "gridpoint11", &wr, this), + grid_point12(_("Control handle 12:"), _("Control handle 12"), "gridpoint12", &wr, this), + grid_point13(_("Control handle 13:"), _("Control handle 13"), "gridpoint13", &wr, this), + grid_point14(_("Control handle 14:"), _("Control handle 14"), "gridpoint14", &wr, this), + grid_point15(_("Control handle 15:"), _("Control handle 15"), "gridpoint15", &wr, this) + +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&grid_point0); + registerParameter(&grid_point1); + registerParameter(&grid_point2); + registerParameter(&grid_point3); + registerParameter(&grid_point4); + registerParameter(&grid_point5); + registerParameter(&grid_point6); + registerParameter(&grid_point7); + registerParameter(&grid_point8); + registerParameter(&grid_point9); + registerParameter(&grid_point10); + registerParameter(&grid_point11); + registerParameter(&grid_point12); + registerParameter(&grid_point13); + registerParameter(&grid_point14); + registerParameter(&grid_point15); + + apply_to_clippath_and_mask = true; +} + +LPELattice::~LPELattice() += default; + + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPELattice::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + D2<SBasis2d> sb2; + + //Initialisation of the sb2 + for(unsigned dim = 0; dim < 2; dim++) { + sb2[dim].us = 2; + sb2[dim].vs = 2; + const int depth = sb2[dim].us*sb2[dim].vs; + sb2[dim].resize(depth, Linear2d(0)); + } + + //Grouping the point params in a convenient vector + std::vector<Geom::Point *> handles(16); + + handles[0] = &grid_point0; + handles[1] = &grid_point1; + handles[2] = &grid_point2; + handles[3] = &grid_point3; + handles[4] = &grid_point4; + handles[5] = &grid_point5; + handles[6] = &grid_point6; + handles[7] = &grid_point7; + handles[8] = &grid_point8; + handles[9] = &grid_point9; + handles[10] = &grid_point10; + handles[11] = &grid_point11; + handles[12] = &grid_point12; + handles[13] = &grid_point13; + handles[14] = &grid_point14; + handles[15] = &grid_point15; + + Geom::Point origin = Geom::Point(boundingbox_X.min(),boundingbox_Y.min()); + + double width = boundingbox_X.extent(); + double height = boundingbox_Y.extent(); + + //numbering is based on 4 rectangles. + for(unsigned dim = 0; dim < 2; dim++) { + Geom::Point dir(0,0); + dir[dim] = 1; + for(unsigned vi = 0; vi < sb2[dim].vs; vi++) { + for(unsigned ui = 0; ui < sb2[dim].us; ui++) { + for(unsigned iv = 0; iv < 2; iv++) { + for(unsigned iu = 0; iu < 2; iu++) { + unsigned corner = iu + 2*iv; + unsigned i = ui + vi*sb2[dim].us; + + //This is the offset from the Upperleft point + Geom::Point base( (ui + iu*(3-2*ui))*width/3., + (vi + iv*(3-2*vi))*height/3.); + + //Special action for corners + if(vi == 0 && ui == 0) { + base = Geom::Point(0,0); + } + + // i = Upperleft corner of the considerated rectangle + // corner = actual corner of the rectangle + // origin = Upperleft point + double dl = dot((*handles[corner+4*i] - (base + origin)), dir)/dot(dir,dir); + sb2[dim][i][corner] = dl/( dim ? height : width )*pow(4.0,ui+vi); + } + } + } + } + } + + Piecewise<D2<SBasis> > output; + output.push_cut(0.); + for(unsigned i = 0; i < pwd2_in.size(); i++) { + D2<SBasis> B = pwd2_in[i]; + B -= origin; + B*= 1/width; + //Here comes the magic + D2<SBasis> tB = compose_each(sb2,B); + tB = tB * width + origin; + + output.push(tB,i+1); + } + + return output; +} + +void +LPELattice::doBeforeEffect (SPLPEItem const* lpeitem) +{ + original_bbox(lpeitem, false, true); +} + +void +LPELattice::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + original_bbox(SP_LPE_ITEM(item), false, true); + + // place the 16 control points + grid_point0[Geom::X] = boundingbox_X.min(); + grid_point0[Geom::Y] = boundingbox_Y.min(); + + grid_point1[Geom::X] = boundingbox_X.max(); + grid_point1[Geom::Y] = boundingbox_Y.min(); + + grid_point2[Geom::X] = boundingbox_X.min(); + grid_point2[Geom::Y] = boundingbox_Y.max(); + + grid_point3[Geom::X] = boundingbox_X.max(); + grid_point3[Geom::Y] = boundingbox_Y.max(); + + grid_point4[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point4[Geom::Y] = boundingbox_Y.min(); + + grid_point5[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point5[Geom::Y] = boundingbox_Y.min(); + + grid_point6[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point6[Geom::Y] = boundingbox_Y.max(); + + grid_point7[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point7[Geom::Y] = boundingbox_Y.max(); + + grid_point8[Geom::X] = boundingbox_X.min(); + grid_point8[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point9[Geom::X] = boundingbox_X.max(); + grid_point9[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point10[Geom::X] = boundingbox_X.min(); + grid_point10[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + + grid_point11[Geom::X] = boundingbox_X.max(); + grid_point11[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + + grid_point12[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point12[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point13[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point13[Geom::Y] = 1.0/3*boundingbox_Y.max()+2.0/3*boundingbox_Y.min(); + + grid_point14[Geom::X] = 1.0/3*boundingbox_X.max()+2.0/3*boundingbox_X.min(); + grid_point14[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + + grid_point15[Geom::X] = 2.0/3*boundingbox_X.max()+1.0/3*boundingbox_X.min(); + grid_point15[Geom::Y] = 2.0/3*boundingbox_Y.max()+1.0/3*boundingbox_Y.min(); + grid_point1.param_update_default(grid_point1); + grid_point2.param_update_default(grid_point2); + grid_point3.param_update_default(grid_point3); + grid_point4.param_update_default(grid_point4); + grid_point5.param_update_default(grid_point5); + grid_point6.param_update_default(grid_point6); + grid_point7.param_update_default(grid_point7); + grid_point8.param_update_default(grid_point8); + grid_point9.param_update_default(grid_point9); + grid_point10.param_update_default(grid_point10); + grid_point11.param_update_default(grid_point11); + grid_point12.param_update_default(grid_point12); + grid_point13.param_update_default(grid_point13); + grid_point14.param_update_default(grid_point14); + grid_point15.param_update_default(grid_point15); +} + +/** +void +LPELattice::addHelperPathsImpl(SPLPEItem *lpeitem, SPDesktop *desktop) +{ + SPCurve *c = new SPCurve (); + c->moveto(grid_point0); + c->lineto(grid_point4); + c->lineto(grid_point5); + c->lineto(grid_point1); + + c->moveto(grid_point8); + c->lineto(grid_point12); + c->lineto(grid_point13); + c->lineto(grid_point9); + + c->moveto(grid_point10); + c->lineto(grid_point14); + c->lineto(grid_point15); + c->lineto(grid_point11); + + c->moveto(grid_point2); + c->lineto(grid_point6); + c->lineto(grid_point7); + c->lineto(grid_point3); + + + c->moveto(grid_point0); + c->lineto(grid_point8); + c->lineto(grid_point10); + c->lineto(grid_point2); + + c->moveto(grid_point4); + c->lineto(grid_point12); + c->lineto(grid_point14); + c->lineto(grid_point6); + + c->moveto(grid_point5); + c->lineto(grid_point13); + c->lineto(grid_point15); + c->lineto(grid_point7); + + c->moveto(grid_point1); + c->lineto(grid_point9); + c->lineto(grid_point11); + c->lineto(grid_point3); + + // TODO: factor this out (and remove the #include of desktop.h above) + SPCanvasItem *canvasitem = sp_nodepath_generate_helperpath(desktop, c, lpeitem, 0x009000ff); + Inkscape::Display::TemporaryItem* tmpitem = desktop->add_temporary_canvasitem (canvasitem, 0); + lpeitem->lpe_helperpaths.push_back(tmpitem); + + c->unref(); +} +**/ + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-lattice.h b/src/live_effects/lpe-lattice.h new file mode 100644 index 0000000..1d81355 --- /dev/null +++ b/src/live_effects/lpe-lattice.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_LATTICE_H +#define INKSCAPE_LPE_LATTICE_H + +/** \file + * LPE <lattice> implementation, see lpe-lattice.cpp. + */ + +/* + * Authors: + * Johan Engelen + * Steren Giannini + * Noé Falzon + * Victor Navez + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/point.h" +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPELattice : public Effect, GroupBBoxEffect { +public: + + LPELattice(LivePathEffectObject *lpeobject); + ~LPELattice() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + +protected: + //virtual void addHelperPathsImpl(SPLPEItem *lpeitem, SPDesktop *desktop); + + +private: + PointParam grid_point0; + PointParam grid_point1; + PointParam grid_point2; + PointParam grid_point3; + PointParam grid_point4; + PointParam grid_point5; + PointParam grid_point6; + PointParam grid_point7; + PointParam grid_point8; + PointParam grid_point9; + PointParam grid_point10; + PointParam grid_point11; + PointParam grid_point12; + PointParam grid_point13; + PointParam grid_point14; + PointParam grid_point15; + LPELattice(const LPELattice&) = delete; + LPELattice& operator=(const LPELattice&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-lattice2.cpp b/src/live_effects/lpe-lattice2.cpp new file mode 100644 index 0000000..3565285 --- /dev/null +++ b/src/live_effects/lpe-lattice2.cpp @@ -0,0 +1,679 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <lattice2> implementation + + */ +/* + * Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * Steren Giannini + * No� Falzon + * Victor Navez + * ~suv + * Jabiertxo Arraiza + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include "live_effects/lpe-lattice2.h" +#include "display/curve.h" +#include "helper/geom.h" +#include <2geom/sbasis-2d.h> +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +LPELattice2::LPELattice2(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + horizontal_mirror(_("Mirror movements in horizontal"), _("Mirror movements in horizontal"), "horizontal_mirror", &wr, this, false), + vertical_mirror(_("Mirror movements in vertical"), _("Mirror movements in vertical"), "vertical_mirror", &wr, this, false), + perimetral(_("Use only perimeter"), _("Use only perimeter"), "perimetral", &wr, this, false), + live_update(_("Update while moving knots (maybe slow)"), _("Update while moving knots (maybe slow)"), "live_update", &wr, this, true), + grid_point_0(_("Control 0:"), _("Control 0 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint0", &wr, this), + grid_point_1(_("Control 1:"), _("Control 1 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint1", &wr, this), + grid_point_2(_("Control 2:"), _("Control 2 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint2", &wr, this), + grid_point_3(_("Control 3:"), _("Control 3 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint3", &wr, this), + grid_point_4(_("Control 4:"), _("Control 4 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint4", &wr, this), + grid_point_5(_("Control 5:"), _("Control 5 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint5", &wr, this), + grid_point_6(_("Control 6:"), _("Control 6 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint6", &wr, this), + grid_point_7(_("Control 7:"), _("Control 7 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint7", &wr, this), + grid_point_8x9(_("Control 8x9:"), _("Control 8x9 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint8x9", &wr, this), + grid_point_10x11(_("Control 10x11:"), _("Control 10x11 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint10x11", &wr, this), + grid_point_12(_("Control 12:"), _("Control 12 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint12", &wr, this), + grid_point_13(_("Control 13:"), _("Control 13 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint13", &wr, this), + grid_point_14(_("Control 14:"), _("Control 14 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint14", &wr, this), + grid_point_15(_("Control 15:"), _("Control 15 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint15", &wr, this), + grid_point_16(_("Control 16:"), _("Control 16 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint16", &wr, this), + grid_point_17(_("Control 17:"), _("Control 17 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint17", &wr, this), + grid_point_18(_("Control 18:"), _("Control 18 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint18", &wr, this), + grid_point_19(_("Control 19:"), _("Control 19 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint19", &wr, this), + grid_point_20x21(_("Control 20x21:"), _("Control 20x21 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint20x21", &wr, this), + grid_point_22x23(_("Control 22x23:"), _("Control 22x23 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint22x23", &wr, this), + grid_point_24x26(_("Control 24x26:"), _("Control 24x26 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint24x26", &wr, this), + grid_point_25x27(_("Control 25x27:"), _("Control 25x27 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint25x27", &wr, this), + grid_point_28x30(_("Control 28x30:"), _("Control 28x30 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint28x30", &wr, this), + grid_point_29x31(_("Control 29x31:"), _("Control 29x31 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint29x31", &wr, this), + grid_point_32x33x34x35(_("Control 32x33x34x35:"), _("Control 32x33x34x35 - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "gridpoint32x33x34x35", &wr, this), + expanded(false) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&horizontal_mirror); + registerParameter(&vertical_mirror); + registerParameter(&perimetral); + registerParameter(&live_update); + registerParameter(&grid_point_0); + registerParameter(&grid_point_1); + registerParameter(&grid_point_2); + registerParameter(&grid_point_3); + registerParameter(&grid_point_4); + registerParameter(&grid_point_5); + registerParameter(&grid_point_6); + registerParameter(&grid_point_7); + registerParameter(&grid_point_8x9); + registerParameter(&grid_point_10x11); + registerParameter(&grid_point_12); + registerParameter(&grid_point_13); + registerParameter(&grid_point_14); + registerParameter(&grid_point_15); + registerParameter(&grid_point_16); + registerParameter(&grid_point_17); + registerParameter(&grid_point_18); + registerParameter(&grid_point_19); + registerParameter(&grid_point_20x21); + registerParameter(&grid_point_22x23); + registerParameter(&grid_point_24x26); + registerParameter(&grid_point_25x27); + registerParameter(&grid_point_28x30); + registerParameter(&grid_point_29x31); + registerParameter(&grid_point_32x33x34x35); + apply_to_clippath_and_mask = true; +} + +LPELattice2::~LPELattice2() += default; + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPELattice2::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + PathVector pathv = path_from_piecewise(pwd2_in,0.001); + //this is because strange problems with sb2 and LineSegment + PathVector cubic = pathv_to_cubicbezier(pathv); + if (cubic.empty()) { + return pwd2_in; + } + Geom::Piecewise<Geom::D2<Geom::SBasis> > const &pwd2_in_linear_and_cubic = paths_to_pw(cubic); + D2<SBasis2d> sb2; + + //Initialisation of the sb2 + for(unsigned dim = 0; dim < 2; dim++) { + sb2[dim].us = 3; + sb2[dim].vs = 3; + const int depth = sb2[dim].us*sb2[dim].vs; + sb2[dim].resize(depth, Linear2d(0)); + } + + //Grouping the point params in a convenient vector + + std::vector<Geom::Point > handles(36); + + handles[0] = grid_point_0; + handles[1] = grid_point_1; + handles[2] = grid_point_2; + handles[3] = grid_point_3; + handles[4] = grid_point_4; + handles[5] = grid_point_5; + handles[6] = grid_point_6; + handles[7] = grid_point_7; + handles[8] = grid_point_8x9; + handles[9] = grid_point_8x9; + handles[10] = grid_point_10x11; + handles[11] = grid_point_10x11; + handles[12] = grid_point_12; + handles[13] = grid_point_13; + handles[14] = grid_point_14; + handles[15] = grid_point_15; + handles[16] = grid_point_16; + handles[17] = grid_point_17; + handles[18] = grid_point_18; + handles[19] = grid_point_19; + handles[20] = grid_point_20x21; + handles[21] = grid_point_20x21; + handles[22] = grid_point_22x23; + handles[23] = grid_point_22x23; + handles[24] = grid_point_24x26; + handles[25] = grid_point_25x27; + handles[26] = grid_point_24x26; + handles[27] = grid_point_25x27; + handles[28] = grid_point_28x30; + handles[29] = grid_point_29x31; + handles[30] = grid_point_28x30; + handles[31] = grid_point_29x31; + handles[32] = grid_point_32x33x34x35; + handles[33] = grid_point_32x33x34x35; + handles[34] = grid_point_32x33x34x35; + handles[35] = grid_point_32x33x34x35; + + Geom::Point origin = Geom::Point(boundingbox_X.min(),boundingbox_Y.min()); + + double width = boundingbox_X.extent(); + double height = boundingbox_Y.extent(); + + //numbering is based on 4 rectangles.16 + for(unsigned dim = 0; dim < 2; dim++) { + Geom::Point dir(0,0); + dir[dim] = 1; + for(unsigned vi = 0; vi < sb2[dim].vs; vi++) { + for(unsigned ui = 0; ui < sb2[dim].us; ui++) { + for(unsigned iv = 0; iv < 2; iv++) { + for(unsigned iu = 0; iu < 2; iu++) { + unsigned corner = iu + 2*iv; + unsigned i = ui + vi*sb2[dim].us; + + //This is the offset from the Upperleft point + Geom::Point base( (ui + iu*(4-2*ui))*width/4., + (vi + iv*(4-2*vi))*height/4.); + + //Special action for corners + if(vi == 0 && ui == 0) { + base = Geom::Point(0,0); + } + + // i = Upperleft corner of the considerated rectangle + // corner = actual corner of the rectangle + // origin = Upperleft point + double dl = dot((handles[corner+4*i] - (base + origin)), dir)/dot(dir,dir); + sb2[dim][i][corner] = dl/( dim ? height : width )*pow(4.0,ui+vi); + } + } + } + } + } + + Piecewise<D2<SBasis> > output; + output.push_cut(0.); + for(unsigned i = 0; i < pwd2_in_linear_and_cubic.size(); i++) { + D2<SBasis> B = pwd2_in_linear_and_cubic[i]; + B[Geom::X] -= origin[Geom::X]; + B[Geom::X]*= 1/width; + B[Geom::Y] -= origin[Geom::Y]; + B[Geom::Y]*= 1/height; + //Here comes the magic + D2<SBasis> tB = compose_each(sb2,B); + tB[Geom::X] = tB[Geom::X] * width + origin[Geom::X]; + tB[Geom::Y] = tB[Geom::Y] * height + origin[Geom::Y]; + + output.push(tB,i+1); + } + return output; +} + + +Gtk::Widget * +LPELattice2::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + Gtk::Box * hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box *vbox_expander = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox_expander->set_border_width(0); + vbox_expander->set_spacing(2); + Gtk::Button * reset_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Reset grid")))); + reset_button->signal_clicked().connect(sigc::mem_fun (*this,&LPELattice2::resetGrid)); + reset_button->set_size_request(140,30); + vbox->pack_start(*hbox, true,true,2); + hbox->pack_start(*reset_button, false, false,2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + if(param->param_key == "grid") { + widg = nullptr; + } + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if (param->param_key == "horizontal_mirror" || + param->param_key == "vertical_mirror" || + param->param_key == "live_update" || + param->param_key == "perimetral") + { + vbox->pack_start(*widg, true, true, 2); + } else { + vbox_expander->pack_start(*widg, true, true, 2); + } + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + + expander = Gtk::manage(new Gtk::Expander(Glib::ustring(_("Show Points")))); + expander->add(*vbox_expander); + expander->set_expanded(expanded); + vbox->pack_start(*expander, true, true, 2); + expander->property_expanded().signal_changed().connect(sigc::mem_fun(*this, &LPELattice2::onExpanderChanged) ); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void +LPELattice2::onExpanderChanged() +{ + expanded = expander->get_expanded(); + if(expanded) { + expander->set_label (Glib::ustring(_("Hide Points"))); + } else { + expander->set_label (Glib::ustring(_("Show Points"))); + } +} +void +LPELattice2::vertical(PointParam ¶m_one, PointParam ¶m_two, Geom::Line vert) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double Y = (A[Geom::Y] + B[Geom::Y])/2; + A[Geom::Y] = Y; + B[Geom::Y] = Y; + Geom::Point nearest = vert.pointAt(vert.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::X] > B[Geom::X]) { + distance_middle *= -1; + } + A[Geom::X] = nearest[Geom::X] - distance_middle; + B[Geom::X] = nearest[Geom::X] + distance_middle; + param_one.param_setValue(A, live_update); + param_two.param_setValue(B, live_update); +} + +void +LPELattice2::horizontal(PointParam ¶m_one, PointParam ¶m_two, Geom::Line horiz) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double X = (A[Geom::X] + B[Geom::X])/2; + A[Geom::X] = X; + B[Geom::X] = X; + Geom::Point nearest = horiz.pointAt(horiz.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::Y] > B[Geom::Y]) { + distance_middle *= -1; + } + A[Geom::Y] = nearest[Geom::Y] - distance_middle; + B[Geom::Y] = nearest[Geom::Y] + distance_middle; + param_one.param_setValue(A, live_update); + param_two.param_setValue(B, live_update); +} + + +void +LPELattice2::doBeforeEffect (SPLPEItem const* lpeitem) +{ + original_bbox(lpeitem, false, true); + setDefaults(); + if (is_applied) { + resetGrid(); + } + Geom::Line vert(grid_point_8x9.param_get_default(),grid_point_10x11.param_get_default()); + Geom::Line horiz(grid_point_24x26.param_get_default(),grid_point_25x27.param_get_default()); + if(vertical_mirror) { + vertical(grid_point_0, grid_point_1,vert); + vertical(grid_point_2, grid_point_3,vert); + vertical(grid_point_4, grid_point_5,vert); + vertical(grid_point_6, grid_point_7,vert); + vertical(grid_point_12, grid_point_13,vert); + vertical(grid_point_14, grid_point_15,vert); + vertical(grid_point_16, grid_point_17,vert); + vertical(grid_point_18, grid_point_19,vert); + vertical(grid_point_24x26, grid_point_25x27,vert); + vertical(grid_point_28x30, grid_point_29x31,vert); + } + if(horizontal_mirror) { + horizontal(grid_point_0, grid_point_2,horiz); + horizontal(grid_point_1, grid_point_3,horiz); + horizontal(grid_point_4, grid_point_6,horiz); + horizontal(grid_point_5, grid_point_7,horiz); + horizontal(grid_point_8x9, grid_point_10x11,horiz); + horizontal(grid_point_12, grid_point_14,horiz); + horizontal(grid_point_13, grid_point_15,horiz); + horizontal(grid_point_16, grid_point_18,horiz); + horizontal(grid_point_17, grid_point_19,horiz); + horizontal(grid_point_20x21, grid_point_22x23,horiz); + } + if (perimetral) { + grid_point_16.param_hide_knot(true); + grid_point_20x21.param_hide_knot(true); + grid_point_17.param_hide_knot(true); + grid_point_28x30.param_hide_knot(true); + grid_point_32x33x34x35.param_hide_knot(true); + grid_point_29x31.param_hide_knot(true); + grid_point_18.param_hide_knot(true); + grid_point_22x23.param_hide_knot(true); + grid_point_19.param_hide_knot(true); + grid_point_16.param_set_default(); + grid_point_20x21.param_set_default(); + grid_point_17.param_set_default(); + grid_point_28x30.param_set_default(); + grid_point_32x33x34x35.param_set_default(); + grid_point_29x31.param_set_default(); + grid_point_18.param_set_default(); + grid_point_22x23.param_set_default(); + grid_point_19.param_set_default(); + } else { + grid_point_16.param_hide_knot(false); + grid_point_20x21.param_hide_knot(false); + grid_point_17.param_hide_knot(false); + grid_point_28x30.param_hide_knot(false); + grid_point_32x33x34x35.param_hide_knot(false); + grid_point_29x31.param_hide_knot(false); + grid_point_18.param_hide_knot(false); + grid_point_22x23.param_hide_knot(false); + grid_point_19.param_hide_knot(false); + } +} + +void +LPELattice2::setDefaults() +{ + Geom::Point gp0((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp1((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp2((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp3((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp4((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp5((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp6((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp7((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp8x9((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*0+boundingbox_Y.min()); + + Geom::Point gp10x11((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*4+boundingbox_Y.min()); + + Geom::Point gp12((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp13((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp14((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp15((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp16((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp17((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp18((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp19((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp20x21((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*1+boundingbox_Y.min()); + + Geom::Point gp22x23((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*3+boundingbox_Y.min()); + + Geom::Point gp24x26((boundingbox_X.max()-boundingbox_X.min())/4*0+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp25x27((boundingbox_X.max()-boundingbox_X.min())/4*4+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp28x30((boundingbox_X.max()-boundingbox_X.min())/4*1+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp29x31((boundingbox_X.max()-boundingbox_X.min())/4*3+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + Geom::Point gp32x33x34x35((boundingbox_X.max()-boundingbox_X.min())/4*2+boundingbox_X.min(), + (boundingbox_Y.max()-boundingbox_Y.min())/4*2+boundingbox_Y.min()); + + grid_point_0.param_update_default(gp0); + grid_point_1.param_update_default(gp1); + grid_point_2.param_update_default(gp2); + grid_point_3.param_update_default(gp3); + grid_point_4.param_update_default(gp4); + grid_point_5.param_update_default(gp5); + grid_point_6.param_update_default(gp6); + grid_point_7.param_update_default(gp7); + grid_point_8x9.param_update_default(gp8x9); + grid_point_10x11.param_update_default(gp10x11); + grid_point_12.param_update_default(gp12); + grid_point_13.param_update_default(gp13); + grid_point_14.param_update_default(gp14); + grid_point_15.param_update_default(gp15); + grid_point_16.param_update_default(gp16); + grid_point_17.param_update_default(gp17); + grid_point_18.param_update_default(gp18); + grid_point_19.param_update_default(gp19); + grid_point_20x21.param_update_default(gp20x21); + grid_point_22x23.param_update_default(gp22x23); + grid_point_24x26.param_update_default(gp24x26); + grid_point_25x27.param_update_default(gp25x27); + grid_point_28x30.param_update_default(gp28x30); + grid_point_29x31.param_update_default(gp29x31); + grid_point_32x33x34x35.param_update_default(gp32x33x34x35); + grid_point_0.param_set_liveupdate(live_update); + grid_point_1.param_set_liveupdate(live_update); + grid_point_2.param_set_liveupdate(live_update); + grid_point_3.param_set_liveupdate(live_update); + grid_point_4.param_set_liveupdate(live_update); + grid_point_5.param_set_liveupdate(live_update); + grid_point_6.param_set_liveupdate(live_update); + grid_point_7.param_set_liveupdate(live_update); + grid_point_8x9.param_set_liveupdate(live_update); + grid_point_10x11.param_set_liveupdate(live_update); + grid_point_12.param_set_liveupdate(live_update); + grid_point_13.param_set_liveupdate(live_update); + grid_point_14.param_set_liveupdate(live_update); + grid_point_15.param_set_liveupdate(live_update); + grid_point_16.param_set_liveupdate(live_update); + grid_point_17.param_set_liveupdate(live_update); + grid_point_18.param_set_liveupdate(live_update); + grid_point_19.param_set_liveupdate(live_update); + grid_point_20x21.param_set_liveupdate(live_update); + grid_point_22x23.param_set_liveupdate(live_update); + grid_point_24x26.param_set_liveupdate(live_update); + grid_point_25x27.param_set_liveupdate(live_update); + grid_point_28x30.param_set_liveupdate(live_update); + grid_point_29x31.param_set_liveupdate(live_update); + grid_point_32x33x34x35.param_set_liveupdate(live_update); +} + +void +LPELattice2::resetGrid() +{ + grid_point_0.param_set_default(); + grid_point_1.param_set_default(); + grid_point_2.param_set_default(); + grid_point_3.param_set_default(); + grid_point_4.param_set_default(); + grid_point_5.param_set_default(); + grid_point_6.param_set_default(); + grid_point_7.param_set_default(); + grid_point_8x9.param_set_default(); + grid_point_10x11.param_set_default(); + grid_point_12.param_set_default(); + grid_point_13.param_set_default(); + grid_point_14.param_set_default(); + grid_point_15.param_set_default(); + grid_point_16.param_set_default(); + grid_point_17.param_set_default(); + grid_point_18.param_set_default(); + grid_point_19.param_set_default(); + grid_point_20x21.param_set_default(); + grid_point_22x23.param_set_default(); + grid_point_24x26.param_set_default(); + grid_point_25x27.param_set_default(); + grid_point_28x30.param_set_default(); + grid_point_29x31.param_set_default(); + grid_point_32x33x34x35.param_set_default(); +} + +void +LPELattice2::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); + setDefaults(); + resetGrid(); +} + +void +LPELattice2::calculateCurve(Geom::Point a,Geom::Point b, SPCurve* c, bool horizontal, bool move) +{ + using Geom::X; + using Geom::Y; + if(move) c->moveto(a); + Geom::Point cubic1 = a + (1./3)* (b - a); + Geom::Point cubic2 = b + (1./3)* (a - b); + if(horizontal) c->curveto(Geom::Point(cubic1[X],a[Y]),Geom::Point(cubic2[X],b[Y]),b); + else c->curveto(Geom::Point(a[X],cubic1[Y]),Geom::Point(b[X],cubic2[Y]),b); +} + +void +LPELattice2::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.clear(); + + SPCurve *c = new SPCurve(); + if (perimetral) { + calculateCurve(grid_point_0,grid_point_4, c,true, true); + calculateCurve(grid_point_4,grid_point_8x9, c,true, false); + calculateCurve(grid_point_8x9,grid_point_5, c,true, false); + calculateCurve(grid_point_5,grid_point_1, c,true, false); + + calculateCurve(grid_point_1,grid_point_13, c, false, true); + calculateCurve(grid_point_13,grid_point_25x27, c,false, false); + calculateCurve(grid_point_25x27,grid_point_15, c,false, false); + calculateCurve(grid_point_15,grid_point_3, c, false, false); + + calculateCurve(grid_point_2,grid_point_6, c,true, true); + calculateCurve(grid_point_6,grid_point_10x11, c,true, false); + calculateCurve(grid_point_10x11,grid_point_7, c,true, false); + calculateCurve(grid_point_7,grid_point_3, c,true, false); + + calculateCurve(grid_point_0,grid_point_12, c,false, true); + calculateCurve(grid_point_12,grid_point_24x26, c,false, false); + calculateCurve(grid_point_24x26,grid_point_14, c,false, false); + calculateCurve(grid_point_14,grid_point_2, c,false, false); + + } else { + calculateCurve(grid_point_0,grid_point_4, c,true, true); + calculateCurve(grid_point_4,grid_point_8x9, c,true, false); + calculateCurve(grid_point_8x9,grid_point_5, c,true, false); + calculateCurve(grid_point_5,grid_point_1, c,true, false); + + calculateCurve(grid_point_12,grid_point_16, c,true, true); + calculateCurve(grid_point_16,grid_point_20x21, c,true, false); + calculateCurve(grid_point_20x21,grid_point_17, c,true, false); + calculateCurve(grid_point_17,grid_point_13, c,true, false); + + calculateCurve(grid_point_24x26,grid_point_28x30, c,true, true); + calculateCurve(grid_point_28x30,grid_point_32x33x34x35, c,true, false); + calculateCurve(grid_point_32x33x34x35,grid_point_29x31, c,true, false); + calculateCurve(grid_point_29x31,grid_point_25x27, c,true, false); + + calculateCurve(grid_point_14,grid_point_18, c,true, true); + calculateCurve(grid_point_18,grid_point_22x23, c,true, false); + calculateCurve(grid_point_22x23,grid_point_19, c,true, false); + calculateCurve(grid_point_19,grid_point_15, c,true, false); + + calculateCurve(grid_point_2,grid_point_6, c,true, true); + calculateCurve(grid_point_6,grid_point_10x11, c,true, false); + calculateCurve(grid_point_10x11,grid_point_7, c,true, false); + calculateCurve(grid_point_7,grid_point_3, c,true, false); + + calculateCurve(grid_point_0,grid_point_12, c,false, true); + calculateCurve(grid_point_12,grid_point_24x26, c,false, false); + calculateCurve(grid_point_24x26,grid_point_14, c,false, false); + calculateCurve(grid_point_14,grid_point_2, c,false, false); + + calculateCurve(grid_point_4,grid_point_16, c,false, true); + calculateCurve(grid_point_16,grid_point_28x30, c,false, false); + calculateCurve(grid_point_28x30,grid_point_18, c,false, false); + calculateCurve(grid_point_18,grid_point_6, c,false, false); + + calculateCurve(grid_point_8x9,grid_point_20x21, c,false, true); + calculateCurve(grid_point_20x21,grid_point_32x33x34x35, c,false, false); + calculateCurve(grid_point_32x33x34x35,grid_point_22x23, c,false, false); + calculateCurve(grid_point_22x23,grid_point_10x11, c,false, false); + + calculateCurve(grid_point_5,grid_point_17, c, false, true); + calculateCurve(grid_point_17,grid_point_29x31, c,false, false); + calculateCurve(grid_point_29x31,grid_point_19, c,false, false); + calculateCurve(grid_point_19,grid_point_7, c,false, false); + + calculateCurve(grid_point_1,grid_point_13, c, false, true); + calculateCurve(grid_point_13,grid_point_25x27, c,false, false); + calculateCurve(grid_point_25x27,grid_point_15, c,false, false); + calculateCurve(grid_point_15,grid_point_3, c, false, false); + } + hp_vec.push_back(c->get_pathvector()); +} + + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + + + + +/* + 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 : diff --git a/src/live_effects/lpe-lattice2.h b/src/live_effects/lpe-lattice2.h new file mode 100644 index 0000000..319a0dc --- /dev/null +++ b/src/live_effects/lpe-lattice2.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_LATTICE2_H +#define INKSCAPE_LPE_LATTICE2_H + +/** \file + * LPE <lattice2> implementation, see lpe-lattice2.cpp. + */ + +/* + * Authors: + * Johan Engelen + * Steren Giannini + * Noé Falzon + * Victor Navez + * ~suv + * Jabiertxo Arraiza + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/point.h" +#include "live_effects/lpegroupbbox.h" + +namespace Gtk { +class Expander; +} + +namespace Inkscape { +namespace LivePathEffect { + +class LPELattice2 : public Effect, GroupBBoxEffect { +public: + + LPELattice2(LivePathEffectObject *lpeobject); + ~LPELattice2() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + void resetDefaults(SPItem const* item) override; + + void doBeforeEffect(SPLPEItem const* lpeitem) override; + + Gtk::Widget * newWidget() override; + + void calculateCurve(Geom::Point a,Geom::Point b, SPCurve *c, bool horizontal, bool move); + + void vertical(PointParam ¶mA,PointParam ¶mB, Geom::Line vert); + + void horizontal(PointParam ¶mA,PointParam ¶mB,Geom::Line horiz); + + void setDefaults(); + + void onExpanderChanged(); + + void resetGrid(); + +protected: + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) override; +private: + + BoolParam horizontal_mirror; + BoolParam vertical_mirror; + BoolParam perimetral; + BoolParam live_update; + PointParam grid_point_0; + PointParam grid_point_1; + PointParam grid_point_2; + PointParam grid_point_3; + PointParam grid_point_4; + PointParam grid_point_5; + PointParam grid_point_6; + PointParam grid_point_7; + PointParam grid_point_8x9; + PointParam grid_point_10x11; + PointParam grid_point_12; + PointParam grid_point_13; + PointParam grid_point_14; + PointParam grid_point_15; + PointParam grid_point_16; + PointParam grid_point_17; + PointParam grid_point_18; + PointParam grid_point_19; + PointParam grid_point_20x21; + PointParam grid_point_22x23; + PointParam grid_point_24x26; + PointParam grid_point_25x27; + PointParam grid_point_28x30; + PointParam grid_point_29x31; + PointParam grid_point_32x33x34x35; + + bool expanded; + Gtk::Expander * expander; + + LPELattice2(const LPELattice2&) = delete; + LPELattice2& operator=(const LPELattice2&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-line_segment.cpp b/src/live_effects/lpe-line_segment.cpp new file mode 100644 index 0000000..b0e9c3d --- /dev/null +++ b/src/live_effects/lpe-line_segment.cpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <line_segment> implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-line_segment.h" +#include "ui/tools/lpe-tool.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<EndType> EndTypeData[] = { + {END_CLOSED , N_("Closed"), "closed"}, + {END_OPEN_INITIAL , N_("Open start"), "open_start"}, + {END_OPEN_FINAL , N_("Open end"), "open_end"}, + {END_OPEN_BOTH , N_("Open both"), "open_both"}, +}; +static const Util::EnumDataConverter<EndType> EndTypeConverter(EndTypeData, sizeof(EndTypeData)/sizeof(*EndTypeData)); + +LPELineSegment::LPELineSegment(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + end_type(_("End type:"), _("Determines on which side the line or line segment is infinite."), "end_type", EndTypeConverter, &wr, this, END_OPEN_BOTH) +{ + /* register all your parameters here, so Inkscape knows which parameters this effect has: */ + registerParameter(&end_type); +} + +LPELineSegment::~LPELineSegment() += default; + +void +LPELineSegment::doBeforeEffect (SPLPEItem const* lpeitem) +{ + Inkscape::UI::Tools::lpetool_get_limiting_bbox_corners(lpeitem->document, bboxA, bboxB); +} + +Geom::PathVector +LPELineSegment::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector output; + + A = path_in.initialPoint(); + B = path_in.finalPoint(); + + Geom::Rect dummyRect(bboxA, bboxB); + std::optional<Geom::LineSegment> intersection_segment = Geom::Line(A, B).clip(dummyRect); + + if (!intersection_segment) { + g_print ("Possible error - no intersection with limiting bounding box.\n"); + return path_in; + } + + if (end_type == END_OPEN_INITIAL || end_type == END_OPEN_BOTH) { + A = intersection_segment->initialPoint(); + } + + if (end_type == END_OPEN_FINAL || end_type == END_OPEN_BOTH) { + B = intersection_segment->finalPoint(); + } + + Geom::Path path(A); + path.appendNew<Geom::LineSegment>(B); + + output.push_back(path); + + return output; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-line_segment.h b/src/live_effects/lpe-line_segment.h new file mode 100644 index 0000000..c8d3080 --- /dev/null +++ b/src/live_effects/lpe-line_segment.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_LINE_SEGMENT_H +#define INKSCAPE_LPE_LINE_SEGMENT_H + +/** \file + * LPE <line_segment> implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum EndType { + END_CLOSED, + END_OPEN_INITIAL, + END_OPEN_FINAL, + END_OPEN_BOTH +}; + +class LPELineSegment : public Effect { +public: + LPELineSegment(LivePathEffectObject *lpeobject); + ~LPELineSegment() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +//private: + EnumParam<EndType> end_type; + +private: + Geom::Point A, B; // intersections of the line segment with the limiting bounding box + Geom::Point bboxA, bboxB; // upper left and lower right corner of limiting bounding box + + LPELineSegment(const LPELineSegment&) = delete; + LPELineSegment& operator=(const LPELineSegment&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-measure-segments.cpp b/src/live_effects/lpe-measure-segments.cpp new file mode 100644 index 0000000..ff229f1 --- /dev/null +++ b/src/live_effects/lpe-measure-segments.cpp @@ -0,0 +1,1326 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author(s): + * Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * Some code and ideas migrated from dimensioning.py by + * Johannes B. Rutzmoser, johannes.rutzmoser (at) googlemail (dot) com + * https://github.com/Rutzmoser/inkscape_dimensioning + * Copyright (C) 2014 Author(s) + + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-measure-segments.h" + +#include <cmath> +#include <iomanip> +#include <libnrtype/font-lister.h> +#include <pangomm/fontdescription.h> + +#include "2geom/affine.h" +#include "2geom/angle.h" +#include "2geom/point.h" +#include "2geom/ray.h" +#include "display/curve.h" +#include "document-undo.h" +#include "document.h" +#include "helper/geom.h" +#include "inkscape.h" +#include "libnrtype/Layout-TNG.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/satellite-reference.h" +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-item-group.h" +#include "object/sp-item.h" +#include "object/sp-path.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "path-chemistry.h" +#include "preferences.h" +#include "style.h" +#include "svg/stringstream.h" +#include "svg/svg-color.h" +#include "svg/svg-length.h" +#include "svg/svg.h" +#include "text-editing.h" +#include "util/units.h" +#include "xml/node.h" +#include "xml/sp-css-attr.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +using namespace Geom; +namespace Inkscape { +namespace LivePathEffect { + + +static const Util::EnumData<OrientationMethod> OrientationMethodData[] = { + { OM_HORIZONTAL , N_("Horizontal"), "horizontal" }, + { OM_VERTICAL , N_("Vertical") , "vertical" }, + { OM_PARALLEL , N_("Parallel") , "parallel" } +}; +static const Util::EnumDataConverter<OrientationMethod> OMConverter(OrientationMethodData, OM_END); + +LPEMeasureSegments::LPEMeasureSegments(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + unit(_("Unit"), _("Unit of measurement"), "unit", &wr, this, "mm"), + orientation(_("Orientation"), _("Orientation of the line and labels"), "orientation", OMConverter, &wr, this, OM_PARALLEL, false), + coloropacity(_("Color and opacity"), _("Set color and opacity of the dimensions"), "coloropacity", &wr, this, 0x000000ff), + fontbutton(_("Font"), _("Select font for labels"), "fontbutton", &wr, this), + precision(_("Precision"), _("Number of digits after the decimal point"), "precision", &wr, this, 2), + fix_overlaps(_("Merge overlaps °"), _("Minimum angle at which overlapping dimension lines are merged into one, use 180° to disable merging"), "fix_overlaps", &wr, this, 0), + position(_("Position"), _("Distance of dimension line from the path"), "position", &wr, this, 5), + text_top_bottom(_("Label position"), _("Distance of the labels from the dimension line"), "text_top_bottom", &wr, this, 0), + helpline_distance(_("Help line distance"), _("Distance of the perpendicular lines from the path"), "helpline_distance", &wr, this, 0.0), + helpline_overlap(_("Help line elongation"), _("Distance of the perpendicular lines' ends from the dimension line"), "helpline_overlap", &wr, this, 2.0), + line_width(_("Line width"), _("Dimension line width. DIN standard: 0.25 or 0.35 mm"), "line_width", &wr, this, 0.25), + scale(_("Scale"), _("Scaling factor"), "scale", &wr, this, 1.0), + + // TRANSLATORS: Don't translate "{measure}" and "{unit}" variables. + format(_("Label format"), _("Label text format, available variables: {measure}, {unit}"), "format", &wr, this,"{measure}{unit}"), + blacklist(_("Blacklist segments"), _("Comma-separated list of indices of segments that should not be measured. You can use another LPE with different parameters to measure these."), "blacklist", &wr, this,""), + whitelist(_("Invert blacklist"), _("Use the blacklist as whitelist"), "whitelist", &wr, this, false), + showindex(_("Show segment index"), _("Display the index of the segments in the text label for easier blacklisting"), "showindex", &wr, this, false), + arrows_outside(_("Arrows outside"), _("Draw arrows pointing in the opposite direction outside the dimension line"), "arrows_outside", &wr, this, false), + flip_side(_("Flip side"), _("Draw dimension lines and labels on the other side of the path"), "flip_side", &wr, this, false), + scale_sensitive(_("Scale sensitive"), _("When the path is grouped and the group is then scaled, adjust the dimensions."), "scale_sensitive", &wr, this, true), + local_locale(_("Localize number format"), _("Use localized number formatting, e.g. '1,0' instead of '1.0' with German locale"), "local_locale", &wr, this, true), + rotate_anotation(_("Rotate labels"), _("Labels are parallel to the dimension line"), "rotate_anotation", &wr, this, true), + hide_back(_("Hide line under label"), _("Hide the dimension line where the label overlaps it"), "hide_back", &wr, this, true), + hide_arrows(_("Hide arrows"), _("Don't show any arrows"), "hide_arrows", &wr, this, false), + // active for 1.1 + smallx100(_("Multiply values < 1"), _("Multiply values smaller than 1 by 100 and leave out the unit"), "smallx100", &wr, this, false), + linked_items(_("Linked objects:"), _("Objects whose nodes are projected onto the path and generate new measurements"), "linked_items", &wr, this, true), + distance_projection(_("Distance"), _("Distance of the dimension lines from the outermost node"), "distance_projection", &wr, this, 20.0), + angle_projection(_("Angle of projection"), _("Angle of projection in 90° steps"), "angle_projection", &wr, this, 0.0), + active_projection(_("Activate projection"), _("Activate projection mode"), "active_projection", &wr, this, false), + avoid_overlapping(_("Avoid label overlap"), _("Rotate labels if the segment is shorter than the label"), "avoid_overlapping", &wr, this, true), + onbbox(_("Measure bounding box"), _("Add measurements for the geometrical bounding box"), "onbbox", &wr, this, false), + bboxonly(_("Only bounding box"), _("Measure only the geometrical bounding box"), "bboxonly", &wr, this, false), + centers(_("Add object center"), _("Add the projected object center"), "centers", &wr, this, false), + maxmin(_("Only max and min"), _("Compute only max/min projection values"), "maxmin", &wr, this, false), + helpdata(_("Help"), _("Measure segments help"), "helpdata", &wr, this, "", "") +{ + //set to true the parameters you want to be changed his default values + registerParameter(&unit); + registerParameter(&orientation); + registerParameter(&coloropacity); + registerParameter(&fontbutton); + registerParameter(&precision); + registerParameter(&fix_overlaps); + registerParameter(&position); + registerParameter(&text_top_bottom); + registerParameter(&helpline_distance); + registerParameter(&helpline_overlap); + registerParameter(&line_width); + registerParameter(&scale); + registerParameter(&format); + registerParameter(&blacklist); + registerParameter(&active_projection); + registerParameter(&whitelist); + registerParameter(&showindex); + registerParameter(&arrows_outside); + registerParameter(&flip_side); + registerParameter(&scale_sensitive); + registerParameter(&local_locale); + registerParameter(&rotate_anotation); + registerParameter(&hide_back); + registerParameter(&hide_arrows); + // active for 1.1 + registerParameter(&smallx100); + registerParameter(&linked_items); + registerParameter(&distance_projection); + registerParameter(&angle_projection); + registerParameter(&avoid_overlapping); + registerParameter(&onbbox); + registerParameter(&bboxonly); + registerParameter(¢ers); + registerParameter(&maxmin); + registerParameter(&helpdata); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + Glib::ustring format_value = prefs->getString("/live_effects/measure-line/format"); + if(format_value.empty()){ + format_value = "{measure}{unit}"; + } + format.param_update_default(format_value.c_str()); + + format.param_hide_canvas_text(); + blacklist.param_hide_canvas_text(); + precision.param_set_range(0, 100); + precision.param_set_increments(1, 1); + precision.param_set_digits(0); + precision.param_make_integer(); + fix_overlaps.param_set_range(0, 180); + fix_overlaps.param_set_increments(1, 1); + fix_overlaps.param_set_digits(0); + fix_overlaps.param_make_integer(); + position.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + position.param_set_increments(1, 1); + position.param_set_digits(2); + scale.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + scale.param_set_increments(1, 1); + scale.param_set_digits(4); + text_top_bottom.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + text_top_bottom.param_set_increments(1, 1); + text_top_bottom.param_set_digits(2); + line_width.param_set_range(0, std::numeric_limits<double>::max()); + line_width.param_set_increments(0.1, 0.1); + line_width.param_set_digits(2); + helpline_distance.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + helpline_distance.param_set_increments(1, 1); + helpline_distance.param_set_digits(2); + helpline_overlap.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + helpline_overlap.param_set_increments(1, 1); + helpline_overlap.param_set_digits(2); + distance_projection.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + distance_projection.param_set_increments(1, 1); + distance_projection.param_set_digits(5); + angle_projection.param_set_range(0.0, 360.0); + angle_projection.param_set_increments(90.0, 90.0); + angle_projection.param_set_digits(2); + locale_base = strdup(setlocale(LC_NUMERIC, nullptr)); + previous_size = 0; + pagenumber = 0; + anotation_width = 0; + fontsize = 0; + rgb32 = 0; + arrow_gap = 0; + //TODO: add newlines for 1.1 (not easy) + helpdata.param_update_default(_("<b><big>General</big></b>\n" + "Display and position dimension lines and labels\n\n" + "<b><big>Projection</big></b>\n" + "Show a line with measurements based on the selected items\n\n" + "<b><big>Options</big></b>\n" + "Options for color, precision, label formatting and display\n\n" + "<b><big>Tips</big></b>\n" + "<b><i>Custom styling:</i></b> To further customize the styles, " + "use the XML editor to find out the class or ID, then use the " + "Style dialog to apply a new style.\n" + "<b><i>Blacklists:</i></b> allow to hide some segments or projection steps.\n" + "<b><i>Multiple Measure LPEs:</i></b> In the same object, in conjunction with blacklists," + "this allows for labels and measurements with different orientations or additional projections.\n" + "<b><i>Set Defaults:</i></b> For every LPE, default values can be set at the bottom.")); +} + +LPEMeasureSegments::~LPEMeasureSegments() { + keep_paths = false; + doOnRemove(nullptr); +} + +Gtk::Widget * +LPEMeasureSegments::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::Box * vbox = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_VERTICAL) ); + vbox->set_border_width(0); + vbox->set_homogeneous(false); + vbox->set_spacing(0); + Gtk::Box *vbox0 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox0->set_border_width(5); + vbox0->set_homogeneous(false); + vbox0->set_spacing(2); + Gtk::Box *vbox1 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox1->set_border_width(5); + vbox1->set_homogeneous(false); + vbox1->set_spacing(2); + Gtk::Box *vbox2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox2->set_border_width(5); + vbox2->set_homogeneous(false); + vbox2->set_spacing(2); + //Help page + Gtk::Box *vbox3 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox3->set_border_width(5); + vbox3->set_homogeneous(false); + vbox3->set_spacing(2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = param->param_newWidget(); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if ( param->param_key == "linked_items") { + vbox1->pack_start(*widg, true, true, 2); + } else if (param->param_key == "active_projection" || + param->param_key == "distance_projection" || + param->param_key == "angle_projection" || + param->param_key == "maxmin" || + param->param_key == "centers" || + param->param_key == "bboxonly" || + param->param_key == "onbbox" ) + { + vbox1->pack_start(*widg, false, true, 2); + } else if (param->param_key == "precision" || + param->param_key == "coloropacity" || + param->param_key == "font" || + param->param_key == "format" || + param->param_key == "blacklist" || + param->param_key == "whitelist" || + param->param_key == "showindex" || + param->param_key == "local_locale" || + param->param_key == "hide_arrows" ) + { + vbox2->pack_start(*widg, false, true, 2); + } else if (param->param_key == "helpdata") { + vbox3->pack_start(*widg, false, true, 2); + } else { + vbox0->pack_start(*widg, false, true, 2); + } + + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + + Gtk::Notebook * notebook = Gtk::manage(new Gtk::Notebook()); + notebook->append_page (*vbox0, Glib::ustring(_("General"))); + notebook->append_page (*vbox1, Glib::ustring(_("Projection"))); + notebook->append_page (*vbox2, Glib::ustring(_("Options"))); + notebook->append_page (*vbox3, Glib::ustring(_("Help"))); + vbox0->show_all(); + vbox1->show_all(); + vbox2->show_all(); + vbox3->show_all(); + vbox->pack_start(*notebook, true, true, 2); + notebook->set_current_page(pagenumber); + notebook->signal_switch_page().connect(sigc::mem_fun(*this, &LPEMeasureSegments::on_my_switch_page)); + if(Gtk::Widget* widg = defaultParamSet()) { + //Wrap to make it more omogenious + Gtk::Box *vbox4 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox4->set_border_width(5); + vbox4->set_homogeneous(false); + vbox4->set_spacing(2); + vbox4->pack_start(*widg, true, true, 2); + vbox->pack_start(*vbox4, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void +LPEMeasureSegments::on_my_switch_page(Gtk::Widget* page, guint page_number) +{ + if(!page->get_parent()->in_destruction()) { + pagenumber = page_number; + } +} + +void +LPEMeasureSegments::createArrowMarker(Glib::ustring mode) +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item|| !sp_lpe_item->getId()) { + return; + } + Glib::ustring lpobjid = this->lpeobj->getId(); + Glib::ustring itemid = sp_lpe_item->getId(); + Glib::ustring style; + style = Glib::ustring("fill:context-stroke;"); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(coloropacity.get_value()); + style = style + Glib::ustring(";fill-opacity:") + Glib::ustring(os.str()); + style = style + Glib::ustring(";stroke:none"); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPObject *elemref = nullptr; + Inkscape::XML::Node *arrow = nullptr; + if ((elemref = document->getObjectById(mode.c_str()))) { + Inkscape::XML::Node *arrow= elemref->getRepr(); + if (arrow) { + arrow->setAttribute("sodipodi:insensitive", "true"); + arrow->removeAttribute("transform"); + Inkscape::XML::Node *arrow_data = arrow->firstChild(); + if (arrow_data) { + arrow_data->removeAttribute("transform"); + arrow_data->setAttribute("style", style); + } + } + } else { + arrow = xml_doc->createElement("svg:marker"); + arrow->setAttribute("id", mode); + Glib::ustring classarrow = itemid; + classarrow += " "; + classarrow += lpobjid; + classarrow += " measure-arrow-marker"; + arrow->setAttribute("class", classarrow); + arrow->setAttributeOrRemoveIfEmpty("inkscape:stockid", mode); + arrow->setAttribute("orient", "auto"); + arrow->setAttribute("refX", "0.0"); + arrow->setAttribute("refY", "0.0"); + + arrow->setAttribute("sodipodi:insensitive", "true"); + /* Create <path> */ + Inkscape::XML::Node *arrow_path = xml_doc->createElement("svg:path"); + if (std::strcmp(mode.c_str(), "ArrowDIN-start") == 0) { + arrow_path->setAttribute("d", "M -8,0 8,-2.11 8,2.11 z"); + } else if (std::strcmp(mode.c_str(), "ArrowDIN-end") == 0) { + arrow_path->setAttribute("d", "M 8,0 -8,2.11 -8,-2.11 z"); + } else if (std::strcmp(mode.c_str(), "ArrowDINout-start") == 0) { + arrow_path->setAttribute("d", "M 0,0 -16,2.11 -16,0.5 -26,0.5 -26,-0.5 -16,-0.5 -16,-2.11 z"); + } else { + arrow_path->setAttribute("d", "M 0,0 16,-2.11 16,-0.5 26,-0.5 26,0.5 16,0.5 16,2.11 z"); + } + Glib::ustring classarrowpath = itemid; + classarrowpath += " "; + classarrowpath += lpobjid; + classarrowpath += " measure-arrow"; + arrow_path->setAttributeOrRemoveIfEmpty("class", classarrowpath); + Glib::ustring arrowpath = mode + Glib::ustring("_path"); + arrow_path->setAttribute("id", arrowpath); + arrow_path->setAttribute("style", style); + arrow->addChild(arrow_path, nullptr); + Inkscape::GC::release(arrow_path); + elemref = document->getDefs()->appendChildRepr(arrow); + Inkscape::GC::release(arrow); + } + items.push_back(mode); +} + +void +LPEMeasureSegments::createTextLabel(Geom::Point pos, size_t counter, double length, Geom::Coord angle, bool remove, bool valid) +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item || !sp_lpe_item->getId()) { + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Glib::ustring lpobjid = this->lpeobj->getId(); + Glib::ustring itemid = sp_lpe_item->getId(); + Glib::ustring id = Glib::ustring("text-on-"); + id += Glib::ustring::format(counter); + id += "-"; + id += lpobjid; + SPObject *elemref = nullptr; + Inkscape::XML::Node *rtext = nullptr; + Inkscape::XML::Node *rtspan = nullptr; + Inkscape::XML::Node *rstring = nullptr; + elemref = document->getObjectById(id.c_str()); + if (elemref) { + rtext = elemref->getRepr(); + rtext->setAttributeSvgDouble("x", pos[Geom::X]); + rtext->setAttributeSvgDouble("y", pos[Geom::Y]); + rtext->setAttribute("sodipodi:insensitive", "true"); + rtext->removeAttribute("transform"); + rtspan = rtext->firstChild(); + rstring = rtspan->firstChild(); + rtspan->removeAttribute("x"); + rtspan->removeAttribute("y"); + Glib::ustring classlabel = itemid; + classlabel += " "; + classlabel += lpobjid; + classlabel += " measure-label"; + rtext->setAttribute("class", classlabel); + } else { + rtext = xml_doc->createElement("svg:text"); + rtext->setAttribute("xml:space", "preserve"); + rtext->setAttribute("id", id); + Glib::ustring classlabel = itemid; + classlabel += " "; + classlabel += lpobjid; + classlabel += " measure-label"; + rtext->setAttribute("class", classlabel); + rtext->setAttribute("sodipodi:insensitive", "true"); + rtext->removeAttribute("transform"); + rtext->setAttributeSvgDouble("x", pos[Geom::X]); + rtext->setAttributeSvgDouble("y", pos[Geom::Y]); + rtspan = xml_doc->createElement("svg:tspan"); + rtspan->setAttribute("sodipodi:role", "line"); + rtspan->removeAttribute("x"); + rtspan->removeAttribute("y"); + elemref = document->getRoot()->appendChildRepr(rtext); + Inkscape::GC::release(rtext); + rtext->addChild(rtspan, nullptr); + Inkscape::GC::release(rtspan); + rstring = xml_doc->createTextNode(""); + rtspan->addChild(rstring, nullptr); + Inkscape::GC::release(rstring); + } + SPCSSAttr *css = sp_repr_css_attr_new(); + Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance(); + auto fontbutton_str = fontbutton.param_getSVGValue(); + fontlister->fill_css(css, fontbutton_str); + std::stringstream font_size; + setlocale (LC_NUMERIC, "C"); + font_size << fontsize << "px"; + setlocale (LC_NUMERIC, locale_base); + gchar c[32]; + sprintf(c, "#%06x", rgb32 >> 8); + sp_repr_css_set_property (css, "fill",c); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(coloropacity.get_value()); + sp_repr_css_set_property (css, "fill-opacity",os.str().c_str()); + sp_repr_css_set_property (css, "font-size",font_size.str().c_str()); + sp_repr_css_unset_property (css, "-inkscape-font-specification"); + if (remove) { + sp_repr_css_set_property (css, "display","none"); + } + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + rtext->setAttributeOrRemoveIfEmpty("style", css_str); + rtspan->setAttributeOrRemoveIfEmpty("style", css_str); + rtspan->removeAttribute("transform"); + sp_repr_css_attr_unref (css); + length = Inkscape::Util::Quantity::convert(length, display_unit.c_str(), unit.get_abbreviation()); + if (local_locale) { + setlocale (LC_NUMERIC, ""); + } else { + setlocale (LC_NUMERIC, "C"); + } + gchar length_str[64]; + bool x100 = false; + // active for 1.1 + if (smallx100 && length < 1 ) { + length *=100; + x100 = true; + g_snprintf(length_str, 64, "%.*f", (int)precision - 2, length); + } else { + g_snprintf(length_str, 64, "%.*f", (int)precision, length); + } + setlocale (LC_NUMERIC, locale_base); + auto label_value = format.param_getSVGValue(); + size_t s = label_value.find(Glib::ustring("{measure}"),0); + if(s < label_value.length()) { + label_value.replace(s, 9, length_str); + } + + s = label_value.find(Glib::ustring("{unit}"),0); + if(s < label_value.length()) { + if (x100) { + label_value.replace(s, 6, ""); + } else { + label_value.replace(s, 6, unit.get_abbreviation()); + } + } + + if (showindex) { + label_value = Glib::ustring("[") + Glib::ustring::format(counter) + Glib::ustring("] ") + label_value; + } + if (!valid) { + label_value = Glib::ustring(_("Non Uniform Scale")); + } + rstring->setContent(label_value.c_str()); + // this boring hack is to update the text with document scale inituialy loaded without root transform + if (elemref) { + Geom::OptRect bounds = SP_ITEM(elemref)->geometricBounds(); + if (bounds) { + anotation_width = bounds->width(); + rtext->setAttributeSvgDouble("x", pos[Geom::X] - (anotation_width / 2.0)); + rtspan->removeAttribute("style"); + } + } + + std::string transform; + if (rotate_anotation) { + Geom::Affine affine = Geom::Affine(Geom::Translate(pos).inverse()); + angle = std::fmod(angle, 2*M_PI); + if (angle < 0) angle += 2*M_PI; + if (angle >= rad_from_deg(90) && angle < rad_from_deg(270)) { + angle = std::fmod(angle + rad_from_deg(180), 2*M_PI); + if (angle < 0) angle += 2*M_PI; + } + affine *= Geom::Rotate(angle); + affine *= Geom::Translate(pos); + transform = sp_svg_transform_write(affine); + } + rtext->setAttributeOrRemoveIfEmpty("transform", transform); +} + +void +LPEMeasureSegments::createLine(Geom::Point start,Geom::Point end, Glib::ustring name, size_t counter, bool main, bool remove, bool arrows) +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item || !sp_lpe_item->getId()) { + return; + } + Glib::ustring lpobjid = this->lpeobj->getId(); + Glib::ustring itemid = sp_lpe_item->getId(); + Glib::ustring id = name; + id += Glib::ustring::format(counter); + id += "-"; + id += lpobjid; + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPObject *elemref = document->getObjectById(id.c_str()); + Inkscape::XML::Node *line = nullptr; + if (!main) { + Geom::Ray ray(start, end); + Geom::Coord angle = ray.angle(); + start = start + Point::polar(angle, helpline_distance ); + end = end + Point::polar(angle, helpline_overlap ); + } + Geom::PathVector line_pathv; + + double k = (Geom::distance(start,end)/2.0) - (anotation_width/1.7); + if (main && + std::abs(text_top_bottom) < fontsize/1.5 && + hide_back && + k > 0) + { + //k = std::max(k , arrow_gap -1); + Geom::Ray ray(end, start); + Geom::Coord angle = ray.angle(); + Geom::Path line_path(start); + line_path.appendNew<Geom::LineSegment>(start - Point::polar(angle, k)); + line_pathv.push_back(line_path); + line_path.clear(); + line_path.start(end + Point::polar(angle, k)); + line_path.appendNew<Geom::LineSegment>(end); + line_pathv.push_back(line_path); + } else { + Geom::Path line_path(start); + line_path.appendNew<Geom::LineSegment>(end); + line_pathv.push_back(line_path); + } + if (elemref) { + line = elemref->getRepr(); + line->setAttribute("d", sp_svg_write_path(line_pathv)); + line->removeAttribute("transform"); + } else { + line = xml_doc->createElement("svg:path"); + line->setAttributeOrRemoveIfEmpty("id", id); + if (main) { + Glib::ustring classlinedim = itemid; + classlinedim += " "; + classlinedim += lpobjid; + classlinedim += " measure-DIM-line measure-line"; + line->setAttribute("class", classlinedim); + } else { + Glib::ustring classlinehelper = itemid; + classlinehelper += " "; + classlinehelper += lpobjid; + classlinehelper += " measure-helper-line measure-line"; + line->setAttribute("class", classlinehelper); + } + line->setAttribute("d", sp_svg_write_path(line_pathv)); + } + + line->setAttribute("sodipodi:insensitive", "true"); + line_pathv.clear(); + + Glib::ustring style; + if (remove) { + style ="display:none;"; + } + if (main) { + line->setAttribute("inkscape:label", "dinline"); + if (!hide_arrows) { + if (arrows_outside) { + style += "marker-start:url(#ArrowDINout-start);marker-end:url(#ArrowDINout-end);"; + } else { + style += "marker-start:url(#ArrowDIN-start);marker-end:url(#ArrowDIN-end);"; + } + } + } else { + line->setAttribute("inkscape:label", "dinhelpline"); + } + std::stringstream stroke_w; + setlocale (LC_NUMERIC, "C"); + + double stroke_width = Inkscape::Util::Quantity::convert(line_width, unit.get_abbreviation(), display_unit.c_str()); + stroke_w << stroke_width; + setlocale (LC_NUMERIC, locale_base); + style += "stroke-width:"; + style += stroke_w.str(); + gchar c[32]; + sprintf(c, "#%06x", rgb32 >> 8); + style += ";stroke:"; + style += Glib::ustring(c); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(coloropacity.get_value()); + style = style + Glib::ustring(";stroke-opacity:") + Glib::ustring(os.str()); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, style.c_str()); + Glib::ustring css_str; + sp_repr_css_write_string(css,css_str); + line->setAttributeOrRemoveIfEmpty("style", css_str); + if (!elemref) { + elemref = document->getRoot()->appendChildRepr(line); + Inkscape::GC::release(line); + } + sp_repr_css_attr_unref (css); +} + +void +LPEMeasureSegments::doOnApply(SPLPEItem const* lpeitem) +{ + if (!SP_IS_SHAPE(lpeitem)) { + g_warning("LPE measure line can only be applied to shapes (not groups)."); + SPLPEItem * item = const_cast<SPLPEItem*>(lpeitem); + item->removeCurrentPathEffect(false); + return; + } + SPDocument *document = getSPDoc(); + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + Inkscape::XML::Node *styleNode = nullptr; + Inkscape::XML::Node* textNode = nullptr; + Inkscape::XML::Node *root = document->getReprRoot(); + for (unsigned i = 0; i < root->childCount(); ++i) { + if (Glib::ustring(root->nthChild(i)->name()) == "svg:style") { + styleNode = root->nthChild(i); + for (unsigned j = 0; j < styleNode->childCount(); ++j) { + if (styleNode->nthChild(j)->type() == Inkscape::XML::NodeType::TEXT_NODE) { + textNode = styleNode->nthChild(j); + } + } + if (textNode == nullptr) { + // Style element found but does not contain text node! + std::cerr << "StyleDialog::_getStyleTextNode(): No text node!" << std::endl; + textNode = document->getReprDoc()->createTextNode(""); + styleNode->appendChild(textNode); + Inkscape::GC::release(textNode); + } + } + } + + if (styleNode == nullptr) { + // Style element not found, create one + styleNode = document->getReprDoc()->createElement("svg:style"); + textNode = document->getReprDoc()->createTextNode(""); + root->addChild(styleNode, nullptr); + Inkscape::GC::release(styleNode); + + styleNode->appendChild(textNode); + Inkscape::GC::release(textNode); + } + // To fix old meassuring files pre 1.0 + Glib::ustring styleContent = Glib::ustring(textNode->content()); + if (styleContent.find(".measure-arrow\n{\n") == std::string::npos) { + styleContent = styleContent + Glib::ustring("\n.measure-arrow") + Glib::ustring("\n{\n}"); + styleContent = styleContent + Glib::ustring("\n.measure-label") + Glib::ustring("\n{\n\n}"); + styleContent = styleContent + Glib::ustring("\n.measure-line") + Glib::ustring("\n{\n}"); + textNode->setContent(styleContent.c_str()); + } + linked_items.update_satellites(); + DocumentUndo::setUndoSensitive(document, saved); +} + +bool +LPEMeasureSegments::isWhitelist (size_t i, std::string listsegments, bool whitelist) +{ + size_t s = listsegments.find(std::to_string(i) + std::string(","), 0); + if (s != std::string::npos) { + if (whitelist) { + return true; + } else { + return false; + } + } else { + if (whitelist) { + return false; + } else { + return true; + } + } + return false; +} + +double getAngle(Geom::Point p1, Geom::Point p2, Geom::Point p3, bool flip_side, double fix_overlaps) +{ + Geom::Ray ray_1(p2,p1); + Geom::Ray ray_2(p3,p1); + bool ccw_toggle = cross(p1 - p2, p3 - p2) < 0; + double angle = angle_between(ray_1, ray_2, ccw_toggle); + if (Geom::deg_from_rad(angle) < fix_overlaps || + Geom::deg_from_rad(angle) > 180 || + ((ccw_toggle && flip_side) || (!ccw_toggle && !flip_side))) + { + angle = 0; + } + return angle; +} + +std::vector< Point > +transformNodes(std::vector< Point > nodes, Geom::Affine transform) +{ + std::vector< Point > result; + for (auto & node : nodes) { + Geom::Point point = node; + result.push_back(point * transform); + } + return result; +} + +std::vector< Point > +getNodes(SPItem * item, Geom::Affine transform, bool onbbox, bool centers, bool bboxonly, double angle_projection) +{ + std::vector< Point > current_nodes; + SPShape * shape = dynamic_cast<SPShape *> (item); + SPText * text = dynamic_cast<SPText *> (item); + SPGroup * group = dynamic_cast<SPGroup *> (item); + SPFlowtext * flowtext = dynamic_cast<SPFlowtext *> (item); + //TODO handle clones/use + + if (group) { + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + for (auto sub_item : item_list) { + std::vector< Point > nodes = transformNodes(getNodes(sub_item, sub_item->transform, onbbox, centers, bboxonly, angle_projection), transform); + current_nodes.insert(current_nodes.end(), nodes.begin(), nodes.end()); + } + } else if (shape && !bboxonly) { + SPCurve const *c = shape->curve(); + if (c) { + current_nodes = transformNodes(c->get_pathvector().nodes(), transform); + } + } else if ((text || flowtext) && !bboxonly) { + Inkscape::Text::Layout::iterator iter = te_get_layout(item)->begin(); + do { + Inkscape::Text::Layout::iterator iter_next = iter; + iter_next.nextGlyph(); // iter_next is one glyph ahead from iter + if (iter == iter_next) { + break; + } + // get path from iter to iter_next: + auto curve = te_get_layout(item)->convertToCurves(iter, iter_next); + iter = iter_next; // shift to next glyph + if (!curve) { + continue; // error converting this glyph + } + if (curve->is_empty()) { // whitespace glyph? + continue; + } + std::vector< Point > letter_nodes = transformNodes(curve->get_pathvector().nodes(), transform); + current_nodes.insert(current_nodes.end(),letter_nodes.begin(),letter_nodes.end()); + if (iter == te_get_layout(item)->end()) { + break; + } + } while (true); + } else { + onbbox = true; + } + if (onbbox || centers) { + Geom::OptRect bbox = item->geometricBounds(); + if (bbox && onbbox) { + current_nodes.push_back((*bbox).corner(0) * transform); + current_nodes.push_back((*bbox).corner(2) * transform); + if (!Geom::are_near(angle_projection, 0.0) && + !Geom::are_near(angle_projection, 90.0) && + !Geom::are_near(angle_projection, 180.0) && + !Geom::are_near(angle_projection, 360.0)) + { + current_nodes.push_back((*bbox).corner(1) * transform); + current_nodes.push_back((*bbox).corner(3) * transform); + } + + } + if (bbox && centers) { + current_nodes.push_back((*bbox).midpoint() * transform); + } + } + return current_nodes; +} + +static void extractFirstPoint(Geom::Point & dest, const Glib::ustring & lpobjid, const char *const prefix, const gint idx, SPDocument *const document) +{ + Glib::ustring id = Glib::ustring(prefix); + id += Glib::ustring::format(idx); + id += "-"; + id += lpobjid; + auto path = dynamic_cast<SPPath *>(document->getObjectById(id)); + if (path) { + SPCurve const *curve = path->curve(); + if (curve) { + dest = *curve->first_point(); + } + } +} + +bool +LPEMeasureSegments::doOnOpen(SPLPEItem const* lpeitem) { + if (!is_load || is_applied) { + return false; + } + if (active_projection) { + linked_items.start_listening(); + linked_items.connect_selection_changed(); + } + return true; +} + +void +LPEMeasureSegments::doBeforeEffect (SPLPEItem const* lpeitem) +{ + if (isOnClipboard()) { + return; + } + if (!linked_items.data().size()) { + linked_items.read_from_SVG(); + if (linked_items.data().size()) { + linked_items.update_satellites(); + } + } + SPLPEItem * splpeitem = const_cast<SPLPEItem *>(lpeitem); + Glib::ustring lpobjid = this->lpeobj->getId(); + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + bool active = !linked_items.data().size(); + for (auto lpereference : linked_items.data()) { + if (lpereference && lpereference.get()->isAttached() && lpereference.get()->getObject() != nullptr) { + active = true; + } + } + if (!active && !is_load && prev_active_projection) { + linked_items.clear(); + prevsatellitecount = 0; + return; + } + prev_active_projection = active_projection; + //Avoid crashes on previews + bool fontsizechanged = false; + Geom::Affine parentaffinetransform = i2anc_affine(lpeitem->parent, document->getRoot()); + Geom::Affine affinetransform = i2anc_affine(lpeitem, document->getRoot()); + Geom::Affine itemtransform = affinetransform * parentaffinetransform.inverse(); + ////Projection prepare + Geom::PathVector pathvector; + std::vector< Point > nodes; + if (active_projection) { + Geom::OptRect bbox = sp_lpe_item->geometricBounds(); + if (bbox) { + Geom::Point mid = bbox->midpoint(); + double angle = Geom::rad_from_deg(angle_projection); + Geom::Affine transform = itemtransform; + transform *= Geom::Translate(mid).inverse(); + transform *= Geom::Rotate(angle).inverse(); + transform *= Geom::Translate(mid); + std::vector< Point > current_nodes = getNodes(splpeitem, transform, onbbox, centers, bboxonly, angle_projection); + nodes.insert(nodes.end(),current_nodes.begin(), current_nodes.end()); + auto satellites = linked_items.data(); + if (satellites.size() != prevsatellitecount ) { + prevsatellitecount = satellites.size(); + linked_items.update_satellites(true); + } + prevsatellitecount = satellites.size(); + for (auto & iter : satellites) { + SPObject *obj; + if (iter && iter->isAttached() && iter->getActive() && (obj = iter->getObject()) && SP_IS_ITEM(obj)) { + SPItem * item = dynamic_cast<SPItem *>(obj); + if (item) { + Geom::Affine affinetransform_sub = i2anc_affine(item, document->getRoot()); + Geom::Affine transform = affinetransform_sub ; + transform *= Geom::Translate(-mid); + transform *= Geom::Rotate(angle).inverse(); + transform *= Geom::Translate(mid); + std::vector< Point > current_nodes = getNodes(item, transform, onbbox, centers, bboxonly, angle_projection); + nodes.insert(nodes.end(),current_nodes.begin(), current_nodes.end()); + } + } + } + + double maxdistance = -std::numeric_limits<double>::max(); + std::vector<double> result; + for (auto & node : nodes) { + Geom::Point point = node; + if (point[Geom::X] > maxdistance) { + maxdistance = point[Geom::X]; + } + result.push_back(point[Geom::Y]); + } + double dproj = Inkscape::Util::Quantity::convert(distance_projection, display_unit.c_str(), unit.get_abbreviation()); + Geom::Coord xpos = maxdistance + dproj; + std::sort (result.begin(), result.end()); + Geom::Path path; + Geom::Point prevpoint(Geom::infinity(),Geom::infinity()); + bool started = false; + Geom::Point point = Geom::Point(); + for (auto & iter : result) { + point = Geom::Point(xpos, iter); + if (Geom::are_near(prevpoint, point)){ + continue; + } + if (!started) { + path.setInitial(point); + started = true; + } else { + if (!maxmin) { + path.appendNew<Geom::LineSegment>(point); + } + } + prevpoint = point; + } + if (maxmin) { + path.appendNew<Geom::LineSegment>(point); + } + pathvector.push_back(path); + pathvector *= Geom::Translate(-mid); + pathvector *= Geom::Rotate(angle); + pathvector *= Geom::Translate(mid); + } + + } + + //end projection prepare + SPShape *shape = dynamic_cast<SPShape *>(splpeitem); + if (shape) { + //only check constrain viewbox on X + display_unit = document->getDisplayUnit()->abbr.c_str(); + guint32 color32 = coloropacity.get_value(); + bool colorchanged = false; + if (color32 != rgb32) { + colorchanged = true; + } + rgb32 = color32; + auto fontdesc_ustring = fontbutton.param_getSVGValue(); + Pango::FontDescription fontdesc(fontdesc_ustring); + double newfontsize = fontdesc.get_size() / (double)Pango::SCALE; + if (newfontsize != fontsize) { + fontsize = Inkscape::Util::Quantity::convert(newfontsize, "pt", display_unit.c_str()); + fontsizechanged = true; + } + Geom::Point prev_stored = Geom::Point(0,0); + Geom::Point start_stored = Geom::Point(0,0); + Geom::Point end_stored = Geom::Point(0,0); + Geom::Point next_stored = Geom::Point(0,0); + if (!active_projection) { + SPCurve const *c = shape->curve(); + pathvector = pathv_to_linear_and_cubic_beziers(c->get_pathvector()); + pathvector *= affinetransform; + } + auto format_str = format.param_getSVGValue(); + if (format_str.empty()) { + format.param_setValue(Glib::ustring("{measure}{unit}")); + } + size_t ncurves = pathvector.curveCount(); + items.clear(); + double start_angle_cross = 0; + double end_angle_cross = 0; + gint counter = -1; + bool previous_fix_overlaps = true; + for (size_t i = 0; i < pathvector.size(); i++) { + size_t count = pathvector[i].size_default(); + if (!pathvector[i].empty() && pathvector[i].closed()) { + const Geom::Curve &closingline = pathvector[i].back_closed(); + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + count = pathvector[i].size_open(); + } + } + for (size_t j = 0; j < count; j++) { + counter++; + gint fix_overlaps_degree = fix_overlaps; + Geom::Point prev = Geom::Point(0,0); + if (j == 0 && pathvector[i].closed()) { + prev = pathvector.pointAt(pathvector[i].size() - 1); + } else if (j != 0) { + prev = pathvector[i].pointAt(j - 1); + } + Geom::Point start = pathvector[i].pointAt(j); + Geom::Point end = pathvector[i].pointAt(j + 1); + Geom::Point next = Geom::Point(0,0); + if (pathvector[i].closed() && pathvector[i].size() == j+1){ + end = pathvector[i].pointAt(0); + next = pathvector[i].pointAt(1); + } else if (pathvector[i].size() > j + 1) { + next = pathvector[i].pointAt(j+2); + } + auto blacklist_str = blacklist.param_getSVGValue(); + std::string listsegments(blacklist_str.raw() + ","); + listsegments.erase(std::remove(listsegments.begin(), listsegments.end(), ' '), listsegments.end()); + if (isWhitelist(counter, listsegments, (bool)whitelist) && !Geom::are_near(start, end)) { + extractFirstPoint(prev_stored, lpobjid, "infoline-on-start-", counter-1, document); + extractFirstPoint(start_stored, lpobjid, "infoline-on-start-", counter, document); + extractFirstPoint(end_stored, lpobjid, "infoline-on-end-", counter, document); + extractFirstPoint(next_stored, lpobjid, "infoline-on-start-", counter+1, document); + Glib::ustring infoline_on_start = "infoline-on-start-"; + infoline_on_start += Glib::ustring::format(counter); + infoline_on_start += "-"; + infoline_on_start += lpobjid; + + Glib::ustring infoline_on_end = "infoline-on-end-"; + infoline_on_end += Glib::ustring::format(counter); + infoline_on_end += "-"; + infoline_on_end += lpobjid; + + Glib::ustring infoline = "infoline-"; + infoline += Glib::ustring::format(counter); + infoline += "-"; + infoline += lpobjid; + + Glib::ustring texton = "text-on-"; + texton += Glib::ustring::format(counter); + texton += "-"; + texton += lpobjid; + items.push_back(infoline_on_start); + items.push_back(infoline_on_end); + items.push_back(infoline); + items.push_back(texton); + if (!hide_arrows) { + if (arrows_outside) { + items.emplace_back("ArrowDINout-start"); + items.emplace_back("ArrowDINout-end"); + } else { + items.emplace_back("ArrowDIN-start"); + items.emplace_back("ArrowDIN-end"); + } + } + if (((Geom::are_near(prev, prev_stored, 0.01) && Geom::are_near(next, next_stored, 0.01)) || + fix_overlaps_degree == 180) && + Geom::are_near(start, start_stored, 0.01) && Geom::are_near(end, end_stored, 0.01) && + !this->refresh_widgets && !colorchanged && !fontsizechanged && !is_load && anotation_width) + { + continue; + } + Geom::Point hstart = start; + Geom::Point hend = end; + bool remove = false; + if (orientation == OM_VERTICAL) { + Coord xpos = std::max(hstart[Geom::X],hend[Geom::X]); + if (flip_side) { + xpos = std::min(hstart[Geom::X],hend[Geom::X]); + } + hstart[Geom::X] = xpos; + hend[Geom::X] = xpos; + if (hstart[Geom::Y] > hend[Geom::Y]) { + swap(hstart,hend); + swap(start,end); + } + if (Geom::are_near(hstart[Geom::Y], hend[Geom::Y])) { + remove = true; + } + } else if (orientation == OM_HORIZONTAL) { + Coord ypos = std::max(hstart[Geom::Y],hend[Geom::Y]); + if (flip_side) { + ypos = std::min(hstart[Geom::Y],hend[Geom::Y]); + } + hstart[Geom::Y] = ypos; + hend[Geom::Y] = ypos; + if (hstart[Geom::X] < hend[Geom::X]) { + swap(hstart,hend); + swap(start,end); + } + if (Geom::are_near(hstart[Geom::X], hend[Geom::X])) { + remove = true; + } + } else if (fix_overlaps_degree != 180) { + start_angle_cross = getAngle( start, prev, end, flip_side, fix_overlaps_degree); + if (prev == Geom::Point(0,0)) { + start_angle_cross = 0; + } + end_angle_cross = getAngle(end, start, next, flip_side, fix_overlaps_degree); + if (next == Geom::Point(0,0)) { + end_angle_cross = 0; + } + } + if (remove) { + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-start-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-end-"), counter, true, true, true); + createTextLabel(Geom::Point(), counter, 0, 0, true, true); + continue; + } + Geom::Ray ray(hstart,hend); + Geom::Coord angle = ray.angle(); + if (flip_side) { + angle = std::fmod(angle + rad_from_deg(180), 2*M_PI); + if (angle < 0) angle += 2*M_PI; + } + Geom::Coord angle_cross = std::fmod(angle + rad_from_deg(90), 2*M_PI); + if (angle_cross < 0) angle_cross += 2*M_PI; + angle = std::fmod(angle, 2*M_PI); + if (angle < 0) angle += 2*M_PI; + double turn = Geom::rad_from_deg(-90); + if (flip_side) { + end_angle_cross *= -1; + start_angle_cross *= -1; + //turn *= -1; + } + double position_turned_start = position / sin(start_angle_cross/2.0); + double length = Geom::distance(start,end); + if (fix_overlaps_degree != 180 && + start_angle_cross != 0 && + position_turned_start < length && + previous_fix_overlaps) + { + hstart = hstart - Point::polar(angle_cross - (start_angle_cross/2.0) - turn, position_turned_start); + } else { + hstart = hstart - Point::polar(angle_cross, position); + } + createLine(start, hstart, Glib::ustring("infoline-on-start-"), counter, false, false); + double position_turned_end = position / sin(end_angle_cross/2.0); + double endlength = Geom::distance(end,next); + if (fix_overlaps_degree != 180 && + end_angle_cross != 0 && + position_turned_end < length && + position_turned_end < endlength) + { + hend = hend - Point::polar(angle_cross + (end_angle_cross/2.0) + turn, position_turned_end); + previous_fix_overlaps = true; + } else { + hend = hend - Point::polar(angle_cross, position); + previous_fix_overlaps = false; + } + length = Geom::distance(start,end) * scale; + Geom::Point pos = Geom::middle_point(hstart, hend); + if (!hide_arrows) { + if (arrows_outside) { + createArrowMarker(Glib::ustring("ArrowDINout-start")); + createArrowMarker(Glib::ustring("ArrowDINout-end")); + } else { + createArrowMarker(Glib::ustring("ArrowDIN-start")); + createArrowMarker(Glib::ustring("ArrowDIN-end")); + } + } + if (angle >= rad_from_deg(90) && angle < rad_from_deg(270)) { + pos = pos - Point::polar(angle_cross, text_top_bottom + (fontsize/2.5)); + } else { + pos = pos + Point::polar(angle_cross, text_top_bottom + (fontsize/2.5)); + } + double parents_scale = (parentaffinetransform.expansionX() + parentaffinetransform.expansionY()) / 2.0; + if (!scale_sensitive) { + length /= parents_scale; + } + if ((anotation_width/2) > Geom::distance(hstart,hend)/2.0) { + if (avoid_overlapping) { + pos = pos - Point::polar(angle_cross, position + (anotation_width/2.0)); + angle += Geom::rad_from_deg(90); + } else { + pos = pos - Point::polar(angle_cross, position); + } + } + if (!scale_sensitive && !parentaffinetransform.preservesAngles()) { + createTextLabel(pos, counter, length, angle, remove, false); + } else { + createTextLabel(pos, counter, length, angle, remove, true); + } + arrow_gap = 8 * Inkscape::Util::Quantity::convert(line_width, unit.get_abbreviation(), display_unit.c_str()); + if(flip_side) { + arrow_gap *= -1; + } + if(hide_arrows) { + arrow_gap *= 0; + } + createLine(end, hend, Glib::ustring("infoline-on-end-"), counter, false, false); + if (!arrows_outside) { + hstart = hstart + Point::polar(angle, arrow_gap); + hend = hend - Point::polar(angle, arrow_gap ); + } + if ((Geom::distance(hstart, hend) / 2.0) > (anotation_width / 1.9) + arrow_gap) { + createLine(hstart, hend, Glib::ustring("infoline-"), counter, true, false, true); + } else { + createLine(hstart, hend, Glib::ustring("infoline-"), counter, true, true, true); + } + } else { + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-start-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-end-"), counter, true, true, true); + createTextLabel(Geom::Point(), counter, 0, 0, true, true); + } + } + } + if (previous_size) { + for (size_t counter = ncurves; counter < previous_size; counter++) { + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-start-"), counter, true, true, true); + createLine(Geom::Point(), Geom::Point(), Glib::ustring("infoline-on-end-"), counter, true, true, true); + createTextLabel(Geom::Point(), counter, 0, 0, true, true); + } + } + previous_size = ncurves; + } +} + +void +LPEMeasureSegments::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ + processObjects(LPE_VISIBILITY); +} + +void +LPEMeasureSegments::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ + //set "keep paths" hook on sp-lpe-item.cpp + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + items.clear(); + return; + } + processObjects(LPE_ERASE); + items.clear(); +} + +// we override processObjects because satellite items are not selectable and dont surf any issues +void +LPEMeasureSegments::processObjects(LPEAction lpe_action) +{ + if (lpe_action == LPE_UPDATE && _lpe_action != LPE_ERASE) { + _lpe_action = lpe_action; + return; + } + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + sp_lpe_item = dynamic_cast<SPLPEItem *>(*getLPEObj()->hrefList.begin()); + if (!document || !sp_lpe_item) { + return; + } + sp_lpe_item_enable_path_effects(sp_lpe_item, false); + for (auto id : items) { + SPObject *elemref = nullptr; + if ((elemref = document->getObjectById(id.c_str()))) { + Inkscape::XML::Node * elemnode = elemref->getRepr(); + auto item = dynamic_cast<SPItem *>(elemref); + SPCSSAttr *css; + Glib::ustring css_str; + switch (lpe_action){ + case LPE_TO_OBJECTS: + if (item->isHidden()) { + item->deleteObject(true); + } else { + elemnode->removeAttribute("sodipodi:insensitive"); + if (!SP_IS_DEFS(item->parent)) { + item->moveTo(sp_lpe_item, false); + } + } + break; + + case LPE_ERASE: + item->deleteObject(true); + break; + + case LPE_VISIBILITY: + css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, elemref->getRepr()->attribute("style")); + if (!this->isVisible()/* && std::strcmp(elemref->getId(),sp_lpe_item->getId()) != 0*/) { + css->setAttribute("display", "none"); + } else { + css->removeAttribute("display"); + } + sp_repr_css_write_string(css,css_str); + elemnode->setAttributeOrRemoveIfEmpty("style", css_str); + sp_repr_css_attr_unref (css); + break; + + default: + break; + } + } + } + if (lpe_action == LPE_ERASE || lpe_action == LPE_TO_OBJECTS) { + items.clear(); + } + sp_lpe_item_enable_path_effects(sp_lpe_item, true); +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offset:((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 : diff --git a/src/live_effects/lpe-measure-segments.h b/src/live_effects/lpe-measure-segments.h new file mode 100644 index 0000000..01b23cc --- /dev/null +++ b/src/live_effects/lpe-measure-segments.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_MEASURE_SEGMENTS_H +#define INKSCAPE_LPE_MEASURE_SEGMENTS_H + +/* + * Author(s): + * Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * + * Copyright (C) 2014 Author(s) + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <gtkmm.h> + +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/colorpicker.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/fontbutton.h" +#include "live_effects/parameter/message.h" +#include "live_effects/parameter/satellitearray.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/unit.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum OrientationMethod { + OM_HORIZONTAL, + OM_VERTICAL, + OM_PARALLEL, + OM_END +}; + +class LPEMeasureSegments : public Effect { +public: + LPEMeasureSegments(LivePathEffectObject *lpeobject); + ~LPEMeasureSegments() override; + void doOnApply(SPLPEItem const* lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* /*lpeitem*/) override; + void doEffect (SPCurve * curve) override {}; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void processObjects(LPEAction lpe_action) override; + Gtk::Widget * newWidget() override; + void createLine(Geom::Point start,Geom::Point end, Glib::ustring name, size_t counter, bool main, bool remove, bool arrows = false); + void createTextLabel(Geom::Point pos, size_t counter, double length, Geom::Coord angle, bool remove, bool valid); + void createArrowMarker(Glib::ustring mode); + bool isWhitelist(size_t i, std::string listsegments, bool whitelist); + void on_my_switch_page(Gtk::Widget* page, guint page_number); +private: + UnitParam unit; + EnumParam<OrientationMethod> orientation; + ColorPickerParam coloropacity; + FontButtonParam fontbutton; + ScalarParam precision; + ScalarParam fix_overlaps; + ScalarParam position; + ScalarParam text_top_bottom; + ScalarParam helpline_distance; + ScalarParam helpline_overlap; + ScalarParam line_width; + ScalarParam scale; + TextParam format; + TextParam blacklist; + BoolParam scale_sensitive; + BoolParam active_projection; + BoolParam whitelist; + BoolParam showindex; + BoolParam arrows_outside; + BoolParam flip_side; + BoolParam local_locale; + BoolParam rotate_anotation; + BoolParam hide_back; + BoolParam hide_arrows; + BoolParam onbbox; + BoolParam bboxonly; + BoolParam centers; + BoolParam maxmin; + BoolParam smallx100; + std::vector<Glib::ustring> items; + SatelliteArrayParam linked_items; + ScalarParam distance_projection; + ScalarParam angle_projection; + BoolParam avoid_overlapping; + MessageParam helpdata; + Glib::ustring display_unit; + double fontsize; + double anotation_width; + double previous_size; + guint32 rgb32; + double arrow_gap; + guint pagenumber; + gchar const* locale_base; + size_t prevsatellitecount = 0; + bool prev_active_projection = false; + SPObject *parent = nullptr; + LPEMeasureSegments(const LPEMeasureSegments &) = delete; + LPEMeasureSegments &operator=(const LPEMeasureSegments &) = delete; + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-mirror_symmetry.cpp b/src/live_effects/lpe-mirror_symmetry.cpp new file mode 100644 index 0000000..5a71804 --- /dev/null +++ b/src/live_effects/lpe-mirror_symmetry.cpp @@ -0,0 +1,705 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <mirror_symmetry> implementation: mirrors a path with respect to a given line. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * Abhishek Sharma + * Jabiertxof + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilin Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-mirror_symmetry.h" + +#include <gtkmm.h> + +#include "2geom/affine.h" +#include "2geom/path-intersection.h" +#include "display/curve.h" +#include "helper/geom.h" +#include "live_effects/parameter/satellite-reference.h" +#include "object/sp-defs.h" +#include "object/sp-lpe-item.h" +#include "object/sp-path.h" +#include "object/sp-text.h" +#include "path-chemistry.h" +#include "style.h" +#include "svg/path-string.h" +#include "svg/svg.h" +#include "xml/sp-css-attr.h" +#include "path/path-boolop.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +typedef FillRule FillRuleFlatten; + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<ModeType> ModeTypeData[] = { + { MT_V, N_("Vertical page center"), "vertical" }, + { MT_H, N_("Horizontal page center"), "horizontal" }, + { MT_FREE, N_("Freely defined mirror line"), "free" }, + { MT_X, N_("X coordinate of mirror line midpoint"), "X" }, + { MT_Y, N_("Y coordinate of mirror line midpoint"), "Y" } +}; +static const Util::EnumDataConverter<ModeType> +MTConverter(ModeTypeData, MT_END); + + +LPEMirrorSymmetry::LPEMirrorSymmetry(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // do not change name of this parameter us used in oncommit + lpesatellites(_("lpesatellites"), _("Items satellites"), "lpesatellites", &wr, this, false), + mode(_("Mode"), _("Set mode of transformation. Either freely defined by mirror line or constrained to certain symmetry points."), "mode", MTConverter, &wr, this, MT_FREE), + discard_orig_path(_("Discard original path"), _("Only keep mirrored part of the path, remove the original."), "discard_orig_path", &wr, this, false), + fuse_paths(_("Fuse paths"), _("Fuse original path and mirror image into a single path"), "fuse_paths", &wr, this, false), + oposite_fuse(_("Fuse opposite sides"), _("Picks the part on the other side of the mirror line as the original."), "oposite_fuse", &wr, this, false), + split_items(_("Split elements"), _("Split original and mirror image into separate paths, so each can have its own style."), "split_items", &wr, this, false), + split_open(_("Keep open paths on split"), _("Do not automatically close paths along the split line."), "split_open", &wr, this, false), + start_point(_("Mirror line start"), _("Start point of mirror line"), "start_point", &wr, this, _("Adjust start point of mirror line")), + end_point(_("Mirror line end"), _("End point of mirror line"), "end_point", &wr, this, _("Adjust end point of mirror line")), + center_point(_("Mirror line mid"), _("Center point of mirror line"), "center_point", &wr, this, _("Adjust center point of mirror line")), + link_styles(_("Link styles"), _("Link styles on split mode"), "link_styles", &wr, this, false) +{ + registerParameter(&lpesatellites); + registerParameter(&mode); + registerParameter(&discard_orig_path); + registerParameter(&fuse_paths); + registerParameter(&oposite_fuse); + registerParameter(&split_items); + registerParameter(&split_open); + registerParameter(&link_styles); + registerParameter(&start_point); + registerParameter(&end_point); + registerParameter(¢er_point); + show_orig_path = true; + apply_to_clippath_and_mask = true; + previous_center = Geom::Point(0,0); + center_point.param_widget_is_visible(false); + reset = link_styles; + center_horiz = false; + center_vert = false; + satellitestoclipboard = true; +} + +LPEMirrorSymmetry::~LPEMirrorSymmetry() +{ + keep_paths = false; + doOnRemove(nullptr); +}; + +bool LPEMirrorSymmetry::doOnOpen(SPLPEItem const *lpeitem) +{ + bool fixed = false; + if (!is_load || is_applied || !split_items) { + return fixed; + } + + Glib::ustring version = lpeversion.param_getSVGValue(); + if (version < "1.2") { + lpesatellites.clear(); + Glib::ustring id = Glib::ustring("mirror-"); + id += getLPEObj()->getId(); + SPObject *elemref = getSPDoc()->getObjectById(id.c_str()); + if (elemref) { + lpesatellites.link(elemref, 0); + } + lpeversion.param_setValue("1.2", true); + fixed = true; + lpesatellites.write_to_SVG(); + } + lpesatellites.start_listening(); + lpesatellites.connect_selection_changed(); + container = dynamic_cast<SPObject *>(lpeitem->parent); + return fixed; +} + +void +LPEMirrorSymmetry::doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + container = dynamic_cast<SPObject *>(sp_lpe_item->parent); + + if (split_items && !discard_orig_path) { + bool active = !lpesatellites.data().size() || is_load; + for (auto lpereference : lpesatellites.data()) { + if (lpereference && lpereference->isAttached() && lpereference.get()->getObject() != nullptr) { + active = true; + } + } + // we need to call this when the LPE is "mirrored 1 or + times in split mode" + // to prevent satellite hidden as in prev status + if (!active && !is_load && prev_split && !prev_discard_orig_path) { + lpesatellites.clear(); + return; + } + Geom::Line ls((Geom::Point)start_point, (Geom::Point)end_point); + Geom::Affine m = Geom::reflection (ls.vector(), (Geom::Point)start_point); + m *= sp_lpe_item->transform; + toMirror(m); + } + prev_split = split_items; + prev_discard_orig_path = discard_orig_path; +} + +Gtk::Widget * +LPEMirrorSymmetry::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (widg && param->param_key != "split_open") { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + Gtk::Box * hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Button * center_vert_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Vertical center")))); + center_vert_button->signal_clicked().connect(sigc::mem_fun (*this,&LPEMirrorSymmetry::centerVert)); + center_vert_button->set_size_request(110,20); + Gtk::Button * center_horiz_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Horizontal center")))); + center_horiz_button->signal_clicked().connect(sigc::mem_fun (*this,&LPEMirrorSymmetry::centerHoriz)); + center_horiz_button->set_size_request(110,20); + vbox->pack_start(*hbox, true,true,2); + hbox->pack_start(*center_vert_button, false, false,2); + hbox->pack_start(*center_horiz_button, false, false,2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void +LPEMirrorSymmetry::centerVert(){ + center_vert = true; + refresh_widgets = true; + writeParamsToSVG(); +} + +void +LPEMirrorSymmetry::centerHoriz(){ + center_horiz = true; + refresh_widgets = true; + writeParamsToSVG(); +} + +void +LPEMirrorSymmetry::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + if ((!split_items || discard_orig_path) && lpesatellites.data().size()) { + processObjects(LPE_ERASE); + } + if (link_styles) { + reset = true; + } + if (!lpesatellites.data().size()) { + lpesatellites.read_from_SVG(); + if (lpesatellites.data().size()) { + lpesatellites.update_satellites(); + } + } + original_bbox(lpeitem, false, true); + Point point_a(boundingbox_X.max(), boundingbox_Y.min()); + Point point_b(boundingbox_X.max(), boundingbox_Y.max()); + Point point_c(boundingbox_X.middle(), boundingbox_Y.middle()); + if (center_vert) { + center_point.param_setValue(point_c); + end_point.param_setValue(Geom::Point(boundingbox_X.middle(), boundingbox_Y.min())); + //force update + start_point.param_setValue(Geom::Point(boundingbox_X.middle(), boundingbox_Y.max()),true); + center_vert = false; + } else if (center_horiz) { + center_point.param_setValue(point_c); + end_point.param_setValue(Geom::Point(boundingbox_X.max(), boundingbox_Y.middle())); + start_point.param_setValue(Geom::Point(boundingbox_X.min(), boundingbox_Y.middle()),true); + //force update + center_horiz = false; + } else { + + if (mode == MT_Y) { + point_a = Geom::Point(boundingbox_X.min(),center_point[Y]); + point_b = Geom::Point(boundingbox_X.max(),center_point[Y]); + center_point.param_setValue(Geom::middle_point((Geom::Point)point_a, (Geom::Point)point_b)); + } + if (mode == MT_X) { + point_a = Geom::Point(center_point[X],boundingbox_Y.min()); + point_b = Geom::Point(center_point[X],boundingbox_Y.max()); + center_point.param_setValue(Geom::middle_point((Geom::Point)point_a, (Geom::Point)point_b)); + } + if ((Geom::Point)start_point == (Geom::Point)end_point) { + start_point.param_setValue(point_a); + end_point.param_setValue(point_b); + previous_center = Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point); + center_point.param_setValue(previous_center); + return; + } + if ( mode == MT_X || mode == MT_Y ) { + if (!are_near(previous_center, (Geom::Point)center_point, 0.01)) { + center_point.param_setValue(Geom::middle_point(point_a, point_b)); + end_point.param_setValue(point_b); + start_point.param_setValue(point_a); + } else { + if ( mode == MT_X ) { + if (!are_near(start_point[X], point_a[X], 0.01)) { + start_point.param_setValue(point_a); + } + if (!are_near(end_point[X], point_b[X], 0.01)) { + end_point.param_setValue(point_b); + } + } else { //MT_Y + if (!are_near(start_point[Y], point_a[Y], 0.01)) { + start_point.param_setValue(point_a); + } + if (!are_near(end_point[Y], point_b[Y], 0.01)) { + end_point.param_setValue(point_b); + } + } + } + } else if ( mode == MT_FREE) { + if (are_near(previous_center, (Geom::Point)center_point, 0.01)) { + center_point.param_setValue(Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point)); + + } else { + Geom::Point trans = center_point - Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point); + start_point.param_setValue(start_point * trans); + end_point.param_setValue(end_point * trans); + } + } else if ( mode == MT_V){ + SPDocument *document = getSPDoc(); + if (document) { + Geom::Affine transform = i2anc_affine(lpeitem, nullptr).inverse(); + Geom::Point sp = Geom::Point(document->getWidth().value("px")/2.0, 0) * transform; + start_point.param_setValue(sp); + Geom::Point ep = Geom::Point(document->getWidth().value("px")/2.0, document->getHeight().value("px")) * transform; + end_point.param_setValue(ep); + center_point.param_setValue(Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point)); + } + } else { //horizontal page + SPDocument *document = getSPDoc(); + if (document) { + Geom::Affine transform = i2anc_affine(lpeitem, nullptr).inverse(); + Geom::Point sp = Geom::Point(0, document->getHeight().value("px")/2.0) * transform; + start_point.param_setValue(sp); + Geom::Point ep = Geom::Point(document->getWidth().value("px"), document->getHeight().value("px")/2.0) * transform; + end_point.param_setValue(ep); + center_point.param_setValue(Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point)); + } + } + } + previous_center = center_point; +} + +void LPEMirrorSymmetry::cloneStyle(SPObject *orig, SPObject *dest) +{ + dest->setAttribute("transform", orig->getAttribute("transform")); + dest->setAttribute("mask", orig->getAttribute("mask")); + dest->setAttribute("clip-path", orig->getAttribute("clip-path")); + dest->setAttribute("class", orig->getAttribute("class")); + dest->setAttribute("style", orig->getAttribute("style")); + for (auto iter : orig->style->properties()) { + if (iter->style_src != SPStyleSrc::UNSET) { + auto key = iter->id(); + if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) { + const gchar *attr = orig->getAttribute(iter->name().c_str()); + if (attr) { + dest->setAttribute(iter->name(), attr); + } + } + } + } +} + +void LPEMirrorSymmetry::cloneD(SPObject *orig, SPObject *dest) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if ( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() == SP_GROUP(dest)->getItemCount() ) { + if (reset) { + cloneStyle(orig, dest); + } + std::vector< SPObject * > childs = orig->childList(true); + size_t index = 0; + for (auto & child : childs) { + SPObject *dest_child = dest->nthChild(index); + cloneD(child, dest_child); + index++; + } + return; + } else if( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() != SP_GROUP(dest)->getItemCount()) { + split_items.param_setValue(false); + return; + } + + if (SP_IS_TEXT(orig) && SP_IS_TEXT(dest) && SP_TEXT(orig)->children.size() == SP_TEXT(dest)->children.size()) { + if (reset) { + cloneStyle(orig, dest); + } + size_t index = 0; + for (auto &child : SP_TEXT(orig)->children) { + SPObject *dest_child = dest->nthChild(index); + cloneD(&child, dest_child); + index++; + } + } + + SPShape * shape = SP_SHAPE(orig); + SPPath * path = SP_PATH(dest); + if (shape) { + SPCurve const *c = shape->curve(); + if (c) { + auto str = sp_svg_write_path(c->get_pathvector()); + if (shape && !path) { + const char *id = dest->getAttribute("id"); + const char *style = dest->getAttribute("style"); + Inkscape::XML::Document *xml_doc = dest->document->getReprDoc(); + Inkscape::XML::Node *dest_node = xml_doc->createElement("svg:path"); + dest_node->setAttribute("id", id); + dest_node->setAttribute("style", style); + dest->updateRepr(xml_doc, dest_node, SP_OBJECT_WRITE_ALL); + path = SP_PATH(dest); + } + path->setAttribute("d", str); + } else { + path->removeAttribute("d"); + } + } + if (reset) { + cloneStyle(orig, dest); + } +} + +Inkscape::XML::Node * +LPEMirrorSymmetry::createPathBase(SPObject *elemref) { + SPDocument *document = getSPDoc(); + if (!document) { + return nullptr; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *prev = elemref->getRepr(); + SPGroup *group = dynamic_cast<SPGroup *>(elemref); + if (group) { + Inkscape::XML::Node *container = xml_doc->createElement("svg:g"); + container->setAttribute("transform", prev->attribute("transform")); + container->setAttribute("mask", prev->attribute("mask")); + container->setAttribute("clip-path", prev->attribute("clip-path")); + container->setAttribute("class", prev->attribute("class")); + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + Inkscape::XML::Node *previous = nullptr; + for (auto sub_item : item_list) { + Inkscape::XML::Node *resultnode = createPathBase(sub_item); + container->addChild(resultnode, previous); + previous = resultnode; + } + return container; + } + Inkscape::XML::Node *resultnode = xml_doc->createElement("svg:path"); + resultnode->setAttribute("transform", prev->attribute("transform")); + resultnode->setAttribute("mask", prev->attribute("mask")); + resultnode->setAttribute("clip-path", prev->attribute("clip-path")); + resultnode->setAttribute("class", prev->attribute("class")); + return resultnode; +} + +void +LPEMirrorSymmetry::toMirror(Geom::Affine transform) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + //Inkscape::XML::Document *xml_doc = document->getReprDoc(); + SPObject *elemref = nullptr; + if (!is_load && container != sp_lpe_item->parent) { + lpesatellites.read_from_SVG(); + return; + } + if (lpesatellites.data().size() && lpesatellites.data()[0]) { + elemref = lpesatellites.data()[0]->getObject(); + } + Inkscape::XML::Node *phantom = nullptr; + bool creation = false; + if (elemref) { + phantom = elemref->getRepr(); + } else { + creation = true; + phantom = createPathBase(sp_lpe_item); + reset = true; + elemref = container->appendChildRepr(phantom); + Inkscape::GC::release(phantom); + } + cloneD(sp_lpe_item, elemref); + reset = link_styles; + elemref->getRepr()->setAttributeOrRemoveIfEmpty("transform", sp_svg_transform_write(transform)); + // Alow work in clones + /* if (elemref->parent != container) { + if (!creation) { + lpesatellites.unlink(elemref); + } + Inkscape::XML::Node *copy = phantom->duplicate(xml_doc); + copy->setAttribute("id", elemref->getId()); + lpesatellites.link(container->appendChildRepr(copy), 0); + Inkscape::GC::release(copy); + elemref->deleteObject(); + lpesatellites.write_to_SVG(); + lpesatellites.update_satellites(); + } else */ + if (creation) { + lpesatellites.clear(); + lpesatellites.link(elemref, 0); + lpesatellites.write_to_SVG(); + if (lpesatellites.is_connected()) { + lpesatellites.update_satellites(); + } + } + if (!lpesatellites.is_connected()) { + if (!creation) { + lpesatellites.write_to_SVG(); + } + lpesatellites.start_listening(); + lpesatellites.update_satellites(true); + } +} + + +//TODO: Migrate the tree next function to effect.cpp/h to avoid duplication +void +LPEMirrorSymmetry::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ + processObjects(LPE_VISIBILITY); +} + +void +LPEMirrorSymmetry::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + return; + } + processObjects(LPE_ERASE); +} + +void +LPEMirrorSymmetry::doOnApply (SPLPEItem const* lpeitem) +{ + using namespace Geom; + + original_bbox(lpeitem, false, true); + + Point point_a(boundingbox_X.max(), boundingbox_Y.min()); + Point point_b(boundingbox_X.max(), boundingbox_Y.max()); + Point point_c(boundingbox_X.max(), boundingbox_Y.middle()); + start_point.param_setValue(point_a, true); + start_point.param_update_default(point_a); + end_point.param_setValue(point_b, true); + end_point.param_update_default(point_b); + center_point.param_setValue(point_c, true); + previous_center = center_point; + //we bump to 1.1 because previous 1.0.2 take no effect because a bug on 1.0.2 + lpeversion.param_setValue("1.2", true); + lpesatellites.update_satellites(); +} + +Geom::PathVector +LPEMirrorSymmetry::doEffect_path (Geom::PathVector const & path_in) +{ + if (split_items && !fuse_paths) { + return path_in; + } + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::PathVector path_out; + + if (!discard_orig_path && !fuse_paths) { + path_out = pathv_to_linear_and_cubic_beziers(path_in); + } + + Geom::Line line_separation((Geom::Point)start_point, (Geom::Point)end_point); + Geom::Affine m = Geom::reflection (line_separation.vector(), (Geom::Point)start_point); + if (fuse_paths && !discard_orig_path) { + for (const auto & path_it : original_pathv) + { + if (path_it.empty()) { + continue; + } + Geom::PathVector tmp_pathvector; + double time_start = 0.0; + int position = 0; + bool end_open = false; + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + if (!are_near(closingline.initialPoint(), closingline.finalPoint())) { + end_open = true; + } + } + Geom::Path original = path_it; + if (end_open && path_it.closed()) { + original.close(false); + original.appendNew<Geom::LineSegment>( original.initialPoint() ); + original.close(true); + } + Geom::Point s = start_point; + Geom::Point e = end_point; + double dir = line_separation.angle(); + double diagonal = Geom::distance(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + Geom::Rect bbox(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + double size_divider = Geom::distance(center_point, bbox) + diagonal; + s = Geom::Point::polar(dir,size_divider) + center_point; + e = Geom::Point::polar(dir + Geom::rad_from_deg(180),size_divider) + center_point; + Geom::Path divider = Geom::Path(s); + divider.appendNew<Geom::LineSegment>(e); + Geom::Crossings cs = crossings(original, divider); + std::vector<double> crossed; + for(auto & c : cs) { + crossed.push_back(c.ta); + } + std::sort(crossed.begin(), crossed.end()); + for (unsigned int i = 0; i < crossed.size(); i++) { + double time_end = crossed[i]; + if (time_start != time_end && time_end - time_start > Geom::EPSILON) { + Geom::Path portion = original.portion(time_start, time_end); + if (!portion.empty()) { + Geom::Point middle = portion.pointAt((double)portion.size()/2.0); + position = Geom::sgn(Geom::cross(e - s, middle - s)); + if (!oposite_fuse) { + position *= -1; + } + if (position == 1) { + if (!split_items) { + Geom::Path mirror = portion.reversed() * m; + mirror.setInitial(portion.finalPoint()); + portion.append(mirror); + if(i != 0) { + portion.setFinal(portion.initialPoint()); + portion.close(); + } + } + tmp_pathvector.push_back(portion); + } + portion.clear(); + } + } + time_start = time_end; + } + position = Geom::sgn(Geom::cross(e - s, original.finalPoint() - s)); + if (!oposite_fuse) { + position *= -1; + } + if (cs.size()!=0 && (position == 1)) { + if (time_start != original.size() && original.size() - time_start > Geom::EPSILON) { + Geom::Path portion = original.portion(time_start, original.size()); + if (!portion.empty()) { + portion = portion.reversed(); + if (!split_items) { + Geom::Path mirror = portion.reversed() * m; + mirror.setInitial(portion.finalPoint()); + portion.append(mirror); + } + portion = portion.reversed(); + if (!original.closed()) { + tmp_pathvector.push_back(portion); + } else { + if (cs.size() > 1 && tmp_pathvector.size() > 0 && tmp_pathvector[0].size() > 0 ) { + if (!split_items) { + portion.setFinal(tmp_pathvector[0].initialPoint()); + portion.setInitial(tmp_pathvector[0].finalPoint()); + } else { + tmp_pathvector[0] = tmp_pathvector[0].reversed(); + portion = portion.reversed(); + portion.setInitial(tmp_pathvector[0].finalPoint()); + } + tmp_pathvector[0].append(portion); + } else { + tmp_pathvector.push_back(portion); + } + if (lpeversion.param_getSVGValue() < "1.1") { + tmp_pathvector[0].close(); + } + } + portion.clear(); + } + } + } + if (!split_open && lpeversion.param_getSVGValue() >= "1.1" && original.closed()) { + for (auto &path : tmp_pathvector) { + if (!path.closed()) { + path.close(); + } + } + sp_flatten(tmp_pathvector, fill_oddEven); + } + if (cs.size() == 0 && position == 1) { + tmp_pathvector.push_back(original); + if (!split_items) { + tmp_pathvector.push_back(original * m); + } + } + path_out.insert(path_out.end(), tmp_pathvector.begin(), tmp_pathvector.end()); + tmp_pathvector.clear(); + } + } else if (!fuse_paths || discard_orig_path) { + for (const auto & i : original_pathv) { + path_out.push_back(i * m); + } + } + return path_out; +} + +void +LPEMirrorSymmetry::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + using namespace Geom; + hp_vec.clear(); + Geom::Path path; + Geom::Point s = start_point; + Geom::Point e = end_point; + path.start( s ); + path.appendNew<Geom::LineSegment>( e ); + Geom::PathVector helper; + helper.push_back(path); + hp_vec.push_back(helper); +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-mirror_symmetry.h b/src/live_effects/lpe-mirror_symmetry.h new file mode 100644 index 0000000..fb0c39f --- /dev/null +++ b/src/live_effects/lpe-mirror_symmetry.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_MIRROR_SYMMETRY_H +#define INKSCAPE_LPE_MIRROR_SYMMETRY_H + +/** \file + * LPE <mirror_symmetry> implementation: mirrors a path with respect to a given line. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * Jabiertxof + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilin Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/satellitearray.h" +#include "live_effects/parameter/text.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum ModeType { + MT_V, + MT_H, + MT_FREE, + MT_X, + MT_Y, + MT_END +}; + +class LPEMirrorSymmetry : public Effect, GroupBBoxEffect { +public: + LPEMirrorSymmetry(LivePathEffectObject *lpeobject); + ~LPEMirrorSymmetry() override; + void doOnApply (SPLPEItem const* lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) override; + bool doOnOpen(SPLPEItem const * /*lpeitem*/) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + Gtk::Widget * newWidget() override; + void cloneStyle(SPObject *orig, SPObject *dest); + void toMirror(Geom::Affine transform); + void cloneD(SPObject *orig, SPObject *dest); + Inkscape::XML::Node * createPathBase(SPObject *elemref); + void centerVert(); + void centerHoriz(); + BoolParam split_items; + +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + +private: + SatelliteArrayParam lpesatellites; + EnumParam<ModeType> mode; + BoolParam discard_orig_path; + BoolParam fuse_paths; + BoolParam oposite_fuse; + BoolParam split_open; + BoolParam link_styles; + PointParam start_point; + PointParam end_point; + PointParam center_point; + Geom::Point previous_center; + SPObject *container; + bool reset; + bool prev_split = false; + bool prev_discard_orig_path = false; + bool center_vert; + bool center_horiz; + LPEMirrorSymmetry(const LPEMirrorSymmetry&) = delete; + LPEMirrorSymmetry& operator=(const LPEMirrorSymmetry&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-offset.cpp b/src/live_effects/lpe-offset.cpp new file mode 100644 index 0000000..b351b6d --- /dev/null +++ b/src/live_effects/lpe-offset.cpp @@ -0,0 +1,710 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <offset> implementation + */ +/* + * Authors: + * Maximilian Albert + * Jabiertxo Arraiza + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * Copyright (C) Jabierto Arraiza 2015 <jabier.arraiza@marker.es> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-offset.h" + +#include <2geom/path-intersection.h> +#include <2geom/piecewise.h> +#include <2geom/svg-path-parser.h> + +#include "inkscape.h" +#include "style.h" + +#include "display/curve.h" +#include "helper/geom-pathstroke.h" +#include "helper/geom.h" +#include "live_effects/parameter/enum.h" +#include "object/sp-shape.h" +#include "path/path-boolop.h" +#include "path/path-util.h" +#include "svg/svg.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" +#include "util/units.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +namespace OfS { + class KnotHolderEntityOffsetPoint : public LPEKnotHolderEntity { + public: + KnotHolderEntityOffsetPoint(LPEOffset * effect) : LPEKnotHolderEntity(effect) {} + ~KnotHolderEntityOffsetPoint() override + { + LPEOffset *lpe = dynamic_cast<LPEOffset *>(_effect); + if (lpe) { + lpe->_knot_entity = nullptr; + } + } + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + private: + }; +} // OfS + + +static const Util::EnumData<unsigned> JoinTypeData[] = { + // clang-format off + {JOIN_BEVEL, N_("Beveled"), "bevel"}, + {JOIN_ROUND, N_("Rounded"), "round"}, + {JOIN_MITER, N_("Miter"), "miter"}, + {JOIN_MITER_CLIP, N_("Miter Clip"), "miter-clip"}, + {JOIN_EXTRAPOLATE, N_("Extrapolated arc"), "extrp_arc"}, + {JOIN_EXTRAPOLATE1, N_("Extrapolated arc Alt1"), "extrp_arc1"}, + {JOIN_EXTRAPOLATE2, N_("Extrapolated arc Alt2"), "extrp_arc2"}, + {JOIN_EXTRAPOLATE3, N_("Extrapolated arc Alt3"), "extrp_arc3"}, + // clang-format on +}; + +static const Util::EnumDataConverter<unsigned> JoinTypeConverter(JoinTypeData, sizeof(JoinTypeData)/sizeof(*JoinTypeData)); + + +LPEOffset::LPEOffset(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + unit(_("Unit"), _("Unit of measurement"), "unit", &wr, this, "mm"), + offset(_("Offset:"), _("Offset"), "offset", &wr, this, 0.0), + linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", JoinTypeConverter, &wr, this, JOIN_MITER), + miter_limit(_("Miter limit:"), _("Maximum length of the miter join (in units of stroke width)"), "miter_limit", &wr, this, 4.0), + attempt_force_join(_("Force miter"), _("Overrides the miter limit and forces a join."), "attempt_force_join", &wr, this, false), + update_on_knot_move(_("Live update"), _("Update while moving handle"), "update_on_knot_move", &wr, this, true) +{ + show_orig_path = true; + registerParameter(&linejoin_type); + registerParameter(&unit); + registerParameter(&offset); + registerParameter(&miter_limit); + registerParameter(&attempt_force_join); + registerParameter(&update_on_knot_move); + offset.param_set_increments(0.1, 0.1); + offset.param_set_digits(6); + offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); + _knot_entity = nullptr; + _provides_knotholder_entities = true; + apply_to_clippath_and_mask = true; + prev_unit = unit.get_abbreviation(); + liveknot = false; + fillrule = fill_nonZero; +} + +LPEOffset::~LPEOffset() +{ + modified_connection.disconnect(); +}; + +bool LPEOffset::doOnOpen(SPLPEItem const *lpeitem) +{ + bool fixed = false; + if (!is_load || is_applied) { + return fixed; + } + legacytest_livarotonly = false; + Glib::ustring version = lpeversion.param_getSVGValue(); + if (version < "1.2") { + if (!SP_ACTIVE_DESKTOP) { + legacytest_livarotonly = true; + } + lpeversion.param_setValue("1.2", true); + fixed = true; + } + return fixed; +} + +void +LPEOffset::doOnApply(SPLPEItem const* lpeitem) +{ + lpeversion.param_setValue("1.2", true); +} + +void +LPEOffset::modified(SPObject *obj, guint flags) +{ + if (flags & SP_OBJECT_STYLE_MODIFIED_FLAG) { + // Get the used fillrule + SPCSSAttr *css; + const gchar *val; + css = sp_repr_css_attr (sp_lpe_item->getRepr() , "style"); + val = sp_repr_css_property (css, "fill-rule", nullptr); + FillRuleFlatten fillrule_chan = fill_nonZero; + if (val && strcmp (val, "evenodd") == 0) + { + fillrule_chan = fill_oddEven; + } + if (fillrule != fillrule_chan) { + sp_lpe_item_update_patheffect (sp_lpe_item, true, true); + } + } +} + +Geom::Point get_nearest_point(Geom::PathVector pathv, Geom::Point point) +{ + Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity()); + std::optional< Geom::PathVectorTime > pathvectortime = pathv.nearestTime(point); + if (pathvectortime) { + Geom::PathTime pathtime = pathvectortime->asPathTime(); + res = pathv[(*pathvectortime).path_index].pointAt(pathtime.curve_index + pathtime.t); + } + return res; +} + +/* double get_separation(Geom::PathVector pathv, Geom::Point point, Geom::Point point_b) +{ + Geom::Point res = Geom::Point(Geom::infinity(), Geom::infinity()); + std::optional<Geom::PathVectorTime> pathvectortime = pathv.nearestTime(point); + std::optional<Geom::PathVectorTime> pathvectortime_b = pathv.nearestTime(point_b); + if (pathvectortime && pathvectortime_b) { + Geom::PathTime pathtime = pathvectortime->asPathTime(); + Geom::PathTime pathtime_b = pathvectortime_b->asPathTime(); + if ((*pathvectortime).path_index == (*pathvectortime_b).path_index) { + return std::abs((pathtime.curve_index + pathtime.t) - (pathtime_b.curve_index + pathtime_b.t)); + } + } + return -1; +} */ + +void LPEOffset::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + refresh_widgets = true; + if (!postmul.isTranslation()) { + Geom::Affine current_affine = sp_item_transform_repr(sp_lpe_item); + offset.param_transform_multiply(postmul * current_affine.inverse(), true); + } + offset_pt *= postmul; +} + +Geom::Point LPEOffset::get_default_point(Geom::PathVector pathv) +{ + Geom::Point origin = Geom::Point(Geom::infinity(), Geom::infinity()); + Geom::OptRect bbox = pathv.boundsFast(); + if (bbox) { + origin = Geom::Point((*bbox).midpoint()[Geom::X], (*bbox).top()); + origin = get_nearest_point(pathv, origin); + } + return origin; +} + +double +LPEOffset::sp_get_offset(Geom::Point origin) +{ + double ret_offset = 0; + int winding_value = mix_pathv_all.winding(origin); + bool inset = false; + if (winding_value % 2 != 0) { + inset = true; + } + ret_offset = Geom::distance(origin, get_nearest_point(mix_pathv_all, origin)); + if (inset) { + ret_offset *= -1; + } + return Inkscape::Util::Quantity::convert(ret_offset, "px", unit.get_abbreviation()) * this->scale; +} + +void +LPEOffset::addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(helper_path); +} + +void +LPEOffset::doBeforeEffect (SPLPEItem const* lpeitem) +{ + SPObject *obj = dynamic_cast<SPObject *>(sp_lpe_item); + if (is_load && obj) { + modified_connection = obj->connectModified(sigc::mem_fun(*this, &LPEOffset::modified)); + } + original_bbox(lpeitem); + SPGroup *group = dynamic_cast<SPGroup *>(sp_lpe_item); + if (group) { + mix_pathv_all.clear(); + } + this->scale = lpeitem->i2doc_affine().descrim(); + if (!is_load && prev_unit != unit.get_abbreviation()) { + offset.param_set_value(Inkscape::Util::Quantity::convert(offset, prev_unit, unit.get_abbreviation())); + } + prev_unit = unit.get_abbreviation(); +} + +int offset_winding(Geom::PathVector pathvector, Geom::Path path) +{ + int wind = 0; + Geom::Point p = path.initialPoint(); + for (auto i:pathvector) { + if (i == path) continue; + if (!i.boundsFast().contains(p)) continue; + wind += i.winding(p); + } + return wind; +} + +void LPEOffset::doAfterEffect(SPLPEItem const * /*lpeitem*/, SPCurve *curve) +{ + if (offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { + if (_knot_entity) { + _knot_entity->knot_get(); + } + } + if (is_load) { + offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); + } + if (_knot_entity && sp_lpe_item && !liveknot) { + Geom::PathVector out; + // we don do this on groups, editing is joining ito so no need to update knot + SPShape *shape = dynamic_cast<SPShape *>(sp_lpe_item); + if (shape) { + out = SP_SHAPE(sp_lpe_item)->curve()->get_pathvector(); + offset_pt = get_nearest_point(out, offset_pt); + _knot_entity->knot_get(); + } + } +} + +// TODO: find a way to not remove wanted self intersections +// previously are some failed attempts + +/* // Taken from Knot LPE duple code +static Geom::Path::size_type size_nondegenerate(Geom::Path const &path) +{ + Geom::Path::size_type retval = path.size_default(); + const Geom::Curve &closingline = path.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! + retval = path.size_open(); + } + return retval; +} +gint get_nearest_corner(Geom::OptRect bbox, Geom::Point point) +{ + if (bbox) { + double distance_a = Geom::distance(point, (*bbox).corner(0)); + double distance_b = Geom::distance(point, (*bbox).corner(1)); + double distance_c = Geom::distance(point, (*bbox).corner(2)); + double distance_d = Geom::distance(point, (*bbox).corner(3)); + std::vector<double> distances{distance_a, distance_b, distance_c, distance_d}; + std::vector<double>::iterator mindistance = std::min_element(distances.begin(), distances.end()); + return std::distance(distances.begin(), mindistance); + } + return -1; +} + +// This way not work with good selfintersections on consecutive curves +// and when there is nodes nearest to points +// I try different methods to cleanup without luky +// if anyone is interested there is a way to explore +// if in original path the section into the 2 nearest point dont have +// self intersections we can suppose this is a intersection to remove +// it works very well but in good selfintersections work only in one offset direction +bool consecutiveCurves(Geom::Path pathin, Geom::Point p) { + Geom::Coord mindist = std::numeric_limits<Geom::Coord>::max(); + size_t pos; + for (size_t i = 0; i < pathin.size_default(); ++i) { + Geom::Curve const &c = pathin.at(i); + double dist = Geom::distance(p, c.boundsFast()); + if (dist >= mindist) { + continue; + } + Geom::Coord t = c.nearestTime(p); + Geom::Coord d = Geom::distance(c.pointAt(t), p); + if (d < mindist) { + pos = i; + mindist = d; + } + } + size_t pos_b; + mindist = std::numeric_limits<Geom::Coord>::max(); + for (size_t i = 0; i < pathin.size_default(); ++i) { + Geom::Curve const &c = pathin.at(i); + double dist = Geom::distance(p, c.boundsFast()); + if (dist >= mindist || pos == i) { + continue; + } + Geom::Coord t = c.nearestTime(p); + Geom::Coord d = Geom::distance(c.pointAt(t), p); + if (d < mindist) { + pos_b = i; + mindist = d; + } + } + if (Geom::are_near(Geom::distance(pos,pos_b), 1)) { + return true; + } + return false; +} + +Geom::Path removeIntersects(Geom::Path pathin, Geom::Path pathorig, size_t skipcross) +{ + Geom::Path out; + Geom::Crossings crossings = Geom::self_crossings(pathin); + size_t counter = 0; + for (auto cross : crossings) { + if (!Geom::are_near(cross.ta, cross.tb, 0.01)) { + size_t sizepath = size_nondegenerate(pathin); + double ta = cross.ta > cross.tb ? cross.tb : cross.ta; + double tb = cross.ta > cross.tb ? cross.ta : cross.tb; + if (!pathin.closed()) { + counter++; + if (skipcross >= counter) { + continue; + } + bool removeint = consecutiveCurves(pathorig, pathin.pointAt(ta)); + Geom::Path p0 = pathin; + if (removeint) { + Geom::Path p1 = pathin.portion(ta, tb); + if ((*p1.boundsFast()).maxExtent() > 0.01) { + p0 = pathin.portion(0, ta); + if (!Geom::are_near(tb, sizepath, 0.01)) { + Geom::Path p2 = pathin.portion(tb, sizepath); + p0.setFinal(p2.initialPoint()); + p0.append(p2); + } + } else { + skipcross++; + } + } else { + skipcross++; + } + out = removeIntersects(p0, pathorig, skipcross); + return out; + } + } + } + return pathin; +} */ + +Geom::Path removeIntersects(Geom::Path pathin) +{ + // I have a pending ping to moazin for 1.2 to fix open paths offeset self intesections (Jabiertxof) + // For 1.1 I comment the code because simply slow a lot or crash sometimes and never work really well + /* Geom::Path out; + Geom::Crossings crossings = Geom::self_crossings(pathin); + static size_t maxiter = 0; + if (!maxiter) { + maxiter = crossings.size(); + } + for (auto cross : crossings) { + maxiter--; + if (!maxiter) { + return pathin; + } + if (!Geom::are_near(cross.ta, cross.tb, 0.01)) { + size_t sizepath = size_nondegenerate(pathin); + double ta = cross.ta > cross.tb ? cross.tb : cross.ta; + double tb = cross.ta > cross.tb ? cross.ta : cross.tb; + if (!pathin.closed()) { + Geom::Path p0 = pathin; + Geom::Path p1 = pathin.portion(ta, tb); + p0 = pathin.portion(0, ta); + if (!Geom::are_near(tb, sizepath, 0.01)) { + Geom::Path p2 = pathin.portion(tb, sizepath); + p0.setFinal(p2.initialPoint()); + p0.append(p2); + } + out = removeIntersects(p0); + return out; + } + } + } */ + return pathin; +} + +static Geom::PathVector +sp_simplify_pathvector(Geom::PathVector original_pathv, double threshold) +{ + Path* pathliv = Path_for_pathvector(original_pathv); + pathliv->ConvertEvenLines(threshold); + pathliv->Simplify(threshold); + return Geom::parse_svg_path(pathliv->svg_dump_path()); +} + +Geom::PathVector +LPEOffset::doEffect_path(Geom::PathVector const & path_in) +{ + Geom::PathVector ret_closed; + Geom::PathVector ret_open; + SPItem *item = current_shape; + SPDocument *document = getSPDoc(); + if (!item || !document) { + return path_in; + } + // Get the used fillrule + SPCSSAttr *css; + const gchar *val; + css = sp_repr_css_attr (item->getRepr() , "style"); + val = sp_repr_css_property (css, "fill-rule", nullptr); + + fillrule = fill_nonZero; + if (val && strcmp (val, "evenodd") == 0) + { + fillrule = fill_oddEven; + } + + double tolerance = -1; + if (liveknot) { + tolerance = 3; + } + // Get the doc units offset + double to_offset = Inkscape::Util::Quantity::convert(offset, unit.get_abbreviation(), "px") / this->scale; + Geom::PathVector open_pathv; + Geom::PathVector closed_pathv; + Geom::PathVector mix_pathv; + Geom::PathVector mix_pathv_workon; + Geom::PathVector orig_pathv = pathv_to_linear_and_cubic_beziers(path_in); + helper_path = orig_pathv; + // Store separated open/closed paths + Geom::PathVector splitter; + for (auto &i : orig_pathv) { + // this improve offset in near closed paths + if (Geom::are_near(i.initialPoint(), i.finalPoint())) { + i.close(true); + } + if (i.closed()) { + closed_pathv.push_back(i); + } else { + open_pathv.push_back(i); + } + } + sp_flatten(closed_pathv, fillrule); + + // we flatten using original fill rule + mix_pathv = open_pathv; + for (auto path : closed_pathv) { + mix_pathv.push_back(path); + } + SPGroup *group = dynamic_cast<SPGroup *>(sp_lpe_item); + // Calculate the original pathvector used outside this function + // to calculate the offset + if (group) { + mix_pathv_all.insert(mix_pathv_all.begin(), mix_pathv.begin(), mix_pathv.end()); + } else { + mix_pathv_all = mix_pathv; + } + if (to_offset < 0) { + Geom::OptRect bbox = mix_pathv.boundsFast(); + if (bbox) { + (*bbox).expandBy(to_offset / 2.0); + if ((*bbox).hasZeroArea()) { + Geom::PathVector empty; + return empty; + } + } + } + for (auto pathin : closed_pathv) { + // Geom::OptRect bbox = pathin.boundsFast(); + // if (pbbox && (*pbbox).minExtent() > to_offset) { + mix_pathv_workon.push_back(pathin); + //} + } + mix_pathv_workon.insert(mix_pathv_workon.begin(), open_pathv.begin(), open_pathv.end()); + + if (offset == 0.0) { + if (is_load && offset_pt == Geom::Point(Geom::infinity(), Geom::infinity())) { + offset_pt = get_default_point(path_in); + if (_knot_entity) { + _knot_entity->knot_get(); + } + } + return path_in; + } + Geom::OptRect bbox = closed_pathv.boundsFast(); + double bboxsize = 0; + if (bbox) { + bboxsize = (*bbox).maxExtent(); + } + LineJoinType join = static_cast<LineJoinType>(linejoin_type.get_value()); + Geom::PathVector ret_closed_tmp; + if (to_offset > 0) { + for (auto &i : mix_pathv_workon) { + Geom::Path tmp = half_outline( + i, to_offset, (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), join, tolerance); + if (tmp.closed()) { + Geom::OptRect pbbox = tmp.boundsFast(); + if (pbbox && (*pbbox).minExtent() > to_offset) { + ret_closed_tmp.push_back(tmp); + } + } else { + Geom::Path tmp_b = half_outline(i.reversed(), to_offset, + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), + join, tolerance); + Geom::PathVector switch_pv_a(tmp); + Geom::PathVector switch_pv_b(tmp_b); + double distance_b = Geom::distance(offset_pt, get_nearest_point(switch_pv_a, offset_pt)); + double distance_a = Geom::distance(offset_pt, get_nearest_point(switch_pv_b, offset_pt)); + if (distance_b < distance_a) { + ret_open.push_back(removeIntersects(tmp)); + } else { + ret_open.push_back(removeIntersects(tmp_b)); + } + } + } + sp_flatten(ret_closed_tmp, fill_nonZero); + for (auto path : ret_closed_tmp) { + ret_closed.push_back(path); + } + } else if (to_offset < 0) { + for (auto &i : mix_pathv_workon) { + double gap = 0.01; + if (legacytest_livarotonly) { + gap = 0; + } + Geom::Path tmp = + half_outline(i.reversed(), std::abs(to_offset + gap), + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), join, tolerance); + // not remember why i instead tmp, afete 1.1 release we can switch to tmp to tests + if (i.closed()) { + Geom::PathVector out(tmp); + for (auto path : out) { + ret_closed.push_back(path); + } + } else { + Geom::Path tmp_b = half_outline(i, std::abs(to_offset), + (attempt_force_join ? std::numeric_limits<double>::max() : miter_limit), + join, tolerance); + Geom::PathVector switch_pv_a(tmp); + Geom::PathVector switch_pv_b(tmp_b); + double distance_b = Geom::distance(offset_pt, get_nearest_point(switch_pv_a, offset_pt)); + double distance_a = Geom::distance(offset_pt, get_nearest_point(switch_pv_b, offset_pt)); + if (distance_b < distance_a) { + ret_open.push_back(removeIntersects(tmp)); + } else { + ret_open.push_back(removeIntersects(tmp_b)); + } + } + } + if (!ret_closed.empty()) { + Geom::PathVector outline; + for (const auto &i : mix_pathv_workon) { + if (i.closed()) { + Geom::PathVector tmp = Inkscape::outline(i, std::abs(to_offset * 2), 4.0, join, + static_cast<LineCapType>(BUTT_FLAT), tolerance); + outline.insert(outline.begin(), tmp.begin(), tmp.end()); + } + } + sp_flatten(outline, fill_nonZero); + double size = Geom::L2(Geom::bounds_fast(ret_closed)->dimensions()); + size /= sp_lpe_item->i2doc_affine().descrim(); + ret_closed = sp_pathvector_boolop(outline, ret_closed, bool_op_diff, fill_nonZero, fill_nonZero, legacytest_livarotonly); + if (!liveknot && legacytest_livarotonly) { + ret_closed = sp_simplify_pathvector(ret_closed, 0.0003 * size); + } + } + } + ret_closed.insert(ret_closed.begin(), ret_open.begin(), ret_open.end()); + return ret_closed; +} + +void LPEOffset::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new OfS::KnotHolderEntityOffsetPoint(this); + _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, + "LPEOffset", _("Offset point")); + _knot_entity->knot->setMode(Inkscape::CANVAS_ITEM_CTRL_MODE_COLOR); + _knot_entity->knot->setShape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + _knot_entity->knot->setFill(0xFF6600FF, 0x4BA1C7FF, 0xCF1410FF, 0xFF6600FF); + _knot_entity->knot->setStroke(0x000000FF, 0x000000FF, 0x000000FF, 0x000000FF); + _knot_entity->knot->updateCtrl(); + offset_pt = Geom::Point(Geom::infinity(), Geom::infinity()); + knotholder->add(_knot_entity); +} + +namespace OfS { + +void KnotHolderEntityOffsetPoint::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + using namespace Geom; + LPEOffset* lpe = dynamic_cast<LPEOffset *>(_effect); + Geom::Point s = snap_knot_position(p, state); + double offset = lpe->sp_get_offset(s); + lpe->offset_pt = s; + if (lpe->update_on_knot_move) { + lpe->liveknot = true; + lpe->offset.param_set_value(offset); + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, false); + } else { + lpe->liveknot = false; + } +} + +void KnotHolderEntityOffsetPoint::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + LPEOffset *lpe = dynamic_cast<LPEOffset *>(_effect); + lpe->refresh_widgets = true; + lpe->liveknot = false; + using namespace Geom; + Geom::Point s = lpe->offset_pt; + double offset = lpe->sp_get_offset(s); + lpe->offset.param_set_value(offset); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +Geom::Point KnotHolderEntityOffsetPoint::knot_get() const +{ + LPEOffset *lpe = dynamic_cast<LPEOffset *>(_effect); + if (!lpe) { + return Geom::Point(); + } + if (!lpe->update_on_knot_move) { + return lpe->offset_pt; + } + Geom::Point nearest = lpe->offset_pt; + if (nearest == Geom::Point(Geom::infinity(), Geom::infinity())) { + Geom::PathVector out; + SPGroup *group = dynamic_cast<SPGroup *>(item); + SPShape *shape = dynamic_cast<SPShape *>(item); + if (group) { + std::vector<SPItem *> item_list = sp_item_group_item_list(group); + for (auto child : item_list) { + SPShape *subchild = dynamic_cast<SPShape *>(child); + if (subchild) { + Geom::PathVector tmp = subchild->curve()->get_pathvector(); + out.insert(out.begin(), tmp.begin(), tmp.end()); + sp_flatten(out, fill_oddEven); + } + } + } else if (shape) { + SPCurve const *c = shape->curve(); + if (c) { + out = c->get_pathvector(); + } + } + if (!out.empty()) { + nearest = lpe->get_default_point(out); + } + } + lpe->offset_pt = nearest; + return lpe->offset_pt; +} + +} // namespace OfS +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-offset.h b/src/live_effects/lpe-offset.h new file mode 100644 index 0000000..1fa0b0e --- /dev/null +++ b/src/live_effects/lpe-offset.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_OFFSET_H +#define INKSCAPE_LPE_OFFSET_H + +/** \file + * LPE <offset> implementation, see lpe-offset.cpp. + */ + +/* + * Authors: + * Maximilian Albert + * Jabiertxo Arraiza + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/unit.h" +// this is only to flatten nonzero fillrule +#include "livarot/Path.h" +#include "livarot/Shape.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace OfS { +// we need a separate namespace to avoid clashes with other LPEs +class KnotHolderEntityOffsetPoint; +} + +typedef FillRule FillRuleFlatten; + +class LPEOffset : public Effect, GroupBBoxEffect { +public: + LPEOffset(LivePathEffectObject *lpeobject); + ~LPEOffset() override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doAfterEffect(SPLPEItem const * /*lpeitem*/, SPCurve *curve) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void doOnApply(SPLPEItem const* lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + void calculateOffset (Geom::PathVector const & path_in); + Geom::Path cleanupPathSelfIntersects(Geom::Path path, size_t originpos, double tolerance); + Geom::Point get_default_point(Geom::PathVector pathv); + double sp_get_offset(Geom::Point origin); + friend class OfS::KnotHolderEntityOffsetPoint; + +private: + UnitParam unit; + ScalarParam offset; + EnumParam<unsigned> linejoin_type; + ScalarParam miter_limit; + BoolParam attempt_force_join; + + BoolParam update_on_knot_move; + Geom::Point offset_pt; + Glib::ustring prev_unit; + double scale = 1; //take document scale and additional parent transformations into account + KnotHolderEntity * _knot_entity; + Geom::PathVector mix_pathv_all; + Geom::PathVector helper_path; + Inkscape::UI::Widget::Scalar *offset_widget; + FillRuleFlatten fillrule; + bool liveknot; + bool legacytest_livarotonly = false; + void modified(SPObject */*obj*/, guint flags); + sigc::connection modified_connection; + LPEOffset(const LPEOffset&); + LPEOffset& operator=(const LPEOffset&); +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-parallel.cpp b/src/live_effects/lpe-parallel.cpp new file mode 100644 index 0000000..799108e --- /dev/null +++ b/src/live_effects/lpe-parallel.cpp @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <parallel> implementation + */ +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007-2012 <j.b.c.engelen@alumnus.utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-parallel.h" + +#include "display/curve.h" + +#include "object/sp-shape.h" + +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +namespace Pl { + +class KnotHolderEntityLeftEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityLeftEnd(LPEParallel *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityRightEnd(LPEParallel *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +} // namespace Pl + +LPEParallel::LPEParallel(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + offset_pt(_("Offset"), _("Adjust the offset"), "offset_pt", &wr, this), + length_left(_("Length left:"), _("Specifies the left end of the parallel"), "length-left", &wr, this, 150), + length_right(_("Length right:"), _("Specifies the right end of the parallel"), "length-right", &wr, this, 150) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + registerParameter(&offset_pt); + registerParameter(&length_left); + registerParameter(&length_right); +} + +LPEParallel::~LPEParallel() += default; + +void +LPEParallel::doOnApply (SPLPEItem const* lpeitem) +{ + auto shape = dynamic_cast<SPShape const *>(lpeitem); + if (!shape) { + g_warning("LPE parallel can only be applied to shapes (not groups)."); + SPLPEItem * item = const_cast<SPLPEItem*>(lpeitem); + item->removeCurrentPathEffect(false); + return; + } + SPCurve const *curve = shape->curve(); + + A = *(curve->first_point()); + B = *(curve->last_point()); + dir = unit_vector(B - A); + Geom::Point offset = (A + B)/2 + dir.ccw() * 100; + offset_pt.param_update_default(offset); + offset_pt.param_setValue(offset, true); +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEParallel::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + Piecewise<D2<SBasis> > output; + + A = pwd2_in.firstValue(); + B = pwd2_in.lastValue(); + dir = unit_vector(B - A); + + C = offset_pt - dir * length_left; + D = offset_pt + dir * length_right; + + output = Piecewise<D2<SBasis> >(D2<SBasis>(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + + return output + dir; +} + +void LPEParallel::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) { + { + KnotHolderEntity *e = new Pl::KnotHolderEntityLeftEnd(this); + e->create(desktop, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:ParallelLeftEnd", + _("Adjust the \"left\" end of the parallel")); + knotholder->add(e); + } + { + KnotHolderEntity *e = new Pl::KnotHolderEntityRightEnd(this); + e->create(desktop, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:ParallelRightEnd", + _("Adjust the \"right\" end of the parallel")); + knotholder->add(e); + } +}; + +namespace Pl { + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + LPEParallel *lpe = dynamic_cast<LPEParallel *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = L2(s - lpe->offset_pt) * sgn(dot(s - lpe->offset_pt, lpe->dir)); + lpe->length_left.param_set_value(-lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + LPEParallel *lpe = dynamic_cast<LPEParallel *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = L2(s - lpe->offset_pt) * sgn(dot(s - lpe->offset_pt, lpe->dir)); + lpe->length_right.param_set_value(lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const +{ + LPEParallel const *lpe = dynamic_cast<LPEParallel const*>(_effect); + return lpe->C; +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const +{ + LPEParallel const *lpe = dynamic_cast<LPEParallel const*>(_effect); + return lpe->D; +} + +} // namespace Pl + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-parallel.h b/src/live_effects/lpe-parallel.h new file mode 100644 index 0000000..0a2f65e --- /dev/null +++ b/src/live_effects/lpe-parallel.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PARALLEL_H +#define INKSCAPE_LPE_PARALLEL_H + +/** \file + * LPE <parallel> implementation + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace Pl { + // we need a separate namespace to avoid clashes with LPEPerpBisector + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; +} + +class LPEParallel : public Effect { +public: + LPEParallel(LivePathEffectObject *lpeobject); + ~LPEParallel() override; + + void doOnApply (SPLPEItem const* lpeitem) override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + /* the knotholder entity classes must be declared friends */ + friend class Pl::KnotHolderEntityLeftEnd; + friend class Pl::KnotHolderEntityRightEnd; + void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + +private: + PointParam offset_pt; + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point A; + Geom::Point B; + Geom::Point C; + Geom::Point D; + Geom::Point M; + Geom::Point N; + Geom::Point dir; + + LPEParallel(const LPEParallel&) = delete; + LPEParallel& operator=(const LPEParallel&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-path_length.cpp b/src/live_effects/lpe-path_length.cpp new file mode 100644 index 0000000..3a4ca88 --- /dev/null +++ b/src/live_effects/lpe-path_length.cpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <path_length> implementation. + */ +/* + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * Johan Engelen + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-path_length.h" +#include "util/units.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEPathLength::LPEPathLength(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + scale(_("Scale:"), _("Scaling factor"), "scale", &wr, this, 1.0), + info_text(this), + unit(_("Unit:"), _("Unit"), "unit", &wr, this), + display_unit(_("Display unit"), _("Print unit after path length"), "display_unit", &wr, this, true) +{ + registerParameter(&scale); + registerParameter(&info_text); + registerParameter(&unit); + registerParameter(&display_unit); +} + +LPEPathLength::~LPEPathLength() += default; + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEPathLength::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + /* convert the measured length to the correct unit ... */ + double lengthval = Geom::length(pwd2_in) * scale; + lengthval = Inkscape::Util::Quantity::convert(lengthval, "px", unit.get_abbreviation()); + + /* ... set it as the canvas text ... */ + gchar *arc_length = g_strdup_printf("%.2f %s", lengthval, + display_unit ? unit.get_abbreviation() : ""); + info_text.param_setValue(arc_length); + g_free(arc_length); + + info_text.setPosAndAnchor(pwd2_in, 0.5, 10); + + // TODO: how can we compute the area (such that cw turns don't count negative)? + // should we display the area here, too, or write a new LPE for this? + Piecewise<D2<SBasis> > A = integral(pwd2_in); + Point c; + double area; + if (centroid(pwd2_in, c, area)) { + //g_print ("Area is zero\n"); + } + //g_print ("Area: %f\n", area); + if (!this->isVisible()) { + info_text.param_setValue(""); + } + return pwd2_in; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-path_length.h b/src/live_effects/lpe-path_length.h new file mode 100644 index 0000000..115bf5c --- /dev/null +++ b/src/live_effects/lpe-path_length.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PATH_LENGTH_H +#define INKSCAPE_LPE_PATH_LENGTH_H + +/** \file + * LPE <path_length> implementation. + */ + +/* + * Authors: + * Maximilian Albert <maximilian.albert@gmail.com> + * + * Copyright (C) 2007-2008 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/unit.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPathLength : public Effect { +public: + LPEPathLength(LivePathEffectObject *lpeobject); + ~LPEPathLength() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + +private: + LPEPathLength(const LPEPathLength&) = delete; + LPEPathLength& operator=(const LPEPathLength&) = delete; + ScalarParam scale; + TextParamInternal info_text; + UnitParam unit; + BoolParam display_unit; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-patternalongpath.cpp b/src/live_effects/lpe-patternalongpath.cpp new file mode 100644 index 0000000..17a099c --- /dev/null +++ b/src/live_effects/lpe-patternalongpath.cpp @@ -0,0 +1,396 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cmath> +#include <algorithm> +#include <vector> + +#include <2geom/bezier-to-sbasis.h> + +#include "live_effects/lpe-patternalongpath.h" +#include "live_effects/lpeobject.h" + +#include "display/curve.h" + +#include "object/sp-shape.h" + +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + + +/* Theory in e-mail from J.F. Barraud +Let B be the skeleton path, and P the pattern (the path to be deformed). + +P is a map t --> P(t) = ( x(t), y(t) ). +B is a map t --> B(t) = ( a(t), b(t) ). + +The first step is to re-parametrize B by its arc length: this is the parametrization in which a point p on B is located by its distance s from start. One obtains a new map s --> U(s) = (a'(s),b'(s)), that still describes the same path B, but where the distance along B from start to +U(s) is s itself. + +We also need a unit normal to the path. This can be obtained by computing a unit tangent vector, and rotate it by 90�. Call this normal vector N(s). + +The basic deformation associated to B is then given by: + + (x,y) --> U(x)+y*N(x) + +(i.e. we go for distance x along the path, and then for distance y along the normal) + +Of course this formula needs some minor adaptations (as is it depends on the absolute position of P for instance, so a little translation is needed +first) but I think we can first forget about them. +*/ + +namespace Inkscape { +namespace LivePathEffect { + +namespace WPAP { + class KnotHolderEntityWidthPatternAlongPath : public LPEKnotHolderEntity { + public: + KnotHolderEntityWidthPatternAlongPath(LPEPatternAlongPath * effect) : LPEKnotHolderEntity(effect) {} + ~KnotHolderEntityWidthPatternAlongPath() override + { + LPEPatternAlongPath *lpe = dynamic_cast<LPEPatternAlongPath *> (_effect); + lpe->_knot_entity = nullptr; + } + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + }; +} // WPAP + +static const Util::EnumData<PAPCopyType> PAPCopyTypeData[PAPCT_END] = { + // clang-format off + {PAPCT_SINGLE, N_("Single"), "single"}, + {PAPCT_SINGLE_STRETCHED, N_("Single, stretched"), "single_stretched"}, + {PAPCT_REPEATED, N_("Repeated"), "repeated"}, + {PAPCT_REPEATED_STRETCHED, N_("Repeated, stretched"), "repeated_stretched"} + // clang-format on +}; +static const Util::EnumDataConverter<PAPCopyType> PAPCopyTypeConverter(PAPCopyTypeData, PAPCT_END); + +LPEPatternAlongPath::LPEPatternAlongPath(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + pattern(_("Pattern source:"), _("Path to put along the skeleton path"), "pattern", &wr, this, "M0,0 L1,0"), + original_height(0.0), + prop_scale(_("_Width:"), _("Width of the pattern"), "prop_scale", &wr, this, 1.0), + copytype(_("Pattern copies:"), _("How many pattern copies to place along the skeleton path"), + "copytype", PAPCopyTypeConverter, &wr, this, PAPCT_SINGLE_STRETCHED), + scale_y_rel(_("Wid_th in units of length"), + _("Scale the width of the pattern in units of its length"), + "scale_y_rel", &wr, this, false), + spacing(_("Spa_cing:"), + // xgettext:no-c-format + _("Space between copies of the pattern. Negative values allowed, but are limited to -90% of pattern width."), + "spacing", &wr, this, 0), + normal_offset(_("No_rmal offset:"), "", "normal_offset", &wr, this, 0), + tang_offset(_("Tan_gential offset:"), "", "tang_offset", &wr, this, 0), + prop_units(_("Offsets in _unit of pattern size"), + _("Spacing, tangential and normal offset are expressed as a ratio of width/height"), + "prop_units", &wr, this, false), + vertical_pattern(_("Pattern is _vertical"), _("Rotate pattern 90 deg before applying"), + "vertical_pattern", &wr, this, false), + hide_knot(_("Hide width knot"), _("Hide width knot"),"hide_knot", &wr, this, false), + fuse_tolerance(_("_Fuse nearby ends:"), _("Fuse ends closer than this number. 0 means don't fuse."), + "fuse_tolerance", &wr, this, 0) +{ + registerParameter(&pattern); + registerParameter(©type); + registerParameter(&prop_scale); + registerParameter(&scale_y_rel); + registerParameter(&spacing); + registerParameter(&normal_offset); + registerParameter(&tang_offset); + registerParameter(&prop_units); + registerParameter(&vertical_pattern); + registerParameter(&hide_knot); + registerParameter(&fuse_tolerance); + prop_scale.param_set_digits(3); + prop_scale.param_set_increments(0.01, 0.10); + _knot_entity = nullptr; + _provides_knotholder_entities = true; + +} + +LPEPatternAlongPath::~LPEPatternAlongPath() += default; + +bool +LPEPatternAlongPath::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + pattern.reload(); + return false; +} + +void LPEPatternAlongPath::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) { + pattern.param_transform_multiply(postmul, false); + } +} + +void +LPEPatternAlongPath::doBeforeEffect (SPLPEItem const* lpeitem) +{ + // get the pattern bounding box + Geom::OptRect bbox = pattern.get_pathvector().boundsFast(); + if (bbox) { + original_height = (*bbox)[Geom::Y].max() - (*bbox)[Geom::Y].min(); + } + if (is_load) { + pattern.reload(); + } + if (_knot_entity) { + if (hide_knot) { + helper_path.clear(); + _knot_entity->knot->hide(); + } else { + _knot_entity->knot->show(); + } + _knot_entity->update_knot(); + } +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEPatternAlongPath::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + // Don't allow empty path parameter: + if ( pattern.get_pathvector().empty() ) { + return pwd2_in; + } + +/* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */ + Piecewise<D2<SBasis> > output; + std::vector<Geom::Piecewise<Geom::D2<Geom::SBasis> > > pre_output; + + PAPCopyType type = copytype.get_value(); + Geom::Affine affine = pattern.get_relative_affine(); + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pattern.get_pwd2() * affine); + Piecewise<SBasis> x0 = vertical_pattern.get_value() ? Piecewise<SBasis>(patternd2[1]) : Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y0 = vertical_pattern.get_value() ? Piecewise<SBasis>(patternd2[0]) : Piecewise<SBasis>(patternd2[1]); + OptInterval pattBndsX = bounds_exact(x0); + OptInterval pattBndsY = bounds_exact(y0); + if (pattBndsX && pattBndsY) { + x0 -= pattBndsX->min(); + y0 -= pattBndsY->middle(); + + double xspace = spacing; + double noffset = normal_offset; + double toffset = tang_offset; + if (prop_units.get_value()){ + xspace *= pattBndsX->extent(); + noffset *= pattBndsY->extent(); + toffset *= pattBndsX->extent(); + } + + //Prevent more than 90% overlap... + if (xspace < -pattBndsX->extent() * 0.9) { + xspace = -pattBndsX->extent() * 0.9; + } + //TODO: dynamical update of parameter ranges? + //if (prop_units.get_value()){ + // spacing.param_set_range(-.9, Geom::infinity()); + // }else{ + // spacing.param_set_range(-pattBndsX.extent()*.9, Geom::infinity()); + // } + + y0 += noffset; + + std::vector<Geom::Piecewise<Geom::D2<Geom::SBasis> > > paths_in; + paths_in = split_at_discontinuities(pwd2_in); + + for (auto path_i : paths_in){ + Piecewise<SBasis> x = x0; + Piecewise<SBasis> y = y0; + Piecewise<D2<SBasis> > uskeleton = arc_length_parametrization(path_i,2, 0.1); + uskeleton = remove_short_cuts(uskeleton, 0.01); + Piecewise<D2<SBasis> > n = rot90(derivative(uskeleton)); + if (Geom::are_near(pwd2_in[0].at0(),pwd2_in[pwd2_in.size()-1].at1(), 0.01)) { + n = force_continuity(remove_short_cuts(n, 0.1), 0.01); + } else { + n = force_continuity(remove_short_cuts(n, 0.1)); + } + int nbCopies = 0; + double scaling = 1; + switch(type) { + case PAPCT_REPEATED: + nbCopies = static_cast<int>(floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace))); + pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); + break; + + case PAPCT_SINGLE: + nbCopies = (toffset + pattBndsX->extent() < uskeleton.domain().extent()) ? 1 : 0; + break; + + case PAPCT_SINGLE_STRETCHED: + nbCopies = 1; + scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); + break; + + case PAPCT_REPEATED_STRETCHED: + // if uskeleton is closed: + if (are_near(path_i.segs.front().at0(), path_i.segs.back().at1())){ + nbCopies = std::max(1, static_cast<int>(std::floor((uskeleton.domain().extent() - toffset)/(pattBndsX->extent()+xspace)))); + pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); + scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent()); + // if not closed: no space at the end + }else{ + nbCopies = std::max(1, static_cast<int>(std::floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace)))); + pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace); + scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent() - xspace); + } + break; + + default: + return pwd2_in; + }; + + //Ceil to 6 decimals + scaling = ceil(scaling * 1000000) / 1000000; + double pattWidth = pattBndsX->extent() * scaling; + + x *= scaling; + if ( scale_y_rel.get_value() ) { + y *= prop_scale * scaling; + } else { + y *= prop_scale; + } + x += toffset; + + double offs = 0; + for (int i=0; i<nbCopies; i++){ + if (fuse_tolerance > 0){ + Geom::Piecewise<Geom::D2<Geom::SBasis> > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs); + std::vector<Geom::Piecewise<Geom::D2<Geom::SBasis> > > splited_output_piece = split_at_discontinuities(output_piece); + pre_output.insert(pre_output.end(), splited_output_piece.begin(), splited_output_piece.end() ); + }else{ + output.concat(compose(uskeleton,x+offs)+y*compose(n,x+offs)); + } + offs+=pattWidth; + } + } + if (fuse_tolerance > 0){ + pre_output = fuse_nearby_ends(pre_output, fuse_tolerance); + for (const auto & i : pre_output){ + output.concat(i); + } + } + return output; + } else { + return pwd2_in; + } +} + +void +LPEPatternAlongPath::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(helper_path); +} + + +void +LPEPatternAlongPath::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new WPAP::KnotHolderEntityWidthPatternAlongPath(this); + _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:PatternAlongPath", + _("Change the width")); + knotholder->add(_knot_entity); + if (hide_knot) { + _knot_entity->knot->hide(); + _knot_entity->update_knot(); + } +} + +namespace WPAP { + +void +KnotHolderEntityWidthPatternAlongPath::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + LPEPatternAlongPath *lpe = dynamic_cast<LPEPatternAlongPath *> (_effect); + + Geom::Point const s = snap_knot_position(p, state); + SPShape const *sp_shape = dynamic_cast<SPShape const *>(SP_LPE_ITEM(item)); + if (sp_shape && lpe->original_height) { + auto curve_before = SPCurve::copy(sp_shape->curveForEdit()); + if (curve_before) { + Geom::Path const *path_in = curve_before->first_path(); + Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point knot_pos = this->knot->pos * item->i2dt_affine().inverse(); + Geom::Coord nearest_to_ray = ray.nearestTime(knot_pos); + if(nearest_to_ray == 0){ + lpe->prop_scale.param_set_value(-Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } else { + lpe->prop_scale.param_set_value(Geom::distance(s , ptA)/(lpe->original_height/2.0)); + } + } + if (!lpe->original_height) { + lpe->prop_scale.param_set_value(0); + } + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/live_effects/skeletal/width", lpe->prop_scale); + } + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityWidthPatternAlongPath::knot_get() const +{ + LPEPatternAlongPath *lpe = dynamic_cast<LPEPatternAlongPath *> (_effect); + SPShape const *sp_shape = dynamic_cast<SPShape const *>(SP_LPE_ITEM(item)); + if (sp_shape) { + auto curve_before = SPCurve::copy(sp_shape->curveForEdit()); + if (curve_before) { + Geom::Path const *path_in = curve_before->first_path(); + Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0)); + Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0)); + Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0)); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*first_curve); + Geom::Ray ray(ptA, B); + if (cubic) { + ray.setPoints(ptA, (*cubic)[1]); + } + ray.setAngle(ray.angle() + Geom::rad_from_deg(90)); + Geom::Point result_point = Geom::Point::polar(ray.angle(), (lpe->original_height/2.0) * lpe->prop_scale) + ptA; + lpe->helper_path.clear(); + if (!lpe->hide_knot) { + Geom::Path hp(result_point); + hp.appendNew<Geom::LineSegment>(ptA); + lpe->helper_path.push_back(hp); + hp.clear(); + } + return result_point; + } + } + return Geom::Point(); +} +} // namespace WPAP +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-patternalongpath.h b/src/live_effects/lpe-patternalongpath.h new file mode 100644 index 0000000..733f2cf --- /dev/null +++ b/src/live_effects/lpe-patternalongpath.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PATTERN_ALONG_PATH_H +#define INKSCAPE_LPE_PATTERN_ALONG_PATH_H + +/* + * Inkscape::LPEPatternAlongPath + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace UI { +namespace Toolbar { +class PencilToolbar; +} +} // namespace UI +namespace LivePathEffect { + +namespace WPAP { +class KnotHolderEntityWidthPatternAlongPath; +} + +enum PAPCopyType { + PAPCT_SINGLE = 0, + PAPCT_SINGLE_STRETCHED, + PAPCT_REPEATED, + PAPCT_REPEATED_STRETCHED, + PAPCT_END // This must be last +}; + +class LPEPatternAlongPath : public Effect { +public: + LPEPatternAlongPath(LivePathEffectObject *lpeobject); + ~LPEPatternAlongPath() override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) override; + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + + PathParam pattern; + friend class WPAP::KnotHolderEntityWidthPatternAlongPath; + friend class Inkscape::UI::Toolbar::PencilToolbar; + +protected: + double original_height; + ScalarParam prop_scale; +private: + EnumParam<PAPCopyType> copytype; + BoolParam scale_y_rel; + ScalarParam spacing; + ScalarParam normal_offset; + ScalarParam tang_offset; + BoolParam prop_units; + BoolParam vertical_pattern; + BoolParam hide_knot; + ScalarParam fuse_tolerance; + KnotHolderEntity * _knot_entity; + Geom::PathVector helper_path; + void on_pattern_pasted(); + + LPEPatternAlongPath(const LPEPatternAlongPath&); + LPEPatternAlongPath& operator=(const LPEPatternAlongPath&); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-perp_bisector.cpp b/src/live_effects/lpe-perp_bisector.cpp new file mode 100644 index 0000000..edc7057 --- /dev/null +++ b/src/live_effects/lpe-perp_bisector.cpp @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <perp_bisector> implementation. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilin Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-perp_bisector.h" +#include "display/curve.h" +#include "line-geometry.h" + +#include "object/sp-path.h" + +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { +namespace PB { + +class KnotHolderEntityEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityEnd(LPEPerpBisector *effect) : LPEKnotHolderEntity(effect) {}; + void bisector_end_set(Geom::Point const &p, guint state, bool left = true); +}; + +class KnotHolderEntityLeftEnd : public KnotHolderEntityEnd { +public: + KnotHolderEntityLeftEnd(LPEPerpBisector *effect) : KnotHolderEntityEnd(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public KnotHolderEntityEnd { +public: + KnotHolderEntityRightEnd(LPEPerpBisector *effect) : KnotHolderEntityEnd(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const { + LPEPerpBisector const* lpe = dynamic_cast<LPEPerpBisector const*>(_effect); + return Geom::Point(lpe->C); +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const { + LPEPerpBisector const* lpe = dynamic_cast<LPEPerpBisector const*>(_effect); + return Geom::Point(lpe->D); +} + +void +KnotHolderEntityEnd::bisector_end_set(Geom::Point const &p, guint state, bool left) { + LPEPerpBisector *lpe = dynamic_cast<LPEPerpBisector *>(_effect); + if (!lpe) return; + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->M, lpe->perp_dir); + if (left) { + lpe->C = lpe->M + lpe->perp_dir * lambda; + lpe->length_left.param_set_value(lambda); + } else { + lpe->D = lpe->M + lpe->perp_dir * lambda; + lpe->length_right.param_set_value(-lambda); + } + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), true, true); +} + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) { + bisector_end_set(p, state); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) { + bisector_end_set(p, state, false); +} + +} //namescape PB + +LPEPerpBisector::LPEPerpBisector(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + length_left(_("Length left:"), _("Specifies the left end of the bisector"), "length-left", &wr, this, 200), + length_right(_("Length right:"), _("Specifies the right end of the bisector"), "length-right", &wr, this, 200), + A(0,0), B(0,0), M(0,0), C(0,0), D(0,0), perp_dir(0,0) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&length_left); + registerParameter(&length_right); +} + +LPEPerpBisector::~LPEPerpBisector() += default; + +void +LPEPerpBisector::doOnApply (SPLPEItem const*/*lpeitem*/) +{ + /* make the path a straight line */ + /** + SPCurve* curve = sp_path_getCurveForEdit (SP_PATH(lpeitem)); // TODO: Should we use sp_shape_get_curve()? + + Geom::Point A(curve->first_point()); + Geom::Point B(curve->last_point()); + + SPCurve *c = new SPCurve(); + c->moveto(A); + c->lineto(B); + // TODO: Why doesn't sp_path_set_curve_before_LPE(SP_PATH(lpeitem), c, TRUE, true) work? + SP_PATH(lpeitem)->original_curve = c->ref(); + c->unref(); + **/ +} + + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEPerpBisector::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + Piecewise<D2<SBasis> > output; + + A = pwd2_in.firstValue(); + B = pwd2_in.lastValue(); + M = (A + B)/2; + + perp_dir = unit_vector((B - A).ccw()); + + C = M + perp_dir * length_left; + D = M - perp_dir * length_right; + + output = Piecewise<D2<SBasis> >(D2<SBasis>(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + + return output; +} + +void +LPEPerpBisector::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) { + { + KnotHolderEntity *e = new PB::KnotHolderEntityLeftEnd(this); + e->create(desktop, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:PerpBisectorLeftEnd", + _("Adjust the \"left\" end of the bisector")); +knotholder->add(e); + } + { + KnotHolderEntity *e = new PB::KnotHolderEntityRightEnd(this); + e->create(desktop, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:PerpBisectorRightEnd", + _("Adjust the \"right\" end of the bisector")); + knotholder->add(e); + } +}; + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-perp_bisector.h b/src/live_effects/lpe-perp_bisector.h new file mode 100644 index 0000000..4d09f88 --- /dev/null +++ b/src/live_effects/lpe-perp_bisector.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PERP_BISECTOR_H +#define INKSCAPE_LPE_PERP_BISECTOR_H + +/** \file + * LPE <perp_bisector> implementation, see lpe-perp_bisector.cpp. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilin Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace PB { + // we need a separate namespace to avoid clashes with LPETangentToCurve + class KnotHolderEntityEnd; + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; + void bisector_end_set(SPItem *item, Geom::Point const &p, guint state, bool left = true); +} + +class LPEPerpBisector : public Effect { +public: + LPEPerpBisector(LivePathEffectObject *lpeobject); + ~LPEPerpBisector() override; + + virtual EffectType effectType () { return PERP_BISECTOR; } + + void doOnApply (SPLPEItem const* lpeitem) override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > + doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + /* the knotholder entity functions must be declared friends */ + friend class PB::KnotHolderEntityEnd; + friend class PB::KnotHolderEntityLeftEnd; + friend class PB::KnotHolderEntityRightEnd; + friend void PB::bisector_end_set(SPItem *item, Geom::Point const &p, guint state, bool left); + void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + +private: + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point A; // start of path + Geom::Point B; // end of path + Geom::Point M; // midpoint + Geom::Point C; // left end of bisector + Geom::Point D; // right end of bisector + Geom::Point perp_dir; + + LPEPerpBisector(const LPEPerpBisector&) = delete; + LPEPerpBisector& operator=(const LPEPerpBisector&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-perspective-envelope.cpp b/src/live_effects/lpe-perspective-envelope.cpp new file mode 100644 index 0000000..17e6639 --- /dev/null +++ b/src/live_effects/lpe-perspective-envelope.cpp @@ -0,0 +1,596 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <perspective-envelope> implementation + + */ +/* + * Authors: + * Jabiertxof Code migration from python extensions envelope and perspective + * Aaron Spike, aaron@ekips.org from envelope and perspective python code + * Dmitry Platonov, shadowjack@mail.ru, 2006 perspective approach & math + * Jose Hevia (freon) Transform algorithm from envelope + * + * Copyright (C) 2007-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include "live_effects/lpe-perspective-envelope.h" +#include "helper/geom.h" +#include "display/curve.h" +#include <gsl/gsl_linalg.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +using namespace Geom; + +namespace Inkscape { +namespace LivePathEffect { + +enum DeformationType { + DEFORMATION_PERSPECTIVE, + DEFORMATION_ENVELOPE +}; + +static const Util::EnumData<unsigned> DeformationTypeData[] = { + {DEFORMATION_PERSPECTIVE , N_("Perspective"), "perspective"}, + {DEFORMATION_ENVELOPE , N_("Envelope deformation"), "envelope_deformation"} +}; + +static const Util::EnumDataConverter<unsigned> DeformationTypeConverter(DeformationTypeData, sizeof(DeformationTypeData)/sizeof(*DeformationTypeData)); + +LPEPerspectiveEnvelope::LPEPerspectiveEnvelope(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + horizontal_mirror(_("Mirror movements in horizontal"), _("Mirror movements in horizontal"), "horizontal_mirror", &wr, this, false), + vertical_mirror(_("Mirror movements in vertical"), _("Mirror movements in vertical"), "vertical_mirror", &wr, this, false), + overflow_perspective(_("Overflow perspective"), _("Overflow perspective"), "overflow_perspective", &wr, this, false), + deform_type(_("Type"), _("Select the type of deformation"), "deform_type", DeformationTypeConverter, &wr, this, DEFORMATION_PERSPECTIVE), + up_left_point(_("Top Left"), _("Top Left - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "up_left_point", &wr, this), + up_right_point(_("Top Right"), _("Top Right - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "up_right_point", &wr, this), + down_left_point(_("Down Left"), _("Down Left - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "down_left_point", &wr, this), + down_right_point(_("Down Right"), _("Down Right - <b>Ctrl+Alt+Click</b>: reset, <b>Ctrl</b>: move along axes"), "down_right_point", &wr, this) +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + registerParameter(&deform_type); + registerParameter(&horizontal_mirror); + registerParameter(&vertical_mirror); + registerParameter(&overflow_perspective); + registerParameter(&up_left_point); + registerParameter(&up_right_point); + registerParameter(&down_left_point); + registerParameter(&down_right_point); + apply_to_clippath_and_mask = true; +} + +LPEPerspectiveEnvelope::~LPEPerspectiveEnvelope() += default; + +void LPEPerspectiveEnvelope::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) { + up_left_point.param_transform_multiply(postmul, false); + up_right_point.param_transform_multiply(postmul, false); + down_left_point.param_transform_multiply(postmul, false); + down_right_point.param_transform_multiply(postmul, false); + } +} + +bool pointInTriangle(Geom::Point const &p, std::vector<Geom::Point> points) +{ + if (points.size() != 3) { + g_warning("Incorrect number of points in pointInTriangle\n"); + return false; + } + Geom::Point p1 = points[0]; + Geom::Point p2 = points[1]; + Geom::Point p3 = points[2]; + // http://totologic.blogspot.com.es/2014/01/accurate-point-in-triangle-test.html + using Geom::X; + using Geom::Y; + double denominator = (p1[X] * (p2[Y] - p3[Y]) + p1[Y] * (p3[X] - p2[X]) + p2[X] * p3[Y] - p2[Y] * p3[X]); + double t1 = (p[X] * (p3[Y] - p1[Y]) + p[Y] * (p1[X] - p3[X]) - p1[X] * p3[Y] + p1[Y] * p3[X]) / denominator; + double t2 = (p[X] * (p2[Y] - p1[Y]) + p[Y] * (p1[X] - p2[X]) - p1[X] * p2[Y] + p1[Y] * p2[X]) / -denominator; + double s = t1 + t2; + + return 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1 && s <= 1; +} + +void LPEPerspectiveEnvelope::doEffect(SPCurve *curve) +{ + double projmatrix[3][3]; + if(deform_type == DEFORMATION_PERSPECTIVE) { + using Geom::X; + using Geom::Y; + std::vector<Geom::Point> source_handles(4); + source_handles[0] = Geom::Point(boundingbox_X.min(), boundingbox_Y.max()); + source_handles[1] = Geom::Point(boundingbox_X.min(), boundingbox_Y.min()); + source_handles[2] = Geom::Point(boundingbox_X.max(), boundingbox_Y.min()); + source_handles[3] = Geom::Point(boundingbox_X.max(), boundingbox_Y.max()); + double solmatrix[8][8] = {{0}}; + double free_term[8] = {0}; + double gslSolmatrix[64]; + for(unsigned int i = 0; i < 4; ++i) { + solmatrix[i][0] = source_handles[i][X]; + solmatrix[i][1] = source_handles[i][Y]; + solmatrix[i][2] = 1; + solmatrix[i][6] = -handles[i][X] * source_handles[i][X]; + solmatrix[i][7] = -handles[i][X] * source_handles[i][Y]; + solmatrix[i+4][3] = source_handles[i][X]; + solmatrix[i+4][4] = source_handles[i][Y]; + solmatrix[i+4][5] = 1; + solmatrix[i+4][6] = -handles[i][Y] * source_handles[i][X]; + solmatrix[i+4][7] = -handles[i][Y] * source_handles[i][Y]; + free_term[i] = handles[i][X]; + free_term[i+4] = handles[i][Y]; + } + int h = 0; + for(auto & i : solmatrix) { + for(double j : i) { + gslSolmatrix[h] = j; + h++; + } + } + //this is get by this page: + //http://www.gnu.org/software/gsl/manual/html_node/Linear-Algebra-Examples.html#Linear-Algebra-Examples + gsl_matrix_view m = gsl_matrix_view_array (gslSolmatrix, 8, 8); + gsl_vector_view b = gsl_vector_view_array (free_term, 8); + gsl_vector *x = gsl_vector_alloc (8); + int s; + gsl_permutation * p = gsl_permutation_alloc (8); + gsl_linalg_LU_decomp (&m.matrix, p, &s); + gsl_linalg_LU_solve (&m.matrix, p, &b.vector, x); + h = 0; + for(auto & i : projmatrix) { + for(double & j : i) { + if(h==8) { + projmatrix[2][2] = 1.0; + continue; + } + j = gsl_vector_get(x, h); + h++; + } + } + gsl_permutation_free (p); + gsl_vector_free (x); + } + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(curve->get_pathvector()); + curve->reset(); + Geom::CubicBezier const *cubic = nullptr; + Geom::Point point_at1(0, 0); + Geom::Point point_at2(0, 0); + Geom::Point point_at3(0, 0); + for (const auto & path_it : original_pathv) { + //Si está vacÃo... + if (path_it.empty()) + continue; + //Itreadores + auto nCurve = std::make_unique<SPCurve>(); + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + if (are_near(closingline.initialPoint(), closingline.finalPoint())) { + curve_endit = path_it.end_open(); + } + } + if(deform_type == DEFORMATION_PERSPECTIVE) { + nCurve->moveto(projectPoint(curve_it1->initialPoint(), projmatrix)); + } else { + nCurve->moveto(projectPoint(curve_it1->initialPoint())); + } + while (curve_it1 != curve_endit) { + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + if (cubic) { + point_at1 = (*cubic)[1]; + point_at2 = (*cubic)[2]; + } else { + point_at1 = curve_it1->initialPoint(); + point_at2 = curve_it1->finalPoint(); + } + point_at3 = curve_it1->finalPoint(); + if(deform_type == DEFORMATION_PERSPECTIVE) { + point_at1 = projectPoint(point_at1, projmatrix); + point_at2 = projectPoint(point_at2, projmatrix); + point_at3 = projectPoint(point_at3, projmatrix); + } else { + point_at1 = projectPoint(point_at1); + point_at2 = projectPoint(point_at2); + point_at3 = projectPoint(point_at3); + } + if (cubic) { + nCurve->curveto(point_at1, point_at2, point_at3); + } else { + nCurve->lineto(point_at3); + } + ++curve_it1; + } + //y cerramos la curva + if (path_it.closed()) { + nCurve->move_endpoints(point_at3, point_at3); + nCurve->closepath_current(); + } + curve->append(*nCurve); + } +} + +Geom::Point +LPEPerspectiveEnvelope::projectPoint(Geom::Point p) +{ + double width = boundingbox_X.extent(); + double height = boundingbox_Y.extent(); + double delta_x = boundingbox_X.min() - p[X]; + double delta_y = boundingbox_Y.max() - p[Y]; + Geom::Coord x_ratio = (delta_x * -1) / width; + Geom::Coord y_ratio = delta_y / height; + Geom::Line horiz; + Geom::Line vert; + vert.setPoints (pointAtRatio(y_ratio,down_left_point,up_left_point),pointAtRatio(y_ratio,down_right_point,up_right_point)); + horiz.setPoints (pointAtRatio(x_ratio,down_left_point,down_right_point),pointAtRatio(x_ratio,up_left_point,up_right_point)); + + OptCrossing crossPoint = intersection(horiz,vert); + if(crossPoint) { + return horiz.pointAt(Geom::Coord(crossPoint->ta)); + } else { + return p; + } +} + +Geom::Point +LPEPerspectiveEnvelope::projectPoint(Geom::Point p, double m[][3]) +{ + Geom::Coord x = p[0]; + Geom::Coord y = p[1]; + return Geom::Point( + Geom::Coord((x*m[0][0] + y*m[0][1] + m[0][2])/(x*m[2][0]+y*m[2][1]+m[2][2])), + Geom::Coord((x*m[1][0] + y*m[1][1] + m[1][2])/(x*m[2][0]+y*m[2][1]+m[2][2]))); +} + +Geom::Point +LPEPerspectiveEnvelope::pointAtRatio(Geom::Coord ratio,Geom::Point A, Geom::Point B) +{ + Geom::Coord x = A[X] + (ratio * (B[X]-A[X])); + Geom::Coord y = A[Y]+ (ratio * (B[Y]-A[Y])); + return Point(x, y); +} + + +Gtk::Widget * +LPEPerspectiveEnvelope::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + std::vector<Parameter *>::iterator it = param_vector.begin(); + Gtk::Box * hbox_up_handles = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box * hbox_down_handles = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + if (param->param_key == "up_left_point" || + param->param_key == "up_right_point" || + param->param_key == "down_left_point" || + param->param_key == "down_right_point") { + Gtk::Box * point_hbox = dynamic_cast<Gtk::Box *>(widg); + std::vector< Gtk::Widget* > child_list = point_hbox->get_children(); + Gtk::Box * point_hboxHBox = dynamic_cast<Gtk::Box *>(child_list[0]); + std::vector< Gtk::Widget* > child_list2 = point_hboxHBox->get_children(); + point_hboxHBox->remove(child_list2[0][0]); + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + if(param->param_key == "up_left_point") { + Gtk::Label* handles = Gtk::manage(new Gtk::Label(Glib::ustring(_("Handles:")),Gtk::ALIGN_START)); + vbox->pack_start(*handles, false, false, 2); + hbox_up_handles->pack_start(*widg, true, true, 2); + hbox_up_handles->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)), Gtk::PACK_EXPAND_WIDGET); + } else if(param->param_key == "up_right_point") { + hbox_up_handles->pack_start(*widg, true, true, 2); + } else if(param->param_key == "down_left_point") { + hbox_down_handles->pack_start(*widg, true, true, 2); + hbox_down_handles->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_VERTICAL)), Gtk::PACK_EXPAND_WIDGET); + } else { + hbox_down_handles->pack_start(*widg, true, true, 2); + } + if (tip) { + widg->set_tooltip_markup(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + } + + ++it; + } + vbox->pack_start(*hbox_up_handles,true, true, 2); + Gtk::Box * hbox_middle = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,2)); + hbox_middle->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), Gtk::PACK_EXPAND_WIDGET); + hbox_middle->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), Gtk::PACK_EXPAND_WIDGET); + vbox->pack_start(*hbox_middle, false, true, 2); + vbox->pack_start(*hbox_down_handles, true, true, 2); + Gtk::Box * hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Button* reset_button = Gtk::manage(new Gtk::Button(_("_Clear"), true)); + reset_button->set_image_from_icon_name("edit-clear"); + reset_button->signal_clicked().connect(sigc::mem_fun (*this,&LPEPerspectiveEnvelope::resetGrid)); + reset_button->set_size_request(140,30); + vbox->pack_start(*hbox, true,true,2); + hbox->pack_start(*reset_button, false, false,2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void +LPEPerspectiveEnvelope::vertical(PointParam ¶m_one, PointParam ¶m_two, Geom::Line vert) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double Y = (A[Geom::Y] + B[Geom::Y])/2; + A[Geom::Y] = Y; + B[Geom::Y] = Y; + Geom::Point nearest = vert.pointAt(vert.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::X] > B[Geom::X]) { + distance_middle *= -1; + } + A[Geom::X] = nearest[Geom::X] - distance_middle; + B[Geom::X] = nearest[Geom::X] + distance_middle; + param_one.param_setValue(A); + param_two.param_setValue(B); +} + +void +LPEPerspectiveEnvelope::horizontal(PointParam ¶m_one, PointParam ¶m_two, Geom::Line horiz) +{ + Geom::Point A = param_one; + Geom::Point B = param_two; + double X = (A[Geom::X] + B[Geom::X])/2; + A[Geom::X] = X; + B[Geom::X] = X; + Geom::Point nearest = horiz.pointAt(horiz.nearestTime(A)); + double distance_one = Geom::distance(A,nearest); + double distance_two = Geom::distance(B,nearest); + double distance_middle = (distance_one + distance_two)/2; + if(A[Geom::Y] > B[Geom::Y]) { + distance_middle *= -1; + } + A[Geom::Y] = nearest[Geom::Y] - distance_middle; + B[Geom::Y] = nearest[Geom::Y] + distance_middle; + param_one.param_setValue(A); + param_two.param_setValue(B); +} + +void +LPEPerspectiveEnvelope::doBeforeEffect (SPLPEItem const* lpeitem) +{ + original_bbox(lpeitem, false, true); + if (Geom::are_near(boundingbox_X.min(),boundingbox_X.max()) || + Geom::are_near(boundingbox_Y.min(),boundingbox_Y.max())) + { + g_warning("Couldn`t apply perspective/envelope to a element with geometric width or height equal 0 we add a temporary bounding box to allow handle"); + if (Geom::are_near(boundingbox_X.min(), boundingbox_X.max())) { + boundingbox_X = Geom::Interval(boundingbox_X.min() - 3, boundingbox_X.max() + 3); + } + if (Geom::are_near(boundingbox_Y.min(), boundingbox_Y.max())) { + boundingbox_Y = Geom::Interval(boundingbox_Y.min() - 3, boundingbox_Y.max() + 3); + } + } + Geom::Line vert(Geom::Point(boundingbox_X.middle(),boundingbox_Y.max()), Geom::Point(boundingbox_X.middle(), boundingbox_Y.min())); + Geom::Line horiz(Geom::Point(boundingbox_X.min(),boundingbox_Y.middle()), Geom::Point(boundingbox_X.max(), boundingbox_Y.middle())); + if(vertical_mirror) { + vertical(up_left_point, up_right_point,vert); + vertical(down_left_point, down_right_point,vert); + } + if(horizontal_mirror) { + horizontal(up_left_point, down_left_point,horiz); + horizontal(up_right_point, down_right_point,horiz); + } + setDefaults(); + if (are_near(up_left_point, up_right_point) && are_near(up_right_point, down_left_point) && + are_near(down_left_point, down_right_point)) { + g_warning( + "Perspective/Envelope LPE::doBeforeEffect - lpeobj with invalid parameter, the same value in 4 handles!"); + resetGrid(); + return; + } + if (deform_type == DEFORMATION_PERSPECTIVE) { + if (!overflow_perspective && handles.size() == 4) { + bool move0 = false; + if (handles[0] != down_left_point) { + move0 = true; + } + bool move1 = false; + if (handles[1] != up_left_point) { + move1 = true; + } + bool move2 = false; + if (handles[2] != up_right_point) { + move2 = true; + } + bool move3 = false; + if (handles[3] != down_right_point) { + move3 = true; + } + handles.resize(4); + handles[0] = down_left_point; + handles[1] = up_left_point; + handles[2] = up_right_point; + handles[3] = down_right_point; + Geom::Line line_a(handles[3], handles[1]); + Geom::Line line_b(handles[1], handles[2]); + Geom::Line line_c(handles[2], handles[3]); + int position_a = Geom::sgn(Geom::cross(handles[3] - handles[1], handles[0] - handles[1])); + int position_b = Geom::sgn(Geom::cross(handles[1] - handles[2], handles[0] - handles[2])); + int position_c = Geom::sgn(Geom::cross(handles[2] - handles[3], handles[0] - handles[3])); + if (position_a != 1 && move0) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[0])); + down_left_point.param_setValue(point_a, true); + } + if (position_b == 1 && move0) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[0])); + down_left_point.param_setValue(point_b, true); + } + if (position_c == 1 && move0) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[0])); + down_left_point.param_setValue(point_c, true); + } + line_a.setPoints(handles[0], handles[2]); + line_b.setPoints(handles[2], handles[3]); + line_c.setPoints(handles[3], handles[0]); + position_a = Geom::sgn(Geom::cross(handles[0] - handles[2], handles[1] - handles[2])); + position_b = Geom::sgn(Geom::cross(handles[2] - handles[3], handles[1] - handles[3])); + position_c = Geom::sgn(Geom::cross(handles[3] - handles[0], handles[1] - handles[0])); + if (position_a != 1 && move1) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[1])); + up_left_point.param_setValue(point_a, true); + } + if (position_b == 1 && move1) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[1])); + up_left_point.param_setValue(point_b, true); + } + if (position_c == 1 && move1) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[1])); + up_left_point.param_setValue(point_c, true); + } + line_a.setPoints(handles[1], handles[3]); + line_b.setPoints(handles[3], handles[0]); + line_c.setPoints(handles[0], handles[1]); + position_a = Geom::sgn(Geom::cross(handles[1] - handles[3], handles[2] - handles[3])); + position_b = Geom::sgn(Geom::cross(handles[3] - handles[0], handles[2] - handles[0])); + position_c = Geom::sgn(Geom::cross(handles[0] - handles[1], handles[2] - handles[1])); + if (position_a != 1 && move2) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[2])); + up_right_point.param_setValue(point_a, true); + } + if (position_b == 1 && move2) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[2])); + up_right_point.param_setValue(point_b, true); + } + if (position_c == 1 && move2) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[2])); + up_right_point.param_setValue(point_c, true); + } + line_a.setPoints(handles[2], handles[0]); + line_b.setPoints(handles[0], handles[1]); + line_c.setPoints(handles[1], handles[2]); + position_a = Geom::sgn(Geom::cross(handles[2] - handles[0], handles[3] - handles[0])); + position_b = Geom::sgn(Geom::cross(handles[0] - handles[1], handles[3] - handles[1])); + position_c = Geom::sgn(Geom::cross(handles[1] - handles[2], handles[3] - handles[2])); + if (position_a != 1 && move3) { + Geom::Point point_a = line_a.pointAt(line_a.nearestTime(handles[3])); + down_right_point.param_setValue(point_a, true); + } + if (position_b == 1 && move3) { + Geom::Point point_b = line_b.pointAt(line_b.nearestTime(handles[3])); + down_right_point.param_setValue(point_b, true); + } + if (position_c == 1 && move3) { + Geom::Point point_c = line_c.pointAt(line_c.nearestTime(handles[3])); + down_right_point.param_setValue(point_c, true); + } + } else { + handles.resize(4); + handles[0] = down_left_point; + handles[1] = up_left_point; + handles[2] = up_right_point; + handles[3] = down_right_point; + } + } +} + +void +LPEPerspectiveEnvelope::setDefaults() +{ + if (Geom::are_near(boundingbox_X.min(),boundingbox_X.max()) || + Geom::are_near(boundingbox_Y.min(),boundingbox_Y.max())) + { + if (Geom::are_near(boundingbox_X.min(), boundingbox_X.max())) { + boundingbox_X = Geom::Interval(boundingbox_X.min() - 3, boundingbox_X.max() + 3); + } + if (Geom::are_near(boundingbox_Y.min(), boundingbox_Y.max())) { + boundingbox_Y = Geom::Interval(boundingbox_Y.min() - 3, boundingbox_Y.max() + 3); + } + } + Geom::Point up_left(boundingbox_X.min(), boundingbox_Y.min()); + Geom::Point up_right(boundingbox_X.max(), boundingbox_Y.min()); + Geom::Point down_left(boundingbox_X.min(), boundingbox_Y.max()); + Geom::Point down_right(boundingbox_X.max(), boundingbox_Y.max()); + + up_left_point.param_update_default(up_left); + up_right_point.param_update_default(up_right); + down_right_point.param_update_default(down_right); + down_left_point.param_update_default(down_left); +} + +void +LPEPerspectiveEnvelope::resetGrid() +{ + up_left_point.param_set_default(); + up_right_point.param_set_default(); + down_right_point.param_set_default(); + down_left_point.param_set_default(); +} + +void +LPEPerspectiveEnvelope::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); + setDefaults(); + resetGrid(); +} + +void +LPEPerspectiveEnvelope::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.clear(); + + auto c = std::make_unique<SPCurve>(); + c->moveto(up_left_point); + c->lineto(up_right_point); + c->lineto(down_right_point); + c->lineto(down_left_point); + c->lineto(up_left_point); + hp_vec.push_back(c->get_pathvector()); +} + + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + + + + +/* + 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 : diff --git a/src/live_effects/lpe-perspective-envelope.h b/src/live_effects/lpe-perspective-envelope.h new file mode 100644 index 0000000..7aa32ec --- /dev/null +++ b/src/live_effects/lpe-perspective-envelope.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PERSPECTIVE_ENVELOPE_H +#define INKSCAPE_LPE_PERSPECTIVE_ENVELOPE_H + +/** \file + * LPE <perspective-envelope> implementation , see lpe-perspective-envelope.cpp. + + */ +/* + * Authors: + * Jabiertxof Code migration from python extensions envelope and perspective + * Aaron Spike, aaron@ekips.org from envelope and perspective python code + * Dmitry Platonov, shadowjack@mail.ru, 2006 perspective approach & math + * Jose Hevia (freon) Transform algorithm from envelope + * + * Copyright (C) 2007-2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/point.h" +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPerspectiveEnvelope : public Effect, GroupBBoxEffect { +public: + + LPEPerspectiveEnvelope(LivePathEffectObject *lpeobject); + + ~LPEPerspectiveEnvelope() override; + + void doEffect(SPCurve *curve) override; + + virtual Geom::Point projectPoint(Geom::Point p); + + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + virtual Geom::Point projectPoint(Geom::Point p, double m[][3]); + + virtual Geom::Point pointAtRatio(Geom::Coord ratio,Geom::Point A, Geom::Point B); + + void resetDefaults(SPItem const* item) override; + + virtual void vertical(PointParam ¶mA,PointParam ¶mB, Geom::Line vert); + + virtual void horizontal(PointParam ¶mA,PointParam ¶mB,Geom::Line horiz); + + void doBeforeEffect(SPLPEItem const* lpeitem) override; + + Gtk::Widget * newWidget() override; + + virtual void setDefaults(); + + virtual void resetGrid(); + +protected: + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) override; +private: + + BoolParam horizontal_mirror; + BoolParam vertical_mirror; + BoolParam overflow_perspective; + EnumParam<unsigned> deform_type; + PointParam up_left_point; + PointParam up_right_point; + PointParam down_left_point; + PointParam down_right_point; + std::vector<Geom::Point> handles; + LPEPerspectiveEnvelope(const LPEPerspectiveEnvelope&) = delete; + LPEPerspectiveEnvelope& operator=(const LPEPerspectiveEnvelope&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-powerclip.cpp b/src/live_effects/lpe-powerclip.cpp new file mode 100644 index 0000000..6b804bb --- /dev/null +++ b/src/live_effects/lpe-powerclip.cpp @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-powerclip.h" +#include "display/curve.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-item-group.h" +#include "object/sp-item.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-use.h" +#include "style.h" +#include "svg/svg.h" + +#include <2geom/intersection-graph.h> +#include <2geom/path-intersection.h> +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEPowerClip::LPEPowerClip(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , hide_clip(_("Hide clip"), _("Hide clip"), "hide_clip", &wr, this, false) + , inverse(_("Inverse clip"), _("Inverse clip"), "inverse", &wr, this, true) + , flatten(_("Flatten clip"), _("Flatten clip, see fill rule once convert to paths"), "flatten", &wr, this, false) + , message( + _("Info Box"), _("Important messages"), "message", &wr, this, + _("Use fill-rule evenodd on <b>fill and stroke</b> dialog if no flatten result after convert clip to paths.")) +{ + registerParameter(&inverse); + registerParameter(&flatten); + registerParameter(&hide_clip); + registerParameter(&message); + message.param_set_min_height(55); + _updating = false; + _legacy = false; + // legazy fix between 0.92.4 launch and 1.0beta1 + if (this->getRepr()->attribute("is_inverse")) { + this->getRepr()->removeAttribute("is_inverse"); + _legacy = true; + } +} + +LPEPowerClip::~LPEPowerClip() = default; + +Geom::Path sp_bbox_without_clip(SPLPEItem *lpeitem) +{ + Geom::OptRect bbox = lpeitem->visualBounds(Geom::identity(), true, false, true); + if (bbox) { + (*bbox).expandBy(5); + return Geom::Path(*bbox); + } + return Geom::Path(); +} + +Geom::PathVector sp_get_recursive_pathvector(SPLPEItem *item, Geom::PathVector res, bool dir, bool inverse) +{ + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + std::vector<SPItem *> item_list = sp_item_group_item_list(group); + for (auto child : item_list) { + if (child) { + SPLPEItem *childitem = dynamic_cast<SPLPEItem *>(child); + if (childitem) { + res = sp_get_recursive_pathvector(childitem, res, dir, inverse); + } + } + } + } + SPShape *shape = dynamic_cast<SPShape *>(item); + if (shape && shape->curve()) { + for (auto path : shape->curve()->get_pathvector()) { + if (!path.empty()) { + bool pathdir = Geom::path_direction(path); + if (pathdir == dir && inverse) { + path = path.reversed(); + } + res.push_back(path); + } + } + } + return res; +} + +Geom::PathVector LPEPowerClip::getClipPathvector() +{ + Geom::PathVector res; + Geom::PathVector res_hlp; + if (!sp_lpe_item) { + return res; + } + + SPObject *clip_path = sp_lpe_item->getClipObject(); + if (clip_path) { + std::vector<SPObject*> clip_path_list = clip_path->childList(true); + clip_path_list.pop_back(); + if (clip_path_list.size()) { + for (auto clip : clip_path_list) { + SPLPEItem *childitem = dynamic_cast<SPLPEItem *>(clip); + if (childitem) { + res_hlp = sp_get_recursive_pathvector(childitem, res_hlp, false, inverse); + if (is_load && _legacy) { + childitem->doWriteTransform(Geom::Translate(0, -999999)); + } + if (!childitem->style || !childitem->style->display.set || + childitem->style->display.value != SP_CSS_DISPLAY_NONE) { + childitem->style->display.set = TRUE; + childitem->style->display.value = SP_CSS_DISPLAY_NONE; + childitem->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + } + } + if (is_load && _legacy) { + res_hlp *= Geom::Translate(0, -999999); + _legacy = false; + } + } + } + Geom::Path bbox = sp_bbox_without_clip(sp_lpe_item); + if (hide_clip) { + return bbox; + } + if (inverse && isVisible()) { + res.push_back(bbox); + } + for (auto path : res_hlp) { + res.push_back(path); + } + return res; +} + +void LPEPowerClip::add() +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item) { + return; + } + SPObject *clip_path = sp_lpe_item->getClipObject(); + SPObject *elemref = NULL; + if (clip_path) { + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *parent = clip_path->getRepr(); + SPLPEItem *childitem = dynamic_cast<SPLPEItem *>(clip_path->childList(true).back()); + if (childitem) { + if (const gchar *powerclip = childitem->getRepr()->attribute("class")) { + if (!strcmp(powerclip, "powerclip")) { + Glib::ustring newclip = Glib::ustring("clipath_") + getId(); + Glib::ustring uri = Glib::ustring("url(#") + newclip + Glib::ustring(")"); + parent = clip_path->getRepr()->duplicate(xml_doc); + parent->setAttribute("id", newclip); + clip_path = document->getDefs()->appendChildRepr(parent); + Inkscape::GC::release(parent); + sp_lpe_item->setAttribute("clip-path", uri); + SPLPEItem *childitemdel = dynamic_cast<SPLPEItem *>(clip_path->childList(true).back()); + if (childitemdel) { + childitemdel->setAttribute("id", getId()); + return; + } + } + } + } + Inkscape::XML::Node *clip_path_node = xml_doc->createElement("svg:path"); + parent->appendChild(clip_path_node); + Inkscape::GC::release(clip_path_node); + elemref = document->getObjectByRepr(clip_path_node); + if (elemref) { + if (childitem) { + elemref->setAttribute("style", childitem->getAttribute("style")); + } else { + elemref->setAttribute("style", "fill-rule:evenodd"); + } + elemref->setAttribute("class", "powerclip"); + elemref->setAttribute("id", getId()); + elemref->setAttribute("d", sp_svg_write_path(getClipPathvector())); + } else { + sp_lpe_item->removeCurrentPathEffect(false); + } + } else { + sp_lpe_item->removeCurrentPathEffect(false); + } +} + +Glib::ustring LPEPowerClip::getId() { return Glib::ustring("lpe_") + Glib::ustring(getLPEObj()->getId()); } + +void LPEPowerClip::upd() +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item) { + return; + } + SPObject *elemref = document->getObjectById(getId().c_str()); + if (elemref && sp_lpe_item) { + elemref->setAttribute("d", sp_svg_write_path(getClipPathvector())); + elemref->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } else { + add(); + } +} + + +void LPEPowerClip::doBeforeEffect(SPLPEItem const *lpeitem) +{ + if (!_updating) { + upd(); + } +} + +void +LPEPowerClip::doOnRemove (SPLPEItem const* /*lpeitem*/) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if (keep_paths || document->onungroup) { + SPObject *clip_path = sp_lpe_item->getClipObject(); + if (clip_path) { + SPLPEItem *childitem = dynamic_cast<SPLPEItem *>(clip_path->childList(true).front()); + childitem->deleteObject(); + } + return; + } + _updating = true; + SPObject *elemref = document->getObjectById(getId().c_str()); + if (elemref) { + elemref->deleteObject(); + } + SPObject *clip_path = sp_lpe_item->getClipObject(); + if (clip_path) { + std::vector<SPObject *> clip_path_list = clip_path->childList(true); + for (auto clip : clip_path_list) { + SPLPEItem *childitem = dynamic_cast<SPLPEItem *>(clip); + if (childitem) { + if (!childitem->style || childitem->style->display.set || + childitem->style->display.value == SP_CSS_DISPLAY_NONE) { + childitem->style->display.set = TRUE; + childitem->style->display.value = SP_CSS_DISPLAY_BLOCK; + childitem->updateRepr(SP_OBJECT_WRITE_NO_CHILDREN | SP_OBJECT_WRITE_EXT); + } + } + } + } +} + +Geom::PathVector +LPEPowerClip::doEffect_path(Geom::PathVector const & path_in){ + Geom::PathVector path_out = path_in; + if (flatten) { + Geom::PathVector c_pv = getClipPathvector(); + std::unique_ptr<Geom::PathIntersectionGraph> pig(new Geom::PathIntersectionGraph(c_pv, path_out)); + if (pig && !c_pv.empty() && !path_out.empty()) { + path_out = pig->getIntersection(); + } + } + return path_out; +} + +void LPEPowerClip::doOnVisibilityToggled(SPLPEItem const *lpeitem) { upd(); } + +void sp_remove_powerclip(Inkscape::Selection *sel) +{ + if (!sel->isEmpty()) { + auto selList = sel->items(); + for (auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(*i); + if (lpeitem) { + if (lpeitem->hasPathEffect() && lpeitem->pathEffectsEnabled()) { + PathEffectList path_effect_list(*lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because + * the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return; + } + if (LPETypeConverter.get_key(lpeobj->effecttype) == "powerclip") { + lpeitem->setCurrentPathEffect(lperef); + lpeitem->removeCurrentPathEffect(false); + break; + } + } + } + } + } + } +} + +void sp_inverse_powerclip(Inkscape::Selection *sel) { + if (!sel->isEmpty()) { + auto selList = sel->items(); + for(auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem* lpeitem = dynamic_cast<SPLPEItem*>(*i); + if (lpeitem) { + SPClipPath *clip_path = lpeitem->getClipObject(); + if(clip_path) { + std::vector<SPObject*> clip_path_list = clip_path->childList(true); + for (auto iter : clip_path_list) { + SPUse *use = dynamic_cast<SPUse*>(iter); + if (use) { + g_warning("We can`t add inverse clip on clones"); + return; + } + } + Effect::createAndApply(POWERCLIP, SP_ACTIVE_DOCUMENT, lpeitem); + Effect* lpe = lpeitem->getCurrentLPE(); + if (lpe) { + lpe->getRepr()->setAttribute("inverse", "true"); + } + } + } + } + } +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-powerclip.h b/src/live_effects/lpe-powerclip.h new file mode 100644 index 0000000..fab59ed --- /dev/null +++ b/src/live_effects/lpe-powerclip.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_POWERCLIP_H +#define INKSCAPE_LPE_POWERCLIP_H + +/* + * Inkscape::LPEPowerClip + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/message.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPowerClip : public Effect { +public: + LPEPowerClip(LivePathEffectObject *lpeobject); + ~LPEPowerClip() override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* lpeitem) override; + Glib::ustring getId(); + void add(); + void upd(); + void del(); + Geom::PathVector getClipPathvector(); + + private: + BoolParam inverse; + BoolParam flatten; + BoolParam hide_clip; + MessageParam message; + bool _updating; + bool _legacy; +}; + +void sp_remove_powerclip(Inkscape::Selection *sel); +void sp_inverse_powerclip(Inkscape::Selection *sel); + +} //namespace LivePathEffect +} //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-powermask.cpp b/src/live_effects/lpe-powermask.cpp new file mode 100644 index 0000000..9b1fb7b --- /dev/null +++ b/src/live_effects/lpe-powermask.cpp @@ -0,0 +1,378 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/lpe-powermask.h" +#include "live_effects/lpeobject.h" +#include "live_effects/lpeobject-reference.h" +#include <2geom/path-intersection.h> +#include <2geom/intersection-graph.h> +#include "display/curve.h" +#include "helper/geom.h" +#include "svg/svg.h" +#include "svg/svg-color.h" +#include "svg/stringstream.h" +#include "path-chemistry.h" +#include "extract-uri.h" +#include <bad-uri-exception.h> + +#include "object/sp-mask.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-defs.h" +#include "object/sp-item-group.h" +#include "object/uri.h" + + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEPowerMask::LPEPowerMask(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + uri("Store the uri of mask", "", "uri", &wr, this, "false", false), + invert(_("Invert mask"), _("Invert mask"), "invert", &wr, this, false), + //wrap(_("Wrap mask data"), _("Wrap mask data allowing previous filters"), "wrap", &wr, this, false), + hide_mask(_("Hide mask"), _("Hide mask"), "hide_mask", &wr, this, false), + background(_("Add background to mask"), _("Add background to mask"), "background", &wr, this, false), + background_color(_("Background color and opacity"), _("Set color and opacity of the background"), "background_color", &wr, this, 0xffffffff) +{ + registerParameter(&uri); + registerParameter(&invert); + registerParameter(&hide_mask); + registerParameter(&background); + registerParameter(&background_color); + previous_color = background_color.get_value(); +} + +LPEPowerMask::~LPEPowerMask() = default; + +Glib::ustring LPEPowerMask::getId() { return Glib::ustring("mask-powermask-") + Glib::ustring(getLPEObj()->getId()); } + +void +LPEPowerMask::doOnApply (SPLPEItem const * lpeitem) +{ + SPLPEItem *item = const_cast<SPLPEItem*>(lpeitem); + SPObject * mask = item->getMaskObject(); + bool hasit = false; + if (lpeitem->hasPathEffect() && lpeitem->pathEffectsEnabled()) { + PathEffectList path_effect_list(*lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return; + } + if (LPETypeConverter.get_key(lpeobj->effecttype) == "powermask") { + hasit = true; + break; + } + } + } + if (!mask || hasit) { + item->removeCurrentPathEffect(false); + } else { + Glib::ustring newmask = getId(); + Glib::ustring uri = Glib::ustring("url(#") + newmask + Glib::ustring(")"); + mask->setAttribute("id", newmask); + item->setAttribute("mask", uri); + } +} + +void LPEPowerMask::tryForkMask() +{ + SPDocument *document = getSPDoc(); + if (!document || !sp_lpe_item) { + return; + } + SPObject *mask = sp_lpe_item->getMaskObject(); + SPObject *elemref = document->getObjectById(getId().c_str()); + if (!elemref && sp_lpe_item && mask) { + Glib::ustring newmask = getId(); + Glib::ustring uri = Glib::ustring("url(#") + newmask + Glib::ustring(")"); + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *fork = mask->getRepr()->duplicate(xml_doc); + mask = document->getDefs()->appendChildRepr(fork); + fork->setAttribute("id", newmask); + Inkscape::GC::release(fork); + sp_lpe_item->setAttribute("mask", uri); + } +} + +void +LPEPowerMask::doBeforeEffect (SPLPEItem const* lpeitem){ + //To avoid close of color dialog and better performance on change color + tryForkMask(); + SPObject * mask = sp_lpe_item->getMaskObject(); + auto uri_str = uri.param_getSVGValue(); + if (hide_mask && mask) { + sp_lpe_item->getMaskRef().detach(); + } else if (!hide_mask && !mask && !uri_str.empty()) { + sp_lpe_item->getMaskRef().try_attach(uri_str.c_str()); + } + mask = sp_lpe_item->getMaskObject(); + if (mask) { + if (previous_color != background_color.get_value()) { + previous_color = background_color.get_value(); + setMask(); + } else { + uri.param_setValue(Glib::ustring(extract_uri(sp_lpe_item->getRepr()->attribute("mask"))), true); + sp_lpe_item->getMaskRef().detach(); + Geom::OptRect bbox = lpeitem->visualBounds(); + if(!bbox) { + return; + } + uri_str = uri.param_getSVGValue(); + sp_lpe_item->getMaskRef().try_attach(uri_str.c_str()); + + Geom::Rect bboxrect = (*bbox); + bboxrect.expandBy(1); + mask_box.clear(); + mask_box = Geom::Path(bboxrect); + SPDocument *document = getSPDoc(); + if (!document || !mask) { + return; + } + DocumentUndo::ScopedInsensitive tmp(document); + setMask(); + } + } else if(!hide_mask) { + SPLPEItem * item = const_cast<SPLPEItem*>(lpeitem); + item->removeCurrentPathEffect(false); + } +} + +void +LPEPowerMask::setMask(){ + SPMask *mask = sp_lpe_item->getMaskObject(); + SPObject *elemref = nullptr; + SPDocument *document = getSPDoc(); + if (!document || !mask) { + return; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *box = nullptr; + Inkscape::XML::Node *filter = nullptr; + SPDefs * defs = document->getDefs(); + Glib::ustring mask_id = getId(); + Glib::ustring box_id = mask_id + (Glib::ustring)"_box"; + Glib::ustring filter_id = mask_id + (Glib::ustring)"_inverse"; + Glib::ustring filter_label = (Glib::ustring)"filter" + mask_id; + Glib::ustring filter_uri = (Glib::ustring)"url(#" + filter_id + (Glib::ustring)")"; + if (!(elemref = document->getObjectById(filter_id))) { + filter = xml_doc->createElement("svg:filter"); + filter->setAttribute("id", filter_id); + filter->setAttribute("inkscape:label", filter_label); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_set_property(css, "color-interpolation-filters", "sRGB"); + sp_repr_css_change(filter, css, "style"); + sp_repr_css_attr_unref(css); + filter->setAttribute("height", "100"); + filter->setAttribute("width", "100"); + filter->setAttribute("x", "-50"); + filter->setAttribute("y", "-50"); + Inkscape::XML::Node *primitive1 = xml_doc->createElement("svg:feColorMatrix"); + Glib::ustring primitive1_id = (mask_id + (Glib::ustring)"_primitive1").c_str(); + primitive1->setAttribute("id", primitive1_id); + primitive1->setAttribute("values", "1"); + primitive1->setAttribute("type", "saturate"); + primitive1->setAttribute("result", "fbSourceGraphic"); + Inkscape::XML::Node *primitive2 = xml_doc->createElement("svg:feColorMatrix"); + Glib::ustring primitive2_id = (mask_id + (Glib::ustring)"_primitive2").c_str(); + primitive2->setAttribute("id", primitive2_id); + primitive2->setAttribute("values", "-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0 "); + primitive2->setAttribute("in", "fbSourceGraphic"); + elemref = defs->appendChildRepr(filter); + Inkscape::GC::release(filter); + filter->appendChild(primitive1); + Inkscape::GC::release(primitive1); + filter->appendChild(primitive2); + Inkscape::GC::release(primitive2); + } + Glib::ustring g_data_id = mask_id + (Glib::ustring)"_container"; + if((elemref = document->getObjectById(g_data_id))){ + std::vector<SPItem*> item_list = sp_item_group_item_list(SP_GROUP(elemref)); + for (auto iter : item_list) { + Inkscape::XML::Node *mask_node = iter->getRepr(); + elemref->getRepr()->removeChild(mask_node); + mask->getRepr()->appendChild(mask_node); + Inkscape::GC::release(mask_node); + } + elemref->deleteObject(true); + } + std::vector<SPObject*> mask_list = mask->childList(true); + for (auto iter : mask_list) { + SPItem * mask_data = SP_ITEM(iter); + Inkscape::XML::Node *mask_node = mask_data->getRepr(); + if (! strcmp(mask_data->getId(), box_id.c_str())){ + continue; + } + Glib::ustring mask_data_id = (Glib::ustring)mask_data->getId(); + SPCSSAttr *css = sp_repr_css_attr_new(); + if(mask_node->attribute("style")) { + sp_repr_css_attr_add_from_string(css, mask_node->attribute("style")); + } + char const* filter = sp_repr_css_property (css, "filter", nullptr); + if(!filter || !strcmp(filter, filter_uri.c_str())) { + if (invert && is_visible) { + sp_repr_css_set_property (css, "filter", filter_uri.c_str()); + } else { + sp_repr_css_set_property (css, "filter", nullptr); + } + Glib::ustring css_str; + sp_repr_css_write_string(css, css_str); + mask_node->setAttribute("style", css_str); + } + } + if ((elemref = document->getObjectById(box_id))) { + elemref->deleteObject(true); + } + if (background && is_visible) { + bool exist = true; + if (!(elemref = document->getObjectById(box_id))) { + box = xml_doc->createElement("svg:path"); + box->setAttribute("id", box_id); + exist = false; + } + Glib::ustring style; + gchar c[32]; + unsigned const rgb24 = background_color.get_value() >> 8; + sprintf(c, "#%06x", rgb24); + style = Glib::ustring("fill:") + Glib::ustring(c); + Inkscape::SVGOStringStream os; + os << SP_RGBA32_A_F(background_color.get_value()); + style = style + Glib::ustring(";fill-opacity:") + Glib::ustring(os.str()); + SPCSSAttr *css = sp_repr_css_attr_new(); + sp_repr_css_attr_add_from_string(css, style.c_str()); + char const* filter = sp_repr_css_property (css, "filter", nullptr); + if(!filter || !strcmp(filter, filter_uri.c_str())) { + if (invert && is_visible) { + sp_repr_css_set_property (css, "filter", filter_uri.c_str()); + } else { + sp_repr_css_set_property (css, "filter", nullptr); + } + + } + Glib::ustring css_str; + sp_repr_css_write_string(css, css_str); + box->setAttributeOrRemoveIfEmpty("style", css_str); + box->setAttribute("d", sp_svg_write_path(mask_box)); + if (!exist) { + elemref = mask->appendChildRepr(box); + Inkscape::GC::release(box); + } + box->setPosition(0); + } else if ((elemref = document->getObjectById(box_id))) { + elemref->deleteObject(true); + } + mask->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +} + +void +LPEPowerMask::doOnVisibilityToggled(SPLPEItem const* lpeitem) +{ + doBeforeEffect(lpeitem); +} + +void +LPEPowerMask::doEffect (SPCurve * curve) +{ +} + +void +LPEPowerMask::doOnRemove (SPLPEItem const* lpeitem) +{ + SPMask *mask = lpeitem->getMaskObject(); + if (mask) { + if (keep_paths || lpeitem->document->onungroup) { + return; + } + invert.param_setValue(false); + //wrap.param_setValue(false); + background.param_setValue(false); + setMask(); + SPObject *elemref = nullptr; + SPDocument *document = getSPDoc(); + Glib::ustring mask_id = getId(); + Glib::ustring filter_id = mask_id + (Glib::ustring)"_inverse"; + if ((elemref = document->getObjectById(filter_id))) { + elemref->deleteObject(true); + } + } +} + +void sp_inverse_powermask(Inkscape::Selection *sel) { + if (!sel->isEmpty()) { + SPDocument *document = SP_ACTIVE_DOCUMENT; + if (!document) { + return; + } + auto selList = sel->items(); + for(auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem* lpeitem = dynamic_cast<SPLPEItem*>(*i); + if (lpeitem) { + SPMask *mask = lpeitem->getMaskObject(); + if (mask) { + Effect::createAndApply(POWERMASK, SP_ACTIVE_DOCUMENT, lpeitem); + Effect* lpe = lpeitem->getCurrentLPE(); + if (lpe) { + lpe->getRepr()->setAttribute("invert", "false"); + lpe->getRepr()->setAttribute("is_visible", "true"); + lpe->getRepr()->setAttribute("hide_mask", "false"); + lpe->getRepr()->setAttribute("background", "true"); + lpe->getRepr()->setAttribute("background_color", "#ffffffff"); + } + } + } + } + } +} + +void sp_remove_powermask(Inkscape::Selection *sel) { + if (!sel->isEmpty()) { + auto selList = sel->items(); + for (auto i = boost::rbegin(selList); i != boost::rend(selList); ++i) { + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(*i); + if (lpeitem) { + if (lpeitem->hasPathEffect() && lpeitem->pathEffectsEnabled()) { + PathEffectList path_effect_list(*lpeitem->path_effect_list); + for (auto &lperef : path_effect_list) { + LivePathEffectObject *lpeobj = lperef->lpeobject; + if (!lpeobj) { + /** \todo Investigate the cause of this. + * For example, this happens when copy pasting an object with LPE applied. Probably because + * the object is pasted while the effect is not yet pasted to defs, and cannot be found. + */ + g_warning("SPLPEItem::performPathEffect - NULL lpeobj in list!"); + return; + } + if (LPETypeConverter.get_key(lpeobj->effecttype) == "powermask") { + lpeitem->setCurrentPathEffect(lperef); + lpeitem->removeCurrentPathEffect(false); + break; + } + } + } + } + } + } +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-powermask.h b/src/live_effects/lpe-powermask.h new file mode 100644 index 0000000..16b74d5 --- /dev/null +++ b/src/live_effects/lpe-powermask.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_POWERMASK_H +#define INKSCAPE_LPE_POWERMASK_H + +/* + * Inkscape::LPEPowerMask + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/colorpicker.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEPowerMask : public Effect { +public: + LPEPowerMask(LivePathEffectObject *lpeobject); + ~LPEPowerMask() override; + void doOnApply (SPLPEItem const * lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doEffect (SPCurve * curve) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* lpeitem) override; + void toggleMaskVisibility(); + void tryForkMask(); + Glib::ustring getId(); + void setMask(); +private: + HiddenParam uri; + BoolParam invert; + //BoolParam wrap; + BoolParam hide_mask; + BoolParam background; + ColorPickerParam background_color; + Geom::Path mask_box; + guint32 previous_color; +}; + +void sp_remove_powermask(Inkscape::Selection *sel); +void sp_inverse_powermask(Inkscape::Selection *sel); + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-powerstroke-interpolators.h b/src/live_effects/lpe-powerstroke-interpolators.h new file mode 100644 index 0000000..940d97e --- /dev/null +++ b/src/live_effects/lpe-powerstroke-interpolators.h @@ -0,0 +1,326 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Interpolators for lists of points. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2010-2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_POWERSTROKE_INTERPOLATORS_H +#define INKSCAPE_LPE_POWERSTROKE_INTERPOLATORS_H + +#include <2geom/path.h> +#include <2geom/bezier-utils.h> +#include <2geom/sbasis-to-bezier.h> + +#include "live_effects/spiro.h" + + +/// @TODO Move this to 2geom? +namespace Geom { +namespace Interpolate { + +enum InterpolatorType { + INTERP_LINEAR, + INTERP_CUBICBEZIER, + INTERP_CUBICBEZIER_JOHAN, + INTERP_SPIRO, + INTERP_CUBICBEZIER_SMOOTH, + INTERP_CENTRIPETAL_CATMULLROM +}; + +class Interpolator { +public: + Interpolator() = default;; + virtual ~Interpolator() = default;; + + static Interpolator* create(InterpolatorType type); + + virtual Geom::Path interpolateToPath(std::vector<Point> const &points) const = 0; + +private: + Interpolator(const Interpolator&) = delete; + Interpolator& operator=(const Interpolator&) = delete; +}; + +class Linear : public Interpolator { +public: + Linear() = default;; + ~Linear() override = default;; + + Path interpolateToPath(std::vector<Point> const &points) const override { + Path path; + path.start( points.at(0) ); + for (unsigned int i = 1 ; i < points.size(); ++i) { + path.appendNew<Geom::LineSegment>(points.at(i)); + } + return path; + }; + +private: + Linear(const Linear&) = delete; + Linear& operator=(const Linear&) = delete; +}; + +// this class is terrible +class CubicBezierFit : public Interpolator { +public: + CubicBezierFit() = default;; + ~CubicBezierFit() override = default;; + + Path interpolateToPath(std::vector<Point> const &points) const override { + unsigned int n_points = points.size(); + // worst case gives us 2 segment per point + int max_segs = 8*n_points; + Geom::Point * b = g_new(Geom::Point, max_segs); + Geom::Point * points_array = g_new(Geom::Point, 4*n_points); + for (unsigned i = 0; i < n_points; ++i) { + points_array[i] = points.at(i); + } + + double tolerance_sq = 0; // this value is just a random guess + + int const n_segs = Geom::bezier_fit_cubic_r(b, points_array, n_points, + tolerance_sq, max_segs); + + Geom::Path fit; + if ( n_segs > 0) + { + fit.start(b[0]); + for (int c = 0; c < n_segs; c++) { + fit.appendNew<Geom::CubicBezier>(b[4*c+1], b[4*c+2], b[4*c+3]); + } + } + g_free(b); + g_free(points_array); + return fit; + }; + +private: + CubicBezierFit(const CubicBezierFit&) = delete; + CubicBezierFit& operator=(const CubicBezierFit&) = delete; +}; + +/// @todo invent name for this class +class CubicBezierJohan : public Interpolator { +public: + CubicBezierJohan(double beta = 0.2) { + _beta = beta; + }; + ~CubicBezierJohan() override = default;; + + Path interpolateToPath(std::vector<Point> const &points) const override { + Path fit; + fit.start(points.at(0)); + for (unsigned int i = 1; i < points.size(); ++i) { + Point p0 = points.at(i-1); + Point p1 = points.at(i); + Point dx = Point(p1[X] - p0[X], 0); + fit.appendNew<CubicBezier>(p0+_beta*dx, p1-_beta*dx, p1); + } + return fit; + }; + + void setBeta(double beta) { + _beta = beta; + } + + double _beta; + +private: + CubicBezierJohan(const CubicBezierJohan&) = delete; + CubicBezierJohan& operator=(const CubicBezierJohan&) = delete; +}; + +/// @todo invent name for this class +class CubicBezierSmooth : public Interpolator { +public: + CubicBezierSmooth(double beta = 0.2) { + _beta = beta; + }; + ~CubicBezierSmooth() override = default;; + + Path interpolateToPath(std::vector<Point> const &points) const override { + Path fit; + fit.start(points.at(0)); + unsigned int num_points = points.size(); + for (unsigned int i = 1; i < num_points; ++i) { + Point p0 = points.at(i-1); + Point p1 = points.at(i); + Point dx = Point(p1[X] - p0[X], 0); + if (i == 1) { + fit.appendNew<CubicBezier>(p0, p1-0.75*dx, p1); + } else if (i == points.size() - 1) { + fit.appendNew<CubicBezier>(p0+0.75*dx, p1, p1); + } else { + fit.appendNew<CubicBezier>(p0+_beta*dx, p1-_beta*dx, p1); + } + } + return fit; + }; + + void setBeta(double beta) { + _beta = beta; + } + + double _beta; + +private: + CubicBezierSmooth(const CubicBezierSmooth&) = delete; + CubicBezierSmooth& operator=(const CubicBezierSmooth&) = delete; +}; + +class SpiroInterpolator : public Interpolator { +public: + SpiroInterpolator() = default;; + ~SpiroInterpolator() override = default;; + + Path interpolateToPath(std::vector<Point> const &points) const override { + Path fit; + + Coord scale_y = 100.; + + guint len = points.size(); + Spiro::spiro_cp *controlpoints = g_new (Spiro::spiro_cp, len); + for (unsigned int i = 0; i < len; ++i) { + controlpoints[i].x = points[i][X]; + controlpoints[i].y = points[i][Y] / scale_y; + controlpoints[i].ty = 'c'; + } + controlpoints[0].ty = '{'; + controlpoints[1].ty = 'v'; + controlpoints[len-2].ty = 'v'; + controlpoints[len-1].ty = '}'; + + Spiro::spiro_run(controlpoints, len, fit); + + fit *= Scale(1,scale_y); + g_free(controlpoints); + return fit; + }; + +private: + SpiroInterpolator(const SpiroInterpolator&) = delete; + SpiroInterpolator& operator=(const SpiroInterpolator&) = delete; +}; + +// Quick mockup for testing the behavior for powerstroke controlpoint interpolation +class CentripetalCatmullRomInterpolator : public Interpolator { +public: + CentripetalCatmullRomInterpolator() = default;; + ~CentripetalCatmullRomInterpolator() override = default;; + + Path interpolateToPath(std::vector<Point> const &points) const override { + unsigned int n_points = points.size(); + + Geom::Path fit(points.front()); + + if (n_points < 3) return fit; // TODO special cases for 0,1 and 2 input points + + // return n_points-1 cubic segments + + // duplicate first point + fit.append(calc_bezier(points[0],points[0],points[1],points[2])); + + for (std::size_t i = 0; i < n_points-2; ++i) { + Point p0 = points[i]; + Point p1 = points[i+1]; + Point p2 = points[i+2]; + Point p3 = (i < n_points-3) ? points[i+3] : points[i+2]; + + fit.append(calc_bezier(p0, p1, p2, p3)); + } + + return fit; + }; + +private: + CubicBezier calc_bezier(Point p0, Point p1, Point p2, Point p3) const { + // create interpolating bezier between p1 and p2 + + // Part of the code comes from StackOverflow user eriatarka84 + // http://stackoverflow.com/a/23980479/2929337 + + // calculate time coords (deltas) of points + // the factor 0.25 can be generalized for other Catmull-Rom interpolation types + // see alpha in Yuksel et al. "On the Parameterization of Catmull-Rom Curves", + // --> http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf + double dt0 = powf(distanceSq(p0, p1), 0.25); + double dt1 = powf(distanceSq(p1, p2), 0.25); + double dt2 = powf(distanceSq(p2, p3), 0.25); + + + // safety check for repeated points + double eps = Geom::EPSILON; + if (dt1 < eps) + dt1 = 1.0; + if (dt0 < eps) + dt0 = dt1; + if (dt2 < eps) + dt2 = dt1; + + // compute tangents when parameterized in [t1,t2] + Point tan1 = (p1 - p0) / dt0 - (p2 - p0) / (dt0 + dt1) + (p2 - p1) / dt1; + Point tan2 = (p2 - p1) / dt1 - (p3 - p1) / (dt1 + dt2) + (p3 - p2) / dt2; + // rescale tangents for parametrization in [0,1] + tan1 *= dt1; + tan2 *= dt1; + + // create bezier from tangents (this is already in 2geom somewhere, or should be moved to it) + // the tangent of a bezier curve is: B'(t) = 3(1-t)^2 (b1 - b0) + 6(1-t)t(b2-b1) + 3t^2(b3-b2) + // So we have to make sure that B'(0) = tan1 and B'(1) = tan2, and we already know that b0=p1 and b3=p2 + // tan1 = B'(0) = 3 (b1 - p1) --> p1 + (tan1)/3 = b1 + // tan2 = B'(1) = 3 (p2 - b2) --> p2 - (tan2)/3 = b2 + + Point b0 = p1; + Point b1 = p1 + tan1 / 3; + Point b2 = p2 - tan2 / 3; + Point b3 = p2; + + return CubicBezier(b0, b1, b2, b3); + } + + CentripetalCatmullRomInterpolator(const CentripetalCatmullRomInterpolator&) = delete; + CentripetalCatmullRomInterpolator& operator=(const CentripetalCatmullRomInterpolator&) = delete; +}; + + +inline Interpolator* +Interpolator::create(InterpolatorType type) { + switch (type) { + case INTERP_LINEAR: + return new Geom::Interpolate::Linear(); + case INTERP_CUBICBEZIER: + return new Geom::Interpolate::CubicBezierFit(); + case INTERP_CUBICBEZIER_JOHAN: + return new Geom::Interpolate::CubicBezierJohan(); + case INTERP_SPIRO: + return new Geom::Interpolate::SpiroInterpolator(); + case INTERP_CUBICBEZIER_SMOOTH: + return new Geom::Interpolate::CubicBezierSmooth(); + case INTERP_CENTRIPETAL_CATMULLROM: + return new Geom::Interpolate::CentripetalCatmullRomInterpolator(); + default: + return new Geom::Interpolate::Linear(); + } +} + +} //namespace Interpolate +} //namespace Geom + +#endif + +/* + 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/live_effects/lpe-powerstroke.cpp b/src/live_effects/lpe-powerstroke.cpp new file mode 100644 index 0000000..63c5bae --- /dev/null +++ b/src/live_effects/lpe-powerstroke.cpp @@ -0,0 +1,820 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * PowerStroke LPE implementation. Creates curves with modifiable stroke width. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2010-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/elliptical-arc.h> +#include <2geom/path-sink.h> +#include <2geom/path-intersection.h> +#include <2geom/circle.h> + +#include "live_effects/lpe-powerstroke.h" +#include "live_effects/lpe-powerstroke-interpolators.h" +#include "live_effects/lpe-simplify.h" +#include "live_effects/lpeobject.h" +#include "live_effects/fill-conversion.h" + +#include "desktop-style.h" +#include "style.h" + +#include "display/curve.h" +#include "display/control/canvas-item-enums.h" +#include "helper/geom.h" +#include "object/sp-shape.h" +#include "svg/css-ostringstream.h" +#include "svg/svg-color.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Geom { +// should all be moved to 2geom at some point + +/** Find the point where two straight lines cross. +*/ +static std::optional<Point> intersection_point( Point const & origin_a, Point const & vector_a, + Point const & origin_b, Point const & vector_b) +{ + Coord denom = cross(vector_a, vector_b); + if (!are_near(denom,0.)){ + Coord t = (cross(vector_b, origin_a) + cross(origin_b, vector_b)) / denom; + return origin_a + t * vector_a; + } + return std::nullopt; +} + +static Geom::CubicBezier sbasis_to_cubicbezier(Geom::D2<Geom::SBasis> const & sbasis_in) +{ + std::vector<Geom::Point> temp; + sbasis_to_bezier(temp, sbasis_in, 4); + return Geom::CubicBezier( temp ); +} + +/** + * document this! + * very quick: this finds the ellipse with minimum eccentricity + passing through point P and Q, with tangent PO at P and QO at Q + http://mathforum.org/kb/message.jspa?messageID=7471596&tstart=0 + */ +static Ellipse find_ellipse(Point P, Point Q, Point O) +{ + Point p = P - O; + Point q = Q - O; + Coord K = 4 * dot(p,q) / (L2sq(p) + L2sq(q)); + + double cross = p[Y]*q[X] - p[X]*q[Y]; + double a = -q[Y]/cross; + double b = q[X]/cross; + double c = (O[X]*q[Y] - O[Y]*q[X])/cross; + + double d = p[Y]/cross; + double e = -p[X]/cross; + double f = (-O[X]*p[Y] + O[Y]*p[X])/cross; + + // Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0 + double A = (a*d*K+d*d+a*a); + double B = (a*e*K+b*d*K+2*d*e+2*a*b); + double C = (b*e*K+e*e+b*b); + double D = (a*f*K+c*d*K+2*d*f-2*d+2*a*c-2*a); + double E = (b*f*K+c*e*K+2*e*f-2*e+2*b*c-2*b); + double F = c*f*K+f*f-2*f+c*c-2*c+1; + + return Ellipse(A, B, C, D, E, F); +} + +/** + * Find circle that touches inside of the curve, with radius matching the curvature, at time value \c t. + * Because this method internally uses unitTangentAt, t should be smaller than 1.0 (see unitTangentAt). + */ +static Circle touching_circle( D2<SBasis> const &curve, double t, double tol=0.01 ) +{ + //Piecewise<SBasis> k = curvature(curve, tol); + D2<SBasis> dM=derivative(curve); + if ( are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) ) { + dM=derivative(dM); + } + if ( are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) ) { // try second time + dM=derivative(dM); + } + if ( dM.isZero(tol) || (are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) )) { // admit defeat + return Geom::Circle(Geom::Point(0., 0.), 0.); + } + Piecewise<D2<SBasis> > unitv = unitVector(dM,tol); + if (unitv.empty()) { // admit defeat + return Geom::Circle(Geom::Point(0., 0.), 0.); + } + Piecewise<SBasis> dMlength = dot(Piecewise<D2<SBasis> >(dM),unitv); + Piecewise<SBasis> k = cross(derivative(unitv),unitv); + k = divide(k,dMlength,tol,3); + double curv = k(t); // note that this value is signed + + Geom::Point normal = unitTangentAt(curve, t).cw(); + double radius = 1/curv; + Geom::Point center = curve(t) + radius*normal; + return Geom::Circle(center, fabs(radius)); +} + +} // namespace Geom + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<unsigned> InterpolatorTypeData[] = { + {Geom::Interpolate::INTERP_CUBICBEZIER_SMOOTH, N_("CubicBezierSmooth"), "CubicBezierSmooth"}, + {Geom::Interpolate::INTERP_LINEAR , N_("Linear"), "Linear"}, + {Geom::Interpolate::INTERP_CUBICBEZIER , N_("CubicBezierFit"), "CubicBezierFit"}, + {Geom::Interpolate::INTERP_CUBICBEZIER_JOHAN , N_("CubicBezierJohan"), "CubicBezierJohan"}, + {Geom::Interpolate::INTERP_SPIRO , N_("SpiroInterpolator"), "SpiroInterpolator"}, + {Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM, N_("Centripetal Catmull-Rom"), "CentripetalCatmullRom"} +}; +static const Util::EnumDataConverter<unsigned> InterpolatorTypeConverter(InterpolatorTypeData, sizeof(InterpolatorTypeData)/sizeof(*InterpolatorTypeData)); + +enum LineJoinType { + LINEJOIN_BEVEL, + LINEJOIN_ROUND, + LINEJOIN_EXTRP_MITER, + LINEJOIN_MITER, + LINEJOIN_SPIRO, + LINEJOIN_EXTRP_MITER_ARC +}; +static const Util::EnumData<unsigned> LineJoinTypeData[] = { + {LINEJOIN_BEVEL, N_("Beveled"), "bevel"}, + {LINEJOIN_ROUND, N_("Rounded"), "round"}, +// {LINEJOIN_EXTRP_MITER, N_("Extrapolated"), "extrapolated"}, // disabled because doesn't work well + {LINEJOIN_EXTRP_MITER_ARC, N_("Extrapolated arc"), "extrp_arc"}, + {LINEJOIN_MITER, N_("Miter"), "miter"}, + {LINEJOIN_SPIRO, N_("Spiro"), "spiro"}, +}; +static const Util::EnumDataConverter<unsigned> LineJoinTypeConverter(LineJoinTypeData, sizeof(LineJoinTypeData)/sizeof(*LineJoinTypeData)); + +LPEPowerStroke::LPEPowerStroke(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + offset_points(_("Offset points"), _("Offset points"), "offset_points", &wr, this), + not_jump(_("No jumping handles"), _("Allow to move handles along the path without them automatically attaching to the nearest path segment"), "not_jump", &wr, this, false), + sort_points(_("Sort points"), _("Sort offset points according to their time value along the curve"), "sort_points", &wr, this, true), + interpolator_type(_("Interpolator type:"), _("Determines which kind of interpolator will be used to interpolate between stroke width along the path"), "interpolator_type", InterpolatorTypeConverter, &wr, this, Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM), + interpolator_beta(_("Smoothness:"), _("Sets the smoothness for the CubicBezierJohan interpolator; 0 = linear interpolation, 1 = smooth"), "interpolator_beta", &wr, this, 0.2), + scale_width(_("Width factor:"), _("Scale the stroke's width uniformly along the whole path"), "scale_width", &wr, this, 1.0), + start_linecap_type(_("Start cap:"), _("Determines the shape of the path's start"), "start_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ZERO_WIDTH), + linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", LineJoinTypeConverter, &wr, this, LINEJOIN_ROUND), + miter_limit(_("Miter limit:"), _("Maximum length of the miter (in units of stroke width)"), "miter_limit", &wr, this, 4.), + end_linecap_type(_("End cap:"), _("Determines the shape of the path's end"), "end_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ZERO_WIDTH) +{ + show_orig_path = true; + + /// @todo offset_points are initialized with empty path, is that bug-save? + + interpolator_beta.addSlider(true); + interpolator_beta.param_set_range(0.,1.); + + registerParameter(&offset_points); + registerParameter(¬_jump); + registerParameter(&sort_points); + registerParameter(&interpolator_type); + registerParameter(&interpolator_beta); + registerParameter(&start_linecap_type); + registerParameter(&linejoin_type); + registerParameter(&miter_limit); + registerParameter(&scale_width); + registerParameter(&end_linecap_type); + scale_width.param_set_range(0.0, std::numeric_limits<double>::max()); + scale_width.param_set_increments(0.1, 0.1); + scale_width.param_set_digits(4); + recusion_limit = 0; + has_recursion = false; +} + +LPEPowerStroke::~LPEPowerStroke() = default; + +void +LPEPowerStroke::doBeforeEffect(SPLPEItem const *lpeItem) +{ + offset_points.set_scale_width(scale_width); + if (has_recursion) { + has_recursion = false; + adjustForNewPath(pathvector_before_effect); + } +} + +void LPEPowerStroke::applyStyle(SPLPEItem *lpeitem) +{ + lpe_shape_convert_stroke_and_fill(SP_SHAPE(lpeitem)); +} + +void +LPEPowerStroke::doOnApply(SPLPEItem const* lpeitem) +{ + if (auto shape = dynamic_cast<SPShape const *>(lpeitem)) { + SPLPEItem* item = const_cast<SPLPEItem*>(lpeitem); + std::vector<Geom::Point> points; + Geom::PathVector const &pathv = pathv_to_linear_and_cubic_beziers(shape->curve()->get_pathvector()); + double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed / 2 : 1.; + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + Glib::ustring pref_path_pp = "/live_effects/powerstroke/powerpencil"; + bool powerpencil = prefs->getBool(pref_path_pp, false); + bool clipboard = offset_points.data().size() > 0; + if (!powerpencil) { + applyStyle(item); + } + if (!clipboard && !powerpencil) { + item->updateRepr(); + if (pathv.empty()) { + points.emplace_back(0.2,width ); + points.emplace_back(0.5, width); + points.emplace_back(0.8, width); + } else { + Geom::Path const &path = pathv.front(); + Geom::Path::size_type const size = path.size_default(); + if (!path.closed()) { + points.emplace_back(0.2, width); + } + points.emplace_back(0.5 * size, width); + if (!path.closed()) { + points.emplace_back(size - 0.2, width); + } + } + offset_points.param_set_and_write_new_value(points); + } + offset_points.set_scale_width(scale_width); + } else { + if (!SP_IS_SHAPE(lpeitem)) { + g_warning("LPE Powerstroke can only be applied to shapes (not groups)."); + } + } +} + +void LPEPowerStroke::doOnRemove(SPLPEItem const* lpeitem) +{ + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + auto shape = dynamic_cast<SPShape *>(lpeitem_mutable); + + if (shape && !keep_paths) { + lpe_shape_revert_stroke_and_fill(shape, offset_points.median_width() * 2); + } +} + +void +LPEPowerStroke::adjustForNewPath(Geom::PathVector const & path_in) +{ + if (!path_in.empty()) { + offset_points.recalculate_controlpoints_for_new_pwd2(path_in[0].toPwSb()); + } +} + +static bool compare_offsets (Geom::Point first, Geom::Point second) +{ + return first[Geom::X] < second[Geom::X]; +} + +static Geom::Path path_from_piecewise_fix_cusps( Geom::Piecewise<Geom::D2<Geom::SBasis> > const & B, + Geom::Piecewise<Geom::SBasis> const & y, // width path + LineJoinType jointype, + double miter_limit, + double tol=Geom::EPSILON) +{ +/* per definition, each discontinuity should be fixed with a join-ending, as defined by linejoin_type +*/ + Geom::PathBuilder pb; + Geom::OptRect bbox = bounds_fast(B); + if (B.empty() || !bbox) { + return pb.peek().front(); + } + + pb.setStitching(true); + + Geom::Point start = B[0].at0(); + pb.moveTo(start); + build_from_sbasis(pb, B[0], tol, false); + unsigned prev_i = 0; + for (unsigned i=1; i < B.size(); i++) { + // Skip degenerate segments. The number below was determined, after examining + // very many paths with powerstrokes of all shapes and sizes, to allow filtering out most + // degenerate segments without losing significant quality; it is close to 1/256. + if (B[i].isConstant(4e-3)) { + continue; + } + if (!are_near(B[prev_i].at1(), B[i].at0(), tol) ) + { // discontinuity found, so fix it :-) + double width = y( B.cuts[i] ); + + Geom::Point tang1 = -unitTangentAt(reverse(B[prev_i]),0.); // = unitTangentAt(B[prev_i],1); + Geom::Point tang2 = unitTangentAt(B[i],0); + Geom::Point discontinuity_vec = B[i].at0() - B[prev_i].at1(); + bool on_outside = ( dot(tang1, discontinuity_vec) >= 0. ); + + if (on_outside) { + // we are on the outside: add some type of join! + switch (jointype) { + case LINEJOIN_ROUND: { + /* for constant width paths, the rounding is a circular arc (rx == ry), + for non-constant width paths, the rounding can be done with an ellipse but is hard and ambiguous. + The elliptical arc should go through the discontinuity's start and end points (of course!) + and also should match the discontinuity tangents at those start and end points. + To resolve the ambiguity, the elliptical arc with minimal eccentricity should be chosen. + A 2Geom method was created to do exactly this :) + */ + + std::optional<Geom::Point> O = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (!O) { + // no center found, i.e. 180 degrees round + pb.lineTo(B[i].at0()); // default to bevel for too shallow cusp angles + break; + } + + Geom::Ellipse ellipse; + try { + ellipse = find_ellipse(B[prev_i].at1(), B[i].at0(), *O); + } + catch (Geom::LogicalError &e) { + // 2geom did not find a fitting ellipse, this happens for weird thick paths :) + // do bevel, and break + pb.lineTo(B[i].at0()); + break; + } + + // check if ellipse.ray is within 'sane' range. + if ( ( fabs(ellipse.ray(Geom::X)) > 1e6 ) || + ( fabs(ellipse.ray(Geom::Y)) > 1e6 ) ) + { + // do bevel, and break + pb.lineTo(B[i].at0()); + break; + } + + pb.arcTo( ellipse.ray(Geom::X), ellipse.ray(Geom::Y), ellipse.rotationAngle(), + false, width < 0, B[i].at0() ); + + break; + } + case LINEJOIN_EXTRP_MITER: { + Geom::D2<Geom::SBasis> newcurve1 = B[prev_i] * Geom::reflection(rot90(tang1), B[prev_i].at1()); + Geom::CubicBezier bzr1 = sbasis_to_cubicbezier( reverse(newcurve1) ); + + Geom::D2<Geom::SBasis> newcurve2 = B[i] * Geom::reflection(rot90(tang2), B[i].at0()); + Geom::CubicBezier bzr2 = sbasis_to_cubicbezier( reverse(newcurve2) ); + + Geom::Crossings cross = crossings(bzr1, bzr2); + if (cross.empty()) { + // empty crossing: default to bevel + pb.lineTo(B[i].at0()); + } else { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(bzr1.pointAt(cross[0].ta), point_on_path); + if (len > fabs(width) * miter_limit) { + // miter too big: default to bevel + pb.lineTo(B[i].at0()); + } else { + std::pair<Geom::CubicBezier, Geom::CubicBezier> sub1 = bzr1.subdivide(cross[0].ta); + std::pair<Geom::CubicBezier, Geom::CubicBezier> sub2 = bzr2.subdivide(cross[0].tb); + pb.curveTo(sub1.first[1], sub1.first[2], sub1.first[3]); + pb.curveTo(sub2.second[1], sub2.second[2], sub2.second[3]); + } + } + break; + } + case LINEJOIN_EXTRP_MITER_ARC: { + // Extrapolate using the curvature at the end of the path segments to join + Geom::Circle circle1 = Geom::touching_circle(reverse(B[prev_i]), 0.0); + Geom::Circle circle2 = Geom::touching_circle(B[i], 0.0); + std::vector<Geom::ShapeIntersection> solutions; + solutions = circle1.intersect(circle2); + if (solutions.size() == 2) { + Geom::Point sol(0.,0.); + bool solok = true; + bool point0bad = false; + bool point1bad = false; + if ( dot(tang2, solutions[0].point() - B[i].at0()) > 0) + { + // points[0] is bad, choose points[1] + point0bad = true; + } + if ( dot(tang2, solutions[1].point() - B[i].at0()) > 0) + { + // points[1] is bad, choose points[0] + point1bad = true; + } + if (!point0bad && !point1bad ) { + // both points are good, choose nearest + sol = ( distanceSq(B[i].at0(), solutions[0].point()) < distanceSq(B[i].at0(), solutions[1].point()) ) ? + solutions[0].point() : solutions[1].point(); + } else if (!point0bad) { + sol = solutions[0].point(); + } else if (!point1bad) { + sol = solutions[1].point(); + } else { + solok = false; + } + (*bbox).expandBy (bbox->width()/4); + + if (!(*bbox).contains(sol)) { + solok = false; + } + Geom::EllipticalArc *arc0 = nullptr; + Geom::EllipticalArc *arc1 = nullptr; + bool build = false; + if (solok) { + arc0 = circle1.arc(B[prev_i].at1(), 0.5*(B[prev_i].at1()+sol), sol); + arc1 = circle2.arc(sol, 0.5*(sol+B[i].at0()), B[i].at0()); + if (arc0) { + // FIX: Some assertions errors here + build_from_sbasis(pb,arc0->toSBasis(), tol, false); + build = true; + } else if (arc1) { + std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + build = true; + } + } + } + if (build) { + build_from_sbasis(pb,arc1->toSBasis(), tol, false); + } else if (arc0) { + pb.lineTo(B[i].at0()); + } + } + if (!solok || !(arc0 && build)) { + // fall back to miter + std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + } + } + pb.lineTo(B[i].at0()); + } + if (arc0) { + delete arc0; + arc0 = nullptr; + } + if (arc1) { + delete arc1; + arc1 = nullptr; + } + } else { + // fall back to miter + std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + } + } + pb.lineTo(B[i].at0()); + } + /*else if (solutions == 1) { // one circle is inside the other + // don't know what to do: default to bevel + pb.lineTo(B[i].at0()); + } else { // no intersections + // don't know what to do: default to bevel + pb.lineTo(B[i].at0()); + } */ + + break; + } + case LINEJOIN_MITER: { + std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1, + B[i].at0(), tang2 ); + if (p) { + // check size of miter + Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width; + Geom::Coord len = distance(*p, point_on_path); + if (len <= fabs(width) * miter_limit) { + // miter OK + pb.lineTo(*p); + } + } + pb.lineTo(B[i].at0()); + break; + } + case LINEJOIN_SPIRO: { + Geom::Point direction = B[i].at0() - B[prev_i].at1(); + double tang1_sign = dot(direction,tang1); + double tang2_sign = dot(direction,tang2); + + Spiro::spiro_cp *controlpoints = g_new (Spiro::spiro_cp, 4); + controlpoints[0].x = (B[prev_i].at1() - tang1_sign*tang1)[Geom::X]; + controlpoints[0].y = (B[prev_i].at1() - tang1_sign*tang1)[Geom::Y]; + controlpoints[0].ty = '{'; + controlpoints[1].x = B[prev_i].at1()[Geom::X]; + controlpoints[1].y = B[prev_i].at1()[Geom::Y]; + controlpoints[1].ty = ']'; + controlpoints[2].x = B[i].at0()[Geom::X]; + controlpoints[2].y = B[i].at0()[Geom::Y]; + controlpoints[2].ty = '['; + controlpoints[3].x = (B[i].at0() + tang2_sign*tang2)[Geom::X]; + controlpoints[3].y = (B[i].at0() + tang2_sign*tang2)[Geom::Y]; + controlpoints[3].ty = '}'; + + Geom::Path spiro; + Spiro::spiro_run(controlpoints, 4, spiro); + pb.append(spiro.portion(1, spiro.size_open() - 1)); + break; + } + case LINEJOIN_BEVEL: + default: + pb.lineTo(B[i].at0()); + break; + } + + build_from_sbasis(pb, B[i], tol, false); + + } else { + // we are on inside of corner! + Geom::Path bzr1 = path_from_sbasis( B[prev_i], tol ); + Geom::Path bzr2 = path_from_sbasis( B[i], tol ); + Geom::Crossings cross = crossings(bzr1, bzr2); + if (cross.size() != 1) { + // empty crossing or too many crossings: default to bevel + pb.lineTo(B[i].at0()); + pb.append(bzr2); + } else { + // :-) quick hack: + for (unsigned i=0; i < bzr1.size_open(); ++i) { + pb.backspace(); + } + + pb.append( bzr1.portion(0, cross[0].ta) ); + pb.append( bzr2.portion(cross[0].tb, bzr2.size_open()) ); + } + } + } else { + build_from_sbasis(pb, B[i], tol, false); + } + + prev_i = i; + } + pb.flush(); + return pb.peek().front(); +} + +Geom::PathVector +LPEPowerStroke::doEffect_path (Geom::PathVector const & path_in) +{ + using namespace Geom; + + Geom::PathVector path_out; + if (path_in.empty()) { + return path_in; + } + Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(path_in); + Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_in = pathv[0].toPwSb(); + if (pwd2_in.empty()) { + return path_in; + } + Piecewise<D2<SBasis> > der = derivative(pwd2_in); + if (der.empty()) { + return path_in; + } + Piecewise<D2<SBasis> > n = unitVector(der,0.00001); + if (n.empty()) { + return path_in; + } + + n = rot90(n); + offset_points.set_pwd2(pwd2_in, n); + + LineCapType end_linecap = static_cast<LineCapType>(end_linecap_type.get_value()); + LineCapType start_linecap = static_cast<LineCapType>(start_linecap_type.get_value()); + + std::vector<Geom::Point> ts_no_scale = offset_points.data(); + if (ts_no_scale.empty()) { + return path_in; + } + std::vector<Geom::Point> ts; + for (auto & tsp : ts_no_scale) { + Geom::Point p = Geom::Point(tsp[Geom::X], tsp[Geom::Y] * scale_width); + ts.push_back(p); + } + if (sort_points) { + sort(ts.begin(), ts.end(), compare_offsets); + } + // create stroke path where points (x,y) := (t, offset) + Geom::Interpolate::Interpolator *interpolator = Geom::Interpolate::Interpolator::create(static_cast<Geom::Interpolate::InterpolatorType>(interpolator_type.get_value())); + if (Geom::Interpolate::CubicBezierJohan *johan = dynamic_cast<Geom::Interpolate::CubicBezierJohan*>(interpolator)) { + johan->setBeta(interpolator_beta); + } + if (Geom::Interpolate::CubicBezierSmooth *smooth = dynamic_cast<Geom::Interpolate::CubicBezierSmooth*>(interpolator)) { + smooth->setBeta(interpolator_beta); + } + if (pathv[0].closed()) { + std::vector<Geom::Point> ts_close; + //we have only one knot or overwrite before + Geom::Point start = Geom::Point( pwd2_in.domain().min(), ts.front()[Geom::Y]); + Geom::Point end = Geom::Point( pwd2_in.domain().max(), ts.front()[Geom::Y]); + if (ts.size() > 1) { + end = Geom::Point(pwd2_in.domain().max(), 0); + Geom::Point tmpstart(0, 0); + tmpstart[Geom::X] = end[Geom::X] + ts.front()[Geom::X]; + tmpstart[Geom::Y] = ts.front()[Geom::Y]; + ts_close.push_back(ts.back()); + ts_close.push_back(middle_point(tmpstart, ts.back())); + ts_close.push_back(tmpstart); + Geom::Path closepath = interpolator->interpolateToPath(ts_close); + end = closepath.pointAt(Geom::nearest_time(end, closepath)); + end[Geom::X] = pwd2_in.domain().max(); + start = end; + start[Geom::X] = pwd2_in.domain().min(); + } + ts.insert(ts.begin(), start ); + ts.push_back( end ); + ts_close.clear(); + } else { + // add width data for first and last point on the path + // depending on cap type, these first and last points have width zero or take the width from the closest width point. + ts.insert(ts.begin(), Point( pwd2_in.domain().min(), + (start_linecap==LINECAP_ZERO_WIDTH) ? 0. : ts.front()[Geom::Y]) ); + ts.emplace_back( pwd2_in.domain().max(), + (end_linecap==LINECAP_ZERO_WIDTH) ? 0. : ts.back()[Geom::Y] ); + } + + // do the interpolation in a coordinate system that is more alike to the on-canvas knots, + // instead of the heavily compressed coordinate system of (segment_no offset, Y) in which the knots are stored + double pwd2_in_arclength = length(pwd2_in); + double xcoord_scaling = pwd2_in_arclength / ts.back()[Geom::X]; + for (auto & t : ts) { + t[Geom::X] *= xcoord_scaling; + } + + Geom::Path strokepath = interpolator->interpolateToPath(ts); + delete interpolator; + + // apply the inverse knot-xcoord scaling that was applied before the interpolation + strokepath *= Scale(1/xcoord_scaling, 1); + + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(strokepath.toPwSb()); + Piecewise<SBasis> x = Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y = Piecewise<SBasis>(patternd2[1]); + // find time values for which x lies outside path domain + // and only take portion of x and y that lies within those time values + std::vector< double > rtsmin = roots (x - pwd2_in.domain().min()); + std::vector< double > rtsmax = roots (x + pwd2_in.domain().max()); + if ( !rtsmin.empty() && !rtsmax.empty() ) { + x = portion(x, rtsmin.at(0), rtsmax.at(0)); + y = portion(y, rtsmin.at(0), rtsmax.at(0)); + } + + LineJoinType jointype = static_cast<LineJoinType>(linejoin_type.get_value()); + if (x.empty() || y.empty()) { + return path_in; + } + Piecewise<D2<SBasis> > pwd2_out = compose(pwd2_in,x) + y*compose(n,x); + Piecewise<D2<SBasis> > mirrorpath = reverse( compose(pwd2_in,x) - y*compose(n,x)); + + Geom::Path fixed_path = path_from_piecewise_fix_cusps( pwd2_out, y, jointype, miter_limit, LPE_CONVERSION_TOLERANCE); + Geom::Path fixed_mirrorpath = path_from_piecewise_fix_cusps( mirrorpath, reverse(y), jointype, miter_limit, LPE_CONVERSION_TOLERANCE); + if (pathv[0].closed()) { + fixed_path.close(true); + path_out.push_back(fixed_path); + fixed_mirrorpath.close(true); + path_out.push_back(fixed_mirrorpath); + } else { + // add linecaps... + switch (end_linecap) { + case LINECAP_ZERO_WIDTH: + // do nothing + break; + case LINECAP_PEAK: + { + Geom::Point end_deriv = -unitTangentAt( reverse(pwd2_in.segs.back()), 0.); + double radius = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint()); + Geom::Point midpoint = 0.5*(fixed_path.finalPoint() + fixed_mirrorpath.initialPoint()) + radius*end_deriv; + fixed_path.appendNew<LineSegment>(midpoint); + fixed_path.appendNew<LineSegment>(fixed_mirrorpath.initialPoint()); + break; + } + case LINECAP_SQUARE: + { + Geom::Point end_deriv = -unitTangentAt( reverse(pwd2_in.segs.back()), 0.); + double radius = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint()); + fixed_path.appendNew<LineSegment>( fixed_path.finalPoint() + radius*end_deriv ); + fixed_path.appendNew<LineSegment>( fixed_mirrorpath.initialPoint() + radius*end_deriv ); + fixed_path.appendNew<LineSegment>( fixed_mirrorpath.initialPoint() ); + break; + } + case LINECAP_BUTT: + { + fixed_path.appendNew<LineSegment>( fixed_mirrorpath.initialPoint() ); + break; + } + case LINECAP_ROUND: + default: + { + double radius1 = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint()); + fixed_path.appendNew<EllipticalArc>( radius1, radius1, M_PI/2., false, y.lastValue() < 0, fixed_mirrorpath.initialPoint() ); + break; + } + } + + fixed_path.append(fixed_mirrorpath); + switch (start_linecap) { + case LINECAP_ZERO_WIDTH: + // do nothing + break; + case LINECAP_PEAK: + { + Geom::Point start_deriv = unitTangentAt( pwd2_in.segs.front(), 0.); + double radius = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint()); + Geom::Point midpoint = 0.5*(fixed_mirrorpath.finalPoint() + fixed_path.initialPoint()) - radius*start_deriv; + fixed_path.appendNew<LineSegment>( midpoint ); + fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() ); + break; + } + case LINECAP_SQUARE: + { + Geom::Point start_deriv = unitTangentAt( pwd2_in.segs.front(), 0.); + double radius = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint()); + fixed_path.appendNew<LineSegment>( fixed_mirrorpath.finalPoint() - radius*start_deriv ); + fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() - radius*start_deriv ); + fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() ); + break; + } + case LINECAP_BUTT: + { + fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() ); + break; + } + case LINECAP_ROUND: + default: + { + double radius2 = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint()); + fixed_path.appendNew<EllipticalArc>( radius2, radius2, M_PI/2., false, y.firstValue() < 0, fixed_path.initialPoint() ); + break; + } + } + fixed_path.close(true); + path_out.push_back(fixed_path); + } + if (path_out.empty()) { + return path_in; + // doEffect_path (path_in); + } + return path_out; +} + +void LPEPowerStroke::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + offset_points.param_transform_multiply(postmul, false); +} + +void LPEPowerStroke::doAfterEffect(SPLPEItem const *lpeitem, SPCurve *curve) +{ + if (pathvector_before_effect[0].size() == pathvector_after_effect[0].size()) { + if (recusion_limit < 6) { + Inkscape::LivePathEffect::Effect *effect = + sp_lpe_item->getFirstPathEffectOfType(Inkscape::LivePathEffect::SIMPLIFY); + if (effect) { + LivePathEffect::LPESimplify *simplify = + dynamic_cast<LivePathEffect::LPESimplify *>(effect->getLPEObj()->get_lpe()); + double threshold = simplify->threshold * 1.2; + simplify->threshold.param_set_value(threshold); + simplify->threshold.write_to_SVG(); + has_recursion = true; + } + } + ++recusion_limit; + } else { + recusion_limit = 0; + } +} + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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/live_effects/lpe-powerstroke.h b/src/live_effects/lpe-powerstroke.h new file mode 100644 index 0000000..d88352d --- /dev/null +++ b/src/live_effects/lpe-powerstroke.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief PowerStroke LPE effect, see lpe-powerstroke.cpp. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2010-2011 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_POWERSTROKE_H +#define INKSCAPE_LPE_POWERSTROKE_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/hidden.h" +#include "live_effects/parameter/powerstrokepointarray.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum LineCapType { LINECAP_BUTT, LINECAP_SQUARE, LINECAP_ROUND, LINECAP_PEAK, LINECAP_ZERO_WIDTH }; + +static const Util::EnumData<unsigned> LineCapTypeData[] = { { LINECAP_BUTT, N_("Butt"), "butt" }, + { LINECAP_SQUARE, N_("Square"), "square" }, + { LINECAP_ROUND, N_("Round"), "round" }, + { LINECAP_PEAK, N_("Peak"), "peak" }, + { LINECAP_ZERO_WIDTH, N_("Zero width"), "zerowidth" } }; +static const Util::EnumDataConverter<unsigned> LineCapTypeConverter(LineCapTypeData, + sizeof(LineCapTypeData) / sizeof(*LineCapTypeData)); + +class LPEPowerStroke : public Effect { +public: + LPEPowerStroke(LivePathEffectObject *lpeobject); + ~LPEPowerStroke() override; + LPEPowerStroke(const LPEPowerStroke&) = delete; + LPEPowerStroke& operator=(const LPEPowerStroke&) = delete; + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doBeforeEffect(SPLPEItem const *lpeItem) override; + void doOnApply(SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* lpeitem) override; + void doAfterEffect(SPLPEItem const *lpeitem, SPCurve *curve) override; + void transform_multiply(Geom::Affine const &postmul, bool set) override; + void applyStyle(SPLPEItem *lpeitem); + // methods called by path-manipulator upon edits + void adjustForNewPath(Geom::PathVector const & path_in); + + PowerStrokePointArrayParam offset_points; + BoolParam not_jump; +private: + BoolParam sort_points; + EnumParam<unsigned> interpolator_type; + ScalarParam interpolator_beta; + ScalarParam scale_width; + EnumParam<unsigned> start_linecap_type; + EnumParam<unsigned> linejoin_type; + ScalarParam miter_limit; + EnumParam<unsigned> end_linecap_type; + size_t recusion_limit; + bool has_recursion; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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/live_effects/lpe-pts2ellipse.cpp b/src/live_effects/lpe-pts2ellipse.cpp new file mode 100644 index 0000000..8559bf4 --- /dev/null +++ b/src/live_effects/lpe-pts2ellipse.cpp @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Points to Ellipse" implementation + */ + +/* + * Authors: + * Markus Schwienbacher + * + * Copyright (C) Markus Schwienbacher 2013 <mschwienbacher@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-pts2ellipse.h" + + +#include <object/sp-item-group.h> +#include <object/sp-item.h> +#include <object/sp-path.h> +#include <object/sp-shape.h> +#include <svg/svg.h> + +#include <2geom/circle.h> +#include <2geom/ellipse.h> +#include <2geom/elliptical-arc.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> + +#include <glib/gi18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<EllipseMethod> EllipseMethodData[] = { + { EM_AUTO, N_("Auto ellipse"), "auto" }, //!< (2..4 points: circle, from 5 points: ellipse) + { EM_CIRCLE, N_("Force circle"), "circle" }, //!< always fit a circle + { EM_ISOMETRIC_CIRCLE, N_("Isometric circle"), "iso_circle" }, //!< use first two edges to generate a sheared + //!< ellipse + { EM_PERSPECTIVE_CIRCLE, N_("Perspective circle"), "perspective_circle" }, //!< use first three edges to generate an + //!< ellipse representing a distorted + //!< circle in perspective + { EM_STEINER_ELLIPSE, N_("Steiner ellipse"), "steiner_ellipse" }, //!< generate a steiner ellipse from the first + //!< three points + { EM_STEINER_INELLIPSE, N_("Steiner inellipse"), "steiner_inellipse" } //!< generate a steiner inellipse from the + //!< first three points +}; +static const Util::EnumDataConverter<EllipseMethod> EMConverter(EllipseMethodData, EM_END); + +LPEPts2Ellipse::LPEPts2Ellipse(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , method( + _("Method:"), + _("Methods to generate the ellipse\n- Auto ellipse: fits a circle (2, 3 or 4 nodes in the path) or an ellipse (at least 5 " + "nodes)\n- Force circle: (at least 2 nodes) always create a circle\n- Isometric circle: (3 nodes) use " + "first two segments as edges\n- Perspective circle: (4 nodes) circle in a square in perspective view\n- Steiner " + "ellipse: (3 nodes) ellipse on a triangle\n- Steiner inellipse: (3 nodes) ellipse inside a triangle"), + "method", EMConverter, &wr, this, EM_AUTO) + , gen_isometric_frame(_("_Frame (isometric rectangle)"), _("Draw parallelogram around the ellipse"), + "gen_isometric_frame", &wr, this, false) + , gen_perspective_frame( + _("_Perspective square"), + _("Draw square surrounding the circle in perspective view\n(only in method \"Perspective circle\")"), + "gen_perspective_frame", &wr, this, false) + , gen_arc(_("_Arc"), + _("Generate open arc (open ellipse) based on first and last node\n(only for methods \"Auto ellipse\" " + "and \"Force circle\")"), + "gen_arc", &wr, this, false) + , other_arc(_("_Other arc side"), _("Switch sides of the arc"), "arc_other", &wr, this, false) + , slice_arc(_("_Slice arc"), _("Create a circle / ellipse segment"), "slice_arc", &wr, this, false) + , draw_axes(_("A_xes"), _("Draw both semi-major and semi-minor axes"), "draw_axes", &wr, this, false) + , draw_perspective_axes(_("Perspective axes"), + _("Draw the axes in perspective view\n(only in method \"Perspective circle\")"), + "draw_perspective_axes", &wr, this, false) + , rot_axes(_("Axes rotation"), _("Axes rotation angle [deg]"), "rot_axes", &wr, this, 0) + , draw_ori_path(_("Source _path"), _("Show the original source path"), "draw_ori_path", &wr, this, false) +{ + registerParameter(&method); + registerParameter(&gen_arc); + registerParameter(&other_arc); + registerParameter(&slice_arc); + registerParameter(&gen_isometric_frame); + registerParameter(&draw_axes); + registerParameter(&gen_perspective_frame); + registerParameter(&draw_perspective_axes); + registerParameter(&rot_axes); + registerParameter(&draw_ori_path); + + rot_axes.param_set_range(-360, 360); + rot_axes.param_set_increments(1, 10); + + show_orig_path = true; + + gsl_x = gsl_vector_alloc(8); + gsl_p = gsl_permutation_alloc(8); +} + +LPEPts2Ellipse::~LPEPts2Ellipse() +{ + gsl_permutation_free(gsl_p); + gsl_vector_free(gsl_x); +} + +// helper function, transforms a given value into range [0, 2pi] +inline double range2pi(double a) +{ + a = fmod(a, 2 * M_PI); + if (a < 0) { + a += 2 * M_PI; + } + return a; +} + +inline double deg2rad(double a) { return a * M_PI / 180.0; } + +inline double rad2deg(double a) { return a * 180.0 / M_PI; } + +// helper function, calculates the angle between a0 and a1 in ccw sense +// examples: 0..1->1, -1..1->2, pi/4..-pi/4->1.5pi +// full rotations: 0..2pi->2pi, -pi..pi->2pi, pi..-pi->0, 2pi..0->0 +inline double calc_delta_angle(const double a0, const double a1) +{ + double da = range2pi(a1 - a0); + if ((fabs(da) < 1e-9) && (a0 < a1)) { + da = 2 * M_PI; + } + return da; +} + +int LPEPts2Ellipse::unit_arc_path(Geom::Path &path_in, Geom::Affine &affine, double start, double end, bool slice) +{ + double arc_angle = calc_delta_angle(start, end); + if (fabs(arc_angle) < 1e-9) { + g_warning("angle was 0"); + return -1; + } + + // the delta angle + double da = M_PI_2; + // number of segments with da length + int nda = (int)ceil(arc_angle / M_PI_2); + // recalculate da + da = arc_angle / (double)nda; + + bool closed = false; + if (fabs(arc_angle - 2 * M_PI) < 1e-8) { + closed = true; + da = M_PI_2; + nda = 4; + } + + double s = range2pi(start); + end = s + arc_angle; + + double x0 = cos(s); + double y0 = sin(s); + // construct the path + Geom::Path path(Geom::Point(x0, y0)); + path.setStitching(true); + for (int i = 0; i < nda;) { + double e = s + da; + if (e > end) { + e = end; + } + const double len = 4 * tan((e - s) / 4) / 3; + const double x1 = x0 + len * cos(s + M_PI_2); + const double y1 = y0 + len * sin(s + M_PI_2); + const double x3 = cos(e); + const double y3 = sin(e); + const double x2 = x3 + len * cos(e - M_PI_2); + const double y2 = y3 + len * sin(e - M_PI_2); + path.appendNew<Geom::CubicBezier>(Geom::Point(x1, y1), Geom::Point(x2, y2), Geom::Point(x3, y3)); + s = (++i) * da + start; + x0 = cos(s); + y0 = sin(s); + } + + if (slice && !closed) { + path.appendNew<Geom::LineSegment>(Geom::Point(0.0, 0.0)); + } + path *= affine; + + path_in.append(path); + if ((slice && !closed) || closed) { + path_in.close(true); + } + return 0; +} + +void LPEPts2Ellipse::gen_iso_frame_paths(Geom::PathVector &path_out, const Geom::Affine &affine) +{ + Geom::Path rect(Geom::Point(-1, -1)); + rect.setStitching(true); + rect.appendNew<Geom::LineSegment>(Geom::Point(+1, -1)); + rect.appendNew<Geom::LineSegment>(Geom::Point(+1, +1)); + rect.appendNew<Geom::LineSegment>(Geom::Point(-1, +1)); + rect *= affine; + rect.close(true); + path_out.push_back(rect); +} + +void LPEPts2Ellipse::gen_perspective_frame_paths(Geom::PathVector &path_out, const double rot_angle, + double projmatrix[3][3]) +{ + Geom::Point pts0[4] = { { -1.0, -1.0 }, { +1.0, -1.0 }, { +1.0, +1.0 }, { -1.0, +1.0 } }; + // five_pts.resize(4); + Geom::Affine affine2; + // const double rot_angle = deg2rad(rot_axes); // negative for ccw rotation + affine2 *= Geom::Rotate(-rot_angle); + for (auto &i : pts0) { + Geom::Point point = i; + point *= affine2; + i = projectPoint(point, projmatrix); + } + + Geom::Path rect(pts0[0]); + rect.setStitching(true); + for (int i = 1; i < 4; i++) + rect.appendNew<Geom::LineSegment>(pts0[i]); + rect.close(true); + path_out.push_back(rect); +} + +void LPEPts2Ellipse::gen_axes_paths(Geom::PathVector &path_out, const Geom::Affine &affine) +{ + Geom::LineSegment clx(Geom::Point(-1, 0), Geom::Point(1, 0)); + Geom::LineSegment cly(Geom::Point(0, -1), Geom::Point(0, 1)); + + Geom::Path plx, ply; + plx.append(clx); + ply.append(cly); + plx *= affine; + ply *= affine; + + path_out.push_back(plx); + path_out.push_back(ply); +} + +void LPEPts2Ellipse::gen_perspective_axes_paths(Geom::PathVector &path_out, const double rot_angle, + double projmatrix[3][3]) +{ + Geom::Point pts[4]; + int h = 0; + double dA = 2.0 * M_PI / 4.0; // delta Angle + for (auto &i : pts) { + const double angle = rot_angle + dA * h++; + const Geom::Point circle_point(sin(angle), cos(angle)); + i = projectPoint(circle_point, projmatrix); + } + { + Geom::LineSegment clx(pts[0], pts[2]); + Geom::LineSegment cly(pts[1], pts[3]); + + Geom::Path plx, ply; + plx.append(clx); + ply.append(cly); + + path_out.push_back(plx); + path_out.push_back(ply); + } +} + +bool LPEPts2Ellipse::is_ccw(const std::vector<Geom::Point> &pts) +{ + // method: sum up the angles between edges + size_t n = pts.size(); + // edges about vertex 0 + Geom::Point e0(pts.front() - pts.back()); + Geom::Point e1(pts[1] - pts[0]); + Geom::Coord sum = cross(e0, e1); + // the rest + for (size_t i = 1; i < n; i++) { + e0 = e1; + e1 = pts[i] - pts[i - 1]; + sum += cross(e0, e1); + } + // edges about last vertex (closing) + e0 = e1; + e1 = pts.front() - pts.back(); + sum += cross(e0, e1); + + // close the + if (sum < 0) { + return true; + } else { + return false; + } +} + +void endpoints2angles(const bool ccw_wind, const bool use_other_arc, const Geom::Point &p0, const Geom::Point &p1, + Geom::Coord &a0, Geom::Coord &a1) +{ + if (!p0.isZero() && !p1.isZero()) { + a0 = atan2(p0); + a1 = atan2(p1); + if (!ccw_wind) { + std::swap(a0, a1); + } + if (!use_other_arc) { + std::swap(a0, a1); + } + } +} + +/** + * Generates an ellipse (or circle) from the vertices of a given path. Thereby, using fitting + * algorithms from 2geom. Depending on the settings made by the user regarding things like arc, + * slice, circle etc. the final result will be different + */ +Geom::PathVector LPEPts2Ellipse::doEffect_path(Geom::PathVector const &path_in) +{ + Geom::PathVector path_out; + + // 1) draw original path? + if (draw_ori_path.get_value()) { + path_out.insert(path_out.end(), path_in.begin(), path_in.end()); + } + + + // 2) get all points + // (from: extension/internal/odf.cpp) + points.resize(0); + for (const auto &pit : path_in) { + // extract first point of this path + points.push_back(pit.initialPoint()); + // iterate over all curves + for (const auto &cit : pit) { + points.push_back(cit.finalPoint()); + } + } + // avoid identical start-point and end-point + if (points.front() == points.back()) { + points.pop_back(); + } + + // 3) modify GUI based on selected method + // 3.1) arc options + switch (method) { + case EM_AUTO: + case EM_CIRCLE: + gen_arc.param_widget_is_enabled(true); + if (gen_arc.get_value()) { + slice_arc.param_widget_is_enabled(true); + other_arc.param_widget_is_enabled(true); + } else { + other_arc.param_widget_is_enabled(false); + slice_arc.param_widget_is_enabled(false); + } + break; + default: + gen_arc.param_widget_is_enabled(false); + other_arc.param_widget_is_enabled(false); + slice_arc.param_widget_is_enabled(false); + } + // 3.2) perspective options + switch (method) { + case EM_PERSPECTIVE_CIRCLE: + gen_perspective_frame.param_widget_is_enabled(true); + draw_perspective_axes.param_widget_is_enabled(true); + break; + default: + gen_perspective_frame.param_widget_is_enabled(false); + draw_perspective_axes.param_widget_is_enabled(false); + } + + // 4) call method specific code + switch (method) { + case EM_ISOMETRIC_CIRCLE: + // special mode: Use first two edges, interpret them as two sides of a parallelogram and + // generate an ellipse residing inside the parallelogram. This effect is quite useful when + // generating isometric views. Hence, the name. + if (0 != genIsometricEllipse(points, path_out)) { + return path_in; + } + break; + case EM_PERSPECTIVE_CIRCLE: + // special mode: Use first four points, interpret them as the perspective representation of a square and + // draw the ellipse as it was a circle inside that square. + if (0 != genPerspectiveEllipse(points, path_out)) { + return path_in; + } + break; + case EM_STEINER_ELLIPSE: + if (0 != genSteinerEllipse(points, false, path_out)) { + return path_in; + } + break; + case EM_STEINER_INELLIPSE: + if (0 != genSteinerEllipse(points, true, path_out)) { + return path_in; + } + break; + default: + if (0 != genFitEllipse(points, path_out)) { + return path_in; + } + } + return path_out; +} + +/** + * Generates an ellipse (or circle) from the vertices of a given path. Thereby, using fitting + * algorithms from 2geom. Depending on the settings made by the user regarding things like arc, + * slice, circle etc. the final result will be different. We need at least 5 points to fit an + * ellipse. With 5 points each point is on the ellipse. For less points we get a circle. + */ +int LPEPts2Ellipse::genFitEllipse(std::vector<Geom::Point> const &pts, Geom::PathVector &path_out) +{ + // rotation angle based on user provided rot_axes to position the vertices + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + Geom::Coord a0 = 0; + Geom::Coord a1 = 2 * M_PI; + + if (pts.size() < 2) { + return -1; + } else if (pts.size() == 2) { + // simple line: circle in the middle of the line to the vertices + Geom::Point line = pts.front() - pts.back(); + double radius = line.length() * 0.5; + if (radius < 1e-9) { + return -1; + } + Geom::Point center = middle_point(pts.front(), pts.back()); + Geom::Circle circle(center[0], center[1], radius); + affine *= Geom::Scale(circle.radius()); + affine *= Geom::Translate(circle.center()); + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + } else if (pts.size() >= 5 && EM_AUTO == method) { + // do ellipse + try { + Geom::Ellipse ellipse; + ellipse.fit(pts); + affine *= Geom::Scale(ellipse.ray(Geom::X), ellipse.ray(Geom::Y)); + affine *= Geom::Rotate(ellipse.rotationAngle()); + affine *= Geom::Translate(ellipse.center()); + if (gen_arc.get_value()) { + Geom::Affine inv_affine = affine.inverse(); + Geom::Point p0 = pts.front() * inv_affine; + Geom::Point p1 = pts.back() * inv_affine; + const bool ccw_wind = is_ccw(pts); + endpoints2angles(ccw_wind, other_arc.get_value(), p0, p1, a0, a1); + } + Geom::Path path; + unit_arc_path(path, affine, a0, a1, slice_arc.get_value()); + path_out.push_back(path); + } catch (...) { + return -1; + } + } else { + // do a circle (3,4 points, or only_circle set) + try { + Geom::Circle circle; + circle.fit(pts); + affine *= Geom::Scale(circle.radius()); + affine *= Geom::Translate(circle.center()); + if (gen_arc.get_value()) { + Geom::Point p0 = pts.front() - circle.center(); + Geom::Point p1 = pts.back() - circle.center(); + const bool ccw_wind = is_ccw(pts); + endpoints2angles(ccw_wind, other_arc.get_value(), p0, p1, a0, a1); + } + Geom::Path path; + unit_arc_path(path, affine, a0, a1, slice_arc.get_value()); + path_out.push_back(path); + } catch (...) { + return -1; + } + } + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + return 0; +} + +int LPEPts2Ellipse::genIsometricEllipse(std::vector<Geom::Point> const &pts, Geom::PathVector &path_out) + +{ + // take the first 3 vertices for the edges + if (pts.size() < 3) { + return -1; + } + // calc edges + Geom::Point e0 = pts[0] - pts[1]; + Geom::Point e1 = pts[2] - pts[1]; + + Geom::Coord ce = cross(e0, e1); + // parallel or one is zero? + if (fabs(ce) < 1e-9) { + return -1; + } + // unit vectors along edges + Geom::Point u0 = unit_vector(e0); + Geom::Point u1 = unit_vector(e1); + // calc angles + Geom::Coord a0 = atan2(e0); + // Coord a1=M_PI_2-atan2(e1)-a0; + Geom::Coord a1 = acos(dot(u0, u1)) - M_PI_2; + // if(fabs(a1)<1e-9) return -1; + if (ce < 0) { + a1 = -a1; + } + // lengths: l0= length of edge 0; l1= height of parallelogram + Geom::Coord l0 = e0.length() * 0.5; + Geom::Point e0n = e1 - dot(u0, e1) * u0; + Geom::Coord l1 = e0n.length() * 0.5; + + // center of the ellipse + Geom::Point pos = pts[1] + 0.5 * (e0 + e1); + + // rotation angle based on user provided rot_axes to position the vertices + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + + // build up the affine transformation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + affine *= Geom::Scale(l0, l1); + affine *= Geom::HShear(-tan(a1)); + affine *= Geom::Rotate(a0); + affine *= Geom::Translate(pos); + + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + return 0; +} + +void evalSteinerEllipse(Geom::Point const &pCenter, Geom::Point const &pCenter_Pt2, Geom::Point const &pPt0_Pt1, + const double &angle, Geom::Point &pRes) +{ + // formula for the evaluation of points on the steiner ellipse using parameter angle + pRes = pCenter + pCenter_Pt2 * cos(angle) + pPt0_Pt1 * sin(angle) / sqrt(3); +} + +int LPEPts2Ellipse::genSteinerEllipse(std::vector<Geom::Point> const &pts, bool gen_inellipse, + Geom::PathVector &path_out) +{ + // take the first 3 vertices for the edges + if (pts.size() < 3) { + return -1; + } + // calc center + Geom::Point pCenter = (pts[0] + pts[1] + pts[2]) / 3; + // calc main directions of affine triangle + Geom::Point f1 = pts[2] - pCenter; + Geom::Point f2 = (pts[1] - pts[0]) / sqrt(3); + + // calc zero angle t0 + const double denominator = dot(f1, f1) - dot(f2, f2); + double t0 = 0; + if (fabs(denominator) > 1e-12) { + const double cot2t0 = 2.0 * dot(f1, f2) / denominator; + t0 = atan(cot2t0) / 2.0; + } + + // calc relative points of main axes (for axis directions) + Geom::Point p0(0, 0), pRel0, pRel1; + evalSteinerEllipse(p0, pts[2] - pCenter, pts[1] - pts[0], t0, pRel0); + evalSteinerEllipse(p0, pts[2] - pCenter, pts[1] - pts[0], t0 + M_PI_2, pRel1); + Geom::Coord l0 = pRel0.length(); + Geom::Coord l1 = pRel1.length(); + + // basic rotation + double a0 = atan2(pRel0); + + bool swapped = false; + + if (l1 > l0) { + std::swap(l0, l1); + a0 += M_PI_2; + swapped = true; + } + + // the Steiner inellipse is just scaled down by 2 + if (gen_inellipse) { + l0 /= 2; + l1 /= 2; + } + + // rotation angle based on user provided rot_axes to position the vertices + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + + // build up the affine transformation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + affine *= Geom::Scale(l0, l1); + affine *= Geom::Rotate(a0); + affine *= Geom::Translate(pCenter); + + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + return 0; +} + +// identical to lpe-perspective-envelope.cpp +Geom::Point LPEPts2Ellipse::projectPoint(Geom::Point p, double m[][3]) +{ + Geom::Coord x = p[0]; + Geom::Coord y = p[1]; + return Geom::Point(Geom::Coord((x * m[0][0] + y * m[0][1] + m[0][2]) / (x * m[2][0] + y * m[2][1] + m[2][2])), + Geom::Coord((x * m[1][0] + y * m[1][1] + m[1][2]) / (x * m[2][0] + y * m[2][1] + m[2][2]))); +} + +int LPEPts2Ellipse::genPerspectiveEllipse(std::vector<Geom::Point> const &pts, Geom::PathVector &path_out) +{ + using Geom::X; + using Geom::Y; + // we need at least four points! + if (pts.size() < 4) + return -1; + + // 1) check if the first three edges are a valid perspective + // calc edge + Geom::Point e[] = { pts[0] - pts[1], pts[1] - pts[2], pts[2] - pts[3], pts[3] - pts[0] }; + // calc directions + Geom::Coord c[] = { cross(e[0], e[1]), cross(e[1], e[2]), cross(e[2], e[3]), cross(e[3], e[0]) }; + // is this quad not convex? + if (!((c[0] > 0 && c[1] > 0 && c[2] > 0 && c[3] > 0) || (c[0] < 0 && c[1] < 0 && c[2] < 0 && c[3] < 0))) + return -1; + + // 2) solve the direct linear transformation (see e.g. lpe-perspective-envelope.cpp or + // https://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/) + + // the square points in the initial configuration (about the unit circle): + Geom::Point pts0[4] = { { -1.0, -1.0 }, { +1.0, -1.0 }, { +1.0, +1.0 }, { -1.0, +1.0 } }; + + // build equation in matrix form + double eqnVec[8] = { 0 }; + double eqnMat[64] = { 0 }; + for (unsigned int i = 0; i < 4; ++i) { + eqnMat[8 * (i + 0) + 0] = pts0[i][X]; + eqnMat[8 * (i + 0) + 1] = pts0[i][Y]; + eqnMat[8 * (i + 0) + 2] = 1; + eqnMat[8 * (i + 0) + 6] = -pts[i][X] * pts0[i][X]; + eqnMat[8 * (i + 0) + 7] = -pts[i][X] * pts0[i][Y]; + eqnMat[8 * (i + 4) + 3] = pts0[i][X]; + eqnMat[8 * (i + 4) + 4] = pts0[i][Y]; + eqnMat[8 * (i + 4) + 5] = 1; + eqnMat[8 * (i + 4) + 6] = -pts[i][Y] * pts0[i][X]; + eqnMat[8 * (i + 4) + 7] = -pts[i][Y] * pts0[i][Y]; + eqnVec[i] = pts[i][X]; + eqnVec[i + 4] = pts[i][Y]; + } + // solve using gsl library + gsl_matrix_view m = gsl_matrix_view_array(eqnMat, 8, 8); + gsl_vector_view b = gsl_vector_view_array(eqnVec, 8); + int s = 0; + gsl_linalg_LU_decomp(&m.matrix, gsl_p, &s); + gsl_linalg_LU_solve(&m.matrix, gsl_p, &b.vector, gsl_x); + // transfer the solution to the projection matrix for further use + size_t h = 0; + double projmatrix[3][3]; + for (auto &matRow : projmatrix) { + for (double &matElement : matRow) { + if (h == 8) { + projmatrix[2][2] = 1.0; + } else { + matElement = gsl_vector_get(gsl_x, h++); + } + } + } + + // 3) generate five points on a unit circle and project them + five_pts.resize(5); // reuse and avoid new/delete + h = 0; + double dA = 2.0 * M_PI / 5.0; // delta Angle + for (auto &i : five_pts) { + const double angle = dA * h++; + const Geom::Point circle_point(sin(angle), cos(angle)); + i = projectPoint(circle_point, projmatrix); + } + + // 4) fit the five points to an ellipse with the already known function inside genFitEllipse() function + // build up the affine transformation + const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation + Geom::Affine affine; + affine *= Geom::Rotate(rot_angle); + + try { + Geom::Ellipse ellipse; + ellipse.fit(five_pts); + affine *= Geom::Scale(ellipse.ray(Geom::X), ellipse.ray(Geom::Y)); + affine *= Geom::Rotate(ellipse.rotationAngle()); + affine *= Geom::Translate(ellipse.center()); + } catch (...) { + return -1; + } + + Geom::Path path; + unit_arc_path(path, affine); + path_out.push_back(path); + + // 5) frames and axes + bool ccw_wind = false; + if (gen_perspective_frame.get_value() || draw_perspective_axes.get_value()) + ccw_wind = is_ccw(pts); + const double ra = ccw_wind ? rot_angle : -rot_angle; + + // draw frame? + if (gen_isometric_frame.get_value()) { + gen_iso_frame_paths(path_out, affine); + } + + // draw perspective frame? + if (gen_perspective_frame.get_value()) { + gen_perspective_frame_paths(path_out, ra, projmatrix); + } + + // draw axes? + if (draw_axes.get_value()) { + gen_axes_paths(path_out, affine); + } + + // draw perspective axes? + if (draw_perspective_axes.get_value()) { + gen_perspective_axes_paths(path_out, ra, projmatrix); + } + + return 0; +} + + +/* ######################## */ + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-pts2ellipse.h b/src/live_effects/lpe-pts2ellipse.h new file mode 100644 index 0000000..3ccf0c4 --- /dev/null +++ b/src/live_effects/lpe-pts2ellipse.h @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_PTS_TO_ELLIPSE_H +#define INKSCAPE_LPE_PTS_TO_ELLIPSE_H + +/** \file + * LPE "Points to Ellipse" implementation + */ + +/* + * Authors: + * Markus Schwienbacher + * + * Copyright (C) Markus Schwienbacher 2013 <mschwienbacher@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/enum.h" + +#include <gsl/gsl_linalg.h> + + +// struct gsl_vector; +// struct gsl_permutation; + +namespace Inkscape { +namespace LivePathEffect { + +enum EllipseMethod { + EM_AUTO, + EM_CIRCLE, + EM_ISOMETRIC_CIRCLE, + EM_PERSPECTIVE_CIRCLE, + EM_STEINER_ELLIPSE, + EM_STEINER_INELLIPSE, + EM_END +}; + +class LPEPts2Ellipse : public Effect { + public: + LPEPts2Ellipse(LivePathEffectObject *lpeobject); + ~LPEPts2Ellipse() override; + + Geom::PathVector doEffect_path(Geom::PathVector const &path_in) override; + + private: + LPEPts2Ellipse(const LPEPts2Ellipse &) = delete; + LPEPts2Ellipse &operator=(const LPEPts2Ellipse &) = delete; + + + int genIsometricEllipse(std::vector<Geom::Point> const &points_in, Geom::PathVector &path_out); + + int genFitEllipse(std::vector<Geom::Point> const &points_in, Geom::PathVector &path_out); + + int genSteinerEllipse(std::vector<Geom::Point> const &points_in, bool gen_inellipse, Geom::PathVector &path_out); + + int genPerspectiveEllipse(std::vector<Geom::Point> const &points_in, Geom::PathVector &path_out); + + // utility functions + static int unit_arc_path(Geom::Path &path_in, Geom::Affine &affine, double start = 0.0, + double end = 2.0 * M_PI, // angles + bool slice = false); + static void gen_iso_frame_paths(Geom::PathVector &path_out, const Geom::Affine &affine); + static void gen_perspective_frame_paths(Geom::PathVector &path_out, const double rot_angle, + double projmatrix[3][3]); + static void gen_axes_paths(Geom::PathVector &path_out, const Geom::Affine &affine); + static void gen_perspective_axes_paths(Geom::PathVector &path_out, const double rot_angle, double projmatrix[3][3]); + static bool is_ccw(const std::vector<Geom::Point> &pts); + static Geom::Point projectPoint(Geom::Point p, double m[][3]); + + // GUI parameters + EnumParam<EllipseMethod> method; + BoolParam gen_isometric_frame; + BoolParam gen_perspective_frame; + BoolParam gen_arc; + BoolParam other_arc; + BoolParam slice_arc; + BoolParam draw_axes; + BoolParam draw_perspective_axes; + ScalarParam rot_axes; + BoolParam draw_ori_path; + + // collect the points from the input paths + std::vector<Geom::Point> points; + + // used for solving perspective circle + gsl_vector *gsl_x; + gsl_permutation *gsl_p; + std::vector<Geom::Point> five_pts; +}; + +} // namespace LivePathEffect +} // namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-recursiveskeleton.cpp b/src/live_effects/lpe-recursiveskeleton.cpp new file mode 100644 index 0000000..0a67cdb --- /dev/null +++ b/src/live_effects/lpe-recursiveskeleton.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Inspired by Hofstadter's 'Goedel Escher Bach', chapter V. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2007-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-recursiveskeleton.h" + +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPERecursiveSkeleton::LPERecursiveSkeleton(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + iterations(_("Iterations:"), _("recursivity"), "iterations", &wr, this, 2) +{ + show_orig_path = true; + concatenate_before_pwd2 = true; + iterations.param_make_integer(true); + iterations.param_set_range(1, 15); + registerParameter(&iterations); + +} + +LPERecursiveSkeleton::~LPERecursiveSkeleton() += default; + + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPERecursiveSkeleton::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + Piecewise<D2<SBasis> > output; + double prop_scale = 1.0; + + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pwd2_in); + Piecewise<SBasis> x0 = false /*vertical_pattern.get_value()*/ ? Piecewise<SBasis>(patternd2[1]) : Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y0 = false /*vertical_pattern.get_value()*/ ? Piecewise<SBasis>(patternd2[0]) : Piecewise<SBasis>(patternd2[1]); + OptInterval pattBndsX = bounds_exact(x0); + OptInterval pattBndsY = bounds_exact(y0); + + if ( !pattBndsX || !pattBndsY) { + return pwd2_in; + } + + x0 -= pattBndsX->min(); + y0 -= pattBndsY->middle(); + + double noffset = 0;//normal_offset; + double toffset = 0;//tang_offset; + if (false /*prop_units.get_value()*/){ + noffset *= pattBndsY->extent(); + toffset *= pattBndsX->extent(); + } + + y0+=noffset; + + output = pwd2_in; + + for (int i = 0; i < iterations; ++i) { + std::vector<Piecewise<D2<SBasis> > > skeleton = split_at_discontinuities(output); + + output.clear(); + for (auto path_i : skeleton){ + Piecewise<SBasis> x = x0; + Piecewise<SBasis> y = y0; + Piecewise<D2<SBasis> > uskeleton = arc_length_parametrization(path_i,2,.1); + uskeleton = remove_short_cuts(uskeleton,.01); + Piecewise<D2<SBasis> > n = rot90(derivative(uskeleton)); + n = force_continuity(remove_short_cuts(n,.1)); + + double scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); + + // TODO investigate why pattWidth is not being used: + // - Doesn't appear to have been used anywhere in bzr history (Alex V: 2013-03-16) + // double pattWidth = pattBndsX->extent() * scaling; + + if (scaling != 1.0) { + x*=scaling; + } + + if ( true /*scale_y_rel.get_value()*/ ) { + y*=(scaling*prop_scale); + } else { + if (prop_scale != 1.0) y *= prop_scale; + } + x += toffset; + + output.concat(compose(uskeleton,x)+y*compose(n,x)); + } + } + + return output; +} + + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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/live_effects/lpe-recursiveskeleton.h b/src/live_effects/lpe-recursiveskeleton.h new file mode 100644 index 0000000..1347bef --- /dev/null +++ b/src/live_effects/lpe-recursiveskeleton.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief see lpe-recursiveskeleton.cpp. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2007-2009 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_RECURSIVESKELETON_H +#define INKSCAPE_LPE_RECURSIVESKELETON_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + + +class LPERecursiveSkeleton : public Effect { +public: + LPERecursiveSkeleton(LivePathEffectObject *lpeobject); + ~LPERecursiveSkeleton() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + +private: + ScalarParam iterations; + + LPERecursiveSkeleton(const LPERecursiveSkeleton&) = delete; + LPERecursiveSkeleton& operator=(const LPERecursiveSkeleton&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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/live_effects/lpe-rough-hatches.cpp b/src/live_effects/lpe-rough-hatches.cpp new file mode 100644 index 0000000..94aa07c --- /dev/null +++ b/src/live_effects/lpe-rough-hatches.cpp @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE Curve Stitching implementation, used as an example for a base starting class + * when implementing new LivePathEffects. + * + */ +/* + * Authors: + * JF Barraud. + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "ui/widget/scalar.h" +#include "live_effects/lpe-rough-hatches.h" + +#include "object/sp-item.h" + +#include "xml/repr.h" + +#include <2geom/sbasis-math.h> +#include <2geom/bezier-to-sbasis.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +using namespace Geom; + +//------------------------------------------------ +// Some goodies to navigate through curve's levels. +//------------------------------------------------ +struct LevelCrossing{ + Point pt; + double t; + bool sign; + bool used; + std::pair<unsigned,unsigned> next_on_curve; + std::pair<unsigned,unsigned> prev_on_curve; +}; +struct LevelCrossingOrder { + bool operator()(LevelCrossing a, LevelCrossing b) { + return ( a.pt[Y] < b.pt[Y] );// a.pt[X] == b.pt[X] since we are supposed to be on the same level... + //return ( a.pt[X] < b.pt[X] || ( a.pt[X] == b.pt[X] && a.pt[Y] < b.pt[Y] ) ); + } +}; +struct LevelCrossingInfo{ + double t; + unsigned level; + unsigned idx; +}; +struct LevelCrossingInfoOrder { + bool operator()(LevelCrossingInfo a, LevelCrossingInfo b) { + return a.t < b.t; + } +}; + +typedef std::vector<LevelCrossing> LevelCrossings; + +static std::vector<double> +discontinuities(Piecewise<D2<SBasis> > const &f){ + std::vector<double> result; + if (f.size()==0) return result; + result.push_back(f.cuts[0]); + Point prev_pt = f.segs[0].at1(); + //double old_t = f.cuts[0]; + for(unsigned i=1; i<f.size(); i++){ + if ( f.segs[i].at0()!=prev_pt){ + result.push_back(f.cuts[i]); + //old_t = f.cuts[i]; + //assert(f.segs[i-1].at1()==f.valueAt(old_t)); + } + prev_pt = f.segs[i].at1(); + } + result.push_back(f.cuts.back()); + //assert(f.segs.back().at1()==f.valueAt(old_t)); + return result; +} + +class LevelsCrossings: public std::vector<LevelCrossings>{ +public: + LevelsCrossings():std::vector<LevelCrossings>(){}; + LevelsCrossings(std::vector<std::vector<double> > const ×, + Piecewise<D2<SBasis> > const &f, + Piecewise<SBasis> const &dx){ + + for (const auto & time : times){ + LevelCrossings lcs; + for (double j : time){ + LevelCrossing lc; + lc.pt = f.valueAt(j); + lc.t = j; + lc.sign = ( dx.valueAt(j)>0 ); + lc.used = false; + lcs.push_back(lc); + } + std::sort(lcs.begin(), lcs.end(), LevelCrossingOrder()); + push_back(lcs); + } + //Now create time ordering. + std::vector<LevelCrossingInfo>temp; + for (unsigned i=0; i<size(); i++){ + for (unsigned j=0; j<(*this)[i].size(); j++){ + LevelCrossingInfo elem; + elem.t = (*this)[i][j].t; + elem.level = i; + elem.idx = j; + temp.push_back(elem); + } + } + std::sort(temp.begin(),temp.end(),LevelCrossingInfoOrder()); + std::vector<double> jumps = discontinuities(f); + unsigned jump_idx = 0; + unsigned first_in_comp = 0; + for (unsigned i=0; i<temp.size(); i++){ + unsigned lvl = temp[i].level, idx = temp[i].idx; + if ( i == temp.size()-1 || temp[i+1].t > jumps[jump_idx+1]){ + std::pair<unsigned,unsigned>next_data(temp[first_in_comp].level,temp[first_in_comp].idx); + (*this)[lvl][idx].next_on_curve = next_data; + first_in_comp = i+1; + jump_idx += 1; + }else{ + std::pair<unsigned,unsigned> next_data(temp[i+1].level,temp[i+1].idx); + (*this)[lvl][idx].next_on_curve = next_data; + } + } + + for (unsigned i=0; i<size(); i++){ + for (unsigned j=0; j<(*this)[i].size(); j++){ + std::pair<unsigned,unsigned> next = (*this)[i][j].next_on_curve; + (*this)[next.first][next.second].prev_on_curve = std::pair<unsigned,unsigned>(i,j); + } + } + } + + void findFirstUnused(unsigned &level, unsigned &idx){ + level = size(); + idx = 0; + for (unsigned i=0; i<size(); i++){ + for (unsigned j=0; j<(*this)[i].size(); j++){ + if (!(*this)[i][j].used){ + level = i; + idx = j; + return; + } + } + } + } + //set indexes to point to the next point in the "snake walk" + //follow_level's meaning: + // 0=yes upward + // 1=no, last move was upward, + // 2=yes downward + // 3=no, last move was downward. + void step(unsigned &level, unsigned &idx, int &direction){ + if ( direction % 2 == 0 ){ + if (direction == 0) { + if ( idx >= (*this)[level].size()-1 || (*this)[level][idx+1].used ) { + level = size(); + return; + } + idx += 1; + }else{ + if ( idx <= 0 || (*this)[level][idx-1].used ) { + level = size(); + return; + } + idx -= 1; + } + direction += 1; + return; + } + //double t = (*this)[level][idx].t; + double sign = ((*this)[level][idx].sign ? 1 : -1); + //---double next_t = t; + //level += 1; + direction = (direction + 1)%4; + if (level == size()){ + return; + } + + std::pair<unsigned,unsigned> next; + if ( sign > 0 ){ + next = (*this)[level][idx].next_on_curve; + }else{ + next = (*this)[level][idx].prev_on_curve; + } + + if ( level+1 != next.first || (*this)[next.first][next.second].used ) { + level = size(); + return; + } + level = next.first; + idx = next.second; + return; + } +}; + +//------------------------------------------------------- +// Bend a path... +//------------------------------------------------------- + +static Piecewise<D2<SBasis> > bend(Piecewise<D2<SBasis> > const &f, Piecewise<SBasis> bending){ + D2<Piecewise<SBasis> > ff = make_cuts_independent(f); + ff[X] += compose(bending, ff[Y]); + return sectionize(ff); +} + +//-------------------------------------------------------- +// The RoughHatches lpe. +//-------------------------------------------------------- +LPERoughHatches::LPERoughHatches(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + hatch_dist(0), + dist_rdm(_("Frequency randomness:"), _("Variation of distance between hatches, in %."), "dist_rdm", &wr, this, 75), + growth(_("Growth:"), _("Growth of distance between hatches."), "growth", &wr, this, 0.), +//FIXME: top/bottom names are inverted in the UI/svg and in the code!! + scale_tf(_("Half-turns smoothness: 1st side, in:"), _("Set smoothness/sharpness of path when reaching a 'bottom' half-turn. 0=sharp, 1=default"), "scale_bf", &wr, this, 1.), + scale_tb(_("1st side, out:"), _("Set smoothness/sharpness of path when leaving a 'bottom' half-turn. 0=sharp, 1=default"), "scale_bb", &wr, this, 1.), + scale_bf(_("2nd side, in:"), _("Set smoothness/sharpness of path when reaching a 'top' half-turn. 0=sharp, 1=default"), "scale_tf", &wr, this, 1.), + scale_bb(_("2nd side, out:"), _("Set smoothness/sharpness of path when leaving a 'top' half-turn. 0=sharp, 1=default"), "scale_tb", &wr, this, 1.), + top_edge_variation(_("Magnitude jitter: 1st side:"), _("Randomly moves 'bottom' half-turns to produce magnitude variations."), "bottom_edge_variation", &wr, this, 0), + bot_edge_variation(_("2nd side:"), _("Randomly moves 'top' half-turns to produce magnitude variations."), "top_edge_variation", &wr, this, 0), + top_tgt_variation(_("Parallelism jitter: 1st side:"), _("Add direction randomness by moving 'bottom' half-turns tangentially to the boundary."), "bottom_tgt_variation", &wr, this, 0), + bot_tgt_variation(_("2nd side:"), _("Add direction randomness by randomly moving 'top' half-turns tangentially to the boundary."), "top_tgt_variation", &wr, this, 0), + top_smth_variation(_("Variance: 1st side:"), _("Randomness of 'bottom' half-turns smoothness"), "top_smth_variation", &wr, this, 0), + bot_smth_variation(_("2nd side:"), _("Randomness of 'top' half-turns smoothness"), "bottom_smth_variation", &wr, this, 0), +// + fat_output(_("Generate thick/thin path"), _("Simulate a stroke of varying width"), "fat_output", &wr, this, true), + do_bend(_("Bend hatches"), _("Add a global bend to the hatches (slower)"), "do_bend", &wr, this, true), + stroke_width_top(_("Thickness: at 1st side:"), _("Width at 'bottom' half-turns"), "stroke_width_top", &wr, this, 1.), + stroke_width_bot(_("At 2nd side:"), _("Width at 'top' half-turns"), "stroke_width_bottom", &wr, this, 1.), +// + front_thickness(_("From 2nd to 1st side:"), _("Width from 'top' to 'bottom'"), "front_thickness", &wr, this, 1.), + back_thickness(_("From 1st to 2nd side:"), _("Width from 'bottom' to 'top'"), "back_thickness", &wr, this, .25), + + direction(_("Hatches width and dir"), _("Defines hatches frequency and direction"), "direction", &wr, this, Geom::Point(50,0)), +// + bender(_("Global bending"), _("Relative position to a reference point defines global bending direction and amount"), "bender", &wr, this, Geom::Point(-5,0)) +{ + registerParameter(&direction); + registerParameter(&dist_rdm); + registerParameter(&growth); + registerParameter(&do_bend); + registerParameter(&bender); + registerParameter(&top_edge_variation); + registerParameter(&bot_edge_variation); + registerParameter(&top_tgt_variation); + registerParameter(&bot_tgt_variation); + registerParameter(&scale_tf); + registerParameter(&scale_tb); + registerParameter(&scale_bf); + registerParameter(&scale_bb); + registerParameter(&top_smth_variation); + registerParameter(&bot_smth_variation); + registerParameter(&fat_output); + registerParameter(&stroke_width_top); + registerParameter(&stroke_width_bot); + registerParameter(&front_thickness); + registerParameter(&back_thickness); + + //hatch_dist.param_set_range(0.1, Geom::infinity()); + growth.param_set_range(0, std::numeric_limits<double>::max()); + dist_rdm.param_set_range(0, 99.); + stroke_width_top.param_set_range(0, std::numeric_limits<double>::max()); + stroke_width_bot.param_set_range(0, std::numeric_limits<double>::max()); + front_thickness.param_set_range(0, std::numeric_limits<double>::max()); + back_thickness.param_set_range(0, std::numeric_limits<double>::max()); + + // hide the widgets for direction and bender vectorparams + direction.widget_is_visible = false; + bender.widget_is_visible = false; + // give distinguishing colors to direction and bender on-canvas params + direction.set_oncanvas_color(0x00ff7d00); + bender.set_oncanvas_color(0xffffb500); + + concatenate_before_pwd2 = false; + show_orig_path = true; +} + +LPERoughHatches::~LPERoughHatches() += default; + + +void LPERoughHatches::doOnApply(SPLPEItem const *lpeitem) +{ + lpeversion.param_setValue("1.2", true); +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPERoughHatches::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in){ + + //std::cout<<"doEffect_pwd2:\n"; + + Piecewise<D2<SBasis> > result; + + Piecewise<D2<SBasis> > transformed_pwd2_in = pwd2_in; + Point start = pwd2_in.segs.front().at0(); + Point end = pwd2_in.segs.back().at1(); + if (end != start ){ + transformed_pwd2_in.push_cut( transformed_pwd2_in.cuts.back() + 1 ); + D2<SBasis> stitch( SBasis( 1, Linear(end[X],start[X]) ), SBasis( 1, Linear(end[Y],start[Y]) ) ); + transformed_pwd2_in.push_seg( stitch ); + } + Point transformed_org = direction.getOrigin(); + Piecewise<SBasis> tilter;//used to bend the hatches + Affine bend_mat;//used to bend the hatches + + if (do_bend.get_value()){ + Point bend_dir = -rot90(unit_vector(bender.getVector())); + double bend_amount = L2(bender.getVector()); + bend_mat = Affine(-bend_dir[Y], bend_dir[X], bend_dir[X], bend_dir[Y],0,0); + transformed_pwd2_in = transformed_pwd2_in * bend_mat; + tilter = Piecewise<SBasis>(shift(Linear(-bend_amount),1)); + OptRect bbox = bounds_exact( transformed_pwd2_in ); + if (!(bbox)) return pwd2_in; + tilter.setDomain((*bbox)[Y]); + transformed_pwd2_in = bend(transformed_pwd2_in, tilter); + transformed_pwd2_in = transformed_pwd2_in * bend_mat.inverse(); + } + hatch_dist = Geom::L2(direction.getVector())/5; + Point hatches_dir = rot90(unit_vector(direction.getVector())); + Affine mat(-hatches_dir[Y], hatches_dir[X], hatches_dir[X], hatches_dir[Y],0,0); + transformed_pwd2_in = transformed_pwd2_in * mat; + transformed_org *= mat; + + std::vector<std::vector<Point> > snakePoints; + snakePoints = linearSnake(transformed_pwd2_in, transformed_org); + if (!snakePoints.empty()){ + Piecewise<D2<SBasis> >smthSnake = smoothSnake(snakePoints); + smthSnake = smthSnake*mat.inverse(); + if (do_bend.get_value()){ + smthSnake = smthSnake*bend_mat; + smthSnake = bend(smthSnake, -tilter); + smthSnake = smthSnake*bend_mat.inverse(); + } + return (smthSnake); + } + return pwd2_in; +} + +//------------------------------------------------ +// Generate the levels with random, growth... +//------------------------------------------------ +std::vector<double> +LPERoughHatches::generateLevels(Interval const &domain, double x_org){ + std::vector<double> result; + int n = int((domain.min()-x_org)/hatch_dist); + double x = x_org + n * hatch_dist; + //double x = domain.min() + double(hatch_dist)/2.; + double step = double(hatch_dist); + double scale = 1+(hatch_dist*growth/domain.extent()); + while (x < domain.max()){ + result.push_back(x); + double rdm = 1; + if (dist_rdm.get_value() != 0) + rdm = 1.+ double((2*dist_rdm - dist_rdm.get_value()))/100.; + x+= step*rdm; + step*=scale;//(1.+double(growth)); + } + return result; +} + + +//------------------------------------------------------- +// Walk through the intersections to create linear hatches +//------------------------------------------------------- +std::vector<std::vector<Point> > +LPERoughHatches::linearSnake(Piecewise<D2<SBasis> > const &f, Point const &org){ + + //std::cout<<"linearSnake:\n"; + std::vector<std::vector<Point> > result; + Piecewise<SBasis> x = make_cuts_independent(f)[X]; + //Remark: derivative is computed twice in the 2 lines below!! + Piecewise<SBasis> dx = derivative(x); + OptInterval range = bounds_exact(x); + + if (!range) return result; + std::vector<double> levels = generateLevels(*range, org[X]); + std::vector<std::vector<double> > times; + times = multi_roots(x,levels); +//TODO: fix multi_roots!!!***************************************** +//remove doubles :-( + std::vector<std::vector<double> > cleaned_times(levels.size(),std::vector<double>()); + for (unsigned i=0; i<times.size(); i++){ + if ( times[i].size()>0 ){ + double last_t = times[i][0]-1;//ugly hack!! + for (unsigned j=0; j<times[i].size(); j++){ + if (times[i][j]-last_t >0.000001){ + last_t = times[i][j]; + cleaned_times[i].push_back(last_t); + } + } + } + } + times = cleaned_times; +//******************************************************************* + + LevelsCrossings lscs(times,f,dx); + + unsigned i,j; + lscs.findFirstUnused(i,j); + + std::vector<Point> result_component; + int n = int((range->min()-org[X])/hatch_dist); + + while ( i < lscs.size() ){ + int dir = 0; + //switch orientation of first segment according to starting point. + if ((static_cast<long long>(i) % 2 == n % 2) && ((j + 1) < lscs[i].size()) && !lscs[i][j].used){ + j += 1; + dir = 2; + } + + while ( i < lscs.size() ){ + result_component.push_back(lscs[i][j].pt); + lscs[i][j].used = true; + lscs.step(i,j, dir); + } + result.push_back(result_component); + result_component = std::vector<Point>(); + lscs.findFirstUnused(i,j); + } + return result; +} + +//------------------------------------------------------- +// Smooth the linear hatches according to params... +//------------------------------------------------------- +Piecewise<D2<SBasis> > +LPERoughHatches::smoothSnake(std::vector<std::vector<Point> > const &linearSnake){ + + Piecewise<D2<SBasis> > result; + for (const auto & comp : linearSnake){ + if (comp.size()>=2){ + Point last_pt = comp[0]; + //Point last_top = linearSnake[comp][0]; + //Point last_bot = linearSnake[comp][0]; + Point last_hdle = comp[0]; + Point last_top_hdle = comp[0]; + Point last_bot_hdle = comp[0]; + Geom::Path res_comp(last_pt); + Geom::Path res_comp_top(last_pt); + Geom::Path res_comp_bot(last_pt); + unsigned i=1; + //bool is_top = true;//Inversion here; due to downward y? + bool is_top = ( comp[0][Y] < comp[1][Y] ); + + while( i+1<comp.size() ){ + Point pt0 = comp[i]; + Point pt1 = comp[i+1]; + Point new_pt = (pt0+pt1)/2; + double scale_in = (is_top ? scale_tf : scale_bf ); + double scale_out = (is_top ? scale_tb : scale_bb ); + if (is_top){ + if (top_edge_variation.get_value() != 0) + new_pt[Y] += double(top_edge_variation)-top_edge_variation.get_value()/2.; + if (top_tgt_variation.get_value() != 0) + new_pt[X] += double(top_tgt_variation)-top_tgt_variation.get_value()/2.; + if (top_smth_variation.get_value() != 0) { + scale_in*=(100.-double(top_smth_variation))/100.; + scale_out*=(100.-double(top_smth_variation))/100.; + } + }else{ + if (bot_edge_variation.get_value() != 0) + new_pt[Y] += double(bot_edge_variation)-bot_edge_variation.get_value()/2.; + if (bot_tgt_variation.get_value() != 0) + new_pt[X] += double(bot_tgt_variation)-bot_tgt_variation.get_value()/2.; + if (bot_smth_variation.get_value() != 0) { + scale_in*=(100.-double(bot_smth_variation))/100.; + scale_out*=(100.-double(bot_smth_variation))/100.; + } + } + Point new_hdle_in = new_pt + (pt0-pt1) * (scale_in /2.); + Point new_hdle_out = new_pt - (pt0-pt1) * (scale_out/2.); + + if ( fat_output.get_value() ){ + //double scaled_width = double((is_top ? stroke_width_top : stroke_width_bot))/(pt1[X]-pt0[X]); + //double scaled_width = 1./(pt1[X]-pt0[X]); + //Point hdle_offset = (pt1-pt0)*scaled_width; + Point inside = new_pt; + Point inside_hdle_in; + Point inside_hdle_out; + inside[Y]+= double((is_top ? -stroke_width_top : stroke_width_bot)); + inside_hdle_in = inside + (new_hdle_in -new_pt);// + hdle_offset * double((is_top ? front_thickness : back_thickness)); + inside_hdle_out = inside + (new_hdle_out-new_pt);// - hdle_offset * double((is_top ? back_thickness : front_thickness)); + + inside_hdle_in += (pt1-pt0)/2*( double((is_top ? front_thickness : back_thickness)) / (pt1[X]-pt0[X]) ); + inside_hdle_out -= (pt1-pt0)/2*( double((is_top ? back_thickness : front_thickness)) / (pt1[X]-pt0[X]) ); + + new_hdle_in -= (pt1-pt0)/2*( double((is_top ? front_thickness : back_thickness)) / (pt1[X]-pt0[X]) ); + new_hdle_out += (pt1-pt0)/2*( double((is_top ? back_thickness : front_thickness)) / (pt1[X]-pt0[X]) ); + //TODO: find a good way to handle limit cases (small smoothness, large stroke). + //if (inside_hdle_in[X] > inside[X]) inside_hdle_in = inside; + //if (inside_hdle_out[X] < inside[X]) inside_hdle_out = inside; + + if (is_top){ + res_comp_top.appendNew<CubicBezier>(last_top_hdle,new_hdle_in,new_pt); + res_comp_bot.appendNew<CubicBezier>(last_bot_hdle,inside_hdle_in,inside); + last_top_hdle = new_hdle_out; + last_bot_hdle = inside_hdle_out; + }else{ + res_comp_top.appendNew<CubicBezier>(last_top_hdle,inside_hdle_in,inside); + res_comp_bot.appendNew<CubicBezier>(last_bot_hdle,new_hdle_in,new_pt); + last_top_hdle = inside_hdle_out; + last_bot_hdle = new_hdle_out; + } + }else{ + res_comp.appendNew<CubicBezier>(last_hdle,new_hdle_in,new_pt); + } + + last_hdle = new_hdle_out; + i+=2; + is_top = !is_top; + } + if ( i<comp.size() ){ + if ( fat_output.get_value() ){ + res_comp_top.appendNew<CubicBezier>(last_top_hdle,comp[i],comp[i]); + res_comp_bot.appendNew<CubicBezier>(last_bot_hdle,comp[i],comp[i]); + }else{ + res_comp.appendNew<CubicBezier>(last_hdle,comp[i],comp[i]); + } + } + if ( fat_output.get_value() ){ + res_comp = res_comp_bot; + res_comp.setStitching(true); + res_comp.append(res_comp_top.reversed()); + } + result.concat(res_comp.toPwSb()); + } + } + return result; +} + +void +LPERoughHatches::doBeforeEffect (SPLPEItem const*/*lpeitem*/) +{ + using namespace Geom; + top_edge_variation.resetRandomizer(); + bot_edge_variation.resetRandomizer(); + top_tgt_variation.resetRandomizer(); + bot_tgt_variation.resetRandomizer(); + top_smth_variation.resetRandomizer(); + bot_smth_variation.resetRandomizer(); + dist_rdm.resetRandomizer(); + + //original_bbox(lpeitem); +} + + +void +LPERoughHatches::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + Geom::OptRect bbox = item->geometricBounds(); + Geom::Point origin(0.,0.); + Geom::Point vector(50.,0.); + if (bbox) { + origin = bbox->midpoint(); + vector = Geom::Point((*bbox)[X].extent()/4, 0.); + top_edge_variation.param_set_value( (*bbox)[Y].extent()/10, 0 ); + bot_edge_variation.param_set_value( (*bbox)[Y].extent()/10, 0 ); + top_edge_variation.write_to_SVG(); + bot_edge_variation.write_to_SVG(); + } + //direction.set_and_write_new_values(origin, vector); + //bender.param_set_and_write_new_value( origin + Geom::Point(5,0) ); + direction.set_and_write_new_values(origin + Geom::Point(0,-5), vector); + bender.set_and_write_new_values( origin, Geom::Point(5,0) ); + hatch_dist = Geom::L2(vector)/2; +} + + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-rough-hatches.h b/src/live_effects/lpe-rough-hatches.h new file mode 100644 index 0000000..8b22c64 --- /dev/null +++ b/src/live_effects/lpe-rough-hatches.h @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_ROUGH_HATCHES_H +#define INKSCAPE_LPE_ROUGH_HATCHES_H + +/** \file + * Fills an area with rough hatches. + */ + +/* + * Authors: + * JFBarraud + * + * Copyright (C) JF Barraud 2008. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/random.h" +#include "live_effects/parameter/vector.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPERoughHatches : public Effect { +public: + LPERoughHatches(LivePathEffectObject *lpeobject); + ~LPERoughHatches() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > + doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + void doOnApply(SPLPEItem const *lpeitem) override; + + void resetDefaults(SPItem const* item) override; + + void doBeforeEffect(SPLPEItem const* item) override; + + std::vector<double> + generateLevels(Geom::Interval const &domain, double x_org); + + std::vector<std::vector<Geom::Point> > + linearSnake(Geom::Piecewise<Geom::D2<Geom::SBasis> > const &f, Geom::Point const &org); + + Geom::Piecewise<Geom::D2<Geom::SBasis> > + smoothSnake(std::vector<std::vector<Geom::Point> > const &linearSnake); + +private: + double hatch_dist; + RandomParam dist_rdm; + ScalarParam growth; + //topfront,topback,bottomfront,bottomback handle scales. + ScalarParam scale_tf, scale_tb, scale_bf, scale_bb; + + RandomParam top_edge_variation; + RandomParam bot_edge_variation; + RandomParam top_tgt_variation; + RandomParam bot_tgt_variation; + RandomParam top_smth_variation; + RandomParam bot_smth_variation; + + BoolParam fat_output, do_bend; + ScalarParam stroke_width_top; + ScalarParam stroke_width_bot; + ScalarParam front_thickness, back_thickness; + + VectorParam direction; + VectorParam bender; + + LPERoughHatches(const LPERoughHatches&) = delete; + LPERoughHatches& operator=(const LPERoughHatches&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-roughen.cpp b/src/live_effects/lpe-roughen.cpp new file mode 100644 index 0000000..c568207 --- /dev/null +++ b/src/live_effects/lpe-roughen.cpp @@ -0,0 +1,568 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Roughen LPE implementation. Creates roughen paths. + */ +/* Authors: + * Jabier Arraiza Cenoz <jabier.arraiza@marker.es> + * + * Thanks to all people involved specially to Josh Andler for the idea and to the + * original extensions authors. + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-roughen.h" +#include "display/curve.h" +#include "helper/geom.h" +#include <boost/functional/hash.hpp> +#include <gtkmm.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<DivisionMethod> DivisionMethodData[] = { + { DM_SEGMENTS, N_("By number of segments"), "segments" }, + { DM_SIZE, N_("By max. segment size"), "size" } +}; +static const Util::EnumDataConverter<DivisionMethod> DMConverter(DivisionMethodData, DM_END); + +static const Util::EnumData<HandlesMethod> HandlesMethodData[] = { + { HM_ALONG_NODES, N_("Along nodes"), "along" }, + { HM_RAND, N_("Rand"), "rand" }, + { HM_RETRACT, N_("Retract"), "retract" }, + { HM_SMOOTH, N_("Smooth"), "smooth" } +}; +static const Util::EnumDataConverter<HandlesMethod> HMConverter(HandlesMethodData, HM_END); + +LPERoughen::LPERoughen(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , method(_("Method"), _("Division method"), "method", DMConverter, &wr, this, DM_SIZE) + , max_segment_size(_("Max. segment size"), _("Max. segment size"), "max_segment_size", &wr, this, 10) + , segments(_("Number of segments"), _("Number of segments"), "segments", &wr, this, 2) + , displace_x(_("Max. displacement in X"), _("Max. displacement in X"), "displace_x", &wr, this, 10.) + , displace_y(_("Max. displacement in Y"), _("Max. displacement in Y"), "displace_y", &wr, this, 10.) + , global_randomize(_("Global randomize"), _("Global randomize"), "global_randomize", &wr, this, 1.) + , handles(_("Handles"), _("Handles options"), "handles", HMConverter, &wr, this, HM_ALONG_NODES) + , shift_nodes(_("Shift nodes"), _("Shift nodes"), "shift_nodes", &wr, this, true) + , fixed_displacement(_("Fixed displacement"), _("Fixed displacement, 1/3 of segment length"), "fixed_displacement", + &wr, this, false) + , spray_tool_friendly(_("Spray Tool friendly"), _("For use with spray tool in copy mode"), "spray_tool_friendly", + &wr, this, false) +{ + registerParameter(&method); + registerParameter(&max_segment_size); + registerParameter(&segments); + registerParameter(&displace_x); + registerParameter(&displace_y); + registerParameter(&global_randomize); + registerParameter(&handles); + registerParameter(&shift_nodes); + registerParameter(&fixed_displacement); + registerParameter(&spray_tool_friendly); + displace_x.param_set_range(0., std::numeric_limits<double>::max()); + displace_y.param_set_range(0., std::numeric_limits<double>::max()); + global_randomize.param_set_range(0., std::numeric_limits<double>::max()); + max_segment_size.param_set_range(0., std::numeric_limits<double>::max()); + max_segment_size.param_set_increments(1, 1); + max_segment_size.param_set_digits(3); + segments.param_make_integer(); + segments.param_set_range(1, 9999); + segments.param_set_increments(1, 1); + seed = 0; + apply_to_clippath_and_mask = true; +} + +LPERoughen::~LPERoughen() = default; + +void LPERoughen::doOnApply(SPLPEItem const *lpeitem) +{ + Geom::OptRect bbox = lpeitem->bounds(SPItem::GEOMETRIC_BBOX); + if (bbox) { + std::vector<Parameter *>::iterator it = param_vector.begin(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + while (it != param_vector.end()) { + Parameter *param = *it; + const gchar *key = param->param_key.c_str(); + Glib::ustring pref_path = (Glib::ustring) "/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring) "/" + (Glib::ustring)key; + + + bool valid = prefs->getEntry(pref_path).isValid(); + Glib::ustring displace_x_str = Glib::ustring::format((*bbox).width() / 150.0); + Glib::ustring displace_y_str = Glib::ustring::format((*bbox).height() / 150.0); + Glib::ustring max_segment_size_str = Glib::ustring::format(std::min((*bbox).height(), (*bbox).width()) / 50.0); + if (!valid) { + if (strcmp(key, "max_segment_size") == 0) { + param->param_readSVGValue(max_segment_size_str.c_str()); + } else if (strcmp(key, "displace_x") == 0) { + param->param_readSVGValue(displace_x_str.c_str()); + } else if (strcmp(key, "displace_y") == 0) { + param->param_readSVGValue(displace_y_str.c_str()); + } + } + ++it; + } + } + lpeversion.param_setValue("1.2", true); +} + +void LPERoughen::doBeforeEffect(SPLPEItem const *lpeitem) +{ + if (spray_tool_friendly && seed == 0 && lpeitem->getId()) { + std::string id_item(lpeitem->getId()); + long seed = static_cast<long>(boost::hash_value(id_item)); + global_randomize.param_set_value(global_randomize.get_value(), seed); + } + displace_x.resetRandomizer(); + displace_y.resetRandomizer(); + global_randomize.resetRandomizer(); + if (lpeversion.param_getSVGValue() < "1.1") { + srand(1); + } else { + displace_x.param_set_randomsign(true); + displace_y.param_set_randomsign(true); + } +} + +Gtk::Widget *LPERoughen::newWidget() +{ + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + if (param->param_key == "method") { + Gtk::Label *method_label = Gtk::manage( + new Gtk::Label(Glib::ustring(_("<b>Add nodes</b> Subdivide each segment")), Gtk::ALIGN_START)); + method_label->set_use_markup(true); + vbox->pack_start(*method_label, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + if (param->param_key == "displace_x") { + Gtk::Label *displace_x_label = Gtk::manage( + new Gtk::Label(Glib::ustring(_("<b>Jitter nodes</b> Move nodes/handles")), Gtk::ALIGN_START)); + displace_x_label->set_use_markup(true); + vbox->pack_start(*displace_x_label, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + if (param->param_key == "global_randomize") { + Gtk::Label *global_rand = Gtk::manage(new Gtk::Label( + Glib::ustring(_("<b>Extra roughen</b> Add an extra layer of rough")), Gtk::ALIGN_START)); + global_rand->set_use_markup(true); + vbox->pack_start(*global_rand, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + if (param->param_key == "handles") { + Gtk::Label *options = Gtk::manage( + new Gtk::Label(Glib::ustring(_("<b>Options</b> Modify options to rough")), Gtk::ALIGN_START)); + options->set_use_markup(true); + vbox->pack_start(*options, false, false, 2); + vbox->pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL)), + Gtk::PACK_EXPAND_WIDGET); + } + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + ++it; + } + if (Gtk::Widget *widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +double LPERoughen::sign(double random_number) +{ + if (lpeversion.param_getSVGValue() < "1.1") { + if (rand() % 100 < 49) { + random_number *= -1.; + } + } + return random_number; +} + +Geom::Point LPERoughen::randomize(double max_length, bool is_node) +{ + double factor = 1.0 / 3.0; + if (is_node) { + factor = 1.0; + } + double displace_x_parsed = displace_x * global_randomize * factor; + double displace_y_parsed = displace_y * global_randomize * factor; + Geom::Point output = Geom::Point(sign(displace_x_parsed), sign(displace_y_parsed)); + if (fixed_displacement) { + Geom::Ray ray(Geom::Point(0, 0), output); + output = Geom::Point::polar(ray.angle(), max_length); + } + return output; +} + +void LPERoughen::doEffect(SPCurve *curve) +{ + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(curve->get_pathvector()); + curve->reset(); + for (const auto &path_it : original_pathv) { + if (path_it.empty()) + continue; + + Geom::Path::const_iterator curve_it1 = path_it.begin(); + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); + Geom::Path::const_iterator curve_endit = path_it.end_default(); + auto nCurve = std::make_unique<SPCurve>(); + Geom::Point prev(0, 0); + Geom::Point last_move(0, 0); + nCurve->moveto(curve_it1->initialPoint()); + if (path_it.closed()) { + const Geom::Curve &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + while (curve_it1 != curve_endit) { + Geom::CubicBezier const *cubic = nullptr; + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + if (cubic) { + nCurve->curveto((*cubic)[1] + last_move, (*cubic)[2], curve_it1->finalPoint()); + } else { + nCurve->lineto(curve_it1->finalPoint()); + } + last_move = Geom::Point(0, 0); + double length = curve_it1->length(0.01); + std::size_t splits = 0; + if (method == DM_SEGMENTS) { + splits = segments; + } else { + splits = ceil(length / max_segment_size); + } + auto const original = std::unique_ptr<Geom::Curve>(nCurve->last_segment()->duplicate()); + for (unsigned int t = 1; t <= splits; t++) { + if (t == splits && splits != 1) { + continue; + } + std::unique_ptr<SPCurve> tmp; + if (splits == 1) { + tmp = jitter(nCurve->last_segment(), prev, last_move); + } else { + bool last = false; + if (t == splits - 1) { + last = true; + } + double time = + Geom::nearest_time(original->pointAt((1. / (double)splits) * t), *nCurve->last_segment()); + tmp = addNodesAndJitter(nCurve->last_segment(), prev, last_move, time, last); + } + assert(tmp); + if (nCurve->get_segment_count() > 1) { + nCurve->backspace(); + nCurve->append_continuous(*tmp, 0.001); + } else { + nCurve = std::move(tmp); + } + } + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + if (handles == HM_SMOOTH && curve_it1 == curve_endit) { + SPCurve *out = new SPCurve(); + nCurve = nCurve->create_reverse(); + Geom::CubicBezier const *cubic_start = dynamic_cast<Geom::CubicBezier const *>(nCurve->first_segment()); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(nCurve->last_segment()); + Geom::Point oposite = nCurve->first_segment()->pointAt(1.0 / 3.0); + if (cubic_start) { + Geom::Ray ray((*cubic_start)[1], (*cubic_start)[0]); + double dist = Geom::distance((*cubic_start)[1], (*cubic_start)[0]); + oposite = Geom::Point::polar(ray.angle(), dist) + (*cubic_start)[0]; + } + if (cubic) { + out->moveto((*cubic)[0]); + out->curveto((*cubic)[1], oposite, (*cubic)[3]); + } else { + out->moveto(nCurve->last_segment()->initialPoint()); + out->curveto(nCurve->last_segment()->initialPoint(), oposite, nCurve->last_segment()->finalPoint()); + } + nCurve->backspace(); + nCurve->append_continuous(*out, 0.001); + nCurve = nCurve->create_reverse(); + } + if (handles == HM_ALONG_NODES && curve_it1 == curve_endit) { + SPCurve *out = new SPCurve(); + nCurve = nCurve->create_reverse(); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(nCurve->last_segment()); + if (cubic) { + out->moveto((*cubic)[0]); + out->curveto((*cubic)[1], (*cubic)[2] - ((*cubic)[3] - nCurve->first_segment()->initialPoint()), + (*cubic)[3]); + nCurve->backspace(); + nCurve->append_continuous(*out, 0.001); + } + nCurve = nCurve->create_reverse(); + } + nCurve->move_endpoints(nCurve->last_segment()->finalPoint(), nCurve->last_segment()->finalPoint()); + nCurve->closepath_current(); + } + curve->append(*nCurve); + } +} + +std::unique_ptr<SPCurve> LPERoughen::addNodesAndJitter(Geom::Curve const *A, Geom::Point &prev, Geom::Point &last_move, double t, + bool last) +{ + auto out = std::make_unique<SPCurve>(); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*A); + double max_length = Geom::distance(A->initialPoint(), A->pointAt(t)) / 3.0; + Geom::Point point_a1(0, 0); + Geom::Point point_a2(0, 0); + Geom::Point point_a3(0, 0); + Geom::Point point_b1(0, 0); + Geom::Point point_b2(0, 0); + Geom::Point point_b3(0, 0); + if (shift_nodes) { + point_a3 = randomize(max_length, true); + if (last) { + point_b3 = randomize(max_length, true); + } + } + if (handles == HM_RAND || handles == HM_SMOOTH) { + point_a1 = randomize(max_length); + point_a2 = randomize(max_length); + point_b1 = randomize(max_length); + if (last) { + point_b2 = randomize(max_length); + } + } else { + point_a2 = point_a3; + point_b1 = point_a3; + if (last) { + point_b2 = point_b3; + } + } + if (handles == HM_SMOOTH) { + if (cubic) { + std::pair<Geom::CubicBezier, Geom::CubicBezier> div = cubic->subdivide(t); + std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + Geom::Ray ray(seg1[3] + point_a3, seg2[1] + point_a3); + double length = max_length; + if (!fixed_displacement) { + length = Geom::distance(seg1[3] + point_a3, seg2[1] + point_a3); + } + point_b1 = seg1[3] + point_a3 + Geom::Point::polar(ray.angle(), length); + point_b2 = seg2[2]; + point_b3 = seg2[3] + point_b3; + point_a3 = seg1[3] + point_a3; + ray.setPoints(prev, A->initialPoint()); + point_a1 = A->initialPoint() + Geom::Point::polar(ray.angle(), max_length); + if (last) { + Geom::Path b2(point_b3); + b2.appendNew<Geom::LineSegment>(point_a3); + length = max_length; + ray.setPoints(point_b3, point_b2); + if (!fixed_displacement) { + length = Geom::distance(b2.pointAt(1.0 / 3.0), point_b3); + } + point_b2 = point_b3 + Geom::Point::polar(ray.angle(), length); + } + ray.setPoints(point_b1, point_a3); + point_a2 = point_a3 + Geom::Point::polar(ray.angle(), max_length); + if (last) { + prev = point_b2; + } else { + prev = point_a2; + } + out->moveto(seg1[0]); + out->curveto(point_a1, point_a2, point_a3); + out->curveto(point_b1, point_b2, point_b3); + } else { + Geom::Ray ray(A->pointAt(t) + point_a3, A->pointAt(t + (t / 3))); + double length = max_length; + if (!fixed_displacement) { + length = Geom::distance(A->pointAt(t) + point_a3, A->pointAt(t + (t / 3))); + } + point_b1 = A->pointAt(t) + point_a3 + Geom::Point::polar(ray.angle(), length); + point_b2 = A->pointAt(t + ((t / 3) * 2)); + point_b3 = A->finalPoint() + point_b3; + point_a3 = A->pointAt(t) + point_a3; + ray.setPoints(prev, A->initialPoint()); + point_a1 = A->initialPoint() + Geom::Point::polar(ray.angle(), max_length); + if (prev == Geom::Point(0, 0)) { + point_a1 = randomize(max_length); + } + if (last) { + Geom::Path b2(point_b3); + b2.appendNew<Geom::LineSegment>(point_a3); + length = max_length; + ray.setPoints(point_b3, point_b2); + if (!fixed_displacement) { + length = Geom::distance(b2.pointAt(1.0 / 3.0), point_b3); + } + point_b2 = point_b3 + Geom::Point::polar(ray.angle(), length); + } + ray.setPoints(point_b1, point_a3); + point_a2 = point_a3 + Geom::Point::polar(ray.angle(), max_length); + if (last) { + prev = point_b2; + } else { + prev = point_a2; + } + out->moveto(A->initialPoint()); + out->curveto(point_a1, point_a2, point_a3); + out->curveto(point_b1, point_b2, point_b3); + } + } else if (handles == HM_RETRACT) { + out->moveto(A->initialPoint()); + out->lineto(A->pointAt(t) + point_a3); + if (cubic && !last) { + std::pair<Geom::CubicBezier, Geom::CubicBezier> div = cubic->subdivide(t); + std::vector<Geom::Point> seg2 = div.second.controlPoints(); + out->curveto(seg2[1], seg2[2], seg2[3]); + } else { + out->lineto(A->finalPoint() + point_b3); + } + } else if (handles == HM_ALONG_NODES) { + if (cubic) { + std::pair<Geom::CubicBezier, Geom::CubicBezier> div = cubic->subdivide(t); + std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + out->moveto(seg1[0]); + out->curveto(seg1[1] + last_move, seg1[2] + point_a3, seg1[3] + point_a3); + last_move = point_a3; + if (last) { + last_move = point_b3; + } + out->curveto(seg2[1] + point_a3, seg2[2] + point_b3, seg2[3] + point_b3); + } else { + out->moveto(A->initialPoint()); + out->lineto(A->pointAt(t) + point_a3); + out->lineto(A->finalPoint() + point_b3); + } + } else if (handles == HM_RAND) { + if (cubic) { + std::pair<Geom::CubicBezier, Geom::CubicBezier> div = cubic->subdivide(t); + std::vector<Geom::Point> seg1 = div.first.controlPoints(), seg2 = div.second.controlPoints(); + out->moveto(seg1[0]); + out->curveto(seg1[1] + point_a1, seg1[2] + point_a2 + point_a3, seg1[3] + point_a3); + out->curveto(seg2[1] + point_a3 + point_b1, seg2[2] + point_b2 + point_b3, seg2[3] + point_b3); + } else { + out->moveto(A->initialPoint()); + out->lineto(A->pointAt(t) + point_a3); + out->lineto(A->finalPoint() + point_b3); + } + } + return out; +} + +std::unique_ptr<SPCurve> LPERoughen::jitter(Geom::Curve const *A, Geom::Point &prev, Geom::Point &last_move) +{ + auto out = std::make_unique<SPCurve>(); + Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*A); + double max_length = Geom::distance(A->initialPoint(), A->finalPoint()) / 3.0; + Geom::Point point_a1(0, 0); + Geom::Point point_a2(0, 0); + Geom::Point point_a3(0, 0); + if (shift_nodes) { + point_a3 = randomize(max_length, true); + } + if (handles == HM_RAND || handles == HM_SMOOTH) { + point_a1 = randomize(max_length); + point_a2 = randomize(max_length); + } + if (handles == HM_SMOOTH) { + if (cubic) { + Geom::Ray ray(prev, A->initialPoint()); + point_a1 = Geom::Point::polar(ray.angle(), max_length); + if (prev == Geom::Point(0, 0)) { + point_a1 = A->pointAt(1.0 / 3.0) + randomize(max_length); + } + ray.setPoints((*cubic)[3] + point_a3, (*cubic)[2] + point_a3); + if (lpeversion.param_getSVGValue() < "1.1") { + point_a2 = randomize(max_length, ray.angle()); + } else { + point_a2 = randomize(max_length, false); + } + prev = (*cubic)[2] + point_a2; + out->moveto((*cubic)[0]); + out->curveto((*cubic)[0] + point_a1, (*cubic)[2] + point_a2 + point_a3, (*cubic)[3] + point_a3); + } else { + Geom::Ray ray(prev, A->initialPoint()); + point_a1 = Geom::Point::polar(ray.angle(), max_length); + if (prev == Geom::Point(0, 0)) { + point_a1 = A->pointAt(1.0 / 3.0) + randomize(max_length); + } + ray.setPoints(A->finalPoint() + point_a3, A->pointAt((1.0 / 3.0) * 2) + point_a3); + if (lpeversion.param_getSVGValue() < "1.1") { + point_a2 = randomize(max_length, ray.angle()); + } else { + point_a2 = randomize(max_length, false); + } + prev = A->pointAt((1.0 / 3.0) * 2) + point_a2 + point_a3; + out->moveto(A->initialPoint()); + out->curveto(A->initialPoint() + point_a1, A->pointAt((1.0 / 3.0) * 2) + point_a2 + point_a3, + A->finalPoint() + point_a3); + } + } else if (handles == HM_RETRACT) { + out->moveto(A->initialPoint()); + out->lineto(A->finalPoint() + point_a3); + } else if (handles == HM_ALONG_NODES) { + if (cubic) { + out->moveto((*cubic)[0]); + out->curveto((*cubic)[1] + last_move, (*cubic)[2] + point_a3, (*cubic)[3] + point_a3); + last_move = point_a3; + } else { + out->moveto(A->initialPoint()); + out->lineto(A->finalPoint() + point_a3); + } + } else if (handles == HM_RAND) { + out->moveto(A->initialPoint()); + out->curveto(A->pointAt(0.3333) + point_a1, A->pointAt(0.6666) + point_a2 + point_a3, + A->finalPoint() + point_a3); + } + return out; +} + +Geom::Point LPERoughen::tPoint(Geom::Point A, Geom::Point B, double t) +{ + using Geom::X; + using Geom::Y; + return Geom::Point(A[X] + t * (B[X] - A[X]), A[Y] + t * (B[Y] - A[Y])); +} + +}; // namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-roughen.h b/src/live_effects/lpe-roughen.h new file mode 100644 index 0000000..8940f93 --- /dev/null +++ b/src/live_effects/lpe-roughen.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Roughen LPE effect, see lpe-roughen.cpp. + */ +/* Authors: + * Jabier Arraiza Cenoz <jabier.arraiza@marker.es> + * + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_ROUGHEN_H +#define INKSCAPE_LPE_ROUGHEN_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/random.h" + +#include <memory> + +namespace Inkscape { +namespace LivePathEffect { + +enum DivisionMethod { + DM_SEGMENTS, + DM_SIZE, + DM_END +}; + +enum HandlesMethod { + HM_ALONG_NODES, + HM_RAND, + HM_RETRACT, + HM_SMOOTH, + HM_END +}; + + +class LPERoughen : public Effect { + +public: + LPERoughen(LivePathEffectObject *lpeobject); + ~LPERoughen() override; + + void doOnApply(SPLPEItem const *lpeitem) override; + void doEffect(SPCurve *curve) override; + virtual double sign(double randNumber); + virtual Geom::Point randomize(double max_length, bool is_node = false); + void doBeforeEffect(SPLPEItem const * lpeitem) override; + virtual Geom::Point tPoint(Geom::Point A, Geom::Point B, double t = 0.5); + Gtk::Widget *newWidget() override; + +private: + std::unique_ptr<SPCurve> addNodesAndJitter(Geom::Curve const *A, Geom::Point &prev, Geom::Point &last_move, + double t, bool last); + std::unique_ptr<SPCurve> jitter(Geom::Curve const *A, Geom::Point &prev, Geom::Point &last_move); + + EnumParam<DivisionMethod> method; + ScalarParam max_segment_size; + ScalarParam segments; + RandomParam displace_x; + RandomParam displace_y; + RandomParam global_randomize; + EnumParam<HandlesMethod> handles; + BoolParam shift_nodes; + BoolParam fixed_displacement; + BoolParam spray_tool_friendly; + long seed; + LPERoughen(const LPERoughen &) = delete; + LPERoughen &operator=(const LPERoughen &) = delete; + +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-ruler.cpp b/src/live_effects/lpe-ruler.cpp new file mode 100644 index 0000000..6005083 --- /dev/null +++ b/src/live_effects/lpe-ruler.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <ruler> implementation, see lpe-ruler.cpp. + */ + +/* + * Authors: + * Maximilian Albert + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-ruler.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +static const Util::EnumData<MarkDirType> MarkDirData[] = { + {MARKDIR_LEFT , N_("Left"), "left"}, + {MARKDIR_RIGHT , N_("Right"), "right"}, + {MARKDIR_BOTH , N_("Both"), "both"}, +}; +static const Util::EnumDataConverter<MarkDirType> MarkDirTypeConverter(MarkDirData, sizeof(MarkDirData)/sizeof(*MarkDirData)); + +static const Util::EnumData<BorderMarkType> BorderMarkData[] = { + {BORDERMARK_NONE , NC_("Border mark", "None"), "none"}, + {BORDERMARK_START , N_("Start"), "start"}, + {BORDERMARK_END , N_("End"), "end"}, + {BORDERMARK_BOTH , N_("Both"), "both"}, +}; +static const Util::EnumDataConverter<BorderMarkType> BorderMarkTypeConverter(BorderMarkData, sizeof(BorderMarkData)/sizeof(*BorderMarkData)); + +LPERuler::LPERuler(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + mark_distance(_("_Mark distance:"), _("Distance between successive ruler marks"), "mark_distance", &wr, this, 20.0), + unit(_("Unit:"), _("Unit"), "unit", &wr, this), + mark_length(_("Ma_jor length:"), _("Length of major ruler marks"), "mark_length", &wr, this, 14.0), + minor_mark_length(_("Mino_r length:"), _("Length of minor ruler marks"), "minor_mark_length", &wr, this, 7.0), + major_mark_steps(_("Major steps_:"), _("Draw a major mark every ... steps"), "major_mark_steps", &wr, this, 5), + shift(_("Shift marks _by:"), _("Shift marks by this many steps"), "shift", &wr, this, 0), + mark_dir(_("Mark direction:"), _("Direction of marks (when viewing along the path from start to end)"), "mark_dir", MarkDirTypeConverter, &wr, this, MARKDIR_LEFT), + offset(_("_Offset:"), _("Offset of first mark"), "offset", &wr, this, 0.0), + border_marks(_("Border marks:"), _("Choose whether to draw marks at the beginning and end of the path"), "border_marks", BorderMarkTypeConverter, &wr, this, BORDERMARK_BOTH) +{ + registerParameter(&unit); + registerParameter(&mark_distance); + registerParameter(&mark_length); + registerParameter(&minor_mark_length); + registerParameter(&major_mark_steps); + registerParameter(&shift); + registerParameter(&offset); + registerParameter(&mark_dir); + registerParameter(&border_marks); + + major_mark_steps.param_make_integer(); + major_mark_steps.param_set_range(1, 1000); + shift.param_make_integer(); + + mark_length.param_set_increments(1.0, 10.0); + minor_mark_length.param_set_increments(1.0, 10.0); + offset.param_set_increments(1.0, 10.0); +} + +LPERuler::~LPERuler() += default; + +Geom::Point LPERuler::n_major; +Geom::Point LPERuler::n_minor; + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPERuler::ruler_mark(Geom::Point const &A, Geom::Point const &n, MarkType const &marktype) +{ + using namespace Geom; + + double real_mark_length = mark_length; + SPDocument *document = getSPDoc(); + if (document) { + real_mark_length = Inkscape::Util::Quantity::convert(real_mark_length, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + double real_minor_mark_length = minor_mark_length; + if (document) { + real_minor_mark_length = Inkscape::Util::Quantity::convert(real_minor_mark_length, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + n_major = real_mark_length * n; + n_minor = real_minor_mark_length * n; + if (mark_dir == MARKDIR_BOTH) { + n_major = n_major * 0.5; + n_minor = n_minor * 0.5; + } + + Point C, D; + switch (marktype) { + case MARK_MAJOR: + C = A; + D = A + n_major; + if (mark_dir == MARKDIR_BOTH) + C -= n_major; + break; + case MARK_MINOR: + C = A; + D = A + n_minor; + if (mark_dir == MARKDIR_BOTH) + C -= n_minor; + break; + default: + // do nothing + break; + } + + Piecewise<D2<SBasis> > seg(D2<SBasis>(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + return seg; +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPERuler::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + const int mminterval = static_cast<int>(major_mark_steps); + const int i_shift = static_cast<int>(shift) % mminterval; + int sign = (mark_dir == MARKDIR_RIGHT ? 1 : -1 ); + + Piecewise<D2<SBasis> >output(pwd2_in); + Piecewise<D2<SBasis> >speed = derivative(pwd2_in); + Piecewise<SBasis> arclength = arcLengthSb(pwd2_in); + double totlength = arclength.lastValue(); + + //find at which times to draw a mark: + std::vector<double> s_cuts; + + double real_mark_distance = mark_distance; + SPDocument *document = getSPDoc(); + if (document) { + real_mark_distance = Inkscape::Util::Quantity::convert(real_mark_distance, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + double real_offset = offset; + if (document) { + real_offset = Inkscape::Util::Quantity::convert(real_offset, unit.get_abbreviation(), document->getDisplayUnit()->abbr.c_str()); + } + for (double s = real_offset; s<totlength; s+=real_mark_distance){ + s_cuts.push_back(s); + } + std::vector<std::vector<double> > roots = multi_roots(arclength, s_cuts); + std::vector<double> t_cuts; + for (auto & root : roots){ + //FIXME: 2geom multi_roots solver seem to sometimes "repeat" solutions. + //Here, we are supposed to have one and only one solution for each s. + if(root.size()>0) + t_cuts.push_back(root[0]); + } + //draw the marks + for (size_t i = 0; i < t_cuts.size(); i++) { + Point A = pwd2_in(t_cuts[i]); + Point n = rot90(unit_vector(speed(t_cuts[i])))*sign; + if (static_cast<int>(i % mminterval) == i_shift) { + output.concat (ruler_mark(A, n, MARK_MAJOR)); + } else { + output.concat (ruler_mark(A, n, MARK_MINOR)); + } + } + //eventually draw a mark at start + if ((border_marks == BORDERMARK_START || border_marks == BORDERMARK_BOTH) && (offset != 0.0 || i_shift != 0)){ + Point A = pwd2_in.firstValue(); + Point n = rot90(unit_vector(speed.firstValue()))*sign; + output.concat (ruler_mark(A, n, MARK_MAJOR)); + } + //eventually draw a mark at end + if (border_marks == BORDERMARK_END || border_marks == BORDERMARK_BOTH){ + Point A = pwd2_in.lastValue(); + Point n = rot90(unit_vector(speed.lastValue()))*sign; + //speed.lastValue() is sometimes wrong when the path is closed: a tiny line seg might added at the end to fix rounding errors... + //TODO: Find a better fix!! (How do we know if the path was closed?) + if ( A == pwd2_in.firstValue() && + speed.segs.size() > 1 && + speed.segs.back()[X].size() <= 1 && + speed.segs.back()[Y].size() <= 1 && + speed.segs.back()[X].tailError(0) <= 1e-10 && + speed.segs.back()[Y].tailError(0) <= 1e-10 + ){ + n = rot90(unit_vector(speed.segs[speed.segs.size()-2].at1()))*sign; + } + output.concat (ruler_mark(A, n, MARK_MAJOR)); + } + + return output; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-ruler.h b/src/live_effects/lpe-ruler.h new file mode 100644 index 0000000..3766717 --- /dev/null +++ b/src/live_effects/lpe-ruler.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_RULER_H +#define INKSCAPE_LPE_RULER_H + +/** \file + * LPE <ruler> implementation, see lpe-ruler.cpp. + */ + +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/unit.h" + +namespace Inkscape { +namespace LivePathEffect { + +enum MarkType { + MARK_MAJOR, + MARK_MINOR +}; + +enum MarkDirType { + MARKDIR_LEFT, + MARKDIR_RIGHT, + MARKDIR_BOTH, +}; + +enum BorderMarkType { + BORDERMARK_NONE, + BORDERMARK_START, + BORDERMARK_END, + BORDERMARK_BOTH, +}; + +class LPERuler : public Effect { +public: + LPERuler(LivePathEffectObject *lpeobject); + ~LPERuler() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + +private: + Geom::Piecewise<Geom::D2<Geom::SBasis> > ruler_mark(Geom::Point const &A, Geom::Point const &n, MarkType const &marktype); + + ScalarParam mark_distance; + UnitParam unit; + ScalarParam mark_length; + ScalarParam minor_mark_length; + ScalarParam major_mark_steps; + ScalarParam shift; + EnumParam<MarkDirType> mark_dir; + ScalarParam offset; + EnumParam<BorderMarkType> border_marks; + + static Geom::Point n_major, n_minor; // used for internal computations + + LPERuler(const LPERuler&) = delete; + LPERuler& operator=(const LPERuler&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-show_handles.cpp b/src/live_effects/lpe-show_handles.cpp new file mode 100644 index 0000000..cc6e36c --- /dev/null +++ b/src/live_effects/lpe-show_handles.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Jabier Arraiza Cenoz + * + * Copyright (C) Jabier Arraiza Cenoz 2014 <jabier.arraiza@marker.es> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> +#include "live_effects/lpe-show_handles.h" +#include <2geom/sbasis-to-bezier.h> +#include <2geom/svg-path-parser.h> +#include "helper/geom.h" +#include "desktop-style.h" +#include "display/curve.h" +#include "svg/svg.h" + +#include "object/sp-shape.h" +#include "style.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPEShowHandles::LPEShowHandles(LivePathEffectObject *lpeobject) + : Effect(lpeobject), + nodes(_("Show nodes"), _("Show nodes"), "nodes", &wr, this, true), + handles(_("Show handles"), _("Show handles"), "handles", &wr, this, true), + original_path(_("Show path"), _("Show path"), "original_path", &wr, this, true), + show_center_node(_("Show center of node"), _("Show center of node"), "show_center_node", &wr, this, false), + original_d(_("Show original"), _("Show original"), "original_d", &wr, this, false), + scale_nodes_and_handles(_("Scale nodes and handles"), _("Scale nodes and handles"), "scale_nodes_and_handles", &wr, this, 10) +{ + registerParameter(&nodes); + registerParameter(&handles); + registerParameter(&original_path); + registerParameter(&show_center_node); + registerParameter(&original_d); + registerParameter(&scale_nodes_and_handles); + scale_nodes_and_handles.param_set_range(0, 500.); + scale_nodes_and_handles.param_set_increments(1, 1); + scale_nodes_and_handles.param_set_digits(2); + stroke_width = 1.0; +} + +bool LPEShowHandles::alerts_off = false; + +/** + * Sets default styles to element + * this permanently remove.some styles of the element + */ + +void LPEShowHandles::doOnApply(SPLPEItem const* lpeitem) +{ + if(!alerts_off) { + char *msg = _("The \"show handles\" path effect will remove any custom style on the object you are applying it to. If this is not what you want, click Cancel."); + Gtk::MessageDialog dialog(msg, false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_OK_CANCEL, true); + gint response = dialog.run(); + alerts_off = true; + if(response == GTK_RESPONSE_CANCEL) { + SPLPEItem* item = const_cast<SPLPEItem*>(lpeitem); + item->removeCurrentPathEffect(false); + return; + } + } + SPLPEItem* item = const_cast<SPLPEItem*>(lpeitem); + SPCSSAttr *css = sp_repr_css_attr_new (); + sp_repr_css_set_property (css, "stroke", "black"); + sp_repr_css_set_property (css, "stroke-width", "1"); + sp_repr_css_set_property (css, "stroke-linecap", "butt"); + sp_repr_css_set_property(css, "fill", "none"); + + sp_desktop_apply_css_recursive(item, css, true); + sp_repr_css_attr_unref (css); +} + +void LPEShowHandles::doBeforeEffect (SPLPEItem const* lpeitem) +{ + stroke_width = lpeitem->style->stroke_width.computed; +} + +Geom::PathVector LPEShowHandles::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + Geom::PathVector original_pathv = pathv_to_linear_and_cubic_beziers(path_in); + if(original_path) { + for (const auto & i : path_in) { + path_out.push_back(i); + } + } + if(!outline_path.empty()) { + outline_path.clear(); + } + if (original_d) { + auto shape_curve = SPCurve::copy(current_shape->curveForEdit()); + if (shape_curve) { + Geom::PathVector original_curve = shape_curve->get_pathvector(); + if(original_path) { + for (const auto & i : original_curve) { + path_out.push_back(i); + } + } + original_pathv.insert(original_pathv.end(), original_curve.begin(), original_curve.end()); + } + generateHelperPath(original_pathv); + } else { + generateHelperPath(original_pathv); + } + for (const auto & i : outline_path) { + path_out.push_back(i); + } + + return path_out; +} + +void +LPEShowHandles::generateHelperPath(Geom::PathVector result) +{ + if(!handles && !nodes) { + return; + } + + Geom::CubicBezier const *cubic = nullptr; + for (auto & path_it : result) { + //Si está vacÃo... + if (path_it.empty()) { + continue; + } + //Itreadores + Geom::Path::iterator curve_it1 = path_it.begin(); // incoming curve + Geom::Path::iterator curve_it2 = ++(path_it.begin()); // outgoing curve + Geom::Path::iterator curve_endit = path_it.end_default(); // this determines when the loop has to stop + + if (path_it.closed()) { + // if the path is closed, maybe we have to stop a bit earlier because the + // closing line segment has zerolength. + Geom::Curve const &closingline = path_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! + curve_endit = path_it.end_open(); + } + } + if(nodes) { + Geom::NodeType nodetype = Geom::NODE_CUSP; + if(path_it.closed()) { + nodetype = Geom::get_nodetype(path_it.finalCurve(), *curve_it1); + } + drawNode(curve_it1->initialPoint(), nodetype); + } + while (curve_it1 != curve_endit) { + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + if (cubic) { + if(handles) { + if(!are_near((*cubic)[0],(*cubic)[1])) { + drawHandle((*cubic)[1]); + drawHandleLine((*cubic)[0],(*cubic)[1]); + } + if(!are_near((*cubic)[3],(*cubic)[2])) { + drawHandle((*cubic)[2]); + drawHandleLine((*cubic)[3],(*cubic)[2]); + } + } + } + if(nodes && (curve_it2 != curve_endit || !path_it.closed())) { + Geom::NodeType nodetype = Geom::get_nodetype(*curve_it1, *curve_it2); + drawNode(curve_it1->finalPoint(), nodetype); + } + ++curve_it1; + if(curve_it2 != curve_endit) { + ++curve_it2; + } + } + } +} + +void +LPEShowHandles::drawNode(Geom::Point p, Geom::NodeType nodetype) +{ + if(stroke_width * scale_nodes_and_handles > 0.0) { + Geom::Rotate rotate = Geom::Rotate(0); + if ( nodetype == Geom::NODE_CUSP) { + rotate = Geom::Rotate::from_degrees(45); + } + double diameter = stroke_width * scale_nodes_and_handles; + char const * svgd; + if (show_center_node) { + svgd = "M 0.05,0 A 0.05,0.05 0 0 1 0,0.05 0.05,0.05 0 0 1 -0.05,0 0.05,0.05 0 0 1 0,-0.05 0.05,0.05 0 0 1 0.05,0 Z M -0.5,-0.5 0.5,-0.5 0.5,0.5 -0.5,0.5 Z"; + } else { + svgd = "M -0.5,-0.5 0.5,-0.5 0.5,0.5 -0.5,0.5 Z"; + } + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= rotate * Geom::Scale(diameter) * Geom::Translate(p); + outline_path.push_back(pathv[0]); + if (show_center_node) { + outline_path.push_back(pathv[1]); + } + } +} + +void +LPEShowHandles::drawHandle(Geom::Point p) +{ + if(stroke_width * scale_nodes_and_handles > 0.0) { + double diameter = stroke_width * scale_nodes_and_handles; + char const * svgd; + svgd = "M 0.7,0.35 A 0.35,0.35 0 0 1 0.35,0.7 0.35,0.35 0 0 1 0,0.35 0.35,0.35 0 0 1 0.35,0 0.35,0.35 0 0 1 0.7,0.35 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Geom::Scale (diameter) * Geom::Translate(p - Geom::Point(diameter * 0.35,diameter * 0.35)); + outline_path.push_back(pathv[0]); + } +} + + +void +LPEShowHandles::drawHandleLine(Geom::Point p,Geom::Point p2) +{ + Geom::Path path; + double diameter = stroke_width * scale_nodes_and_handles; + if(diameter > 0.0 && Geom::distance(p,p2) > (diameter * 0.35)) { + Geom::Ray ray2(p, p2); + p2 = p2 - Geom::Point::polar(ray2.angle(),(diameter * 0.35)); + } + path.start( p ); + path.appendNew<Geom::LineSegment>( p2 ); + outline_path.push_back(path); +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-show_handles.h b/src/live_effects/lpe-show_handles.h new file mode 100644 index 0000000..865034e --- /dev/null +++ b/src/live_effects/lpe-show_handles.h @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_SHOW_HANDLES_H +#define INKSCAPE_LPE_SHOW_HANDLES_H + +/* + * Authors: + * Jabier Arraiza Cenoz + * + * Copyright (C) Jabier Arraiza Cenoz 2014 <jabier.arraiza@marker.es> + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "helper/geom-nodetype.h" +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEShowHandles : public Effect , GroupBBoxEffect { + +public: + LPEShowHandles(LivePathEffectObject *lpeobject); + ~LPEShowHandles() override = default; + + void doOnApply(SPLPEItem const* lpeitem) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + virtual void generateHelperPath(Geom::PathVector result); + + virtual void drawNode(Geom::Point p, Geom::NodeType nodetype); + + virtual void drawHandle(Geom::Point p); + + virtual void drawHandleLine(Geom::Point p,Geom::Point p2); + +protected: + + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + +private: + + BoolParam nodes; + BoolParam handles; + BoolParam original_path; + BoolParam original_d; + BoolParam show_center_node; + ScalarParam scale_nodes_and_handles; + double stroke_width; + static bool alerts_off; + + Geom::PathVector outline_path; + + LPEShowHandles(const LPEShowHandles &) = delete; + LPEShowHandles &operator=(const LPEShowHandles &) = delete; + +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape +#endif + +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/live_effects/lpe-simplify.cpp b/src/live_effects/lpe-simplify.cpp new file mode 100644 index 0000000..ccd1b6b --- /dev/null +++ b/src/live_effects/lpe-simplify.cpp @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm.h> + +#include "live_effects/lpe-simplify.h" + +#include "display/curve.h" +#include "helper/geom.h" +#include "path/path-util.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "ui/tools/node-tool.h" + +#include <2geom/svg-path-parser.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPESimplify::LPESimplify(LivePathEffectObject *lpeobject) + : Effect(lpeobject) + , steps(_("Steps:"), _("Change number of simplify steps "), "steps", &wr, this, 1) + , threshold(_("Roughly threshold:"), _("Roughly threshold:"), "threshold", &wr, this, 0.002) + , smooth_angles(_("Smooth angles:"), _("Max degree difference on handles to perform a smooth"), "smooth_angles", + &wr, this, 0.) + , helper_size(_("Helper size:"), _("Helper size"), "helper_size", &wr, this, 5) + , simplify_individual_paths(_("Paths separately"), _("Simplifying paths (separately)"), "simplify_individual_paths", + &wr, this, false, "", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")) + , simplify_just_coalesce(_("Just coalesce"), _("Simplify just coalesce"), "simplify_just_coalesce", &wr, this, + false, "", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")) +{ + registerParameter(&steps); + registerParameter(&threshold); + registerParameter(&smooth_angles); + registerParameter(&helper_size); + registerParameter(&simplify_individual_paths); + registerParameter(&simplify_just_coalesce); + + threshold.param_set_range(0.0001, Geom::infinity()); + threshold.param_set_increments(0.0001, 0.0001); + threshold.param_set_digits(6); + + steps.param_set_range(0, 100); + steps.param_set_increments(1, 1); + steps.param_set_digits(0); + + smooth_angles.param_set_range(0.0, 360.0); + smooth_angles.param_set_increments(10, 10); + smooth_angles.param_set_digits(2); + + helper_size.param_set_range(0.0, 999.0); + helper_size.param_set_increments(5, 5); + helper_size.param_set_digits(2); + + radius_helper_nodes = 6.0; + apply_to_clippath_and_mask = true; +} + +LPESimplify::~LPESimplify() = default; + +void +LPESimplify::doBeforeEffect (SPLPEItem const* lpeitem) +{ + if(!hp.empty()) { + hp.clear(); + } + bbox = lpeitem->visualBounds(); + radius_helper_nodes = helper_size; +} + +Gtk::Widget * +LPESimplify::newWidget() +{ + // use manage here, because after deletion of Effect object, others might still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + Gtk::Box * buttons = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter * param = *it; + Gtk::Widget * widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + if (param->param_key == "simplify_individual_paths" || + param->param_key == "simplify_just_coalesce") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + buttons->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + Gtk::Box * horizontal_box = dynamic_cast<Gtk::Box *>(widg); + std::vector< Gtk::Widget* > child_list = horizontal_box->get_children(); + Gtk::Entry* entry_widg = dynamic_cast<Gtk::Entry *>(child_list[1]); + entry_widg->set_width_chars(8); + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + } + + ++it; + } + vbox->pack_start(*buttons,true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void +LPESimplify::doEffect(SPCurve *curve) +{ + Geom::PathVector const original_pathv = pathv_to_linear_and_cubic_beziers(curve->get_pathvector()); + gdouble size = Geom::L2(bbox->dimensions()); + //size /= Geom::Affine(0,0,0,0,0,0).descrim(); + Path* pathliv = Path_for_pathvector(original_pathv); + if(simplify_individual_paths) { + size = Geom::L2(Geom::bounds_fast(original_pathv)->dimensions()); + } + size /= sp_lpe_item->i2doc_affine().descrim(); + for (int unsigned i = 0; i < steps; i++) { + if ( simplify_just_coalesce ) { + pathliv->Coalesce(threshold * size); + } else { + pathliv->ConvertEvenLines(threshold * size); + pathliv->Simplify(threshold * size); + } + } + Geom::PathVector result = Geom::parse_svg_path(pathliv->svg_dump_path()); + generateHelperPathAndSmooth(result); + curve->set_pathvector(result); + update_helperpath(); +} + +void +LPESimplify::generateHelperPathAndSmooth(Geom::PathVector &result) +{ + if(steps < 1) { + return; + } + Geom::PathVector tmp_path; + Geom::CubicBezier const *cubic = nullptr; + for (auto & path_it : result) { + if (path_it.empty()) { + continue; + } + + Geom::Path::iterator curve_it1 = path_it.begin(); // incoming curve + Geom::Path::iterator curve_it2 = ++(path_it.begin());// outgoing curve + Geom::Path::iterator curve_endit = path_it.end_default(); // this determines when the loop has to stop + SPCurve *nCurve = new SPCurve(); + if (path_it.closed()) { + // if the path is closed, maybe we have to stop a bit earlier because the + // closing line segment has zerolength. + const Geom::Curve &closingline = + path_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! + curve_endit = path_it.end_open(); + } + } + if(helper_size > 0) { + drawNode(curve_it1->initialPoint()); + } + nCurve->moveto(curve_it1->initialPoint()); + Geom::Point start = Geom::Point(0,0); + while (curve_it1 != curve_endit) { + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1); + Geom::Point point_at1 = curve_it1->initialPoint(); + Geom::Point point_at2 = curve_it1->finalPoint(); + Geom::Point point_at3 = curve_it1->finalPoint(); + Geom::Point point_at4 = curve_it1->finalPoint(); + + if(start == Geom::Point(0,0)) { + start = point_at1; + } + + if (cubic) { + point_at1 = (*cubic)[1]; + point_at2 = (*cubic)[2]; + } + + if(path_it.closed() && curve_it2 == curve_endit) { + point_at4 = start; + } + if(curve_it2 != curve_endit) { + cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it2); + if (cubic) { + point_at4 = (*cubic)[1]; + } + } + Geom::Ray ray1(point_at2, point_at3); + Geom::Ray ray2(point_at3, point_at4); + double angle1 = Geom::deg_from_rad(ray1.angle()); + double angle2 = Geom::deg_from_rad(ray2.angle()); + if((smooth_angles >= std::abs(angle2 - angle1)) && !are_near(point_at4,point_at3) && !are_near(point_at2,point_at3)) { + double dist = Geom::distance(point_at2,point_at3); + Geom::Angle angleFixed = ray2.angle(); + angleFixed -= Geom::Angle::from_degrees(180.0); + point_at2 = Geom::Point::polar(angleFixed, dist) + point_at3; + } + nCurve->curveto(point_at1, point_at2, curve_it1->finalPoint()); + cubic = dynamic_cast<Geom::CubicBezier const *>(nCurve->last_segment()); + if (cubic) { + point_at1 = (*cubic)[1]; + point_at2 = (*cubic)[2]; + if(helper_size > 0) { + if(!are_near((*cubic)[0],(*cubic)[1])) { + drawHandle((*cubic)[1]); + drawHandleLine((*cubic)[0],(*cubic)[1]); + } + if(!are_near((*cubic)[3],(*cubic)[2])) { + drawHandle((*cubic)[2]); + drawHandleLine((*cubic)[3],(*cubic)[2]); + } + } + } + if(helper_size > 0) { + drawNode(curve_it1->finalPoint()); + } + ++curve_it1; + ++curve_it2; + } + if (path_it.closed()) { + nCurve->closepath_current(); + } + tmp_path.push_back(nCurve->get_pathvector()[0]); + nCurve->reset(); + delete nCurve; + } + result = tmp_path; +} + +void +LPESimplify::drawNode(Geom::Point p) +{ + double r = radius_helper_nodes; + char const * svgd; + svgd = "M 0.55,0.5 A 0.05,0.05 0 0 1 0.5,0.55 0.05,0.05 0 0 1 0.45,0.5 0.05,0.05 0 0 1 0.5,0.45 0.05,0.05 0 0 1 0.55,0.5 Z M 0,0 1,0 1,1 0,1 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Geom::Scale(r) * Geom::Translate(p - Geom::Point(0.5*r,0.5*r)); + hp.push_back(pathv[0]); + hp.push_back(pathv[1]); +} + +void +LPESimplify::drawHandle(Geom::Point p) +{ + double r = radius_helper_nodes; + char const * svgd; + svgd = "M 0.7,0.35 A 0.35,0.35 0 0 1 0.35,0.7 0.35,0.35 0 0 1 0,0.35 0.35,0.35 0 0 1 0.35,0 0.35,0.35 0 0 1 0.7,0.35 Z"; + Geom::PathVector pathv = sp_svg_read_pathv(svgd); + pathv *= Geom::Scale(r) * Geom::Translate(p - Geom::Point(0.35*r,0.35*r)); + hp.push_back(pathv[0]); +} + + +void +LPESimplify::drawHandleLine(Geom::Point p,Geom::Point p2) +{ + Geom::Path path; + path.start( p ); + double diameter = radius_helper_nodes; + if(helper_size > 0 && Geom::distance(p,p2) > (diameter * 0.35)) { + Geom::Ray ray2(p, p2); + p2 = p2 - Geom::Point::polar(ray2.angle(),(diameter * 0.35)); + } + path.appendNew<Geom::LineSegment>( p2 ); + hp.push_back(path); +} + +void +LPESimplify::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(hp); +} + + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-simplify.h b/src/live_effects/lpe-simplify.h new file mode 100644 index 0000000..9eb3d15 --- /dev/null +++ b/src/live_effects/lpe-simplify.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_SIMPLIFY_H +#define INKSCAPE_LPE_SIMPLIFY_H + +/* + * Inkscape::LPESimplify + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/effect.h" +#include "live_effects/parameter/togglebutton.h" +#include "live_effects/lpegroupbbox.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPESimplify : public Effect , GroupBBoxEffect { + +public: + LPESimplify(LivePathEffectObject *lpeobject); + ~LPESimplify() override; + LPESimplify(const LPESimplify &) = delete; + LPESimplify &operator=(const LPESimplify &) = delete; + + void doEffect(SPCurve *curve) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + virtual void generateHelperPathAndSmooth(Geom::PathVector &result); + + Gtk::Widget * newWidget() override; + + virtual void drawNode(Geom::Point p); + + virtual void drawHandle(Geom::Point p); + + virtual void drawHandleLine(Geom::Point p,Geom::Point p2); + ScalarParam threshold; + + protected: + void addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) override; + +private: + ScalarParam steps; + ScalarParam smooth_angles; + ScalarParam helper_size; + ToggleButtonParam simplify_individual_paths; + ToggleButtonParam simplify_just_coalesce; + + double radius_helper_nodes; + Geom::PathVector hp; + Geom::OptRect bbox; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape +#endif diff --git a/src/live_effects/lpe-skeleton.cpp b/src/live_effects/lpe-skeleton.cpp new file mode 100644 index 0000000..302a6a2 --- /dev/null +++ b/src/live_effects/lpe-skeleton.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Minimal dummy LPE effect implementation, used as an example for a base + * starting class when implementing new LivePathEffects. + * + * In vi, three global search-and-replaces will let you rename everything + * in this and the .h file: + * + * :%s/SKELETON/YOURNAME/g + * :%s/Skeleton/Yourname/g + * :%s/skeleton/yourname/g + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2007-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-skeleton.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPESkeleton::LPESkeleton(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + number(_("Float parameter"), _("just a real number like 1.4!"), "svgname", &wr, this, 1.2) +{ + /* uncomment the following line to have the original path displayed while the item is selected */ + //show_orig_path = true; + /* uncomment the following line to enable display of the effect-specific on-canvas handles (knotholder entities) */ + //_provides_knotholder_entities + + /* register all your parameters here, so Inkscape knows which parameters this effect has: */ + registerParameter(&number); +} + +LPESkeleton::~LPESkeleton() += default; + + +/* ######################## + * Choose to implement one of the doEffect functions. You can delete or comment out the others. + */ + +/* +void +LPESkeleton::doEffect (SPCurve * curve) +{ + // spice this up to make the effect actually *do* something! +} + +Geom::PathVector +LPESkeleton::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + + path_out = path_in; // spice this up to make the effect actually *do* something! + + return path_out; +} +*/ + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPESkeleton::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + Geom::Piecewise<Geom::D2<Geom::SBasis> > output; + + output = pwd2_in; // spice this up to make the effect actually *do* something! + + return output; +} + +/* ######################## + * If you want to provide effect-specific on-canvas handles (knotholder entities), define them here: + */ + +/* +namespace Skeleton { + +class KnotHolderEntityMyHandle : public LPEKnotHolderEntity +{ +public: + // the set() and get() methods must be implemented, click() is optional + virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state); + virtual Geom::Point knot_get() const; + //virtual void knot_click(guint state); +}; + +} // namespace Skeleton + +void +LPESkeleton::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) { + { + KnotHolderEntityMyHandle *e = new KnotHolderEntityMyHandle(this); + e->create( NULL, item, knotholder, + _("Text describing what this handle does"), + //optional: knot_shape, knot_mode, knot_color); + knotholder->add(e); + } +}; +*/ + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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/live_effects/lpe-skeleton.h b/src/live_effects/lpe-skeleton.h new file mode 100644 index 0000000..57d6d73 --- /dev/null +++ b/src/live_effects/lpe-skeleton.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Minimal LPE effect, see lpe-skeleton.cpp. + */ +/* Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2007-2012 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_SKELETON_H +#define INKSCAPE_LPE_SKELETON_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { +namespace LivePathEffect { + +// each knotholder handle for your LPE requires a separate class derived from LPEKnotHolderEntity; +// define it in lpe-skeleton.cpp and add code to create it in addKnotHolderEntities +// note that the LPE parameter classes implement their own handles! So in most cases, you will +// not have to do anything like this. +/** +namespace Skeleton { + // we need a separate namespace to avoid clashes with other LPEs + class KnotHolderEntityMyHandle; +} +**/ + +class LPESkeleton : public Effect { +public: + LPESkeleton(LivePathEffectObject *lpeobject); + ~LPESkeleton() override; + +// Choose to implement one of the doEffect functions. You can delete or comment out the others. +// virtual void doEffect (SPCurve * curve); +// virtual Geom::PathVector doEffect_path (Geom::PathVector const &path_in); + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const &pwd2_in) override; + + /* the knotholder entity classes (if any) can be declared friends */ + //friend class Skeleton::KnotHolderEntityMyHandle; + //virtual void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item); + +private: + // add the parameters for your effect here: + ScalarParam number; + // there are all kinds of parameters. Check the /live_effects/parameter directory which types exist! + + LPESkeleton(const LPESkeleton&) = delete; + LPESkeleton& operator=(const LPESkeleton&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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/live_effects/lpe-sketch.cpp b/src/live_effects/lpe-sketch.cpp new file mode 100644 index 0000000..802f791 --- /dev/null +++ b/src/live_effects/lpe-sketch.cpp @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * LPE sketch effect implementation. + */ +/* Authors: + * Jean-Francois Barraud <jf.barraud@gmail.com> + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-sketch.h" + +// You might need to include other 2geom files. You can add them here: +#include <2geom/sbasis-math.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/path-intersection.h> + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPESketch::LPESketch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // initialise your parameters here: + //testpointA(_("Test Point A"), _("Test A"), "ptA", &wr, this, Geom::Point(100,100)), + nbiter_approxstrokes(_("Strokes:"), _("Draw that many approximating strokes"), "nbiter_approxstrokes", &wr, this, 5), + strokelength(_("Max stroke length:"), + _("Maximum length of approximating strokes"), "strokelength", &wr, this, 100.), + strokelength_rdm(_("Stroke length variation:"), + _("Random variation of stroke length (relative to maximum length)"), "strokelength_rdm", &wr, this, .3), + strokeoverlap(_("Max. overlap:"), + _("How much successive strokes should overlap (relative to maximum length)"), "strokeoverlap", &wr, this, .3), + strokeoverlap_rdm(_("Overlap variation:"), + _("Random variation of overlap (relative to maximum overlap)"), "strokeoverlap_rdm", &wr, this, .3), + ends_tolerance(_("Max. end tolerance:"), + _("Maximum distance between ends of original and approximating paths (relative to maximum length)"), "ends_tolerance", &wr, this, .1), + parallel_offset(_("Average offset:"), + _("Average distance each stroke is away from the original path"), "parallel_offset", &wr, this, 5.), + tremble_size(_("Max. tremble:"), + _("Maximum tremble magnitude"), "tremble_size", &wr, this, 5.), + tremble_frequency(_("Tremble frequency:"), + _("Average number of tremble periods in a stroke"), "tremble_frequency", &wr, this, 1.) +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + ,nbtangents(_("Construction lines:"), + _("How many construction lines (tangents) to draw"), "nbtangents", &wr, this, 5), + tgtscale(_("Scale:"), + _("Scale factor relating curvature and length of construction lines (try 5*offset)"), "tgtscale", &wr, this, 10.0), + tgtlength(_("Max. length:"), _("Maximum length of construction lines"), "tgtlength", &wr, this, 100.0), + tgtlength_rdm(_("Length variation:"), _("Random variation of the length of construction lines"), "tgtlength_rdm", &wr, this, .3), + tgt_places_rdmness(_("Placement randomness:"), _("0: evenly distributed construction lines, 1: purely random placement"), "tgt_places_rdmness", &wr, this, 1.) +#ifdef LPE_SKETCH_USE_CURVATURE + ,min_curvature(_("k_min:"), _("min curvature"), "k_min", &wr, this, 4.0) + ,max_curvature(_("k_max:"), _("max curvature"), "k_max", &wr, this, 1000.0) +#endif +#endif +{ + // register all your parameters here, so Inkscape knows which parameters this effect has: + //Add some comment in the UI: *warning* the precise output of this effect might change in future releases! + //convert to path if you want to keep exact output unchanged in future releases... + //registerParameter(&testpointA) ); + registerParameter(&nbiter_approxstrokes); + registerParameter(&strokelength); + registerParameter(&strokelength_rdm); + registerParameter(&strokeoverlap); + registerParameter(&strokeoverlap_rdm); + registerParameter(&ends_tolerance); + registerParameter(¶llel_offset); + registerParameter(&tremble_size); + registerParameter(&tremble_frequency); +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + registerParameter(&nbtangents); + registerParameter(&tgt_places_rdmness); + registerParameter(&tgtscale); + registerParameter(&tgtlength); + registerParameter(&tgtlength_rdm); +#ifdef LPE_SKETCH_USE_CURVATURE + registerParameter(&min_curvature); + registerParameter(&max_curvature); +#endif +#endif + + nbiter_approxstrokes.param_make_integer(); + nbiter_approxstrokes.param_set_range(0, std::numeric_limits<gint>::max()); + strokelength.param_set_range(1, std::numeric_limits<double>::max()); + strokelength.param_set_increments(1., 5.); + strokelength_rdm.param_set_range(0, 1.); + strokeoverlap.param_set_range(0, 1.); + strokeoverlap.param_set_increments(0.1, 0.30); + ends_tolerance.param_set_range(0., 1.); + parallel_offset.param_set_range(0, std::numeric_limits<double>::max()); + tremble_frequency.param_set_range(0.01, 100.); + tremble_frequency.param_set_increments(.5, 1.5); + strokeoverlap_rdm.param_set_range(0, 1.); + +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + nbtangents.param_make_integer(); + nbtangents.param_set_range(0, std::numeric_limits<gint>::max()); + tgtscale.param_set_range(0, std::numeric_limits<double>::max()); + tgtscale.param_set_increments(.1, .5); + tgtlength.param_set_range(0, std::numeric_limits<double>::max()); + tgtlength.param_set_increments(1., 5.); + tgtlength_rdm.param_set_range(0, 1.); + tgt_places_rdmness.param_set_range(0, 1.); + //this is not very smart, but required to avoid having lot of tangents stacked on short components. + //Note: we could specify a density instead of an absolute number, but this would be scale dependent. + concatenate_before_pwd2 = true; +#endif +} + +LPESketch::~LPESketch() += default; + +/* +Geom::Piecewise<Geom::D2<Geom::SBasis> > +addLinearEnds (Geom::Piecewise<Geom::D2<Geom::SBasis> > & m){ + using namespace Geom; + Piecewise<D2<SBasis> > output; + Piecewise<D2<SBasis> > start; + Piecewise<D2<SBasis> > end; + double x,y,vx,vy; + + x = m.segs.front()[0].at0(); + y = m.segs.front()[1].at0(); + vx = m.segs.front()[0][1][0]+Tri(m.segs.front()[0][0]); + vy = m.segs.front()[1][1][0]+Tri(m.segs.front()[1][0]); + start = Piecewise<D2<SBasis> >(D2<SBasis>(Linear (x-vx,x),Linear (y-vy,y))); + start.offsetDomain(m.cuts.front()-1.); + + x = m.segs.back()[0].at1(); + y = m.segs.back()[1].at1(); + vx = -m.segs.back()[0][1][1]+Tri(m.segs.back()[0][0]);; + vy = -m.segs.back()[1][1][1]+Tri(m.segs.back()[1][0]);; + end = Piecewise<D2<SBasis> >(D2<SBasis>(Linear (x,x+vx),Linear (y,y+vy))); + //end.offsetDomain(m.cuts.back()); + + output = start; + output.concat(m); + output.concat(end); + return output; +} +*/ + + + +//This returns a random perturbation. Notice the domain is [s0,s0+first multiple of period>s1]... +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPESketch::computePerturbation (double s0, double s1){ + using namespace Geom; + Piecewise<D2<SBasis> >res; + + //global offset for this stroke. + double offsetX = 2*parallel_offset-parallel_offset.get_value(); + double offsetY = 2*parallel_offset-parallel_offset.get_value(); + Point A,dA,B,dB,offset = Point(offsetX,offsetY); + //start point A + for (unsigned dim=0; dim<2; dim++){ + A[dim] = offset[dim] + 2*tremble_size-tremble_size.get_value(); + dA[dim] = 2*tremble_size-tremble_size.get_value(); + } + //compute howmany deg 3 sbasis to concat according to frequency. + + unsigned count = unsigned((s1-s0)/strokelength*tremble_frequency)+1; + //unsigned count = unsigned((s1-s0)/tremble_frequency)+1; + + for (unsigned i=0; i<count; i++){ + D2<SBasis> perturb = D2<SBasis>(SBasis(2, Linear()), SBasis(2, Linear())); + for (unsigned dim=0; dim<2; dim++){ + B[dim] = offset[dim] + 2*tremble_size-tremble_size.get_value(); + perturb[dim][0] = Linear(A[dim],B[dim]); + dA[dim] = dA[dim]-B[dim]+A[dim]; + //avoid dividing by 0. Very short strokes will have ends parallel to the curve... + if ( s1-s0 > 1e-2) + dB[dim] = -(2*tremble_size-tremble_size.get_value())/(s0-s1)-B[dim]+A[dim]; + else + dB[dim] = -(2*tremble_size-tremble_size.get_value())-B[dim]+A[dim]; + perturb[dim][1] = Linear(dA[dim],dB[dim]); + } + dA = B-A-dB; + A = B; + //dA = B-A-dB; + res.concat(Piecewise<D2<SBasis> >(perturb)); + } + res.setDomain(Interval(s0,s0+count*strokelength/tremble_frequency)); + //res.setDomain(Interval(s0,s0+count*tremble_frequency)); + return res; +} + + +// Main effect body... +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPESketch::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + //If the input path is empty, do nothing. + //Note: this happens when duplicating a 3d box... dunno why. + if (pwd2_in.size()==0) return pwd2_in; + + Piecewise<D2<SBasis> > output; + + // some variables for futur use (for construction lines; compute arclength only once...) + // notations will be : t = path time, s = distance from start along the path. + Piecewise<SBasis> pathlength; + double total_length = 0; + + //TODO: split Construction Lines/Approximated Strokes into two separate effects? + + //----- Approximated Strokes. + std::vector<Piecewise<D2<SBasis> > > pieces_in = split_at_discontinuities (pwd2_in); + + //work separately on each component. + for (auto piece : pieces_in){ + + Piecewise<SBasis> piecelength = arcLengthSb(piece,.1); + double piece_total_length = piecelength.segs.back().at1()-piecelength.segs.front().at0(); + pathlength.concat(piecelength + total_length); + total_length += piece_total_length; + + + //TODO: better check this on the Geom::Path. + bool closed = piece.segs.front().at0() == piece.segs.back().at1(); + if (closed){ + piece.concat(piece); + piecelength.concat(piecelength+piece_total_length); + } + + for (unsigned i = 0; i<nbiter_approxstrokes; i++){ + //Basic steps: + //- Choose a rdm seg [s0,s1], find corresponding [t0,t1], + //- Pick a rdm perturbation delta(s), collect 'piece(t)+delta(s(t))' over [t0,t1] into output. + + // pick a point where to start the stroke (s0 = dist from start). + double s1=0.,s0 = ends_tolerance*strokelength+0.0001;//the root finder might miss 0. + double t1, t0; + double s0_initial = s0; + bool done = false;// was the end of the component reached? + + while (!done){ + // if the start point is already too far... do nothing. (this should not happen!) + if (!closed && s1>piece_total_length - ends_tolerance.get_value()*strokelength) break; + if ( closed && s0>piece_total_length + s0_initial) break; + + std::vector<double> times; + times = roots(piecelength-s0); + t0 = times.at(0);//there should be one and only one solution!! + + // pick a new end point (s1 = s0 + strokelength). + s1 = s0 + strokelength*(1-strokelength_rdm); + // don't let it go beyond the end of the original path. + // TODO/FIXME: this might result in short strokes near the end... + if (!closed && s1>piece_total_length-ends_tolerance.get_value()*strokelength){ + done = true; + //!!the root solver might miss s1==piece_total_length... + if (s1>piece_total_length){s1 = piece_total_length - ends_tolerance*strokelength-0.0001;} + } + if (closed && s1>piece_total_length + s0_initial){ + done = true; + if (closed && s1>2*piece_total_length){ + s1 = 2*piece_total_length - strokeoverlap*(1-strokeoverlap_rdm)*strokelength-0.0001; + } + } + times = roots(piecelength-s1); + if (times.empty()) break;//we should not be there. + t1 = times[0]; + + //pick a rdm perturbation, and collect the perturbed piece into output. + Piecewise<D2<SBasis> > pwperturb = computePerturbation(s0-0.01,s1+0.01); + pwperturb = compose(pwperturb,portion(piecelength,t0,t1)); + + output.concat(portion(piece,t0,t1)+pwperturb); + + //step points: s0 = s1 - overlap. + //TODO: make sure this has to end? + s0 = s1 - strokeoverlap*(1-strokeoverlap_rdm)*(s1-s0); + } + } + } + +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + + //----- Construction lines. + //TODO: choose places according to curvature?. + + //at this point we should have: + //pathlength = arcLengthSb(pwd2_in,.1); + //total_length = pathlength.segs.back().at1()-pathlength.segs.front().at0(); + Piecewise<D2<SBasis> > m = pwd2_in; + Piecewise<D2<SBasis> > v = derivative(pwd2_in); + Piecewise<D2<SBasis> > a = derivative(v); + +#ifdef LPE_SKETCH_USE_CURVATURE + //---- curvature experiment...(enable + Piecewise<SBasis> k = curvature(pwd2_in); + OptInterval k_bnds = bounds_exact(abs(k)); + double k_min = k_bnds->min() + k_bnds->extent() * min_curvature; + double k_max = k_bnds->min() + k_bnds->extent() * max_curvature; + + Piecewise<SBasis> bump; + //SBasis bump_seg = SBasis( 2, Linear(0) ); + //bump_seg[1] = Linear( 4. ); + SBasis bump_seg = SBasis( 1, Linear(1) ); + bump.push_cut( k_bnds->min() - 1 ); + bump.push( Linear(0), k_min ); + bump.push(bump_seg,k_max); + bump.push( Linear(0), k_bnds->max()+1 ); + + Piecewise<SBasis> repartition = compose( bump, k ); + repartition = integral(repartition); + //------------------------------- +#endif + + for (unsigned i=0; i<nbtangents; i++){ + + // pick a point where to draw a tangent (s = dist from start along path). +#ifdef LPE_SKETCH_USE_CURVATURE + double proba = repartition.firstValue()+ (rand()%100)/100.*(repartition.lastValue()-repartition.firstValue()); + std::vector<double> times; + times = roots(repartition - proba); + double t = times.at(0);//there should be one and only one solution! +#else + //double s = total_length * ( i + tgtlength_rdm ) / (nbtangents+1.); + double reg_place = total_length * ( i + .5) / ( nbtangents ); + double rdm_place = total_length * tgt_places_rdmness; + double s = ( 1.- tgt_places_rdmness.get_value() ) * reg_place + rdm_place ; + std::vector<double> times; + times = roots(pathlength-s); + double t = times.at(0);//there should be one and only one solution! +#endif + Point m_t = m(t), v_t = v(t), a_t = a(t); + //Compute tgt length according to curvature (not exceeding tgtlength) so that + // dist to original curve ~ 4 * (parallel_offset+tremble_size). + //TODO: put this 4 as a parameter in the UI... + //TODO: what if with v=0? + double l = tgtlength*(1-tgtlength_rdm)/v_t.length(); + double r = std::pow(v_t.length(), 3) / cross(v_t, a_t); + r = sqrt((2*fabs(r)-tgtscale)*tgtscale)/v_t.length(); + l=(r<l)?r:l; + //collect the tgt segment into output. + D2<SBasis> tgt = D2<SBasis>(); + for (unsigned dim=0; dim<2; dim++){ + tgt[dim] = SBasis(Linear(m_t[dim]-v_t[dim]*l, m_t[dim]+v_t[dim]*l)); + } + output.concat(Piecewise<D2<SBasis> >(tgt)); + } +#endif + + return output; +} + +void +LPESketch::doBeforeEffect (SPLPEItem const*/*lpeitem*/) +{ + //init random parameters. + parallel_offset.resetRandomizer(); + strokelength_rdm.resetRandomizer(); + strokeoverlap_rdm.resetRandomizer(); + ends_tolerance.resetRandomizer(); + tremble_size.resetRandomizer(); +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + tgtlength_rdm.resetRandomizer(); + tgt_places_rdmness.resetRandomizer(); +#endif +} + +/* ######################## */ + +} //namespace LivePathEffect (setq default-directory "c:/Documents And Settings/jf/Mes Documents/InkscapeSVN") +} /* namespace Inkscape */ + +/* + 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/live_effects/lpe-sketch.h b/src/live_effects/lpe-sketch.h new file mode 100644 index 0000000..4d34088 --- /dev/null +++ b/src/live_effects/lpe-sketch.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * @brief LPE sketch effect implementation, see lpe-sketch.cpp. + */ +/* Authors: + * Jean-Francois Barraud <jf.barraud@gmail.com> + * Johan Engelen <j.b.c.engelen@utwente.nl> + * + * Copyright (C) 2007 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_SKETCH_H +#define INKSCAPE_LPE_SKETCH_H + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/random.h" +#include "live_effects/parameter/point.h" + +#define LPE_SKETCH_USE_CONSTRUCTION_LINES +//#define LPE_SKETCH_USE_CURVATURE + +namespace Inkscape { +namespace LivePathEffect { + +class LPESketch : public Effect { +public: + LPESketch(LivePathEffectObject *lpeobject); + ~LPESketch() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + +private: + // add the parameters for your effect here: + //PointParam testpointA; + ScalarParam nbiter_approxstrokes; + ScalarParam strokelength; + RandomParam strokelength_rdm; + ScalarParam strokeoverlap; + RandomParam strokeoverlap_rdm; + RandomParam ends_tolerance; + RandomParam parallel_offset; + RandomParam tremble_size; + ScalarParam tremble_frequency; + +#ifdef LPE_SKETCH_USE_CONSTRUCTION_LINES + ScalarParam nbtangents; + ScalarParam tgtscale; + ScalarParam tgtlength; + RandomParam tgtlength_rdm; + RandomParam tgt_places_rdmness; +#ifdef LPE_SKETCH_USE_CURVATURE + ScalarParam min_curvature; + ScalarParam max_curvature; +#endif +#endif + LPESketch(const LPESketch&) = delete; + LPESketch& operator=(const LPESketch&) = delete; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > computePerturbation (double s0, double s1); + +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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/live_effects/lpe-slice.cpp b/src/live_effects/lpe-slice.cpp new file mode 100644 index 0000000..50c7713 --- /dev/null +++ b/src/live_effects/lpe-slice.cpp @@ -0,0 +1,969 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <slice> implementation: slices a path with respect to a given line. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * Abhishek Sharma + * Jabiertxof + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilin Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-slice.h" + +#include <gtkmm.h> + +#include "2geom/affine.h" +#include "2geom/path-intersection.h" +#include "display/curve.h" +#include "helper/geom.h" +#include "live_effects/parameter/satellite-reference.h" +#include "object/sp-defs.h" +#include "object/sp-lpe-item.h" +#include "object/sp-path.h" +#include "object/sp-text.h" +#include "path-chemistry.h" +#include "path/path-boolop.h" +#include "style.h" +#include "svg/path-string.h" +#include "svg/svg.h" +#include "xml/sp-css-attr.h" + +// this is only to flatten nonzero fillrule +#include "livarot/Path.h" +#include "livarot/Shape.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +typedef FillRule FillRuleFlatten; + +namespace Inkscape { +namespace LivePathEffect { +LPESlice::LPESlice(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // do not change name of this parameter us used in oncommit + lpesatellites(_("lpesatellites"), _("Items satellites"), "lpesatellites", &wr, this, false), + allow_transforms(_("Allow Transforms"), _("Allow transforms"), "allow_transforms", &wr, this, true), + start_point(_("Slice line start"), _("Start point of slice line"), "start_point", &wr, this, _("Adjust start point of slice line")), + end_point(_("Slice line end"), _("End point of slice line"), "end_point", &wr, this, _("Adjust end point of slice line")), + center_point(_("Slice line mid"), _("Center point of slice line"), "center_point", &wr, this, _("Adjust center point of slice line")) +{ + registerParameter(&lpesatellites); + registerParameter(&allow_transforms); + registerParameter(&start_point); + registerParameter(&end_point); + registerParameter(¢er_point); + show_orig_path = true; + apply_to_clippath_and_mask = false; + previous_center = Geom::Point(0,0); + center_point.param_widget_is_visible(false); + reset = false; + center_horiz = false; + center_vert = false; + allow_transforms_prev = allow_transforms; + on_remove_all = false; + container = nullptr; + satellitestoclipboard = true; +} + +LPESlice::~LPESlice() +{ + keep_paths = false; + doOnRemove(nullptr); +}; + +bool +LPESlice::doOnOpen(SPLPEItem const* lpeitem) { + bool fixed = false; + Glib::ustring version = lpeversion.param_getSVGValue(); + if (version < "1.2") { + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() >= 1) { + sp_lpe_item_update_patheffect(lpeitems[0], false, true); + } + lpeversion.param_setValue("1.2", true); + fixed = true; + lpesatellites.write_to_SVG(); + } + lpesatellites.start_listening(); + lpesatellites.connect_selection_changed(); + return fixed; +} + +Gtk::Widget * +LPESlice::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(2); + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 0)); + Gtk::Button *center_vert_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Vertical")))); + center_vert_button->signal_clicked().connect(sigc::mem_fun(*this, &LPESlice::centerVert)); + center_vert_button->set_size_request(110, 20); + Gtk::Button *center_horiz_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Horizontal")))); + center_horiz_button->signal_clicked().connect(sigc::mem_fun(*this, &LPESlice::centerHoriz)); + center_horiz_button->set_size_request(110, 20); + Gtk::Button *reset_button = Gtk::manage(new Gtk::Button(Glib::ustring(_("Reset styles")))); + reset_button->signal_clicked().connect(sigc::mem_fun(*this, &LPESlice::resetStyles)); + reset_button->set_size_request(110, 20); + + vbox->pack_start(*hbox, true, true, 2); + hbox->pack_start(*reset_button, false, false, 2); + hbox->pack_start(*center_vert_button, false, false, 2); + hbox->pack_start(*center_horiz_button, false, false, 2); + std::vector<Parameter *>::iterator it = param_vector.begin(); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + if (Gtk::Widget *widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + + +void +LPESlice::centerVert(){ + center_vert = true; + refresh_widgets = true; + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + sp_lpe_item_update_patheffect(sp_lpe_item, false, false); + } +} + +void +LPESlice::centerHoriz(){ + center_horiz = true; + refresh_widgets = true; + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + sp_lpe_item_update_patheffect(sp_lpe_item, false, false); + } +} + +bool sp_has_path_data(SPItem *item, bool originald) +{ + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + std::vector<SPObject *> childs = group->childList(true); + for (auto &child : childs) { + SPItem *item = dynamic_cast<SPItem *>(child); + if (sp_has_path_data(item, originald)) { + return true; + } + } + } + SPShape *shape = dynamic_cast<SPShape *>(item); + if (shape) { + SPCurve const *c = shape->curve(); + if (c && !c->is_empty()) { + return true; + } + if (originald) { + if (shape->hasPathEffectRecursive()) { + SPCurve const *c = shape->curveBeforeLPE(); + if (c && !c->is_empty()) { + return true; + } + } + } + } + return false; +} +/* + * Allow changing original-d to d to "reset" temporary the LPE + * when the slice doesn't pass through item till sp_lpe_item is crossed + */ +void +LPESlice::originalDtoD(SPShape const *shape, SPCurve *curve) +{ + SPCurve const *c = shape->curveBeforeLPE(); + if (c && !c->is_empty()) { + curve->set_pathvector(c->get_pathvector()); + } +} + +/* + * Allow changing original-d to d to "reset" temporary the LPE + * when the slice doesn't pass through item till sp_lpe_item is crossed + */ +void +LPESlice::originalDtoD(SPItem *item) +{ + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + std::vector<SPObject *> childs = group->childList(true); + for (auto &child : childs) { + SPItem *item = dynamic_cast<SPItem *>(child); + originalDtoD(item); + } + return; + } + SPShape *shape = dynamic_cast<SPShape *>(item); + if (shape) { + SPCurve const *c = shape->curveBeforeLPE(); + if (c && !c->is_empty()) { + shape->bbox_vis_cache_is_valid = false; + shape->bbox_geom_cache_is_valid = false; + shape->setCurveInsync(std::move(c)); + auto str = sp_svg_write_path(c->get_pathvector()); + shape->setAttribute("d", str); + } + } +} + +void +LPESlice::doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) +{ + Glib::ustring version = lpeversion.param_getSVGValue(); + // this avoid regenerate fake satellites un undo after open a legacy LPE + if (!is_load && version < "1.2") { + return; + } + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + bool m_saved = DocumentUndo::getUndoSensitive(getSPDoc()); + DocumentUndo::ScopedInsensitive _no_undo(document); + if (document->isPartial()) { + DocumentUndo::setUndoSensitive(document, m_saved); + } else if (document->isSeeking()) { + return; + } + bool is_applied_on = false; + if (is_applied) { + is_applied_on = true; + is_applied = false; + } + bool write = false; + bool active = !lpesatellites.data().size() || is_load; + for (auto lpereference : lpesatellites.data()) { + if (lpereference && lpereference->isAttached() && lpereference.get()->getObject() != nullptr) { + active = true; + } + } + if (!active && !is_load) { + lpesatellites.clear(); + return; + } + + LPESlice *nextslice = dynamic_cast<LPESlice *>(sp_lpe_item->getNextLPE(this)); + if (is_visible && (!nextslice || !nextslice->is_visible)) { + LPESlice *prevslice = dynamic_cast<LPESlice *>(sp_lpe_item->getPrevLPE(this)); + if (boundingbox_X.isSingular() || boundingbox_Y.isSingular()) { + for (auto & iter : lpesatellites.data()) { + SPObject *elemref; + if (iter && iter->isAttached() && (elemref = iter->getObject())) { + if (auto *splpeitem = dynamic_cast<SPLPEItem *>(elemref)) { + splpeitem->setHidden(true); + } + } + } + return; + } + //ungroup + if (!is_load && container && container != sp_lpe_item->parent && container != sp_lpe_item->parent->parent) { + processObjects(LPE_UPDATE); + } else if (!is_load && container && container != sp_lpe_item->parent) { // group + processObjects(LPE_UPDATE); + } + std::vector<std::pair<Geom::Line, size_t> > slicer = getSplitLines(); + if (!slicer.size()) { + return; + } + container = lpeitem->parent; + objindex = 0; + legacy = false; + bool creation = write; + split(sp_lpe_item, curve, slicer, 0, creation); + bool connected = lpesatellites.is_connected(); + if (lpesatellites.data().size() && (creation || !connected)) { + lpesatellites.write_to_SVG(); + lpesatellites.start_listening(); + lpesatellites.update_satellites(!connected); + } + bool maindata = sp_has_path_data(sp_lpe_item, true); + for (auto & iter : lpesatellites.data()) { + SPObject *elemref; + if (iter && iter->isAttached() && (elemref = iter->getObject())) { + SPLPEItem *splpeitem = dynamic_cast<SPLPEItem *>(elemref); + if (splpeitem || lpeitem->isHidden()) { + if (!maindata || lpeitem->isHidden()) { + splpeitem->setHidden(true); + } + sp_lpe_item_update_patheffect(splpeitem, false, false); + } + } + } + if (!maindata) { + if (!curve) { // group + originalDtoD(sp_lpe_item); + } else { + originalDtoD(getCurrentShape(), curve); + } + return; + } + reset = false; + if (is_applied_on && prevslice) { + sp_lpe_item_update_patheffect(sp_lpe_item, false, false); + for (auto link : prevslice->lpesatellites.data()) { + if (link && link->isAttached()) { + SPGroup *spgrp = dynamic_cast<SPGroup *>(link->getObject()); + SPShape *spit = dynamic_cast<SPShape *>(link->getObject()); + Glib::ustring transform = ""; + Glib::ustring patheffects = ""; + Geom::OptRect _gbbox = Geom::OptRect(); + if (spgrp) { + if (spgrp->getAttribute("transform")) { + transform = spgrp->getAttribute("transform"); + } + if (spgrp->getAttribute("inkscape:path-effect")) { + patheffects = spgrp->getAttribute("inkscape:path-effect"); + } + spgrp->setAttribute("transform", nullptr); + spgrp->setAttribute("inkscape:path-effect", nullptr); + _gbbox = spgrp->geometricBounds(); + } + if (spit || spgrp) { + for (auto link2 : lpesatellites.data()) { + if (link2 && link2->isAttached()) { + SPGroup *spgrp2 = dynamic_cast<SPGroup *>(link2->getObject()); + SPShape *spit2 = dynamic_cast<SPShape *>(link2->getObject()); + if (spit && spit2) { + auto edit = SPCurve::copy(spit->curveForEdit()); + auto edit2 = SPCurve::copy(spit2->curveForEdit()); + Geom::OptRect _bbox = edit->get_pathvector().boundsFast(); + Geom::OptRect _bbox2 = edit2->get_pathvector().boundsFast(); + if (_bbox && _bbox2) { + (*_bbox).expandBy(1); + if ((*_bbox).contains(*_bbox2)) { + spit2->setAttribute("transform", spit->getAttribute("transform")); + spit2->setAttribute("inkscape:path-effect", spit->getAttribute("inkscape:path-effect")); + spit2->setAttribute("style", spit->getAttribute("style")); + } + } + } else if (spgrp && spgrp2) { + Geom::OptRect _gbbox2 = spgrp2->geometricBounds(); + if (_gbbox && _gbbox2) { + (*_gbbox).expandBy(1); + if ((*_gbbox).contains(*_gbbox2)) { + spgrp2->setAttribute("transform", transform); + spgrp2->setAttribute("inkscape:path-effect", patheffects); + cloneStyle(spgrp, spgrp2); + } + } + } + } + } + if (spgrp) { + spgrp->setAttribute("transform", transform); + spgrp->setAttribute("inkscape:path-effect", patheffects); + } + } + } + } + + } + } else { + for (auto itemrf : lpesatellites.data()) { + if (itemrf && itemrf->isAttached()) { + SPLPEItem *splpeitem = dynamic_cast<SPLPEItem *>(itemrf->getObject()); + if (splpeitem) { + splpeitem->setHidden(true); + sp_lpe_item_update_patheffect(splpeitem, false, false); + } + } + } + } +} + +bool +LPESlice::split(SPItem* item, SPCurve *curve, std::vector<std::pair<Geom::Line, size_t> > slicer, size_t splitindex, bool &creation) { + bool splited = false; + size_t nsplits = slicer.size(); + SPDocument *document = getSPDoc(); + if (!document) { + return splited; + } + + SPObject *elemref = nullptr; + if (!is_load && container != sp_lpe_item->parent) { + lpesatellites.read_from_SVG(); + return splited; + } + if (objindex < lpesatellites.data().size() && lpesatellites.data()[objindex]) { + elemref = lpesatellites.data()[objindex]->getObject(); + } + bool prevreset = reset; + + if (!elemref && item->getId()) { + + Glib::ustring elemref_id = Glib::ustring("slice-"); + elemref_id += Glib::ustring::format(slicer[splitindex].second); + elemref_id += "-"; + Glib::ustring clean_id = item->getId(); + //First check is to allow effects on "satellites" + SPLPEItem *lpeitem = dynamic_cast<SPLPEItem *>(item); + if (!lpeitem) { + return splited; + } + if (!lpeitem->hasPathEffectOfType(SLICE) && clean_id.find("slice-") != Glib::ustring::npos) { + clean_id = clean_id.replace(0,6,""); + elemref_id += clean_id; + } else { + elemref_id += clean_id; + } + creation = true; + if (is_load && (elemref = document->getObjectById(elemref_id))) { + legacy = true; + lpesatellites.link(elemref, objindex); + } else { + reset = true; + Inkscape::XML::Node *phantom = createPathBase(item); + if (!container) { + return splited; + } + elemref = container->appendChildRepr(phantom); + Inkscape::GC::release(phantom); + lpesatellites.link(elemref, objindex); + } + } + SPItem *other = dynamic_cast<SPItem *>(elemref); + if (other) { + objindex++; + other->setHidden(false); + if (nsplits) { + cloneD(item, other, false); + reset = prevreset; + splited = splititem(item, curve, slicer[splitindex], true); + splititem(other, nullptr, slicer[splitindex], false); + if (!splited) { + other->setHidden(true); + } + splitindex++; + if (nsplits > splitindex) { + SPLPEItem *splpeother = dynamic_cast<SPLPEItem *>(other); + SPLPEItem *splpeitem = dynamic_cast<SPLPEItem *>(item); + if (item == sp_lpe_item || !splpeitem->hasPathEffectOfType(SLICE)) { + split(item, curve, slicer, splitindex, creation); + if (other == sp_lpe_item || !splpeother->hasPathEffectOfType(SLICE)) { + split(other, nullptr, slicer, splitindex, creation); + } + } + } + } + } + return splited; +} + +std::vector<std::pair<Geom::Line, size_t> > +LPESlice::getSplitLines() { + std::vector<std::pair<Geom::Line, size_t> > splitlines; + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() >= 1) { + sp_lpe_item = lpeitems[0]; + } else { + return splitlines; + } + LPESlice *prevslice = dynamic_cast<LPESlice *>(sp_lpe_item->getPrevLPE(this)); + if (prevslice) { + splitlines = prevslice->getSplitLines(); + } + Geom::Line line_separation((Geom::Point)start_point, (Geom::Point)end_point); + size_t index = sp_lpe_item->getLPEIndex(this); + std::pair<Geom::Line, size_t> slice = std::make_pair(line_separation, index); + splitlines.push_back(slice); + return splitlines; +} + +Inkscape::XML::Node * +LPESlice::createPathBase(SPObject *elemref) { + SPDocument *document = getSPDoc(); + if (!document) { + return nullptr; + } + Inkscape::XML::Document *xml_doc = getSPDoc()->getReprDoc(); + Inkscape::XML::Node *prev = elemref->getRepr(); + SPGroup *group = dynamic_cast<SPGroup *>(elemref); + if (group) { + Inkscape::XML::Node *container = xml_doc->createElement("svg:g"); + container->setAttribute("transform", prev->attribute("transform")); + container->setAttribute("mask", prev->attribute("mask")); + container->setAttribute("clip-path", prev->attribute("clip-path")); + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + Inkscape::XML::Node *previous = nullptr; + for (auto sub_item : item_list) { + Inkscape::XML::Node *resultnode = createPathBase(sub_item); + container->addChild(resultnode, previous); + previous = resultnode; + } + return container; + } + Inkscape::XML::Node *resultnode = xml_doc->createElement("svg:path"); + resultnode->setAttribute("transform", prev->attribute("transform")); + resultnode->setAttribute("mask", prev->attribute("mask")); + resultnode->setAttribute("clip-path", prev->attribute("clip-path")); + return resultnode; +} + +void +LPESlice::cloneD(SPObject *orig, SPObject *dest, bool is_original) +{ + if (!is_original && !g_strcmp0(sp_lpe_item->getId(), orig->getId())) { + is_original = true; + } + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + SPItem *originalitem = dynamic_cast<SPItem *>(orig); + if ( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() == SP_GROUP(dest)->getItemCount() ) { + if (reset) { + cloneStyle(orig, dest); + } + if (!allow_transforms) { + auto str = sp_svg_transform_write(originalitem->transform); + dest->setAttributeOrRemoveIfEmpty("transform", str); + } + std::vector< SPObject * > childs = orig->childList(true); + size_t index = 0; + for (auto &child : childs) { + SPObject *dest_child = dest->nthChild(index); + cloneD(child, dest_child, is_original); + index++; + } + return; + } + + SPShape * shape = SP_SHAPE(orig); + SPPath * path = SP_PATH(dest); + SPLPEItem *splpeitem = dynamic_cast<SPLPEItem *>(path); + if (path && shape && splpeitem) { + SPCurve const *c = shape->curve(); + if (c && !c->is_empty()) { + auto str = sp_svg_write_path(c->get_pathvector()); + if (path->hasPathEffectRecursive()) { + sp_lpe_item_enable_path_effects(path, false); + dest->setAttribute("inkscape:original-d", str); + sp_lpe_item_enable_path_effects(path, true); + dest->setAttribute("d", str); + } else { + dest->setAttribute("d", str); + } + if (!allow_transforms) { + auto str = sp_svg_transform_write(originalitem->transform); + dest->setAttributeOrRemoveIfEmpty("transform", str); + } + if (reset) { + cloneStyle(orig, dest); + } + } + } +} + +static fill_typ GetFillTyp(SPItem *item) +{ + 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) { + return fill_nonZero; + } else if (val && strcmp(val, "evenodd") == 0) { + return fill_oddEven; + } else { + return fill_nonZero; + } +} + +bool +LPESlice::splititem(SPItem* item, SPCurve * curve, std::pair<Geom::Line, size_t> slicer, bool toggle, bool is_original) +{ + bool splited = false; + if (!is_original && !g_strcmp0(sp_lpe_item->getId(), item->getId())) { + is_original = true; + } + Geom::Line line_separation = slicer.first; + Geom::Point s = line_separation.initialPoint(); + Geom::Point e = line_separation.finalPoint(); + Geom::Point center = Geom::middle_point(s, e); + SPGroup *group = dynamic_cast<SPGroup *>(item); + if (group) { + std::vector<SPObject *> childs = group->childList(true); + for (auto &child : childs) { + SPItem *dest_child = dynamic_cast<SPItem *>(child); + // groups not need update curve + splited = splititem(dest_child, nullptr, slicer, toggle, is_original) ? true : splited; + } + if (!is_original && group->hasPathEffectRecursive()) { + sp_lpe_item_update_patheffect(group, false, false); + } + return splited; + } + SPShape *shape = dynamic_cast<SPShape *>(item); + SPPath *path = dynamic_cast<SPPath *>(item); + if (shape) { + SPCurve const *c; + c = shape->curve(); + if (c) { + Geom::PathVector original_pathv = pathv_to_linear_and_cubic_beziers(c->get_pathvector()); + sp_flatten(original_pathv, GetFillTyp(shape)); + Geom::PathVector path_out; + Geom::Affine t = shape->transform; + if (!dynamic_cast<SPGroup *>(sp_lpe_item)) { + t = Geom::identity(); + } + for (auto & path_it : original_pathv) { + path_it *= t; + if (path_it.empty()) { + continue; + } + Geom::PathVector tmp_pathvector; + double time_start = 0.0; + int position = 0; + bool end_open = false; + if (path_it.closed()) { + const Geom::Curve &closingline = path_it.back_closed(); + if (!are_near(closingline.initialPoint(), closingline.finalPoint())) { + end_open = true; + } + } + Geom::Path original = path_it; + if (end_open && path_it.closed()) { + original.close(false); + original.appendNew<Geom::LineSegment>( original.initialPoint() ); + original.close(true); + } + double dir = line_separation.angle(); + Geom::Ray ray = line_separation.ray(0); + double diagonal = Geom::distance(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + Geom::Rect bbox(Geom::Point(boundingbox_X.min(),boundingbox_Y.min()),Geom::Point(boundingbox_X.max(),boundingbox_Y.max())); + double size_divider = Geom::distance(center, bbox) + diagonal; + s = Geom::Point::polar(dir,size_divider) + center; + e = Geom::Point::polar(dir + Geom::rad_from_deg(180),size_divider) + center; + Geom::Path divider = Geom::Path(s); + divider.appendNew<Geom::LineSegment>(e); + std::vector<double> crossed; + if (Geom::are_near(s,e)) { + continue; + } + Geom::Crossings cs = crossings(original, divider); + for(auto & c : cs) { + crossed.push_back(c.ta); + } + double angle = Geom::deg_from_rad(ray.angle()); + bool toggleside = !(angle > 0 && angle < 180); + std::sort(crossed.begin(), crossed.end()); + for (double time_end : crossed) { + if (time_start != time_end && time_end - time_start > Geom::EPSILON) { + Geom::Path portion = original.portion(time_start, time_end); + if (!portion.empty()) { + Geom::Point middle = portion.pointAt((double)portion.size()/2.0); + position = Geom::sgn(Geom::cross(e - s, middle - s)); + if (toggleside) { + position *= -1; + } + if (toggle) { + position *= -1; + } + if (position == 1) { + tmp_pathvector.push_back(portion); + } + portion.clear(); + } + } + time_start = time_end; + } + position = Geom::sgn(Geom::cross(e - s, original.finalPoint() - s)); + if (toggleside) { + position *= -1; + } + if (toggle) { + position *= -1; + } + if (cs.size()!=0 && (position == 1)) { + if (time_start != original.size() && original.size() - time_start > Geom::EPSILON) { + Geom::Path portion = original.portion(time_start, original.size()); + if (!portion.empty()) { + if (!original.closed()) { + tmp_pathvector.push_back(portion); + } else { + if (cs.size() > 1 && tmp_pathvector.size() > 0 && tmp_pathvector[0].size() > 0 ) { + tmp_pathvector[0] = tmp_pathvector[0].reversed(); + portion = portion.reversed(); + portion.setInitial(tmp_pathvector[0].finalPoint()); + tmp_pathvector[0].append(portion); + tmp_pathvector[0] = tmp_pathvector[0].reversed(); + } else { + tmp_pathvector.push_back(portion); + } + } + portion.clear(); + } + } + } + if (cs.size() > 0 && original.closed()) { + for (auto &path : tmp_pathvector) { + if (!path.closed()) { + path.close(); + } + } + } + if (cs.size() == 0 && position == 1) { + splited = false; + tmp_pathvector.push_back(original); + } else { + splited = true; + } + tmp_pathvector *= t.inverse(); + path_out.insert(path_out.end(), tmp_pathvector.begin(), tmp_pathvector.end()); + + tmp_pathvector.clear(); + } + if (curve && is_original) { + curve->set_pathvector(path_out); + } + auto cpro = SPCurve::copy(shape->curve()); + if (cpro) { + shape->bbox_vis_cache_is_valid = false; + shape->bbox_geom_cache_is_valid = false; + cpro->set_pathvector(path_out); + shape->setCurveInsync(std::move(cpro)); + auto str = sp_svg_write_path(path_out); + if (!is_original && shape->hasPathEffectRecursive()) { + sp_lpe_item_enable_path_effects(shape, false); + if (path) { + shape->setAttribute("inkscape:original-d", str); + } else { + shape->setAttribute("d", str); + } + sp_lpe_item_enable_path_effects(shape, true); + } else { + shape->setAttribute("d", str); + } + } + } + } + return splited; +} + +void +LPESlice::doBeforeEffect (SPLPEItem const* lpeitem) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if (!lpesatellites.data().size()) { + lpesatellites.read_from_SVG(); + if (lpesatellites.data().size()) { + lpesatellites.update_satellites(); + } + } + using namespace Geom; + original_bbox(lpeitem, false, true); + Point point_a(boundingbox_X.max(), boundingbox_Y.min()); + Point point_b(boundingbox_X.max(), boundingbox_Y.max()); + Point point_c(boundingbox_X.middle(), boundingbox_Y.middle()); + if (center_vert) { + double dista = std::abs(end_point[Geom::Y] - boundingbox_Y.min()); + double distb = std::abs(start_point[Geom::Y] - boundingbox_Y.min()); + previous_center = Geom::Point(Geom::infinity(), g_random_double_range(0, 1000)); + end_point.param_setValue( + Geom::Point(center_point[Geom::X], dista <= distb ? boundingbox_Y.min() : boundingbox_Y.max()), true); + start_point.param_setValue( + Geom::Point(center_point[Geom::X], dista > distb ? boundingbox_Y.min() : boundingbox_Y.max()), true); + //force update + center_vert = false; + } else if (center_horiz) { + double dista = std::abs(end_point[Geom::X] - boundingbox_X.min()); + double distb = std::abs(start_point[Geom::X] - boundingbox_X.min()); + previous_center = Geom::Point(Geom::infinity(), g_random_double_range(0, 1000)); + end_point.param_setValue( + Geom::Point(dista <= distb ? boundingbox_X.min() : boundingbox_X.max(), center_point[Geom::Y]), true); + start_point.param_setValue( + Geom::Point(dista > distb ? boundingbox_X.min() : boundingbox_X.max(), center_point[Geom::Y]), true); + //force update + center_horiz = false; + } else { + if ((Geom::Point)start_point == (Geom::Point)end_point) { + start_point.param_setValue(point_a); + end_point.param_setValue(point_b); + previous_center = Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point); + center_point.param_setValue(previous_center); + return; + } + if (are_near(previous_center, (Geom::Point)center_point, 0.001)) { + center_point.param_setValue(Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point)); + } else { + Geom::Point trans = center_point - Geom::middle_point((Geom::Point)start_point, (Geom::Point)end_point); + start_point.param_setValue(start_point * trans); + end_point.param_setValue(end_point * trans); + } + } + if (allow_transforms_prev != allow_transforms) { + LPESlice *nextslice = dynamic_cast<LPESlice *>(sp_lpe_item->getNextLPE(this)); + while (nextslice) { + if (nextslice->allow_transforms != allow_transforms) { + nextslice->allow_transforms_prev = allow_transforms; + nextslice->allow_transforms.param_setValue(allow_transforms); + } + nextslice = dynamic_cast<LPESlice *>(sp_lpe_item->getNextLPE(nextslice)); + } + LPESlice *prevslice = dynamic_cast<LPESlice *>(sp_lpe_item->getPrevLPE(this)); + while (prevslice) { + if (prevslice->allow_transforms != allow_transforms) { + prevslice->allow_transforms_prev = allow_transforms; + prevslice->allow_transforms.param_setValue(allow_transforms); + } + prevslice = dynamic_cast<LPESlice *>(sp_lpe_item->getNextLPE(prevslice)); + } + } + allow_transforms_prev = allow_transforms; +} + +void LPESlice::cloneStyle(SPObject *orig, SPObject *dest) +{ + for (auto iter : orig->style->properties()) { + if (iter->style_src != SPStyleSrc::UNSET) { + auto key = iter->id(); + if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) { + const gchar *attr = orig->getAttribute(iter->name().c_str()); + if (attr) { + dest->setAttribute(iter->name(), attr); + } + } + } + } + dest->setAttribute("style", orig->getAttribute("style")); +} + +void +LPESlice::resetStyles(){ + std::vector<SPLPEItem *> lpeitems = getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + sp_lpe_item = lpeitems[0]; + LPESlice *nextslice = dynamic_cast<LPESlice *>(sp_lpe_item->getNextLPE(this)); + while (nextslice) { + nextslice->reset = true; + nextslice = dynamic_cast<LPESlice *>(sp_lpe_item->getNextLPE(nextslice)); + } + reset = true; + sp_lpe_item_update_patheffect(sp_lpe_item, false, false); + } +} + +void +LPESlice::doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) +{ + if (!is_visible) { + for (auto itemrf : lpesatellites.data()) { + if (itemrf && itemrf->isAttached()) { + SPLPEItem *splpeitem = dynamic_cast<SPLPEItem *>(itemrf->getObject()); + if (splpeitem) { + splpeitem->setHidden(true); + sp_lpe_item_update_patheffect(splpeitem, false, false); + } + } + } + } +} + + +void +LPESlice::doOnRemove(SPLPEItem const* lpeitem) +{ + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + return; + } + processObjects(LPE_ERASE); +} + +void +LPESlice::doOnApply (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + LPESlice *prevslice = dynamic_cast<LPESlice *>(sp_lpe_item->getPrevLPE(this)); + if (prevslice) { + allow_transforms_prev = prevslice->allow_transforms; + allow_transforms.param_setValue(prevslice->allow_transforms); + } + Point point_a(boundingbox_X.middle(), boundingbox_Y.min()); + Point point_b(boundingbox_X.middle(), boundingbox_Y.max()); + Point point_c(boundingbox_X.middle(), boundingbox_Y.middle()); + start_point.param_setValue(point_a, true); + start_point.param_update_default(point_a); + end_point.param_setValue(point_b, true); + end_point.param_update_default(point_b); + center_point.param_setValue(point_c, true); + end_point.param_update_default(point_c); + previous_center = center_point; + lpeversion.param_setValue("1.2", true); + lpesatellites.update_satellites(true); +} + + +Geom::PathVector +LPESlice::doEffect_path (Geom::PathVector const & path_in) +{ + return path_in; +} + +void +LPESlice::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + using namespace Geom; + hp_vec.clear(); + Geom::Path path; + Geom::Point s = start_point; + Geom::Point e = end_point; + path.start( s ); + path.appendNew<Geom::LineSegment>( e ); + Geom::PathVector helper; + helper.push_back(path); + hp_vec.push_back(helper); +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-slice.h b/src/live_effects/lpe-slice.h new file mode 100644 index 0000000..106a1ab --- /dev/null +++ b/src/live_effects/lpe-slice.h @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_SLICE_H +#define INKSCAPE_LPE_SLICE_H + +/** \file + * LPE <mirror_symmetry> implementation: mirrors a path with respect to a given line. + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * Jabiertxof + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilin Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/bool.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/satellitearray.h" + +namespace Inkscape { +namespace LivePathEffect { + + +class LPESlice : public Effect, GroupBBoxEffect { +public: + LPESlice(LivePathEffectObject *lpeobject); + ~LPESlice() override; + void doOnApply (SPLPEItem const* lpeitem) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + Gtk::Widget *newWidget() override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + void cloneStyle(SPObject *orig, SPObject *dest); + bool split(SPItem *item, SPCurve *curve, std::vector<std::pair<Geom::Line, size_t>> slicer, size_t splitindex, + bool &creation); + bool splititem(SPItem *item, SPCurve *curve, std::pair<Geom::Line, size_t> slicer, bool toggle, + bool is_original = false); + bool haschildslice(SPItem *item); + std::vector<std::pair<Geom::Line, size_t> > getSplitLines(); + void cloneD(SPObject *orig, SPObject *dest, bool is_original); + Inkscape::XML::Node *createPathBase(SPObject *elemref); + Geom::PathVector cutter(Geom::PathVector const & path_in); + void originalDtoD(SPShape const *shape, SPCurve *curve); + void originalDtoD(SPItem *item); + void resetStyles(); + void centerVert(); + void centerHoriz(); + SPObject *container; + +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + +private: + SatelliteArrayParam lpesatellites; + BoolParam allow_transforms; + PointParam start_point; + PointParam end_point; + PointParam center_point; + Geom::Point previous_center; + bool reset; + bool blockreset; + bool center_vert; + bool center_horiz; + bool allow_transforms_prev; + size_t objindex = 0; + bool legacy = false; + LPESlice(const LPESlice&) = delete; + LPESlice& operator=(const LPESlice&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-spiro.cpp b/src/live_effects/lpe-spiro.cpp new file mode 100644 index 0000000..e0464a2 --- /dev/null +++ b/src/live_effects/lpe-spiro.cpp @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#define INKSCAPE_LPE_SPIRO_C + +/* + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-spiro.h" + +#include "display/curve.h" +#include <2geom/curves.h> +#include "helper/geom-nodetype.h" +#include "helper/geom-curves.h" + +#include "live_effects/spiro.h" + +// For handling un-continuous paths: +#include "message-stack.h" +#include "inkscape.h" + +namespace Inkscape { +namespace LivePathEffect { + +LPESpiro::LPESpiro(LivePathEffectObject *lpeobject) : + Effect(lpeobject) +{ +} + +LPESpiro::~LPESpiro() += default; + +void +LPESpiro::doEffect(SPCurve * curve) +{ + sp_spiro_do_effect(curve); +} + +void sp_spiro_do_effect(SPCurve *curve){ + using Geom::X; + using Geom::Y; + + // Make copy of old path as it is changed during processing + Geom::PathVector const original_pathv = curve->get_pathvector(); + guint len = curve->get_segment_count() + 2; + + curve->reset(); + Spiro::spiro_cp *path = g_new (Spiro::spiro_cp, len); + int ip = 0; + + for(const auto & path_it : original_pathv) { + if (path_it.empty()) + continue; + + // start of path + { + Geom::Point p = path_it.initialPoint(); + path[ip].x = p[X]; + path[ip].y = p[Y]; + path[ip].ty = '{' ; // for closed paths, this is overwritten + ip++; + } + + // midpoints + Geom::Path::const_iterator curve_it1 = path_it.begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it.begin()); // outgoing curve + Geom::Path::const_iterator curve_endit = path_it.end_default(); // this determines when the loop has to stop + + while ( curve_it2 != curve_endit ) + { + /* This deals with the node between curve_it1 and curve_it2. + * Loop to end_default (so without last segment), loop ends when curve_it2 hits the end + * and then curve_it1 points to end or closing segment */ + Geom::Point p = curve_it1->finalPoint(); + path[ip].x = p[X]; + path[ip].y = p[Y]; + + // Determine type of spiro node this is, determined by the tangents (angles) of the curves + // TODO: see if this can be simplified by using /helpers/geom-nodetype.cpp:get_nodetype + bool this_is_line = is_straight_curve(*curve_it1); + bool next_is_line = is_straight_curve(*curve_it2); + + Geom::NodeType nodetype = Geom::get_nodetype(*curve_it1, *curve_it2); + + if ( nodetype == Geom::NODE_SMOOTH || nodetype == Geom::NODE_SYMM ) + { + if (this_is_line && !next_is_line) { + path[ip].ty = ']'; + } else if (next_is_line && !this_is_line) { + path[ip].ty = '['; + } else { + path[ip].ty = 'c'; + } + } else { + path[ip].ty = 'v'; + } + + ++curve_it1; + ++curve_it2; + ip++; + } + + // add last point to the spiropath + Geom::Point p = curve_it1->finalPoint(); + path[ip].x = p[X]; + path[ip].y = p[Y]; + if (path_it.closed()) { + // curve_it1 points to the (visually) closing segment. determine the match between first and this last segment (the closing node) + Geom::NodeType nodetype = Geom::get_nodetype(*curve_it1, path_it.front()); + switch (nodetype) { + case Geom::NODE_NONE: // can't happen! but if it does, it means the path isn't closed :-) + path[ip].ty = '}'; + ip++; + break; + case Geom::NODE_CUSP: + path[0].ty = path[ip].ty = 'v'; + break; + case Geom::NODE_SMOOTH: + case Geom::NODE_SYMM: + path[0].ty = path[ip].ty = 'c'; + break; + } + } else { + // set type to path closer + path[ip].ty = '}'; + ip++; + } + + // run subpath through spiro + int sp_len = ip; + Spiro::spiro_run(path, sp_len, *curve); + ip = 0; + } + + g_free (path); +} + +}; //namespace LivePathEffect +}; /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-spiro.h b/src/live_effects/lpe-spiro.h new file mode 100644 index 0000000..ce07de8 --- /dev/null +++ b/src/live_effects/lpe-spiro.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_SPIRO_H +#define INKSCAPE_LPE_SPIRO_H + +/* + * Inkscape::LPESpiro + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" + + +namespace Inkscape { +namespace LivePathEffect { + +class LPESpiro : public Effect { +public: + LPESpiro(LivePathEffectObject *lpeobject); + ~LPESpiro() override; + LPESpiro(const LPESpiro&) = delete; + LPESpiro& operator=(const LPESpiro&) = delete; + + LPEPathFlashType pathFlashType() const override { return SUPPRESS_FLASH; } + + void doEffect(SPCurve * curve) override; +}; + +void sp_spiro_do_effect(SPCurve *curve); + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-tangent_to_curve.cpp b/src/live_effects/lpe-tangent_to_curve.cpp new file mode 100644 index 0000000..62f5fcb --- /dev/null +++ b/src/live_effects/lpe-tangent_to_curve.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Implementation of tangent-to-curve LPE. + */ + +/* + * Authors: + * Johan Engelen + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-tangent_to_curve.h" + +#include "display/curve.h" + +#include "object/sp-shape.h" +#include "object/sp-object-group.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +namespace TtC { + +class KnotHolderEntityAttachPt : public LPEKnotHolderEntity { +public: + KnotHolderEntityAttachPt(LPETangentToCurve *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityLeftEnd : public LPEKnotHolderEntity { +public: + KnotHolderEntityLeftEnd(LPETangentToCurve *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +class KnotHolderEntityRightEnd : public LPEKnotHolderEntity +{ +public: + KnotHolderEntityRightEnd(LPETangentToCurve *effect) : LPEKnotHolderEntity(effect) {}; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; +}; + +} // namespace TtC + +LPETangentToCurve::LPETangentToCurve(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + angle(_("Angle:"), _("Additional angle between tangent and curve"), "angle", &wr, this, 0.0), + t_attach(_("Location along curve:"), _("Location of the point of attachment along the curve (between 0.0 and number-of-segments)"), "t_attach", &wr, this, 0.5), + length_left(_("Length left:"), _("Specifies the left end of the tangent"), "length-left", &wr, this, 150), + length_right(_("Length right:"), _("Specifies the right end of the tangent"), "length-right", &wr, this, 150) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + registerParameter(&angle); + registerParameter(&t_attach); + registerParameter(&length_left); + registerParameter(&length_right); +} + +LPETangentToCurve::~LPETangentToCurve() += default; + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPETangentToCurve::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + Piecewise<D2<SBasis> > output; + + ptA = pwd2_in.valueAt(t_attach); + derivA = unit_vector(derivative(pwd2_in).valueAt(t_attach)); + + // TODO: Why are positive angles measured clockwise, not counterclockwise? + Geom::Rotate rot(Geom::Rotate::from_degrees(-angle)); + derivA = derivA * rot; + + C = ptA - derivA * length_left; + D = ptA + derivA * length_right; + + output = Piecewise<D2<SBasis> >(D2<SBasis>(SBasis(C[X], D[X]), SBasis(C[Y], D[Y]))); + + return output; +} + +void +LPETangentToCurve::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) { + { + KnotHolderEntity *e = new TtC::KnotHolderEntityAttachPt(this); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TangentToCurvePT", + _("Adjust the point of attachment of the tangent")); + knotholder->add(e); + } + { + KnotHolderEntity *e = new TtC::KnotHolderEntityLeftEnd(this); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TangentToCurveLeftEnd", + _("Adjust the <b>left</b> end of the tangent")); + knotholder->add(e); + } + { + KnotHolderEntity *e = new TtC::KnotHolderEntityRightEnd(this); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TangetToCurveRightEnd", + _("Adjust the <b>right</b> end of the tangent")); + knotholder->add(e); + } +}; + +namespace TtC { + +void +KnotHolderEntityAttachPt::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + LPETangentToCurve* lpe = dynamic_cast<LPETangentToCurve *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + if ( !SP_IS_SHAPE(lpe->sp_lpe_item) ) { + //lpe->t_attach.param_set_value(0); + g_warning("LPEItem is not a path! %s:%d\n", __FILE__, __LINE__); + return; + } + Piecewise<D2<SBasis> > pwd2 = paths_to_pw( lpe->pathvector_before_effect ); + + double t0 = nearest_time(s, pwd2); + lpe->t_attach.param_set_value(t0); + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityLeftEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPETangentToCurve *lpe = dynamic_cast<LPETangentToCurve *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->derivA); + lpe->length_left.param_set_value(-lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +void +KnotHolderEntityRightEnd::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + LPETangentToCurve *lpe = dynamic_cast<LPETangentToCurve *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + double lambda = Geom::nearest_time(s, lpe->ptA, lpe->derivA); + lpe->length_right.param_set_value(lambda); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point +KnotHolderEntityAttachPt::knot_get() const +{ + LPETangentToCurve const *lpe = dynamic_cast<LPETangentToCurve const*>(_effect); + return lpe->ptA; +} + +Geom::Point +KnotHolderEntityLeftEnd::knot_get() const +{ + LPETangentToCurve const *lpe = dynamic_cast<LPETangentToCurve const*>(_effect); + return lpe->C; +} + +Geom::Point +KnotHolderEntityRightEnd::knot_get() const +{ + LPETangentToCurve const *lpe = dynamic_cast<LPETangentToCurve const*>(_effect); + return lpe->D; +} + +} // namespace TtC + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-tangent_to_curve.h b/src/live_effects/lpe-tangent_to_curve.h new file mode 100644 index 0000000..32cacd1 --- /dev/null +++ b/src/live_effects/lpe-tangent_to_curve.h @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_TANGENT_TO_CURVE_H +#define INKSCAPE_LPE_TANGENT_TO_CURVE_H + +/** \file + * LPE <tangent_to_curve> implementation, see lpe-tangent_to_curve.cpp. + */ + +/* + * Authors: + * Johan Engelen + * Maximilian Albert + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace TtC { + // we need a separate namespace to avoid clashes with LPEPerpBisector + class KnotHolderEntityLeftEnd; + class KnotHolderEntityRightEnd; + class KnotHolderEntityAttachPt; +} + +class LPETangentToCurve : public Effect { +public: + LPETangentToCurve(LivePathEffectObject *lpeobject); + ~LPETangentToCurve() override; + Geom::Piecewise<Geom::D2<Geom::SBasis> > + doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + /* the knotholder entity classes must be declared friends */ + friend class TtC::KnotHolderEntityLeftEnd; + friend class TtC::KnotHolderEntityRightEnd; + friend class TtC::KnotHolderEntityAttachPt; + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + +private: + ScalarParam angle; + + ScalarParam t_attach; + ScalarParam length_left; + ScalarParam length_right; + + Geom::Point ptA; // point of attachment to the curve + Geom::Point derivA; + + Geom::Point C; // left end of tangent + Geom::Point D; // right end of tangent + + LPETangentToCurve(const LPETangentToCurve&) = delete; + LPETangentToCurve& operator=(const LPETangentToCurve&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-taperstroke.cpp b/src/live_effects/lpe-taperstroke.cpp new file mode 100644 index 0000000..f78012e --- /dev/null +++ b/src/live_effects/lpe-taperstroke.cpp @@ -0,0 +1,590 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Taper Stroke path effect, provided as an alternative to Power Strokes + * for otherwise constant-width paths. + * + * Authors: + * Liam P White + * + * Copyright (C) 2014-2020 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-taperstroke.h" +#include "live_effects/fill-conversion.h" + +#include <2geom/circle.h> +#include <2geom/sbasis-to-bezier.h> + +#include "style.h" + +#include "display/curve.h" +#include "helper/geom-nodetype.h" +#include "helper/geom-pathstroke.h" +#include "object/sp-shape.h" +#include "svg/svg-color.h" +#include "svg/css-ostringstream.h" +#include "svg/svg.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +template<typename T> +inline bool withinRange(T value, T low, T high) { + return (value > low && value < high); +} + +namespace Inkscape { +namespace LivePathEffect { + +namespace TpS { + class KnotHolderEntityAttachBegin : public LPEKnotHolderEntity { + public: + KnotHolderEntityAttachBegin(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {} + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + Geom::Point knot_get() const override; + }; + + class KnotHolderEntityAttachEnd : public LPEKnotHolderEntity { + public: + KnotHolderEntityAttachEnd(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {} + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + Geom::Point knot_get() const override; + }; +} // TpS + +static const Util::EnumData<unsigned> JoinType[] = { + // clang-format off + {JOIN_BEVEL, N_("Beveled"), "bevel"}, + {JOIN_ROUND, N_("Rounded"), "round"}, + {JOIN_MITER, N_("Miter"), "miter"}, + {JOIN_EXTRAPOLATE, N_("Extrapolated"), "extrapolated"}, + // clang-format on +}; + +enum TaperShape { + TAPER_CENTER, + TAPER_RIGHT, + TAPER_LEFT, + LAST_SHAPE +}; + +static const Util::EnumData<unsigned> TaperShapeType[] = { + {TAPER_CENTER, N_("Center"), "center"}, + {TAPER_LEFT, N_("Left"), "left"}, + {TAPER_RIGHT, N_("Right"), "right"}, +}; + +static const Util::EnumDataConverter<unsigned> JoinTypeConverter(JoinType, sizeof (JoinType)/sizeof(*JoinType)); +static const Util::EnumDataConverter<unsigned> TaperShapeTypeConverter(TaperShapeType, sizeof (TaperShapeType)/sizeof(*TaperShapeType)); + +LPETaperStroke::LPETaperStroke(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + line_width(_("Stroke width:"), _("The (non-tapered) width of the path"), "stroke_width", &wr, this, 1.), + attach_start(_("Start offset:"), _("Taper distance from path start"), "attach_start", &wr, this, 0.2), + attach_end(_("End offset:"), _("The ending position of the taper"), "end_offset", &wr, this, 0.2), + start_smoothing(_("Start smoothing:"), _("Amount of smoothing to apply to the start taper"), "start_smoothing", &wr, this, 0.5), + end_smoothing(_("End smoothing:"), _("Amount of smoothing to apply to the end taper"), "end_smoothing", &wr, this, 0.5), + join_type(_("Join type:"), _("Join type for non-smooth nodes"), "jointype", JoinTypeConverter, &wr, this, JOIN_EXTRAPOLATE), + start_shape(_("Start direction:"), _("Direction of the taper at the path start"), "start_shape", TaperShapeTypeConverter, &wr, this, TAPER_CENTER), + end_shape(_("End direction:"), _("Direction of the taper at the path end"), "end_shape", TaperShapeTypeConverter, &wr, this, TAPER_CENTER), + miter_limit(_("Miter limit:"), _("Limit for miter joins"), "miter_limit", &wr, this, 100.) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + + attach_start.param_set_digits(3); + attach_end.param_set_digits(3); + + registerParameter(&line_width); + registerParameter(&attach_start); + registerParameter(&attach_end); + registerParameter(&start_smoothing); + registerParameter(&end_smoothing); + registerParameter(&join_type); + registerParameter(&start_shape); + registerParameter(&end_shape); + registerParameter(&miter_limit); +} + +// from LPEPowerStroke -- sets fill if stroke color because we will +// be converting to a fill to make the new join. + +void LPETaperStroke::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke) { + line_width.param_transform_multiply(postmul, false); + } +} + +void LPETaperStroke::doOnApply(SPLPEItem const* lpeitem) +{ + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + auto item = dynamic_cast<SPShape *>(lpeitem_mutable); + + if (!item) { + printf("WARNING: It only makes sense to apply Taper stroke to paths (not groups).\n"); + } + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed : 1.; + + lpe_shape_convert_stroke_and_fill(item); + + Glib::ustring pref_path = (Glib::ustring)"/live_effects/" + + (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() + + (Glib::ustring)"/" + + (Glib::ustring)"stroke_width"; + + bool valid = prefs->getEntry(pref_path).isValid(); + + if (!valid) { + line_width.param_set_value(width); + } + + line_width.write_to_SVG(); +} + +void LPETaperStroke::doOnRemove(SPLPEItem const* lpeitem) +{ + auto lpeitem_mutable = const_cast<SPLPEItem *>(lpeitem); + auto item = dynamic_cast<SPShape *>(lpeitem_mutable); + + if (!item) { + return; + } + + lpe_shape_revert_stroke_and_fill(item, line_width); +} + +using Geom::Piecewise; +using Geom::D2; +using Geom::SBasis; +// leave Geom::Path + +static Geom::Path return_at_first_cusp(Geom::Path const & path_in, double /*smooth_tolerance*/ = 0.05) +{ + Geom::Path temp; + + for (unsigned i = 0; i < path_in.size(); i++) { + temp.append(path_in[i]); + if (path_in.size() > i+1) { + if (Geom::get_nodetype(path_in[i], path_in[i + 1]) != Geom::NODE_SMOOTH ) { + break; + } + } + } + + return temp; +} + +Piecewise<D2<SBasis> > stretch_along(Piecewise<D2<SBasis> > pwd2_in, Geom::Path pattern, double width); + +// actual effect + +Geom::PathVector LPETaperStroke::doEffect_path(Geom::PathVector const& path_in) +{ + Geom::Path first_cusp = return_at_first_cusp(path_in[0]); + Geom::Path last_cusp = return_at_first_cusp(path_in[0].reversed()); + + bool zeroStart = false; // [distance from start taper knot -> start of path] == 0 + bool zeroEnd = false; // [distance from end taper knot -> end of path] == 0 + bool metInMiddle = false; // knots are touching + + // there is a pretty good chance that people will try to drag the knots + // on top of each other, so block it + + unsigned size = path_in[0].size(); + if (size == first_cusp.size()) { + // check to see if the knots were dragged over each other + // if so, reset the end offset, but still allow the start offset. + if ( attach_start >= (size - attach_end) ) { + attach_end.param_set_value( size - attach_start ); + metInMiddle = true; + } + } + + if (attach_start == size - attach_end) { + metInMiddle = true; + } + if (attach_end == size - attach_start) { + metInMiddle = true; + } + + // don't let it be integer (TODO this is stupid!) + { + if (double(unsigned(attach_start)) == attach_start) { + attach_start.param_set_value(attach_start - 0.00001); + } + if (double(unsigned(attach_end)) == attach_end) { + attach_end.param_set_value(attach_end - 0.00001); + } + } + + unsigned allowed_start = first_cusp.size(); + unsigned allowed_end = last_cusp.size(); + + // don't let the knots be farther than they are allowed to be + { + if ((unsigned)attach_start >= allowed_start) { + attach_start.param_set_value((double)allowed_start - 0.00001); + } + if ((unsigned)attach_end >= allowed_end) { + attach_end.param_set_value((double)allowed_end - 0.00001); + } + } + + // don't let it be zero (this is stupid too!) + if (attach_start < 0.0000001 || withinRange(double(attach_start), 0.00000001, 0.000001)) { + attach_start.param_set_value( 0.0000001 ); + zeroStart = true; + } + if (attach_end < 0.0000001 || withinRange(double(attach_end), 0.00000001, 0.000001)) { + attach_end.param_set_value( 0.0000001 ); + zeroEnd = true; + } + + // Path::operator () means get point at time t + start_attach_point = first_cusp(attach_start); + end_attach_point = last_cusp(attach_end); + Geom::PathVector pathv_out; + + // the following function just splits it up into three pieces. + pathv_out = doEffect_simplePath(path_in); + + // now for the actual tapering. the stretch_along method (stolen from PaP) is used to accomplish this + + Geom::PathVector real_pathv; + Geom::Path real_path; + Geom::PathVector pat_vec; + Piecewise<D2<SBasis> > pwd2; + Geom::Path throwaway_path; + + if (!zeroStart) { + // Construct the pattern + std::stringstream pat_str; + pat_str.imbue(std::locale::classic()); + + switch (start_shape.get_value()) { + case TAPER_RIGHT: + pat_str << "M 1,0 Q " << 1 - (double)start_smoothing << ",0 0,1 L 1,1"; + break; + case TAPER_LEFT: + pat_str << "M 1,0 L 0,0 Q " << 1 - (double)start_smoothing << ",1 1,1"; + break; + default: + pat_str << "M 1,0 C " << 1 - (double)start_smoothing << ",0 0,0.5 0,0.5 0,0.5 " << 1 - (double)start_smoothing << ",1 1,1"; + break; + } + + pat_vec = sp_svg_read_pathv(pat_str.str().c_str()); + pwd2.concat(stretch_along(pathv_out[0].toPwSb(), pat_vec[0], fabs(line_width))); + throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0]; + + real_path.append(throwaway_path); + } + + // if this condition happens to evaluate false, i.e. there was no space for a path to be drawn, it is simply skipped. + // although this seems obvious, it can probably lead to bugs. + if (!metInMiddle) { + // append the outside outline of the path (goes with the direction of the path) + throwaway_path = half_outline(pathv_out[1], fabs(line_width)/2., miter_limit, static_cast<LineJoinType>(join_type.get_value())); + if (!zeroStart && real_path.size() >= 1 && throwaway_path.size() >= 1) { + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint())) { + real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + } + real_path.append(throwaway_path); + } + + if (!zeroEnd) { + // append the ending taper + std::stringstream pat_str_1; + pat_str_1.imbue(std::locale::classic()); + + switch (end_shape.get_value()) { + case TAPER_RIGHT: + pat_str_1 << "M 0,1 L 1,1 Q " << (double)end_smoothing << ",0 0,0"; + break; + case TAPER_LEFT: + pat_str_1 << "M 0,1 Q " << (double)end_smoothing << ",1 1,0 L 0,0"; + break; + default: + pat_str_1 << "M 0,1 C " << (double)end_smoothing << ",1 1,0.5 1,0.5 1,0.5 " << (double)end_smoothing << ",0 0,0"; + break; + } + + pat_vec = sp_svg_read_pathv(pat_str_1.str().c_str()); + + pwd2 = Piecewise<D2<SBasis> >(); + pwd2.concat(stretch_along(pathv_out[2].toPwSb(), pat_vec[0], fabs(line_width))); + + throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0]; + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) { + real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + real_path.append(throwaway_path); + } + + if (!metInMiddle) { + // append the inside outline of the path (against direction) + throwaway_path = half_outline(pathv_out[1].reversed(), fabs(line_width)/2., miter_limit, static_cast<LineJoinType>(join_type.get_value())); + + if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) { + real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint()); + } else { + real_path.setFinal(throwaway_path.initialPoint()); + } + real_path.append(throwaway_path); + } + + if (!Geom::are_near(real_path.finalPoint(), real_path.initialPoint())) { + real_path.appendNew<Geom::LineSegment>(real_path.initialPoint()); + } else { + real_path.setFinal(real_path.initialPoint()); + } + real_path.close(); + + real_pathv.push_back(real_path); + + return real_pathv; +} + +/** + * @return Always returns a PathVector with three elements. + * + * The positions of the effect knots are accessed to determine + * where exactly the input path should be split. + */ +Geom::PathVector LPETaperStroke::doEffect_simplePath(Geom::PathVector const & path_in) +{ + Geom::Coord endTime = path_in[0].size() - attach_end; + + Geom::Path p1 = path_in[0].portion(0., attach_start); + Geom::Path p2 = path_in[0].portion(attach_start, endTime); + Geom::Path p3 = path_in[0].portion(endTime, path_in[0].size()); + + Geom::PathVector out; + out.push_back(p1); + out.push_back(p2); + out.push_back(p3); + + return out; +} + + +/** + * Most of the below function is verbatim from Pattern Along Path. However, it needed a little + * tweaking to get it to work right in this case. Also, large portions of the effect have been + * stripped out as I deemed them unnecessary for the relative simplicity of this effect. + */ +Piecewise<D2<SBasis> > stretch_along(Piecewise<D2<SBasis> > pwd2_in, Geom::Path pattern, double prop_scale) +{ + using namespace Geom; + + // Don't allow empty path parameter: + if ( pattern.empty() ) { + return pwd2_in; + } + + /* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */ + Piecewise<D2<SBasis> > output; + std::vector<Piecewise<D2<SBasis> > > pre_output; + + D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pattern.toPwSb()); + Piecewise<SBasis> x0 = Piecewise<SBasis>(patternd2[0]); + Piecewise<SBasis> y0 = Piecewise<SBasis>(patternd2[1]); + OptInterval pattBndsX = bounds_exact(x0); + OptInterval pattBndsY = bounds_exact(y0); + if (pattBndsX && pattBndsY) { + x0 -= pattBndsX->min(); + y0 -= pattBndsY->middle(); + + double noffset = 0; + double toffset = 0; + // Prevent more than 90% overlap... + + y0+=noffset; + + std::vector<Piecewise<D2<SBasis> > > paths_in; + paths_in = split_at_discontinuities(pwd2_in); + + for (auto path_i : paths_in) { + Piecewise<SBasis> x = x0; + Piecewise<SBasis> y = y0; + Piecewise<D2<SBasis> > uskeleton = arc_length_parametrization(path_i,2,.1); + uskeleton = remove_short_cuts(uskeleton,.01); + Piecewise<D2<SBasis> > n = rot90(derivative(uskeleton)); + n = force_continuity(remove_short_cuts(n,.1)); + + int nbCopies = 0; + double scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent(); + nbCopies = 1; + + double pattWidth = pattBndsX->extent() * scaling; + + if (scaling != 1.0) { + x*=scaling; + } + if ( false ) { + y*=(scaling*prop_scale); + } else { + if (prop_scale != 1.0) y *= prop_scale; + } + x += toffset; + + double offs = 0; + for (int i=0; i<nbCopies; i++) { + if (false) { + Piecewise<D2<SBasis> > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs); + std::vector<Piecewise<D2<SBasis> > > splited_output_piece = split_at_discontinuities(output_piece); + pre_output.insert(pre_output.end(), splited_output_piece.begin(), splited_output_piece.end() ); + } else { + output.concat(compose(uskeleton,x+offs)+y*compose(n,x+offs)); + } + offs+=pattWidth; + } + } + return output; + } else { + return pwd2_in; + } +} + +void LPETaperStroke::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + KnotHolderEntity *e = new TpS::KnotHolderEntityAttachBegin(this); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TaperStrokeBegin", + _("<b>Start point of the taper</b>: drag to alter the taper, <b>Shift+click</b> changes the taper direction")); + knotholder->add(e); + + KnotHolderEntity *f = new TpS::KnotHolderEntityAttachEnd(this); + f->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TaperStrokeEnd", + _("<b>End point of the taper</b>: drag to alter the taper, <b>Shift+click</b> changes the taper direction")); + knotholder->add(f); +} + +namespace TpS { + +void KnotHolderEntityAttachBegin::knot_set(Geom::Point const &p, Geom::Point const&/*origin*/, guint state) +{ + using namespace Geom; + + LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + if (!SP_IS_SHAPE(lpe->sp_lpe_item)) { + printf("WARNING: LPEItem is not a path!\n"); + return; + } + + if (!SP_SHAPE(lpe->sp_lpe_item)->curve()) { + // oops + return; + } + // in case you are wondering, the above are simply sanity checks. we never want to actually + // use that object. + + Geom::PathVector pathv = lpe->pathvector_before_effect; + Piecewise<D2<SBasis> > pwd2; + Geom::Path p_in = return_at_first_cusp(pathv[0]); + pwd2.concat(p_in.toPwSb()); + + double t0 = nearest_time(s, pwd2); + lpe->attach_start.param_set_value(t0); + + // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating. + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, true); +} + +void KnotHolderEntityAttachBegin::knot_click(guint state) +{ + if (!(state & GDK_SHIFT_MASK)) { + return; + } + + LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect); + + lpe->start_shape.param_set_value((lpe->start_shape.get_value() + 1) % LAST_SHAPE); + lpe->start_shape.write_to_SVG(); +} + +void KnotHolderEntityAttachEnd::knot_click(guint state) +{ + if (!(state & GDK_SHIFT_MASK)) { + return; + } + + LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect); + + lpe->end_shape.param_set_value((lpe->end_shape.get_value() + 1) % LAST_SHAPE); + lpe->end_shape.write_to_SVG(); +} + +void KnotHolderEntityAttachEnd::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + using namespace Geom; + + LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + + if (!SP_IS_SHAPE(lpe->sp_lpe_item) ) { + printf("WARNING: LPEItem is not a path!\n"); + return; + } + + if (!SP_SHAPE(lpe->sp_lpe_item)->curve()) { + // oops + return; + } + Geom::PathVector pathv = lpe->pathvector_before_effect; + Geom::Path p_in = return_at_first_cusp(pathv[0].reversed()); + Piecewise<D2<SBasis> > pwd2 = p_in.toPwSb(); + + double t0 = nearest_time(s, pwd2); + lpe->attach_end.param_set_value(t0); + + sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true); +} + +Geom::Point KnotHolderEntityAttachBegin::knot_get() const +{ + LPETaperStroke const * lpe = dynamic_cast<LPETaperStroke const*> (_effect); + return lpe->start_attach_point; +} + +Geom::Point KnotHolderEntityAttachEnd::knot_get() const +{ + LPETaperStroke const * lpe = dynamic_cast<LPETaperStroke const*> (_effect); + return lpe->end_attach_point; +} + +} // namespace TpS +} // namespace LivePathEffect +} // namespace Inkscape + +/* + 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 : diff --git a/src/live_effects/lpe-taperstroke.h b/src/live_effects/lpe-taperstroke.h new file mode 100644 index 0000000..21aa796 --- /dev/null +++ b/src/live_effects/lpe-taperstroke.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * @brief Taper Stroke path effect (meant as a replacement for using Power Strokes for tapering) + */ +/* Authors: + * Liam P White <inkscapebrony@gmail.com> + * Copyright (C) 2014 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_LPE_TAPERSTROKE_H +#define INKSCAPE_LPE_TAPERSTROKE_H + +#include "live_effects/parameter/enum.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/vector.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace TpS { +// we need a separate namespace to avoid clashes with other LPEs +class KnotHolderEntityAttachBegin; +class KnotHolderEntityAttachEnd; +} + +class LPETaperStroke : public Effect { +public: + LPETaperStroke(LivePathEffectObject *lpeobject); + ~LPETaperStroke() override = default; + + void doOnApply(SPLPEItem const* lpeitem) override; + void doOnRemove(SPLPEItem const* lpeitem) override; + + Geom::PathVector doEffect_path (Geom::PathVector const& path_in) override; + Geom::PathVector doEffect_simplePath(Geom::PathVector const& path_in); + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; + + friend class TpS::KnotHolderEntityAttachBegin; + friend class TpS::KnotHolderEntityAttachEnd; +private: + ScalarParam line_width; + ScalarParam attach_start; + ScalarParam attach_end; + ScalarParam start_smoothing; + ScalarParam end_smoothing; + EnumParam<unsigned> join_type; + EnumParam<unsigned> start_shape; + EnumParam<unsigned> end_shape; + ScalarParam miter_limit; + + Geom::Point start_attach_point; + Geom::Point end_attach_point; + + LPETaperStroke(const LPETaperStroke&) = delete; + LPETaperStroke& operator=(const LPETaperStroke&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-test-doEffect-stack.cpp b/src/live_effects/lpe-test-doEffect-stack.cpp new file mode 100644 index 0000000..30b9388 --- /dev/null +++ b/src/live_effects/lpe-test-doEffect-stack.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-test-doEffect-stack.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + + +LPEdoEffectStackTest::LPEdoEffectStackTest(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + step(_("Stack step:"), ("How deep we should go into the stack"), "step", &wr, this), + point(_("Point param:"), "tooltip of point parameter", "point_param", &wr, this), + path(_("Path param:"), "tooltip of path parameter", "path_param", &wr, this,"M 0,100 100,0") +{ + registerParameter(&step); + registerParameter(&point); + registerParameter(&path); + + point.set_oncanvas_looks(Inkscape::CANVAS_ITEM_CTRL_SHAPE_SQUARE, + Inkscape::CANVAS_ITEM_CTRL_MODE_XOR, 0x00ff0000); + point.param_setValue(point); +} + +LPEdoEffectStackTest::~LPEdoEffectStackTest() += default; + +void +LPEdoEffectStackTest::doEffect (SPCurve * curve) +{ + if (step >= 1) { + Effect::doEffect(curve); + } else { + // return here + return; + } +} + +Geom::PathVector +LPEdoEffectStackTest::doEffect_path (Geom::PathVector const &path_in) +{ + if (step >= 2) { + return Effect::doEffect_path(path_in); + } else { + // return here + Geom::PathVector path_out = path_in; + return path_out; + } +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPEdoEffectStackTest::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + Geom::Piecewise<Geom::D2<Geom::SBasis> > output = pwd2_in; + + return output; +} + + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-test-doEffect-stack.h b/src/live_effects/lpe-test-doEffect-stack.h new file mode 100644 index 0000000..1103e89 --- /dev/null +++ b/src/live_effects/lpe-test-doEffect-stack.h @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_DOEFFECT_STACK_H +#define INKSCAPE_LPE_DOEFFECT_STACK_H + +/* + * Inkscape::LPEdoEffectStackTest + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * This effect is to test whether running up and down the doEffect stack does not change the original-d too much. + * i.e. for this effect, the output should match more or less exactly with the input. + * + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/path.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPEdoEffectStackTest : public Effect { +public: + LPEdoEffectStackTest(LivePathEffectObject *lpeobject); + ~LPEdoEffectStackTest() override; + + void doEffect (SPCurve * curve) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + +private: + ScalarParam step; + PointParam point; + PathParam path; + + LPEdoEffectStackTest(const LPEdoEffectStackTest&) = delete; + LPEdoEffectStackTest& operator=(const LPEdoEffectStackTest&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpe-text_label.cpp b/src/live_effects/lpe-text_label.cpp new file mode 100644 index 0000000..aa50296 --- /dev/null +++ b/src/live_effects/lpe-text_label.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <text_label> implementation + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-text_label.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +LPETextLabel::LPETextLabel(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + label(_("Label:"), _("Text label attached to the path"), "label", &wr, this, "This is a label") +{ + registerParameter(&label); +} + +LPETextLabel::~LPETextLabel() += default; + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPETextLabel::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + using namespace Geom; + + double t = (pwd2_in.cuts.front() + pwd2_in.cuts.back()) / 2; + Point pos(pwd2_in.valueAt(t)); + Point dir(unit_vector(derivative(pwd2_in).valueAt(t))); + Point n(-rot90(dir) * 30); + + double angle = angle_between(dir, Point(1,0)); + label.setPos(pos + n); + label.setAnchor(std::sin(angle), -std::cos(angle)); + + return pwd2_in; +} + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-text_label.h b/src/live_effects/lpe-text_label.h new file mode 100644 index 0000000..61fff9a --- /dev/null +++ b/src/live_effects/lpe-text_label.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_TEXT_LABEL_H +#define INKSCAPE_LPE_TEXT_LABEL_H + +/** \file + * LPE <text_label> implementation + */ +/* + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/parameter/text.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPETextLabel : public Effect { +public: + LPETextLabel(LivePathEffectObject *lpeobject); + ~LPETextLabel() override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + +private: + TextParam label; + + LPETextLabel(const LPETextLabel&) = delete; + LPETextLabel& operator=(const LPETextLabel&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-tiling.cpp b/src/live_effects/lpe-tiling.cpp new file mode 100644 index 0000000..c094732 --- /dev/null +++ b/src/live_effects/lpe-tiling.cpp @@ -0,0 +1,1639 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE <tiling> implementation + */ +/* + * Authors: + * Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * Adam Belis <> + * Copyright (C) Authors 2022-2022 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpe-tiling.h" + +#include <2geom/intersection-graph.h> +#include <2geom/path-intersection.h> +#include <2geom/sbasis-to-bezier.h> +#include <gdk/gdk.h> +#include <gtkmm.h> + +#include "display/curve.h" +#include "helper/geom.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/satellite-reference.h" +#include "object/sp-object.h" +#include "object/sp-path.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "path-chemistry.h" +#include "path/path-boolop.h" +#include "style.h" +#include "svg/path-string.h" +#include "svg/svg.h" +#include "xml/sp-css-attr.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +namespace CoS { + class KnotHolderEntityCopyGapX : public LPEKnotHolderEntity { + public: + KnotHolderEntityCopyGapX(LPETiling * effect) : LPEKnotHolderEntity(effect) {}; + ~KnotHolderEntityCopyGapX() override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + double startpos = dynamic_cast<LPETiling const*> (_effect)->gapx_unit; + }; + class KnotHolderEntityCopyGapY : public LPEKnotHolderEntity { + public: + KnotHolderEntityCopyGapY(LPETiling * effect) : LPEKnotHolderEntity(effect) {}; + ~KnotHolderEntityCopyGapY() override; + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + double startpos = dynamic_cast<LPETiling const*> (_effect)->gapy_unit; + }; +} // CoS + +LPETiling::LPETiling(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + // do not change name of this parameter is used in oncommit + unit(_("Unit:"), _("Unit"), "unit", &wr, this, "px"), + lpesatellites(_("lpesatellites"), _("Items satellites"), "lpesatellites", &wr, this, false), + num_cols(_("Columns"), _("Number of columns"), "num_cols", &wr, this, 3), + num_rows(_("Rows"), _("Number of rows"), "num_rows", &wr, this, 3), + gapx(_("Gap X"), _("Horizontal gap between tiles (uses selected unit)"), "gapx", &wr, this, 0.0), + gapy(_("Gap Y"), _("Vertical gap between tiles (uses selected unit)"), "gapy", &wr, this, 0.0), + scale(_("Scale %"), _("Scale tiles by this percentage"), "scale", &wr, this, 0.0), + rotate(_("Rotate °"), _("Rotate tiles by this amount of degrees"), "rotate", &wr, this, 0.0), + offset(_("Offset %"), _("Offset tiles by this percentage of width/height"), "offset", &wr, this, 0.0), + offset_type(_("Offset type"), _("Choose whether to offset rows or columns"), "offset_type", &wr, this, false), + interpolate_scalex(_("Interpolate scale X"), _("Interpolate tile size in each row"), "interpolate_scalex", &wr, this, false), + interpolate_scaley(_("Interpolate scale Y"), _("Interpolate tile size in each column"), "interpolate_scaley", &wr, this, true), + shrink_interp(_("Minimize gaps"), _("Minimize gaps between scaled objects (does not work with rotation/diagonal mode)"), "shrink_interp", &wr, this, false), + interpolate_rotatex(_("Interpolate rotation X"), _("Interpolate tile rotation in row"), "interpolate_rotatex", &wr, this, false), + interpolate_rotatey(_("Interpolate rotation Y"), _("Interpolate tile rotation in column"), "interpolate_rotatey", &wr, this, true), + split_items(_("Split elements"), _("Split elements, so they can be selected, styled, and moved (if grouped) independently"), "split_items", &wr, this, false), + mirrorrowsx(_("Mirror rows in X"), _("Mirror rows horizontally"), "mirrorrowsx", &wr, this, false), + mirrorrowsy(_("Mirror rows in Y"), _("Mirror rows vertically"), "mirrorrowsy", &wr, this, false), + mirrorcolsx(_("Mirror cols in X"), _("Mirror columns horizontally"), "mirrorcolsx", &wr, this, false), + mirrorcolsy(_("Mirror cols in Y"), _("Mirror columns vertically"), "mirrorcolsy", &wr, this, false), + mirrortrans(_("Mirror transforms"), _("Mirror transformations"), "mirrortrans", &wr, this, false), + link_styles(_("Link styles"), _("Link styles in split mode, can also be used to reset style of copies"), "link_styles", &wr, this, false), + random_gap_x(_("Random gaps X"), _("Randomize horizontal gaps"), "random_gap_x", &wr, this, false), + random_gap_y(_("Random gaps Y"), _("Randomize vertical gaps"), "random_gap_y", &wr, this, false), + random_rotate(_("Random rotation"), _("Randomize tile rotation"), "random_rotate", &wr, this, false), + random_scale(_("Random scale"), _("Randomize scale"), "random_scale", &wr, this, false), + seed(_("Seed"), _("Randomization seed"), "seed", &wr, this, 1.), + transformorigin("transformorigin:", "transformorigin","transformorigin", &wr, this, "", true) +{ + show_orig_path = true; + _provides_knotholder_entities = true; + // register all your parameters here, so Inkscape knows which parameters this effect has: + // please intense work on this widget and is important reorder parameters very carefully + registerParameter(&unit); + registerParameter(&seed); + registerParameter(&lpesatellites); + registerParameter(&num_rows); + registerParameter(&num_cols); + registerParameter(&gapx); + registerParameter(&gapy); + registerParameter(&offset); + registerParameter(&offset_type); + registerParameter(&scale); + registerParameter(&rotate); + registerParameter(&mirrorrowsx); + registerParameter(&mirrorrowsy); + registerParameter(&mirrorcolsx); + registerParameter(&mirrorcolsy); + registerParameter(&mirrortrans); + registerParameter(&shrink_interp); + registerParameter(&split_items); + registerParameter(&link_styles); + registerParameter(&interpolate_scalex); + registerParameter(&interpolate_scaley); + registerParameter(&interpolate_rotatex); + registerParameter(&interpolate_rotatey); + registerParameter(&random_scale); + registerParameter(&random_rotate); + registerParameter(&random_gap_y); + registerParameter(&random_gap_x); + registerParameter(&transformorigin); + + num_cols.param_set_range(1, 9999);// we need the input a bit tiny so this seems enough + num_cols.param_make_integer(); + num_cols.param_set_increments(1, 10); + num_rows.param_set_range(1, 9999); + num_rows.param_make_integer(); + num_rows.param_set_increments(1, 10); + scale.param_set_range(-9999.99,9999.99); + scale.param_set_increments(1, 10); + gapx.param_set_range(-99999,99999); + gapx.param_set_increments(1.0, 10.0); + gapy.param_set_range(-99999,99999); + gapy.param_set_increments(1.0, 10.0); + rotate.param_set_increments(1.0, 10.0); + rotate.param_set_range(-900, 900); + offset.param_set_range(-300, 300); + offset.param_set_increments(1.0, 10.0); + seed.param_set_range(1.0, 1.0); + seed.param_set_randomsign(true); + apply_to_clippath_and_mask = true; + _provides_knotholder_entities = true; + prev_num_cols = num_cols; + prev_num_rows = num_rows; + _knotholder = nullptr; + reset = link_styles; + prev_unit = unit.get_abbreviation(); +} + +LPETiling::~LPETiling() +{ + keep_paths = false; + doOnRemove(nullptr); +}; + +bool LPETiling::doOnOpen(SPLPEItem const *lpeitem) +{ + bool fixed = false; + if (!is_load || is_applied) { + return fixed; + } + if (!split_items) { + return fixed; + } + lpesatellites.update_satellites(); + container = lpeitem->parent; + return fixed; +} + +void +LPETiling::doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) +{ + if (split_items) { + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + bool write = false; + bool active = !lpesatellites.data().size(); + for (auto lpereference : lpesatellites.data()) { + if (lpereference && lpereference->isAttached() && lpereference.get()->getObject() != nullptr) { + active = true; + } + } + if (!active && !is_load && prev_split) { + lpesatellites.clear(); + prev_num_cols = 0; + prev_num_rows = 0; + } + prev_split = split_items; + + container = sp_lpe_item->parent; + if (prev_num_cols * prev_num_rows != num_cols * num_rows) { + write = true; + size_t pos = 0; + for (auto lpereference : lpesatellites.data()) { + if (lpereference && lpereference->isAttached()) { + SPItem *copies = dynamic_cast<SPItem *>(lpereference->getObject()); + if (copies) { + if (pos > num_cols * num_rows - 2) { + copies->setHidden(true); + } else if (copies->isHidden()) { + copies->setHidden(false); + } + } + } + pos++; + } + prev_num_cols = num_cols; + prev_num_rows = num_rows; + } + if (!gap_bbox) { + return; + } + Geom::Point center = (*gap_bbox).midpoint() * transformoriginal.inverse(); + bool forcewrite = false; + Geom::Affine origin = Geom::Translate(center).inverse(); + if (!interpolate_rotatex && !interpolate_rotatey && !random_rotate) { + origin *= Geom::Rotate::from_degrees(rotate); + } + if (!interpolate_scalex && !interpolate_scaley && !random_scale) { + origin *= Geom::Scale(scaleok, scaleok); + } + origin *= Geom::Translate(center); + origin = origin.inverse(); + size_t counter = 0; + double gapscalex = 0; + double maxheight = 0; + double maxwidth = 0; + double minheight = std::numeric_limits<double>::max(); + double y[(int)num_cols]; + double ygap[(int)num_cols]; + double yset = 0; + Geom::OptRect prev_bbox; + Geom::OptRect bbox = sp_lpe_item->geometricBounds(); + + Geom::Affine base_transform = sp_item_transform_repr(sp_lpe_item); + Geom::Affine gapp = base_transform.inverse() * transformoriginal; + Geom::Point spcenter_base = (*sp_lpe_item->geometricBounds(transformoriginal)).midpoint(); + Geom::Point spcenter = (*sp_lpe_item->geometricBounds(base_transform)).midpoint(); + Geom::Affine gap = gapp.withoutTranslation(); + if (!bbox) { + return; + } + (*bbox) *= transformoriginal; + for (int i = 0; i < num_rows; ++i) { + double fracy = 1; + if (num_rows != 1) { + fracy = i/(double)(num_rows - 1); + } + for (int j = 0; j < num_cols; ++j) { + double x = 0; + double fracx = 1; + if (num_cols != 1) { + fracx = j/(double)(num_cols - 1); + } + Geom::Affine r = Geom::identity(); + Geom::Scale mirror = Geom::Scale(1,1); + if(mirrorrowsx || mirrorrowsy || mirrorcolsx || mirrorcolsy) { + gint mx = 1; + gint my = 1; + if (mirrorrowsx && mirrorcolsx) { + mx = (j+i)%2 != 0 ? -1 : 1; + } else { + if (mirrorrowsx) { + mx = i%2 != 0 ? -1 : 1; + } else if (mirrorcolsx) { + mx = j%2 != 0 ? -1 : 1; + } + } + if (mirrorrowsy && mirrorcolsy) { + my = (j+i)%2 != 0 ? -1 : 1; + } else { + if (mirrorrowsy) { + my = i%2 != 0 ? -1 : 1; + } else if (mirrorcolsy) { + my = j%2 != 0 ? -1 : 1; + } + } + mirror = Geom::Scale(mx, my); + } + if (mirrortrans && interpolate_scalex && i%2 != 0) { + fracx = 1-fracx; + } + double fracyin = fracy; + if (mirrortrans && interpolate_scaley && j%2 != 0) { + fracyin = 1-fracyin; + } + double rotatein = rotate; + if (interpolate_rotatex && interpolate_rotatey) { + rotatein = rotatein * (i + j); + } else if (interpolate_rotatex) { + rotatein = rotatein * j; + } else if (interpolate_rotatey) { + rotatein = rotatein * i; + } + if (mirrortrans && + ((interpolate_rotatex && i%2 != 0) || + (interpolate_rotatey && j%2 != 0) || + (interpolate_rotatex && interpolate_rotatey))) + { + rotatein *=-1; + } + double scalein = 1; + double scalegap = scaleok - scalein; + if (interpolate_scalex && interpolate_scaley) { + scalein = (scalegap * (i + j)) + 1; + } else if (interpolate_scalex) { + scalein = (scalegap * j) + 1; + } else if (interpolate_scaley) { + scalein = (scalegap * i) + 1; + } else { + scalein = scaleok; + } + if (!interpolate_rotatex && !interpolate_rotatey && !random_rotate) { + r *= Geom::Rotate::from_degrees(rotatein).inverse(); + } + if (random_scale && scaleok != 1.0) { + if (random_s.size() == counter) { + double max = std::max(1.0,scaleok); + double min = std::min(1.0,scaleok); + random_s.emplace_back(seed.param_get_random_number() * (max - min) + min); + } + scalein = random_s[counter]; + } + if (random_rotate && rotate) { + if (random_r.size() == counter) { + random_r.emplace_back((seed.param_get_random_number() - seed.param_get_random_number()) * rotate); + } + rotatein = random_r[counter]; + } + if (random_x.size() == counter) { + if (random_gap_x && gapx_unit) { + random_x.emplace_back((seed.param_get_random_number() * gapx_unit)); // avoid overlapping + } else { + random_x.emplace_back(0); + } + } + if (random_y.size() == counter) { + if (random_gap_y && gapy_unit) { + random_y.emplace_back((seed.param_get_random_number() * gapy_unit)); // avoid overlapping + } else { + random_y.emplace_back(0); + } + } + r *= Geom::Rotate::from_degrees(rotatein); + r *= Geom::Scale(scalein, scalein); + double scale_fix = end_scale(scaleok, true); + double heightrows = original_height * scale_fix; + double widthcols = original_width * scale_fix; + double fixed_heightrows = heightrows; + double fixed_widthcols = widthcols; + bool shrink_interpove = shrink_interp; + if (rotatein) { + shrink_interpove = false; + } + if (scaleok != 1.0 && (interpolate_scalex || interpolate_scaley)) { + maxheight = std::max(maxheight,(*bbox).height() * scalein); + maxwidth = std::max(maxwidth,(*bbox).width() * scalein); + minheight = std::min(minheight,(*bbox).height() * scalein); + widthcols = std::max(original_width * end_scale(scaleok, false), original_width); + heightrows = std::max(original_height * end_scale(scaleok, false), original_height); + fixed_widthcols = widthcols; + fixed_heightrows = heightrows; + double cx = (*bbox).width() * scalein; + double cy = (*bbox).height() * scalein; + cx += gapx_unit; + cy += gapy_unit; + if (shrink_interpove && (!interpolate_scalex || !interpolate_scaley)) { + double px = 0; + double py = 0; + if (prev_bbox) { + px = (*prev_bbox).width(); + py = (*prev_bbox).height(); + px += gapx_unit; + py += gapy_unit; + } + if (interpolate_scalex) { + if (j) { + x = cx - ((cx-px)/2.0); + gapscalex += x; + x = gapscalex; + } else { + x = 0; + gapscalex = 0; + } + widthcols = 0; + } else if (interpolate_scaley) { + x = 0; + if (i == 1) { + ygap[j] = ((cy-y[j])/2.0); + y[j] += ygap[j]; + } + yset = y[j]; + y[j] += cy + ygap[j]; + heightrows = 0; + } + } + prev_bbox = bbox; + } else { + y[j] = 0; + } + if (!counter) { + counter++; + continue; + } + double xset = x; + xset += widthcols * j; + if (heightrows) { + yset = heightrows * i; + } + SPItem * item = toItem(counter - 1, reset, write); + if (item) { + if (!(lpesatellites.data().size() > counter - 1 && lpesatellites.data()[counter - 1])) { + item->deleteObject(true); + return; + } + prev_bbox = item->geometricBounds(); + (*prev_bbox) *= r; + double offset_x = 0; + double offset_y = 0; + if (offset != 0) { + if (offset_type && j%2) { + offset_y = fixed_heightrows/(100.0/(double)offset); + } + if (!offset_type && i%2) { + offset_x = fixed_widthcols/(100.0/(double)offset); + } + } + + + auto p = Geom::Point(xset + offset_x - random_x[counter], yset + offset_y - random_y[counter]); + auto translate = p * gap.inverse(); + Geom::Affine finalit = (transformoriginal * Geom::Translate(spcenter_base).inverse() * mirror * Geom::Translate(spcenter_base)); + finalit *= gapp.inverse() * Geom::Translate(spcenter).inverse() * originatrans.withoutTranslation().inverse() * r * translate * Geom::Translate(spcenter) ; + item->doWriteTransform(finalit); + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + forcewrite = forcewrite || write; + } + counter++; + } + } + //we keep satellites connected and active if write needed + bool connected = lpesatellites.is_connected(); + if (forcewrite || !connected) { + lpesatellites.write_to_SVG(); + lpesatellites.start_listening(); + lpesatellites.update_satellites(!connected); + } + reset = link_styles; + } +} + +void LPETiling::cloneStyle(SPObject *orig, SPObject *dest) +{ + dest->setAttribute("transform", orig->getAttribute("transform")); + dest->setAttribute("style", orig->getAttribute("style")); + dest->setAttribute("mask", orig->getAttribute("mask")); + dest->setAttribute("clip-path", orig->getAttribute("clip-path")); + dest->setAttribute("class", orig->getAttribute("class")); + for (auto iter : orig->style->properties()) { + if (iter->style_src != SPStyleSrc::UNSET) { + auto key = iter->id(); + if (key != SPAttr::FONT && key != SPAttr::D && key != SPAttr::MARKER) { + const gchar *attr = orig->getAttribute(iter->name().c_str()); + if (attr) { + dest->setAttribute(iter->name(), attr); + } + } + } + } +} + +void LPETiling::cloneD(SPObject *orig, SPObject *dest) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return; + } + if ( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() == SP_GROUP(dest)->getItemCount() ) { + if (reset) { + cloneStyle(orig, dest); + } + std::vector< SPObject * > childs = orig->childList(true); + size_t index = 0; + for (auto & child : childs) { + SPObject *dest_child = dest->nthChild(index); + cloneD(child, dest_child); + index++; + } + return; + } else if( SP_IS_GROUP(orig) && SP_IS_GROUP(dest) && SP_GROUP(orig)->getItemCount() != SP_GROUP(dest)->getItemCount()) { + split_items.param_setValue(false); + return; + } + + if ( SP_IS_TEXT(orig) && SP_IS_TEXT(dest) && SP_TEXT(orig)->children.size() == SP_TEXT(dest)->children.size()) { + if (reset) { + cloneStyle(orig, dest); + } + size_t index = 0; + for (auto & child : SP_TEXT(orig)->children) { + SPObject *dest_child = dest->nthChild(index); + cloneD(&child, dest_child); + index++; + } + } + + SPShape * shape = SP_SHAPE(orig); + SPPath * path = SP_PATH(dest); + if (shape) { + SPCurve *c = shape->curve(); + if (c) { + auto str = sp_svg_write_path(c->get_pathvector()); + if (shape && !path) { + const char *id = dest->getAttribute("id"); + const char *style = dest->getAttribute("style"); + Inkscape::XML::Document *xml_doc = dest->document->getReprDoc(); + Inkscape::XML::Node *dest_node = xml_doc->createElement("svg:path"); + dest_node->setAttribute("id", id); + dest_node->setAttribute("style", style); + dest->updateRepr(xml_doc, dest_node, SP_OBJECT_WRITE_ALL); + path = SP_PATH(dest); + } + path->setAttribute("d", str); + } else { + path->removeAttribute("d"); + } + } + if (reset) { + cloneStyle(orig, dest); + } +} + +Inkscape::XML::Node * +LPETiling::createPathBase(SPObject *elemref) { + SPDocument *document = getSPDoc(); + if (!document) { + return nullptr; + } + Inkscape::XML::Document *xml_doc = document->getReprDoc(); + Inkscape::XML::Node *prev = elemref->getRepr(); + SPGroup *group = dynamic_cast<SPGroup *>(elemref); + if (group) { + Inkscape::XML::Node *container = xml_doc->createElement("svg:g"); + container->setAttribute("transform", prev->attribute("transform")); + container->setAttribute("mask", prev->attribute("mask")); + container->setAttribute("clip-path", prev->attribute("clip-path")); + container->setAttribute("class", prev->attribute("class")); + container->setAttribute("style", prev->attribute("style")); + std::vector<SPItem*> const item_list = sp_item_group_item_list(group); + Inkscape::XML::Node *previous = nullptr; + for (auto sub_item : item_list) { + Inkscape::XML::Node *resultnode = createPathBase(sub_item); + + container->addChild(resultnode, previous); + previous = resultnode; + } + return container; + } + Inkscape::XML::Node *resultnode = xml_doc->createElement("svg:path"); + resultnode->setAttribute("transform", prev->attribute("transform")); + resultnode->setAttribute("style", prev->attribute("style")); + resultnode->setAttribute("mask", prev->attribute("mask")); + resultnode->setAttribute("clip-path", prev->attribute("clip-path")); + resultnode->setAttribute("class", prev->attribute("class")); + return resultnode; +} + + +SPItem * +LPETiling::toItem(size_t i, bool reset, bool &write) +{ + SPDocument *document = getSPDoc(); + if (!document) { + return nullptr; + } + + SPObject *elemref = nullptr; + if (container != sp_lpe_item->parent) { + lpesatellites.read_from_SVG(); + return nullptr; + } + if (lpesatellites.data().size() > i && lpesatellites.data()[i]) { + elemref = lpesatellites.data()[i]->getObject(); + } + Inkscape::XML::Node *phantom = nullptr; + bool creation = false; + if (elemref) { + phantom = elemref->getRepr(); + } else { + creation = true; + phantom = createPathBase(sp_lpe_item); + reset = true; + elemref = container->appendChildRepr(phantom); + + Inkscape::GC::release(phantom); + } + cloneD(sp_lpe_item, elemref); + reset = link_styles; + if (creation) { + write = true; + lpesatellites.link(elemref, i); + } + return dynamic_cast<SPItem *>(elemref); +} + +Gtk::Widget * LPETiling::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(0); + Gtk::Widget *combo = nullptr; + Gtk::Widget *randbutton = nullptr; + Gtk::Box *containerstart = nullptr; + Gtk::Box *containerend = nullptr; + Gtk::Box *movestart = nullptr; + Gtk::Box *moveend = nullptr; + Gtk::Box *rowcols = nullptr; + std::vector<Parameter *>::iterator it = param_vector.begin(); + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool usemirroricons = prefs->getBool("/live_effects/copy/mirroricons",true); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (widg) { + if (param->param_key == "unit") { + auto widgcombo = dynamic_cast<Inkscape::UI::Widget::RegisteredUnitMenu*>(widg); + delete widgcombo->get_children()[0]; + combo = dynamic_cast<Gtk::Widget*>(widgcombo); + if (usemirroricons) { + Gtk::RadioButton::Group group; + Gtk::Frame * frame = Gtk::manage(new Gtk::Frame(_("Mirroring mode"))); + Gtk::Box * cbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,8)); + Gtk::Box * vbox1 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + Gtk::Box * hbox1 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box * hbox2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box * vbox2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + Gtk::Box * hbox3 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box * hbox4 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + vbox2->set_margin_start(5); + cbox->pack_start(*vbox1, false, false, 0); + cbox->pack_start(*vbox2, false, false, 0); + cbox->set_margin_start(3); + cbox->set_margin_end(3); + cbox->set_margin_bottom(3); + frame->add(*cbox); + vbox->pack_start(*frame, false, false, 1); + vbox1->pack_start(*hbox1, false, false, 0); + vbox1->pack_start(*hbox2, false, false, 0); + vbox2->pack_start(*hbox3, false, false, 0); + vbox2->pack_start(*hbox4, false, false, 0); + generate_buttons(hbox1, group, 0); + generate_buttons(hbox2, group, 1); + generate_buttons(hbox3, group, 2); + generate_buttons(hbox4, group, 3); + } + ++it; + continue; + } else if (param->param_key == "seed"){ + auto widgrand = dynamic_cast<Inkscape::UI::Widget::RegisteredRandom*>(widg); + delete widgrand->get_children()[0]; + widgrand->get_children()[0]->hide(); + widgrand->get_children()[0]->set_no_show_all(true); + auto button = dynamic_cast<Gtk::Button*>(widgrand->get_children()[1]); + button->set_always_show_image(true); + button->set_label(_("Randomize")); + button->set_tooltip_markup(_("Randomization seed for random mode for scaling, rotation and gaps")); + button->set_relief(Gtk::RELIEF_NORMAL); + button->set_image_from_icon_name(INKSCAPE_ICON("randomize"), Gtk::IconSize(Gtk::ICON_SIZE_BUTTON)); + widgrand->set_vexpand(false); + widgrand->set_hexpand(true); + widgrand->set_valign(Gtk::ALIGN_START); + widgrand->set_halign(Gtk::ALIGN_START); + randbutton = dynamic_cast<Gtk::Widget*>(Gtk::manage(widgrand)); + ++it; + continue; + } else if (param->param_key == "offset_type" || + param->param_key == "mirrorrowsx" && usemirroricons || + param->param_key == "mirrorrowsy" && usemirroricons || + param->param_key == "mirrorcolsx" && usemirroricons || + param->param_key == "mirrorcolsy" && usemirroricons || + param->param_key == "interpolate_rotatex" || + param->param_key == "interpolate_rotatey" || + param->param_key == "interpolate_scalex" || + param->param_key == "interpolate_scaley" || + param->param_key == "random_scale" || + param->param_key == "random_rotate" || + param->param_key == "random_gap_x" || + param->param_key == "random_gap_y") + { + ++it; + continue; + } else if (param->param_key == "offset") { + movestart->pack_start(*widg, false, false, 2); + /* widg->set_halign(Gtk::ALIGN_END); */ + /* widg->set_hexpand(true); */ + /* auto widgscalar = dynamic_cast<Inkscape::UI::Widget::RegisteredScalar *>(widg); + widgscalar->get_children()[0]->set_halign(Gtk::ALIGN_START); */ + Gtk::Box *container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::RadioButton::Group group; + Gtk::RadioToolButton * rows = Gtk::manage(new Gtk::RadioToolButton(group, _("Offset rows"))); + rows->set_icon_name(INKSCAPE_ICON("rows")); + rows->set_tooltip_markup(_("Offset alternate rows")); + Gtk::RadioToolButton * cols = Gtk::manage(new Gtk::RadioToolButton(group, _("Offset cols"))); + cols->set_icon_name(INKSCAPE_ICON("cols")); + cols->set_tooltip_markup(_("Offset alternate cols")); + if (offset_type) { + cols->set_active(); + } else { + rows->set_active(); + } + container->pack_start(*rows, false, false, 1); + container->pack_start(*cols, false, false, 1); + cols->signal_clicked().connect(sigc::mem_fun (*this, &LPETiling::setOffsetCols)); + rows->signal_clicked().connect(sigc::mem_fun (*this, &LPETiling::setOffsetRows)); + moveend->pack_start(*container, false, false, 2); + } else if (param->param_key == "scale") { + Gtk::Box *container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::RadioButton::Group group; + Gtk::RadioToolButton * cols = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate X"))); + cols->set_icon_name(INKSCAPE_ICON("interpolate-scale-x")); + Gtk::RadioToolButton * rows = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate Y"))); + rows->set_icon_name(INKSCAPE_ICON("interpolate-scale-y")); + Gtk::RadioToolButton * both = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate both"))); + both->set_icon_name(INKSCAPE_ICON("interpolate-scale-both")); + Gtk::RadioToolButton * none = Gtk::manage(new Gtk::RadioToolButton(group, _("No interpolation"))); + none->set_icon_name(INKSCAPE_ICON("interpolate-scale-none")); + Gtk::RadioToolButton * rand = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate random"))); + rand->set_icon_name(INKSCAPE_ICON("scale-random")); + if (interpolate_scalex && interpolate_scaley) { + both->set_active(); + } else if (interpolate_scalex) { + cols->set_active(); + } else if (interpolate_scaley) { + rows->set_active(); + } else if (random_scale) { + rand->set_active(); + } else { + none->set_active(); + } + cols->set_tooltip_markup(_("Blend scale from <b>left to right</b> (left column uses original scale, right column uses new scale)")); + rows->set_tooltip_markup(_("Blend scale from <b>top to bottom</b> (top row uses original scale, bottom row uses new scale)")); + both->set_tooltip_markup(_("Blend scale <b>diagonally</b> (top left tile uses original scale, bottom right tile uses new scale)")); + none->set_tooltip_markup(_("Uniform scale")); + rand->set_tooltip_markup(_("Random scale (hit <b>Randomize</b> button to shuffle)")); + container->pack_start(*rows, false, false, 1); + container->pack_start(*cols, false, false, 1); + container->pack_start(*both, false, false, 1); + container->pack_start(*none, false, false, 1); + container->pack_start(*rand, false, false, 1); + rand->signal_clicked().connect(sigc::mem_fun(*this, &LPETiling::setScaleRandom)); + none->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setScaleInterpolate), false, false)); + cols->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setScaleInterpolate), true, false)); + rows->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setScaleInterpolate), false, true)); + both->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setScaleInterpolate), true, true)); + movestart->pack_start(*widg, false, false, 2); + moveend->pack_start(*container, false, false, 2); + } else if (param->param_key == "rotate") { + movestart->pack_start(*widg, false, false, 2); + Gtk::Box *container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::RadioButton::Group group; + Gtk::RadioToolButton * cols = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate X"))); + cols->set_icon_name(INKSCAPE_ICON("interpolate-rotate-x")); + Gtk::RadioToolButton * rows = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate Y"))); + rows->set_icon_name(INKSCAPE_ICON("interpolate-rotate-y")); + Gtk::RadioToolButton * both = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate both"))); + both->set_icon_name(INKSCAPE_ICON("interpolate-rotate-both")); + Gtk::RadioToolButton * none = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate none"))); + none->set_icon_name(INKSCAPE_ICON("interpolate-rotate-none")); + Gtk::RadioToolButton * rand = Gtk::manage(new Gtk::RadioToolButton(group, _("Interpolate random"))); + rand->set_icon_name(INKSCAPE_ICON("rotate-random")); + if (interpolate_rotatex && interpolate_rotatey) { + both->set_active(); + } else if (interpolate_rotatex) { + cols->set_active(); + } else if (interpolate_rotatey) { + rows->set_active(); + } else if (random_rotate) { + rand->set_active(); + } else { + none->set_active(); + } + cols->set_tooltip_markup(_("Blend rotation from <b>left to right</b> (left column uses original rotation, right column uses new rotation)")); + rows->set_tooltip_markup(_("Blend rotation from <b>top to bottom</b> (top row uses original rotation, bottom row uses new rotation)")); + both->set_tooltip_markup(_("Blend rotation <b>diagonally</b> (top left tile uses original rotation, bottom right tile uses new rotation)")); + none->set_tooltip_markup(_("Uniform rotation")); + rand->set_tooltip_markup(_("Random rotation (hit <b>Randomize</b> button to shuffle)")); + container->pack_start(*rows, false, false, 1); + container->pack_start(*cols, false, false, 1); + container->pack_start(*both, false, false, 1); + container->pack_start(*none, false, false, 1); + container->pack_start(*rand, false, false, 1); + rand->signal_clicked().connect(sigc::mem_fun(*this, &LPETiling::setRotateRandom)); + none->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setRotateInterpolate), false, false)); + cols->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setRotateInterpolate), true, false)); + rows->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setRotateInterpolate), false, true)); + both->signal_clicked().connect(sigc::bind<bool,bool>(sigc::mem_fun(*this, &LPETiling::setRotateInterpolate), true, true)); + moveend->pack_start(*container, false, false, 2); + } else if (param->param_key == "gapx") { + Gtk::Box *wrapper = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + movestart = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + moveend = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + Gtk::Box *container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::RadioButton::Group group; + Gtk::RadioToolButton * normal = Gtk::manage(new Gtk::RadioToolButton(group, _("Normal"))); + normal->set_icon_name(INKSCAPE_ICON("interpolate-scale-none")); // same icon + Gtk::RadioToolButton * randx = Gtk::manage(new Gtk::RadioToolButton(group, _("Random"))); + randx->set_icon_name(INKSCAPE_ICON("gap-random-x")); + if (random_gap_x) { + randx->set_active(); + } else { + normal->set_active(); + } + normal->set_tooltip_markup(_("All horizontal gaps have the same width")); + randx->set_tooltip_markup(_("Random horizontal gaps (hit <b>Randomize</b> button to shuffle)")); + normal->signal_clicked().connect(sigc::bind<bool>(sigc::mem_fun(*this, &LPETiling::setGapXMode), false)); + randx->signal_clicked().connect(sigc::bind<bool>(sigc::mem_fun(*this, &LPETiling::setGapXMode), true)); + container->pack_start(*normal, false, false, 1); + container->pack_start(*randx, false, false, 1); + container->pack_end(*combo, false, false, 1); + movestart->pack_start(*widg, false, false, 2); + moveend->pack_start(*container, true, true, 2); + wrapper->pack_start(*movestart, false, false, 0); + wrapper->pack_start(*moveend, false, false, 0); + //bwidg->set_hexpand(true); + combo->set_halign(Gtk::ALIGN_END); + widg->set_halign(Gtk::ALIGN_START); + vbox->pack_start(*wrapper, true, true, 0); + } else if (param->param_key == "gapy") { + movestart->pack_start(*widg, true, true, 2); + Gtk::Box *container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::RadioButton::Group group; + Gtk::RadioToolButton * normal = Gtk::manage(new Gtk::RadioToolButton(group, _("Normal"))); + normal->set_icon_name(INKSCAPE_ICON("interpolate-scale-none")); // same icon + Gtk::RadioToolButton * randy = Gtk::manage(new Gtk::RadioToolButton(group, _("Random"))); + randy->set_icon_name(INKSCAPE_ICON("gap-random-y")); + if (random_gap_y) { + randy->set_active(); + } else { + normal->set_active(); + } + normal->set_tooltip_markup(_("All vertical gaps have the same height")); + randy->set_tooltip_markup(_("Random vertical gaps (hit <b>Randomize</b> button to shuffle)")); + normal->signal_clicked().connect(sigc::bind<bool>(sigc::mem_fun(*this, &LPETiling::setGapYMode), false)); + randy->signal_clicked().connect(sigc::bind<bool>(sigc::mem_fun(*this, &LPETiling::setGapYMode), true)); + container->pack_start(*normal, false, false, 1); + container->pack_start(*randy, false, false, 1); + widg->set_halign(Gtk::ALIGN_START); + moveend->pack_start(*container, false, false, 2); + } else if (param->param_key == "mirrortrans"){ + Gtk::Box *container = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + Gtk::Box *containerwraper = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + containerend = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + containerstart = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL,0)); + container->pack_start(*containerwraper, true, true, 0); + containerwraper->pack_start(*containerstart, true, true, 0); + containerwraper->pack_start(*containerend, true, true, 0); + containerend->pack_end(*randbutton, true, true, 2); + containerstart->pack_start(*widg, true, true, 2); + container->set_hexpand(true); + containerwraper->set_hexpand(true); + containerend->set_hexpand(true); + containerstart->set_hexpand(true); + vbox->pack_start(*container, true, true, 1); + } else if ( + param->param_key == "split_items" || + param->param_key == "link_styles" || + param->param_key == "shrink_interp") + { + containerstart->pack_start(*widg, true, true, 2); + widg->set_vexpand(false); + widg->set_hexpand(false); + widg->set_valign(Gtk::ALIGN_START); + widg->set_halign(Gtk::ALIGN_START); + } else if (param->param_key == "num_rows") { + rowcols = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + rowcols->pack_start(*widg, false, false, 2); + vbox->pack_start(*rowcols, true, true, 2); + } else if (param->param_key == "num_cols") { + rowcols->pack_start(*widg, false, false, 2); + } else { + vbox->pack_start(*widg, true, true, 2); + } + if (tip) { + widg->set_tooltip_markup(*tip); + } else { + widg->set_tooltip_markup(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +void +LPETiling::generate_buttons(Gtk::Box *container, Gtk::RadioButton::Group &group, gint pos) +{ + for (int i = 0; i < 4; i++) { + gint position = (pos * 4) + i; + Glib::ustring result = getMirrorMap(position); + Gtk::RadioToolButton * button = Gtk::manage(new Gtk::RadioToolButton(group)); + Glib::ustring iconname = "mirroring"; + iconname += "-"; + iconname += result; + button->set_icon_name(INKSCAPE_ICON(iconname)); + if (getActiveMirror(position)) { + _updating = true; + button->set_active(); + _updating = false; + } + button->signal_clicked().connect(sigc::bind<gint>(sigc::mem_fun(*this, &LPETiling::setMirroring),position)); + gint zero = Glib::ustring("0")[0]; + Glib::ustring tooltip = result[0] == zero ? "" : "rx+"; + tooltip += result[1] == zero ? "" : "ry+"; + tooltip += result[2] == zero ? "" : "cx+"; + tooltip += result[3] == zero ? "" : "cy+"; + if (tooltip.size()) { + tooltip.erase(tooltip.size()-1); + } + button->set_tooltip_markup(tooltip); + button->set_margin_start(1); + container->pack_start(*button, false, false, 1); + } +} + +Glib::ustring +LPETiling::getMirrorMap(gint index) +{ + Glib::ustring result = "0000"; + if (index == 1) { + result = "1000"; + } else if (index == 2) { + result = "1100"; + } else if (index == 3) { + result = "0100"; + } else if (index == 4) { + result = "0011"; + } else if (index == 5) { + result = "1011"; + } else if (index == 6) { + result = "1111"; + } else if (index == 7) { + result = "0111"; + } else if (index == 8) { + result = "0010"; + } else if (index == 9) { + result = "1010"; + } else if (index == 10) { + result = "1110"; + } else if (index == 11) { + result = "0110"; + } else if (index == 12) { + result = "0001"; + } else if (index == 13) { + result = "1001"; + } else if (index == 14) { + result = "1101"; + } else if (index == 15) { + result = "0101"; + } + return result; +} + +bool +LPETiling::getActiveMirror(gint index) +{ + Glib::ustring result = getMirrorMap(index); + return result[0] == Glib::ustring::format(mirrorrowsx)[0] && + result[1] == Glib::ustring::format(mirrorrowsy)[0] && + result[2] == Glib::ustring::format(mirrorcolsx)[0] && + result[3] == Glib::ustring::format(mirrorcolsy)[0]; +} + +void +LPETiling::setMirroring(gint index) +{ + if (_updating) { + return; + } + _updating = true; + Glib::ustring result = getMirrorMap(index); + gint zero = Glib::ustring("0")[0]; + mirrorrowsx.param_setValue(result[0] == zero ? false : true); + mirrorrowsy.param_setValue(result[1] == zero ? false : true); + mirrorcolsx.param_setValue(result[2] == zero ? false : true); + mirrorcolsy.param_setValue(result[3] == zero ? false : true); + writeParamsToSVG(); + _updating = false; +} + +void +LPETiling::setOffsetCols(){ + offset_type.param_setValue(true); + offset_type.write_to_SVG(); +} +void +LPETiling::setOffsetRows(){ + offset_type.param_setValue(false); + offset_type.write_to_SVG(); +} + +void +LPETiling::setRotateInterpolate(bool x, bool y){ + interpolate_rotatex.param_setValue(x); + interpolate_rotatey.param_setValue(y); + random_rotate.param_setValue(false); + writeParamsToSVG(); +} + +void +LPETiling::setScaleInterpolate(bool x, bool y){ + interpolate_scalex.param_setValue(x); + interpolate_scaley.param_setValue(y); + random_scale.param_setValue(false); + writeParamsToSVG(); +} + +void +LPETiling::setRotateRandom() { + interpolate_rotatex.param_setValue(false); + interpolate_rotatey.param_setValue(false); + random_rotate.param_setValue(true); + writeParamsToSVG(); +} + +void +LPETiling::setScaleRandom() { + interpolate_scalex.param_setValue(false); + interpolate_scaley.param_setValue(false); + random_scale.param_setValue(true); + writeParamsToSVG(); +} + +void +LPETiling::setGapXMode(bool random) { + random_gap_x.param_setValue(random); + writeParamsToSVG(); +} + +void +LPETiling::setGapYMode(bool random) { + random_gap_y.param_setValue(random); + writeParamsToSVG(); +} + +void +LPETiling::doOnApply(SPLPEItem const* lpeitem) +{ + if (lpeitem->getAttribute("transform")) { + transformorigin.param_setValue(lpeitem->getAttribute("transform"), true); + } else { + transformorigin.param_setValue("", true); + } + doBeforeEffect(lpeitem); +} + +void +LPETiling::doBeforeEffect (SPLPEItem const* lpeitem) +{ + auto transformorigin_str = lpeitem->getAttribute("transform"); + if (transformorigin_str) { + transformorigin.read_from_SVG(); + auto transformorigin_str = transformorigin.param_getSVGValue(); + transformoriginal = Geom::identity(); + if (!transformorigin_str.empty()) { + sp_svg_transform_read(transformorigin_str.c_str(), &transformoriginal); + } + } else { + transformorigin.param_setValue("", true); + transformoriginal = Geom::identity(); + } + //transformoriginal = transformoriginal.withoutTranslation(); + using namespace Geom; + seed.resetRandomizer(); + random_x.clear(); + random_y.clear(); + random_s.clear(); + random_r.clear(); + if (prev_unit != unit.get_abbreviation()) { + double newgapx = Inkscape::Util::Quantity::convert(gapx, prev_unit, unit.get_abbreviation()); + double newgapy = Inkscape::Util::Quantity::convert(gapy, prev_unit, unit.get_abbreviation()); + gapx.param_set_value(newgapx); + gapy.param_set_value(newgapy); + prev_unit = unit.get_abbreviation(); + writeParamsToSVG(); + } + scaleok = (scale + 100) / 100.0; + double seedset = seed.param_get_random_number() - seed.param_get_random_number(); + affinebase = Geom::identity(); + if (random_rotate && rotate) { + affinebase *= Geom::Rotate::from_degrees(seedset * rotate); + } + if (random_scale && scaleok != 1) { + affinebase *= Geom::Scale(seed.param_get_random_number() * (std::max(scaleok,1.0) - std::min(scaleok,1.0)) + std::min(scaleok,1.0)); + } + if (random_gap_x && gapx_unit) { + affinebase *= Geom::Translate(seed.param_get_random_number() * gapx_unit * -1, 0); + } + if (random_gap_y && gapy_unit) { + affinebase *= Geom::Translate(0,seed.param_get_random_number() * gapy_unit * -1); + } + if (!split_items && lpesatellites.data().size()) { + processObjects(LPE_ERASE); + } + if (link_styles) { + reset = true; + } + if (split_items && !lpesatellites.data().size()) { + lpesatellites.read_from_SVG(); + if (lpesatellites.data().size()) { + lpesatellites.update_satellites(); + } + } + Glib::ustring display_unit = lpeitem->document->getDisplayUnit()->abbr.c_str(); + gapx_unit = Inkscape::Util::Quantity::convert(gapx, unit.get_abbreviation(), display_unit.c_str()); + gapy_unit = Inkscape::Util::Quantity::convert(gapy, unit.get_abbreviation(), display_unit.c_str()); + original_bbox(sp_lpe_item, false, true, transformoriginal); + originalbbox = Geom::OptRect(boundingbox_X,boundingbox_Y); + Geom::Point A = Point(boundingbox_X.min() - (gapx_unit / 2.0), boundingbox_Y.min() - (gapy_unit / 2.0)); + Geom::Point B = Point(boundingbox_X.max() + (gapx_unit / 2.0), boundingbox_Y.max() + (gapy_unit / 2.0)); + gap_bbox = Geom::OptRect(A,B); + if (!gap_bbox) { + return; + } + + double scale_fix = end_scale(scaleok, true); + (*originalbbox) *= Geom::Translate((*originalbbox).midpoint()).inverse() * Geom::Scale(scale_fix) * Geom::Translate((*originalbbox).midpoint()); + if (!interpolate_scalex && !interpolate_scaley && !random_scale) { + (*gap_bbox) *= Geom::Translate((*gap_bbox).midpoint()).inverse() * Geom::Scale(scaleok,scaleok) * Geom::Translate((*gap_bbox).midpoint()); + (*originalbbox) *= Geom::Translate((*originalbbox).midpoint()).inverse() * Geom::Scale(scaleok,scaleok) * Geom::Translate((*originalbbox).midpoint()); + } + original_width = (*gap_bbox).width(); + original_height = (*gap_bbox).height(); +} + +double +LPETiling::end_scale(double scale_fix, bool tomax) const { + if (interpolate_scalex && interpolate_scaley) { + scale_fix = 1 + ((scale_fix - 1) * (num_rows + num_cols -1)); + } else if (interpolate_scalex) { + scale_fix = 1 + ((scale_fix - 1) * (num_cols -1)); + } else if (interpolate_scaley) { + scale_fix = 1 + ((scale_fix - 1) * (num_rows -1)); + } + if (tomax && (random_scale || interpolate_scalex || interpolate_scaley)) { + scale_fix = std::max(scale_fix, 1.0); + } + return scale_fix; +} + +Geom::PathVector +LPETiling::doEffect_path (Geom::PathVector const & path_in) +{ + Geom::PathVector path_out; + FillRuleBool fillrule = fill_nonZero; + if (current_shape->style && + current_shape->style->fill_rule.set && + current_shape->style->fill_rule.computed == SP_WIND_RULE_EVENODD) + { + fillrule = (FillRuleBool)fill_oddEven; + } + path_out = doEffect_path_post(path_in, fillrule); + if (_knotholder) { + _knotholder->update_knots(); + } + if (split_items) { + return path_out; + } else { + return path_out * transformoriginal.inverse(); + } +} + +Geom::PathVector +LPETiling::doEffect_path_post (Geom::PathVector const & path_in, FillRuleBool fillrule) +{ + if (!gap_bbox) { + return path_in; + } + Geom::Point spcenter_base = (*sp_lpe_item->geometricBounds(transformoriginal)).midpoint(); + Geom::Point center = (*gap_bbox).midpoint() * transformoriginal.inverse(); + Geom::PathVector output; + gint counter = 0; + Geom::OptRect prev_bbox; + double gapscalex = 0; + double maxheight = 0; + double maxwidth = 0; + double minheight = std::numeric_limits<double>::max(); + Geom::OptRect bbox = path_in.boundsFast(); + if (!bbox) { + return path_in; + } + (*bbox) *= transformoriginal; + + double posx = ((*gap_bbox).left() - (*bbox).left()) / (*gap_bbox).width(); + double factorx = original_width/(*bbox).width(); + double factory = original_height/(*bbox).height(); + double y[(int)num_cols]; + double yset = 0; + double gap[(int)num_cols]; + for (int i = 0; i < num_rows; ++i) { + double fracy = 1; + if (num_rows != 1) { + fracy = i/(double)(num_rows - 1); + } + for (int j = 0; j < num_cols; ++j) { + double x = 0; + double fracx = 1; + if (num_cols != 1) { + fracx = j/(double)(num_cols - 1); + } + Geom::Affine r = Geom::identity(); + r = Geom::identity(); + Geom::Scale mirror = Geom::Scale(1,1); + if(mirrorrowsx || mirrorrowsy || mirrorcolsx || mirrorcolsy) { + gint mx = 1; + gint my = 1; + if (mirrorrowsx && mirrorcolsx) { + mx = (j+i)%2 != 0 ? -1 : 1; + } else { + if (mirrorrowsx) { + mx = i%2 != 0 ? -1 : 1; + } else if (mirrorcolsx) { + mx = j%2 != 0 ? -1 : 1; + } + } + if (mirrorrowsy && mirrorcolsy) { + my = (j+i)%2 != 0 ? -1 : 1; + } else { + if (mirrorrowsy) { + my = i%2 != 0 ? -1 : 1; + } else if (mirrorcolsy) { + my = j%2 != 0 ? -1 : 1; + } + } + mirror = Geom::Scale(mx, my); + } + if (mirrortrans && interpolate_scalex && i%2 != 0) { + fracx = 1-fracx; + } + double fracyin = fracy; + if (mirrortrans && interpolate_scaley && j%2 != 0) { + fracyin = 1-fracyin; + } + /* if (mirrortrans && interpolate_scaley && interpolate_scalex) { + fract = 1-fract; + } */ + double rotatein = rotate; + if (interpolate_rotatex && interpolate_rotatey) { + rotatein = rotatein * (i + j); + } else if (interpolate_rotatex) { + rotatein = rotatein * j; + } else if (interpolate_rotatey) { + rotatein = rotatein * i; + } + if (mirrortrans && + ((interpolate_rotatex && i%2 != 0) || + (interpolate_rotatey && j%2 != 0) || + (interpolate_rotatex && interpolate_rotatey))) + { + rotatein *=-1; + } + double scalein = 1; + double scalegap = scaleok - scalein; + if (interpolate_scalex && interpolate_scaley) { + scalein = (scalegap * (i + j)) + 1; + } else if (interpolate_scalex) { + scalein = (scalegap * j) + 1; + } else if (interpolate_scaley) { + scalein = (scalegap * i) + 1; + } else { + scalein = scaleok; + } + + if (random_scale && scaleok != 1.0) { + if (random_s.size() == counter) { + double max = std::max(1.0,scaleok); + double min = std::min(1.0,scaleok); + random_s.emplace_back(seed.param_get_random_number() * (max - min) + min); + } + scalein = random_s[counter]; + } + if (random_rotate && rotate) { + if (random_r.size() == counter) { + random_r.emplace_back((seed.param_get_random_number() - seed.param_get_random_number()) * rotate); + } + rotatein = random_r[counter]; + } + if (random_x.size() == counter) { + if (random_gap_x && gapx_unit && (j || i)) { + random_x.emplace_back((seed.param_get_random_number() * gapx_unit)); // avoid overlapping + } else { + random_x.emplace_back(0); + } + } + if (random_y.size() == counter) { + if (random_gap_y && gapy_unit && (j || i)) { + random_y.emplace_back((seed.param_get_random_number() * gapy_unit)); // avoid overlapping + } else { + random_y.emplace_back(0); + } + } + r *= Geom::Scale(scalein, scalein); + r *= Geom::Rotate::from_degrees(rotatein); + + Geom::PathVector output_pv = pathv_to_linear_and_cubic_beziers(path_in); + + output_pv *= Geom::Translate(center).inverse(); + output_pv *= r; + if (!interpolate_rotatex && !interpolate_rotatey && !random_rotate) { + output_pv *= Geom::Rotate::from_degrees(rotate); + } + if (!interpolate_scalex && !interpolate_scaley && !random_scale) { + output_pv *= Geom::Scale(scaleok, scaleok); + } + originatrans = r; + output_pv *= Geom::Translate(center); + if (split_items) { + return output_pv; + } + double scale_fix = end_scale(scaleok, true); + double heightrows = original_height * scale_fix; + double widthcols = original_width * scale_fix; + double fixed_heightrows = heightrows; + double fixed_widthcols = widthcols; + + if (rotatein && shrink_interp) { + shrink_interp.param_setValue(false); + shrink_interp.write_to_SVG(); + return path_in; + } + if (scaleok != 1.0 && (interpolate_scalex || interpolate_scaley )) { + Geom::OptRect bbox = output_pv.boundsFast(); + if (bbox) { + maxheight = std::max(maxheight,(*bbox).height()); + maxwidth = std::max(maxwidth,(*bbox).width()); + minheight = std::min(minheight,(*bbox).height()); + widthcols = std::max(original_width * end_scale(scaleok, false),original_width); + heightrows = std::max(original_height * end_scale(scaleok, false),original_height); + fixed_widthcols = widthcols; + fixed_heightrows = heightrows; + double cx = (*bbox).width(); + double cy = (*bbox).height(); + if (shrink_interp && (!interpolate_scalex || !interpolate_scaley)) { + heightrows = 0; + widthcols = 0; + double px = 0; + double py = 0; + if (prev_bbox) { + px = (*prev_bbox).width(); + py = (*prev_bbox).height(); + } + if (interpolate_scalex) { + if (j) { + x = ((cx - ((cx - px) / 2.0))) * factorx; + gapscalex += x; + x = gapscalex; + } else { + x = 0; + gapscalex = 0; + } + } else { + x = (std::max(original_width * end_scale(scaleok, false), original_width) + posx) * j; + } + if (interpolate_scalex && i == 1) { + y[j] = maxheight * factory; + } else if(i == 0) { + y[j] = 0; + } + if (i == 1 && !interpolate_scalex) { + gap[j] = ((cy * factory) - y[j])/2.0; + } else if (i == 0) { + gap[j] = 0; + } + yset = y[j] + (gap[j] * i); + if (interpolate_scaley) { + y[j] += cy * factory; + } else { + y[j] += maxheight * factory; + } + } + prev_bbox = bbox; + } + } else { + y[j] = 0; + } + double xset = x; + xset += widthcols * j; + if (heightrows) { + yset = heightrows * i; + } + double offset_x = 0; + double offset_y = 0; + if (offset != 0) { + if (offset_type && j%2) { + offset_y = fixed_heightrows/(100.0/(double)offset); + } + if (!offset_type && i%2) { + offset_x = fixed_widthcols/(100.0/(double)offset); + + } + } + output_pv *= Geom::Translate(center).inverse() * mirror * Geom::Translate(center); + output_pv *= transformoriginal; + output_pv *= Geom::Translate(Geom::Point(xset + offset_x - random_x[counter],yset + offset_y - random_y[counter])); + output.insert(output.end(), output_pv.begin(), output_pv.end()); + counter++; + } + } + return output; +} + +void +LPETiling::addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) +{ + if (!gap_bbox) { + return; + } + using namespace Geom; + hp_vec.clear(); + Geom::Path hp = Geom::Path(*gap_bbox); + double scale_fix = end_scale(scaleok, true); + hp *= Geom::Translate((*gap_bbox).midpoint()).inverse() * Geom::Scale(scale_fix) * Geom::Translate((*gap_bbox).midpoint()); + hp *= transformoriginal.inverse(); + Geom::PathVector pathv; + pathv.push_back(hp); + hp_vec.push_back(pathv); +} + +void +LPETiling::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + original_bbox(SP_LPE_ITEM(item), false, true); +} + +void +LPETiling::doOnVisibilityToggled(SPLPEItem const* lpeitem) +{ + auto transformorigin_str = lpeitem->getAttribute("transform"); + Geom::Affine ontoggle = Geom::identity(); + if (transformorigin_str) { + sp_svg_transform_read(transformorigin_str, &ontoggle); + } + ontoggle = ontoggle; + if (is_visible) { + if ( ontoggle == Geom::identity()) { + transformorigin.param_setValue("", true); + } else { + ontoggle = ontoggle * hideaffine.inverse() * transformoriginal; + transformorigin.param_setValue(sp_svg_transform_write(ontoggle), true); + } + } else { + hideaffine = ontoggle; + } + processObjects(LPE_VISIBILITY); +} + +void +LPETiling::doOnRemove (SPLPEItem const* lpeitem) +{ + if (keep_paths) { + processObjects(LPE_TO_OBJECTS); + return; + } + processObjects(LPE_ERASE); +} + +void LPETiling::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knotholder = knotholder; + KnotHolderEntity *e = new CoS::KnotHolderEntityCopyGapX(this); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:CopiesGapX", + _("<b>Horizontal gaps between tiles</b>: drag to adjust, <b>Shift+click</b> to reset")); + knotholder->add(e); + + KnotHolderEntity *f = new CoS::KnotHolderEntityCopyGapY(this); + f->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:CopiesGapY", + _("<b>Vertical gaps between tiles</b>: drag to adjust, <b>Shift+click</b> to reset")); + knotholder->add(f); +} + +namespace CoS { + +KnotHolderEntityCopyGapX::~KnotHolderEntityCopyGapX() +{ + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + if (lpe) { + lpe->_knotholder = nullptr; + } +} + +KnotHolderEntityCopyGapY::~KnotHolderEntityCopyGapY() +{ + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + if (lpe) { + lpe->_knotholder = nullptr; + } +} + +void KnotHolderEntityCopyGapX::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + lpe->refresh_widgets = true; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +void KnotHolderEntityCopyGapY::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + lpe->refresh_widgets = true; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +void KnotHolderEntityCopyGapX::knot_click(guint state) +{ + if (!(state & GDK_SHIFT_MASK)) { + return; + } + + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + + lpe->gapx.param_set_value(0); + startpos = 0; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +void KnotHolderEntityCopyGapY::knot_click(guint state) +{ + if (!(state & GDK_SHIFT_MASK)) { + return; + } + + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + + lpe->gapy.param_set_value(0); + startpos = 0; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +void KnotHolderEntityCopyGapX::knot_set(Geom::Point const &p, Geom::Point const&/*origin*/, guint state) +{ + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + if (lpe->originalbbox) { + Geom::Point point = (*lpe->originalbbox).corner(1); + point *= lpe->transformoriginal.inverse(); + double value = s[Geom::X] - point[Geom::X]; + Glib::ustring display_unit = SP_ACTIVE_DOCUMENT->getDisplayUnit()->abbr.c_str(); + value = Inkscape::Util::Quantity::convert((value/lpe->end_scale(lpe->scaleok, false)) * 2, display_unit.c_str(),lpe->unit.get_abbreviation()); + lpe->gapx.param_set_value(value); + lpe->gapx.write_to_SVG(); + } +} + +void KnotHolderEntityCopyGapY::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state) +{ + LPETiling* lpe = dynamic_cast<LPETiling *>(_effect); + + Geom::Point const s = snap_knot_position(p, state); + if (lpe->originalbbox) { + Geom::Point point = (*lpe->originalbbox).corner(3); + point *= lpe->transformoriginal.inverse(); + double value = s[Geom::Y] - point[Geom::Y]; + Glib::ustring display_unit = SP_ACTIVE_DOCUMENT->getDisplayUnit()->abbr.c_str(); + value = Inkscape::Util::Quantity::convert((value/lpe->end_scale(lpe->scaleok, false)) * 2, display_unit.c_str(),lpe->unit.get_abbreviation()); + lpe->gapy.param_set_value(value); + lpe->gapy.write_to_SVG(); + } +} + +Geom::Point KnotHolderEntityCopyGapX::knot_get() const +{ + LPETiling const * lpe = dynamic_cast<LPETiling const*> (_effect); + Geom::Point ret = Geom::Point(Geom::infinity(),Geom::infinity()); + if (lpe->originalbbox) { + auto bbox = (*lpe->originalbbox); + Glib::ustring display_unit = SP_ACTIVE_DOCUMENT->getDisplayUnit()->abbr.c_str(); + double value = Inkscape::Util::Quantity::convert(lpe->gapx, lpe->unit.get_abbreviation(), display_unit.c_str()); + double scale = lpe->scaleok; + ret = (bbox).corner(1) + Geom::Point((value * lpe->end_scale(scale, false))/2.0,0); + ret *= lpe->transformoriginal.inverse(); + } + return ret; +} + +Geom::Point KnotHolderEntityCopyGapY::knot_get() const +{ + LPETiling const * lpe = dynamic_cast<LPETiling const*> (_effect); + Geom::Point ret = Geom::Point(Geom::infinity(),Geom::infinity()); + if (lpe->originalbbox) { + auto bbox = (*lpe->originalbbox); + Glib::ustring display_unit = SP_ACTIVE_DOCUMENT->getDisplayUnit()->abbr.c_str(); + double value = Inkscape::Util::Quantity::convert(lpe->gapy, lpe->unit.get_abbreviation(), display_unit.c_str()); + double scale = lpe->scaleok; + ret = (bbox).corner(3) + Geom::Point(0,(value * lpe->end_scale(scale, false))/2.0); + ret *= lpe->transformoriginal.inverse(); + } + return ret; +} + +} // namespace CoS +} // namespace LivePathEffect +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-gaps:((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 : diff --git a/src/live_effects/lpe-tiling.h b/src/live_effects/lpe-tiling.h new file mode 100644 index 0000000..0a47f3e --- /dev/null +++ b/src/live_effects/lpe-tiling.h @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_TILING_H +#define INKSCAPE_LPE_TILING_H + +/** \file + * LPE <tiling> implementation, see lpe-tiling.cpp. + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/enum.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/satellitearray.h" +#include "live_effects/parameter/text.h" +#include "live_effects/parameter/unit.h" +#include "live_effects/parameter/random.h" +// this is only to fillrule +#include "livarot/Shape.h" + +namespace Inkscape { +namespace LivePathEffect { + +namespace CoS { +// we need a separate namespace to avoid clashes with other LPEs +class KnotHolderEntityCopyGapX; +class KnotHolderEntityCopyGapY; +} + +typedef FillRule FillRuleBool; + +class LPETiling : public Effect, GroupBBoxEffect { +public: + LPETiling(LivePathEffectObject *lpeobject); + ~LPETiling() override; + void doOnApply (SPLPEItem const* lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + void doBeforeEffect (SPLPEItem const* lpeitem) override; + void doAfterEffect (SPLPEItem const* lpeitem, SPCurve *curve) override; + void split(Geom::PathVector &path_in, Geom::Path const ÷r); + void resetDefaults(SPItem const* item) override; + void doOnRemove (SPLPEItem const* /*lpeitem*/) override; + bool doOnOpen(SPLPEItem const * /*lpeitem*/) override; + void doOnVisibilityToggled(SPLPEItem const* /*lpeitem*/) override; + Gtk::Widget * newWidget() override; + void cloneStyle(SPObject *orig, SPObject *dest); + Geom::PathVector doEffect_path_post (Geom::PathVector const & path_in, FillRuleBool fillrule); + SPItem * toItem(size_t i, bool reset, bool &write); + void cloneD(SPObject *orig, SPObject *dest); + Inkscape::XML::Node * createPathBase(SPObject *elemref); + friend class CoS::KnotHolderEntityCopyGapX; + friend class CoS::KnotHolderEntityCopyGapY; + void addKnotHolderEntities(KnotHolder * knotholder, SPItem * item) override; +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + KnotHolder *_knotholder; + double gapx_unit = 0; + double gapy_unit = 0; + double offset_unit = 0; +private: + void setOffsetCols(); + void setOffsetRows(); + void setScaleInterpolate(bool x, bool y); + void setRotateInterpolate(bool x, bool y); + void setScaleRandom(); + void setRotateRandom(); + void setGapXMode(bool random); + void setGapYMode(bool random); + bool getActiveMirror(gint index); + double end_scale(double scale_fix, bool tomax) const; + bool _updating = false; + void setMirroring(gint index); + Glib::ustring getMirrorMap(gint index); + void generate_buttons(Gtk::Box *container, Gtk::RadioButton::Group &group, gint pos); + UnitParam unit; + SatelliteArrayParam lpesatellites; + ScalarParam gapx; + ScalarParam gapy; + ScalarParam num_rows; + ScalarParam num_cols; + ScalarParam rotate; + ScalarParam scale; + ScalarParam offset; + BoolParam offset_type; + BoolParam random_scale; + BoolParam random_rotate; + BoolParam random_gap_x; + BoolParam random_gap_y; + RandomParam seed; + BoolParam interpolate_scalex; + BoolParam interpolate_rotatex; + BoolParam interpolate_scaley; + BoolParam interpolate_rotatey; + BoolParam mirrorrowsx; + BoolParam mirrorrowsy; + BoolParam mirrorcolsx; + BoolParam mirrorcolsy; + BoolParam mirrortrans; + BoolParam split_items; + BoolParam link_styles; + BoolParam shrink_interp; + HiddenParam transformorigin; + double original_width = 0; + double original_height = 0; + Geom::OptRect gap_bbox; + Geom::OptRect originalbbox; + double prev_num_cols; + double prev_num_rows; + bool reset; + gdouble scaleok = 1.0; + Glib::ustring prev_unit = "px"; + std::vector<double> random_x; + std::vector<double> random_y; + std::vector<double> random_s; + std::vector<double> random_r; + Geom::Affine affinebase = Geom::identity(); + Geom::Affine transformoriginal = Geom::identity(); + Geom::Affine hideaffine = Geom::identity(); + Geom::Affine originatrans = Geom::identity(); + bool prev_split = false; + SPObject *container; + LPETiling(const LPETiling&) = delete; + LPETiling& operator=(const LPETiling&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-gaps:((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 : diff --git a/src/live_effects/lpe-transform_2pts.cpp b/src/live_effects/lpe-transform_2pts.cpp new file mode 100644 index 0000000..b3685ac --- /dev/null +++ b/src/live_effects/lpe-transform_2pts.cpp @@ -0,0 +1,482 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * LPE "Transform through 2 points" implementation + */ + +/* + * Authors: + * Jabier Arraiza Cenoz<jabier.arraiza@marker.es> + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "lpe-transform_2pts.h" + +#include <gtkmm.h> + +#include "display/curve.h" +#include "helper/geom.h" +#include "object/sp-path.h" +#include "svg/svg.h" +#include "ui/icon-names.h" + +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + + +namespace Inkscape { +namespace LivePathEffect { + +LPETransform2Pts::LPETransform2Pts(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + elastic(_("Elastic"), _("Elastic transform mode"), "elastic", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + from_original_width(_("From original width"), _("From original width"), "from_original_width", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + lock_length(_("Lock length"), _("Lock length to current distance"), "lock_length", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + lock_angle(_("Lock angle"), _("Lock angle"), "lock_angle", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + flip_horizontal(_("Flip horizontal"), _("Flip horizontal"), "flip_horizontal", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + flip_vertical(_("Flip vertical"), _("Flip vertical"), "flip_vertical", &wr, this, false,"", INKSCAPE_ICON("on-outline"), INKSCAPE_ICON("off-outline")), + start(_("Start"), _("Start point"), "start", &wr, this, "Start point"), + end(_("End"), _("End point"), "end", &wr, this, "End point"), + stretch(_("Stretch"), _("Stretch the result"), "stretch", &wr, this, 1), + offset(_("Offset"), _("Offset from knots"), "offset", &wr, this, 0), + first_knot(_("First Knot"), _("First Knot"), "first_knot", &wr, this, 1), + last_knot(_("Last Knot"), _("Last Knot"), "last_knot", &wr, this, 1), + helper_size(_("Helper size:"), _("Rotation helper size"), "helper_size", &wr, this, 3), + from_original_width_toggler(false), + point_a(Geom::Point()), + point_b(Geom::Point()), + pathvector(), + append_path(false), + previous_angle(Geom::rad_from_deg(0)), + previous_start(Geom::Point()), + previous_length(-1) +{ + + registerParameter(&first_knot); + registerParameter(&last_knot); + registerParameter(&helper_size); + registerParameter(&stretch); + registerParameter(&offset); + registerParameter(&start); + registerParameter(&end); + registerParameter(&elastic); + registerParameter(&from_original_width); + registerParameter(&flip_vertical); + registerParameter(&flip_horizontal); + registerParameter(&lock_length); + registerParameter(&lock_angle); + + first_knot.param_make_integer(); + first_knot.param_set_undo(false); + last_knot.param_make_integer(); + last_knot.param_set_undo(false); + helper_size.param_set_range(0, 999); + helper_size.param_set_increments(1, 1); + helper_size.param_set_digits(0); + offset.param_set_range(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::max()); + offset.param_set_increments(1, 1); + offset.param_set_digits(2); + stretch.param_set_range(0, 999.0); + stretch.param_set_increments(0.01, 0.01); + stretch.param_set_digits(4); + apply_to_clippath_and_mask = true; +} + +LPETransform2Pts::~LPETransform2Pts() += default; + +void +LPETransform2Pts::doOnApply(SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + point_a = Point(boundingbox_X.min(), boundingbox_Y.middle()); + point_b = Point(boundingbox_X.max(), boundingbox_Y.middle()); + SPLPEItem * splpeitem = const_cast<SPLPEItem *>(lpeitem); + SPPath *sp_path = dynamic_cast<SPPath *>(splpeitem); + if (sp_path) { + pathvector = sp_path->curveForEdit()->get_pathvector(); + } + if(!pathvector.empty()) { + point_a = pathvector.initialPoint(); + point_b = pathvector.finalPoint(); + if(are_near(point_a,point_b)) { + point_b = pathvector.back().finalCurve().initialPoint(); + } + size_t nnodes = nodeCount(pathvector); + last_knot.param_set_value(nnodes); + } + + previous_length = Geom::distance(point_a,point_b); + Geom::Ray transformed(point_a,point_b); + previous_angle = transformed.angle(); + start.param_update_default(point_a); + start.param_set_default(); + end.param_update_default(point_b); + end.param_set_default(); +} + +void LPETransform2Pts::transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) { + start.param_transform_multiply(postmul, false); + end.param_transform_multiply(postmul, false); + } +} + +void +LPETransform2Pts::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + point_a = Point(boundingbox_X.min(), boundingbox_Y.middle()); + point_b = Point(boundingbox_X.max(), boundingbox_Y.middle()); + + SPLPEItem * splpeitem = const_cast<SPLPEItem *>(lpeitem); + SPPath *sp_path = dynamic_cast<SPPath *>(splpeitem); + if (sp_path) { + pathvector = sp_path->curveForEdit()->get_pathvector(); + } + if(from_original_width_toggler != from_original_width) { + from_original_width_toggler = from_original_width; + reset(); + } + if(!pathvector.empty() && !from_original_width) { + append_path = false; + point_a = pointAtNodeIndex(pathvector,(size_t)first_knot-1); + point_b = pointAtNodeIndex(pathvector,(size_t)last_knot-1); + size_t nnodes = nodeCount(pathvector); + first_knot.param_set_range(1, last_knot-1); + last_knot.param_set_range(first_knot+1, nnodes); + if (from_original_width){ + from_original_width.param_setValue(false); + } + } else { + if (first_knot != 1){ + first_knot.param_set_value(1); + } + if (last_knot != 2){ + last_knot.param_set_value(2); + } + first_knot.param_set_range(1,1); + last_knot.param_set_range(2,2); + append_path = false; + if (!from_original_width){ + from_original_width.param_setValue(true); + } + } + if(lock_length && !lock_angle && previous_length != -1) { + Geom::Ray transformed((Geom::Point)start,(Geom::Point)end); + if(previous_start == start || previous_angle == Geom::rad_from_deg(0)) { + previous_angle = transformed.angle(); + } + } else if(lock_angle && !lock_length && previous_angle != Geom::rad_from_deg(0)) { + if(previous_start == start){ + previous_length = Geom::distance((Geom::Point)start, (Geom::Point)end); + } + } + if(lock_length || lock_angle ) { + Geom::Point end_point = Geom::Point::polar(previous_angle, previous_length) + (Geom::Point)start; + end.param_setValue(end_point); + } + Geom::Ray transformed((Geom::Point)start,(Geom::Point)end); + previous_angle = transformed.angle(); + previous_length = Geom::distance((Geom::Point)start, (Geom::Point)end); + previous_start = start; +} + +void +LPETransform2Pts::updateIndex() +{ + SPLPEItem * splpeitem = const_cast<SPLPEItem *>(sp_lpe_item); + SPPath *sp_path = dynamic_cast<SPPath *>(splpeitem); + if (sp_path) { + pathvector = sp_path->curveForEdit()->get_pathvector(); + } + if(pathvector.empty()) { + return; + } + if(!from_original_width) { + point_a = pointAtNodeIndex(pathvector,(size_t)first_knot-1); + point_b = pointAtNodeIndex(pathvector,(size_t)last_knot-1); + start.param_update_default(point_a); + start.param_set_default(); + end.param_update_default(point_b); + end.param_set_default(); + start.param_update_default(point_a); + end.param_update_default(point_b); + start.param_set_default(); + end.param_set_default(); + } + DocumentUndo::done(getSPDoc(), _("Change index of knot"), INKSCAPE_ICON("dialog-path-effects")); +} +//todo migrate to PathVector class? +size_t +LPETransform2Pts::nodeCount(Geom::PathVector pathvector) const +{ + size_t n = 0; + for (auto & it : pathvector) { + n += count_path_nodes(it); + } + return n; +} +//todo migrate to PathVector class? +Geom::Point +LPETransform2Pts::pointAtNodeIndex(Geom::PathVector pathvector, size_t index) const +{ + size_t n = 0; + for (auto & pv_it : pathvector) { + for (Geom::Path::iterator curve_it = pv_it.begin(); curve_it != pv_it.end_closed(); ++curve_it) { + if(index == n) { + return curve_it->initialPoint(); + } + n++; + } + } + return Geom::Point(); +} +//todo migrate to PathVector class? Not used +Geom::Path +LPETransform2Pts::pathAtNodeIndex(Geom::PathVector pathvector, size_t index) const +{ + size_t n = 0; + for (auto & pv_it : pathvector) { + for (Geom::Path::iterator curve_it = pv_it.begin(); curve_it != pv_it.end_closed(); ++curve_it) { + if(index == n) { + return pv_it; + } + n++; + } + } + return Geom::Path(); +} + + +void +LPETransform2Pts::reset() +{ + point_a = Geom::Point(boundingbox_X.min(), boundingbox_Y.middle()); + point_b = Geom::Point(boundingbox_X.max(), boundingbox_Y.middle()); + if(!pathvector.empty() && !from_original_width) { + size_t nnodes = nodeCount(pathvector); + first_knot.param_set_range(1, last_knot-1); + last_knot.param_set_range(first_knot+1, nnodes); + first_knot.param_set_value(1); + last_knot.param_set_value(nnodes); + point_a = pathvector.initialPoint(); + point_b = pathvector.finalPoint(); + } else { + first_knot.param_set_value(1); + last_knot.param_set_value(2); + } + offset.param_set_value(0.0); + stretch.param_set_value(1.0); + Geom::Ray transformed(point_a, point_b); + previous_angle = transformed.angle(); + previous_length = Geom::distance(point_a, point_b); + start.param_update_default(point_a); + end.param_update_default(point_b); + start.param_set_default(); + end.param_set_default(); +} + +Gtk::Widget *LPETransform2Pts::newWidget() +{ + // use manage here, because after deletion of Effect object, others might + // still be pointing to this widget. + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + vbox->set_border_width(5); + vbox->set_homogeneous(false); + vbox->set_spacing(6); + + std::vector<Parameter *>::iterator it = param_vector.begin(); + Gtk::Box * button1 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box * button2 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box * button3 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + Gtk::Box * button4 = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL,0)); + while (it != param_vector.end()) { + if ((*it)->widget_is_visible) { + Parameter *param = *it; + Gtk::Widget *widg = dynamic_cast<Gtk::Widget *>(param->param_newWidget()); + Glib::ustring *tip = param->param_getTooltip(); + if (param->param_key == "first_knot" || param->param_key == "last_knot") { + Inkscape::UI::Widget::Scalar *registered_widget = Gtk::manage(dynamic_cast<Inkscape::UI::Widget::Scalar *>(widg)); + registered_widget->signal_value_changed().connect(sigc::mem_fun(*this, &LPETransform2Pts::updateIndex)); + widg = registered_widget; + if (widg) { + Gtk::Box *hbox_scalar = dynamic_cast<Gtk::Box *>(widg); + std::vector<Gtk::Widget *> child_list = hbox_scalar->get_children(); + Gtk::Entry *entry_widget = dynamic_cast<Gtk::Entry *>(child_list[1]); + entry_widget->set_width_chars(3); + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (param->param_key == "from_original_width" || param->param_key == "elastic") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + button1->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (param->param_key == "flip_horizontal" || param->param_key == "flip_vertical") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + button2->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (param->param_key == "lock_angle" || param->param_key == "lock_length") { + Glib::ustring * tip = param->param_getTooltip(); + if (widg) { + button3->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } else if (widg) { + vbox->pack_start(*widg, true, true, 2); + if (tip) { + widg->set_tooltip_text(*tip); + } else { + widg->set_tooltip_text(""); + widg->set_has_tooltip(false); + } + } + } + + ++it; + } + Gtk::Button *reset = Gtk::manage(new Gtk::Button(Glib::ustring(_("Reset")))); + reset->signal_clicked().connect(sigc::mem_fun(*this, &LPETransform2Pts::reset)); + button4->pack_start(*reset, true, true, 2); + vbox->pack_start(*button1, true, true, 2); + vbox->pack_start(*button2, true, true, 2); + vbox->pack_start(*button3, true, true, 2); + vbox->pack_start(*button4, true, true, 2); + if(Gtk::Widget* widg = defaultParamSet()) { + vbox->pack_start(*widg, true, true, 2); + } + return dynamic_cast<Gtk::Widget *>(vbox); +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > +LPETransform2Pts::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + Geom::Piecewise<Geom::D2<Geom::SBasis> > output; + double sca = Geom::distance((Geom::Point)start,(Geom::Point)end)/Geom::distance(point_a,point_b); + Geom::Ray original(point_a,point_b); + Geom::Ray transformed((Geom::Point)start,(Geom::Point)end); + double rot = transformed.angle() - original.angle(); + Geom::Path helper; + helper.start(point_a); + helper.appendNew<Geom::LineSegment>(point_b); + Geom::Affine m; + Geom::Angle original_angle = original.angle(); + if(flip_horizontal && flip_vertical){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(-1,-1); + m *= Geom::Rotate(original_angle); + } else if(flip_vertical){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(1,-1); + m *= Geom::Rotate(original_angle); + } else if(flip_horizontal){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(-1,1); + m *= Geom::Rotate(original_angle); + } + if(stretch != 1){ + m *= Geom::Rotate(-original_angle); + m *= Geom::Scale(1,stretch); + m *= Geom::Rotate(original_angle); + } + if(elastic) { + m *= Geom::Rotate(-original_angle); + if(sca > 1){ + m *= Geom::Scale(sca, 1.0); + } else { + m *= Geom::Scale(sca, 1.0-((1.0-sca)/2.0)); + } + m *= Geom::Rotate(transformed.angle()); + } else { + m *= Geom::Scale(sca); + m *= Geom::Rotate(rot); + } + helper *= m; + Geom::Point trans = (Geom::Point)start - helper.initialPoint(); + if(flip_horizontal){ + trans = (Geom::Point)end - helper.initialPoint(); + } + if(offset != 0){ + trans = Geom::Point::polar(transformed.angle() + Geom::rad_from_deg(-90),offset) + trans; + } + m *= Geom::Translate(trans); + + output.concat(pwd2_in * m); + + return output; +} + +void +LPETransform2Pts::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + using namespace Geom; + hp_vec.clear(); + Geom::Path hp; + hp.start((Geom::Point)start); + hp.appendNew<Geom::LineSegment>((Geom::Point)end); + Geom::PathVector pathv; + pathv.push_back(hp); + double r = helper_size*.1; + if(lock_length || lock_angle ) { + char const * svgd; + svgd = "M -5.39,8.78 -9.13,5.29 -10.38,10.28 Z M -7.22,7.07 -3.43,3.37 m -1.95,-12.16 -3.74,3.5 -1.26,-5 z m -1.83,1.71 3.78,3.7 M 5.24,8.78 8.98,5.29 10.24,10.28 Z M 7.07,7.07 3.29,3.37 M 5.24,-8.78 l 3.74,3.5 1.26,-5 z M 7.07,-7.07 3.29,-3.37"; + PathVector pathv_move = sp_svg_read_pathv(svgd); + pathv_move *= Affine(r,0,0,r,0,0) * Translate(Geom::Point(start)); + hp_vec.push_back(pathv_move); + } + if(!lock_angle && lock_length) { + char const * svgd; + svgd = "M 0,9.94 C -2.56,9.91 -5.17,8.98 -7.07,7.07 c -3.91,-3.9 -3.91,-10.24 0,-14.14 1.97,-1.97 4.51,-3.02 7.07,-3.04 2.56,0.02 5.1,1.07 7.07,3.04 3.91,3.9 3.91,10.24 0,14.14 C 5.17,8.98 2.56,9.91 0,9.94 Z"; + PathVector pathv_turn = sp_svg_read_pathv(svgd); + pathv_turn *= Geom::Rotate(previous_angle); + pathv_turn *= Affine(r,0,0,r,0,0) * Translate(Geom::Point(end)); + hp_vec.push_back(pathv_turn); + } + hp_vec.push_back(pathv); +} + + +/* ######################## */ + +} //namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-transform_2pts.h b/src/live_effects/lpe-transform_2pts.h new file mode 100644 index 0000000..d72e69a --- /dev/null +++ b/src/live_effects/lpe-transform_2pts.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_TRANSFORM_2PTS_H +#define INKSCAPE_LPE_TRANSFORM_2PTS_H + +/** \file + * LPE "Transform through 2 points" implementation + */ + +/* + * Authors: + * + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/togglebutton.h" +#include "live_effects/parameter/point.h" + +namespace Inkscape { +namespace LivePathEffect { + +class LPETransform2Pts : public Effect, GroupBBoxEffect { +public: + LPETransform2Pts(LivePathEffectObject *lpeobject); + ~LPETransform2Pts() override; + + void doOnApply (SPLPEItem const* lpeitem) override; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) override; + + void doBeforeEffect (SPLPEItem const* lpeitem) override; + + void transform_multiply(Geom::Affine const &postmul, bool set) override; + + Gtk::Widget *newWidget() override; + + void updateIndex(); + + size_t nodeCount(Geom::PathVector pathvector) const; + + Geom::Point pointAtNodeIndex(Geom::PathVector pathvector, size_t index) const; + + Geom::Path pathAtNodeIndex(Geom::PathVector pathvector, size_t index) const; + + void reset(); + +protected: + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + +private: + ToggleButtonParam elastic; + ToggleButtonParam from_original_width; + ToggleButtonParam lock_length; + ToggleButtonParam lock_angle; + ToggleButtonParam flip_horizontal; + ToggleButtonParam flip_vertical; + PointParam start; + PointParam end; + ScalarParam stretch; + ScalarParam offset; + ScalarParam first_knot; + ScalarParam last_knot; + ScalarParam helper_size; + bool from_original_width_toggler; + Geom::Point point_a; + Geom::Point point_b; + Geom::PathVector pathvector; + bool append_path; + Geom::Angle previous_angle; + Geom::Point previous_start; + double previous_length; + LPETransform2Pts(const LPETransform2Pts&) = delete; + LPETransform2Pts& operator=(const LPETransform2Pts&) = delete; +}; + +} //namespace LivePathEffect +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/lpe-vonkoch.cpp b/src/live_effects/lpe-vonkoch.cpp new file mode 100644 index 0000000..277c5f3 --- /dev/null +++ b/src/live_effects/lpe-vonkoch.cpp @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) JF Barraud 2007 <jf.barraud@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> +#include "live_effects/lpe-vonkoch.h" +// TODO due to internal breakage in glibmm headers, this must be last: +#include <glibmm/i18n.h> + +namespace Inkscape { +namespace LivePathEffect { + +void +VonKochPathParam::param_setup_nodepath(Inkscape::NodePath::Path *np) +{ + PathParam::param_setup_nodepath(np); + //sp_nodepath_make_straight_path(np); +} + +//FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. +void +VonKochRefPathParam::param_setup_nodepath(Inkscape::NodePath::Path *np) +{ + PathParam::param_setup_nodepath(np); + //sp_nodepath_make_straight_path(np); +} +bool +VonKochRefPathParam::param_readSVGValue(const gchar * strvalue) +{ + Geom::PathVector old = _pathvector; + bool res = PathParam::param_readSVGValue(strvalue); + if (res && _pathvector.size()==1 && _pathvector.front().size()==1){ + return true; + }else{ + _pathvector = old; + return false; + } +} + +LPEVonKoch::LPEVonKoch(LivePathEffectObject *lpeobject) : + Effect(lpeobject), + nbgenerations(_("N_r of generations:"), _("Depth of the recursion --- keep low!!"), "nbgenerations", &wr, this, 1), + generator(_("Generating path:"), _("Path whose segments define the iterated transforms"), "generator", &wr, this, "M0,0 L30,0 M0,10 L10,10 M 20,10 L30,10"), + similar_only(_("_Use uniform transforms only"), _("2 consecutive segments are used to reverse/preserve orientation only (otherwise, they define a general transform)."), "similar_only", &wr, this, false), + drawall(_("Dra_w all generations"), _("If unchecked, draw only the last generation"), "drawall", &wr, this, true), + //,draw_boxes(_("Display boxes"), _("Display boxes instead of paths only"), "draw_boxes", &wr, this, true) + ref_path(_("Reference segment:"), _("The reference segment. Defaults to the horizontal midline of the bbox."), "ref_path", &wr, this, "M0,0 L10,0"), + //refA(_("Ref Start"), _("Left side middle of the reference box"), "refA", &wr, this), + //refB(_("Ref End"), _("Right side middle of the reference box"), "refB", &wr, this), + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. + maxComplexity(_("_Max complexity:"), _("Disable effect if the output is too complex"), "maxComplexity", &wr, this, 1000) +{ + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. + registerParameter(&ref_path); + //registerParameter(&refA) ); + //registerParameter(&refB) ); + registerParameter(&generator); + registerParameter(&similar_only); + registerParameter(&nbgenerations); + registerParameter(&drawall); + registerParameter(&maxComplexity); + //registerParameter(&draw_boxes) ); + apply_to_clippath_and_mask = true; + nbgenerations.param_make_integer(); + nbgenerations.param_set_range(0, std::numeric_limits<gint>::max()); + maxComplexity.param_make_integer(); + maxComplexity.param_set_range(0, std::numeric_limits<gint>::max()); +} + +LPEVonKoch::~LPEVonKoch() += default; + +bool +LPEVonKoch::doOnOpen(SPLPEItem const *lpeitem) +{ + if (!is_load || is_applied) { + return false; + } + generator.reload(); + ref_path.reload(); + return false; +} + + +Geom::PathVector +LPEVonKoch::doEffect_path (Geom::PathVector const & path_in) +{ + using namespace Geom; + Geom::Affine affine = generator.get_relative_affine(); + Geom::PathVector generating_path = generator.get_pathvector() * affine; + + if (generating_path.empty()) { + return path_in; + } + if (is_load) { + generator.reload(); + ref_path.reload(); + } + + //Collect transform matrices. + affine = ref_path.get_relative_affine(); + + Affine m0; + Geom::Path refpath = ref_path.get_pathvector().front() * affine ; + Point A = refpath.pointAt(0); + Point B = refpath.pointAt(refpath.size()); + Point u = B-A; + m0 = Affine(u[X], u[Y],-u[Y], u[X], A[X], A[Y]); + + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + //Point u = refB-refA; + //m0 = Affine(u[X], u[Y],-u[Y], u[X], refA[X], refA[Y]); + m0 = m0.inverse(); + + std::vector<Affine> transforms; + for (const auto & i : generating_path){ + Affine m; + if(i.size()==1){ + Point p = i.pointAt(0); + Point u = i.pointAt(1)-p; + m = Affine(u[X], u[Y],-u[Y], u[X], p[X], p[Y]); + m = m0*m; + transforms.push_back(m); + }else if(i.size()>=2){ + Point p = i.pointAt(1); + Point u = i.pointAt(2)-p; + Point v = p-i.pointAt(0); + if (similar_only.get_value()){ + int sign = (u[X]*v[Y]-u[Y]*v[X]>=0?1:-1); + v[X] = -u[Y]*sign; + v[Y] = u[X]*sign; + } + m = Affine(u[X], u[Y],v[X], v[Y], p[X], p[Y]); + m = m0*m; + transforms.push_back(m); + } + } + + if (transforms.empty()){ + return path_in; + } + + //Do nothing if the output is too complex... + int path_in_complexity = 0; + for (const auto & k : path_in){ + path_in_complexity+=k.size(); + } + double complexity = std::pow(transforms.size(), nbgenerations) * path_in_complexity; + if (drawall.get_value()){ + int k = transforms.size(); + if(k>1){ + complexity = (std::pow(k,nbgenerations+1)-1)/(k-1)*path_in_complexity; + }else{ + complexity = nbgenerations*k*path_in_complexity; + } + } + if (complexity > double(maxComplexity)){ + g_warning("VonKoch lpe's output too complex. Effect bypassed."); + return path_in; + } + + //Generate path: + Geom::PathVector pathi = path_in; + Geom::PathVector path_out = path_in; + + for (unsigned i = 0; i<nbgenerations; i++){ + if (drawall.get_value()){ + path_out = path_in; + complexity = path_in_complexity; + }else{ + path_out = Geom::PathVector(); + complexity = 0; + } + for (const auto & transform : transforms){ + for (unsigned k = 0; k<pathi.size() && complexity < maxComplexity; k++){ + path_out.push_back(pathi[k]*transform); + complexity+=pathi[k].size(); + } + } + pathi = path_out; + } + return path_out; +} + + +//Useful?? +//void +//LPEVonKoch::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +/*{ + using namespace Geom; + if (draw_boxes.get_value()){ + double ratio = .5; + if (similar_only.get_value()) ratio = boundingbox_Y.extent()/boundingbox_X.extent()/2; + + Point BB1,BB2,BB3,BB4,v; + + //Draw the reference box (ref_path is supposed to consist in one line segment) + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + Geom::Path refpath = ref_path.get_pathvector().front(); + if (refpath.size()==1){ + BB1 = BB4 = refpath.front().pointAt(0); + BB2 = BB3 = refpath.front().pointAt(1); + v = rot90(BB2 - BB1)*ratio; + BB1 -= v; + BB2 -= v; + BB3 += v; + BB4 += v; + Geom::Path refbox(BB1); + refbox.appendNew<LineSegment>(BB2); + refbox.appendNew<LineSegment>(BB3); + refbox.appendNew<LineSegment>(BB4); + refbox.close(); + PathVector refbox_as_vect; + refbox_as_vect.push_back(refbox); + hp_vec.push_back(refbox_as_vect); + } + //Draw the transformed boxes + Geom::PathVector generating_path = generator.get_pathvector(); + for (unsigned i=0;i<generating_path.size(); i++){ + if (generating_path[i].size()==0){ + //Ooops! this should not happen. + }else if (generating_path[i].size()==1){ + BB1 = BB4 = generating_path[i].pointAt(0); + BB2 = BB3 = generating_path[i].pointAt(1); + v = rot90(BB2 - BB1)*ratio; + }else{//Only tak the first 2 segments into account. + BB1 = BB4 = generating_path[i].pointAt(1); + BB2 = BB3 = generating_path[i].pointAt(2); + if(similar_only.get_value()){ + v = rot90(BB2 - BB1)*ratio; + }else{ + v = (generating_path[i].pointAt(0) - BB1)*ratio; + } + } + BB1 -= v; + BB2 -= v; + BB3 += v; + BB4 += v; + Geom::Path path(BB1); + path.appendNew<LineSegment>(BB2); + path.appendNew<LineSegment>(BB3); + path.appendNew<LineSegment>(BB4); + path.close(); + PathVector pathv; + pathv.push_back(path); + hp_vec.push_back(pathv); + } + } +} +*/ + +void +LPEVonKoch::doBeforeEffect (SPLPEItem const* lpeitem) +{ + using namespace Geom; + original_bbox(lpeitem, false, true); + + Geom::PathVector paths = ref_path.get_pathvector(); + Geom::Point A,B; + if (paths.empty()||paths.front().size()==0){ + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + //refA.param_setValue( Geom::Point(boundingbox_X.min(), boundingbox_Y.middle()) ); + //refB.param_setValue( Geom::Point(boundingbox_X.max(), boundingbox_Y.middle()) ); + A = Point(boundingbox_X.min(), boundingbox_Y.middle()); + B = Point(boundingbox_X.max(), boundingbox_Y.middle()); + }else{ + A = paths.front().pointAt(0); + B = paths.front().pointAt(paths.front().size()); + } + if (paths.size()!=1||paths.front().size()!=1){ + Geom::Path tmp_path(A); + tmp_path.appendNew<LineSegment>(B); + Geom::PathVector tmp_pathv; + tmp_pathv.push_back(tmp_path); + ref_path.set_new_value(tmp_pathv,true); + } +} + + +void +LPEVonKoch::resetDefaults(SPItem const* item) +{ + Effect::resetDefaults(item); + + using namespace Geom; + original_bbox(SP_LPE_ITEM(item), false, true); + + Point A,B; + A[Geom::X] = boundingbox_X.min(); + A[Geom::Y] = boundingbox_Y.middle(); + B[Geom::X] = boundingbox_X.max(); + B[Geom::Y] = boundingbox_Y.middle(); + + Geom::PathVector paths,refpaths; + Geom::Path path = Geom::Path(A); + path.appendNew<Geom::LineSegment>(B); + + refpaths.push_back(path); + ref_path.set_new_value(refpaths, true); + + paths.push_back(path * Affine(1./3,0,0,1./3, A[X]*2./3, A[Y]*2./3 + boundingbox_Y.extent()/2)); + paths.push_back(path * Affine(1./3,0,0,1./3, B[X]*2./3, B[Y]*2./3 + boundingbox_Y.extent()/2)); + generator.set_new_value(paths, true); + + //FIXME: a path is used as ref instead of 2 points to work around path/point param incompatibility bug. + //refA[Geom::X] = boundingbox_X.min(); + //refA[Geom::Y] = boundingbox_Y.middle(); + //refB[Geom::X] = boundingbox_X.max(); + //refB[Geom::Y] = boundingbox_Y.middle(); + //Geom::PathVector paths; + //Geom::Path path = Geom::Path( (Point) refA); + //path.appendNew<Geom::LineSegment>( (Point) refB ); + //paths.push_back(path * Affine(1./3,0,0,1./3, refA[X]*2./3, refA[Y]*2./3 + boundingbox_Y.extent()/2)); + //paths.push_back(path * Affine(1./3,0,0,1./3, refB[X]*2./3, refB[Y]*2./3 + boundingbox_Y.extent()/2)); + //paths.push_back(path); + //generator.set_new_value(paths, true); +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpe-vonkoch.h b/src/live_effects/lpe-vonkoch.h new file mode 100644 index 0000000..f24b94f --- /dev/null +++ b/src/live_effects/lpe-vonkoch.h @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPE_VONKOCH_H +#define INKSCAPE_LPE_VONKOCH_H + +/* + * Inkscape::LPEVonKoch + * + * Copyright (C) JF Barraud 2007 <jf.barraud@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/effect.h" +#include "live_effects/lpegroupbbox.h" +#include "live_effects/parameter/path.h" +#include "live_effects/parameter/point.h" +#include "live_effects/parameter/bool.h" + +namespace Inkscape { +namespace LivePathEffect { + +class VonKochPathParam : public PathParam{ +public: + VonKochPathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_value = "M0,0 L1,1"):PathParam(label,tip,key,wr,effect,default_value){} + ~VonKochPathParam() override= default; + void param_setup_nodepath(Inkscape::NodePath::Path *np) override; + }; + + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. +class VonKochRefPathParam : public PathParam{ +public: + VonKochRefPathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_value = "M0,0 L1,1"):PathParam(label,tip,key,wr,effect,default_value){} + ~VonKochRefPathParam() override= default; + void param_setup_nodepath(Inkscape::NodePath::Path *np) override; + bool param_readSVGValue(const gchar * strvalue) override; + }; + +class LPEVonKoch : public Effect, GroupBBoxEffect { +public: + LPEVonKoch(LivePathEffectObject *lpeobject); + ~LPEVonKoch() override; + bool doOnOpen(SPLPEItem const *lpeitem) override; + Geom::PathVector doEffect_path (Geom::PathVector const & path_in) override; + + void resetDefaults(SPItem const* item) override; + + void doBeforeEffect(SPLPEItem const* item) override; + + //Useful?? + // protected: + //virtual void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec); + +private: + ScalarParam nbgenerations; + VonKochPathParam generator; + BoolParam similar_only; + BoolParam drawall; + //BoolParam draw_boxes; + //FIXME: a path is used here instead of 2 points to work around path/point param incompatibility bug. + VonKochRefPathParam ref_path; + // PointParam refA; + // PointParam refB; + ScalarParam maxComplexity; + + LPEVonKoch(const LPEVonKoch&) = delete; + LPEVonKoch& operator=(const LPEVonKoch&) = delete; +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpegroupbbox.cpp b/src/live_effects/lpegroupbbox.cpp new file mode 100644 index 0000000..90fdc35 --- /dev/null +++ b/src/live_effects/lpegroupbbox.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Steren Giannini 2008 <steren.giannini@gmail.com> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "document.h" +#include "live_effects/lpegroupbbox.h" +#include "object/sp-clippath.h" +#include "object/sp-mask.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-item-group.h" +#include "object/sp-lpe-item.h" + +namespace Inkscape { +namespace LivePathEffect { + +/** + * Updates the \c boundingbox_X and \c boundingbox_Y values from the geometric bounding box of \c lpeitem. + * + * @pre lpeitem must have an existing geometric boundingbox (usually this is guaranteed when: \code SP_SHAPE(lpeitem)->curve != NULL \endcode ) + * It's not possible to run LPEs on items without their original-d having a bbox. + * @param lpeitem This is not allowed to be NULL. + * @param absolute Determines whether the bbox should be calculated of the untransformed lpeitem (\c absolute = \c false) + * or of the transformed lpeitem (\c absolute = \c true) using sp_item_i2doc_affine. + * @post Updated values of boundingbox_X and boundingbox_Y. These intervals are set to empty intervals when the precondition is not met. + */ + +Geom::OptRect +GroupBBoxEffect::clip_mask_bbox(SPLPEItem *item, Geom::Affine transform) +{ + Geom::OptRect bbox; + Geom::Affine affine = transform * item->transform; + SPClipPath *clip_path = item->getClipObject(); + if(clip_path) { + bbox.unionWith(clip_path->geometricBounds(affine)); + } + SPMask * mask_path = item->getMaskObject(); + if(mask_path) { + bbox.unionWith(mask_path->visualBounds(affine)); + } + SPGroup * group = dynamic_cast<SPGroup *>(item); + if (group) { + std::vector<SPItem*> item_list = sp_item_group_item_list(group); + for (auto iter : item_list) { + SPLPEItem * subitem = dynamic_cast<SPLPEItem *>(iter); + if (subitem) { + bbox.unionWith(clip_mask_bbox(subitem, affine)); + } + } + } + return bbox; +} + +void GroupBBoxEffect::original_bbox(SPLPEItem const* lpeitem, bool absolute, bool clip_mask, Geom::Affine base_transform) +{ + // Get item bounding box + Geom::Affine transform; + if (absolute) { + transform = lpeitem->i2doc_affine(); + } + else { + transform = base_transform; + } + + Geom::OptRect bbox = lpeitem->geometricBounds(transform); + if (clip_mask) { + SPLPEItem * item = const_cast<SPLPEItem *>(lpeitem); + bbox.unionWith(clip_mask_bbox(item, transform * item->transform.inverse())); + } + if (bbox) { + boundingbox_X = (*bbox)[Geom::X]; + boundingbox_Y = (*bbox)[Geom::Y]; + } else { + boundingbox_X = Geom::Interval(); + boundingbox_Y = Geom::Interval(); + } +} + +} // namespace LivePathEffect +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/lpegroupbbox.h b/src/live_effects/lpegroupbbox.h new file mode 100644 index 0000000..5d17240 --- /dev/null +++ b/src/live_effects/lpegroupbbox.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LPEGROUPBBOX_H +#define INKSCAPE_LPEGROUPBBOX_H + +/* + * Inkscape::LivePathEffect_group_bbox + * + * Copyright (C) Steren Giannini 2008 <steren.giannini@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +class SPLPEItem; + +#include <2geom/interval.h> + +namespace Inkscape { +namespace LivePathEffect { + +class GroupBBoxEffect { +protected: + // Bounding box of the item the path effect is applied on + Geom::Interval boundingbox_X; + Geom::Interval boundingbox_Y; + + //This sets boundingbox_X and boundingbox_Y + Geom::OptRect clip_mask_bbox(SPLPEItem * item, Geom::Affine transform); + void original_bbox(SPLPEItem const* lpeitem, bool absolute = false, bool clip_mask = false, Geom::Affine base_transform = Geom::identity()); +}; + +}; //namespace LivePathEffect +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/lpeobject-reference.cpp b/src/live_effects/lpeobject-reference.cpp new file mode 100644 index 0000000..700b8f5 --- /dev/null +++ b/src/live_effects/lpeobject-reference.cpp @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to the inkscape:live-effect attribute + * + * Copyright (C) 2007 Johan Engelen + * + * Release under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpeobject-reference.h" +#include "live_effects/effect.h" + +#include <cstring> + +#include "bad-uri-exception.h" +#include "live_effects/lpeobject.h" +#include "object/uri.h" + +namespace Inkscape { + +namespace LivePathEffect { + +static void lpeobjectreference_href_changed(SPObject *old_ref, SPObject *ref, LPEObjectReference *lpeobjref); +static void lpeobjectreference_release_self(SPObject *release, LPEObjectReference *lpeobjref); +static void lpeobjectreference_source_modified(SPObject *iSource, guint flags, LPEObjectReference *lpeobjref); + +LPEObjectReference::LPEObjectReference(SPObject* i_owner) : URIReference(i_owner) +{ + owner=i_owner; + lpeobject_href = nullptr; + lpeobject_repr = nullptr; + lpeobject = nullptr; + _changed_connection = changedSignal().connect(sigc::bind(sigc::ptr_fun(lpeobjectreference_href_changed), this)); // listening to myself, this should be virtual instead + + user_unlink = nullptr; +} + +LPEObjectReference::~LPEObjectReference() +{ + _changed_connection.disconnect(); // to do before unlinking + + quit_listening(); + unlink(); +} + +bool LPEObjectReference::_acceptObject(SPObject * const obj) const +{ + LivePathEffectObject *lpobj = dynamic_cast<LivePathEffectObject *>(obj); + if (lpobj) { + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + +void +LPEObjectReference::link(const char *to) +{ + if (!to || !to[0]) { + quit_listening(); + unlink(); + } else { + if ( !lpeobject_href || ( strcmp(to, lpeobject_href) != 0 ) ) { + if (lpeobject_href) { + g_free(lpeobject_href); + } + lpeobject_href = g_strdup(to); + try { + attach(Inkscape::URI(to)); + } catch (Inkscape::BadURIException &e) { + /* TODO: Proper error handling as per + * http://www.w3.org/TR/SVG11/implnote.html#ErrorProcessing. + */ + g_warning("%s", e.what()); + detach(); + } + } + } +} + +void +LPEObjectReference::unlink() +{ + if (lpeobject_href) { + g_free(lpeobject_href); + lpeobject_href = nullptr; + } + detach(); +} + +void +LPEObjectReference::start_listening(LivePathEffectObject* to) +{ + if ( to == nullptr ) { + return; + } + lpeobject = to; + lpeobject_repr = to->getRepr(); + _release_connection = to->connectRelease(sigc::bind(sigc::ptr_fun(&lpeobjectreference_release_self), this)); + _modified_connection = to->connectModified(sigc::bind<2>(sigc::ptr_fun(&lpeobjectreference_source_modified), this)); +} + +void +LPEObjectReference::quit_listening() +{ + _modified_connection.disconnect(); + _release_connection.disconnect(); + lpeobject_repr = nullptr; + lpeobject = nullptr; +} + +static void +lpeobjectreference_href_changed(SPObject */*old_ref*/, SPObject */*ref*/, LPEObjectReference *lpeobjref) +{ + //lpeobjref->quit_listening(); + LivePathEffectObject *refobj = dynamic_cast<LivePathEffectObject *>( lpeobjref->getObject() ); + if ( refobj ) { + lpeobjref->start_listening(refobj); + } + if (lpeobjref->owner) { + lpeobjref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +static void +lpeobjectreference_release_self(SPObject */*release*/, LPEObjectReference *lpeobjref) +{ + lpeobjref->quit_listening(); + lpeobjref->unlink(); + if (lpeobjref->user_unlink) { + lpeobjref->user_unlink(lpeobjref, lpeobjref->owner); + } + +} + +static void +lpeobjectreference_source_modified(SPObject */*iSource*/, guint /*flags*/, LPEObjectReference *lpeobjref) +{ +// We dont need to request update when LPE XML is updated +// Retain it temporary, drop if no regression +// SPObject *owner_obj = lpeobjref->owner; +// if (owner_obj) { +// lpeobjref->owner->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); +// } +} + +} //namespace LivePathEffect + +} // namespace inkscape + +/* + 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 : diff --git a/src/live_effects/lpeobject-reference.h b/src/live_effects/lpeobject-reference.h new file mode 100644 index 0000000..28c1df0 --- /dev/null +++ b/src/live_effects/lpeobject-reference.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LPEOBJECT_REFERENCE_H +#define SEEN_LPEOBJECT_REFERENCE_H + +/* + * The reference corresponding to the inkscape:live-effect attribute + * + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + +#include "object/uri-references.h" + +namespace Inkscape { + +namespace XML { +class Node; +} +} + +class LivePathEffectObject; + +namespace Inkscape { + +namespace LivePathEffect { + +class LPEObjectReference : public Inkscape::URIReference { +public: + LPEObjectReference(SPObject *owner); + ~LPEObjectReference() override; + + SPObject *owner; + + // concerning the LPEObject that is referred to: + char *lpeobject_href; + Inkscape::XML::Node *lpeobject_repr; + LivePathEffectObject *lpeobject; + + sigc::connection _modified_connection; + sigc::connection _release_connection; + sigc::connection _changed_connection; + + void link(const char* to); + void unlink(); + void start_listening(LivePathEffectObject* to); + void quit_listening(); + + void (*user_unlink) (LPEObjectReference *me, SPObject *user); + +protected: + bool _acceptObject(SPObject * const obj) const override; + +}; + +} //namespace LivePathEffect + +} // namespace inkscape + +#endif /* !SEEN_LPEOBJECT_REFERENCE_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 : diff --git a/src/live_effects/lpeobject.cpp b/src/live_effects/lpeobject.cpp new file mode 100644 index 0000000..7a17364 --- /dev/null +++ b/src/live_effects/lpeobject.cpp @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007-2008 <j.b.c.engelen@utwente.nl> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/lpeobject.h" + +#include "live_effects/effect.h" + +#include "xml/repr.h" +#include "xml/node-event-vector.h" +#include "attributes.h" +#include "document.h" + +#include "object/sp-defs.h" + +//#define LIVEPATHEFFECT_VERBOSE + +static void livepatheffect_on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data); + +static Inkscape::XML::NodeEventVector const livepatheffect_repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + livepatheffect_on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ +}; + + +LivePathEffectObject::LivePathEffectObject() + : SPObject(), effecttype(Inkscape::LivePathEffect::INVALID_LPE), effecttype_set(false), + lpe(nullptr) +{ +#ifdef LIVEPATHEFFECT_VERBOSE + g_message("Init livepatheffectobject"); +#endif +} + +LivePathEffectObject::~LivePathEffectObject() = default; + +/** + * Virtual build: set livepatheffect attributes from its associated XML node. + */ +void LivePathEffectObject::build(SPDocument *document, Inkscape::XML::Node *repr) { + g_assert(SP_IS_OBJECT(this)); + + SPObject::build(document, repr); + + this->readAttr(SPAttr::PATH_EFFECT); + + if (repr) { + repr->addListener (&livepatheffect_repr_events, this); + } + + /* Register ourselves, is this necessary? */ +// document->addResource("path-effect", object); +} + +/** + * Virtual release of livepatheffect members before destruction. + */ +void LivePathEffectObject::release() { + this->getRepr()->removeListenerByData(this); + +/* + if (object->document) { + // Unregister ourselves + sp_document_removeResource(object->document, "livepatheffect", object); + } + + if (gradient->ref) { + gradient->modified_connection.disconnect(); + gradient->ref->detach(); + delete gradient->ref; + gradient->ref = NULL; + } + + gradient->modified_connection.~connection(); +*/ + + if (this->lpe) { + delete this->lpe; + this->lpe = nullptr; + } + + this->effecttype = Inkscape::LivePathEffect::INVALID_LPE; + + SPObject::release(); +} + +/** + * Virtual set: set attribute to value. + */ +void LivePathEffectObject::set(SPAttr key, gchar const *value) { +#ifdef LIVEPATHEFFECT_VERBOSE + g_print("Set livepatheffect"); +#endif + + switch (key) { + case SPAttr::PATH_EFFECT: + if (this->lpe) { + delete this->lpe; + this->lpe = nullptr; + } + + if ( value && Inkscape::LivePathEffect::LPETypeConverter.is_valid_key(value) ) { + this->effecttype = Inkscape::LivePathEffect::LPETypeConverter.get_id_from_key(value); + this->lpe = Inkscape::LivePathEffect::Effect::New(this->effecttype, this); + this->effecttype_set = true; + } else { + this->effecttype = Inkscape::LivePathEffect::INVALID_LPE; + this->lpe = nullptr; + this->effecttype_set = false; + } + + this->requestModified(SP_OBJECT_MODIFIED_FLAG); + break; + } + + SPObject::set(key, value); +} + +/** + * Virtual write: write object attributes to repr. + */ +Inkscape::XML::Node* LivePathEffectObject::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) { + if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) { + repr = xml_doc->createElement("inkscape:path-effect"); + } + + if ((flags & SP_OBJECT_WRITE_ALL) || this->lpe) { + repr->setAttributeOrRemoveIfEmpty("effect", Inkscape::LivePathEffect::LPETypeConverter.get_key(this->effecttype)); + + this->lpe->writeParamsToSVG(); + } + + SPObject::write(xml_doc, repr, flags); + + return repr; +} + +static void +livepatheffect_on_repr_attr_changed ( Inkscape::XML::Node * /*repr*/, + const gchar *key, + const gchar */*oldval*/, + const gchar *newval, + bool /*is_interactive*/, + void * data ) +{ +#ifdef LIVEPATHEFFECT_VERBOSE + g_print("livepatheffect_on_repr_attr_changed"); +#endif + + if (!data) + return; + + LivePathEffectObject *lpeobj = (LivePathEffectObject*) data; + if (!lpeobj->get_lpe()) + return; + + lpeobj->get_lpe()->setParameter(key, newval); + + lpeobj->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +// Caution using this function, just compare id and same type of +// effect, we use on clipboard to do not fork in same doc on pastepatheffect +bool LivePathEffectObject::is_similar(LivePathEffectObject *that) +{ + if (that) { + const char *thisid = this->getId(); + const char *thatid = that->getId(); + if (!thisid || !thatid || strcmp(thisid, thatid) != 0) { + return false; + } + Inkscape::LivePathEffect::Effect *thislpe = this->get_lpe(); + Inkscape::LivePathEffect::Effect *thatlpe = that->get_lpe(); + if (thatlpe && thislpe && thislpe->getName() != thatlpe->getName()) { + return false; + } + } + return true; +} + +/** + * If this has other users, create a new private duplicate and return it + * returns 'this' when no forking was necessary (and therefore no duplicate was made) + * Check out SPLPEItem::forkPathEffectsIfNecessary ! + */ +LivePathEffectObject *LivePathEffectObject::fork_private_if_necessary(unsigned int nr_of_allowed_users) +{ + if (hrefcount > nr_of_allowed_users) { + SPDocument *doc = this->document; + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *dup_repr = this->getRepr()->duplicate(xml_doc); + + doc->getDefs()->getRepr()->addChild(dup_repr, nullptr); + LivePathEffectObject *lpeobj_new = dynamic_cast<LivePathEffectObject *>(doc->getObjectByRepr(dup_repr)); + Inkscape::GC::release(dup_repr); + // To regenerate ID + sp_object_ref(lpeobj_new, nullptr); + gchar *id = sp_object_get_unique_id(this, nullptr); + lpeobj_new->setAttribute("id", id); + g_free(id); + // Load all volatile vars of forked item + sp_object_unref(lpeobj_new, nullptr); + return lpeobj_new; + } + return this; +} + +/* + 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 : diff --git a/src/live_effects/lpeobject.h b/src/live_effects/lpeobject.h new file mode 100644 index 0000000..7353d4e --- /dev/null +++ b/src/live_effects/lpeobject.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_OBJECT_H +#define INKSCAPE_LIVEPATHEFFECT_OBJECT_H + +/* + * Inkscape::LivePathEffect + * + * Copyright (C) Johan Engelen 2007-2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "effect-enum.h" + +#include "object/sp-object.h" + +namespace Inkscape { + namespace XML { + class Node; + struct Document; + } + namespace LivePathEffect { + class Effect; + } +} + +#define LIVEPATHEFFECT(obj) ((LivePathEffectObject*)obj) +#define IS_LIVEPATHEFFECT(obj) (dynamic_cast<const LivePathEffectObject*>((SPObject*)obj)) + +class LivePathEffectObject : public SPObject { +public: + LivePathEffectObject(); + ~LivePathEffectObject() override; + + Inkscape::LivePathEffect::EffectType effecttype; + + bool effecttype_set; + // dont check values only structure and ID + bool is_similar(LivePathEffectObject *that); + + LivePathEffectObject * fork_private_if_necessary(unsigned int nr_of_allowed_users = 1); + + /* Note that the returned pointer can be NULL in a valid LivePathEffectObject contained in a valid list of lpeobjects in an lpeitem! + * So one should always check whether the returned value is NULL or not */ + Inkscape::LivePathEffect::Effect * get_lpe() { + return lpe; + } + Inkscape::LivePathEffect::Effect const * get_lpe() const { + return lpe; + }; + + Inkscape::LivePathEffect::Effect *lpe; // this can be NULL in a valid LivePathEffectObject + +protected: + void build(SPDocument* doc, Inkscape::XML::Node* repr) override; + void release() override; + + void set(SPAttr key, char const* value) override; + + Inkscape::XML::Node* write(Inkscape::XML::Document* doc, Inkscape::XML::Node* repr, unsigned int flags) override; +}; + +#endif + +/* + 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/live_effects/parameter/array.cpp b/src/live_effects/parameter/array.cpp new file mode 100644 index 0000000..98b3db2 --- /dev/null +++ b/src/live_effects/parameter/array.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include "live_effects/parameter/array.h" + +#include <2geom/coord.h> +#include <2geom/point.h> + +#include "helper-fns.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" + +namespace Inkscape { + +namespace LivePathEffect { + +template <> +double +ArrayParam<double>::readsvg(const gchar * str) +{ + double newx = Geom::infinity(); + sp_svg_number_read_d(str, &newx); + return newx; +} + +template <> +float +ArrayParam<float>::readsvg(const gchar * str) +{ + float newx = Geom::infinity(); + sp_svg_number_read_f(str, &newx); + return newx; +} + +template <> +Geom::Point +ArrayParam<Geom::Point>::readsvg(const gchar * str) +{ + gchar ** strarray = g_strsplit(str, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + return Geom::Point(newx, newy); + } + return Geom::Point(Geom::infinity(),Geom::infinity()); +} + +template <> +std::shared_ptr<SatelliteReference> ArrayParam<std::shared_ptr<SatelliteReference>>::readsvg(const gchar *str) +{ + std::shared_ptr<SatelliteReference> satellitereference = nullptr; + if (!str) { + return satellitereference; + } + + gchar **strarray = g_strsplit(str, ",", 2); + if (strarray[0] != nullptr && g_strstrip(strarray[0])[0] == '#') { + try { + bool active = strarray[1] != nullptr; + satellitereference = std::make_shared<SatelliteReference>(param_effect->getLPEObj(), active); + satellitereference->attach(Inkscape::URI(g_strstrip(strarray[0]))); + if (active) { + satellitereference->setActive(strncmp(strarray[1], "1", 1) == 0); + } + } catch (Inkscape::BadURIException &e) { + g_warning("%s (%s)", e.what(), strarray[0]); + satellitereference->detach(); + } + } + g_strfreev(strarray); + return satellitereference; +} + +template <> +std::vector<NodeSatellite> ArrayParam<std::vector<NodeSatellite>>::readsvg(const gchar *str) +{ + std::vector<NodeSatellite> subpath_nodesatellites; + if (!str) { + return subpath_nodesatellites; + } + gchar ** strarray = g_strsplit(str, "@", 0); + gchar ** iter = strarray; + while (*iter != nullptr) { + gchar ** strsubarray = g_strsplit(*iter, ",", 8); + if (*strsubarray[7]) {//steps always > 0 + NodeSatellite *nodesatellite = new NodeSatellite(); + nodesatellite->setNodeSatellitesType(g_strstrip(strsubarray[0])); + nodesatellite->is_time = strncmp(strsubarray[1], "1", 1) == 0; + nodesatellite->selected = strncmp(strsubarray[2], "1", 1) == 0; + nodesatellite->has_mirror = strncmp(strsubarray[3], "1", 1) == 0; + nodesatellite->hidden = strncmp(strsubarray[4], "1", 1) == 0; + double amount,angle; + float stepsTmp; + sp_svg_number_read_d(strsubarray[5], &amount); + sp_svg_number_read_d(strsubarray[6], &angle); + sp_svg_number_read_f(g_strstrip(strsubarray[7]), &stepsTmp); + unsigned int steps = (unsigned int)stepsTmp; + nodesatellite->amount = amount; + nodesatellite->angle = angle; + nodesatellite->steps = steps; + subpath_nodesatellites.push_back(*nodesatellite); + } + g_strfreev (strsubarray); + iter++; + } + g_strfreev (strarray); + return subpath_nodesatellites; +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/array.h b/src/live_effects/parameter/array.h new file mode 100644 index 0000000..64e8d6d --- /dev/null +++ b/src/live_effects/parameter/array.h @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <vector> + +#include "bad-uri-exception.h" +#include "helper/geom-nodesatellite.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/satellite-reference.h" +#include "object/uri.h" +#include "svg/stringstream.h" +#include "svg/svg.h" + +namespace Inkscape { + +namespace LivePathEffect { + +template <typename StorageType> +class ArrayParam : public Parameter { +public: + ArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + size_t n = 0 ) + : Parameter(label, tip, key, wr, effect), _vector(n), _default_size(n) + { + + } + + ~ArrayParam() override = default;; + + std::vector<StorageType> const & data() const { + return _vector; + } + + Gtk::Widget * param_newWidget() override { + return nullptr; + } + + bool param_readSVGValue(const gchar * strvalue) override { + _vector.clear(); + gchar ** strarray = g_strsplit(strvalue, "|", 0); + gchar ** iter = strarray; + while (*iter != nullptr) { + _vector.push_back( readsvg(*iter) ); + iter++; + } + g_strfreev (strarray); + return true; + } + void param_update_default(const gchar * default_value) override{}; + Glib::ustring param_getSVGValue() const override { + Inkscape::SVGOStringStream os; + writesvg(os, _vector); + return os.str(); + } + + Glib::ustring param_getDefaultSVGValue() const override { + return ""; + } + + void param_setValue(std::vector<StorageType> const &new_vector) { + _vector = new_vector; + } + + void param_set_default() override { + param_setValue( std::vector<StorageType>(_default_size) ); + } + + void param_set_and_write_new_value(std::vector<StorageType> const &new_vector) { + Inkscape::SVGOStringStream os; + writesvg(os, new_vector); + gchar * str = g_strdup(os.str().c_str()); + param_write_to_repr(str); + g_free(str); + } + ParamType paramType() const override { return ParamType::ARRAY; }; +protected: + std::vector<StorageType> _vector; + size_t _default_size; + + void writesvg(SVGOStringStream &str, std::vector<StorageType> const &vector) const { + for (unsigned int i = 0; i < vector.size(); ++i) { + if (i != 0) { + // separate items with pipe symbol + str << " | "; + } + writesvgData(str,vector[i]); + } + } + + void writesvgData(SVGOStringStream &str, float const &vector_data) const { + str << vector_data; + } + + void writesvgData(SVGOStringStream &str, double const &vector_data) const { + str << vector_data; + } + + void writesvgData(SVGOStringStream &str, Geom::Point const &vector_data) const { + str << vector_data; + } + + void writesvgData(SVGOStringStream &str, std::shared_ptr<SatelliteReference> const &vector_data) const + { + if (vector_data && vector_data->isAttached()) { + str << vector_data->getURI()->str(); + if (vector_data->getHasActive()) { + str << ","; + str << vector_data->getActive(); + } + } + } + + void writesvgData(SVGOStringStream &str, std::vector<NodeSatellite> const &vector_data) const + { + for (size_t i = 0; i < vector_data.size(); ++i) { + if (i != 0) { + // separate nodes with @ symbol ( we use | for paths) + str << " @ "; + } + str << vector_data[i].getNodeSatellitesTypeGchar(); + str << ","; + str << vector_data[i].is_time; + str << ","; + str << vector_data[i].selected; + str << ","; + str << vector_data[i].has_mirror; + str << ","; + str << vector_data[i].hidden; + str << ","; + str << vector_data[i].amount; + str << ","; + str << vector_data[i].angle; + str << ","; + str << static_cast<int>(vector_data[i].steps); + } + } + + StorageType readsvg(const gchar * str); + +private: + ArrayParam(const ArrayParam&); + ArrayParam& operator=(const ArrayParam&); +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/bool.cpp b/src/live_effects/parameter/bool.cpp new file mode 100644 index 0000000..79841b4 --- /dev/null +++ b/src/live_effects/parameter/bool.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "bool.h" + +#include <glibmm/i18n.h> + +#include "helper-fns.h" +#include "inkscape.h" + +#include "live_effects/effect.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "ui/widget/registered-widget.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +BoolParam::BoolParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, bool default_value) + : Parameter(label, tip, key, wr, effect), value(default_value), defvalue(default_value) +{ +} + +BoolParam::~BoolParam() += default; + +void +BoolParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +BoolParam::param_update_default(bool const default_value) +{ + defvalue = default_value; +} + +void +BoolParam::param_update_default(const gchar * default_value) +{ + param_update_default(helperfns_read_bool(default_value, defvalue)); +} + +bool +BoolParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(helperfns_read_bool(strvalue, defvalue)); + return true; // not correct: if value is unacceptable, should return false! +} + +Glib::ustring +BoolParam::param_getSVGValue() const +{ + return value ? "true" : "false"; +} + +Glib::ustring +BoolParam::param_getDefaultSVGValue() const +{ + return defvalue ? "true" : "false"; +} + +Gtk::Widget * +BoolParam::param_newWidget() +{ + if(widget_is_visible){ + Inkscape::UI::Widget::RegisteredCheckButton * checkwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredCheckButton( param_label, + param_tooltip, + param_key, + *param_wr, + false, + param_effect->getRepr(), + param_effect->getSPDoc()) ); + + checkwdg->setActive(value); + checkwdg->setProgrammatically = false; + checkwdg->set_undo_parameters(_("Change bool parameter"), INKSCAPE_ICON("dialog-path-effects")); + return dynamic_cast<Gtk::Widget *> (checkwdg); + } else { + return nullptr; + } +} + +void +BoolParam::param_setValue(bool newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/bool.h b/src/live_effects/parameter/bool.h new file mode 100644 index 0000000..b67f555 --- /dev/null +++ b/src/live_effects/parameter/bool.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_BOOL_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_BOOL_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + + +class BoolParam : public Parameter { +public: + BoolParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + bool default_value = false); + ~BoolParam() override; + BoolParam(const BoolParam&) = delete; + BoolParam& operator=(const BoolParam&) = delete; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(bool newvalue); + void param_set_default() override; + void param_update_default(bool const default_value); + void param_update_default(const gchar * default_value) override; + bool get_value() const { return value; }; + inline operator bool() const { return value; }; + ParamType paramType() const override { return ParamType::BOOL; }; + +private: + bool value; + bool defvalue; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/colorpicker.cpp b/src/live_effects/parameter/colorpicker.cpp new file mode 100644 index 0000000..cf9243e --- /dev/null +++ b/src/live_effects/parameter/colorpicker.cpp @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "colorpicker.h" + +#include <gtkmm.h> + +#include "color.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" + +#include "live_effects/effect.h" +#include "live_effects/parameter/colorpicker.h" + +#include "svg/stringstream.h" +#include "svg/svg-color.h" +#include "svg/svg.h" + +#include "ui/icon-names.h" +#include "ui/widget/registered-widget.h" + +#include <glibmm/i18n.h> + +namespace Inkscape { + +namespace LivePathEffect { + +ColorPickerParam::ColorPickerParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const guint32 default_color ) + : Parameter(label, tip, key, wr, effect), + value(default_color), + defvalue(default_color) +{ + +} + +void +ColorPickerParam::param_set_default() +{ + param_setValue(defvalue); +} + +static guint32 sp_read_color_alpha(gchar const *str, guint32 def) +{ + guint32 val = 0; + if (str == nullptr) return def; + while ((*str <= ' ') && *str) str++; + if (!*str) return def; + + if (str[0] == '#') { + gint i; + for (i = 1; str[i]; i++) { + int hexval; + if (str[i] >= '0' && str[i] <= '9') + hexval = str[i] - '0'; + else if (str[i] >= 'A' && str[i] <= 'F') + hexval = str[i] - 'A' + 10; + else if (str[i] >= 'a' && str[i] <= 'f') + hexval = str[i] - 'a' + 10; + else + break; + val = (val << 4) + hexval; + } + if (i != 1 + 8) { + return def; + } + } + return val; +} + +void +ColorPickerParam::param_update_default(const gchar * default_value) +{ + defvalue = sp_read_color_alpha(default_value, 0x000000ff); +} + +bool +ColorPickerParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(sp_read_color_alpha(strvalue, 0x000000ff)); + return true; +} + +Glib::ustring +ColorPickerParam::param_getSVGValue() const +{ + gchar c[32]; + sprintf(c, "#%08x", value); + return c; +} + +Glib::ustring +ColorPickerParam::param_getDefaultSVGValue() const +{ + gchar c[32]; + sprintf(c, "#%08x", defvalue); + return c; +} + +Gtk::Widget * +ColorPickerParam::param_newWidget() +{ + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + + hbox->set_border_width(5); + hbox->set_homogeneous(false); + hbox->set_spacing(2); + Inkscape::UI::Widget::RegisteredColorPicker * colorpickerwdg = + new Inkscape::UI::Widget::RegisteredColorPicker( param_label, + param_label, + param_tooltip, + param_key, + param_key + "_opacity_LPE", + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ); + SPDocument *document = param_effect->getSPDoc(); + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + colorpickerwdg->setRgba32(value); + DocumentUndo::setUndoSensitive(document, saved); + colorpickerwdg->set_undo_parameters(_("Change color button parameter"), INKSCAPE_ICON("dialog-path-effects")); + hbox->pack_start(*dynamic_cast<Gtk::Widget *> (colorpickerwdg), true, true); + return dynamic_cast<Gtk::Widget *> (hbox); +} + +void +ColorPickerParam::param_setValue(const guint32 newvalue) +{ + value = newvalue; +} + + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/colorpicker.h b/src/live_effects/parameter/colorpicker.h new file mode 100644 index 0000000..ff7abe0 --- /dev/null +++ b/src/live_effects/parameter/colorpicker.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_COLOR_BUTTON_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_COLOR_BUTTON_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glib.h> +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class ColorPickerParam : public Parameter { +public: + ColorPickerParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const guint32 default_color = 0x000000ff); + ~ColorPickerParam() override = default; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + void param_update_default(const gchar * default_value) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(guint32 newvalue); + + void param_set_default() override; + + guint32 get_value() const { return value; }; + ParamType paramType() const override { return ParamType::COLOR_PICKER; }; + +private: + ColorPickerParam(const ColorPickerParam&) = delete; + ColorPickerParam& operator=(const ColorPickerParam&) = delete; + guint32 value; + guint32 defvalue; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/enum.h b/src/live_effects/parameter/enum.h new file mode 100644 index 0000000..91af18c --- /dev/null +++ b/src/live_effects/parameter/enum.h @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ENUM_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ENUM_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/ustring.h> + +#include "live_effects/effect.h" +#include "live_effects/parameter/parameter.h" + +#include "ui/icon-names.h" +#include "ui/widget/registered-enums.h" + +namespace Inkscape { + +namespace LivePathEffect { + +template<typename E> class EnumParam : public Parameter { +public: + EnumParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + const Util::EnumDataConverter<E>& c, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + E default_value, + bool sort = true) + : Parameter(label, tip, key, wr, effect) + { + enumdataconv = &c; + defvalue = default_value; + value = defvalue; + sorted = sort; + }; + + ~EnumParam() override = default;; + EnumParam(const EnumParam&) = delete; + EnumParam& operator=(const EnumParam&) = delete; + + Gtk::Widget * param_newWidget() override { + Inkscape::UI::Widget::RegisteredEnum<E> *regenum = Gtk::manage ( + new Inkscape::UI::Widget::RegisteredEnum<E>( param_label, param_tooltip, + param_key, *enumdataconv, *param_wr, param_effect->getRepr(), param_effect->getSPDoc(), sorted ) ); + + regenum->set_active_by_id(value); + regenum->combobox()->setProgrammatically = false; + regenum->combobox()->signal_changed().connect(sigc::mem_fun (*this, &EnumParam::_on_change_combo)); + regenum->set_undo_parameters(_("Change enumeration parameter"), INKSCAPE_ICON("dialog-path-effects")); + + return dynamic_cast<Gtk::Widget *> (regenum); + }; + void _on_change_combo() { param_effect->refresh_widgets = true; } + bool param_readSVGValue(const gchar * strvalue) override { + if (!strvalue) { + param_set_default(); + return true; + } + + param_set_value( enumdataconv->get_id_from_key(Glib::ustring(strvalue)) ); + + return true; + }; + Glib::ustring param_getSVGValue() const override { + return enumdataconv->get_key(value); + }; + + Glib::ustring param_getDefaultSVGValue() const override { + return enumdataconv->get_key(defvalue).c_str(); + }; + + E get_value() const { + return value; + } + + inline operator E() const { + return value; + }; + + void param_set_default() override { + param_set_value(defvalue); + } + + void param_update_default(E default_value) { + defvalue = default_value; + } + + void param_update_default(const gchar * default_value) override { + param_update_default(enumdataconv->get_id_from_key(Glib::ustring(default_value))); + } + + void param_set_value(E val) { + value = val; + } + ParamType paramType() const override { return ParamType::ENUM; }; +private: + E value; + E defvalue; + bool sorted; + + const Util::EnumDataConverter<E> * enumdataconv; +}; + + +}; //namespace LivePathEffect + +}; //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/fontbutton.cpp b/src/live_effects/parameter/fontbutton.cpp new file mode 100644 index 0000000..441f14e --- /dev/null +++ b/src/live_effects/parameter/fontbutton.cpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "fontbutton.h" + +#include <glibmm/i18n.h> +#include <gtkmm.h> + +#include "live_effects/effect.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "ui/widget/font-button.h" +#include "ui/widget/registered-widget.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +FontButtonParam::FontButtonParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const Glib::ustring default_value ) + : Parameter(label, tip, key, wr, effect), + value(default_value), + defvalue(default_value) +{ +} + +void +FontButtonParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +FontButtonParam::param_update_default(const gchar * default_value) +{ + defvalue = Glib::ustring(default_value); +} + +bool +FontButtonParam::param_readSVGValue(const gchar * strvalue) +{ + Inkscape::SVGOStringStream os; + os << strvalue; + param_setValue((Glib::ustring)os.str()); + return true; +} + +Glib::ustring +FontButtonParam::param_getSVGValue() const +{ + return value.c_str(); +} + +Glib::ustring +FontButtonParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + + + +Gtk::Widget * +FontButtonParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredFontButton * fontbuttonwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredFontButton( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + Glib::ustring fontspec = param_getSVGValue(); + fontbuttonwdg->setValue( fontspec); + fontbuttonwdg->set_undo_parameters(_("Change font button parameter"), INKSCAPE_ICON("dialog-path-effects")); + return dynamic_cast<Gtk::Widget *> (fontbuttonwdg); +} + +void +FontButtonParam::param_setValue(const Glib::ustring newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; +} + + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/fontbutton.h b/src/live_effects/parameter/fontbutton.h new file mode 100644 index 0000000..266d324 --- /dev/null +++ b/src/live_effects/parameter/fontbutton.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_FONT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_FONT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glib.h> +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class FontButtonParam : public Parameter { +public: + FontButtonParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const Glib::ustring default_value = "Sans 10"); + ~FontButtonParam() override = default; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + void param_update_default(const gchar * default_value) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(Glib::ustring newvalue); + + void param_set_default() override; + + const Glib::ustring get_value() const { return defvalue; }; + ParamType paramType() const override { return ParamType::FONT_BUTTON; }; +private: + FontButtonParam(const FontButtonParam&) = delete; + FontButtonParam& operator=(const FontButtonParam&) = delete; + Glib::ustring value; + Glib::ustring defvalue; + +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/hidden.cpp b/src/live_effects/parameter/hidden.cpp new file mode 100644 index 0000000..dcb468e --- /dev/null +++ b/src/live_effects/parameter/hidden.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) jabiertxof 2017 <jabier.arraiza@marker.es> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Authors: + * Jabiertxof + * Maximilian Albert + * Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include "live_effects/parameter/hidden.h" +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "svg/stringstream.h" + +namespace Inkscape { + +namespace LivePathEffect { + +HiddenParam::HiddenParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const Glib::ustring default_value, bool is_visible) + : Parameter(label, tip, key, wr, effect), + value(default_value), + defvalue(default_value) +{ + param_widget_is_visible(is_visible); +} + +void +HiddenParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +HiddenParam::param_update_default(const gchar * default_value) +{ + defvalue = (Glib::ustring)default_value; +} + + +bool +HiddenParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(strvalue); + return true; +} + +Glib::ustring +HiddenParam::param_getSVGValue() const +{ + return value; +} + +Glib::ustring +HiddenParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + +Gtk::Widget * +HiddenParam::param_newWidget() +{ + return nullptr; +} + +void +HiddenParam::param_setValue(const Glib::ustring newvalue, bool write) +{ + value = newvalue; + if (write) { + param_write_to_repr(value.c_str()); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/hidden.h b/src/live_effects/parameter/hidden.h new file mode 100644 index 0000000..714455a --- /dev/null +++ b/src/live_effects/parameter/hidden.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_HIDDEN_H +#define INKSCAPE_LIVEPATHEFFECT_HIDDEN_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Jabiertxof + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) jabiertxof 2017 <jabier.arraiza@marker.es> + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/parameter.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +class HiddenParam : public Parameter { +public: + HiddenParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const Glib::ustring default_value = "", + bool widget_is_visible = false); + ~HiddenParam() override = default; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(Glib::ustring newvalue, bool write = false); + void param_set_default() override; + void param_update_default(const gchar * default_value) override; + + const Glib::ustring get_value() const { return value; }; + ParamType paramType() const override { return ParamType::HIDDEN; }; +private: + HiddenParam(const HiddenParam&) = delete; + HiddenParam& operator=(const HiddenParam&) = delete; + Glib::ustring value; + Glib::ustring defvalue; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/message.cpp b/src/live_effects/parameter/message.cpp new file mode 100644 index 0000000..341fc58 --- /dev/null +++ b/src/live_effects/parameter/message.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <gtkmm.h> + +#include <utility> + +#include "include/gtkmm_version.h" +#include "live_effects/parameter/message.h" +#include "live_effects/effect.h" + +namespace Inkscape { + +namespace LivePathEffect { + +MessageParam::MessageParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const gchar * default_message, Glib::ustring legend, + Gtk::Align halign, Gtk::Align valign, double marginstart, double marginend) + : Parameter(label, tip, key, wr, effect), + message(default_message), + defmessage(default_message), + _legend(std::move(legend)), + _halign(halign), + _valign(valign), + _marginstart(marginstart), + _marginend(marginend) +{ + if (_legend == Glib::ustring("Use Label")) { + _legend = label; + } + _label = nullptr; + _min_height = -1; +} + +void +MessageParam::param_set_default() +{ + param_setValue(defmessage); +} + +void +MessageParam::param_update_default(const gchar * default_message) +{ + defmessage = default_message; +} + +bool +MessageParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(strvalue); + return true; +} + +Glib::ustring +MessageParam::param_getSVGValue() const +{ + return message; +} + +Glib::ustring +MessageParam::param_getDefaultSVGValue() const +{ + return defmessage; +} + +void +MessageParam::param_set_min_height(int height) +{ + _min_height = height; + if (_label) { + _label->set_size_request(-1, _min_height); + } +} + + +Gtk::Widget * +MessageParam::param_newWidget() +{ + Gtk::Frame * frame = new Gtk::Frame (_legend); + Gtk::Widget * widg_frame = frame->get_label_widget(); + + widg_frame->set_margin_end(_marginend); + widg_frame->set_margin_start(_marginstart); + _label = new Gtk::Label (message, Gtk::ALIGN_END); + _label->set_use_underline (true); + _label->set_use_markup(); + _label->set_line_wrap(true); + _label->set_size_request(-1, _min_height); + Gtk::Widget* widg_label = dynamic_cast<Gtk::Widget *> (_label); + widg_label->set_halign(_halign); + widg_label->set_valign(_valign); + widg_label->set_margin_end(_marginend); + widg_label->set_margin_start(_marginstart); + frame->add(*widg_label); + return dynamic_cast<Gtk::Widget *> (frame); +} + +void +MessageParam::param_setValue(const gchar * strvalue) +{ + if (strcmp(strvalue, message) != 0) { + param_effect->refresh_widgets = true; + } + message = strvalue; +} + + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/message.h b/src/live_effects/parameter/message.h new file mode 100644 index 0000000..aa6a8da --- /dev/null +++ b/src/live_effects/parameter/message.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_MESSAGE_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_MESSAGE_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#include <glib.h> +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class MessageParam : public Parameter { +public: + MessageParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_message = "Default message", + Glib::ustring legend = "Use Label", + Gtk::Align halign = Gtk::ALIGN_START, + Gtk::Align valign = Gtk::ALIGN_CENTER, + double marginstart = 6, + double marginend = 6); + ~MessageParam() override = default; + + Gtk::Widget * param_newWidget() override; + bool param_readSVGValue(const gchar * strvalue) override; + void param_update_default(const gchar * default_value) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(const gchar * message); + + void param_set_default() override; + void param_set_min_height(int height); + const gchar * get_value() const { return message; }; + ParamType paramType() const override { return ParamType::MESSAGE; }; +private: + Gtk::Label * _label; + int _min_height; + MessageParam(const MessageParam&) = delete; + MessageParam& operator=(const MessageParam&) = delete; + const gchar * message; + const gchar * defmessage; + Glib::ustring _legend; + Gtk::Align _halign; + Gtk::Align _valign; + double _marginstart; + double _marginend; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/nodesatellitesarray.cpp b/src/live_effects/parameter/nodesatellitesarray.cpp new file mode 100644 index 0000000..5f12f4c --- /dev/null +++ b/src/live_effects/parameter/nodesatellitesarray.cpp @@ -0,0 +1,569 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Author(s): + * Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * + * Copyright (C) 2014 Author(s) + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "nodesatellitesarray.h" + +#include "helper/geom.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-fillet-chamfer.h" +#include "preferences.h" +#include "ui/dialog/lpe-fillet-chamfer-properties.h" +#include "ui/knot/knot-holder.h" +#include "ui/shape-editor.h" +#include "ui/tools/node-tool.h" + +// TODO due to internal breakage in glibmm headers, +// this has to be included last. +#include <glibmm/i18n.h> + +namespace Inkscape { + +namespace LivePathEffect { + +NodeSatelliteArrayParam::NodeSatelliteArrayParam(const Glib::ustring &label, const Glib::ustring &tip, + const Glib::ustring &key, Inkscape::UI::Widget::Registry *wr, + Effect *effect) + : ArrayParam<std::vector<NodeSatellite>>(label, tip, key, wr, effect, 0) + , _knoth(nullptr) +{ + param_widget_is_visible(false); +} + +void NodeSatelliteArrayParam::set_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, Inkscape::CanvasItemCtrlMode mode, + guint32 color) +{ + _knot_shape = shape; + _knot_mode = mode; + _knot_color = color; +} + +void NodeSatelliteArrayParam::setPathVectorNodeSatellites(PathVectorNodeSatellites *pathVectorNodeSatellites, + bool write) +{ + _last_pathvector_nodesatellites = pathVectorNodeSatellites; + if (write) { + param_set_and_write_new_value(_last_pathvector_nodesatellites->getNodeSatellites()); + } else { + param_setValue(_last_pathvector_nodesatellites->getNodeSatellites()); + } +} + +void NodeSatelliteArrayParam::reloadKnots() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool *>(desktop->event_context); + if (nt) { + for (auto &_shape_editor : nt->_shape_editors) { + Inkscape::UI::ShapeEditor *shape_editor = _shape_editor.second.get(); + if (shape_editor && shape_editor->lpeknotholder) { + SPItem *item = shape_editor->knotholder->item; + shape_editor->unset_item(true); + shape_editor->set_item(item); + } + } + } + } +} +void NodeSatelliteArrayParam::setUseDistance(bool use_knot_distance) +{ + _use_distance = use_knot_distance; +} + +void NodeSatelliteArrayParam::setCurrentZoom(double current_zoom) +{ + _current_zoom = current_zoom; +} + +void NodeSatelliteArrayParam::setGlobalKnotHide(bool global_knot_hide) +{ + _global_knot_hide = global_knot_hide; +} + +void NodeSatelliteArrayParam::setEffectType(EffectType et) +{ + _effectType = et; +} + +void NodeSatelliteArrayParam::updateCanvasIndicators(bool mirror) +{ + if (!_last_pathvector_nodesatellites) { + return; + } + + if (!_hp.empty()) { + _hp.clear(); + } + Geom::PathVector pathv = _last_pathvector_nodesatellites->getPathVector(); + if (pathv.empty()) { + return; + } + if (mirror == true) { + _hp.clear(); + } + if (_effectType == FILLET_CHAMFER) { + for (size_t i = 0; i < _vector.size(); ++i) { + for (size_t j = 0; j < _vector[i].size(); ++j) { + if (_vector[i][j].hidden || // Ignore if hidden + (!_vector[i][j].has_mirror && mirror == true) || // Ignore if not have mirror and we are in mirror + // loop + _vector[i][j].amount == 0 || // no helper in 0 value + j >= count_path_nodes(pathv[i]) || // ignore last nodesatellite in open paths with fillet chamfer + // effect + (!pathv[i].closed() && j == 0) || // ignore first nodesatellites on open paths + count_path_nodes(pathv[i]) == 2) { + continue; + } + Geom::Curve *curve_in = pathv[i][j].duplicate(); + double pos = 0; + bool overflow = false; + double size_out = _vector[i][j].arcDistance(*curve_in); + double length_out = curve_in->length(); + gint previous_index = + j - 1; // Always are previous index because we skip first nodesatellite on open paths + if (j == 0 && pathv[i].closed()) { + previous_index = count_path_nodes(pathv[i]) - 1; + } + if ( previous_index < 0 ) { + return; + } + double length_in = pathv.curveAt(previous_index).length(); + if (mirror) { + curve_in = const_cast<Geom::Curve *>(&pathv.curveAt(previous_index)); + pos = _vector[i][j].time(size_out, true, *curve_in); + if (length_out < size_out) { + overflow = true; + } + } else { + pos = _vector[i][j].time(*curve_in); + if (length_in < size_out) { + overflow = true; + } + } + if (pos <= 0 || pos >= 1) { + continue; + } + } + } + } + if (mirror) { + updateCanvasIndicators(false); + } +} +void NodeSatelliteArrayParam::updateCanvasIndicators() +{ + updateCanvasIndicators(true); +} + +void NodeSatelliteArrayParam::addCanvasIndicators(SPLPEItem const * /*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(_hp); +} + +void NodeSatelliteArrayParam::param_transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + + if (prefs->getBool("/options/transform/rectcorners", true)) { + for (auto & i : _vector) { + for (auto & j : i) { + if (!j.is_time && j.amount > 0) { + j.amount = j.amount * ((postmul.expansionX() + postmul.expansionY()) / 2); + } + } + } + param_set_and_write_new_value(_vector); + } +} + +void NodeSatelliteArrayParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item, bool mirror) +{ + if (!_last_pathvector_nodesatellites) { + return; + } + size_t index = 0; + for (size_t i = 0; i < _vector.size(); ++i) { + for (size_t j = 0; j < _vector[i].size(); ++j) { + if (!_vector[i][j].has_mirror && mirror) { + continue; + } + NodeSatelliteType type = _vector[i][j].nodesatellite_type; + if (mirror && i == 0 && j == 0) { + index += _last_pathvector_nodesatellites->getTotalNodeSatellites(); + } + using namespace Geom; + //If is for filletChamfer effect... + if (_effectType == FILLET_CHAMFER) { + const gchar *tip; + if (type == CHAMFER) { + tip = _("<b>Chamfer</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> reset"); + } else if (type == INVERSE_CHAMFER) { + tip = _("<b>Inverse Chamfer</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> reset"); + } else if (type == INVERSE_FILLET) { + tip = _("<b>Inverse Fillet</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> reset"); + } else { + tip = _("<b>Fillet</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> reset"); + } + FilletChamferKnotHolderEntity *e = new FilletChamferKnotHolderEntity(this, index); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:Chamfer", + _(tip), _knot_color); + knotholder->add(e); + } + index++; + } + } + if (mirror) { + addKnotHolderEntities(knotholder, item, false); + } +} + +void NodeSatelliteArrayParam::updateAmmount(double amount) +{ + Geom::PathVector const pathv = _last_pathvector_nodesatellites->getPathVector(); + NodeSatellites nodesatellites = _last_pathvector_nodesatellites->getNodeSatellites(); + for (size_t i = 0; i < nodesatellites.size(); ++i) { + for (size_t j = 0; j < nodesatellites[i].size(); ++j) { + Geom::Curve const &curve_in = pathv[i][j]; + if (param_effect->isNodePointSelected(curve_in.initialPoint()) ){ + _vector[i][j].amount = amount; + _vector[i][j].setSelected(true); + } else { + _vector[i][j].setSelected(false); + } + } + } +} + +void NodeSatelliteArrayParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knoth = knotholder; + addKnotHolderEntities(knotholder, item, true); +} + +FilletChamferKnotHolderEntity::FilletChamferKnotHolderEntity(NodeSatelliteArrayParam *p, size_t index) + : _pparam(p) + , _index(index) +{} + +void FilletChamferKnotHolderEntity::knot_set(Geom::Point const &p, + Geom::Point const &/*origin*/, + guint state) +{ + if (!_pparam->_last_pathvector_nodesatellites) { + return; + } + size_t total_nodesatellites = _pparam->_last_pathvector_nodesatellites->getTotalNodeSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_nodesatellites) { + index = _index - total_nodesatellites; + is_mirror = true; + } + std::pair<size_t, size_t> index_data = _pparam->_last_pathvector_nodesatellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + + Geom::Point s = snap_knot_position(p, state); + if (!valid_index(satelite_index, subsatelite_index)) { + return; + } + NodeSatellite nodesatellite = _pparam->_vector[satelite_index][subsatelite_index]; + Geom::PathVector pathv = _pparam->_last_pathvector_nodesatellites->getPathVector(); + if (nodesatellite.hidden || + (!pathv[satelite_index].closed() && + (subsatelite_index == 0 || // ignore first nodesatellites on open paths + count_path_nodes(pathv[satelite_index]) - 1 == subsatelite_index))) // ignore last nodesatellite in open paths + // with fillet chamfer effect + { + return; + } + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return; + } + Geom::Curve const &curve_in = pathv[satelite_index][previous_index]; + double mirror_time = Geom::nearest_time(s, curve_in); + Geom::Point mirror = curve_in.pointAt(mirror_time); + double normal_time = Geom::nearest_time(s, pathv[satelite_index][subsatelite_index]); + Geom::Point normal = pathv[satelite_index][subsatelite_index].pointAt(normal_time); + double distance_mirror = Geom::distance(mirror,s); + double distance_normal = Geom::distance(normal,s); + if (Geom::are_near(s, pathv[satelite_index][subsatelite_index].initialPoint(), 1.5 / _pparam->_current_zoom)) { + nodesatellite.amount = 0; + } else if (distance_mirror < distance_normal) { + double time_start = 0; + NodeSatellites nodesatellites = _pparam->_last_pathvector_nodesatellites->getNodeSatellites(); + time_start = nodesatellites[satelite_index][previous_index].time(curve_in); + if (time_start > mirror_time) { + mirror_time = time_start; + } + double size = arcLengthAt(mirror_time, curve_in); + double amount = curve_in.length() - size; + if (nodesatellite.is_time) { + amount = timeAtArcLength(amount, pathv[satelite_index][subsatelite_index]); + } + nodesatellite.amount = amount; + } else { + nodesatellite.setPosition(s, pathv[satelite_index][subsatelite_index]); + } + Inkscape::LivePathEffect::LPEFilletChamfer *filletchamfer = dynamic_cast<Inkscape::LivePathEffect::LPEFilletChamfer *>(_pparam->param_effect); + filletchamfer->helperpath = true; + _pparam->updateAmmount(nodesatellite.amount); + _pparam->_vector[satelite_index][subsatelite_index] = nodesatellite; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +void +FilletChamferKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + Inkscape::LivePathEffect::LPEFilletChamfer *filletchamfer = dynamic_cast<Inkscape::LivePathEffect::LPEFilletChamfer *>(_pparam->param_effect); + if (filletchamfer) { + filletchamfer->refresh_widgets = true; + filletchamfer->helperpath = false; + filletchamfer->writeParamsToSVG(); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + } +} + +Geom::Point FilletChamferKnotHolderEntity::knot_get() const +{ + if (!_pparam->_last_pathvector_nodesatellites || _pparam->_global_knot_hide) { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + Geom::Point tmp_point; + size_t total_nodesatellites = _pparam->_last_pathvector_nodesatellites->getTotalNodeSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_nodesatellites) { + index = _index - total_nodesatellites; + is_mirror = true; + } + std::pair<size_t, size_t> index_data = _pparam->_last_pathvector_nodesatellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + if (!valid_index(satelite_index, subsatelite_index)) { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + NodeSatellite nodesatellite = _pparam->_vector[satelite_index][subsatelite_index]; + Geom::PathVector pathv = _pparam->_last_pathvector_nodesatellites->getPathVector(); + if (nodesatellite.hidden || + (!pathv[satelite_index].closed() && + (subsatelite_index == 0 || subsatelite_index == count_path_nodes(pathv[satelite_index]) - + 1))) // ignore first and last nodesatellites on open paths + { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + this->knot->show(); + if (is_mirror) { + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + Geom::Curve const &curve_in = pathv[satelite_index][previous_index]; + double s = nodesatellite.arcDistance(pathv[satelite_index][subsatelite_index]); + double t = nodesatellite.time(s, true, curve_in); + if (t > 1) { + t = 1; + } + if (t < 0) { + t = 0; + } + double time_start = 0; + time_start = _pparam->_last_pathvector_nodesatellites->getNodeSatellites()[satelite_index][previous_index].time( + curve_in); + if (time_start > t) { + t = time_start; + } + tmp_point = (curve_in).pointAt(t); + } else { + tmp_point = nodesatellite.getPosition(pathv[satelite_index][subsatelite_index]); + } + Geom::Point const canvas_point = tmp_point; + return canvas_point; +} + +void FilletChamferKnotHolderEntity::knot_click(guint state) +{ + if (!_pparam->_last_pathvector_nodesatellites) { + return; + } + size_t total_nodesatellites = _pparam->_last_pathvector_nodesatellites->getTotalNodeSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_nodesatellites) { + index = _index - total_nodesatellites; + is_mirror = true; + } + std::pair<size_t, size_t> index_data = _pparam->_last_pathvector_nodesatellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + if (!valid_index(satelite_index, subsatelite_index)) { + return; + } + Geom::PathVector pathv = _pparam->_last_pathvector_nodesatellites->getPathVector(); + if (!pathv[satelite_index].closed() && + (subsatelite_index == 0 || + count_path_nodes(pathv[satelite_index]) - 1 == subsatelite_index)) // ignore last nodesatellite in open paths + // with fillet chamfer effect + { + return; + } + if (state & GDK_CONTROL_MASK) { + if (state & GDK_MOD1_MASK) { + _pparam->_vector[satelite_index][subsatelite_index].amount = 0.0; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + } else { + using namespace Geom; + NodeSatelliteType type = _pparam->_vector[satelite_index][subsatelite_index].nodesatellite_type; + switch (type) { + case FILLET: + type = INVERSE_FILLET; + break; + case INVERSE_FILLET: + type = CHAMFER; + break; + case CHAMFER: + type = INVERSE_CHAMFER; + break; + default: + type = FILLET; + break; + } + _pparam->_vector[satelite_index][subsatelite_index].nodesatellite_type = type; + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + const gchar *tip; + if (type == CHAMFER) { + tip = _("<b>Chamfer</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> resets"); + } else if (type == INVERSE_CHAMFER) { + tip = _("<b>Inverse Chamfer</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> resets"); + } else if (type == INVERSE_FILLET) { + tip = _("<b>Inverse Fillet</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> resets"); + } else { + tip = _("<b>Fillet</b>: <b>Ctrl+Click</b> toggles type, " + "<b>Shift+Click</b> open dialog, " + "<b>Ctrl+Alt+Click</b> resets"); + } + this->knot->tip = g_strdup(tip); + this->knot->show(); + } + } else if (state & GDK_SHIFT_MASK) { + double amount = _pparam->_vector[satelite_index][subsatelite_index].amount; + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return; + } + if (!_pparam->_use_distance && !_pparam->_vector[satelite_index][subsatelite_index].is_time) { + amount = _pparam->_vector[satelite_index][subsatelite_index].lenToRad( + amount, pathv[satelite_index][previous_index], pathv[satelite_index][subsatelite_index], + _pparam->_vector[satelite_index][previous_index]); + } + bool aprox = false; + Geom::D2<Geom::SBasis> d2_out = pathv[satelite_index][subsatelite_index].toSBasis(); + Geom::D2<Geom::SBasis> d2_in = pathv[satelite_index][previous_index].toSBasis(); + aprox = ((d2_in)[0].degreesOfFreedom() != 2 || + d2_out[0].degreesOfFreedom() != 2) && + !_pparam->_use_distance + ? true + : false; + Inkscape::UI::Dialogs::FilletChamferPropertiesDialog::showDialog( + this->desktop, amount, this, _pparam->_use_distance, aprox, + _pparam->_vector[satelite_index][subsatelite_index]); + } +} + +void FilletChamferKnotHolderEntity::knot_set_offset(NodeSatellite nodesatellite) +{ + if (!_pparam->_last_pathvector_nodesatellites) { + return; + } + size_t total_nodesatellites = _pparam->_last_pathvector_nodesatellites->getTotalNodeSatellites(); + bool is_mirror = false; + size_t index = _index; + if (_index >= total_nodesatellites) { + index = _index - total_nodesatellites; + is_mirror = true; + } + std::pair<size_t, size_t> index_data = _pparam->_last_pathvector_nodesatellites->getIndexData(index); + size_t satelite_index = index_data.first; + size_t subsatelite_index = index_data.second; + if (!valid_index(satelite_index, subsatelite_index)) { + return; + } + Geom::PathVector pathv = _pparam->_last_pathvector_nodesatellites->getPathVector(); + if (nodesatellite.hidden || + (!pathv[satelite_index].closed() && + (subsatelite_index == 0 || + count_path_nodes(pathv[satelite_index]) - 1 == subsatelite_index))) // ignore last nodesatellite in open paths + // with fillet chamfer effect + { + return; + } + double amount = nodesatellite.amount; + double max_amount = amount; + if (!_pparam->_use_distance && !nodesatellite.is_time) { + gint previous_index = subsatelite_index - 1; + if (subsatelite_index == 0 && pathv[satelite_index].closed()) { + previous_index = count_path_nodes(pathv[satelite_index]) - 1; + } + if ( previous_index < 0 ) { + return; + } + amount = _pparam->_vector[satelite_index][subsatelite_index].radToLen( + amount, pathv[satelite_index][previous_index], pathv[satelite_index][subsatelite_index]); + if (max_amount > 0 && amount == 0) { + amount = _pparam->_vector[satelite_index][subsatelite_index].amount; + } + } + nodesatellite.amount = amount; + _pparam->_vector[satelite_index][subsatelite_index] = nodesatellite; + this->parent_holder->knot_ungrabbed_handler(this->knot, 0); + SPLPEItem *splpeitem = dynamic_cast<SPLPEItem *>(item); + if (splpeitem) { + sp_lpe_item_update_patheffect(splpeitem, false, false); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/nodesatellitesarray.h b/src/live_effects/parameter/nodesatellitesarray.h new file mode 100644 index 0000000..2075e43 --- /dev/null +++ b/src/live_effects/parameter/nodesatellitesarray.h @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_NODESATELLITES_ARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_NODESATELLITES_ARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * Copyright (C) Jabiertxo Arraiza Cenoz <jabier.arraiza@marker.es> + * Special thanks to Johan Engelen for the base of the effect -powerstroke- + * Also to ScislaC for pointing me to the idea + * Also su_v for his constructive feedback and time + * To Nathan Hurst for his review and help on refactor + * and finally to Liam P. White for his big help on coding, + * that saved me a lot of hours + * + * + * This parameter acts as a bridge from pathVectorNodeSatellites class to serialize it as a LPE + * parameter + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include "helper/geom-pathvector_nodesatellites.h" +#include "live_effects/effect-enum.h" +#include "live_effects/parameter/array.h" +#include "ui/knot/knot-holder-entity.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class FilletChamferKnotHolderEntity; + +class NodeSatelliteArrayParam : public ArrayParam<std::vector<NodeSatellite>> +{ +public: + NodeSatelliteArrayParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect); + + Gtk::Widget *param_newWidget() override + { + return nullptr; + } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + virtual void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item, bool mirror); + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + virtual void updateCanvasIndicators(); + virtual void updateCanvasIndicators(bool mirror); + bool providesKnotHolderEntities() const override + { + return true; + } + void param_transform_multiply(Geom::Affine const &postmul, bool /*set*/) override; + void setUseDistance(bool use_knot_distance); + void setCurrentZoom(double current_zoom); + void setGlobalKnotHide(bool global_knot_hide); + void setEffectType(EffectType et); + void reloadKnots(); + void updateAmmount(double amount); + void setPathVectorNodeSatellites(PathVectorNodeSatellites *pathVectorNodeSatellites, bool write = true); + + void set_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color); + + + friend class FilletChamferKnotHolderEntity; + friend class LPEFilletChamfer; + ParamType paramType() const override { return ParamType::NODE_SATELLITE_ARRAY; }; +protected: + KnotHolder *_knoth; + +private: + NodeSatelliteArrayParam(const NodeSatelliteArrayParam &) = delete; + NodeSatelliteArrayParam &operator=(const NodeSatelliteArrayParam &) = delete; + + Inkscape::CanvasItemCtrlShape _knot_shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND; + Inkscape::CanvasItemCtrlMode _knot_mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR; + guint32 _knot_color = 0xaaff8800; + Geom::PathVector _hp; + bool _use_distance = false; + bool _global_knot_hide = false; + double _current_zoom = 0; + EffectType _effectType = FILLET_CHAMFER; + PathVectorNodeSatellites *_last_pathvector_nodesatellites = nullptr; +}; + +class FilletChamferKnotHolderEntity : public KnotHolderEntity { +public: + FilletChamferKnotHolderEntity(NodeSatelliteArrayParam *p, size_t index); + ~FilletChamferKnotHolderEntity() override + { + _pparam->_knoth = nullptr; + } + void knot_set(Geom::Point const &p, Geom::Point const &origin, + guint state) override; + Geom::Point knot_get() const override; + void knot_click(guint state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_set_offset(NodeSatellite); + /** Checks whether the index falls within the size of the parameter's vector + */ + bool valid_index(size_t index, size_t subindex) const + { + return (_pparam->_vector.size() > index && _pparam->_vector[index].size() > subindex); + }; + +private: + NodeSatelliteArrayParam *_pparam; + size_t _index; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/originalpath.cpp b/src/live_effects/parameter/originalpath.cpp new file mode 100644 index 0000000..c4d582d --- /dev/null +++ b/src/live_effects/parameter/originalpath.cpp @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2012 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/box.h> +#include "live_effects/parameter/originalpath.h" + +#include <glibmm/i18n.h> +#include <gtkmm/button.h> +#include <gtkmm/label.h> + +#include "display/curve.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" + +#include "object/uri.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" + +#include "inkscape.h" +#include "desktop.h" +#include "selection.h" +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +OriginalPathParam::OriginalPathParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect) + : PathParam(label, tip, key, wr, effect, "") +{ + oncanvas_editable = false; + _from_original_d = false; +} + +OriginalPathParam::~OriginalPathParam() += default; + +Gtk::Widget * +OriginalPathParam::param_newWidget() +{ + Gtk::Box *_widget = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + + { // Label + Gtk::Label *pLabel = Gtk::manage(new Gtk::Label(param_label)); + _widget->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + } + + { // Paste path to link button + Gtk::Image *pIcon = Gtk::manage(new Gtk::Image()); + pIcon->set_from_icon_name("edit-clone", Gtk::ICON_SIZE_BUTTON); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathParam::on_link_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to path in clipboard")); + } + + { // Select original button + Gtk::Image *pIcon = Gtk::manage(new Gtk::Image()); + pIcon->set_from_icon_name("edit-select-original", Gtk::ICON_SIZE_BUTTON); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalPathParam::on_select_original_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Select original")); + } + + _widget->show_all_children(); + + return dynamic_cast<Gtk::Widget *> (_widget); +} + +void +OriginalPathParam::on_select_original_button_click() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPItem *original = ref.getObject(); + if (desktop == nullptr || original == nullptr) { + return; + } + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->set(original); + param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/originalpath.h b/src/live_effects/parameter/originalpath.h new file mode 100644 index 0000000..200d1cf --- /dev/null +++ b/src/live_effects/parameter/originalpath.h @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_PATH_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_PATH_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2012 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/path.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class OriginalPathParam: public PathParam { +public: + OriginalPathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect); + ~OriginalPathParam() override; + bool linksToPath() const { return (href != nullptr); } + SPItem * getObject() const { return ref.getObject(); } + + Gtk::Widget * param_newWidget() override; + /** Disable the canvas indicators of parent class by overriding this method */ + void param_editOncanvas(SPItem * /*item*/, SPDesktop * /*dt*/) override {}; + /** Disable the canvas indicators of parent class by overriding this method */ + void addCanvasIndicators(SPLPEItem const* /*lpeitem*/, std::vector<Geom::PathVector> & /*hp_vec*/) override {}; + ParamType paramType() const override { return ParamType::ORIGINAL_PATH; }; +protected: + void on_select_original_button_click(); + +private: + OriginalPathParam(const OriginalPathParam&) = delete; + OriginalPathParam& operator=(const OriginalPathParam&) = delete; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/originalsatellite.cpp b/src/live_effects/parameter/originalsatellite.cpp new file mode 100644 index 0000000..5c98c52 --- /dev/null +++ b/src/live_effects/parameter/originalsatellite.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2012 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/originalsatellite.h" + +#include <glibmm/i18n.h> +#include <gtkmm/box.h> +#include <gtkmm/button.h> +#include <gtkmm/label.h> + +#include "desktop.h" +#include "display/curve.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/parameter/satellite-reference.h" +#include "object/uri.h" +#include "selection.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +OriginalSatelliteParam::OriginalSatelliteParam(const Glib::ustring &label, const Glib::ustring &tip, + const Glib::ustring &key, Inkscape::UI::Widget::Registry *wr, + Effect *effect) + : SatelliteParam(label, tip, key, wr, effect) +{ +} + +OriginalSatelliteParam::~OriginalSatelliteParam() = default; + +Gtk::Widget *OriginalSatelliteParam::param_newWidget() +{ + Gtk::Box *_widget = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + + { // Label + Gtk::Label *pLabel = Gtk::manage(new Gtk::Label(param_label)); + _widget->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + } + + { // Paste item to link button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-paste", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &OriginalSatelliteParam::on_link_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to item")); + } + + { // Select original button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-select-original", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect( + sigc::mem_fun(*this, &OriginalSatelliteParam::on_select_original_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Select original")); + } + + _widget->show_all_children(); + + return dynamic_cast<Gtk::Widget *> (_widget); +} + +void OriginalSatelliteParam::on_select_original_button_click() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + SPItem *original = dynamic_cast<SPItem *>(lperef->getObject()); + if (desktop == nullptr || original == nullptr) { + return; + } + Inkscape::Selection *selection = desktop->getSelection(); + selection->clear(); + selection->set(original); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/originalsatellite.h b/src/live_effects/parameter/originalsatellite.h new file mode 100644 index 0000000..683f66d --- /dev/null +++ b/src/live_effects/parameter/originalsatellite.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_ITEM_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINAL_ITEM_H + +/* + * Inkscape::LiveItemEffectParameters + * + * Copyright (C) Johan Engelen 2012 <j.b.c.engelen@alumnus.utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/satellite.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class OriginalSatelliteParam : public SatelliteParam +{ +public: + OriginalSatelliteParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect); + ~OriginalSatelliteParam() override; + Gtk::Widget * param_newWidget() override; + ParamType paramType() const override { return ParamType::ORIGINAL_SATELLITE; }; +protected: + void on_select_original_button_click(); + +private: + OriginalSatelliteParam(const OriginalSatelliteParam &) = delete; + OriginalSatelliteParam &operator=(const OriginalSatelliteParam &) = delete; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/parameter.cpp b/src/live_effects/parameter/parameter.cpp new file mode 100644 index 0000000..ec40e5e --- /dev/null +++ b/src/live_effects/parameter/parameter.cpp @@ -0,0 +1,389 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "parameter.h" + +#include <glibmm/i18n.h> +#include <utility> + +#include "display/control/canvas-item-bpath.h" +#include "display/curve.h" +#include "live_effects/effect.h" +#include "live_effects/effect-enum.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "xml/repr.h" + +#define noLPEREALPARAM_DEBUG + +namespace Inkscape { + +namespace LivePathEffect { + + +Parameter::Parameter(Glib::ustring label, Glib::ustring tip, Glib::ustring key, Inkscape::UI::Widget::Registry *wr, + Effect *effect) + : param_key(std::move(key)) + , param_wr(wr) + , param_label(std::move(label)) + , oncanvas_editable(false) + , widget_is_visible(true) + , widget_is_enabled(true) + , param_tooltip(std::move(tip)) + , param_effect(effect) +{ +} + +Parameter::~Parameter() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop && ownerlocator) { + desktop->remove_temporary_canvasitem(ownerlocator); + } + if (selection_changed_connection) { + selection_changed_connection->disconnect(); + delete selection_changed_connection; + selection_changed_connection = nullptr; + } +} + +void Parameter::param_write_to_repr(const char *svgd) +{ + param_effect->getRepr()->setAttribute(param_key, svgd); +} + +void Parameter::write_to_SVG() +{ + param_write_to_repr(param_getSVGValue().c_str()); +} + +EffectType Parameter::effectType() const +{ + if (param_effect) { + return param_effect->effectType(); + } + return INVALID_LPE; +}; + +ParamType Parameter::paramType() const +{ + return INVALID_PARAM; +}; + +void +sp_add_class(SPObject *item, Glib::ustring classglib) { + gchar const *classlpe = item->getAttribute("class"); + if (classlpe) { + classglib = classlpe; + if (classglib.find("UnoptimicedTransforms") == Glib::ustring::npos) { + classglib += " UnoptimicedTransforms"; + item->setAttribute("class",classglib.c_str()); + } + } else { + item->setAttribute("class","UnoptimicedTransforms"); + } +} + +/* + * sometimes for example on ungrouping or loading documents we need to relay in stored value instead the volatile + * version in the parameter + */ +void Parameter::read_from_SVG() +{ + const gchar *val = param_effect->getRepr()->attribute(param_key.c_str()); + if (val) { + param_readSVGValue(val); + } +} + +void Parameter::param_higlight(bool highlight, bool select) +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + std::vector<SPLPEItem *> lpeitems = param_effect->getCurrrentLPEItems(); + if (lpeitems.size()) { + sp_add_class(lpeitems[0], "UnoptimicedTransforms"); + } + if (!highlight && ownerlocator) { + desktop->remove_temporary_canvasitem(ownerlocator); + ownerlocator = nullptr; + } + if (highlight) { + if (lpeitems.size() == 1 && param_effect->is_visible) { + if (select && !lpeitems[0]->isHidden()) { + desktop->selection->clear(); + desktop->selection->add(lpeitems[0]); + return; + } + auto c = std::make_unique<SPCurve>(); + std::vector<Geom::PathVector> cs; // = param_effect->getCanvasIndicators(lpeitems[0]); + Geom::OptRect bbox = lpeitems[0]->documentVisualBounds(); + + if (param_effect->helperLineSatellites) { + std::vector<SPObject *> satellites = param_get_satellites(); + for (auto iter : satellites) { + SPItem *satelliteitem = dynamic_cast<SPItem *>(iter); + if (satelliteitem) { + bbox.unionWith(satelliteitem->documentVisualBounds()); + } + } + } + Geom::PathVector out; + if (bbox) { + Geom::Path p = Geom::Path(*bbox); + out.push_back(p); + } + cs.push_back(out); + for (auto &p2 : cs) { + p2 *= desktop->dt2doc(); + c->append(p2); + } + if (!c->is_empty()) { + desktop->remove_temporary_canvasitem(ownerlocator); + auto tmpitem = new Inkscape::CanvasItemBpath(desktop->getCanvasTemp(), c.get(), true); + tmpitem->set_stroke(0x0000ff9a); + tmpitem->set_fill(0x0, SP_WIND_RULE_NONZERO); // No fill + ownerlocator = desktop->add_temporary_canvasitem(tmpitem, 0); + } + } + } + } +} + +void Parameter::change_selection(Inkscape::Selection *selection) +{ + update_satellites(false); +} + +void Parameter::connect_selection_changed() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + Inkscape::Selection *selection = desktop->selection; + if (selection) { + std::vector<SPObject *> satellites = param_get_satellites(); + if (!selection_changed_connection) { + selection_changed_connection = new sigc::connection( + selection->connectChanged(sigc::mem_fun(*this, &Parameter::change_selection))); + } + } + } +} + +void Parameter::update_satellites(bool updatelpe) +{ + if (paramType() == ParamType::SATELLITE || paramType() == ParamType::SATELLITE_ARRAY || paramType() == ParamType::PATH || + paramType() == ParamType::PATH_ARRAY || paramType() == ParamType::ORIGINAL_PATH || paramType() == ParamType::ORIGINAL_SATELLITE) { + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + std::vector<SPLPEItem *> lpeitems = param_effect->getCurrrentLPEItems(); + if (lpeitems.size() == 1){ + if (desktop) { + DocumentUndo::ScopedInsensitive _no_undo(desktop->getDocument()); + param_higlight(false, false); + Inkscape::Selection *selection = desktop->selection; + if (selection) { + std::vector<SPObject *> satellites = param_get_satellites(); + connect_selection_changed(); + if (selection->singleItem()) { + if (param_effect->isOnClipboard()) { + return; + } + // we always start hiding helper path + for (auto iter : satellites) { + sp_add_class(iter, "UnoptimicedTransforms"); + // if selection is current ref we highlight original sp_lpe_item to + // give visual feedback to the user to know what's the LPE item that generated the selection + if (iter && selection->includes(iter, true)) { + const gchar *classtoparentchar = iter->getAttribute("class"); + if (classtoparentchar) { + Glib::ustring classtoparent = classtoparentchar; + if (classtoparent.find("lpeselectparent ") != Glib::ustring::npos) { + param_higlight(true, true); + } else { + param_higlight(true, false); + } + } else { + param_higlight(true, false); + } + break; + } + } + } + } + } + if (updatelpe && param_effect->is_visible) { + sp_lpe_item_update_patheffect(lpeitems[0], false, false); + } + } + } +} + +/* + * we get satellites of parameter, virtual function overided by some parameter with linked satellites + */ +std::vector<SPObject *> Parameter::param_get_satellites() +{ + std::vector<SPObject *> objs; + return objs; +}; + +/*########################################### + * REAL PARAM + */ +ScalarParam::ScalarParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, gdouble default_value) + : Parameter(label, tip, key, wr, effect) + , value(default_value) + , min(-SCALARPARAM_G_MAXDOUBLE) + , max(SCALARPARAM_G_MAXDOUBLE) + , integer(false) + , defvalue(default_value) + , digits(2) + , inc_step(0.1) + , inc_page(1) + , add_slider(false) + , _set_undo(true) +{ +} + +ScalarParam::~ScalarParam() = default; + +bool ScalarParam::param_readSVGValue(const gchar *strvalue) +{ + double newval; + unsigned int success = sp_svg_number_read_d(strvalue, &newval); + if (success == 1) { + param_set_value(newval); + return true; + } + return false; +} + +Glib::ustring ScalarParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << value; + return os.str(); +} + +Glib::ustring ScalarParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +void ScalarParam::param_set_default() { param_set_value(defvalue); } + +void ScalarParam::param_update_default(gdouble default_value) { defvalue = default_value; } + +void ScalarParam::param_update_default(const gchar *default_value) +{ + double newval; + unsigned int success = sp_svg_number_read_d(default_value, &newval); + if (success == 1) { + param_update_default(newval); + } +} + +void ScalarParam::param_transform_multiply(Geom::Affine const &postmul, bool set) +{ + // Check if proportional stroke-width scaling is on + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke || set) { + param_set_value(value * postmul.descrim()); + write_to_SVG(); + } +} + +void ScalarParam::param_set_value(gdouble val) +{ + value = val; + if (integer) + value = round(value); + if (value > max) + value = max; + if (value < min) + value = min; +} + +void ScalarParam::param_set_range(gdouble min, gdouble max) +{ + // if you look at client code, you'll see that many effects + // has a tendency to set an upper range of Geom::infinity(). + // Once again, in gtk2, this is not a problem. But in gtk3, + // widgets get allocated the amount of size they ask for, + // leading to excessively long widgets. + if (min >= -SCALARPARAM_G_MAXDOUBLE) { + this->min = min; + } else { + this->min = -SCALARPARAM_G_MAXDOUBLE; + } + if (max <= SCALARPARAM_G_MAXDOUBLE) { + this->max = max; + } else { + this->max = SCALARPARAM_G_MAXDOUBLE; + } + param_set_value(value); // reset value to see whether it is in ranges +} + +void ScalarParam::param_make_integer(bool yes) +{ + integer = yes; + digits = 0; + inc_step = 1; + inc_page = 10; +} + +void ScalarParam::param_set_undo(bool set_undo) { _set_undo = set_undo; } + +Gtk::Widget *ScalarParam::param_newWidget() +{ + if (widget_is_visible) { + Inkscape::UI::Widget::RegisteredScalar *rsu = Gtk::manage(new Inkscape::UI::Widget::RegisteredScalar( + param_label, param_tooltip, param_key, *param_wr, param_effect->getRepr(), param_effect->getSPDoc())); + + rsu->setValue(value); + rsu->setDigits(digits); + rsu->setIncrements(inc_step, inc_page); + rsu->setRange(min, max); + rsu->setProgrammatically = false; + if (add_slider) { + rsu->addSlider(); + } + if (_set_undo) { + rsu->set_undo_parameters(_("Change scalar parameter"), INKSCAPE_ICON("dialog-path-effects")); + } + return dynamic_cast<Gtk::Widget *>(rsu); + } else { + return nullptr; + } +} + +void ScalarParam::param_set_digits(unsigned digits) { this->digits = digits; } + +void ScalarParam::param_set_increments(double step, double page) +{ + inc_step = step; + inc_page = page; +} + +} /* namespace LivePathEffect */ +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/parameter.h b/src/live_effects/parameter/parameter.h new file mode 100644 index 0000000..d9a901b --- /dev/null +++ b/src/live_effects/parameter/parameter.h @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <2geom/pathvector.h> +#include <glibmm/ustring.h> + +#include "live_effects/lpeobject.h" +#include "ui/widget/registered-widget.h" + +// In gtk2, this wasn't an issue; we could toss around +// G_MAXDOUBLE and not worry about size allocations. But +// in gtk3, it is an issue: it allocates widget size for the maxmium +// value you pass to it, leading to some insane lengths. +// If you need this to be more, please be conservative about it. +const double SCALARPARAM_G_MAXDOUBLE = + 10000000000.0; // TODO fixme: using an arbitrary large number as a magic value seems fragile. + +class KnotHolder; +class SPLPEItem; +class SPDesktop; +class SPItem; + +namespace Gtk { +class Widget; +} + +namespace Inkscape { +namespace Display { +class TemporaryItem; +} +namespace NodePath { +class Path; +} + +namespace UI { +namespace Widget { +class Registry; +} +} // namespace UI + +namespace LivePathEffect { +class Effect; + +class Parameter { + public: + Parameter(Glib::ustring label, Glib::ustring tip, Glib::ustring key, Inkscape::UI::Widget::Registry *wr, + Effect *effect); + virtual ~Parameter(); + + Parameter(const Parameter &) = delete; + Parameter &operator=(const Parameter &) = delete; + + virtual bool param_readSVGValue(const gchar *strvalue) = 0; // returns true if new value is valid / accepted. + virtual Glib::ustring param_getSVGValue() const = 0; + virtual Glib::ustring param_getDefaultSVGValue() const = 0; + virtual void param_widget_is_visible(bool is_visible) { widget_is_visible = is_visible; } + virtual void param_widget_is_enabled(bool is_enabled) { widget_is_enabled = is_enabled; } + void write_to_SVG(); + void read_from_SVG(); + + virtual void param_set_default() = 0; + virtual void param_update_default(const gchar *default_value) = 0; + // This creates a new widget (newed with Gtk::manage(new ...);) + virtual Gtk::Widget *param_newWidget() = 0; + virtual Glib::ustring *param_getTooltip() { return ¶m_tooltip; }; + + // overload these for your particular parameter to make it provide knotholder handles or canvas helperpaths + virtual bool providesKnotHolderEntities() const { return false; } + virtual void addKnotHolderEntities(KnotHolder * /*knotholder*/, SPItem * /*item*/){}; + virtual void addCanvasIndicators(SPLPEItem const * /*lpeitem*/, std::vector<Geom::PathVector> & /*hp_vec*/){}; + + virtual void param_editOncanvas(SPItem * /*item*/, SPDesktop * /*dt*/){}; + virtual void param_setup_nodepath(Inkscape::NodePath::Path * /*np*/){}; + + virtual void param_transform_multiply(Geom::Affine const & /*postmul*/, bool set){}; + virtual std::vector<SPObject *> param_get_satellites(); + void param_higlight(bool highlight, bool select); + sigc::connection *selection_changed_connection = nullptr; + void change_selection(Inkscape::Selection *selection); + void update_satellites(bool updatelpe = false); + Glib::ustring param_key; + Glib::ustring param_tooltip; + Inkscape::UI::Widget::Registry *param_wr; + Glib::ustring param_label; + EffectType effectType() const; + virtual ParamType paramType() const; + bool oncanvas_editable; + bool widget_is_visible; + bool widget_is_enabled; + void connect_selection_changed(); + + protected: + Inkscape::Display::TemporaryItem *ownerlocator = nullptr; + Effect *param_effect; + void param_write_to_repr(const char *svgd); +}; + + +class ScalarParam : public Parameter { + public: + ScalarParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, gdouble default_value = 1.0); + ~ScalarParam() override; + ScalarParam(const ScalarParam &) = delete; + ScalarParam &operator=(const ScalarParam &) = delete; + + bool param_readSVGValue(const gchar *strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + + void param_set_default() override; + void param_update_default(gdouble default_value); + void param_update_default(const gchar *default_value) override; + void param_set_value(gdouble val); + void param_make_integer(bool yes = true); + void param_set_range(gdouble min, gdouble max); + void param_set_digits(unsigned digits); + void param_set_increments(double step, double page); + void addSlider(bool add_slider_widget) { add_slider = add_slider_widget; }; + double param_get_max() { return max; }; + double param_get_min() { return min; }; + void param_set_undo(bool set_undo); + Gtk::Widget *param_newWidget() override; + + inline operator gdouble() const { return value; }; + + protected: + gdouble value; + gdouble min; + gdouble max; + bool integer; + gdouble defvalue; + unsigned digits; + double inc_step; + double inc_page; + bool add_slider; + bool _set_undo; +}; + +} // namespace LivePathEffect + +} // namespace Inkscape + +#endif + +/* + 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/live_effects/parameter/path-reference.cpp b/src/live_effects/parameter/path-reference.cpp new file mode 100644 index 0000000..c3ce3d5 --- /dev/null +++ b/src/live_effects/parameter/path-reference.cpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * The reference corresponding to href of LPE Path parameter. + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/path-reference.h" + +#include "object/sp-shape.h" +#include "object/sp-text.h" + +namespace Inkscape { +namespace LivePathEffect { + +bool PathReference::_acceptObject(SPObject * const obj) const +{ + if (SP_IS_SHAPE(obj) || SP_IS_TEXT(obj)) { + /* Refuse references to lpeobject */ + if (obj == getOwner()) { + return false; + } + // TODO: check whether the referred path has this LPE applied, if so: deny deny deny! + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + +} // namespace LivePathEffect +} // namespace Inkscape + +/* + 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 : diff --git a/src/live_effects/parameter/path-reference.h b/src/live_effects/parameter/path-reference.h new file mode 100644 index 0000000..0b33194 --- /dev/null +++ b/src/live_effects/parameter/path-reference.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_LPE_PATH_REFERENCE_H +#define SEEN_LPE_PATH_REFERENCE_H + +/* + * Copyright (C) 2008-2012 Authors + * Authors: Johan Engelen + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "object/uri-references.h" + +class SPItem; +namespace Inkscape { +namespace XML { class Node; } + +namespace LivePathEffect { + +/** + * The reference corresponding to href of LPE PathParam. + */ +class PathReference : public Inkscape::URIReference { +public: + PathReference(SPObject *owner) : URIReference(owner) {} + + SPItem *getObject() const { + return (SPItem *)URIReference::getObject(); + } + +protected: + bool _acceptObject(SPObject * const obj) const override; + +private: + PathReference(const PathReference&) = delete; + PathReference& operator=(const PathReference&) = delete; +}; + +} // namespace LivePathEffect + +} // namespace Inkscape + + + +#endif /* !SEEN_LPE_PATH_REFERENCE_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 : diff --git a/src/live_effects/parameter/path.cpp b/src/live_effects/parameter/path.cpp new file mode 100644 index 0000000..7dc5895 --- /dev/null +++ b/src/live_effects/parameter/path.cpp @@ -0,0 +1,655 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "path.h" + +#include <glibmm/i18n.h> +#include <glibmm/utility.h> + +#include <gtkmm/button.h> +#include <gtkmm/label.h> + + +#include <2geom/svg-path-parser.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/pathvector.h> +#include <2geom/d2.h> + +#include "bad-uri-exception.h" + +#include "desktop.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "message-stack.h" +#include "selection.h" +#include "selection-chemistry.h" + +#include "actions/actions-tools.h" +#include "display/curve.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "object/uri.h" +#include "object/sp-shape.h" +#include "object/sp-item.h" +#include "object/sp-text.h" +#include "svg/svg.h" + +#include "ui/clipboard.h" // clipboard support +#include "ui/icon-loader.h" +#include "ui/icon-names.h" +#include "ui/shape-editor.h" // needed for on-canvas editing: +#include "ui/tools/node-tool.h" +#include "ui/tool/multi-path-manipulator.h" +#include "ui/tool/shape-record.h" +#include "ui/widget/point.h" + +#include "xml/repr.h" + +namespace Inkscape { + +namespace LivePathEffect { + +PathParam::PathParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const gchar * default_value) + : Parameter(label, tip, key, wr, effect), + changed(true), + _pathvector(), + _pwd2(), + must_recalculate_pwd2(false), + href(nullptr), + ref( (SPObject*)effect->getLPEObj() ) +{ + defvalue = g_strdup(default_value); + param_readSVGValue(defvalue); + oncanvas_editable = true; + _from_original_d = false; + _edit_button = true; + _copy_button = true; + _paste_button = true; + _link_button = true; + ref_changed_connection = ref.changedSignal().connect(sigc::mem_fun(*this, &PathParam::ref_changed)); +} + +PathParam::~PathParam() +{ + unlink(); +//TODO: Removed to fix a bug https://bugs.launchpad.net/inkscape/+bug/1716926 +// Maybe we need to resurrect, not know when this code is added, but seems also not working now in a few test I do. +// in the future and do a deeper fix in multi-path-manipulator +// using namespace Inkscape::UI; +// SPDesktop *desktop = SP_ACTIVE_DESKTOP; +// if (desktop) { +// Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(desktop->event_context); +// if (nt) { +// SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); +// if (item) { +// std::set<ShapeRecord> shapes; +// ShapeRecord r; +// r.item = item; +// shapes.insert(r); +// nt->_multipath->setItems(shapes); +// } +// } +// } + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (desktop) { + if (dynamic_cast<Inkscape::UI::Tools::NodeTool* >(desktop->event_context)) { + // Why is this switching tools twice? Probably to reinitialize Node Tool. + set_active_tool(desktop, "Select"); + set_active_tool(desktop, "Node"); + } + } + g_free(defvalue); +} + +void PathParam::reload() { + setUpdating(false); + start_listening(getObject()); + connect_selection_changed(); + SPItem *item = nullptr; + if (( item = dynamic_cast<SPItem *>(getObject()) )) { + item->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG); + } +} + +Geom::Affine +PathParam::get_relative_affine() { + Geom::Affine affine = Geom::identity(); + SPItem *item = nullptr; + if (( item = dynamic_cast<SPItem *>(getObject()) )) { + std::vector<SPLPEItem *> lpeitems = param_effect->getCurrrentLPEItems(); + if (lpeitems.size() == 1) { + param_effect->sp_lpe_item = lpeitems[0]; + } + affine = item->getRelativeTransform(param_effect->sp_lpe_item); + } + return affine; +} + +Geom::PathVector const & +PathParam::get_pathvector() const +{ + return _pathvector; +} + +Geom::Piecewise<Geom::D2<Geom::SBasis> > const & +PathParam::get_pwd2() +{ + ensure_pwd2(); + return _pwd2; +} + +void +PathParam::param_set_default() +{ + param_readSVGValue(defvalue); +} + +void +PathParam::param_set_and_write_default() +{ + param_write_to_repr(defvalue); +} + +std::vector<SPObject *> PathParam::param_get_satellites() +{ + + std::vector<SPObject *> objs; + if (ref.isAttached()) { + // we reload connexions in case are lost for example item recreation on ungroup + if (!linked_transformed_connection) { + write_to_SVG(); + } + + SPObject * linked_obj = ref.getObject(); + if (linked_obj) { + objs.push_back(linked_obj); + } + } + return objs; +} + +bool +PathParam::param_readSVGValue(const gchar * strvalue) +{ + if (strvalue) { + _pathvector.clear(); + unlink(); + must_recalculate_pwd2 = true; + + + if (strvalue[0] == '#') { + bool write = false; + SPObject * old_ref = param_effect->getSPDoc()->getObjectByHref(strvalue); + Glib::ustring id_tmp; + if (old_ref) { + SPObject * successor = old_ref->_successor; + if (successor) { + id_tmp = successor->getId(); + id_tmp.insert(id_tmp.begin(), '#'); + write = true; + } + } + if (href) + g_free(href); + href = g_strdup(id_tmp.empty() ? strvalue : id_tmp.c_str()); + + // Now do the attaching, which emits the changed signal. + try { + ref.attach(Inkscape::URI(href)); + //lp:1299948 + SPItem* i = ref.getObject(); + if (i) { + linked_modified_callback(i, SP_OBJECT_MODIFIED_FLAG); + } // else: document still processing new events. Repr of the linked object not created yet. + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + ref.detach(); + _pathvector = sp_svg_read_pathv(defvalue); + } + if (write) { + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + } + } else { + _pathvector = sp_svg_read_pathv(strvalue); + } + + emit_changed(); + return true; + } + + return false; +} + +Glib::ustring +PathParam::param_getSVGValue() const +{ + if (href) { + return href; + } else { + return sp_svg_write_path(_pathvector); + } +} + +Glib::ustring +PathParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + +void +PathParam::set_buttons(bool edit_button, bool copy_button, bool paste_button, bool link_button) +{ + _edit_button = edit_button; + _copy_button = copy_button; + _paste_button = paste_button; + _link_button = link_button; +} + +Gtk::Widget * +PathParam::param_newWidget() +{ + Gtk::Box * _widget = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + + Gtk::Label* pLabel = Gtk::manage(new Gtk::Label(param_label)); + _widget->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + Gtk::Image * pIcon = nullptr; + Gtk::Button * pButton = nullptr; + if (_edit_button) { + pIcon = Gtk::manage(sp_get_icon_image("tool-node-editor", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_edit_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Edit on-canvas")); + } + + if (_copy_button) { + pIcon = Gtk::manage(sp_get_icon_image("edit-copy", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_copy_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Copy path")); + } + + if (_paste_button) { + pIcon = Gtk::manage(sp_get_icon_image("edit-paste", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_paste_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Paste path")); + } + if (_link_button) { + pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathParam::on_link_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to path in clipboard")); + } + + _widget->show_all_children(); + + return dynamic_cast<Gtk::Widget *> (_widget); +} + +void +PathParam::param_editOncanvas(SPItem *item, SPDesktop * dt) +{ + SPDocument *document = dt->getDocument(); + bool saved = DocumentUndo::getUndoSensitive(document); + DocumentUndo::setUndoSensitive(document, false); + using namespace Inkscape::UI; + + Inkscape::UI::Tools::NodeTool *nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(dt->event_context); + if (!nt) { + set_active_tool(dt, "Node"); + nt = dynamic_cast<Inkscape::UI::Tools::NodeTool*>(dt->event_context); + } + + std::set<ShapeRecord> shapes; + ShapeRecord r; + + r.role = SHAPE_ROLE_LPE_PARAM; + r.edit_transform = item->i2dt_affine(); // TODO is it right? + if (!href) { + r.object = dynamic_cast<SPObject *>(param_effect->getLPEObj()); + r.lpe_key = param_key; + Geom::PathVector stored_pv = _pathvector; + if (_pathvector.empty()) { + param_write_to_repr("M0,0 L1,0"); + } else { + param_write_to_repr(sp_svg_write_path(stored_pv).c_str()); + } + } else { + r.object = ref.getObject(); + } + shapes.insert(r); + nt->_multipath->setItems(shapes); + DocumentUndo::setUndoSensitive(document, saved); +} + +void +PathParam::param_setup_nodepath(Inkscape::NodePath::Path *) +{ + // TODO this method should not exist at all! +} + +void +PathParam::addCanvasIndicators(SPLPEItem const*/*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) +{ + hp_vec.push_back(_pathvector); +} + +/* + * Only applies transform when not referring to other path! + */ +void +PathParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + // only apply transform when not referring to other path + if (!href) { + set_new_value( _pathvector * postmul, true ); + } +} + +/* + * See comments for set_new_value(Geom::PathVector). + */ +void +PathParam::set_new_value (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & newpath, bool write_to_svg) +{ + unlink(); + _pathvector = Geom::path_from_piecewise(newpath, LPE_CONVERSION_TOLERANCE); + + if (write_to_svg) { + param_write_to_repr(sp_svg_write_path(_pathvector).c_str()); + + // After the whole "writing to svg avalanche of function calling": force value upon pwd2 and don't recalculate. + _pwd2 = newpath; + must_recalculate_pwd2 = false; + } else { + _pwd2 = newpath; + must_recalculate_pwd2 = false; + emit_changed(); + } +} + +/* + * This method sets new path data. + * If this PathParam refers to another path, this link is removed (and replaced with explicit path data). + * + * If write_to_svg = true : + * The new path data is written to SVG. In this case the signal_path_changed signal + * is not directly emitted in this method, because writing to SVG + * triggers the LPEObject to which this belongs to call Effect::setParameter which calls + * PathParam::readSVGValue, which finally emits the signal_path_changed signal. + * If write_to_svg = false : + * The new path data is not written to SVG. This method will emit the signal_path_changed signal. + */ +void +PathParam::set_new_value (Geom::PathVector const &newpath, bool write_to_svg) +{ + unlink(); + if (newpath.empty()) { + param_set_and_write_default(); + return; + } else { + _pathvector = newpath; + } + must_recalculate_pwd2 = true; + + if (write_to_svg) { + param_write_to_repr(sp_svg_write_path(_pathvector).c_str()); + } else { + emit_changed(); + } +} + +void +PathParam::ensure_pwd2() +{ + if (must_recalculate_pwd2) { + _pwd2.clear(); + for (const auto & i : _pathvector) { + _pwd2.concat( i.toPwSb() ); + } + + must_recalculate_pwd2 = false; + } +} + +void +PathParam::emit_changed() +{ + changed = true; + signal_path_changed.emit(); +} + +void +PathParam::start_listening(SPObject * to) +{ + if ( to == nullptr ) { + return; + } + linked_delete_connection = to->connectDelete(sigc::mem_fun(*this, &PathParam::linked_delete)); + linked_modified_connection = to->connectModified(sigc::mem_fun(*this, &PathParam::linked_modified)); + if (SP_IS_ITEM(to)) { + linked_transformed_connection = SP_ITEM(to)->connectTransformed(sigc::mem_fun(*this, &PathParam::linked_transformed)); + } + linked_modified(to, SP_OBJECT_MODIFIED_FLAG); // simulate linked_modified signal, so that path data is updated +} + +void +PathParam::quit_listening() +{ + linked_modified_connection.disconnect(); + linked_delete_connection.disconnect(); + linked_transformed_connection.disconnect(); +} + +void +PathParam::ref_changed(SPObject */*old_ref*/, SPObject *new_ref) +{ + quit_listening(); + if ( new_ref ) { + start_listening(new_ref); + } +} + +void PathParam::unlink() +{ + if (href) { + ref.detach(); + g_free(href); + href = nullptr; + } +} + +void +PathParam::linked_delete(SPObject */*deleted*/) +{ + quit_listening(); + unlink(); + set_new_value (_pathvector, true); +} + +void PathParam::linked_modified(SPObject *linked_obj, guint flags) +{ + if (!param_effect->is_load || ownerlocator || !SP_ACTIVE_DESKTOP) { + linked_modified_callback(linked_obj, flags); + } +} + +void PathParam::linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item) +{ + linked_transformed_callback(rel_transf, moved_item); +} + +void +PathParam::linked_modified_callback(SPObject *linked_obj, guint flags) +{ + if (!_updating && flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) + { + std::unique_ptr<SPCurve> curve; + if (auto shape = dynamic_cast<SPShape const *>(linked_obj)) { + if (_from_original_d) { + curve = SPCurve::copy(shape->curveForEdit()); + } else { + curve = SPCurve::copy(shape->curve()); + } + } + + SPText *text = dynamic_cast<SPText *>(linked_obj); + if (text) { + bool hidden = text->isHidden(); + if (hidden) { + if (_pathvector.empty()) { + text->setHidden(false); + curve = text->getNormalizedBpath(); + text->setHidden(true); + } else { + if (curve == nullptr) { + curve.reset(new SPCurve()); + } + curve->set_pathvector(_pathvector); + } + } else { + curve = text->getNormalizedBpath(); + } + } + + if (curve == nullptr) { + // curve invalid, set default value + _pathvector = sp_svg_read_pathv(defvalue); + } else { + _pathvector = curve->get_pathvector(); + } + + must_recalculate_pwd2 = true; + emit_changed(); + param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); + } +} + +void +PathParam::param_update_default(const gchar * default_value){ + defvalue = strdup(default_value); +} + +/* CALLBACK FUNCTIONS FOR THE BUTTONS */ +void +PathParam::on_edit_button_click() +{ + SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + if (item != nullptr) { + param_editOncanvas(item, SP_ACTIVE_DESKTOP); + } +} + +void +PathParam::paste_param_path(const char *svgd) +{ + // only recognize a non-null, non-empty string + if (svgd && *svgd) { + // remove possible link to path + unlink(); + SPItem * item = SP_ACTIVE_DESKTOP->getSelection()->singleItem(); + std::string svgd_new; + if (item != nullptr) { + Geom::PathVector path_clipboard = sp_svg_read_pathv(svgd); + path_clipboard *= item->i2doc_affine().inverse(); + svgd_new = sp_svg_write_path(path_clipboard); + svgd = svgd_new.c_str(); + } + + param_write_to_repr(svgd); + signal_path_pasted.emit(); + } +} + +void +PathParam::on_paste_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + Glib::ustring svgd = cm->getPathParameter(SP_ACTIVE_DESKTOP); + paste_param_path(svgd.data()); + DocumentUndo::done(param_effect->getSPDoc(), _("Paste path parameter"), INKSCAPE_ICON("dialog-path-effects")); +} + +void +PathParam::on_copy_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + cm->copyPathParameter(this); +} + +void +PathParam::linkitem(Glib::ustring pathid) +{ + if (pathid.empty()) { + return; + } + + // add '#' at start to make it an uri. + pathid.insert(pathid.begin(), '#'); + if ( href && strcmp(pathid.c_str(), href) == 0 ) { + // no change, do nothing + return; + } else { + // TODO: + // check if id really exists in document, or only in clipboard document: if only in clipboard then invalid + // check if linking to object to which LPE is applied (maybe delegated to PathReference + + param_write_to_repr(pathid.c_str()); + DocumentUndo::done(param_effect->getSPDoc(), _("Link path parameter to path"), INKSCAPE_ICON("dialog-path-effects")); + } +} + +void +PathParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + Glib::ustring pathid = cm->getShapeOrTextObjectId(SP_ACTIVE_DESKTOP); + + linkitem(pathid); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/path.h b/src/live_effects/parameter/path.h new file mode 100644 index 0000000..a4b5eea --- /dev/null +++ b/src/live_effects/parameter/path.h @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_PATH_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_PATH_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <2geom/path.h> + +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path-reference.h" +#include <cstddef> +#include <sigc++/sigc++.h> + +namespace Inkscape { + +namespace LivePathEffect { + +class PathParam : public Parameter { +public: + PathParam ( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar * default_value = "M0,0 L1,1"); + ~PathParam() override; + + Geom::PathVector const & get_pathvector() const; + void reload(); + Geom::Affine get_relative_affine(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > const & get_pwd2(); + + Gtk::Widget * param_newWidget() override; + std::vector<SPObject *> param_get_satellites() override; + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_set_default() override; + void param_update_default(const gchar * default_value) override; + void param_set_and_write_default(); + void set_new_value (Geom::PathVector const &newpath, bool write_to_svg); + void set_new_value (Geom::Piecewise<Geom::D2<Geom::SBasis> > const &newpath, bool write_to_svg); + void set_buttons(bool edit_button, bool copy_button, bool paste_button, bool link_button); + void setUpdating(bool updating) {_updating = updating;}; + void param_editOncanvas(SPItem * item, SPDesktop * dt) override; + void param_setup_nodepath(Inkscape::NodePath::Path *np) override; + void addCanvasIndicators(SPLPEItem const* lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + void setFromOriginalD(bool from_original_d){ _from_original_d = from_original_d; }; + + sigc::signal <void> signal_path_pasted; + sigc::signal <void> signal_path_changed; + bool changed; /* this gets set whenever the path is changed (this is set to true, and then the signal_path_changed signal is emitted). + * the user must set it back to false if she wants to use it sensibly */ + SPObject * getObject() const { if (ref.isAttached()) {return ref.getObject();} return nullptr;} + void paste_param_path(const char *svgd); + void on_paste_button_click(); + void linkitem(Glib::ustring pathid); + ParamType paramType() const override { return ParamType::PATH; }; + +protected: + Geom::PathVector _pathvector; // this is primary data storage, since it is closest to SVG. + + Geom::Piecewise<Geom::D2<Geom::SBasis> > _pwd2; // secondary, hence the bool must_recalculate_pwd2 + bool must_recalculate_pwd2; // set when _pathvector was updated, but _pwd2 not + void ensure_pwd2(); // ensures _pwd2 is up to date + + gchar * href; // contains link to other object, e.g. "#path2428", NULL if PathParam contains pathdata itself + PathReference ref; + friend class LPEFillBetweenStrokes; + friend class LPEPatternAlongPath; + friend class LPEBendPath; + friend class LPECurveStitch; + friend class LPEAttachPath; + friend class LPEEnvelope; + friend class LPEBoundingBox; + friend class LPEInterpolate; + friend class LPEVonKoch; + sigc::connection ref_changed_connection; + sigc::connection linked_delete_connection; + sigc::connection linked_modified_connection; + sigc::connection linked_transformed_connection; + void ref_changed(SPObject *old_ref, SPObject *new_ref); + void unlink(); + void start_listening(SPObject * to); + void quit_listening(); + void linked_delete(SPObject *deleted); + void linked_modified(SPObject *linked_obj, guint flags); + void linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item); + virtual void linked_modified_callback(SPObject *linked_obj, guint flags); + virtual void linked_transformed_callback(Geom::Affine const * rel_transf, SPItem * /*moved_item*/) {}; + + void on_edit_button_click(); + void on_copy_button_click(); + void on_link_button_click(); + + void emit_changed(); + + gchar * defvalue; + bool _from_original_d; +private: + bool _edit_button; + bool _copy_button; + bool _paste_button; + bool _link_button; + bool _updating = false; + PathParam(const PathParam&) = delete; + PathParam& operator=(const PathParam&) = delete; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/patharray.cpp b/src/live_effects/parameter/patharray.cpp new file mode 100644 index 0000000..e2e81dc --- /dev/null +++ b/src/live_effects/parameter/patharray.cpp @@ -0,0 +1,593 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/patharray.h" + +#include <2geom/coord.h> +#include <2geom/point.h> +#include <glibmm/i18n.h> +#include <gtkmm/icontheme.h> +#include <gtkmm/imagemenuitem.h> +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/separatormenuitem.h> +#include <gtkmm/widget.h> + +#include "display/curve.h" +#include "document-undo.h" +#include "document.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/lpe-bspline.h" +#include "live_effects/lpe-spiro.h" +#include "live_effects/lpeobject-reference.h" +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/patharray.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/uri.h" +#include "originalpath.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/clipboard.h" +#include "ui/icon-loader.h" +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class PathArrayParam::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + + ModelColumns() + { + add(_colObject); + add(_colLabel); + add(_colReverse); + add(_colVisible); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<PathAndDirectionAndVisible*> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<bool> _colReverse; + Gtk::TreeModelColumn<bool> _colVisible; +}; + +PathArrayParam::PathArrayParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect) + : Parameter(label, tip, key, wr, effect) + , _vector() +{ + _tree = nullptr; + _scroller = nullptr; + _model = nullptr; + // refresh widgets on load to allow to remove the + // memory leak calling initui here + param_effect->refresh_widgets = true; + oncanvas_editable = true; + _from_original_d = false; + _allow_only_bspline_spiro = false; +} + +PathArrayParam::~PathArrayParam() +{ + while (!_vector.empty()) { + PathAndDirectionAndVisible *w = _vector.back(); + unlink(w); + } + delete _model; +} + +void PathArrayParam::initui() +{ + SPDesktop * desktop = SP_ACTIVE_DESKTOP; + if (!desktop) { + return; + } + if (!_tree) { + _tree = manage(new Gtk::TreeView()); + _model = new ModelColumns(); + _store = Gtk::TreeStore::create(*_model); + _tree->set_model(_store); + + _tree->set_reorderable(true); + _tree->enable_model_drag_dest (Gdk::ACTION_MOVE); + + Gtk::CellRendererToggle *toggle_reverse = manage(new Gtk::CellRendererToggle()); + int reverseColNum = _tree->append_column(_("Reverse"), *toggle_reverse) - 1; + Gtk::TreeViewColumn* col_reverse = _tree->get_column(reverseColNum); + toggle_reverse->set_activatable(true); + toggle_reverse->signal_toggled().connect(sigc::mem_fun(*this, &PathArrayParam::on_reverse_toggled)); + col_reverse->add_attribute(toggle_reverse->property_active(), _model->_colReverse); + + + Gtk::CellRendererToggle *toggle_visible = manage(new Gtk::CellRendererToggle()); + int visibleColNum = _tree->append_column(_("Visible"), *toggle_visible) - 1; + Gtk::TreeViewColumn* col_visible = _tree->get_column(visibleColNum); + toggle_visible->set_activatable(true); + toggle_visible->signal_toggled().connect(sigc::mem_fun(*this, &PathArrayParam::on_visible_toggled)); + col_visible->add_attribute(toggle_visible->property_active(), _model->_colVisible); + + Gtk::CellRendererText *text_renderer = manage(new Gtk::CellRendererText()); + int nameColNum = _tree->append_column(_("Name"), *text_renderer) - 1; + Gtk::TreeView::Column *name_column = _tree->get_column(nameColNum); + name_column->add_attribute(text_renderer->property_text(), _model->_colLabel); + + _tree->set_expander_column(*_tree->get_column(nameColNum) ); + _tree->set_search_column(_model->_colLabel); + _scroller = Gtk::manage(new Gtk::ScrolledWindow()); + //quick little hack -- newer versions of gtk gave the item zero space allotment + _scroller->set_size_request(-1, 120); + + _scroller->add(*_tree); + _scroller->set_policy( Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC ); + //_scroller.set_shadow_type(Gtk::SHADOW_IN); + } + param_readSVGValue(param_getSVGValue().c_str()); +} + +void PathArrayParam::on_reverse_toggled(const Glib::ustring &path) +{ + Gtk::TreeModel::iterator iter = _store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + PathAndDirectionAndVisible *w = row[_model->_colObject]; + row[_model->_colReverse] = !row[_model->_colReverse]; + w->reversed = row[_model->_colReverse]; + + param_write_to_repr(param_getSVGValue().c_str()); + DocumentUndo::done(param_effect->getSPDoc(), _("Link path parameter to path"), INKSCAPE_ICON("dialog-path-effects")); +} + +void PathArrayParam::on_visible_toggled(const Glib::ustring &path) +{ + Gtk::TreeModel::iterator iter = _store->get_iter(path); + Gtk::TreeModel::Row row = *iter; + PathAndDirectionAndVisible *w = row[_model->_colObject]; + row[_model->_colVisible] = !row[_model->_colVisible]; + w->visibled = row[_model->_colVisible]; + + param_write_to_repr(param_getSVGValue().c_str()); + DocumentUndo::done(param_effect->getSPDoc(), _("Toggle path parameter visibility"), INKSCAPE_ICON("dialog-path-effects")); +} + +void PathArrayParam::param_set_default() {} + +Gtk::Widget *PathArrayParam::param_newWidget() +{ + + Gtk::Box* vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Box* hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + _tree = nullptr; + _model = nullptr; + _scroller = nullptr; + initui(); + vbox->pack_start(*_scroller, Gtk::PACK_EXPAND_WIDGET); + + + { // Paste path to link button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_link_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Link to path in clipboard")); + } + + { // Remove linked path + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("list-remove", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_remove_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Remove Path")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_down_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Down")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &PathArrayParam::on_up_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Up")); + } + + vbox->pack_end(*hbox, Gtk::PACK_SHRINK); + + vbox->show_all_children(true); + + return vbox; +} + +bool PathArrayParam::_selectIndex(const Gtk::TreeIter &iter, int *i) +{ + if ((*i)-- <= 0) { + _tree->get_selection()->select(iter); + return true; + } + return false; +} + +std::vector<SPObject *> PathArrayParam::param_get_satellites() +{ + std::vector<SPObject *> objs; + for (auto &iter : _vector) { + if (iter && iter->ref.isAttached()) { + SPObject *obj = iter->ref.getObject(); + if (obj) { + objs.push_back(obj); + } + } + } + return objs; +} + +void PathArrayParam::on_up_button_click() +{ + Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + + int i = -1; + std::vector<PathAndDirectionAndVisible*>::iterator piter = _vector.begin(); + for (std::vector<PathAndDirectionAndVisible*>::iterator iter = _vector.begin(); iter != _vector.end(); piter = iter, i++, ++iter) { + if (*iter == row[_model->_colObject]) { + _vector.erase(iter); + _vector.insert(piter, row[_model->_colObject]); + break; + } + } + + param_write_to_repr(param_getSVGValue().c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), _("Move path up"), INKSCAPE_ICON("dialog-path-effects")); + + _store->foreach_iter(sigc::bind<int *>(sigc::mem_fun(*this, &PathArrayParam::_selectIndex), &i)); + } +} + +void PathArrayParam::on_down_button_click() +{ + Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + + int i = 0; + for (std::vector<PathAndDirectionAndVisible*>::iterator iter = _vector.begin(); iter != _vector.end(); i++, ++iter) { + if (*iter == row[_model->_colObject]) { + std::vector<PathAndDirectionAndVisible*>::iterator niter = _vector.erase(iter); + if (niter != _vector.end()) { + ++niter; + i++; + } + _vector.insert(niter, row[_model->_colObject]); + break; + } + } + + param_write_to_repr(param_getSVGValue().c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), _("Move path down"), INKSCAPE_ICON("dialog-path-effects")); + + _store->foreach_iter(sigc::bind<int *>(sigc::mem_fun(*this, &PathArrayParam::_selectIndex), &i)); + } +} + +void PathArrayParam::on_remove_button_click() +{ + Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + unlink(row[_model->_colObject]); + + param_write_to_repr(param_getSVGValue().c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), _("Remove path"), INKSCAPE_ICON("dialog-path-effects")); + } +} + +void PathArrayParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + std::vector<Glib::ustring> pathsid = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "svg:path"); + std::vector<Glib::ustring> textsid = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "svg:text"); + pathsid.insert(pathsid.end(), textsid.begin(), textsid.end()); + if (pathsid.empty()) { + return; + } + bool foundOne = false; + Inkscape::SVGOStringStream os; + for (auto iter : _vector) { + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << iter->href << "," << (iter->reversed ? "1" : "0") << "," << (iter->visibled ? "1" : "0"); + } + for (auto pathid : pathsid) { + // add '#' at start to make it an uri. + pathid.insert(pathid.begin(), '#'); + + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << pathid.c_str() << ",0,1"; + } + param_write_to_repr(os.str().c_str()); + DocumentUndo::done(param_effect->getSPDoc(), _("Link patharray parameter to path"), INKSCAPE_ICON("dialog-path-effects")); +} + +void PathArrayParam::unlink(PathAndDirectionAndVisible *to) +{ + to->linked_modified_connection.disconnect(); + to->linked_delete_connection.disconnect(); + to->ref.detach(); + to->_pathvector = Geom::PathVector(); + if (to->href) { + g_free(to->href); + to->href = nullptr; + } + for (std::vector<PathAndDirectionAndVisible*>::iterator iter = _vector.begin(); iter != _vector.end(); ++iter) { + if (*iter == to) { + PathAndDirectionAndVisible *w = *iter; + _vector.erase(iter); + delete w; + return; + } + } +} + +void PathArrayParam::start_listening() +{ + for (auto w : _vector) { + linked_changed(nullptr,w->ref.getObject(), w); + } +} + +void PathArrayParam::linked_delete(SPObject * /*deleted*/, PathAndDirectionAndVisible * /*to*/) +{ + // unlink(to); + + param_write_to_repr(param_getSVGValue().c_str()); +} + +bool PathArrayParam::_updateLink(const Gtk::TreeIter &iter, PathAndDirectionAndVisible *pd) +{ + Gtk::TreeModel::Row row = *iter; + if (row[_model->_colObject] == pd) { + SPObject *obj = pd->ref.getObject(); + row[_model->_colLabel] = obj && obj->getId() ? ( obj->label() ? obj->label() : obj->getId() ) : pd->href; + return true; + } + return false; +} + +void PathArrayParam::linked_changed(SPObject * /*old_obj*/, SPObject *new_obj, PathAndDirectionAndVisible *to) +{ + to->linked_delete_connection.disconnect(); + to->linked_modified_connection.disconnect(); + + if (new_obj && SP_IS_ITEM(new_obj)) { + to->linked_delete_connection = new_obj->connectDelete( + sigc::bind<PathAndDirectionAndVisible *>(sigc::mem_fun(*this, &PathArrayParam::linked_delete), to)); + to->linked_modified_connection = new_obj->connectModified( + sigc::bind<PathAndDirectionAndVisible *>(sigc::mem_fun(*this, &PathArrayParam::linked_modified), to)); + + linked_modified(new_obj, SP_OBJECT_MODIFIED_FLAG, to); + } else { + to->_pathvector = Geom::PathVector(); + param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); + if (_store.get()) { + _store->foreach_iter( + sigc::bind<PathAndDirectionAndVisible *>(sigc::mem_fun(*this, &PathArrayParam::_updateLink), to)); + } + } +} + +void PathArrayParam::setPathVector(SPObject *linked_obj, guint /*flags*/, PathAndDirectionAndVisible *to) +{ + if (!to) { + return; + } + std::unique_ptr<SPCurve> curve; + SPText *text = dynamic_cast<SPText *>(linked_obj); + if (auto shape = dynamic_cast<SPShape const *>(linked_obj)) { + SPLPEItem * lpe_item = SP_LPE_ITEM(linked_obj); + if (_from_original_d) { + curve = SPCurve::copy(shape->curveForEdit()); + } else if (_allow_only_bspline_spiro && lpe_item && lpe_item->hasPathEffect()){ + curve = SPCurve::copy(shape->curveForEdit()); + PathEffectList lpelist = lpe_item->getEffectList(); + PathEffectList::iterator i; + for (i = lpelist.begin(); i != lpelist.end(); ++i) { + LivePathEffectObject *lpeobj = (*i)->lpeobject; + if (lpeobj) { + Inkscape::LivePathEffect::Effect *lpe = lpeobj->get_lpe(); + if (dynamic_cast<Inkscape::LivePathEffect::LPEBSpline *>(lpe)) { + Geom::PathVector hp; + LivePathEffect::sp_bspline_do_effect(curve.get(), 0, hp); + } else if (dynamic_cast<Inkscape::LivePathEffect::LPESpiro *>(lpe)) { + LivePathEffect::sp_spiro_do_effect(curve.get()); + } + } + } + } else { + curve = SPCurve::copy(shape->curve()); + } + } else if (text) { + bool hidden = text->isHidden(); + if (hidden) { + if (to->_pathvector.empty()) { + text->setHidden(false); + curve = text->getNormalizedBpath(); + text->setHidden(true); + } else { + if (curve == nullptr) { + curve = std::make_unique<SPCurve>(); + } + curve->set_pathvector(to->_pathvector); + } + } else { + curve = text->getNormalizedBpath(); + } + } + + if (curve == nullptr) { + // curve invalid, set empty pathvector + to->_pathvector = Geom::PathVector(); + } else { + to->_pathvector = curve->get_pathvector(); + } + +} + +void PathArrayParam::linked_modified(SPObject *linked_obj, guint flags, PathAndDirectionAndVisible *to) +{ + if (!_updating && param_effect->getSPDoc()->isSensitive() && flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) + { + if (!to) { + return; + } + setPathVector(linked_obj, flags, to); + param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); + if (_store.get()) { + _store->foreach_iter( + sigc::bind<PathAndDirectionAndVisible *>(sigc::mem_fun(*this, &PathArrayParam::_updateLink), to)); + } + } +} + +bool PathArrayParam::param_readSVGValue(const gchar *strvalue) +{ + if (strvalue) { + while (!_vector.empty()) { + PathAndDirectionAndVisible *w = _vector.back(); + unlink(w); + } + + if (_store.get()) { + _store->clear(); + } + + gchar ** strarray = g_strsplit(strvalue, "|", 0); + bool write = false; + for (gchar ** iter = strarray; *iter != nullptr; iter++) { + if ((*iter)[0] == '#') { + gchar ** substrarray = g_strsplit(*iter, ",", 0); + SPObject * old_ref = param_effect->getSPDoc()->getObjectByHref(*substrarray); + if (old_ref) { + SPObject * successor = old_ref->_successor; + Glib::ustring id = *substrarray; + if (successor) { + id = successor->getId(); + id.insert(id.begin(), '#'); + write = true; + } + *(substrarray) = g_strdup(id.c_str()); + } + PathAndDirectionAndVisible* w = new PathAndDirectionAndVisible((SPObject *)param_effect->getLPEObj()); + w->href = g_strdup(*substrarray); + w->reversed = *(substrarray+1) != nullptr && (*(substrarray+1))[0] == '1'; + //Like this to make backwards compatible, new value added in 0.93 + w->visibled = *(substrarray+2) == nullptr || (*(substrarray+2))[0] == '1'; + w->linked_changed_connection = w->ref.changedSignal().connect( + sigc::bind<PathAndDirectionAndVisible *>(sigc::mem_fun(*this, &PathArrayParam::linked_changed), w)); + w->ref.attach(URI(w->href)); + + _vector.push_back(w); + if (_store.get()) { + Gtk::TreeModel::iterator iter = _store->append(); + Gtk::TreeModel::Row row = *iter; + SPObject *obj = w->ref.getObject(); + + row[_model->_colObject] = w; + row[_model->_colLabel] = obj ? ( obj->label() ? obj->label() : obj->getId() ) : w->href; + row[_model->_colReverse] = w->reversed; + row[_model->_colVisible] = w->visibled; + } + g_strfreev (substrarray); + } + } + g_strfreev (strarray); + if (write) { + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + } + return true; + + } + return false; +} + +Glib::ustring PathArrayParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + bool foundOne = false; + for (auto iter : _vector) { + if (foundOne) { + os << "|"; + } else { + foundOne = true; + } + os << iter->href << "," << (iter->reversed ? "1" : "0") << "," << (iter->visibled ? "1" : "0"); + } + return os.str(); +} + +Glib::ustring PathArrayParam::param_getDefaultSVGValue() const +{ + return ""; +} + +void PathArrayParam::update() +{ + for (auto & iter : _vector) { + SPObject *linked_obj = iter->ref.getObject(); + linked_modified(linked_obj, SP_OBJECT_MODIFIED_FLAG, iter); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/patharray.h b/src/live_effects/parameter/patharray.h new file mode 100644 index 0000000..a85d0f9 --- /dev/null +++ b/src/live_effects/parameter/patharray.h @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINALPATHARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_ORIGINALPATHARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <vector> + +#include <gtkmm/box.h> +#include <gtkmm/treeview.h> +#include <gtkmm/treestore.h> +#include <gtkmm/scrolledwindow.h> + +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/path-reference.h" + +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "path-reference.h" + +class SPObject; + +namespace Inkscape { + +namespace LivePathEffect { + +class PathAndDirectionAndVisible { +public: + PathAndDirectionAndVisible(SPObject *owner) + : href(nullptr), + ref(owner), + _pathvector(Geom::PathVector()), + reversed(false), + visibled(true) + { + + } + gchar *href; + URIReference ref; + Geom::PathVector _pathvector; + bool reversed; + bool visibled; + + sigc::connection linked_changed_connection; + sigc::connection linked_delete_connection; + sigc::connection linked_modified_connection; +}; + +class PathArrayParam : public Parameter +{ +public: + class ModelColumns; + + PathArrayParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect); + + ~PathArrayParam() override; + + Gtk::Widget * param_newWidget() override; + std::vector<SPObject *> param_get_satellites() override; + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_update_default(const gchar * default_value) override{}; + /** Disable the canvas indicators of parent class by overriding this method */ + void param_editOncanvas(SPItem * /*item*/, SPDesktop * /*dt*/) override {}; + /** Disable the canvas indicators of parent class by overriding this method */ + void addCanvasIndicators(SPLPEItem const* /*lpeitem*/, std::vector<Geom::PathVector> & /*hp_vec*/) override {}; + void setFromOriginalD(bool from_original_d){ _from_original_d = from_original_d; update();}; + void allowOnlyBsplineSpiro(bool allow_only_bspline_spiro){ _allow_only_bspline_spiro = allow_only_bspline_spiro; update();}; + void setUpdating(bool updating) {_updating = updating;}; + std::vector<PathAndDirectionAndVisible*> _vector; + ParamType paramType() const override { return ParamType::PATH_ARRAY; }; +protected: + friend class LPEFillBetweenMany; + bool _updateLink(const Gtk::TreeIter& iter, PathAndDirectionAndVisible* pd); + bool _selectIndex(const Gtk::TreeIter& iter, int* i); + void unlink(PathAndDirectionAndVisible* to); + void start_listening(); + void setPathVector(SPObject *linked_obj, guint flags, PathAndDirectionAndVisible* to); + + void linked_changed(SPObject *old_obj, SPObject *new_obj, PathAndDirectionAndVisible* to); + void linked_modified(SPObject *linked_obj, guint flags, PathAndDirectionAndVisible* to); + void linked_delete(SPObject *deleted, PathAndDirectionAndVisible* to); + + ModelColumns *_model; + Glib::RefPtr<Gtk::TreeStore> _store; + Gtk::TreeView *_tree; + Gtk::ScrolledWindow *_scroller; + + void on_link_button_click(); + void on_remove_button_click(); + void on_up_button_click(); + void on_down_button_click(); + void on_reverse_toggled(const Glib::ustring& path); + void on_visible_toggled(const Glib::ustring& path); + +private: + bool _from_original_d; + bool _allow_only_bspline_spiro; + bool _updating = false; + void update(); + void initui(); + PathArrayParam(const PathArrayParam &) = delete; + PathArrayParam &operator=(const PathArrayParam &) = delete; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/point.cpp b/src/live_effects/parameter/point.cpp new file mode 100644 index 0000000..f99a098 --- /dev/null +++ b/src/live_effects/parameter/point.cpp @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "point.h" + +#include "inkscape.h" + +#include "live_effects/effect.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" +#include "ui/widget/point.h" + +#include <glibmm/i18n.h> + +namespace Inkscape { + +namespace LivePathEffect { + +PointParam::PointParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const gchar *htip, Geom::Point default_value, + bool live_update ) + : Parameter(label, tip, key, wr, effect) + , defvalue(default_value) + , liveupdate(live_update) +{ + handle_tip = g_strdup(htip); +} + +PointParam::~PointParam() +{ + if (handle_tip) + g_free(handle_tip); +} + +void +PointParam::param_set_default() +{ + param_setValue(defvalue,true); +} + +void +PointParam::param_set_liveupdate( bool live_update) +{ + liveupdate = live_update; +} + +Geom::Point +PointParam::param_get_default() const{ + return defvalue; +} + +void +PointParam::param_update_default(Geom::Point default_point) +{ + defvalue = default_point; +} + +void +PointParam::param_update_default(const gchar * default_point) +{ + gchar ** strarray = g_strsplit(default_point, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_update_default( Geom::Point(newx, newy) ); + } +} + +void +PointParam::param_hide_knot(bool hide) { + if (_knot_entity) { + bool update = false; + if (hide && _knot_entity->knot->flags & SP_KNOT_VISIBLE) { + update = true; + _knot_entity->knot->hide(); + } else if(!hide && !(_knot_entity->knot->flags & SP_KNOT_VISIBLE)) { + update = true; + _knot_entity->knot->show(); + } + if (update) { + _knot_entity->update_knot(); + } + } +} + +void +PointParam::param_setValue(Geom::Point newpoint, bool write) +{ + *dynamic_cast<Geom::Point *>( this ) = newpoint; + if(write){ + Inkscape::SVGOStringStream os; + os << newpoint; + gchar * str = g_strdup(os.str().c_str()); + param_write_to_repr(str); + g_free(str); + } + if(_knot_entity && liveupdate){ + _knot_entity->update_knot(); + } +} + +bool +PointParam::param_readSVGValue(const gchar * strvalue) +{ + gchar ** strarray = g_strsplit(strvalue, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_setValue( Geom::Point(newx, newy) ); + return true; + } + return false; +} + +Glib::ustring +PointParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << *dynamic_cast<Geom::Point const *>( this ); + return os.str(); +} + +Glib::ustring +PointParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +void +PointParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + param_setValue( (*this) * postmul, true); +} + +Gtk::Widget * +PointParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredTransformedPoint * pointwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredTransformedPoint( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + Geom::Affine transf = SP_ACTIVE_DESKTOP->doc2dt(); + pointwdg->setTransform(transf); + pointwdg->setValue( *this ); + pointwdg->clearProgrammatically(); + pointwdg->set_undo_parameters(_("Change point parameter"), INKSCAPE_ICON("dialog-path-effects")); + pointwdg->signal_button_release_event().connect(sigc::mem_fun (*this, &PointParam::on_button_release)); + + Gtk::Box * hbox = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) ); + hbox->pack_start(*pointwdg, true, true); + hbox->show_all_children(); + return dynamic_cast<Gtk::Widget *> (hbox); +} + +bool PointParam::on_button_release(GdkEventButton* button_event) { + param_effect->refresh_widgets = true; + return false; +} + +void +PointParam::set_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color) +{ + knot_shape = shape; + knot_mode = mode; + knot_color = color; +} + +class PointParamKnotHolderEntity : public KnotHolderEntity { +public: + PointParamKnotHolderEntity(PointParam *p) { this->pparam = p; } + ~PointParamKnotHolderEntity() override { this->pparam->_knot_entity = nullptr;} + + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_click(guint state) override; + +private: + PointParam *pparam; +}; + +void +PointParamKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + Geom::Point s = snap_knot_position(p, state); + if (state & GDK_CONTROL_MASK) { + Geom::Point A(origin[Geom::X],p[Geom::Y]); + Geom::Point B(p[Geom::X],origin[Geom::Y]); + double distanceA = Geom::distance(A,p); + double distanceB = Geom::distance(B,p); + if(distanceA > distanceB){ + s = B; + } else { + s = A; + } + } + if(this->pparam->liveupdate){ + pparam->param_setValue(s, true); + } else { + pparam->param_setValue(s); + } +} + +void +PointParamKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + pparam->param_setValue(*pparam, true); + pparam->param_effect->refresh_widgets = true; +} + +Geom::Point +PointParamKnotHolderEntity::knot_get() const +{ + return *pparam; +} + +void +PointParamKnotHolderEntity::knot_click(guint state) +{ + if (state & GDK_CONTROL_MASK) { + if (state & GDK_MOD1_MASK) { + this->pparam->param_set_default(); + pparam->param_setValue(*pparam,true); + } + } +} + +void +PointParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + _knot_entity = new PointParamKnotHolderEntity(this); + // TODO: can we ditch handleTip() etc. because we have access to handle_tip etc. itself??? + _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:Point", + handleTip(), knot_color); + knotholder->add(_knot_entity); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/point.h b/src/live_effects/parameter/point.h new file mode 100644 index 0000000..0f45c62 --- /dev/null +++ b/src/live_effects/parameter/point.h @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_POINT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_POINT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include <2geom/point.h> + +#include "display/control/canvas-item-enums.h" +#include "live_effects/parameter/parameter.h" +#include "ui/widget/registered-widget.h" + +class KnotHolder; +class KnotHolderEntity; + +namespace Inkscape { + +namespace LivePathEffect { + +class PointParamKnotHolderEntity; + +class PointParam : public Geom::Point, public Parameter { +public: + PointParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const gchar *handle_tip = nullptr,// tip for automatically associated on-canvas handle + Geom::Point default_value = Geom::Point(0,0), + bool live_update = true ); + ~PointParam() override; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + inline const gchar *handleTip() const { return handle_tip ? handle_tip : param_tooltip.c_str(); } + void param_setValue(Geom::Point newpoint, bool write = false); + void param_set_default() override; + void param_hide_knot(bool hide); + Geom::Point param_get_default() const; + void param_set_liveupdate(bool live_update); + void param_update_default(Geom::Point default_point); + + void param_update_default(const gchar * default_point) override; + void param_transform_multiply(Geom::Affine const & /*postmul*/, bool set) override; + + void set_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color); + + bool providesKnotHolderEntities() const override { return true; } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + ParamType paramType() const override { return ParamType::POINT; }; + friend class PointParamKnotHolderEntity; +private: + PointParam(const PointParam&) = delete; + PointParam& operator=(const PointParam&) = delete; + bool on_button_release(GdkEventButton* button_event); + Geom::Point defvalue; + bool liveupdate; + KnotHolderEntity * _knot_entity = nullptr; + Inkscape::CanvasItemCtrlShape knot_shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND; + Inkscape::CanvasItemCtrlMode knot_mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR; + guint32 knot_color = 0xffffff00; + gchar *handle_tip; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/powerstrokepointarray.cpp b/src/live_effects/parameter/powerstrokepointarray.cpp new file mode 100644 index 0000000..b9e1d6d --- /dev/null +++ b/src/live_effects/parameter/powerstrokepointarray.cpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "powerstrokepointarray.h" + +#include <2geom/sbasis-2d.h> +#include <2geom/bezier-to-sbasis.h> +#include <2geom/piecewise.h> +#include <2geom/sbasis-geometric.h> + +#include "ui/dialog/lpe-powerstroke-properties.h" + +#include "ui/knot/knot-holder.h" + +#include "live_effects/effect.h" +#include "live_effects/lpe-powerstroke.h" + + +#include "preferences.h" // for proportional stroke/path scaling behavior + +#include <glibmm/i18n.h> + +namespace Inkscape { + +namespace LivePathEffect { + +PowerStrokePointArrayParam::PowerStrokePointArrayParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect) + : ArrayParam<Geom::Point>(label, tip, key, wr, effect, 0) + , knot_shape(Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND) + , knot_mode(Inkscape::CANVAS_ITEM_CTRL_MODE_XOR) + , knot_color(0xff88ff00) +{ +} + +PowerStrokePointArrayParam::~PowerStrokePointArrayParam() += default; + +Gtk::Widget * +PowerStrokePointArrayParam::param_newWidget() +{ + return nullptr; +} + +void PowerStrokePointArrayParam::param_transform_multiply(Geom::Affine const &postmul, bool /*set*/) +{ + // Check if proportional stroke-width scaling is on + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true; + if (transform_stroke) { + std::vector<Geom::Point> result; + result.reserve(_vector.size()); // reserve space for the points that will be added in the for loop + for (auto point_it : _vector) + { + // scale each width knot with the average scaling in X and Y + Geom::Coord const A = point_it[Geom::Y] * postmul.descrim(); + result.emplace_back(point_it[Geom::X], A); + } + param_set_and_write_new_value(result); + } +} + +/** call this method to recalculate the controlpoints such that they stay at the same location relative to the new path. Useful after adding/deleting nodes to the path.*/ +void +PowerStrokePointArrayParam::recalculate_controlpoints_for_new_pwd2(Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in) +{ + Inkscape::LivePathEffect::LPEPowerStroke *lpe = dynamic_cast<Inkscape::LivePathEffect::LPEPowerStroke *>(param_effect); + if (lpe) { + if (last_pwd2.size() > pwd2_in.size()) { + double factor = (double)pwd2_in.size() / (double)last_pwd2.size(); + for (auto & i : _vector) { + i[Geom::X] *= factor; + } + } else if (last_pwd2.size() < pwd2_in.size()) { + // Path has become longer: probably node added, maintain position of knots + Geom::Piecewise<Geom::D2<Geom::SBasis> > normal = rot90(unitVector(derivative(pwd2_in))); + for (auto & i : _vector) { + Geom::Point pt = i; + Geom::Point position = last_pwd2.valueAt(pt[Geom::X]) + pt[Geom::Y] * last_pwd2_normal.valueAt(pt[Geom::X]); + double t = nearest_time(position, pwd2_in); + i[Geom::X] = t; + } + } + write_to_SVG(); + } +} + +/** call this method to recalculate the controlpoints when path is reversed.*/ +std::vector<Geom::Point> +PowerStrokePointArrayParam::reverse_controlpoints(bool write) +{ + std::vector<Geom::Point> controlpoints; + if (!last_pwd2.empty()) { + Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in_reverse = reverse(last_pwd2); + for (auto & i : _vector) { + Geom::Point control_pos = last_pwd2.valueAt(i[Geom::X]); + double new_pos = Geom::nearest_time(control_pos, pwd2_in_reverse); + controlpoints.emplace_back(new_pos,i[Geom::Y]); + i[Geom::X] = new_pos; + } + if (write) { + write_to_SVG(); + _vector.clear(); + _vector = controlpoints; + controlpoints.clear(); + write_to_SVG(); + return _vector; + } + } + return controlpoints; +} + +float PowerStrokePointArrayParam::median_width() +{ + size_t size = _vector.size(); + if (size > 0) + { + if (size % 2 == 0) + { + return (_vector[size / 2 - 1].y() + _vector[size / 2].y()) / 2; + } + else + { + return _vector[size / 2].y(); + } + } + return 1; +} + +void +PowerStrokePointArrayParam::set_pwd2(Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in, Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_normal_in) +{ + last_pwd2 = pwd2_in; + last_pwd2_normal = pwd2_normal_in; +} + + +void +PowerStrokePointArrayParam::set_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color) +{ + knot_shape = shape; + knot_mode = mode; + knot_color = color; +} +/* +class PowerStrokePointArrayParamKnotHolderEntity : public KnotHolderEntity { +public: + PowerStrokePointArrayParamKnotHolderEntity(PowerStrokePointArrayParam *p, unsigned int index); + virtual ~PowerStrokePointArrayParamKnotHolderEntity() {} + + virtual void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state); + virtual Geom::Point knot_get() const; + virtual void knot_click(guint state); + + // Checks whether the index falls within the size of the parameter's vector + bool valid_index(unsigned int index) const { + return (_pparam->_vector.size() > index); + }; + +private: + PowerStrokePointArrayParam *_pparam; + unsigned int _index; +};*/ + +PowerStrokePointArrayParamKnotHolderEntity::PowerStrokePointArrayParamKnotHolderEntity(PowerStrokePointArrayParam *p, unsigned int index) + : _pparam(p), + _index(index) +{ +} + +void +PowerStrokePointArrayParamKnotHolderEntity::knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) +{ + using namespace Geom; + + if (!valid_index(_index)) { + return; + } + static gint prev_index = 0; + Piecewise<D2<SBasis> > const & pwd2 = _pparam->get_pwd2(); + Piecewise<D2<SBasis> > pwd2port = _pparam->get_pwd2(); + Geom::Point s = snap_knot_position(p, state); + double t2 = 0; + LPEPowerStroke *ps = dynamic_cast<LPEPowerStroke *>(_pparam->param_effect); + if (ps && ps->not_jump) { + s = p; + t2 = _pparam->_vector.at(_index)[Geom::X]; + Geom::PathVector pathv = path_from_piecewise(pwd2port, 0.001); + pathv[0] = pathv[0].portion(std::max(std::floor(t2) - 1, 0.0), std::min(std::ceil(t2) + 1, (double)pathv[0].size())); + pwd2port = paths_to_pw(pathv); + } + /// @todo how about item transforms??? + + Piecewise<D2<SBasis> > const & n = _pparam->get_pwd2_normal(); + gint index = std::floor(nearest_time(s, pwd2)); + bool bigjump = false; + if (std::abs(prev_index - index) > 1) { + bigjump = true; + } else { + prev_index = index; + } + double t = nearest_time(s, pwd2port); + double offset = 0.0; + if (ps && ps->not_jump) { + double tpos = t + std::max(std::floor(t2) - 1, 0.0); + double prevpos = _pparam->_vector.at(_index)[Geom::X]; + if (bigjump) { + tpos = prevpos; + } + offset = dot(s - pwd2.valueAt(tpos), n.valueAt(tpos)); + _pparam->_vector.at(_index) = Geom::Point(tpos, offset/_pparam->_scale_width); + } else { + offset = dot(s - pwd2.valueAt(t), n.valueAt(t)); + _pparam->_vector.at(_index) = Geom::Point(t, offset/_pparam->_scale_width); + } + if (_pparam->_vector.size() == 1 ) { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + prefs->setDouble("/live_effects/powerstroke/width", offset); + } + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); +} + +Geom::Point +PowerStrokePointArrayParamKnotHolderEntity::knot_get() const +{ + using namespace Geom; + + if (!valid_index(_index)) { + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + + Piecewise<D2<SBasis> > const & pwd2 = _pparam->get_pwd2(); + Piecewise<D2<SBasis> > const & n = _pparam->get_pwd2_normal(); + + Point offset_point = _pparam->_vector.at(_index); + if (offset_point[X] > pwd2.size() || offset_point[X] < 0) { + g_warning("Broken powerstroke point at %f, I won't try to add that", offset_point[X]); + return Geom::Point(Geom::infinity(), Geom::infinity()); + } + Point canvas_point = pwd2.valueAt(offset_point[X]) + (offset_point[Y] * _pparam->_scale_width) * n.valueAt(offset_point[X]); + return canvas_point; +} + +void +PowerStrokePointArrayParamKnotHolderEntity::knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) +{ + _pparam->param_effect->refresh_widgets = true; + _pparam->write_to_SVG(); +} + +void PowerStrokePointArrayParamKnotHolderEntity::knot_set_offset(Geom::Point offset) +{ + _pparam->_vector.at(_index) = Geom::Point(offset.x(), offset.y() / 2); + this->parent_holder->knot_ungrabbed_handler(this->knot, 0); +} + +void +PowerStrokePointArrayParamKnotHolderEntity::knot_click(guint state) +{ + if (state & GDK_CONTROL_MASK) { + if (state & GDK_MOD1_MASK) { + // delete the clicked knot + std::vector<Geom::Point> & vec = _pparam->_vector; + if (vec.size() > 1) { //Force don't remove last knot + vec.erase(vec.begin() + _index); + _pparam->param_set_and_write_new_value(vec); + // shift knots down one index + for(auto & ent : parent_holder->entity) { + PowerStrokePointArrayParamKnotHolderEntity *pspa_ent = dynamic_cast<PowerStrokePointArrayParamKnotHolderEntity *>(ent); + if ( pspa_ent && pspa_ent->_pparam == this->_pparam ) { // check if the knotentity belongs to this powerstrokepointarray parameter + if (pspa_ent->_index > this->_index) { + --pspa_ent->_index; + } + } + }; + // temporary hide, when knotholder were recreated it finally drop + this->knot->hide(); + } + return; + } else { + // add a knot to XML + std::vector<Geom::Point> & vec = _pparam->_vector; + vec.insert(vec.begin() + _index, 1, vec.at(_index)); // this clicked knot is duplicated + _pparam->param_set_and_write_new_value(vec); + + // shift knots up one index + for(auto & ent : parent_holder->entity) { + PowerStrokePointArrayParamKnotHolderEntity *pspa_ent = dynamic_cast<PowerStrokePointArrayParamKnotHolderEntity *>(ent); + if ( pspa_ent && pspa_ent->_pparam == this->_pparam ) { // check if the knotentity belongs to this powerstrokepointarray parameter + if (pspa_ent->_index > this->_index) { + ++pspa_ent->_index; + } + } + }; + // add knot to knotholder + PowerStrokePointArrayParamKnotHolderEntity *e = new PowerStrokePointArrayParamKnotHolderEntity(_pparam, _index+1); + e->create(this->desktop, this->item, parent_holder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:PowerStroke", + _("<b>Stroke width control point</b>: drag to alter the stroke width. <b>Ctrl+click</b> adds a " + "control point, <b>Ctrl+Alt+click</b> deletes it, <b>Shift+click</b> launches width dialog."), + _pparam->knot_color); + parent_holder->add(e); + } + } + else if ((state & GDK_MOD1_MASK) || (state & GDK_SHIFT_MASK)) + { + Geom::Point offset = Geom::Point(_pparam->_vector.at(_index).x(), _pparam->_vector.at(_index).y() * 2); + Inkscape::UI::Dialogs::PowerstrokePropertiesDialog::showDialog(this->desktop, offset, this); + } +} + +void PowerStrokePointArrayParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + for (unsigned int i = 0; i < _vector.size(); ++i) { + PowerStrokePointArrayParamKnotHolderEntity *e = new PowerStrokePointArrayParamKnotHolderEntity(this, i); + e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:PowerStroke", + _("<b>Stroke width control point</b>: drag to alter the stroke width. <b>Ctrl+click</b> adds a " + "control point, <b>Ctrl+Alt+click</b> deletes it, <b>Shift+click</b> launches width dialog."), + knot_color); + knotholder->add(e); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/powerstrokepointarray.h b/src/live_effects/parameter/powerstrokepointarray.h new file mode 100644 index 0000000..73d1686 --- /dev/null +++ b/src/live_effects/parameter/powerstrokepointarray.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_POWERSTROKE_POINT_ARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_POWERSTROKE_POINT_ARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <2geom/point.h> + +#include "live_effects/parameter/array.h" + +#include "ui/knot/knot-holder-entity.h" + +namespace Inkscape { + +namespace LivePathEffect { + +class PowerStrokePointArrayParam : public ArrayParam<Geom::Point> { +public: + PowerStrokePointArrayParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect); + ~PowerStrokePointArrayParam() override; + + PowerStrokePointArrayParam(const PowerStrokePointArrayParam&) = delete; + PowerStrokePointArrayParam& operator=(const PowerStrokePointArrayParam&) = delete; + + Gtk::Widget * param_newWidget() override; + + void param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) override; + + void set_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color); + + float median_width(); + + bool providesKnotHolderEntities() const override { return true; } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + void param_update_default(const gchar * default_value) override{}; + + void set_pwd2(Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in, Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_normal_in); + Geom::Piecewise<Geom::D2<Geom::SBasis> > const & get_pwd2() const { return last_pwd2; } + Geom::Piecewise<Geom::D2<Geom::SBasis> > const & get_pwd2_normal() const { return last_pwd2_normal; } + + void recalculate_controlpoints_for_new_pwd2(Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in); + std::vector<Geom::Point> reverse_controlpoints(bool write); + void set_scale_width(double scale_width){_scale_width = scale_width;}; + double _scale_width; + ParamType paramType() const override { return ParamType::POWERSTROKE_POINT_ARRAY; }; + friend class PowerStrokePointArrayParamKnotHolderEntity; + +private: + Inkscape::CanvasItemCtrlShape knot_shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND; + Inkscape::CanvasItemCtrlMode knot_mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR; + guint32 knot_color; + + Geom::Piecewise<Geom::D2<Geom::SBasis> > last_pwd2; + Geom::Piecewise<Geom::D2<Geom::SBasis> > last_pwd2_normal; +}; + +class PowerStrokePointArrayParamKnotHolderEntity : public KnotHolderEntity { +public: + PowerStrokePointArrayParamKnotHolderEntity(PowerStrokePointArrayParam *p, unsigned int index); + ~PowerStrokePointArrayParamKnotHolderEntity() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override; + Geom::Point knot_get() const override; + virtual void knot_set_offset(Geom::Point offset); + void knot_click(guint state) override; + + /** Checks whether the index falls within the size of the parameter's vector */ + bool valid_index(unsigned int index) const { + return (_pparam->_vector.size() > index); + }; + +private: + PowerStrokePointArrayParam *_pparam; + unsigned int _index; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/random.cpp b/src/live_effects/parameter/random.cpp new file mode 100644 index 0000000..42f5065 --- /dev/null +++ b/src/live_effects/parameter/random.cpp @@ -0,0 +1,227 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "random.h" + +#include <glibmm/i18n.h> + +#include "live_effects/effect.h" +#include "svg/stringstream.h" +#include "svg/svg.h" +#include "ui/icon-names.h" +#include "ui/widget/random.h" +#include "ui/widget/registered-widget.h" + +#define noLPERANDOMPARAM_DEBUG + +/* RNG stolen from /display/nr-filter-turbulence.cpp */ +#define RAND_m 2147483647 /* 2**31 - 1 */ +#define RAND_a 16807 /* 7**5; primitive root of m */ +#define RAND_q 127773 /* m / a */ +#define RAND_r 2836 /* m % a */ +#define BSize 0x100 + +namespace Inkscape { + +namespace LivePathEffect { + + +RandomParam::RandomParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, gdouble default_value, long default_seed, bool randomsign) + : Parameter(label, tip, key, wr, effect) +{ + defvalue = default_value; + value = defvalue; + min = -Geom::infinity(); + max = Geom::infinity(); + integer = false; + + defseed = default_seed; + startseed = defseed; + seed = startseed; + _randomsign = randomsign; +} + +RandomParam::~RandomParam() += default; + +bool +RandomParam::param_readSVGValue(const gchar * strvalue) +{ + double newval, newstartseed; + gchar** stringarray = g_strsplit (strvalue, ";", 2); + unsigned int success = sp_svg_number_read_d(stringarray[0], &newval); + if (success == 1) { + success += sp_svg_number_read_d(stringarray[1], &newstartseed); + if (success == 2) { + param_set_value(newval, static_cast<long>(newstartseed)); + } else { + param_set_value(newval, defseed); + } + g_strfreev(stringarray); + return true; + } + g_strfreev(stringarray); + return false; +} + +Glib::ustring +RandomParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << value << ';' << startseed; + return os.str(); +} + +Glib::ustring +RandomParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue << ';' << defseed; + return os.str(); +} + +void +RandomParam::param_set_default() +{ + param_set_value(defvalue, defseed); +} + +void +RandomParam::param_update_default(gdouble default_value){ + defvalue = default_value; +} + +void +RandomParam::param_update_default(const gchar * default_value){ + double newval; + unsigned int success = sp_svg_number_read_d(default_value, &newval); + if (success == 1) { + param_update_default(newval); + } +} + +void +RandomParam::param_set_value(gdouble val, long newseed) +{ + value = val; + if (integer) + value = round(value); + if (value > max) + value = max; + if (value < min) + value = min; + + startseed = setup_seed(newseed); + // we reach maximum value so randomize over to fix duple in next cycle + Glib::ustring version = param_effect->lpeversion.param_getSVGValue(); + if (startseed == RAND_m - 1 && (( + effectType() != ROUGH_HATCHES && + effectType() != ROUGHEN) || + version >= "1.2")) + { + startseed = rand() * startseed; + } + seed = startseed; +} + +void +RandomParam::param_set_range(gdouble min, gdouble max) +{ + this->min = min; + this->max = max; +} + +void +RandomParam::param_make_integer(bool yes) +{ + integer = yes; +} + +void +RandomParam::resetRandomizer() +{ + seed = startseed; +} + + +Gtk::Widget * +RandomParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredRandom* regrandom = Gtk::manage( + new Inkscape::UI::Widget::RegisteredRandom( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + + regrandom->setValue(value, startseed); + if (integer) { + regrandom->setDigits(0); + regrandom->setIncrements(1, 10); + } + regrandom->setRange(min, max); + regrandom->setProgrammatically = false; + regrandom->signal_button_release_event().connect(sigc::mem_fun (*this, &RandomParam::on_button_release)); + + regrandom->set_undo_parameters(_("Change random parameter"), INKSCAPE_ICON("dialog-path-effects")); + + return dynamic_cast<Gtk::Widget *> (regrandom); +} + +bool RandomParam::on_button_release(GdkEventButton* button_event) { + param_effect->refresh_widgets = true; + return false; +} + +RandomParam::operator gdouble() +{ + if (_randomsign) { + return (rand() * value) - (rand() * value); + } else { + return rand() * value; + } +}; + + +long +RandomParam::setup_seed(long lSeed) +{ + if (lSeed <= 0) lSeed = -(lSeed % (RAND_m - 1)) + 1; + if (lSeed > RAND_m - 1) lSeed = RAND_m - 1; + return lSeed; +} + +// generates random number between 0 and 1 +gdouble +RandomParam::rand() +{ + long result; + result = RAND_a * (seed % RAND_q) - RAND_r * (seed / RAND_q); + if (result <= 0) result += RAND_m; + seed = result; + + gdouble dresult = (gdouble)(result % BSize) / BSize; + return dresult; +} + + +} /* namespace LivePathEffect */ +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/random.h b/src/live_effects/parameter/random.h new file mode 100644 index 0000000..b727885 --- /dev/null +++ b/src/live_effects/parameter/random.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_RANDOM_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_RANDOM_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/parameter.h" +#include <glibmm/ustring.h> +#include <2geom/point.h> +#include <2geom/path.h> + +namespace Inkscape { + +namespace LivePathEffect { + +class RandomParam : public Parameter { +public: + RandomParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + gdouble default_value = 1.0, + long default_seed = 0, + bool randomsign = false); + ~RandomParam() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_set_randomsign(bool randomsign) {_randomsign = randomsign;}; + Gtk::Widget * param_newWidget() override; + double param_get_random_number() { return rand(); }; + void param_set_value(gdouble val, long newseed); + void param_make_integer(bool yes = true); + void param_set_range(gdouble min, gdouble max); + void param_update_default(gdouble default_value); + void param_update_default(const gchar * default_value) override; + void resetRandomizer(); + operator gdouble(); + inline gdouble get_value() { return value; } ; + ParamType paramType() const override { return ParamType::RANDOM; }; + +protected: + long startseed; + long seed; + long defseed; + + gdouble value; + gdouble min; + gdouble max; + bool integer; + bool _randomsign; + gdouble defvalue; + +private: + bool on_button_release(GdkEventButton* button_event); + long setup_seed(long); + gdouble rand(); + + RandomParam(const RandomParam&) = delete; + RandomParam& operator=(const RandomParam&) = delete; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/satellite-reference.cpp b/src/live_effects/parameter/satellite-reference.cpp new file mode 100644 index 0000000..d86be33 --- /dev/null +++ b/src/live_effects/parameter/satellite-reference.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "satellite-reference.h" + +#include "document.h" +#include "live_effects/lpeobject.h" +#include "object/sp-item-group.h" +#include "object/sp-lpe-item.h" +#include "object/sp-shape.h" +#include "object/sp-text.h" +#include "object/uri-references.h" + +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +namespace Inkscape { + +namespace LivePathEffect { + +bool SatelliteReference::_acceptObject(SPObject *const obj) const +{ + if (SP_IS_SHAPE(obj) || SP_IS_TEXT(obj) || SP_IS_GROUP(obj)) { + /* Refuse references to lpeobject */ + SPObject *owner = getOwner(); + if (obj == owner) { + return false; + } + if (!dynamic_cast<LivePathEffectObject *>(owner)) { + return false; + } + return URIReference::_acceptObject(obj); + } else { + return false; + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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/live_effects/parameter/satellite-reference.h b/src/live_effects/parameter/satellite-reference.h new file mode 100644 index 0000000..a9f31a9 --- /dev/null +++ b/src/live_effects/parameter/satellite-reference.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_SP_SATELLITE_REFERENCE_H +#define SEEN_SP_SATELLITE_REFERENCE_H + +#include "object/sp-item.h" +#include "object/uri-references.h" +/** + * The reference corresponding to a satelite in a LivePathEffectObject. + */ +namespace Inkscape { +namespace LivePathEffect { + +class SatelliteReference : public Inkscape::URIReference +{ +public: + SatelliteReference(SPObject *owner, bool hasactive = false) + : URIReference(owner) + , _hasactive(hasactive) + , _active(true) + { + } + + bool getHasActive() const { return _hasactive; } + bool getActive() const { return _active; } + void setActive(bool active) { _active = active; } + +protected: + bool _acceptObject(SPObject *obj) const override; + +private: + bool _active; + bool _hasactive; +}; + +} // namespace LivePathEffect +} // namespace Inkscape + +#endif /* !SEEN_SP_SATELLITE_REFERENCE_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 :
\ No newline at end of file diff --git a/src/live_effects/parameter/satellite.cpp b/src/live_effects/parameter/satellite.cpp new file mode 100644 index 0000000..989a73a --- /dev/null +++ b/src/live_effects/parameter/satellite.cpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Abhishek Sharma + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/satellite.h" + +#include "bad-uri-exception.h" +#include "desktop.h" +#include "enums.h" +#include "inkscape.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "message-stack.h" +#include "selection-chemistry.h" +#include "svg/svg.h" +#include "ui/icon-loader.h" +#include "ui/widget/point.h" +#include "xml/repr.h" +// clipboard support +#include "ui/clipboard.h" +// required for linking to other paths +#include <glibmm/i18n.h> + +#include "bad-uri-exception.h" +#include "object/sp-item.h" +#include "object/uri.h" +#include "ui/icon-names.h" + +namespace Inkscape { + +namespace LivePathEffect { + +SatelliteParam::SatelliteParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect) + : Parameter(label, tip, key, wr, effect) + , lperef(std::make_shared<SatelliteReference>(param_effect->getLPEObj(), false)) + , last_transform(Geom::identity()) +{} + +SatelliteParam::~SatelliteParam() +{ + quit_listening(); +} + +std::vector<SPObject *> SatelliteParam::param_get_satellites() +{ + std::vector<SPObject *> objs; + // we reload connexions in case are lost for example item recreation on ungroup + if (!linked_transformed_connection) { + write_to_SVG(); + } + + SPObject * linked_obj = lperef->getObject(); + if (linked_obj) { + objs.push_back(linked_obj); + } + return objs; +} + +bool SatelliteParam::param_readSVGValue(const gchar *strvalue) +{ + if (strvalue) { + bool write = false; + auto lpeitems = param_effect->getCurrrentLPEItems(); + Glib::ustring id_tmp; + if (!lpeitems.size() && !param_effect->is_applied && !param_effect->getSPDoc()->isSeeking()) { + SPObject * old_ref = param_effect->getSPDoc()->getObjectByHref(strvalue); + if (old_ref) { + SPObject * successor = old_ref->_successor; + // cast to effect is not possible now + if (!g_strcmp0("clone_original", param_effect->getLPEObj()->getAttribute("effect"))) { + id_tmp = strvalue; + } + if (successor) { + id_tmp = successor->getId(); + id_tmp.insert(id_tmp.begin(), '#'); + write = true; + } + strvalue = id_tmp.c_str(); + } + } + SPObject *old_ref = lperef->getObject(); + if (old_ref) { + unlink(); + } + if (strvalue[0] == '#') { + try { + lperef->attach(Inkscape::URI(g_strdup(strvalue))); + // lp:1299948 + SPObject *new_ref = lperef->getObject(); + if (new_ref) { + linked_changed(old_ref, new_ref); + // linked_modified(new_ref, SP_OBJECT_STYLESHEET_MODIFIED_FLAG); + } // else: document still processing new events. Repr of the linked object not created yet. + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + lperef->detach(); + } + } else if (!lpeitems.size() && !param_effect->is_applied && !param_effect->getSPDoc()->isSeeking()) { + param_write_to_repr(""); + } + if (write) { + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + } + return true; + } + + return false; +} + +bool SatelliteParam::linksToItem() const +{ + return lperef->isAttached(); +} + +SPObject *SatelliteParam::getObject() const +{ + return lperef->isAttached() ? lperef->getObject() : nullptr; +} + +Glib::ustring SatelliteParam::param_getSVGValue() const +{ + if (lperef->getURI()) { + return lperef->getURI()->str(); + } + return ""; +} + +Glib::ustring SatelliteParam::param_getDefaultSVGValue() const +{ + return ""; +} + +void SatelliteParam::param_set_default() +{ + param_readSVGValue(""); +} + +void SatelliteParam::unlink() +{ + quit_listening(); + if (linksToItem()) { + lperef->detach(); + } +} + +void SatelliteParam::link(Glib::ustring itemid) +{ + if (itemid.empty()) { + return; + } + auto *document = param_effect->getSPDoc(); + SPObject *object = document->getObjectById(itemid); + + if (object && object != getObject()) { + itemid.insert(itemid.begin(), '#'); + param_write_to_repr(itemid.c_str()); + } else { + param_write_to_repr(""); + } + DocumentUndo::done(document, _("Link item parameter to path"), ""); +} + +// SIGNALS + +void SatelliteParam::start_listening(SPObject *to) +{ + if (!to) { + return; + } + quit_listening(); + linked_changed_connection = lperef->changedSignal().connect(sigc::mem_fun(*this, &SatelliteParam::linked_changed)); + SPItem *item = dynamic_cast<SPItem *>(to); + if (item) { + linked_released_connection = item->connectRelease(sigc::mem_fun(*this, &SatelliteParam::linked_released)); + linked_modified_connection = item->connectModified(sigc::mem_fun(*this, &SatelliteParam::linked_modified)); + linked_transformed_connection = + item->connectTransformed(sigc::mem_fun(*this, &SatelliteParam::linked_transformed)); + if (!param_effect->is_load) { + linked_modified(item, SP_OBJECT_MODIFIED_FLAG); + } + } +} + +void SatelliteParam::quit_listening() +{ + if (linked_changed_connection) { + linked_changed_connection.disconnect(); + } + if (linked_released_connection) { + linked_released_connection.disconnect(); + } + if (linked_modified_connection) { + linked_modified_connection.disconnect(); + } + if (linked_transformed_connection) { + linked_transformed_connection.disconnect(); + } +} + +void SatelliteParam::linked_changed(SPObject *old_obj, SPObject *new_obj) +{ + quit_listening(); + if (new_obj) { + start_listening(new_obj); + } +} + +void SatelliteParam::linked_released(SPObject *released) +{ + unlink(); + param_effect->processObjects(LPE_UPDATE); +} + + +void SatelliteParam::linked_modified(SPObject *linked_obj, guint flags) +{ + if (!_updating && (!param_effect->is_load || ownerlocator || !SP_ACTIVE_DESKTOP) && + flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_CHILD_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) + { + param_effect->getLPEObj()->requestModified(SP_OBJECT_MODIFIED_FLAG); + last_transform = Geom::identity(); + if (effectType() != CLONE_ORIGINAL) { + update_satellites(); + } + } +} + +void SatelliteParam::linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item) +{ + if (!_updating) { + update_satellites(); + } +} + +// UI + +void SatelliteParam::addCanvasIndicators(SPLPEItem const * /*lpeitem*/, std::vector<Geom::PathVector> &hp_vec) {} + +void SatelliteParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + // here prevent item is reseted transform on link + if (effectType() == CLONE_ORIGINAL) { + param_effect->is_load = false; + } + auto itemid = cm->getFirstObjectID(); + if (itemid.empty()) { + return; + } + + link(itemid); +} + +Gtk::Widget *SatelliteParam::param_newWidget() +{ + Gtk::Box *_widget = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + Gtk::Label *pLabel = Gtk::manage(new Gtk::Label(param_label)); + _widget->pack_start(*pLabel, true, true); + pLabel->set_tooltip_text(param_tooltip); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &SatelliteParam::on_link_button_click)); + _widget->pack_start(*pButton, true, true); + pButton->set_tooltip_text(_("Link to item on clipboard")); + + _widget->show_all_children(); + + return dynamic_cast<Gtk::Widget *>(_widget); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/satellite.h b/src/live_effects/parameter/satellite.h new file mode 100644 index 0000000..d3faf29 --- /dev/null +++ b/src/live_effects/parameter/satellite.h @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_SATELLITE_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_SATELLITE_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/satellite-reference.h" + +namespace Inkscape { + +namespace LivePathEffect { +class LPECloneOriginal; + +class SatelliteParam : public Parameter +{ +public: + SatelliteParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect); + ~SatelliteParam() override; + bool param_readSVGValue(const gchar *strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_update_default(const gchar *default_value) override{}; + void setUpdating(bool updating) { _updating = updating; } + bool getUpdating() const { return _updating; } + bool linksToItem() const; + SPObject *getObject() const; + // UI + Gtk::Widget *param_newWidget() override; + void addCanvasIndicators(SPLPEItem const *lpeitem, std::vector<Geom::PathVector> &hp_vec) override; + void on_link_button_click(); + friend class LPEBool; + friend class LPECloneOriginal; + friend class Effect; + Geom::Affine last_transform; + bool isConnected() {return !(!linked_changed_connection);} + void start_listening(SPObject *to); + void unlink(); + ParamType paramType() const override { return ParamType::SATELLITE; }; +protected: + void link(Glib::ustring itemid); + void quit_listening(); + void linked_released(SPObject *released_item); + void linked_deleted(SPObject *deleted_item); + void linked_transformed(Geom::Affine const *rel_transf, SPItem *moved_item); + void linked_modified(SPObject *linked_obj, guint flags); + void linked_changed(SPObject *old_obj, SPObject *new_obj); + + std::shared_ptr<SatelliteReference> lperef; + +private: + std::vector<SPObject *> param_get_satellites() override; + bool _updating = false; + sigc::connection linked_released_connection; + sigc::connection linked_modified_connection; + sigc::connection linked_transformed_connection; + sigc::connection linked_changed_connection; + SatelliteParam(const SatelliteParam &) = delete; + SatelliteParam &operator=(const SatelliteParam &) = delete; +}; + +} // namespace LivePathEffect + +} // namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/satellitearray.cpp b/src/live_effects/parameter/satellitearray.cpp new file mode 100644 index 0000000..6516e1f --- /dev/null +++ b/src/live_effects/parameter/satellitearray.cpp @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Jabiertxof <jabier.arraiza@marker.es> + * this class handle satellites of a lpe as a parameter + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/satellitearray.h" +#include "live_effects/effect.h" +#include "live_effects/lpeobject.h" +#include "inkscape.h" +#include "ui/clipboard.h" +#include "ui/icon-loader.h" +#include <glibmm/i18n.h> + +namespace Inkscape { + +namespace LivePathEffect { + +class SatelliteArrayParam::ModelColumns : public Gtk::TreeModel::ColumnRecord +{ +public: + ModelColumns() + { + add(_colObject); + add(_colLabel); + add(_colActive); + } + ~ModelColumns() override = default; + + Gtk::TreeModelColumn<Glib::ustring> _colObject; + Gtk::TreeModelColumn<Glib::ustring> _colLabel; + Gtk::TreeModelColumn<bool> _colActive; +}; +SatelliteArrayParam::SatelliteArrayParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, bool visible) + : ArrayParam<std::shared_ptr<SatelliteReference>>(label, tip, key, wr, effect) + , _visible(visible) +{ + param_widget_is_visible(_visible); + if (_visible) { + _tree = nullptr; + _scroller = nullptr; + _model = nullptr; + initui(); + oncanvas_editable = true; + } +} + +SatelliteArrayParam::~SatelliteArrayParam() +{ + _vector.clear(); + if (_store.get() && _model) { + delete _model; + } + quit_listening(); +} + +void SatelliteArrayParam::initui() +{ + SPDesktop *desktop = SP_ACTIVE_DESKTOP; + if (!desktop) { + return; + } + if (!_tree) { + _tree = manage(new Gtk::TreeView()); + _model = new ModelColumns(); + _store = Gtk::TreeStore::create(*_model); + _tree->set_model(_store); + + _tree->set_reorderable(true); + _tree->enable_model_drag_dest(Gdk::ACTION_MOVE); + Gtk::CellRendererToggle *_toggle_active = manage(new Gtk::CellRendererToggle()); + int activeColNum = _tree->append_column(_("Active"), *_toggle_active) - 1; + Gtk::TreeViewColumn *col_active = _tree->get_column(activeColNum); + _toggle_active->set_activatable(true); + _toggle_active->signal_toggled().connect(sigc::mem_fun(*this, &SatelliteArrayParam::on_active_toggled)); + col_active->add_attribute(_toggle_active->property_active(), _model->_colActive); + + _text_renderer = manage(new Gtk::CellRendererText()); + int nameColNum = _tree->append_column(_("Name"), *_text_renderer) - 1; + _name_column = _tree->get_column(nameColNum); + _name_column->add_attribute(_text_renderer->property_text(), _model->_colLabel); + + _tree->set_expander_column(*_tree->get_column(nameColNum)); + _tree->set_search_column(_model->_colLabel); + + // quick little hack -- newer versions of gtk gave the item zero space allotment + _scroller = manage(new Gtk::ScrolledWindow()); + _scroller->set_size_request(-1, 120); + + _scroller->add(*_tree); + _scroller->set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + //_scroller->set_shadow_type(Gtk::SHADOW_IN); + } + param_readSVGValue(param_getSVGValue().c_str()); +} + +void SatelliteArrayParam::start_listening() +{ + quit_listening(); + for (auto ref : _vector) { + if (ref && ref->isAttached()) { + SPItem *item = dynamic_cast<SPItem *>(ref->getObject()); + if (item) { + linked_connections.emplace_back(item->connectRelease( + sigc::hide(sigc::mem_fun(*this, &SatelliteArrayParam::updatesignal)))); + linked_connections.emplace_back(item->connectModified( + sigc::mem_fun(*this, &SatelliteArrayParam::linked_modified))); + linked_connections.emplace_back(item->connectTransformed( + sigc::hide(sigc::hide(sigc::mem_fun(*this, &SatelliteArrayParam::updatesignal))))); + linked_connections.emplace_back(ref->changedSignal().connect( + sigc::hide(sigc::hide(sigc::mem_fun(*this, &SatelliteArrayParam::updatesignal))))); + } + } + } +} + +void SatelliteArrayParam::linked_modified(SPObject *linked_obj, guint flags) { + if (!param_effect->is_load && param_effect->_lpe_action == LPE_NONE && + flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | + SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) + { + param_effect->processObjects(LPE_UPDATE); + } +} + +void SatelliteArrayParam::updatesignal() +{ + if (!param_effect->is_load && param_effect->_lpe_action == LPE_NONE) { + param_effect->processObjects(LPE_UPDATE); + } +} + +void SatelliteArrayParam::quit_listening() +{ + for (auto connexion : linked_connections) { + if (connexion) { + connexion.disconnect(); + } + } + linked_connections.clear(); +}; + +void SatelliteArrayParam::on_active_toggled(const Glib::ustring &item) +{ + int i = 0; + for (auto w : _vector) { + if (w && w->isAttached() && w->getObject()) { + Gtk::TreeModel::Row row = *_store->get_iter(Glib::ustring::format(i)); + Glib::ustring id = w->getObject()->getId() ? w->getObject()->getId() : ""; + if (id == row[_model->_colObject]) { + row[_model->_colActive] = !row[_model->_colActive]; + w->setActive(row[_model->_colActive]); + i++; + break; + } + } + } + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + DocumentUndo::done(param_effect->getSPDoc(), _("Active switched"), ""); +} + +bool SatelliteArrayParam::param_readSVGValue(const gchar *strvalue) +{ + if (strvalue) { + bool changed = !linked_connections.size() || !param_effect->is_load; + if (!ArrayParam::param_readSVGValue(strvalue)) { + return false; + } + auto lpeitems = param_effect->getCurrrentLPEItems(); + if (!lpeitems.size() && !param_effect->is_applied && !param_effect->getSPDoc()->isSeeking()) { + size_t pos = 0; + for (auto w : _vector) { + if (w) { + SPObject * tmp = w->getObject(); + if (tmp) { + SPObject * successor = tmp->_successor; + unlink(tmp); + if (successor) { + link(successor,pos); + } + } + } + pos ++; + } + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + update_satellites(false); + } + if (_store.get()) { + _store->clear(); + for (auto w : _vector) { + if (w) { + Gtk::TreeModel::iterator iter = _store->append(); + Gtk::TreeModel::Row row = *iter; + if (auto obj = w->getObject()) { + row[_model->_colObject] = Glib::ustring(obj->getId()); + row[_model->_colLabel] = obj->label() ? obj->label() : obj->getId(); + row[_model->_colActive] = w->getActive(); + } + } + } + } + if (changed) { + start_listening(); + } + return true; + } + return false; +} + +bool SatelliteArrayParam::_selectIndex(const Gtk::TreeIter &iter, int *i) +{ + if ((*i)-- <= 0) { + _tree->get_selection()->select(iter); + return true; + } + return false; +} + +void SatelliteArrayParam::on_up_button_click() +{ + Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row rowselected = *iter; + int i = 0; + for (auto w : _vector) { + if (w && w->isAttached() && w->getObject()) { + Gtk::TreeModel::Row row = *_store->get_iter(Glib::ustring::format(i)); + if (rowselected == row && i > 0) { + std::swap(_vector[i],_vector[i-1]); + i--; + break; + } + i++; + } + } + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), _("Move item up"), ""); + + _store->foreach_iter(sigc::bind<int *>(sigc::mem_fun(*this, &SatelliteArrayParam::_selectIndex), &i)); + } +} + +void SatelliteArrayParam::on_down_button_click() +{ + Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row rowselected = *iter; + int i = 0; + for (auto w : _vector) { + if (w && w->isAttached() && w->getObject()) { + Gtk::TreeModel::Row row = *_store->get_iter(Glib::ustring::format(i)); + if (rowselected == row && i < _vector.size() - 1) { + std::swap(_vector[i],_vector[i+1]); + i++; + break; + } + i++; + } + } + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), _("Move item down"), ""); + + _store->foreach_iter(sigc::bind<int *>(sigc::mem_fun(*this, &SatelliteArrayParam::_selectIndex), &i)); + } +} + +void SatelliteArrayParam::on_remove_button_click() +{ + Gtk::TreeModel::iterator iter = _tree->get_selection()->get_selected(); + if (iter) { + Gtk::TreeModel::Row row = *iter; + unlink(param_effect->getSPDoc()->getObjectById(row[_model->_colObject])); + + auto full = param_getSVGValue(); + param_write_to_repr(full.c_str()); + + DocumentUndo::done(param_effect->getSPDoc(), _("Remove item"), ""); + } +} + +void SatelliteArrayParam::on_link_button_click() +{ + Inkscape::UI::ClipboardManager *cm = Inkscape::UI::ClipboardManager::get(); + std::vector<Glib::ustring> itemsid; + // Here we ignore auto clipboard group wrapper + std::vector<Glib::ustring> itemsids = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "*", 2); + std::vector<Glib::ustring> containers = cm->getElementsOfType(SP_ACTIVE_DESKTOP, "*", 1); + for (auto item : itemsids) { + bool cont = false; + for (auto citems : containers) { + if (citems == item) { + cont = true; + } + } + if (cont == false) { + itemsid.push_back(item); + } + } + if (itemsid.empty()) { + return; + } + auto hreflist = param_effect->getLPEObj()->hrefList; + if (hreflist.size()) { + SPLPEItem *sp_lpe_item = dynamic_cast<SPLPEItem *>(*hreflist.begin()); + if (sp_lpe_item) { + for (auto itemid : itemsid) { + SPObject *added = param_effect->getSPDoc()->getObjectById(itemid); + if (added && sp_lpe_item != added) { + itemid.insert(itemid.begin(), '#'); + std::shared_ptr<SatelliteReference> satellitereference = + std::make_shared<SatelliteReference>(param_effect->getLPEObj(), _visible); + try { + satellitereference->attach(Inkscape::URI(itemid.c_str())); + satellitereference->setActive(true); + _vector.push_back(satellitereference); + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + satellitereference->detach(); + } + } + } + } + } + write_to_SVG(); + DocumentUndo::done(param_effect->getSPDoc(), _("Link itemarray parameter to item"), ""); +} + +Gtk::Widget *SatelliteArrayParam::param_newWidget() +{ + if (!_visible) { + return nullptr; + } + Gtk::Box *vbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + Gtk::Box *hbox = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL)); + _tree = nullptr; + _scroller = nullptr; + _model = nullptr; + initui(); + vbox->pack_start(*_scroller, Gtk::PACK_EXPAND_WIDGET); + + { // Paste item to link button + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("edit-clone", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &SatelliteArrayParam::on_link_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Link to item")); + } + + { // Remove linked item + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("list-remove", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &SatelliteArrayParam::on_remove_button_click)); + hbox->pack_start(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Remove Item")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-down", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &SatelliteArrayParam::on_down_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Down")); + } + + { // Move Down + Gtk::Image *pIcon = Gtk::manage(sp_get_icon_image("go-up", Gtk::ICON_SIZE_BUTTON)); + Gtk::Button *pButton = Gtk::manage(new Gtk::Button()); + pButton->set_relief(Gtk::RELIEF_NONE); + pIcon->show(); + pButton->add(*pIcon); + pButton->show(); + pButton->signal_clicked().connect(sigc::mem_fun(*this, &SatelliteArrayParam::on_up_button_click)); + hbox->pack_end(*pButton, Gtk::PACK_SHRINK); + pButton->set_tooltip_text(_("Move Up")); + } + + vbox->pack_end(*hbox, Gtk::PACK_SHRINK); + + vbox->show_all_children(true); + + return vbox; +} + +std::vector<SPObject *> SatelliteArrayParam::param_get_satellites() +{ + std::vector<SPObject *> objs; + for (auto &iter : _vector) { + if (iter && iter->isAttached()) { + SPObject *obj = iter->getObject(); + if (obj) { + objs.push_back(obj); + } + } + } + return objs; +} + +/* + * This function link a satellite writing into XML directly + * @param obj: object to link + * @param obj: position in vector + */ +void SatelliteArrayParam::link(SPObject *obj, size_t pos) +{ + if (obj && obj->getId()) { + Glib::ustring itemid = "#"; + itemid += obj->getId(); + std::shared_ptr<SatelliteReference> satellitereference = + std::make_shared<SatelliteReference>(param_effect->getLPEObj(), _visible); + try { + satellitereference->attach(Inkscape::URI(itemid.c_str())); + if (_visible) { + satellitereference->setActive(true); + } + if (_vector.size() == pos || pos == Glib::ustring::npos) { + _vector.push_back(satellitereference); + } else { + _vector[pos] = satellitereference; + } + } catch (Inkscape::BadURIException &e) { + g_warning("%s", e.what()); + satellitereference->detach(); + } + } +} + +void SatelliteArrayParam::unlink(SPObject *obj) +{ + if (!obj) { + return; + } + gint pos = -1; + for (auto w : _vector) { + pos++; + if (w) { + if (w->getObject() == obj) { + break; + } + } + } + if (pos != -1) { + _vector.erase(_vector.begin() + pos); + _vector.insert(_vector.begin() + pos, nullptr); + } +} + +void SatelliteArrayParam::unlink(std::shared_ptr<SatelliteReference> to) +{ + if (!to) { + return; + } + gint pos = -1; + for (auto w : _vector) { + pos++; + if (w) { + if (w->getObject() == to->getObject()) { + break; + } + + } + } + if (pos != -1) { + _vector.erase(_vector.begin() + pos); + _vector.insert(_vector.begin() + pos, nullptr); + } +} + +void SatelliteArrayParam::clear() +{ + _vector.clear(); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/satellitearray.h b/src/live_effects/parameter/satellitearray.h new file mode 100644 index 0000000..0a8534d --- /dev/null +++ b/src/live_effects/parameter/satellitearray.h @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_SATELLITEARRAY_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_SATELLITEARRAY_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <gtkmm/scrolledwindow.h> +#include <gtkmm/treestore.h> +#include <sigc++/sigc++.h> + +#include "live_effects/lpeobject.h" +#include "live_effects/parameter/array.h" +#include "live_effects/parameter/parameter.h" +#include "live_effects/parameter/satellite-reference.h" + +class SPObject; + +namespace Inkscape { +namespace LivePathEffect { +class SatelliteReference; +class SatelliteArrayParam : public ArrayParam<std::shared_ptr<SatelliteReference>> +{ +public: + class ModelColumns; + + SatelliteArrayParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, bool visible); + + ~SatelliteArrayParam() override; + Gtk::Widget *param_newWidget() override; + bool param_readSVGValue(const gchar *strvalue) override; + void link(SPObject *to, size_t pos = Glib::ustring::npos); + void unlink(std::shared_ptr<SatelliteReference> to); + void unlink(SPObject *to); + bool is_connected(){ return linked_connections.size() != 0; }; + void clear(); + void setUpdating(bool updating) { _updating = updating; } + bool getUpdating() const { return _updating; } + void start_listening(); + ParamType paramType() const override { return ParamType::SATELLITE_ARRAY; }; +protected: + void quit_listening(); + void linked_modified(SPObject *linked_obj, guint flags); + bool _updateLink(const Gtk::TreeIter &iter, std::shared_ptr<SatelliteReference> lpref); + bool _selectIndex(const Gtk::TreeIter &iter, int *i); + void updatesignal(); + ModelColumns *_model; + Glib::RefPtr<Gtk::TreeStore> _store; + Gtk::TreeView *_tree; + Gtk::ScrolledWindow *_scroller; + Gtk::CellRendererText *_text_renderer; + Gtk::CellRendererToggle *_toggle_active; + Gtk::TreeView::Column *_name_column; + void on_link_button_click(); + void on_remove_button_click(); + void on_up_button_click(); + void on_down_button_click(); + void on_active_toggled(const Glib::ustring &item); + +private: + bool _updating = false; + void update(); + void initui(); + bool _visible; + std::vector<sigc::connection> linked_connections; + std::vector<SPObject *> param_get_satellites() override; + SatelliteArrayParam(const SatelliteArrayParam &) = delete; + SatelliteArrayParam &operator=(const SatelliteArrayParam &) = delete; +}; + +} // namespace LivePathEffect + +} // namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/text.cpp b/src/live_effects/parameter/text.cpp new file mode 100644 index 0000000..ea0171d --- /dev/null +++ b/src/live_effects/parameter/text.cpp @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "text.h" + +#include <glibmm/i18n.h> +#include <gtkmm/alignment.h> + +#include <2geom/sbasis-geometric.h> + +#include "inkscape.h" + +#include "display/control/canvas-item-text.h" + +#include "live_effects/effect.h" + +#include "svg/stringstream.h" +#include "svg/svg.h" + +#include "ui/icon-names.h" +#include "ui/widget/registered-widget.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +TextParam::TextParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, const Glib::ustring default_value ) + : Parameter(label, tip, key, wr, effect) + , value(default_value) + , defvalue(default_value) +{ + if (SPDesktop *desktop = SP_ACTIVE_DESKTOP) { // FIXME: we shouldn't use this! + canvas_text = new Inkscape::CanvasItemText(desktop->getCanvasTemp(), Geom::Point(0, 0), default_value); + } +} + +TextParam::~TextParam() +{ + if (canvas_text) { + delete canvas_text; + } +} + +void +TextParam::param_set_default() +{ + param_setValue(defvalue); +} + +void +TextParam::param_update_default(const gchar * default_value) +{ + defvalue = (Glib::ustring)default_value; +} + +// This is a bit silly, we should have an option in the constructor to not create the canvas_text object. +void +TextParam::param_hide_canvas_text() +{ + if (canvas_text) { + delete canvas_text; + canvas_text = nullptr; + } +} + +void +TextParam::setPos(Geom::Point pos) +{ + if (canvas_text) { + canvas_text->set_coord(pos); + } +} + +void +TextParam::setPosAndAnchor(const Geom::Piecewise<Geom::D2<Geom::SBasis> > &pwd2, + const double t, const double length, bool /*use_curvature*/) +{ + using namespace Geom; + + Piecewise<D2<SBasis> > pwd2_reparam = arc_length_parametrization(pwd2, 2 , 0.1); + double t_reparam = pwd2_reparam.cuts.back() * t; + Point pos = pwd2_reparam.valueAt(t_reparam); + Point dir = unit_vector(derivative(pwd2_reparam).valueAt(t_reparam)); + Point n = -rot90(dir); + double angle = Geom::angle_between(dir, Point(1,0)); + if (canvas_text) { + canvas_text->set_coord(pos + n * length); + canvas_text->set_anchor(Geom::Point(std::sin(angle), -std::cos(angle))); + } +} + +void +TextParam::setAnchor(double x_value, double y_value) +{ + anchor_x = x_value; + anchor_y = y_value; + if (canvas_text) { + canvas_text->set_anchor(Geom::Point(anchor_x, anchor_y)); + } +} + +bool +TextParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(strvalue); + return true; +} + +Glib::ustring +TextParam::param_getSVGValue() const +{ + return value; +} + +Glib::ustring +TextParam::param_getDefaultSVGValue() const +{ + return defvalue; +} + +void +TextParam::setTextParam(Inkscape::UI::Widget::RegisteredText *rsu) +{ + Glib::ustring str(rsu->getText()); + param_setValue(str); + write_to_SVG(); +} + +Gtk::Widget * +TextParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredText *rsu = Gtk::manage(new Inkscape::UI::Widget::RegisteredText( + param_label, param_tooltip, param_key, *param_wr, param_effect->getRepr(), param_effect->getSPDoc())); + rsu->setText(value); + rsu->setProgrammatically = false; + rsu->set_undo_parameters(_("Change text parameter"), INKSCAPE_ICON("dialog-path-effects")); + Gtk::Box *text_container = Gtk::manage(new Gtk::Box()); + Gtk::Button *set = Gtk::manage(new Gtk::Button(Glib::ustring("✔"))); + set->signal_clicked() + .connect(sigc::bind<Inkscape::UI::Widget::RegisteredText *>(sigc::mem_fun(*this, &TextParam::setTextParam),rsu)); + text_container->pack_start(*rsu, false, false, 2); + text_container->pack_start(*set, false, false, 2); + Gtk::Widget *return_widg = dynamic_cast<Gtk::Widget *> (text_container); + return_widg->set_halign(Gtk::ALIGN_END); + return return_widg; +} + +void +TextParam::param_setValue(const Glib::ustring newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; + if (canvas_text) { + canvas_text->set_text(newvalue); + } +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/text.h b/src/live_effects/parameter/text.h new file mode 100644 index 0000000..42700ad --- /dev/null +++ b/src/live_effects/parameter/text.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_TEXT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_TEXT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Authors: + * Maximilian Albert + * Johan Engelen + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +class CanvasItemText; + +namespace LivePathEffect { + +class TextParam : public Parameter { +public: + TextParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + const Glib::ustring default_value = ""); + ~TextParam() override; + + Gtk::Widget * param_newWidget() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(Glib::ustring newvalue); + void param_hide_canvas_text(); + void setTextParam(Inkscape::UI::Widget::RegisteredText *rsu); + void param_set_default() override; + void param_update_default(const gchar * default_value) override; + void setPos(Geom::Point pos); + void setPosAndAnchor(const Geom::Piecewise<Geom::D2<Geom::SBasis> > &pwd2, + const double t, const double length, bool use_curvature = false); + void setAnchor(double x_value, double y_value); + + const Glib::ustring get_value() const { return value; }; + ParamType paramType() const override { return ParamType::TEXT; }; + +private: + TextParam(const TextParam&) = delete; + TextParam& operator=(const TextParam&) = delete; + double anchor_x; + double anchor_y; + Glib::ustring value; + Glib::ustring defvalue; + Inkscape::CanvasItemText *canvas_text = nullptr; +}; + +/* + * This parameter does not display a widget in the LPE dialog; LPEs can use it to display on-canvas + * text that should not be settable by the user. Note that since no widget is provided, the + * parameter must be initialized differently than usual (only with a pointer to the parent effect; + * no label, no tooltip, etc.). + */ +class TextParamInternal : public TextParam { +public: + TextParamInternal(Effect* effect) : + TextParam("", "", "", nullptr, effect) {} + + Gtk::Widget * param_newWidget() override { return nullptr; } +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif + +/* + 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 : diff --git a/src/live_effects/parameter/togglebutton.cpp b/src/live_effects/parameter/togglebutton.cpp new file mode 100644 index 0000000..c8a80c8 --- /dev/null +++ b/src/live_effects/parameter/togglebutton.cpp @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl> + * Copyright (C) Jabiertxo Arraiza Cenoz 2014 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "togglebutton.h" + +#include <utility> + +#include <glibmm/i18n.h> + +#include "helper-fns.h" +#include "inkscape.h" +#include "selection.h" + +#include "live_effects/effect.h" + +#include "svg/stringstream.h" +#include "svg/svg.h" + +#include "ui/icon-names.h" +#include "ui/icon-loader.h" +#include "ui/widget/registered-widget.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +ToggleButtonParam::ToggleButtonParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, bool default_value, + Glib::ustring inactive_label, char const *_icon_active, char const *_icon_inactive, + Gtk::BuiltinIconSize _icon_size) + : Parameter(label, tip, key, wr, effect) + , value(default_value) + , defvalue(default_value) + , inactive_label(std::move(inactive_label)) + , _icon_active(_icon_active) + , _icon_inactive(_icon_inactive) + , _icon_size(_icon_size) +{ + checkwdg = nullptr; +} + +ToggleButtonParam::~ToggleButtonParam() +{ + if (_toggled_connection.connected()) { + _toggled_connection.disconnect(); + } +} + +void +ToggleButtonParam::param_set_default() +{ + param_setValue(defvalue); +} + +bool +ToggleButtonParam::param_readSVGValue(const gchar * strvalue) +{ + param_setValue(helperfns_read_bool(strvalue, defvalue)); + return true; // not correct: if value is unacceptable, should return false! +} + +Glib::ustring +ToggleButtonParam::param_getSVGValue() const +{ + return value ? "true" : "false"; +} + +Glib::ustring +ToggleButtonParam::param_getDefaultSVGValue() const +{ + return defvalue ? "true" : "false"; +} + +void +ToggleButtonParam::param_update_default(bool default_value) +{ + defvalue = default_value; +} + +void +ToggleButtonParam::param_update_default(const gchar * default_value) +{ + param_update_default(helperfns_read_bool(default_value, defvalue)); +} + +Gtk::Widget * +ToggleButtonParam::param_newWidget() +{ + if (_toggled_connection.connected()) { + _toggled_connection.disconnect(); + } + + checkwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredToggleButton(param_label, + param_tooltip, + param_key, + *param_wr, + false, + param_effect->getRepr(), + param_effect->getSPDoc()) ); + auto box_button = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL); + box_button->set_homogeneous(false); + Gtk::Label *label = new Gtk::Label(""); + if (!param_label.empty()) { + if (value || inactive_label.empty()) { + label->set_text(param_label.c_str()); + } else { + label->set_text(inactive_label.c_str()); + } + } + label->show(); + if (_icon_active) { + if (!_icon_inactive) { + _icon_inactive = _icon_active; + } + box_button->show(); + Gtk::Widget *icon_button = nullptr; + if (!value) { + icon_button = sp_get_icon_image(_icon_inactive, _icon_size); + } else { + icon_button = sp_get_icon_image(_icon_active, _icon_size); + } + icon_button->show(); + box_button->pack_start(*icon_button, false, false, 1); + if (!param_label.empty()) { + box_button->pack_start(*label, false, false, 1); + } + } else { + box_button->pack_start(*label, false, false, 1); + } + + checkwdg->add(*Gtk::manage(box_button)); + checkwdg->setActive(value); + checkwdg->setProgrammatically = false; + checkwdg->set_undo_parameters(_("Change togglebutton parameter"), INKSCAPE_ICON("dialog-path-effects")); + + _toggled_connection = checkwdg->signal_toggled().connect(sigc::mem_fun(*this, &ToggleButtonParam::toggled)); + return checkwdg; +} + +void +ToggleButtonParam::refresh_button() +{ + if (!_toggled_connection.connected()) { + return; + } + + if(!checkwdg){ + return; + } + Gtk::Container *box_button = dynamic_cast<Gtk::Container *>(checkwdg->get_child()); + if(!box_button){ + return; + } + std::vector<Gtk::Widget *> children = box_button->get_children(); + if (!param_label.empty()) { + Gtk::Label *lab = dynamic_cast<Gtk::Label*>(children[children.size()-1]); + if (!lab) return; + if(value || inactive_label.empty()){ + lab->set_text(param_label.c_str()); + }else{ + lab->set_text(inactive_label.c_str()); + } + } + if ( _icon_active ) { + Gtk::Widget *im = dynamic_cast<Gtk::Image *>(children[0]); + if (!im) return; + if (!value) { + im = sp_get_icon_image(_icon_inactive, _icon_size); + } else { + im = sp_get_icon_image(_icon_active, _icon_size); + } + } +} + +void +ToggleButtonParam::param_setValue(bool newvalue) +{ + if (value != newvalue) { + param_effect->refresh_widgets = true; + } + value = newvalue; + refresh_button(); +} + +void +ToggleButtonParam::toggled() { + if (SP_ACTIVE_DESKTOP) { + Inkscape::Selection *selection = SP_ACTIVE_DESKTOP->getSelection(); + selection->emitModified(); + } + _signal_toggled.emit(); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/togglebutton.h b/src/live_effects/parameter/togglebutton.h new file mode 100644 index 0000000..205598a --- /dev/null +++ b/src/live_effects/parameter/togglebutton.h @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_TOGGLEBUTTON_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_TOGGLEBUTTON_H + +/* + * Copyright (C) Jabiertxo Arraiza Cenoz 2014 + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <sigc++/connection.h> +#include <sigc++/signal.h> + +#include "live_effects/parameter/parameter.h" +#include "ui/widget/registered-widget.h" + +namespace Inkscape { + +namespace LivePathEffect { + +/** + * class ToggleButtonParam: + * represents a Gtk::ToggleButton as a Live Path Effect parameter + */ +class ToggleButtonParam : public Parameter { +public: + ToggleButtonParam(const Glib::ustring &label, const Glib::ustring &tip, const Glib::ustring &key, + Inkscape::UI::Widget::Registry *wr, Effect *effect, bool default_value = false, + Glib::ustring inactive_label = "", char const *icon_active = nullptr, + char const *icon_inactive = nullptr, Gtk::BuiltinIconSize icon_size = Gtk::ICON_SIZE_SMALL_TOOLBAR); + ~ToggleButtonParam() override; + ToggleButtonParam(const ToggleButtonParam &) = delete; + ToggleButtonParam &operator=(const ToggleButtonParam &) = delete; + + Gtk::Widget *param_newWidget() override; + + bool param_readSVGValue(const gchar *strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + void param_setValue(bool newvalue); + void param_set_default() override; + + bool get_value() const { return value; }; + + inline operator bool() const { return value; }; + + sigc::signal<void> &signal_toggled() { return _signal_toggled; } + virtual void toggled(); + void param_update_default(bool default_value); + void param_update_default(const gchar *default_value) override; + ParamType paramType() const override { return ParamType::TOGGLE_BUTTON; }; +private: + void refresh_button(); + bool value; + bool defvalue; + const Glib::ustring inactive_label; + const char * _icon_active; + const char * _icon_inactive; + Gtk::BuiltinIconSize _icon_size; + Inkscape::UI::Widget::RegisteredToggleButton * checkwdg; + + sigc::signal<void> _signal_toggled; + sigc::connection _toggled_connection; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/transformedpoint.cpp b/src/live_effects/parameter/transformedpoint.cpp new file mode 100644 index 0000000..9b2b41f --- /dev/null +++ b/src/live_effects/parameter/transformedpoint.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "transformedpoint.h" + +#include <glibmm/i18n.h> + +#include "desktop.h" + +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "ui/icon-names.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" +#include "ui/widget/registered-widget.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +TransformedPointParam::TransformedPointParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, Geom::Point default_vector, + bool dontTransform) + : Parameter(label, tip, key, wr, effect), + defvalue(default_vector), + vector(default_vector), + noTransform(dontTransform) +{ +} + +TransformedPointParam::~TransformedPointParam() += default; + +void +TransformedPointParam::param_set_default() +{ + setOrigin(Geom::Point(0.,0.)); + setVector(defvalue); +} + +bool +TransformedPointParam::param_readSVGValue(const gchar * strvalue) +{ + gchar ** strarray = g_strsplit(strvalue, ",", 4); + if (!strarray) { + return false; + } + double val[4]; + unsigned int i = 0; + while (i < 4 && strarray[i]) { + if (sp_svg_number_read_d(strarray[i], &val[i]) != 0) { + i++; + } else { + break; + } + } + g_strfreev (strarray); + if (i == 4) { + setOrigin( Geom::Point(val[0], val[1]) ); + setVector( Geom::Point(val[2], val[3]) ); + return true; + } + return false; +} + +Glib::ustring +TransformedPointParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << origin << " , " << vector; + return os.str(); +} + +Glib::ustring +TransformedPointParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +void +TransformedPointParam::param_update_default(Geom::Point default_point) +{ + defvalue = default_point; +} + +void +TransformedPointParam::param_update_default(const gchar * default_point) +{ + gchar ** strarray = g_strsplit(default_point, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_update_default( Geom::Point(newx, newy) ); + } +} + +Gtk::Widget * +TransformedPointParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredVector * pointwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredVector( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + pointwdg->setPolarCoords(); + pointwdg->setValue( vector, origin ); + pointwdg->clearProgrammatically(); + pointwdg->set_undo_parameters(_("Change vector parameter"), INKSCAPE_ICON("dialog-path-effects")); + + Gtk::Box * hbox = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) ); + hbox->pack_start(*pointwdg, true, true); + hbox->show_all_children(); + + return dynamic_cast<Gtk::Widget *> (hbox); +} + +void +TransformedPointParam::set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector) +{ + setValues(new_origin, new_vector); + param_write_to_repr(param_getSVGValue().c_str()); +} + +void +TransformedPointParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + if (!noTransform) { + set_and_write_new_values( origin * postmul, vector * postmul.withoutTranslation() ); + } +} + + +void +TransformedPointParam::set_vector_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color) +{ + vec_knot_shape = shape; + vec_knot_mode = mode; + vec_knot_color = color; +} + +void +TransformedPointParam::set_oncanvas_color(guint32 color) +{ + vec_knot_color = color; +} + +class TransformedPointParamKnotHolderEntity_Vector : public KnotHolderEntity { +public: + TransformedPointParamKnotHolderEntity_Vector(TransformedPointParam *p) : param(p) { } + ~TransformedPointParamKnotHolderEntity_Vector() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint /*state*/) override { + Geom::Point const s = p - param->origin; + /// @todo implement angle snapping when holding CTRL + param->setVector(s); + param->set_and_write_new_values(param->origin, param->vector); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + }; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override + { + param->param_effect->refresh_widgets = true; + param->write_to_SVG(); + }; + Geom::Point knot_get() const override{ + return param->origin + param->vector; + }; + void knot_click(guint /*state*/) override{ + g_print ("This is the vector handle associated to parameter '%s'\n", param->param_key.c_str()); + }; + +private: + TransformedPointParam *param; +}; + +void +TransformedPointParam::addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item) +{ + TransformedPointParamKnotHolderEntity_Vector *vector_e = new TransformedPointParamKnotHolderEntity_Vector(this); + vector_e->create(desktop, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:Point", handleTip(), + vec_knot_color); + knotholder->add(vector_e); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/transformedpoint.h b/src/live_effects/parameter/transformedpoint.h new file mode 100644 index 0000000..34c07bc --- /dev/null +++ b/src/live_effects/parameter/transformedpoint.h @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_TRANSFORMED_POINT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_TRANSFORMED_POINT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Theodore Janeczko 2012 <flutterguy317@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <2geom/point.h> + +#include "live_effects/parameter/parameter.h" +#include "display/control/canvas-item-enums.h" + +namespace Inkscape { + +namespace LivePathEffect { + + +class TransformedPointParam : public Parameter { +public: + TransformedPointParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + Geom::Point default_vector = Geom::Point(1,0), + bool dontTransform = false); + ~TransformedPointParam() override; + + Gtk::Widget * param_newWidget() override; + inline const gchar *handleTip() const { return param_tooltip.c_str(); } + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + Geom::Point getVector() const { return vector; }; + Geom::Point getOrigin() const { return origin; }; + void setValues(Geom::Point const &new_origin, Geom::Point const &new_vector) { setVector(new_vector); setOrigin(new_origin); }; + void setVector(Geom::Point const &new_vector) { vector = new_vector; }; + void setOrigin(Geom::Point const &new_origin) { origin = new_origin; }; + void param_set_default() override; + + void set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector); + + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + + void set_vector_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color); + + void set_oncanvas_color(guint32 color); + Geom::Point param_get_default() { return defvalue; } + void param_update_default(Geom::Point default_point); + void param_update_default(const gchar * default_point) override; + bool providesKnotHolderEntities() const override { return true; } + virtual void addKnotHolderEntities(KnotHolder *knotholder, SPDesktop *desktop, SPItem *item); + ParamType paramType() const override { return ParamType::TRANSFORMED_POINT; }; +private: + TransformedPointParam(const TransformedPointParam&) = delete; + TransformedPointParam& operator=(const TransformedPointParam&) = delete; + + Geom::Point defvalue; + + Geom::Point origin; + Geom::Point vector; + + bool noTransform; + + /// The looks of the vector and origin knots oncanvas + Inkscape::CanvasItemCtrlShape vec_knot_shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND; + Inkscape::CanvasItemCtrlMode vec_knot_mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR; + guint32 vec_knot_color = 0xffffb500; + + friend class TransformedPointParamKnotHolderEntity_Vector; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/unit.cpp b/src/live_effects/parameter/unit.cpp new file mode 100644 index 0000000..7be0fc2 --- /dev/null +++ b/src/live_effects/parameter/unit.cpp @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "unit.h" + +#include <glibmm/i18n.h> + +#include "live_effects/effect.h" +#include "ui/icon-names.h" +#include "ui/widget/registered-widget.h" +#include "util/units.h" + +using Inkscape::Util::unit_table; + +namespace Inkscape { + +namespace LivePathEffect { + + +UnitParam::UnitParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, Glib::ustring default_unit) + : Parameter(label, tip, key, wr, effect) +{ + defunit = unit_table.getUnit(default_unit); + unit = defunit; +} + +UnitParam::~UnitParam() += default; + +bool +UnitParam::param_readSVGValue(const gchar * strvalue) +{ + if (strvalue) { + param_set_value(*unit_table.getUnit(strvalue)); + return true; + } + return false; +} + +Glib::ustring +UnitParam::param_getSVGValue() const +{ + return unit->abbr; +} + +Glib::ustring +UnitParam::param_getDefaultSVGValue() const +{ + return defunit->abbr; +} + +void +UnitParam::param_set_default() +{ + param_set_value(*defunit); +} + +void +UnitParam::param_update_default(const gchar * default_unit) +{ + defunit = unit_table.getUnit((Glib::ustring)default_unit); +} + +void +UnitParam::param_set_value(Inkscape::Util::Unit const &val) +{ + param_effect->refresh_widgets = true; + unit = new Inkscape::Util::Unit(val); +} + +const gchar * +UnitParam::get_abbreviation() const +{ + return unit->abbr.c_str(); +} + +Gtk::Widget * +UnitParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredUnitMenu* unit_menu = Gtk::manage( + new Inkscape::UI::Widget::RegisteredUnitMenu(param_label, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc())); + + unit_menu->setUnit(unit->abbr); + unit_menu->set_undo_parameters(_("Change unit parameter"), INKSCAPE_ICON("dialog-path-effects")); + + return dynamic_cast<Gtk::Widget *> (unit_menu); +} + +} /* namespace LivePathEffect */ +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/unit.h b/src/live_effects/parameter/unit.h new file mode 100644 index 0000000..0c407d5 --- /dev/null +++ b/src/live_effects/parameter/unit.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_UNIT_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_UNIT_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Maximilian Albert 2008 <maximilian.albert@gmail.com> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "live_effects/parameter/parameter.h" + +namespace Inkscape { + +namespace Util { + class Unit; +} + +namespace LivePathEffect { + +class UnitParam : public Parameter { +public: + UnitParam(const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + Glib::ustring default_unit = "px"); + ~UnitParam() override; + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + void param_set_default() override; + void param_set_value(Inkscape::Util::Unit const &val); + void param_update_default(const gchar * default_unit) override; + const gchar *get_abbreviation() const; + Gtk::Widget * param_newWidget() override; + + operator Inkscape::Util::Unit const *() const { return unit; } + ParamType paramType() const override { return ParamType::UNIT; }; +private: + Inkscape::Util::Unit const *unit; + Inkscape::Util::Unit const *defunit; + + UnitParam(const UnitParam&) = delete; + UnitParam& operator=(const UnitParam&) = delete; +}; + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/parameter/vector.cpp b/src/live_effects/parameter/vector.cpp new file mode 100644 index 0000000..16b83fb --- /dev/null +++ b/src/live_effects/parameter/vector.cpp @@ -0,0 +1,250 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "vector.h" + +#include <glibmm/i18n.h> + +#include "live_effects/effect.h" +#include "svg/svg.h" +#include "svg/stringstream.h" +#include "ui/icon-names.h" +#include "ui/knot/knot-holder.h" +#include "ui/knot/knot-holder-entity.h" +#include "ui/widget/registered-widget.h" + + +namespace Inkscape { + +namespace LivePathEffect { + +VectorParam::VectorParam( const Glib::ustring& label, const Glib::ustring& tip, + const Glib::ustring& key, Inkscape::UI::Widget::Registry* wr, + Effect* effect, Geom::Point default_vector) + : Parameter(label, tip, key, wr, effect), + defvalue(default_vector), + origin(0.,0.), + vector(default_vector) +{ +} + +VectorParam::~VectorParam() += default; + +void +VectorParam::param_set_default() +{ + setOrigin(Geom::Point(0.,0.)); + setVector(defvalue); +} + +void +VectorParam::param_update_default(Geom::Point default_point) +{ + defvalue = default_point; +} + +void +VectorParam::param_update_default(const gchar * default_point) +{ + gchar ** strarray = g_strsplit(default_point, ",", 2); + double newx, newy; + unsigned int success = sp_svg_number_read_d(strarray[0], &newx); + success += sp_svg_number_read_d(strarray[1], &newy); + g_strfreev (strarray); + if (success == 2) { + param_update_default( Geom::Point(newx, newy) ); + } +} + +bool +VectorParam::param_readSVGValue(const gchar * strvalue) +{ + gchar ** strarray = g_strsplit(strvalue, ",", 4); + if (!strarray) { + return false; + } + double val[4]; + unsigned int i = 0; + while (i < 4 && strarray[i]) { + if (sp_svg_number_read_d(strarray[i], &val[i]) != 0) { + i++; + } else { + break; + } + } + g_strfreev (strarray); + if (i == 4) { + setOrigin( Geom::Point(val[0], val[1]) ); + setVector( Geom::Point(val[2], val[3]) ); + return true; + } + return false; +} + +Glib::ustring +VectorParam::param_getSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << origin << " , " << vector; + return os.str(); +} + +Glib::ustring +VectorParam::param_getDefaultSVGValue() const +{ + Inkscape::SVGOStringStream os; + os << defvalue; + return os.str(); +} + +Gtk::Widget * +VectorParam::param_newWidget() +{ + Inkscape::UI::Widget::RegisteredVector * pointwdg = Gtk::manage( + new Inkscape::UI::Widget::RegisteredVector( param_label, + param_tooltip, + param_key, + *param_wr, + param_effect->getRepr(), + param_effect->getSPDoc() ) ); + pointwdg->setPolarCoords(); + pointwdg->setValue( vector, origin ); + pointwdg->clearProgrammatically(); + pointwdg->set_undo_parameters(_("Change vector parameter"), INKSCAPE_ICON("dialog-path-effects")); + + Gtk::Box * hbox = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL) ); + hbox->pack_start(*pointwdg, true, true); + hbox->show_all_children(); + + return dynamic_cast<Gtk::Widget *> (hbox); +} + +void +VectorParam::set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector) +{ + setValues(new_origin, new_vector); + param_write_to_repr(param_getSVGValue().c_str()); +} + +void +VectorParam::param_transform_multiply(Geom::Affine const& postmul, bool /*set*/) +{ + set_and_write_new_values( origin * postmul, vector * postmul.withoutTranslation() ); +} + + +void +VectorParam::set_vector_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color) +{ + vec_knot_shape = shape; + vec_knot_mode = mode; + vec_knot_color = color; +} + +void +VectorParam::set_origin_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color) +{ + ori_knot_shape = shape; + ori_knot_mode = mode; + ori_knot_color = color; +} + +void +VectorParam::set_oncanvas_color(guint32 color) +{ + vec_knot_color = color; + ori_knot_color = color; +} + +class VectorParamKnotHolderEntity_Origin : public KnotHolderEntity { +public: + VectorParamKnotHolderEntity_Origin(VectorParam *p) : param(p) { } + ~VectorParamKnotHolderEntity_Origin() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint state) override { + Geom::Point const s = snap_knot_position(p, state); + param->setOrigin(s); + param->set_and_write_new_values(param->origin, param->vector); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + }; + Geom::Point knot_get() const override { + return param->origin; + }; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override + { + param->param_effect->refresh_widgets = true; + param->write_to_SVG(); + }; + void knot_click(guint /*state*/) override{ + g_print ("This is the origin handle associated to parameter '%s'\n", param->param_key.c_str()); + }; + +private: + VectorParam *param; +}; + +class VectorParamKnotHolderEntity_Vector : public KnotHolderEntity { +public: + VectorParamKnotHolderEntity_Vector(VectorParam *p) : param(p) { } + ~VectorParamKnotHolderEntity_Vector() override = default; + + void knot_set(Geom::Point const &p, Geom::Point const &/*origin*/, guint /*state*/) override { + Geom::Point const s = p - param->origin; + /// @todo implement angle snapping when holding CTRL + param->setVector(s); + param->set_and_write_new_values(param->origin, param->vector); + sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, false); + }; + Geom::Point knot_get() const override { + return param->origin + param->vector; + }; + void knot_ungrabbed(Geom::Point const &p, Geom::Point const &origin, guint state) override + { + param->param_effect->refresh_widgets = true; + param->write_to_SVG(); + }; + void knot_click(guint /*state*/) override{ + g_print ("This is the vector handle associated to parameter '%s'\n", param->param_key.c_str()); + }; + +private: + VectorParam *param; +}; + +void +VectorParam::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) +{ + VectorParamKnotHolderEntity_Origin *origin_e = new VectorParamKnotHolderEntity_Origin(this); + origin_e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:Origin", + handleTip(), ori_knot_color); + knotholder->add(origin_e); + + VectorParamKnotHolderEntity_Vector *vector_e = new VectorParamKnotHolderEntity_Vector(this); + vector_e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:Vector", + handleTip(), vec_knot_color); + knotholder->add(vector_e); +} + +} /* namespace LivePathEffect */ + +} /* namespace Inkscape */ + +/* + 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 : diff --git a/src/live_effects/parameter/vector.h b/src/live_effects/parameter/vector.h new file mode 100644 index 0000000..65e7355 --- /dev/null +++ b/src/live_effects/parameter/vector.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_LIVEPATHEFFECT_PARAMETER_VECTOR_H +#define INKSCAPE_LIVEPATHEFFECT_PARAMETER_VECTOR_H + +/* + * Inkscape::LivePathEffectParameters + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> +#include <2geom/point.h> + +#include "live_effects/parameter/parameter.h" +#include "display/control/canvas-item-enums.h" + +namespace Inkscape { + +namespace LivePathEffect { + + +class VectorParam : public Parameter { +public: + VectorParam( const Glib::ustring& label, + const Glib::ustring& tip, + const Glib::ustring& key, + Inkscape::UI::Widget::Registry* wr, + Effect* effect, + Geom::Point default_vector = Geom::Point(1,0) ); + ~VectorParam() override; + + Gtk::Widget * param_newWidget() override; + inline const gchar *handleTip() const { return param_tooltip.c_str(); } + + bool param_readSVGValue(const gchar * strvalue) override; + Glib::ustring param_getSVGValue() const override; + Glib::ustring param_getDefaultSVGValue() const override; + + Geom::Point getVector() const { return vector; }; + Geom::Point getOrigin() const { return origin; }; + void setValues(Geom::Point const &new_origin, Geom::Point const &new_vector) { setVector(new_vector); setOrigin(new_origin); }; + void setVector(Geom::Point const &new_vector) { vector = new_vector; }; + void setOrigin(Geom::Point const &new_origin) { origin = new_origin; }; + void param_set_default() override; + + void set_and_write_new_values(Geom::Point const &new_origin, Geom::Point const &new_vector); + + void param_transform_multiply(Geom::Affine const &postmul, bool set) override; + + void set_vector_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color); + void set_origin_oncanvas_looks(Inkscape::CanvasItemCtrlShape shape, + Inkscape::CanvasItemCtrlMode mode, + guint32 color); + void set_oncanvas_color(guint32 color); + void param_update_default(Geom::Point default_point); + void param_update_default(const gchar * default_point) override; + bool providesKnotHolderEntities() const override { return true; } + void addKnotHolderEntities(KnotHolder *knotholder, SPItem *item) override; + ParamType paramType() const override { return ParamType::VECTOR; }; +private: + VectorParam(const VectorParam&) = delete; + VectorParam& operator=(const VectorParam&) = delete; + + Geom::Point defvalue; + + Geom::Point origin; + Geom::Point vector; + + /// The looks of the vector and origin knots oncanvas + Inkscape::CanvasItemCtrlShape vec_knot_shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND; + Inkscape::CanvasItemCtrlMode vec_knot_mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR;; + guint32 vec_knot_color = 0xffffb500; + Inkscape::CanvasItemCtrlShape ori_knot_shape = Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE; + Inkscape::CanvasItemCtrlMode ori_knot_mode = Inkscape::CANVAS_ITEM_CTRL_MODE_XOR;; + guint32 ori_knot_color = 0xffffb500; + + friend class VectorParamKnotHolderEntity_Origin; + friend class VectorParamKnotHolderEntity_Vector; +}; + + +} //namespace LivePathEffect + +} //namespace Inkscape + +#endif diff --git a/src/live_effects/spiro-converters.cpp b/src/live_effects/spiro-converters.cpp new file mode 100644 index 0000000..411fb65 --- /dev/null +++ b/src/live_effects/spiro-converters.cpp @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Johan Engelen + * + * Copyright (C) 2010-2012 Authors + * + * Released under GNU GPL, read the file 'COPYING' for more information + */ + +#include "spiro-converters.h" +#include <2geom/path.h> +#include "display/curve.h" +#include <glib.h> + +#define SPIRO_SHOW_INFINITE_COORDINATE_CALLS +#ifdef SPIRO_SHOW_INFINITE_COORDINATE_CALLS +# define SPIRO_G_MESSAGE(x) g_message(x) +#else +# define SPIRO_G_MESSAGE(x) +#endif + +namespace Spiro { + +void +ConverterSPCurve::moveto(double x, double y) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _curve.moveto(x, y); + } else { + SPIRO_G_MESSAGE("Spiro: moveto not finite"); + } +} + +void +ConverterSPCurve::lineto(double x, double y, bool close_last) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _curve.lineto(x, y); + if (close_last) { + _curve.closepath(); + } + } else { + SPIRO_G_MESSAGE("Spiro: lineto not finite"); + } +} + +void +ConverterSPCurve::quadto(double xm, double ym, double x3, double y3, bool close_last) +{ + if ( std::isfinite(xm) && std::isfinite(ym) && std::isfinite(x3) && std::isfinite(y3) ) { + _curve.quadto(xm, ym, x3, y3); + if (close_last) { + _curve.closepath(); + } + } else { + SPIRO_G_MESSAGE("Spiro: quadto not finite"); + } +} + +void +ConverterSPCurve::curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) +{ + if ( std::isfinite(x1) && std::isfinite(y1) && std::isfinite(x2) && std::isfinite(y2) ) { + _curve.curveto(x1, y1, x2, y2, x3, y3); + if (close_last) { + _curve.closepath(); + } + } else { + SPIRO_G_MESSAGE("Spiro: curveto not finite"); + } +} + + +ConverterPath::ConverterPath(Geom::Path &path) + : _path(path) +{ + _path.setStitching(true); +} + +void +ConverterPath::moveto(double x, double y) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _path.start(Geom::Point(x, y)); + } else { + SPIRO_G_MESSAGE("spiro moveto not finite"); + } +} + +void +ConverterPath::lineto(double x, double y, bool close_last) +{ + if ( std::isfinite(x) && std::isfinite(y) ) { + _path.appendNew<Geom::LineSegment>( Geom::Point(x, y) ); + _path.close(close_last); + } else { + SPIRO_G_MESSAGE("spiro lineto not finite"); + } +} + +void +ConverterPath::quadto(double xm, double ym, double x3, double y3, bool close_last) +{ + if ( std::isfinite(xm) && std::isfinite(ym) && std::isfinite(x3) && std::isfinite(y3) ) { + _path.appendNew<Geom::QuadraticBezier>(Geom::Point(xm, ym), Geom::Point(x3, y3)); + _path.close(close_last); + } else { + SPIRO_G_MESSAGE("spiro quadto not finite"); + } +} + +void +ConverterPath::curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) +{ + if ( std::isfinite(x1) && std::isfinite(y1) && std::isfinite(x2) && std::isfinite(y2) ) { + _path.appendNew<Geom::CubicBezier>(Geom::Point(x1, y1), Geom::Point(x2, y2), Geom::Point(x3, y3)); + _path.close(close_last); + } else { + SPIRO_G_MESSAGE("spiro curveto not finite"); + } +} + +} // namespace Spiro + + + +/* + 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 : diff --git a/src/live_effects/spiro-converters.h b/src/live_effects/spiro-converters.h new file mode 100644 index 0000000..98041a2 --- /dev/null +++ b/src/live_effects/spiro-converters.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SPIRO_CONVERTERS_H +#define INKSCAPE_SPIRO_CONVERTERS_H + +#include <2geom/forward.h> +class SPCurve; + +namespace Spiro { + +class ConverterBase { +public: + ConverterBase() = default;; + virtual ~ConverterBase() = default;; + + virtual void moveto(double x, double y) = 0; + virtual void lineto(double x, double y, bool close_last) = 0; + virtual void quadto(double x1, double y1, double x2, double y2, bool close_last) = 0; + virtual void curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) = 0; +}; + + +/** + * Converts Spiro to Inkscape's SPCurve + */ +class ConverterSPCurve : public ConverterBase { +public: + ConverterSPCurve(SPCurve &curve) + : _curve(curve) + {} + + void moveto(double x, double y) override; + void lineto(double x, double y, bool close_last) override; + void quadto(double x1, double y1, double x2, double y2, bool close_last) override; + void curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) override; + +private: + SPCurve &_curve; + + ConverterSPCurve(const ConverterSPCurve&) = delete; + ConverterSPCurve& operator=(const ConverterSPCurve&) = delete; +}; + + +/** + * Converts Spiro to 2Geom's Path + */ +class ConverterPath : public ConverterBase { +public: + ConverterPath(Geom::Path &path); + + void moveto(double x, double y) override; + void lineto(double x, double y, bool close_last) override; + void quadto(double x1, double y1, double x2, double y2, bool close_last) override; + void curveto(double x1, double y1, double x2, double y2, double x3, double y3, bool close_last) override; + +private: + Geom::Path &_path; + + ConverterPath(const ConverterPath&) = delete; + ConverterPath& operator=(const ConverterPath&) = delete; +}; + + +} // namespace Spiro + +#endif diff --git a/src/live_effects/spiro.cpp b/src/live_effects/spiro.cpp new file mode 100644 index 0000000..4c129ba --- /dev/null +++ b/src/live_effects/spiro.cpp @@ -0,0 +1,1122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * C implementation of third-order polynomial spirals. + *//* + * Authors: see git history + * Raph Levien + * Johan Engelen + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "spiro.h" + +#include <cmath> +#include <cstdlib> +#include <cstring> + +#include "display/curve.h" + +#define SPIRO_SHOW_INFINITE_COORDINATE_CALLS + +namespace Spiro { + +void spiro_run(const spiro_cp *src, int src_len, SPCurve &curve) +{ + spiro_seg *s = Spiro::run_spiro(src, src_len); + Spiro::ConverterSPCurve bc(curve); + Spiro::spiro_to_otherpath(s, src_len, bc); + free(s); +} + +void spiro_run(const spiro_cp *src, int src_len, Geom::Path &path) +{ + spiro_seg *s = Spiro::run_spiro(src, src_len); + Spiro::ConverterPath bc(path); + Spiro::spiro_to_otherpath(s, src_len, bc); + free(s); +} + + +/************************************ + * Spiro math + */ + +struct spiro_seg_s { + double x; + double y; + char ty; + double bend_th; + double ks[4]; + double seg_ch; + double seg_th; + double l; +}; + +struct bandmat { + double a[11]; /* band-diagonal matrix */ + double al[5]; /* lower part of band-diagonal decomposition */ +}; + +#ifndef M_PI +#define M_PI 3.14159265358979323846 /* pi */ +#endif + +int n = 4; + +#ifndef ORDER +#define ORDER 12 +#endif + +/* Integrate polynomial spiral curve over range -.5 .. .5. */ +static void +integrate_spiro(const double ks[4], double xy[2]) +{ +#if 0 + int n = 1024; +#endif + double th1 = ks[0]; + double th2 = .5 * ks[1]; + double th3 = (1./6) * ks[2]; + double th4 = (1./24) * ks[3]; + double x, y; + double ds = 1. / n; + double ds2 = ds * ds; + double ds3 = ds2 * ds; + double k0 = ks[0] * ds; + double k1 = ks[1] * ds; + double k2 = ks[2] * ds; + double k3 = ks[3] * ds; + int i; + double s = .5 * ds - .5; + + x = 0; + y = 0; + + for (i = 0; i < n; i++) { + +#if ORDER > 2 + double u, v; + double km0, km1, km2, km3; + + if (n == 1) { + km0 = k0; + km1 = k1 * ds; + km2 = k2 * ds2; + } else { + km0 = (((1./6) * k3 * s + .5 * k2) * s + k1) * s + k0; + km1 = ((.5 * k3 * s + k2) * s + k1) * ds; + km2 = (k3 * s + k2) * ds2; + } + km3 = k3 * ds3; +#endif + + { + +#if ORDER == 4 + double km0_2 = km0 * km0; + u = 24 - km0_2; + v = km1; +#endif + +#if ORDER == 6 + double km0_2 = km0 * km0; + double km0_4 = km0_2 * km0_2; + u = 24 - km0_2 + (km0_4 - 4 * km0 * km2 - 3 * km1 * km1) * (1./80); + v = km1 + (km3 - 6 * km0_2 * km1) * (1./80); +#endif + +#if ORDER == 8 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t6_6 = t4_4 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6; + v -= (1./480) * t3_4 + (1./2688) * t3_6; + u += (1./1920) * t4_4 + (1./10752) * t4_6; + v += (1./53760) * t5_6; + u -= (1./322560) * t6_6; +#endif + +#if ORDER == 10 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t8_8 = t6_6 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6 + (1./4608) * t2_8; + v -= (1./480) * t3_4 + (1./2688) * t3_6 + (1./13824) * t3_8; + u += (1./1920) * t4_4 + (1./10752) * t4_6 + (1./55296) * t4_8; + v += (1./53760) * t5_6 + (1./276480) * t5_8; + u -= (1./322560) * t6_6 + (1./1.65888e+06) * t6_8; + v -= (1./1.16122e+07) * t7_8; + u += (1./9.28973e+07) * t8_8; +#endif + +#if ORDER == 12 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t3_10 = t2_6 * t1_4 + t2_7 * t1_3 + t2_8 * t1_2; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t4_9 = 2 * (t2_2 * t2_7 + t2_3 * t2_6 + t2_4 * t2_5); + double t4_10 = 2 * (t2_2 * t2_8 + t2_3 * t2_7 + t2_4 * t2_6) + t2_5 * t2_5; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t5_10 = t4_6 * t1_4 + t4_7 * t1_3 + t4_8 * t1_2 + t4_9 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t6_9 = t4_4 * t2_5 + t4_5 * t2_4 + t4_6 * t2_3 + t4_7 * t2_2; + double t6_10 = t4_4 * t2_6 + t4_5 * t2_5 + t4_6 * t2_4 + t4_7 * t2_3 + t4_8 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t7_10 = t6_6 * t1_4 + t6_7 * t1_3 + t6_8 * t1_2 + t6_9 * t1_1; + double t8_8 = t6_6 * t2_2; + double t8_9 = t6_6 * t2_3 + t6_7 * t2_2; + double t8_10 = t6_6 * t2_4 + t6_7 * t2_3 + t6_8 * t2_2; + double t9_10 = t8_8 * t1_2 + t8_9 * t1_1; + double t10_10 = t8_8 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6 + (1./4608) * t2_8; + v -= (1./480) * t3_4 + (1./2688) * t3_6 + (1./13824) * t3_8 + (1./67584) * t3_10; + u += (1./1920) * t4_4 + (1./10752) * t4_6 + (1./55296) * t4_8 + (1./270336) * t4_10; + v += (1./53760) * t5_6 + (1./276480) * t5_8 + (1./1.35168e+06) * t5_10; + u -= (1./322560) * t6_6 + (1./1.65888e+06) * t6_8 + (1./8.11008e+06) * t6_10; + v -= (1./1.16122e+07) * t7_8 + (1./5.67706e+07) * t7_10; + u += (1./9.28973e+07) * t8_8 + (1./4.54164e+08) * t8_10; + v += (1./4.08748e+09) * t9_10; + u -= (1./4.08748e+10) * t10_10; +#endif + +#if ORDER == 14 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t3_10 = t2_6 * t1_4 + t2_7 * t1_3 + t2_8 * t1_2; + double t3_12 = t2_8 * t1_4; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t4_9 = 2 * (t2_2 * t2_7 + t2_3 * t2_6 + t2_4 * t2_5); + double t4_10 = 2 * (t2_2 * t2_8 + t2_3 * t2_7 + t2_4 * t2_6) + t2_5 * t2_5; + double t4_11 = 2 * (t2_3 * t2_8 + t2_4 * t2_7 + t2_5 * t2_6); + double t4_12 = 2 * (t2_4 * t2_8 + t2_5 * t2_7) + t2_6 * t2_6; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t5_10 = t4_6 * t1_4 + t4_7 * t1_3 + t4_8 * t1_2 + t4_9 * t1_1; + double t5_12 = t4_8 * t1_4 + t4_9 * t1_3 + t4_10 * t1_2 + t4_11 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t6_9 = t4_4 * t2_5 + t4_5 * t2_4 + t4_6 * t2_3 + t4_7 * t2_2; + double t6_10 = t4_4 * t2_6 + t4_5 * t2_5 + t4_6 * t2_4 + t4_7 * t2_3 + t4_8 * t2_2; + double t6_11 = t4_4 * t2_7 + t4_5 * t2_6 + t4_6 * t2_5 + t4_7 * t2_4 + t4_8 * t2_3 + t4_9 * t2_2; + double t6_12 = t4_4 * t2_8 + t4_5 * t2_7 + t4_6 * t2_6 + t4_7 * t2_5 + t4_8 * t2_4 + t4_9 * t2_3 + t4_10 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t7_10 = t6_6 * t1_4 + t6_7 * t1_3 + t6_8 * t1_2 + t6_9 * t1_1; + double t7_12 = t6_8 * t1_4 + t6_9 * t1_3 + t6_10 * t1_2 + t6_11 * t1_1; + double t8_8 = t6_6 * t2_2; + double t8_9 = t6_6 * t2_3 + t6_7 * t2_2; + double t8_10 = t6_6 * t2_4 + t6_7 * t2_3 + t6_8 * t2_2; + double t8_11 = t6_6 * t2_5 + t6_7 * t2_4 + t6_8 * t2_3 + t6_9 * t2_2; + double t8_12 = t6_6 * t2_6 + t6_7 * t2_5 + t6_8 * t2_4 + t6_9 * t2_3 + t6_10 * t2_2; + double t9_10 = t8_8 * t1_2 + t8_9 * t1_1; + double t9_12 = t8_8 * t1_4 + t8_9 * t1_3 + t8_10 * t1_2 + t8_11 * t1_1; + double t10_10 = t8_8 * t2_2; + double t10_11 = t8_8 * t2_3 + t8_9 * t2_2; + double t10_12 = t8_8 * t2_4 + t8_9 * t2_3 + t8_10 * t2_2; + double t11_12 = t10_10 * t1_2 + t10_11 * t1_1; + double t12_12 = t10_10 * t2_2; + u = 1; + v = 0; + v += (1./12) * t1_2 + (1./80) * t1_4; + u -= (1./24) * t2_2 + (1./160) * t2_4 + (1./896) * t2_6 + (1./4608) * t2_8; + v -= (1./480) * t3_4 + (1./2688) * t3_6 + (1./13824) * t3_8 + (1./67584) * t3_10 + (1./319488) * t3_12; + u += (1./1920) * t4_4 + (1./10752) * t4_6 + (1./55296) * t4_8 + (1./270336) * t4_10 + (1./1.27795e+06) * t4_12; + v += (1./53760) * t5_6 + (1./276480) * t5_8 + (1./1.35168e+06) * t5_10 + (1./6.38976e+06) * t5_12; + u -= (1./322560) * t6_6 + (1./1.65888e+06) * t6_8 + (1./8.11008e+06) * t6_10 + (1./3.83386e+07) * t6_12; + v -= (1./1.16122e+07) * t7_8 + (1./5.67706e+07) * t7_10 + (1./2.6837e+08) * t7_12; + u += (1./9.28973e+07) * t8_8 + (1./4.54164e+08) * t8_10 + (1./2.14696e+09) * t8_12; + v += (1./4.08748e+09) * t9_10 + (1./1.93226e+10) * t9_12; + u -= (1./4.08748e+10) * t10_10 + (1./1.93226e+11) * t10_12; + v -= (1./2.12549e+12) * t11_12; + u += (1./2.55059e+13) * t12_12; +#endif + +#if ORDER == 16 + double t1_1 = km0; + double t1_2 = .5 * km1; + double t1_3 = (1./6) * km2; + double t1_4 = (1./24) * km3; + double t2_2 = t1_1 * t1_1; + double t2_3 = 2 * (t1_1 * t1_2); + double t2_4 = 2 * (t1_1 * t1_3) + t1_2 * t1_2; + double t2_5 = 2 * (t1_1 * t1_4 + t1_2 * t1_3); + double t2_6 = 2 * (t1_2 * t1_4) + t1_3 * t1_3; + double t2_7 = 2 * (t1_3 * t1_4); + double t2_8 = t1_4 * t1_4; + double t3_4 = t2_2 * t1_2 + t2_3 * t1_1; + double t3_6 = t2_2 * t1_4 + t2_3 * t1_3 + t2_4 * t1_2 + t2_5 * t1_1; + double t3_8 = t2_4 * t1_4 + t2_5 * t1_3 + t2_6 * t1_2 + t2_7 * t1_1; + double t3_10 = t2_6 * t1_4 + t2_7 * t1_3 + t2_8 * t1_2; + double t3_12 = t2_8 * t1_4; + double t4_4 = t2_2 * t2_2; + double t4_5 = 2 * (t2_2 * t2_3); + double t4_6 = 2 * (t2_2 * t2_4) + t2_3 * t2_3; + double t4_7 = 2 * (t2_2 * t2_5 + t2_3 * t2_4); + double t4_8 = 2 * (t2_2 * t2_6 + t2_3 * t2_5) + t2_4 * t2_4; + double t4_9 = 2 * (t2_2 * t2_7 + t2_3 * t2_6 + t2_4 * t2_5); + double t4_10 = 2 * (t2_2 * t2_8 + t2_3 * t2_7 + t2_4 * t2_6) + t2_5 * t2_5; + double t4_11 = 2 * (t2_3 * t2_8 + t2_4 * t2_7 + t2_5 * t2_6); + double t4_12 = 2 * (t2_4 * t2_8 + t2_5 * t2_7) + t2_6 * t2_6; + double t4_13 = 2 * (t2_5 * t2_8 + t2_6 * t2_7); + double t4_14 = 2 * (t2_6 * t2_8) + t2_7 * t2_7; + double t5_6 = t4_4 * t1_2 + t4_5 * t1_1; + double t5_8 = t4_4 * t1_4 + t4_5 * t1_3 + t4_6 * t1_2 + t4_7 * t1_1; + double t5_10 = t4_6 * t1_4 + t4_7 * t1_3 + t4_8 * t1_2 + t4_9 * t1_1; + double t5_12 = t4_8 * t1_4 + t4_9 * t1_3 + t4_10 * t1_2 + t4_11 * t1_1; + double t5_14 = t4_10 * t1_4 + t4_11 * t1_3 + t4_12 * t1_2 + t4_13 * t1_1; + double t6_6 = t4_4 * t2_2; + double t6_7 = t4_4 * t2_3 + t4_5 * t2_2; + double t6_8 = t4_4 * t2_4 + t4_5 * t2_3 + t4_6 * t2_2; + double t6_9 = t4_4 * t2_5 + t4_5 * t2_4 + t4_6 * t2_3 + t4_7 * t2_2; + double t6_10 = t4_4 * t2_6 + t4_5 * t2_5 + t4_6 * t2_4 + t4_7 * t2_3 + t4_8 * t2_2; + double t6_11 = t4_4 * t2_7 + t4_5 * t2_6 + t4_6 * t2_5 + t4_7 * t2_4 + t4_8 * t2_3 + t4_9 * t2_2; + double t6_12 = t4_4 * t2_8 + t4_5 * t2_7 + t4_6 * t2_6 + t4_7 * t2_5 + t4_8 * t2_4 + t4_9 * t2_3 + t4_10 * t2_2; + double t6_13 = t4_5 * t2_8 + t4_6 * t2_7 + t4_7 * t2_6 + t4_8 * t2_5 + t4_9 * t2_4 + t4_10 * t2_3 + t4_11 * t2_2; + double t6_14 = t4_6 * t2_8 + t4_7 * t2_7 + t4_8 * t2_6 + t4_9 * t2_5 + t4_10 * t2_4 + t4_11 * t2_3 + t4_12 * t2_2; + double t7_8 = t6_6 * t1_2 + t6_7 * t1_1; + double t7_10 = t6_6 * t1_4 + t6_7 * t1_3 + t6_8 * t1_2 + t6_9 * t1_1; + double t7_12 = t6_8 * t1_4 + t6_9 * t1_3 + t6_10 * t1_2 + t6_11 * t1_1; + double t7_14 = t6_10 * t1_4 + t6_11 * t1_3 + t6_12 * t1_2 + t6_13 * t1_1; + double t8_8 = t6_6 * t2_2; + double t8_9 = t6_6 * t2_3 + t6_7 * t2_2; + double t8_10 = t6_6 * t2_4 + t6_7 * t2_3 + t6_8 * t2_2; + double t8_11 = t6_6 * t2_5 + t6_7 * t2_4 + t6_8 * t2_3 + t6_9 * t2_2; + double t8_12 = t6_6 * t2_6 + t6_7 * t2_5 + t6_8 * t2_4 + t6_9 * t2_3 + t6_10 * t2_2; + double t8_13 = t6_6 * t2_7 + t6_7 * t2_6 + t6_8 * t2_5 + t6_9 * t2_4 + t6_10 * t2_3 + t6_11 * t2_2; + double t8_14 = t6_6 * t2_8 + t6_7 * t2_7 + t6_8 * t2_6 + t6_9 * t2_5 + t6_10 * t2_4 + t6_11 * t2_3 + t6_12 * t2_2; + double t9_10 = t8_8 * t1_2 + t8_9 * t1_1; + double t9_12 = t8_8 * t1_4 + t8_9 * t1_3 + t8_10 * t1_2 + t8_11 * t1_1; + double t9_14 = t8_10 * t1_4 + t8_11 * t1_3 + t8_12 * t1_2 + t8_13 * t1_1; + double t10_10 = t8_8 * t2_2; + double t10_11 = t8_8 * t2_3 + t8_9 * t2_2; + double t10_12 = t8_8 * t2_4 + t8_9 * t2_3 + t8_10 * t2_2; + double t10_13 = t8_8 * t2_5 + t8_9 * t2_4 + t8_10 * t2_3 + t8_11 * t2_2; + double t10_14 = t8_8 * t2_6 + t8_9 * t2_5 + t8_10 * t2_4 + t8_11 * t2_3 + t8_12 * t2_2; + double t11_12 = t10_10 * t1_2 + t10_11 * t1_1; + double t11_14 = t10_10 * t1_4 + t10_11 * t1_3 + t10_12 * t1_2 + t10_13 * t1_1; + double t12_12 = t10_10 * t2_2; + double t12_13 = t10_10 * t2_3 + t10_11 * t2_2; + double t12_14 = t10_10 * t2_4 + t10_11 * t2_3 + t10_12 * t2_2; + double t13_14 = t12_12 * t1_2 + t12_13 * t1_1; + double t14_14 = t12_12 * t2_2; + u = 1; + u -= 1./24 * t2_2 + 1./160 * t2_4 + 1./896 * t2_6 + 1./4608 * t2_8; + u += 1./1920 * t4_4 + 1./10752 * t4_6 + 1./55296 * t4_8 + 1./270336 * t4_10 + 1./1277952 * t4_12 + 1./5898240 * t4_14; + u -= 1./322560 * t6_6 + 1./1658880 * t6_8 + 1./8110080 * t6_10 + 1./38338560 * t6_12 + 1./176947200 * t6_14; + u += 1./92897280 * t8_8 + 1./454164480 * t8_10 + 4.6577500191e-10 * t8_12 + 1.0091791708e-10 * t8_14; + u -= 2.4464949595e-11 * t10_10 + 5.1752777990e-12 * t10_12 + 1.1213101898e-12 * t10_14; + u += 3.9206649992e-14 * t12_12 + 8.4947741650e-15 * t12_14; + u -= 4.6674583324e-17 * t14_14; + v = 0; + v += 1./12 * t1_2 + 1./80 * t1_4; + v -= 1./480 * t3_4 + 1./2688 * t3_6 + 1./13824 * t3_8 + 1./67584 * t3_10 + 1./319488 * t3_12; + v += 1./53760 * t5_6 + 1./276480 * t5_8 + 1./1351680 * t5_10 + 1./6389760 * t5_12 + 1./29491200 * t5_14; + v -= 1./11612160 * t7_8 + 1./56770560 * t7_10 + 1./268369920 * t7_12 + 8.0734333664e-10 * t7_14; + v += 2.4464949595e-10 * t9_10 + 5.1752777990e-11 * t9_12 + 1.1213101898e-11 * t9_14; + v -= 4.7047979991e-13 * t11_12 + 1.0193728998e-13 * t11_14; + v += 6.5344416654e-16 * t13_14; +#endif + + } + + if (n == 1) { +#if ORDER == 2 + x = 1; + y = 0; +#else + x = u; + y = v; +#endif + } else { + double th = (((th4 * s + th3) * s + th2) * s + th1) * s; + double cth = cos(th); + double sth = sin(th); + +#if ORDER == 2 + x += cth; + y += sth; +#else + x += cth * u - sth * v; + y += cth * v + sth * u; +#endif + s += ds; + } + } + +#if ORDER == 4 || ORDER == 6 + xy[0] = x * (1./24 * ds); + xy[1] = y * (1./24 * ds); +#else + xy[0] = x * ds; + xy[1] = y * ds; +#endif +} + +static double +compute_ends(const double ks[4], double ends[2][4], double seg_ch) +{ + double xy[2]; + double ch, th; + double l, l2, l3; + double th_even, th_odd; + double k0_even, k0_odd; + double k1_even, k1_odd; + double k2_even, k2_odd; + + integrate_spiro(ks, xy); + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + l = ch / seg_ch; + + th_even = .5 * ks[0] + (1./48) * ks[2]; + th_odd = .125 * ks[1] + (1./384) * ks[3] - th; + ends[0][0] = th_even - th_odd; + ends[1][0] = th_even + th_odd; + k0_even = l * (ks[0] + .125 * ks[2]); + k0_odd = l * (.5 * ks[1] + (1./48) * ks[3]); + ends[0][1] = k0_even - k0_odd; + ends[1][1] = k0_even + k0_odd; + l2 = l * l; + k1_even = l2 * (ks[1] + .125 * ks[3]); + k1_odd = l2 * .5 * ks[2]; + ends[0][2] = k1_even - k1_odd; + ends[1][2] = k1_even + k1_odd; + l3 = l2 * l; + k2_even = l3 * ks[2]; + k2_odd = l3 * .5 * ks[3]; + ends[0][3] = k2_even - k2_odd; + ends[1][3] = k2_even + k2_odd; + + return l; +} + +static void +compute_pderivs(const spiro_seg *s, double ends[2][4], double derivs[4][2][4], + int jinc) +{ + double recip_d = 2e6; + double delta = 1./ recip_d; + double try_ks[4]; + double try_ends[2][4]; + int i, j, k; + + compute_ends(s->ks, ends, s->seg_ch); + for (i = 0; i < jinc; i++) { + for (j = 0; j < 4; j++) + try_ks[j] = s->ks[j]; + try_ks[i] += delta; + compute_ends(try_ks, try_ends, s->seg_ch); + for (k = 0; k < 2; k++) + for (j = 0; j < 4; j++) + derivs[j][k][i] = recip_d * (try_ends[k][j] - ends[k][j]); + } +} + +static double +mod_2pi(double th) +{ + double u = th / (2 * M_PI); + return 2 * M_PI * (u - floor(u + 0.5)); +} + +static spiro_seg * +setup_path(const spiro_cp *src, int n) +{ + int n_seg = src[0].ty == '{' ? n - 1 : n; + spiro_seg *r = (spiro_seg *)malloc((n_seg + 1) * sizeof(spiro_seg)); + int i; + int ilast; + + for (i = 0; i < n_seg; i++) { + r[i].x = src[i].x; + r[i].y = src[i].y; + r[i].ty = src[i].ty; + r[i].ks[0] = 0.; + r[i].ks[1] = 0.; + r[i].ks[2] = 0.; + r[i].ks[3] = 0.; + } + r[n_seg].x = src[n_seg % n].x; + r[n_seg].y = src[n_seg % n].y; + r[n_seg].ty = src[n_seg % n].ty; + + for (i = 0; i < n_seg; i++) { + double dx = r[i + 1].x - r[i].x; + double dy = r[i + 1].y - r[i].y; + r[i].seg_ch = hypot(dx, dy); + r[i].seg_th = atan2(dy, dx); + } + + ilast = n_seg - 1; + for (i = 0; i < n_seg; i++) { + if (r[i].ty == '{' || r[i].ty == '}' || r[i].ty == 'v') + r[i].bend_th = 0.; + else + r[i].bend_th = mod_2pi(r[i].seg_th - r[ilast].seg_th); + ilast = i; + } + return r; +} + +static void +bandec11(bandmat *m, int *perm, int n) +{ + int i, j, k; + int l; + + /* pack top triangle to the left. */ + for (i = 0; i < 5; i++) { + for (j = 0; j < i + 6; j++) + m[i].a[j] = m[i].a[j + 5 - i]; + for (; j < 11; j++) + m[i].a[j] = 0.; + } + l = 5; + for (k = 0; k < n; k++) { + int pivot = k; + double pivot_val = m[k].a[0]; + double pivot_scale; + + l = l < n ? l + 1 : n; + + for (j = k + 1; j < l; j++) + if (fabs(m[j].a[0]) > fabs(pivot_val)) { + pivot_val = m[j].a[0]; + pivot = j; + } + + perm[k] = pivot; + if (pivot != k) { + for (j = 0; j < 11; j++) { + double tmp = m[k].a[j]; + m[k].a[j] = m[pivot].a[j]; + m[pivot].a[j] = tmp; + } + } + + if (fabs(pivot_val) < 1e-12) pivot_val = 1e-12; + pivot_scale = 1. / pivot_val; + for (i = k + 1; i < l; i++) { + double x = m[i].a[0] * pivot_scale; + m[k].al[i - k - 1] = x; + for (j = 1; j < 11; j++) + m[i].a[j - 1] = m[i].a[j] - x * m[k].a[j]; + m[i].a[10] = 0.; + } + } +} + +static void +banbks11(const bandmat *m, const int *perm, double *v, int n) +{ + int i, k, l; + + /* forward substitution */ + l = 5; + for (k = 0; k < n; k++) { + i = perm[k]; + if (i != k) { + double tmp = v[k]; + v[k] = v[i]; + v[i] = tmp; + } + if (l < n) l++; + for (i = k + 1; i < l; i++) + v[i] -= m[k].al[i - k - 1] * v[k]; + } + + /* back substitution */ + l = 1; + for (i = n - 1; i >= 0; i--) { + double x = v[i]; + for (k = 1; k < l; k++) + x -= m[i].a[k] * v[k + i]; + v[i] = x / m[i].a[0]; + if (l < 11) l++; + } +} + +static int compute_jinc(char ty0, char ty1) +{ + if (ty0 == 'o' || ty1 == 'o' || + ty0 == ']' || ty1 == '[') + return 4; + else if (ty0 == 'c' && ty1 == 'c') + return 2; + else if (((ty0 == '{' || ty0 == 'v' || ty0 == '[') && ty1 == 'c') || + (ty0 == 'c' && (ty1 == '}' || ty1 == 'v' || ty1 == ']'))) + return 1; + else + return 0; +} + +static int count_vec(const spiro_seg *s, int nseg) +{ + int i; + int n = 0; + + for (i = 0; i < nseg; i++) + n += compute_jinc(s[i].ty, s[i + 1].ty); + return n; +} + +static void +add_mat_line(bandmat *m, double *v, + double derivs[4], double x, double y, int j, int jj, int jinc, + int nmat) +{ + if (jj >= 0) { + int joff = (j + 5 - jj + nmat) % nmat; + if (nmat < 6) { + joff = j + 5 - jj; + } else if (nmat == 6) { + joff = 2 + (j + 3 - jj + nmat) % nmat; + } +#ifdef VERBOSE + printf("add_mat_line j=%d jj=%d jinc=%d nmat=%d joff=%d\n", j, jj, jinc, nmat, joff); +#endif + v[jj] += x; + for (int k = 0; k < jinc; k++) + m[jj].a[joff + k] += y * derivs[k]; + } +} + +static double +spiro_iter(spiro_seg *s, bandmat *m, int *perm, double *v, const int n) +{ + int cyclic = s[0].ty != '{' && s[0].ty != 'v'; + int nmat = count_vec(s, n); + int n_invert; + + for (int i = 0; i < nmat; i++) { + v[i] = 0.; + for (double & j : m[i].a) { + j = 0.; + } + for (double & j : m[i].al) { + j = 0.; + } + } + + int j = 0; + int jj; + if (s[0].ty == 'o') { + jj = nmat - 2; + } else if (s[0].ty == 'c') { + jj = nmat - 1; + } else { + jj = 0; + } + for (int i = 0; i < n; i++) { + char ty0 = s[i].ty; + char ty1 = s[i + 1].ty; + int jinc = compute_jinc(ty0, ty1); + double th = s[i].bend_th; + double ends[2][4]; + double derivs[4][2][4]; + int jthl = -1, jk0l = -1, jk1l = -1, jk2l = -1; + int jthr = -1, jk0r = -1, jk1r = -1, jk2r = -1; + + compute_pderivs(&s[i], ends, derivs, jinc); + + /* constraints crossing left */ + if (ty0 == 'o' || ty0 == 'c' || ty0 == '[' || ty0 == ']') { + jthl = jj++; + jj %= nmat; + jk0l = jj++; + } + if (ty0 == 'o') { + jj %= nmat; + jk1l = jj++; + jk2l = jj++; + } + + /* constraints on left */ + if ((ty0 == '[' || ty0 == 'v' || ty0 == '{' || ty0 == 'c') && + jinc == 4) { + if (ty0 != 'c') + jk1l = jj++; + jk2l = jj++; + } + + /* constraints on right */ + if ((ty1 == ']' || ty1 == 'v' || ty1 == '}' || ty1 == 'c') && + jinc == 4) { + if (ty1 != 'c') + jk1r = jj++; + jk2r = jj++; + } + + /* constraints crossing right */ + if (ty1 == 'o' || ty1 == 'c' || ty1 == '[' || ty1 == ']') { + jthr = jj; + jk0r = (jj + 1) % nmat; + } + if (ty1 == 'o') { + jk1r = (jj + 2) % nmat; + jk2r = (jj + 3) % nmat; + } + + add_mat_line(m, v, derivs[0][0], th - ends[0][0], 1, j, jthl, jinc, nmat); + add_mat_line(m, v, derivs[1][0], ends[0][1], -1, j, jk0l, jinc, nmat); + add_mat_line(m, v, derivs[2][0], ends[0][2], -1, j, jk1l, jinc, nmat); + add_mat_line(m, v, derivs[3][0], ends[0][3], -1, j, jk2l, jinc, nmat); + add_mat_line(m, v, derivs[0][1], -ends[1][0], 1, j, jthr, jinc, nmat); + add_mat_line(m, v, derivs[1][1], -ends[1][1], 1, j, jk0r, jinc, nmat); + add_mat_line(m, v, derivs[2][1], -ends[1][2], 1, j, jk1r, jinc, nmat); + add_mat_line(m, v, derivs[3][1], -ends[1][3], 1, j, jk2r, jinc, nmat); + if (jthl >= 0) + v[jthl] = mod_2pi(v[jthl]); + if (jthr >= 0) + v[jthr] = mod_2pi(v[jthr]); + j += jinc; + } + if (cyclic) { + memcpy(m + nmat, m, sizeof(bandmat) * nmat); + memcpy(m + 2 * nmat, m, sizeof(bandmat) * nmat); + memcpy(v + nmat, v, sizeof(double) * nmat); + memcpy(v + 2 * nmat, v, sizeof(double) * nmat); + n_invert = 3 * nmat; + j = nmat; + } else { + n_invert = nmat; + j = 0; + } +#ifdef VERBOSE + for (int i = 0; i < n; i++) { + for (int k = 0; k < 11; k++) { + printf(" %2.4f", m[i].a[k]); + } + printf(": %2.4f\n", v[i]); + } + printf("---\n"); +#endif + bandec11(m, perm, n_invert); + banbks11(m, perm, v, n_invert); + + double norm = 0.; + for (int i = 0; i < n; i++) { + char ty0 = s[i].ty; + char ty1 = s[i + 1].ty; + int jinc = compute_jinc(ty0, ty1); + int k; + + for (k = 0; k < jinc; k++) { + double dk = v[j++]; + +#ifdef VERBOSE + printf("s[%d].ks[%d] += %f\n", i, k, dk); +#endif + s[i].ks[k] += dk; + norm += dk * dk; + } + s[i].ks[0] = 2.0*mod_2pi(s[i].ks[0]/2.0); + } + return norm; +} + +static int +solve_spiro(spiro_seg *s, const int nseg) +{ + int nmat = count_vec(s, nseg); + int n_alloc = nmat; + + if (nmat == 0) { + return 0; + } + if (s[0].ty != '{' && s[0].ty != 'v') { + n_alloc *= 3; + } + if (n_alloc < 5) { + n_alloc = 5; + } + + bandmat *m = (bandmat *)malloc(sizeof(bandmat) * n_alloc); + double *v = (double *)malloc(sizeof(double) * n_alloc); + int *perm = (int *)malloc(sizeof(int) * n_alloc); + + for (unsigned i = 0; i < 10; i++) { + double norm = spiro_iter(s, m, perm, v, nseg); +#ifdef VERBOSE + printf("%% norm = %g\n", norm); +#endif + if (norm < 1e-12) break; + } + + free(m); + free(v); + free(perm); + return 0; +} + +static void +spiro_seg_to_otherpath(const double ks[4], + double x0, double y0, double x1, double y1, + ConverterBase &bc, int depth, bool close_last) +{ + double bend = fabs(ks[0]) + fabs(.5 * ks[1]) + fabs(.125 * ks[2]) + + fabs((1./48) * ks[3]); + + if (!(bend > 1e-8)) { + bc.lineto(x1, y1, close_last); + } else { + double seg_ch = hypot(x1 - x0, y1 - y0); + double seg_th = atan2(y1 - y0, x1 - x0); + double xy[2]; + double ch, th; + double scale, rot; + + integrate_spiro(ks, xy); + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + scale = seg_ch / ch; + rot = seg_th - th; + if (depth > 5 || bend < 1.) { + double ul, vl; + double ur, vr; + double th_even, th_odd; + th_even = (1./384) * ks[3] + (1./8) * ks[1] + rot; + th_odd = (1./48) * ks[2] + .5 * ks[0]; + ul = (scale * (1./3)) * cos(th_even - th_odd); + vl = (scale * (1./3)) * sin(th_even - th_odd); + ur = (scale * (1./3)) * cos(th_even + th_odd); + vr = (scale * (1./3)) * sin(th_even + th_odd); + bc.curveto(x0 + ul, y0 + vl, x1 - ur, y1 - vr, x1, y1, close_last); + } else { + /* subdivide */ + double ksub[4]; + double thsub; + double xysub[2]; + double xmid, ymid; + double cth, sth; + + ksub[0] = .5 * ks[0] - .125 * ks[1] + (1./64) * ks[2] - (1./768) * ks[3]; + ksub[1] = .25 * ks[1] - (1./16) * ks[2] + (1./128) * ks[3]; + ksub[2] = .125 * ks[2] - (1./32) * ks[3]; + ksub[3] = (1./16) * ks[3]; + thsub = rot - .25 * ks[0] + (1./32) * ks[1] - (1./384) * ks[2] + (1./6144) * ks[3]; + cth = .5 * scale * cos(thsub); + sth = .5 * scale * sin(thsub); + integrate_spiro(ksub, xysub); + xmid = x0 + cth * xysub[0] - sth * xysub[1]; + ymid = y0 + cth * xysub[1] + sth * xysub[0]; + spiro_seg_to_otherpath(ksub, x0, y0, xmid, ymid, bc, depth + 1, false); + ksub[0] += .25 * ks[1] + (1./384) * ks[3]; + ksub[1] += .125 * ks[2]; + ksub[2] += (1./16) * ks[3]; + spiro_seg_to_otherpath(ksub, xmid, ymid, x1, y1, bc, depth + 1, close_last); + } + } +} + +spiro_seg * +run_spiro(const spiro_cp *src, int n) +{ + int nseg = src[0].ty == '{' ? n - 1 : n; + spiro_seg *s = setup_path(src, n); + if (nseg > 1) + solve_spiro(s, nseg); + return s; +} + +void +free_spiro(spiro_seg *s) +{ + free(s); +} + +void +spiro_to_otherpath(const spiro_seg *s, int n, ConverterBase &bc) +{ + int i; + int nsegs = s[n - 1].ty == '}' ? n - 1 : n; + + for (i = 0; i < nsegs; i++) { + double x0 = s[i].x; + double y0 = s[i].y; + double x1 = s[i + 1].x; + double y1 = s[i + 1].y; + + if (i == 0) { + bc.moveto(x0, y0); + } + // on the last segment, set the 'close_last' flag if path is closed + spiro_seg_to_otherpath(s[i].ks, x0, y0, x1, y1, bc, 0, (nsegs == n) && (i == n - 1)); + } +} + +double +get_knot_th(const spiro_seg *s, int i) +{ + double ends[2][4]; + + if (i == 0) { + compute_ends(s[i].ks, ends, s[i].seg_ch); + return s[i].seg_th - ends[0][0]; + } else { + compute_ends(s[i - 1].ks, ends, s[i - 1].seg_ch); + return s[i - 1].seg_th + ends[1][0]; + } +} + + +} // namespace Spiro + +/************************************ + * Unit_test code + */ + + +#ifdef UNIT_TEST +#include <stdio.h> +#include <sys/time.h> /* for gettimeofday */ + +using namespace Spiro; + +static double +get_time (void) +{ + struct timeval tv; + struct timezone tz; + + gettimeofday (&tv, &tz); + + return tv.tv_sec + 1e-6 * tv.tv_usec; +} + +int +test_integ(void) { + double ks[] = {1, 2, 3, 4}; + double xy[2]; + double xynom[2]; + int i, j; + int nsubdiv; + + n = ORDER < 6 ? 4096 : 1024; + integrate_spiro(ks, xynom); + nsubdiv = ORDER < 12 ? 8 : 7; + for (i = 0; i < nsubdiv; i++) { + double st, en; + double err; + int n_iter = (1 << (20 - i)); + + n = 1 << i; + st = get_time(); + for (j = 0; j < n_iter; j++) + integrate_spiro(ks, xy); + en = get_time(); + err = hypot(xy[0] - xynom[0], xy[1] - xynom[1]); + printf("%d %d %g %g\n", ORDER, n, (en - st) / n_iter, err); +#if 0 + double ch, th; + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + printf("n = %d: integ(%g %g %g %g) = %g %g, ch = %g, th = %g\n", n, + ks[0], ks[1], ks[2], ks[3], xy[0], xy[1], ch, th); + printf("%d: %g %g\n", n, xy[0] - xynom[0], xy[1] - xynom[1]); +#endif + } + return 0; +} + +void +print_seg(const double ks[4], double x0, double y0, double x1, double y1) +{ + double bend = fabs(ks[0]) + fabs(.5 * ks[1]) + fabs(.125 * ks[2]) + + fabs((1./48) * ks[3]); + + if (bend < 1e-8) { + printf("%g %g lineto\n", x1, y1); + } else { + double seg_ch = hypot(x1 - x0, y1 - y0); + double seg_th = atan2(y1 - y0, x1 - x0); + double xy[2]; + double ch, th; + double scale, rot; + + integrate_spiro(ks, xy); + ch = hypot(xy[0], xy[1]); + th = atan2(xy[1], xy[0]); + scale = seg_ch / ch; + rot = seg_th - th; + if (bend < 1.) { + double th_even, th_odd; + double ul, vl; + double ur, vr; + th_even = (1./384) * ks[3] + (1./8) * ks[1] + rot; + th_odd = (1./48) * ks[2] + .5 * ks[0]; + ul = (scale * (1./3)) * cos(th_even - th_odd); + vl = (scale * (1./3)) * sin(th_even - th_odd); + ur = (scale * (1./3)) * cos(th_even + th_odd); + vr = (scale * (1./3)) * sin(th_even + th_odd); + printf("%g %g %g %g %g %g curveto\n", + x0 + ul, y0 + vl, x1 - ur, y1 - vr, x1, y1); + + } else { + /* subdivide */ + double ksub[4]; + double thsub; + double xysub[2]; + double xmid, ymid; + double cth, sth; + + ksub[0] = .5 * ks[0] - .125 * ks[1] + (1./64) * ks[2] - (1./768) * ks[3]; + ksub[1] = .25 * ks[1] - (1./16) * ks[2] + (1./128) * ks[3]; + ksub[2] = .125 * ks[2] - (1./32) * ks[3]; + ksub[3] = (1./16) * ks[3]; + thsub = rot - .25 * ks[0] + (1./32) * ks[1] - (1./384) * ks[2] + (1./6144) * ks[3]; + cth = .5 * scale * cos(thsub); + sth = .5 * scale * sin(thsub); + integrate_spiro(ksub, xysub); + xmid = x0 + cth * xysub[0] - sth * xysub[1]; + ymid = y0 + cth * xysub[1] + sth * xysub[0]; + print_seg(ksub, x0, y0, xmid, ymid); + ksub[0] += .25 * ks[1] + (1./384) * ks[3]; + ksub[1] += .125 * ks[2]; + ksub[2] += (1./16) * ks[3]; + print_seg(ksub, xmid, ymid, x1, y1); + } + } +} + +void +print_segs(const spiro_seg *segs, int nsegs) +{ + int i; + + for (i = 0; i < nsegs; i++) { + double x0 = segs[i].x; + double y0 = segs[i].y; + double x1 = segs[i + 1].x; + double y1 = segs[i + 1].y; + + if (i == 0) + printf("%g %g moveto\n", x0, y0); + printf("%% ks = [ %g %g %g %g ]\n", + segs[i].ks[0], segs[i].ks[1], segs[i].ks[2], segs[i].ks[3]); + print_seg(segs[i].ks, x0, y0, x1, y1); + } + printf("stroke\n"); +} + +int +test_curve(void) +{ + spiro_cp path[] = { + {334, 117, 'v'}, + {305, 176, 'v'}, + {212, 142, 'c'}, + {159, 171, 'c'}, + {224, 237, 'c'}, + {347, 335, 'c'}, + {202, 467, 'c'}, + {81, 429, 'v'}, + {114, 368, 'v'}, + {201, 402, 'c'}, + {276, 369, 'c'}, + {218, 308, 'c'}, + {91, 211, 'c'}, + {124, 111, 'c'}, + {229, 82, 'c'} + }; + spiro_seg *segs; + int i; + + n = 1; + for (i = 0; i < 1000; i++) { + segs = setup_path(path, 15); + solve_spiro(segs, 15); + } + printf("100 800 translate 1 -1 scale 1 setlinewidth\n"); + print_segs(segs, 15); + printf("showpage\n"); + return 0; +} + +int main(int argc, char **argv) +{ + return test_curve(); +} +#endif diff --git a/src/live_effects/spiro.h b/src/live_effects/spiro.h new file mode 100644 index 0000000..3ca1fb8 --- /dev/null +++ b/src/live_effects/spiro.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * C implementation of third-order polynomial spirals. + *//* + * Authors: see git history + * Raph Levien + * Johan Engelen + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef INKSCAPE_SPIRO_H +#define INKSCAPE_SPIRO_H + +#include "live_effects/spiro-converters.h" +#include <2geom/forward.h> + +class SPCurve; + +namespace Spiro { + +struct spiro_cp { + double x; + double y; + char ty; +}; + + +void spiro_run(const spiro_cp *src, int src_len, SPCurve &curve); +void spiro_run(const spiro_cp *src, int src_len, Geom::Path &path); + +/* the following methods are only for expert use: */ +typedef struct spiro_seg_s spiro_seg; +spiro_seg * run_spiro(const spiro_cp *src, int n); +void free_spiro(spiro_seg *s); +void spiro_to_otherpath(const spiro_seg *s, int n, ConverterBase &bc); +double get_knot_th(const spiro_seg *s, int i); + + +} // namespace Spiro + +#endif // INKSCAPE_SPIRO_H diff --git a/src/live_effects/todo.txt b/src/live_effects/todo.txt new file mode 100644 index 0000000..a208867 --- /dev/null +++ b/src/live_effects/todo.txt @@ -0,0 +1,11 @@ +reminder list + + +cleanup nodepath code that draws helper path + +ARCS !!! see sp_arc_set_elliptical_path_attribute(SPArc *arc, Inkscape::XML::Node *repr) + +make sp_nodepath_is_over_stroke perhaps + + +find dir "fixme" and fix'em!
\ No newline at end of file |