diff options
Diffstat (limited to '')
30 files changed, 5283 insertions, 0 deletions
diff --git a/src/helper-fns.h b/src/helper-fns.h new file mode 100644 index 0000000..27dfc73 --- /dev/null +++ b/src/helper-fns.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_HELPER_FNS_H +#define SEEN_HELPER_FNS_H +/* + * Authors: + * Felipe CorrĂȘa da Silva Sanches <juca@members.fsf.org> + * + * + * Copyright (C) 2006 Hugo Rodrigues + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cstring> +#include <vector> +#include <sstream> + +// calling helperfns_read_number(string, false), it's not obvious, what +// that false stands for. helperfns_read_number(string, HELPERFNS_NO_WARNING) +// can be more clear. +#define HELPERFNS_NO_WARNING false + +/* convert ascii representation to double + * the function can only be used to convert numbers as given by gui elements that use localized representation + * @param value ascii representation of the number + * @return the converted number + * + * Setting warning to false disables conversion error warnings from + * this function. This can be useful in places, where the input type + * is not known beforehand. For example, see sp_feColorMatrix_set in + * sp-fecolormatrix.cpp */ +inline double helperfns_read_number(gchar const *value, bool warning = true) { + if (!value) { + g_warning("Called helperfns_read_number with value==null_ptr, this can lead to unexpected behaviour."); + return 0; + } + char *end; + double ret = g_ascii_strtod(value, &end); + if (*end) { + if (warning) { + g_warning("helper-fns::helperfns_read_number() Unable to convert \"%s\" to number", value); + } + // We could leave this out, too. If strtod can't convert + // anything, it will return zero. + ret = 0; + } + return ret; +} + +inline bool helperfns_read_bool(gchar const *value, bool default_value){ + if (!value) return default_value; + switch(value[0]){ + case 't': + if (strncmp(value, "true", 4) == 0) return true; + break; + case 'f': + if (strncmp(value, "false", 5) == 0) return false; + break; + } + return default_value; +} + +/* convert ascii representation to double + * the function can only be used to convert numbers as given by gui elements that use localized representation + * numbers are delimited by space + * @param value ascii representation of the number + * @return the vector of the converted numbers + */ +inline std::vector<gdouble> helperfns_read_vector(const gchar* value){ + std::vector<gdouble> v; + + gchar const* beg = value; + while(isspace(*beg) || (*beg == ',')) beg++; + while(*beg) + { + char *end; + double ret = g_ascii_strtod(beg, &end); + if (end==beg){ + g_warning("helper-fns::helperfns_read_vector() Unable to convert \"%s\" to number", beg); + // We could leave this out, too. If strtod can't convert + // anything, it will return zero. + // ret = 0; + break; + } + v.push_back(ret); + + beg = end; + while(isspace(*beg) || (*beg == ',')) beg++; + } + return v; +} + +#endif /* !SEEN_HELPER_FNS_H */ + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/CMakeLists.txt b/src/helper/CMakeLists.txt new file mode 100644 index 0000000..60e42ac --- /dev/null +++ b/src/helper/CMakeLists.txt @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +include(${CMAKE_SOURCE_DIR}/CMakeScripts/UseGlibMarshal.cmake) + +GLIB_MARSHAL(sp_marshal sp-marshal "${CMAKE_CURRENT_BINARY_DIR}/helper") + +set(sp_marshal_SRC + ${CMAKE_CURRENT_BINARY_DIR}/sp-marshal.cpp + ${CMAKE_CURRENT_BINARY_DIR}/sp-marshal.h +) + +set(helper_SRC + action.cpp + action-context.cpp + geom.cpp + geom-nodetype.cpp + geom-pathstroke.cpp + geom-pathvectorsatellites.cpp + geom-satellite.cpp + gettext.cpp + pixbuf-ops.cpp + png-write.cpp + stock-items.cpp + verb-action.cpp + #units-test.cpp + + # we generate this file and it's .h counter-part + ${sp_marshal_SRC} + + + # ------- + # Headers + action.h + action-context.h + geom-curves.h + geom-nodetype.h + geom-pathstroke.h + geom-pathvectorsatellites.h + geom-satellite.h + geom.h + gettext.h + mathfns.h + pixbuf-ops.h + png-write.h + stock-items.h + verb-action.h +) + +set_source_files_properties(sp_marshal_SRC PROPERTIES GENERATED true) + +# add_inkscape_lib(helper_LIB "${helper_SRC}") +add_inkscape_source("${helper_SRC}") diff --git a/src/helper/README b/src/helper/README new file mode 100644 index 0000000..d9a6b90 --- /dev/null +++ b/src/helper/README @@ -0,0 +1,8 @@ + + +This directory contains a variety of helper code. + +To do: + +* Merge with 'util'. +* Move individual files to more appropriate directories. diff --git a/src/helper/action-context.cpp b/src/helper/action-context.cpp new file mode 100644 index 0000000..36b6914 --- /dev/null +++ b/src/helper/action-context.cpp @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ActionContext implementation. + * + * Author: + * Eric Greveson <eric@greveson.co.uk> + * + * Copyright (C) 2013 Eric Greveson + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "desktop.h" +#include "document.h" +#include "layer-model.h" +#include "selection.h" +#include "helper/action-context.h" + +namespace Inkscape { + +ActionContext::ActionContext() + : _selection(nullptr) + , _view(nullptr) +{ +} + +ActionContext::ActionContext(Selection *selection) + : _selection(selection) + , _view(nullptr) +{ +} + +ActionContext::ActionContext(UI::View::View *view) + : _selection(nullptr) + , _view(view) +{ + SPDesktop *desktop = static_cast<SPDesktop *>(view); + if (desktop) { + _selection = desktop->selection; + } +} + +SPDocument *ActionContext::getDocument() const +{ + if (_selection == nullptr) { + return nullptr; + } + + // Should be the same as the view's document, if view is non-NULL + return _selection->layers()->getDocument(); +} + +Selection *ActionContext::getSelection() const +{ + return _selection; +} + +UI::View::View *ActionContext::getView() const +{ + return _view; +} + +SPDesktop *ActionContext::getDesktop() const +{ + return static_cast<SPDesktop *>(_view); +} + +} // 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/helper/action-context.h b/src/helper/action-context.h new file mode 100644 index 0000000..d3468ea --- /dev/null +++ b/src/helper/action-context.h @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Inkscape UI action context implementation + *//* + * Author: + * Eric Greveson <eric@greveson.co.uk> + * + * Copyright (C) 2013 Eric Greveson + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_ACTION_CONTEXT_H +#define SEEN_INKSCAPE_ACTION_CONTEXT_H + +class SPDesktop; +class SPDocument; + +namespace Inkscape { + +class Selection; + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI + +/** This structure contains all the document/view context required + for an action. Some actions may be executed on a document without + requiring a GUI, hence not providing the info directly through + Inkscape::UI::View::View. Actions that do require GUI objects should + check to see if the relevant pointers are NULL before attempting to + use them. + + TODO: we store a UI::View::View* because that's what the actions and verbs + used to take as parameters in their methods. Why is this? They almost + always seemed to cast straight to an SPDesktop* - so shouldn't we actually + be storing an SPDesktop*? Is there a case where a non-SPDesktop + UI::View::View is used by the actions? YES: Command-line wihtout GUI. + + ActionContext is designed to be copyable, so it may be used with stack + storage if required. */ +class ActionContext { + // NB: Only one of these is typically set - selection model if in console mode, view if in GUI mode + Selection *_selection; /**< The selection model to which this action applies, if running in console mode. May be NULL. */ + UI::View::View *_view; /**< The view to which this action applies. May be NULL (e.g. if running in console mode). */ + +public: + /** Construct without any document or GUI */ + ActionContext(); + + /** Construct an action context for when the app is being run without + any GUI, i.e. in console mode */ + ActionContext(Selection *selection); + + /** Construct an action context for when the app is being run in GUI mode */ + ActionContext(UI::View::View *view); + + /** Get the document for the action context. May be NULL. Prefer this + function to getView()->doc() if the action doesn't require a GUI. */ + SPDocument *getDocument() const; + + /** Get the selection for the action context. May be NULL. Should be + non-NULL if getDocument() is non-NULL. */ + Selection *getSelection() const; + + /** Get the view for the action context. May be NULL. Guaranteed to be + NULL if running in console mode. */ + UI::View::View *getView() const; + + /** Get the desktop for the action context. May be NULL. Guaranteed to be + NULL if running in console mode. */ + SPDesktop *getDesktop() const; +}; + +} // 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/helper/action.cpp b/src/helper/action.cpp new file mode 100644 index 0000000..6b09a91 --- /dev/null +++ b/src/helper/action.cpp @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape UI action implementation + *//* + * Authors: + * see git history + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "helper/action.h" +#include "ui/icon-loader.h" + +#include <gtkmm/toolbutton.h> + +#include "debug/logger.h" +#include "debug/timestamp.h" +#include "debug/simple-event.h" +#include "debug/event-tracker.h" +#include "ui/view/view.h" +#include "desktop.h" +#include "document.h" +#include "verbs.h" + +static void sp_action_finalize (GObject *object); + +G_DEFINE_TYPE(SPAction, sp_action, G_TYPE_OBJECT); + +/** + * SPAction vtable initialization. + */ +static void +sp_action_class_init (SPActionClass *klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + object_class->finalize = sp_action_finalize; +} + +/** + * Callback for SPAction object initialization. + */ +static void +sp_action_init (SPAction *action) +{ + action->sensitive = 0; + action->active = 0; + action->context = Inkscape::ActionContext(); + action->id = action->name = action->tip = nullptr; + action->image = nullptr; + + new (&action->signal_perform) sigc::signal<void>(); + new (&action->signal_set_sensitive) sigc::signal<void, bool>(); + new (&action->signal_set_active) sigc::signal<void, bool>(); + new (&action->signal_set_name) sigc::signal<void, Glib::ustring const &>(); +} + +/** + * Called before SPAction object destruction. + */ +static void +sp_action_finalize (GObject *object) +{ + SPAction *action = SP_ACTION(object); + + g_free (action->image); + g_free (action->tip); + g_free (action->name); + g_free (action->id); + + action->signal_perform.~signal(); + action->signal_set_sensitive.~signal(); + action->signal_set_active.~signal(); + action->signal_set_name.~signal(); + + G_OBJECT_CLASS(sp_action_parent_class)->finalize (object); +} + +/** + * Create new SPAction object and set its properties. + */ +SPAction * +sp_action_new(Inkscape::ActionContext const &context, + const gchar *id, + const gchar *name, + const gchar *tip, + const gchar *image, + Inkscape::Verb * verb) +{ + SPAction *action = (SPAction *)g_object_new(SP_TYPE_ACTION, nullptr); + + action->context = context; + action->sensitive = TRUE; + action->id = g_strdup (id); + action->name = g_strdup (name); + action->tip = g_strdup (tip); + action->image = g_strdup (image); + action->verb = verb; + + return action; +} + +namespace { + +using Inkscape::Debug::SimpleEvent; +using Inkscape::Debug::Event; +using Inkscape::Debug::timestamp; + +typedef SimpleEvent<Event::INTERACTION> ActionEventBase; + +class ActionEvent : public ActionEventBase { +public: + ActionEvent(SPAction const *action) + : ActionEventBase("action") + { + _addProperty("timestamp", timestamp()); + SPDocument *document = action->context.getDocument(); + if (document) { + _addProperty("document", document->serial()); + } + _addProperty("verb", action->id); + } +}; + +} + +/** + * Executes an action. + * @param action The action to be executed. + * @param data ignored. + */ +void sp_action_perform(SPAction *action, void * /*data*/) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + Inkscape::Debug::EventTracker<ActionEvent> tracker(action); + action->signal_perform.emit(); +} + +/** + * Change activation in all actions that can be taken with the action. + */ +void +sp_action_set_active (SPAction *action, unsigned int active) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + action->signal_set_active.emit(active); +} + +/** + * Change sensitivity in all actions that can be taken with the action. + */ +void +sp_action_set_sensitive (SPAction *action, unsigned int sensitive) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + action->signal_set_sensitive.emit(sensitive); +} + +void +sp_action_set_name (SPAction *action, Glib::ustring const &name) +{ + g_return_if_fail (action != nullptr); + g_return_if_fail (SP_IS_ACTION (action)); + + g_free(action->name); + action->name = g_strdup(name.data()); + action->signal_set_name.emit(name); +} + +/** + * Return Document associated with the action. + */ +SPDocument * +sp_action_get_document (SPAction *action) +{ + g_return_val_if_fail (SP_IS_ACTION (action), NULL); + return action->context.getDocument(); +} + +/** + * Return Selection associated with the action + */ +Inkscape::Selection * +sp_action_get_selection (SPAction *action) +{ + g_return_val_if_fail (SP_IS_ACTION (action), NULL); + return action->context.getSelection(); +} + +/** + * Return View associated with the action, if any. + */ +Inkscape::UI::View::View * +sp_action_get_view (SPAction *action) +{ + g_return_val_if_fail (SP_IS_ACTION (action), NULL); + return action->context.getView(); +} + +/** + * Return Desktop associated with the action, if any. + */ +SPDesktop * +sp_action_get_desktop (SPAction *action) +{ + // TODO: this slightly horrible storage of a UI::View::View*, and + // casting to an SPDesktop*, is only done because that's what was + // already the norm in the Inkscape codebase. This seems wrong. Surely + // we should store an SPDesktop* in the first place? Is there a case + // of actions being carried out on a View that is not an SPDesktop? + return static_cast<SPDesktop *>(sp_action_get_view(action)); +} + +/** + * \brief Create a toolbutton whose "clicked" signal performs an Inkscape verb + * + * \param[in] verb_code The code (e.g., SP_VERB_EDIT_SELECT_ALL) for the verb we want + * + * \todo This should really attach the toolbutton to an application action instead of + * hooking up the "clicked" signal. This should probably wait until we've + * migrated to Gtk::Application + */ +Gtk::ToolButton * +SPAction::create_toolbutton_for_verb(unsigned int verb_code, + Inkscape::ActionContext &context) +{ + // Get display properties for the verb + auto verb = Inkscape::Verb::get(verb_code); + auto target_action = verb->get_action(context); + auto icon_name = verb->get_image() ? verb->get_image() : Glib::ustring(); + + // Create a button with the required display properties + auto button = Gtk::manage(new Gtk::ToolButton(verb->get_tip())); + auto icon_widget = sp_get_icon_image(icon_name, "/toolbox/small"); + button->set_icon_widget(*icon_widget); + button->set_tooltip_text(verb->get_tip()); + + // Hook up signal handler + auto button_clicked_cb = sigc::bind(sigc::ptr_fun(&sp_action_perform), + target_action, nullptr); + button->signal_clicked().connect(button_clicked_cb); + + return button; +} +/* + 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/helper/action.h b/src/helper/action.h new file mode 100644 index 0000000..1f871db --- /dev/null +++ b/src/helper/action.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Inkscape UI action implementation + *//* + * Authors: + * see git history + * Lauris Kaplinski <lauris@kaplinski.com> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_SP_ACTION_H +#define SEEN_INKSCAPE_SP_ACTION_H + +#include <glib-object.h> + +#include "helper/action-context.h" +#include <sigc++/signal.h> +#include <glibmm/ustring.h> + +#define SP_TYPE_ACTION (sp_action_get_type()) +#define SP_ACTION(o) (G_TYPE_CHECK_INSTANCE_CAST((o), SP_TYPE_ACTION, SPAction)) +#define SP_ACTION_CLASS(o) (G_TYPE_CHECK_CLASS_CAST((o), SP_TYPE_ACTION, SPActionClass)) +#define SP_IS_ACTION(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), SP_TYPE_ACTION)) + +namespace Gtk { +class ToolButton; +} + +class SPDesktop; +class SPDocument; +namespace Inkscape { + +class Selection; +class Verb; + +namespace UI { +namespace View { +class View; +} // namespace View +} // namespace UI +} + +/** All the data that is required to be an action. This + structure identifies the action and has the data to + create menus and toolbars for the action */ +struct SPAction : public GObject { + unsigned sensitive : 1; /**< Value to track whether the action is sensitive */ + unsigned active : 1; /**< Value to track whether the action is active */ + Inkscape::ActionContext context; /**< The context (doc/view) to which this action is attached */ + gchar *id; /**< The identifier for the action */ + gchar *name; /**< Full text name of the action */ + gchar *tip; /**< A tooltip to describe the action */ + gchar *image; /**< An image to visually identify the action */ + Inkscape::Verb *verb; /**< The verb that produced this action */ + + sigc::signal<void> signal_perform; + sigc::signal<void, bool> signal_set_sensitive; + sigc::signal<void, bool> signal_set_active; + sigc::signal<void, Glib::ustring const &> signal_set_name; + + static Gtk::ToolButton * create_toolbutton_for_verb(unsigned int verb_code, + Inkscape::ActionContext &context); +}; + +/** The action class is the same as its parent. */ +struct SPActionClass { + GObjectClass parent_class; /**< Parent Class */ +}; + +GType sp_action_get_type(); + +SPAction *sp_action_new(Inkscape::ActionContext const &context, + gchar const *id, + gchar const *name, + gchar const *tip, + gchar const *image, + Inkscape::Verb *verb); + +void sp_action_perform(SPAction *action, void *data); +void sp_action_set_active(SPAction *action, unsigned active); +void sp_action_set_sensitive(SPAction *action, unsigned sensitive); +void sp_action_set_name(SPAction *action, Glib::ustring const &name); +SPDocument *sp_action_get_document(SPAction *action); +Inkscape::Selection *sp_action_get_selection(SPAction *action); +Inkscape::UI::View::View *sp_action_get_view(SPAction *action); +SPDesktop *sp_action_get_desktop(SPAction *action); + +#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/helper/geom-curves.h b/src/helper/geom-curves.h new file mode 100644 index 0000000..08403e2 --- /dev/null +++ b/src/helper/geom-curves.h @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_GEOM_CURVES_H +#define INKSCAPE_HELPER_GEOM_CURVES_H + +/** + * @file + * Specific curve type functions for Inkscape, not provided by lib2geom. + */ +/* + * Author: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2008-2009 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/line.h> +#include <2geom/bezier-curve.h> + +/// \todo un-inline this function +inline bool is_straight_curve(Geom::Curve const & c) +{ + if( dynamic_cast<Geom::LineSegment const*>(&c) ) + { + return true; + } + // the curve can be a quad/cubic bezier, but could still be a perfect straight line + // if the control points are exactly on the line connecting the initial and final points. + Geom::BezierCurve const *curve = dynamic_cast<Geom::BezierCurve const *>(&c); + if (curve) { + Geom::Line line(curve->initialPoint(), curve->finalPoint()); + std::vector<Geom::Point> pts = curve->controlPoints(); + for (unsigned i = 1; i < pts.size() - 1; ++i) { + if (!are_near(pts[i], line)) + return false; + } + return true; + } + + return false; +} + +#endif // INKSCAPE_HELPER_GEOM_CURVES_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-nodetype.cpp b/src/helper/geom-nodetype.cpp new file mode 100644 index 0000000..e04b08d --- /dev/null +++ b/src/helper/geom-nodetype.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Specific nodetype geometry functions for Inkscape, not provided my lib2geom. + * + * Author: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "helper/geom-nodetype.h" + +#include <2geom/curve.h> + +namespace Geom { + +/* + * NOTE: THIS METHOD NEVER RETURNS "NODE_SYMM". + * Returns the nodetype between c_incoming and c_outgoing. Location of the node is + * at c_incoming.pointAt(1) == c_outgoing.pointAt(0). If these two are unequal, + * the returned type is NODE_NONE. + * Comparison is based on the unitTangent, does not work for NODE_SYMM! + */ +NodeType get_nodetype(Curve const &c_incoming, Curve const &c_outgoing) +{ + if ( !are_near(c_incoming.pointAt(1), c_outgoing.pointAt(0)) ) + return NODE_NONE; + + Geom::Curve *crv = c_incoming.reverse(); + Geom::Point deriv_1 = -crv->unitTangentAt(0); + delete crv; + Geom::Point deriv_2 = c_outgoing.unitTangentAt(0); + double this_angle_L2 = Geom::L2(deriv_1); + double next_angle_L2 = Geom::L2(deriv_2); + double both_angles_L2 = Geom::L2(deriv_1 + deriv_2); + if ( (this_angle_L2 > 1e-6) && + (next_angle_L2 > 1e-6) && + ((this_angle_L2 + next_angle_L2 - both_angles_L2) < 1e-3) ) + { + return NODE_SMOOTH; + } + + return NODE_CUSP; +} + +} // end namespace Geom + +/* + 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/helper/geom-nodetype.h b/src/helper/geom-nodetype.h new file mode 100644 index 0000000..d7d4d4c --- /dev/null +++ b/src/helper/geom-nodetype.h @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_GEOM_NODETYPE_H +#define INKSCAPE_HELPER_GEOM_NODETYPE_H + +/** + * @file + * Specific nodetype geometry functions for Inkscape, not provided my lib2geom. + */ +/* + * Author: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> + +namespace Geom { + +/** + * What kind of node is this? This is the value for the node->type + * field. NodeType indicates the degree of continuity required for + * the node. I think that the corresponding integer indicates which + * derivate is connected. (Thus 2 means that the node is continuous + * to the second derivative, i.e. has matching endpoints and tangents) + */ +enum NodeType { +/** Discontinuous node, usually either start or endpoint of a path */ + NODE_NONE, +/** This node continuously joins two segments, but the unit tangent is discontinuous.*/ + NODE_CUSP, +/** This node continuously joins two segments, with continuous *unit* tangent. */ + NODE_SMOOTH, +/** This node is symmetric. I.e. continuously joins two segments with continuous derivative */ + NODE_SYMM +}; + + +NodeType get_nodetype(Curve const &c_incoming, Curve const &c_outgoing); + + +} // namespace Geom + +#endif // INKSCAPE_HELPER_GEOM_NODETYPE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-pathstroke.cpp b/src/helper/geom-pathstroke.cpp new file mode 100644 index 0000000..e706667 --- /dev/null +++ b/src/helper/geom-pathstroke.cpp @@ -0,0 +1,1160 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* Authors: + * Liam P. White + * Tavmjong Bah + * Alexander Brock + * + * Copyright (C) 2014-2015 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <iomanip> +#include <2geom/path-sink.h> +#include <2geom/sbasis-to-bezier.h> // cubicbezierpath_from_sbasis +#include <2geom/path-intersection.h> +#include <2geom/circle.h> + +#include "helper/geom-pathstroke.h" + +namespace Geom { + +static Point intersection_point(Point origin_a, Point vector_a, Point origin_b, Point 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 + vector_a*t; + } + return Point(infinity(), infinity()); +} + +/** +* 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 ) +{ + D2<SBasis> dM=derivative(curve); + if ( are_near(L2sq(dM(t)), tol) ) { + dM=derivative(dM); + } + if ( are_near(L2sq(dM(t)), tol) ) { // try second time + dM=derivative(dM); + } + Piecewise<D2<SBasis> > unitv = unitVector(dM,tol); + 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)); +} + + +// Area of triangle given three corner points +static double area( Geom::Point a, Geom::Point b, Geom::Point c ) { + + using Geom::X; + using Geom::Y; + return( 0.5 * fabs( ( a[X]*(b[Y]-c[Y]) + b[X]*(c[Y]-a[Y]) + c[X]*(a[Y]-b[Y]) ) ) ); +} + +// Alternative touching circle routine directly using Beziers. Works only at end points. +static Circle touching_circle( CubicBezier const &curve, bool start ) { + + double k = 0; + Geom::Point p; + Geom::Point normal; + if ( start ) { + double distance = Geom::distance( curve[1], curve[0] ); + k = 4.0/3.0 * area( curve[0], curve[1], curve[2] ) / + (distance * distance * distance); + if( Geom::cross(curve[0]-curve[1], curve[1]-curve[2]) < 0 ) { + k = -k; + } + p = curve[0]; + normal = Geom::Point(curve[1] - curve[0]).cw(); + normal.normalize(); + // std::cout << "Start k: " << k << " d: " << distance << " normal: " << normal << std::endl; + } else { + double distance = Geom::distance( curve[3], curve[2] ); + k = 4.0/3.0 * area( curve[1], curve[2], curve[3] ) / + (distance * distance * distance); + if( Geom::cross(curve[1]-curve[2], curve[2]-curve[3]) < 0 ) { + k = -k; + } + p = curve[3]; + normal = Geom::Point(curve[3] - curve[2]).cw(); + normal.normalize(); + // std::cout << "End k: " << k << " d: " << distance << " normal: " << normal << std::endl; + } + if( k == 0 ) { + return Geom::Circle( Geom::Point(0,std::numeric_limits<float>::infinity()), + std::numeric_limits<float>::infinity()); + } else { + double radius = 1/k; + Geom::Point center = p + normal * radius; + return Geom::Circle( center, fabs(radius) ); + } +} +} + +namespace { + +// Internal data structure + +struct join_data { + join_data(Geom::Path &_res, Geom::Path const&_outgoing, Geom::Point _in_tang, Geom::Point _out_tang, double _miter, double _width) + : res(_res), outgoing(_outgoing), in_tang(_in_tang), out_tang(_out_tang), miter(_miter), width(_width) {}; + + // contains the current path that is being built on + Geom::Path &res; + + // contains the next curve to append + Geom::Path const& outgoing; + + // input tangents + Geom::Point in_tang; + Geom::Point out_tang; + + // line parameters + double miter; + double width; // half stroke width +}; + +// Join functions must append the outgoing path + +typedef void join_func(join_data jd); + +void bevel_join(join_data jd) +{ + jd.res.appendNew<Geom::LineSegment>(jd.outgoing.initialPoint()); + jd.res.append(jd.outgoing); +} + +void round_join(join_data jd) +{ + jd.res.appendNew<Geom::EllipticalArc>(jd.width, jd.width, 0, false, jd.width <= 0, jd.outgoing.initialPoint()); + jd.res.append(jd.outgoing); +} + +void miter_join_internal(join_data jd, bool clip) +{ + using namespace Geom; + + Curve const& incoming = jd.res.back(); + Curve const& outgoing = jd.outgoing.front(); + Path &res = jd.res; + double width = jd.width, miter = jd.miter; + + Point tang1 = jd.in_tang; + Point tang2 = jd.out_tang; + Point p = intersection_point(incoming.finalPoint(), tang1, outgoing.initialPoint(), tang2); + + bool satisfied = false; + bool inc_ls = res.back_open().degreesOfFreedom() <= 4; + + if (p.isFinite()) { + // check size of miter + Point point_on_path = incoming.finalPoint() + rot90(tang1)*width; + // SVG defines miter length as distance between inner intersection and outer intersection, + // which is twice the distance from p to point_on_path but width is half stroke width. + satisfied = distance(p, point_on_path) <= miter * width; + if (satisfied) { + // miter OK, check to see if we can do a relocation + if (inc_ls) { + res.setFinal(p); + } else { + res.appendNew<LineSegment>(p); + } + } else if (clip) { + // std::cout << " Clipping ------------ " << std::endl; + // miter needs clipping, find two points + Point bisector_versor = Line(point_on_path, p).versor(); + Point point_limit = point_on_path + miter * width * bisector_versor; + // std::cout << " bisector_versor: " << bisector_versor << std::endl; + // std::cout << " point_limit: " << point_limit << std::endl; + Point p1 = intersection_point(incoming.finalPoint(), tang1, point_limit, bisector_versor.cw()); + Point p2 = intersection_point(outgoing.initialPoint(), tang2, point_limit, bisector_versor.cw()); + // std::cout << " p1: " << p1 << std::endl; + // std::cout << " p2: " << p2 << std::endl; + if (inc_ls) { + res.setFinal(p1); + } else { + res.appendNew<LineSegment>(p1); + } + res.appendNew<LineSegment>(p2); + } + } + + res.appendNew<LineSegment>(outgoing.initialPoint()); + + // check if we can do another relocation + bool out_ls = outgoing.degreesOfFreedom() <= 4; + + if ((satisfied || clip) && out_ls) { + res.setFinal(outgoing.finalPoint()); + } else { + res.append(outgoing); + } + + // either way, add the rest of the path + res.insert(res.end(), ++jd.outgoing.begin(), jd.outgoing.end()); +} + +void miter_join(join_data jd) { miter_join_internal(jd, false); } +void miter_clip_join(join_data jd) { miter_join_internal(jd, true); } + +Geom::Point pick_solution(std::vector<Geom::ShapeIntersection> points, Geom::Point tang2, Geom::Point endPt) +{ + assert(points.size() == 2); + Geom::Point sol; + if ( dot(tang2, points[0].point() - endPt) > 0 ) { + // points[0] is bad, choose points[1] + sol = points[1]; + } else if ( dot(tang2, points[1].point() - endPt) > 0 ) { // points[0] could be good, now check points[1] + // points[1] is bad, choose points[0] + sol = points[0]; + } else { + // both points are good, choose nearest + sol = ( distanceSq(endPt, points[0].point()) < distanceSq(endPt, points[1].point()) ) + ? points[0].point() : points[1].point(); + } + return sol; +} + +// Arcs line join. If two circles don't intersect, expand inner circle. +Geom::Point expand_circle( Geom::Circle &inner_circle, Geom::Circle const &outer_circle, Geom::Point const &start_pt, Geom::Point const &start_tangent ) { + // std::cout << "expand_circle:" << std::endl; + // std::cout << " outer_circle: radius: " << outer_circle.radius() << " center: " << outer_circle.center() << std::endl; + // std::cout << " start: point: " << start_pt << " tangent: " << start_tangent << std::endl; + + if( !(outer_circle.contains(start_pt) ) ) { + // std::cout << " WARNING: Outer circle does not contain starting point!" << std::endl; + return Geom::Point(0,0); + } + + Geom::Line secant1(start_pt, start_pt + start_tangent); + std::vector<Geom::ShapeIntersection> chord1_pts = outer_circle.intersect(secant1); + // std::cout << " chord1: " << chord1_pts[0].point() << ", " << chord1_pts[1].point() << std::endl; + Geom::LineSegment chord1(chord1_pts[0].point(), chord1_pts[1].point()); + + Geom::Line bisector = make_bisector_line( chord1 ); + std::vector<Geom::ShapeIntersection> chord2_pts = outer_circle.intersect(bisector); + // std::cout << " chord2: " << chord2_pts[0].point() << ", " << chord2_pts[1].point() << std::endl; + Geom::LineSegment chord2(chord2_pts[0].point(), chord2_pts[1].point()); + + // Find D, point on chord2 and on circle closest to start point + Geom::Coord d0 = Geom::distance(chord2_pts[0].point(),start_pt); + Geom::Coord d1 = Geom::distance(chord2_pts[1].point(),start_pt); + // std::cout << " d0: " << d0 << " d1: " << d1 << std::endl; + Geom::Point d = (d0 < d1) ? chord2_pts[0].point() : chord2_pts[1].point(); + Geom::Line da(d,start_pt); + + // Chord through start point and point D + std::vector<Geom::ShapeIntersection> chord3_pts = outer_circle.intersect(da); + // std::cout << " chord3: " << chord3_pts[0].point() << ", " << chord3_pts[1].point() << std::endl; + + // Find farthest point on chord3 and on circle (could be more robust) + Geom::Coord d2 = Geom::distance(chord3_pts[0].point(),d); + Geom::Coord d3 = Geom::distance(chord3_pts[1].point(),d); + // std::cout << " d2: " << d2 << " d3: " << d3 << std::endl; + + // Find point P, the intersection of outer circle and new inner circle + Geom::Point p = (d2 > d3) ? chord3_pts[0].point() : chord3_pts[1].point(); + + // Find center of new circle: it is at the intersection of the bisector + // of the chord defined by the start point and point P and a line through + // the start point and parallel to the first bisector. + Geom::LineSegment chord4(start_pt,p); + Geom::Line bisector2 = make_bisector_line( chord4 ); + Geom::Line diameter = make_parallel_line( start_pt, bisector ); + std::vector<Geom::ShapeIntersection> center_new = bisector2.intersect( diameter ); + // std::cout << " center_new: " << center_new[0].point() << std::endl; + Geom::Coord r_new = Geom::distance( center_new[0].point(), start_pt ); + // std::cout << " r_new: " << r_new << std::endl; + + inner_circle.setCenter( center_new[0].point() ); + inner_circle.setRadius( r_new ); + return p; +} + +// Arcs line join. If two circles don't intersect, adjust both circles so they just touch. +// Increase (decrease) the radius of circle 1 and decrease (increase) of circle 2 by the same amount keeping the given points and tangents fixed. +Geom::Point adjust_circles( Geom::Circle &circle1, Geom::Circle &circle2, Geom::Point const &point1, Geom::Point const &point2, Geom::Point const &tan1, Geom::Point const &tan2 ) { + + Geom::Point n1 = (circle1.center() - point1).normalized(); // Always points towards center + Geom::Point n2 = (circle2.center() - point2).normalized(); + Geom::Point sum_n = n1 + n2; + + double r1 = circle1.radius(); + double r2 = circle2.radius(); + double delta_r = r2 - r1; + Geom::Point c1 = circle1.center(); + Geom::Point c2 = circle2.center(); + Geom::Point delta_c = c2 - c1; + + // std::cout << "adjust_circles:" << std::endl; + // std::cout << " norm: " << n1 << "; " << n2 << std::endl; + // std::cout << " sum_n: " << sum_n << std::endl; + // std::cout << " delta_r: " << delta_r << std::endl; + // std::cout << " delta_c: " << delta_c << std::endl; + + // Quadratic equation + double A = 4 - sum_n.length() * sum_n.length(); + double B = 4.0 * delta_r - 2.0 * Geom::dot( delta_c, sum_n ); + double C = delta_r * delta_r - delta_c.length() * delta_c.length(); + + double s1 = 0; + double s2 = 0; + + if( fabs(A) < 0.01 ) { + // std::cout << " A near zero! $$$$$$$$$$$$$$$$$$" << std::endl; + if( B != 0 ) { + s1 = -C/B; + s2 = -s1; + } + } else { + s1 = (-B + sqrt(B*B - 4*A*C))/(2*A); + s2 = (-B - sqrt(B*B - 4*A*C))/(2*A); + } + + double dr = (fabs(s1)<=fabs(s2)?s1:s2); + + // std::cout << " A: " << A << std::endl; + // std::cout << " B: " << B << std::endl; + // std::cout << " C: " << C << std::endl; + // std::cout << " s1: " << s1 << " s2: " << s2 << " dr: " << dr << std::endl; + + circle1 = Geom::Circle( c1 - dr*n1, r1-dr ); + circle2 = Geom::Circle( c2 + dr*n2, r2+dr ); + + // std::cout << " C1: " << circle1 << std::endl; + // std::cout << " C2: " << circle2 << std::endl; + // std::cout << " d': " << Geom::Point( circle1.center() - circle2.center() ).length() << std::endl; + + Geom::Line bisector( circle1.center(), circle2.center() ); + std::vector<Geom::ShapeIntersection> points = circle1.intersect( bisector ); + Geom::Point p0 = points[0].point(); + Geom::Point p1 = points[1].point(); + // std::cout << " points: " << p0 << "; " << p1 << std::endl; + if( std::abs( Geom::distance( p0, circle2.center() ) - circle2.radius() ) < + std::abs( Geom::distance( p1, circle2.center() ) - circle2.radius() ) ) { + return p0; + } else { + return p1; + } +} + +void extrapolate_join_internal(join_data jd, int alternative) +{ + // std::cout << "\nextrapolate_join: entrance: alternative: " << alternative << std::endl; + using namespace Geom; + + Geom::Path &res = jd.res; + Geom::Curve const& incoming = res.back(); + Geom::Curve const& outgoing = jd.outgoing.front(); + Geom::Point startPt = incoming.finalPoint(); + Geom::Point endPt = outgoing.initialPoint(); + Geom::Point tang1 = jd.in_tang; + Geom::Point tang2 = jd.out_tang; + // width is half stroke-width + double width = jd.width, miter = jd.miter; + + // std::cout << " startPt: " << startPt << " endPt: " << endPt << std::endl; + // std::cout << " tang1: " << tang1 << " tang2: " << tang2 << std::endl; + // std::cout << " dot product: " << Geom::dot( tang1, tang2 ) << std::endl; + // std::cout << " width: " << width << std::endl; + + // CIRCLE CALCULATION TESTING + Geom::Circle circle1 = touching_circle(Geom::reverse(incoming.toSBasis()), 0.); + Geom::Circle circle2 = touching_circle(outgoing.toSBasis(), 0); + // std::cout << " circle1: " << circle1 << std::endl; + // std::cout << " circle2: " << circle2 << std::endl; + if( Geom::CubicBezier const * in_bezier = dynamic_cast<Geom::CubicBezier const*>(&incoming) ) { + Geom::Circle circle_test1 = touching_circle(*in_bezier, false); + if( !Geom::are_near( circle1, circle_test1, 0.01 ) ) { + // std::cout << " Circle 1 error!!!!!!!!!!!!!!!!!" << std::endl; + // std::cout << " " << circle_test1 << std::endl; + } + } + if( Geom::CubicBezier const * out_bezier = dynamic_cast<Geom::CubicBezier const*>(&outgoing) ) { + Geom::Circle circle_test2 = touching_circle(*out_bezier, true); + if( !Geom::are_near( circle2, circle_test2, 0.01 ) ) { + // std::cout << " Circle 2 error!!!!!!!!!!!!!!!!!" << std::endl; + // std::cout << " " << circle_test2 << std::endl; + } + } + // END TESTING + + Geom::Point center1 = circle1.center(); + Geom::Point center2 = circle2.center(); + double side1 = tang1[Geom::X]*(startPt[Geom::Y]-center1[Geom::Y]) - tang1[Geom::Y]*(startPt[Geom::X]-center1[Geom::X]); + double side2 = tang2[Geom::X]*( endPt[Geom::Y]-center2[Geom::Y]) - tang2[Geom::Y]*( endPt[Geom::X]-center2[Geom::X]); + // std::cout << " side1: " << side1 << " side2: " << side2 << std::endl; + + bool inc_ls = !circle1.center().isFinite(); + bool out_ls = !circle2.center().isFinite(); + + std::vector<Geom::ShapeIntersection> points; + + Geom::EllipticalArc *arc1 = nullptr; + Geom::EllipticalArc *arc2 = nullptr; + Geom::LineSegment *seg1 = nullptr; + Geom::LineSegment *seg2 = nullptr; + Geom::Point sol; + Geom::Point p1; + Geom::Point p2; + + if (!inc_ls && !out_ls) { + // std::cout << " two circles" << std::endl; + + // See if tangent is backwards (radius < width/2 and circle is inside stroke). + Geom::Point node_on_path = startPt + Geom::rot90(tang1)*width; + // std::cout << " node_on_path: " << node_on_path << " -------------- " << std::endl; + bool b1 = false; + bool b2 = false; + if (circle1.radius() < width && distance( circle1.center(), node_on_path ) < width) { + b1 = true; + } + if (circle2.radius() < width && distance( circle2.center(), node_on_path ) < width) { + b2 = true; + } + // std::cout << " b1: " << (b1?"true":"false") + // << " b2: " << (b2?"true":"false") << std::endl; + + // Two circles + points = circle1.intersect(circle2); + + if (points.size() != 2) { + // std::cout << " Circles do not intersect, do backup" << std::endl; + switch (alternative) { + case 1: + { + // Fallback to round if one path has radius smaller than half line width. + if(b1 || b2) { + // std::cout << "At one least path has radius smaller than half line width." << std::endl; + return( round_join(jd) ); + } + + Point p; + if( circle2.contains( startPt ) && !circle1.contains( endPt ) ) { + // std::cout << "Expand circle1" << std::endl; + p = expand_circle( circle1, circle2, startPt, tang1 ); + points.emplace_back( 0, 0, p ); + points.emplace_back( 0, 0, p ); + } else if( circle1.contains( endPt ) && !circle2.contains( startPt ) ) { + // std::cout << "Expand circle2" << std::endl; + p = expand_circle( circle2, circle1, endPt, tang2 ); + points.emplace_back( 0, 0, p ); + points.emplace_back( 0, 0, p ); + } else { + // std::cout << "Either both points inside or both outside" << std::endl; + return( miter_clip_join(jd) ); + } + break; + + } + case 2: + { + // Fallback to round if one path has radius smaller than half line width. + if(b1 || b2) { + // std::cout << "At one least path has radius smaller than half line width." << std::endl; + return( round_join(jd) ); + } + + if( ( circle2.contains( startPt ) && !circle1.contains( endPt ) ) || + ( circle1.contains( endPt ) && !circle2.contains( startPt ) ) ) { + + Geom::Point apex = adjust_circles( circle1, circle2, startPt, endPt, tang1, tang2 ); + points.emplace_back( 0, 0, apex ); + points.emplace_back( 0, 0, apex ); + } else { + // std::cout << "Either both points inside or both outside" << std::endl; + return( miter_clip_join(jd) ); + } + + break; + } + case 3: + if( side1 > 0 ) { + Geom::Line secant(startPt, startPt + tang1); + points = circle2.intersect(secant); + circle1.setRadius(std::numeric_limits<float>::infinity()); + circle1.setCenter(Geom::Point(0,std::numeric_limits<float>::infinity())); + } else { + Geom::Line secant(endPt, endPt + tang2); + points = circle1.intersect(secant); + circle2.setRadius(std::numeric_limits<float>::infinity()); + circle2.setCenter(Geom::Point(0,std::numeric_limits<float>::infinity())); + } + break; + + + case 0: + default: + // Do nothing + break; + } + } + + if (points.size() == 2) { + sol = pick_solution(points, tang2, endPt); + if( circle1.radius() != std::numeric_limits<float>::infinity() ) { + arc1 = circle1.arc(startPt, 0.5*(startPt+sol), sol); + } else { + seg1 = new Geom::LineSegment(startPt, sol); + } + if( circle2.radius() != std::numeric_limits<float>::infinity() ) { + arc2 = circle2.arc(sol, 0.5*(sol+endPt), endPt); + } else { + seg2 = new Geom::LineSegment(sol, endPt); + } + } + } else if (inc_ls && !out_ls) { + // Line and circle + // std::cout << " line circle" << std::endl; + points = circle2.intersect(Line(incoming.initialPoint(), incoming.finalPoint())); + if (points.size() == 2) { + sol = pick_solution(points, tang2, endPt); + arc2 = circle2.arc(sol, 0.5*(sol+endPt), endPt); + } + } else if (!inc_ls && out_ls) { + // Circle and line + // std::cout << " circle line" << std::endl; + points = circle1.intersect(Line(outgoing.initialPoint(), outgoing.finalPoint())); + if (points.size() == 2) { + sol = pick_solution(points, tang2, endPt); + arc1 = circle1.arc(startPt, 0.5*(sol+startPt), sol); + } + } + if (points.size() != 2) { + // std::cout << " no solutions" << std::endl; + // no solutions available, fall back to miter + return miter_join(jd); + } + + // We have a solution, thus sol is defined. + p1 = sol; + + // See if we need to clip. Miter length is measured along a circular arc that is tangent to the + // bisector of the incoming and out going angles and passes through the end point (sol) of the + // line join. + + // Center of circle is intersection of a line orthogonal to bisector and a line bisecting + // a chord connecting the path end point (point_on_path) and the join end point (sol). + Geom::Point point_on_path = startPt + Geom::rot90(tang1)*width; + Geom::Line bisector = make_angle_bisector_line(startPt, point_on_path, endPt); + Geom::Line ortho = make_orthogonal_line(point_on_path, bisector); + + Geom::LineSegment chord(point_on_path, sol); + Geom::Line bisector_chord = make_bisector_line(chord); + + Geom::Line limit_line; + double miter_limit = width * miter; + bool clipped = false; + + if (are_parallel(bisector_chord, ortho)) { + // No intersection (can happen if curvatures are equal but opposite) + if (Geom::distance(point_on_path, sol) > miter_limit) { + clipped = true; + Geom::Point temp = bisector.versor(); + Geom::Point limit_point = point_on_path + miter_limit * temp; + limit_line = make_parallel_line( limit_point, ortho ); + } + } else { + Geom::Point center = + Geom::intersection_point( bisector_chord.pointAt(0), bisector_chord.versor(), + ortho.pointAt(0), ortho.versor() ); + Geom::Coord radius = distance(center, point_on_path); + Geom::Circle circle_center(center, radius); + + double limit_angle = miter_limit / radius; + + Geom::Ray start_ray(center, point_on_path); + Geom::Ray end_ray(center, sol); + Geom::Line limit_line(center, 0); // Angle set below + + if (Geom::cross(start_ray.versor(), end_ray.versor()) < 0) { + limit_line.setAngle(start_ray.angle() - limit_angle); + } else { + limit_line.setAngle(start_ray.angle() + limit_angle); + } + + Geom::EllipticalArc *arc_center = circle_center.arc(point_on_path, 0.5*(point_on_path + sol), sol); + if (arc_center && arc_center->sweepAngle() > limit_angle) { + // We need to clip + clipped = true; + + if (!inc_ls) { + // Incoming circular + points = circle1.intersect(limit_line); + if (points.size() == 2) { + p1 = pick_solution(points, tang2, endPt); + delete arc1; + arc1 = circle1.arc(startPt, 0.5*(p1+startPt), p1); + } + } else { + p1 = Geom::intersection_point(startPt, tang1, limit_line.pointAt(0), limit_line.versor()); + } + + if (!out_ls) { + // Outgoing circular + points = circle2.intersect(limit_line); + if (points.size() == 2) { + p2 = pick_solution(points, tang1, endPt); + delete arc2; + arc2 = circle2.arc(p2, 0.5*(p2+endPt), endPt); + } + } else { + p2 = Geom::intersection_point(endPt, tang2, limit_line.pointAt(0), limit_line.versor()); + } + } + } + + // Add initial + if (arc1) { + res.append(*arc1); + } else if (seg1 ) { + res.append(*seg1); + } else { + // Straight line segment: move last point + res.setFinal(p1); + } + + if (clipped) { + res.appendNew<Geom::LineSegment>(p2); + } + + // Add outgoing + if (arc2) { + res.append(*arc2); + res.append(outgoing); + } else if (seg2 ) { + res.append(*seg2); + res.append(outgoing); + } else { + // Straight line segment: + res.appendNew<Geom::LineSegment>(outgoing.finalPoint()); + } + + // add the rest of the path + res.insert(res.end(), ++jd.outgoing.begin(), jd.outgoing.end()); + + delete arc1; + delete arc2; + delete seg1; + delete seg2; +} + +void extrapolate_join( join_data jd) { extrapolate_join_internal(jd, 0); } +void extrapolate_join_alt1(join_data jd) { extrapolate_join_internal(jd, 1); } +void extrapolate_join_alt2(join_data jd) { extrapolate_join_internal(jd, 2); } +void extrapolate_join_alt3(join_data jd) { extrapolate_join_internal(jd, 3); } + + +void tangents(Geom::Point tang[2], Geom::Curve const& incoming, Geom::Curve const& outgoing) +{ + Geom::Point tang1 = Geom::unitTangentAt(reverse(incoming.toSBasis()), 0.); + Geom::Point tang2 = outgoing.unitTangentAt(0.); + tang[0] = tang1, tang[1] = tang2; +} + +// Offsetting a line segment is mathematically stable and quick to do +Geom::LineSegment offset_line(Geom::LineSegment const& l, double width) +{ + Geom::Point tang1 = Geom::rot90(l.unitTangentAt(0)); + Geom::Point tang2 = Geom::rot90(unitTangentAt(reverse(l.toSBasis()), 0.)); + + Geom::Point start = l.initialPoint() + tang1 * width; + Geom::Point end = l.finalPoint() - tang2 * width; + + return Geom::LineSegment(start, end); +} + +void get_cubic_data(Geom::CubicBezier const& bez, double time, double& len, double& rad) +{ + // get derivatives + std::vector<Geom::Point> derivs = bez.pointAndDerivatives(time, 3); + + Geom::Point der1 = derivs[1]; // first deriv (tangent vector) + Geom::Point der2 = derivs[2]; // second deriv (tangent's tangent) + double l = Geom::L2(der1); // length + + len = rad = 0; + + // TODO: we might want to consider using Geom::touching_circle to determine the + // curvature radius here. Less code duplication, but slower + + if (Geom::are_near(l, 0, 1e-4)) { + l = Geom::L2(der2); + Geom::Point der3 = derivs.at(3); // try second time + if (Geom::are_near(l, 0, 1e-4)) { + l = Geom::L2(der3); + if (Geom::are_near(l, 0)) { + return; // this isn't a segment... + } + rad = 1e8; + } else { + rad = -l * (Geom::dot(der2, der2) / Geom::cross(der2, der3)); + } + } else { + rad = -l * (Geom::dot(der1, der1) / Geom::cross(der1, der2)); + } + len = l; +} + +double _offset_cubic_stable_sub( + Geom::CubicBezier const& bez, + Geom::CubicBezier& c, + const Geom::Point& start_normal, + const Geom::Point& end_normal, + const Geom::Point& start_new, + const Geom::Point& end_new, + const double start_rad, + const double end_rad, + const double start_len, + const double end_len, + const double width, + const double width_correction) { + using Geom::X; + using Geom::Y; + + double start_off = 1, end_off = 1; + // correction of the lengths of the tangent to the offset + if (!Geom::are_near(start_rad, 0)) + start_off += (width + width_correction) / start_rad; + if (!Geom::are_near(end_rad, 0)) + end_off += (width + width_correction) / end_rad; + + // We don't change the direction of the control points + if (start_off < 0) { + start_off = 0; + } + if (end_off < 0) { + end_off = 0; + } + start_off *= start_len; + end_off *= end_len; + // -------- + + Geom::Point mid1_new = start_normal.ccw()*start_off; + mid1_new = Geom::Point(start_new[X] + mid1_new[X]/3., start_new[Y] + mid1_new[Y]/3.); + Geom::Point mid2_new = end_normal.ccw()*end_off; + mid2_new = Geom::Point(end_new[X] - mid2_new[X]/3., end_new[Y] - mid2_new[Y]/3.); + + // create the estimate curve + c = Geom::CubicBezier(start_new, mid1_new, mid2_new, end_new); + + // check the tolerance for our estimate to be a parallel curve + + double worst_residual = 0; + for (size_t ii = 3; ii <= 7; ii+=2) { + const double t = static_cast<double>(ii) / 10; + const Geom::Point req = bez.pointAt(t); + const Geom::Point chk = c.pointAt(c.nearestTime(req)); + const double current_residual = (chk-req).length() - std::abs(width); + if (std::abs(current_residual) > std::abs(worst_residual)) { + worst_residual = current_residual; + } + } + return worst_residual; +} + +void offset_cubic(Geom::Path& p, Geom::CubicBezier const& bez, double width, double tol, size_t levels) +{ + using Geom::X; + using Geom::Y; + + const Geom::Point start_pos = bez.initialPoint(); + const Geom::Point end_pos = bez.finalPoint(); + + const Geom::Point start_normal = Geom::rot90(bez.unitTangentAt(0)); + const Geom::Point end_normal = -Geom::rot90(Geom::unitTangentAt(Geom::reverse(bez.toSBasis()), 0.)); + + // offset the start and end control points out by the width + const Geom::Point start_new = start_pos + start_normal*width; + const Geom::Point end_new = end_pos + end_normal*width; + + // -------- + double start_rad, end_rad; + double start_len, end_len; // tangent lengths + get_cubic_data(bez, 0, start_len, start_rad); + get_cubic_data(bez, 1, end_len, end_rad); + + + Geom::CubicBezier c; + + double best_width_correction = 0; + double best_residual = _offset_cubic_stable_sub( + bez, c, + start_normal, end_normal, + start_new, end_new, + start_rad, end_rad, + start_len, end_len, + width, best_width_correction); + double stepsize = std::abs(width)/2; + bool seen_success = false; + double stepsize_threshold = 0; + // std::cout << "Residual from " << best_residual << " "; + size_t ii = 0; + for (; ii < 100 && stepsize > stepsize_threshold; ++ii) { + bool success = false; + const double width_correction = best_width_correction - (best_residual > 0 ? 1 : -1) * stepsize; + Geom::CubicBezier current_curve; + const double residual = _offset_cubic_stable_sub( + bez, current_curve, + start_normal, end_normal, + start_new, end_new, + start_rad, end_rad, + start_len, end_len, + width, width_correction); + if (std::abs(residual) < std::abs(best_residual)) { + best_residual = residual; + best_width_correction = width_correction; + c = current_curve; + success = true; + if (std::abs(best_residual) < tol/4) { + break; + } + } + + if (success) { + if (!seen_success) { + seen_success = true; + //std::cout << "Stepsize factor: " << std::abs(width) / stepsize << std::endl; + stepsize_threshold = stepsize / 1000; + } + } + else { + stepsize /= 2; + } + if (std::abs(best_width_correction) >= std::abs(width)/2) { + //break; // Seems to prevent some numerical instabilities, not clear if useful + } + } + + // reached maximum recursive depth + // don't bother with any more correction + if (levels == 0) { + try { + p.append(c); + } + catch (...) { + if ((p.finalPoint() - c.initialPoint()).length() < 1e-6) { + c.setInitial(p.finalPoint()); + } + else { + auto line = Geom::LineSegment(p.finalPoint(), c.initialPoint()); + p.append(line); + } + p.append(c); + } + + return; + } + + // We find the point on our new curve (c) for which the distance between + // (c) and (bez) differs the most from the desired distance (width). + double worst_err = std::abs(best_residual); + double worst_time = .5; + for (size_t ii = 1; ii <= 9; ++ii) { + const double t = static_cast<double>(ii) / 10; + const Geom::Point req = bez.pointAt(t); + // We use the exact solution with nearestTime because it is numerically + // much more stable than simply assuming that the point on (c) closest + // to bez.pointAt(t) is given by c.pointAt(t) + const Geom::Point chk = c.pointAt(c.nearestTime(req)); + + Geom::Point const diff = req - chk; + const double err = std::abs(diff.length() - std::abs(width)); + if (err > worst_err) { + worst_err = err; + worst_time = t; + } + } + + if (worst_err < tol) { + if (Geom::are_near(start_new, p.finalPoint())) { + p.setFinal(start_new); // if it isn't near, we throw + } + + // we're good, curve is accurate enough + p.append(c); + return; + } else { + // split the curve in two + std::pair<Geom::CubicBezier, Geom::CubicBezier> s = bez.subdivide(worst_time); + offset_cubic(p, s.first, width, tol, levels - 1); + offset_cubic(p, s.second, width, tol, levels - 1); + } +} + +void offset_quadratic(Geom::Path& p, Geom::QuadraticBezier const& bez, double width, double tol, size_t levels) +{ + // cheat + // it's faster + // seriously + std::vector<Geom::Point> points = bez.controlPoints(); + Geom::Point b1 = points[0] + (2./3) * (points[1] - points[0]); + Geom::Point b2 = b1 + (1./3) * (points[2] - points[0]); + Geom::CubicBezier cub = Geom::CubicBezier(points[0], b1, b2, points[2]); + offset_cubic(p, cub, width, tol, levels); +} + +void offset_curve(Geom::Path& res, Geom::Curve const* current, double width, double tolerance) +{ + size_t levels = 8; + + if (current->isDegenerate()) return; // don't do anything + + // TODO: we can handle SVGEllipticalArc here as well, do that! + + if (Geom::BezierCurve const *b = dynamic_cast<Geom::BezierCurve const*>(current)) { + size_t order = b->order(); + switch (order) { + case 1: + res.append(offset_line(static_cast<Geom::LineSegment const&>(*current), width)); + break; + case 2: { + Geom::QuadraticBezier const& q = static_cast<Geom::QuadraticBezier const&>(*current); + offset_quadratic(res, q, width, tolerance, levels); + break; + } + case 3: { + Geom::CubicBezier const& cb = static_cast<Geom::CubicBezier const&>(*current); + offset_cubic(res, cb, width, tolerance, levels); + break; + } + default: { + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), tolerance); + for (const auto & i : sbasis_path) + offset_curve(res, &i, width, tolerance); + break; + } + } + } else { + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(current->toSBasis(), 0.1); + for (const auto & i : sbasis_path) + offset_curve(res, &i, width, tolerance); + } +} + +typedef void cap_func(Geom::PathBuilder& res, Geom::Path const& with_dir, Geom::Path const& against_dir, double width); + +void flat_cap(Geom::PathBuilder& res, Geom::Path const&, Geom::Path const& against_dir, double) +{ + res.lineTo(against_dir.initialPoint()); +} + +void round_cap(Geom::PathBuilder& res, Geom::Path const&, Geom::Path const& against_dir, double width) +{ + res.arcTo(width / 2., width / 2., 0., true, false, against_dir.initialPoint()); +} + +void square_cap(Geom::PathBuilder& res, Geom::Path const& with_dir, Geom::Path const& against_dir, double width) +{ + width /= 2.; + Geom::Point normal_1 = -Geom::unitTangentAt(Geom::reverse(with_dir.back().toSBasis()), 0.); + Geom::Point normal_2 = -against_dir[0].unitTangentAt(0.); + res.lineTo(with_dir.finalPoint() + normal_1*width); + res.lineTo(against_dir.initialPoint() + normal_2*width); + res.lineTo(against_dir.initialPoint()); +} + +void peak_cap(Geom::PathBuilder& res, Geom::Path const& with_dir, Geom::Path const& against_dir, double width) +{ + width /= 2.; + Geom::Point normal_1 = -Geom::unitTangentAt(Geom::reverse(with_dir.back().toSBasis()), 0.); + Geom::Point normal_2 = -against_dir[0].unitTangentAt(0.); + Geom::Point midpoint = ((with_dir.finalPoint() + normal_1*width) + (against_dir.initialPoint() + normal_2*width)) * 0.5; + res.lineTo(midpoint); + res.lineTo(against_dir.initialPoint()); +} + +} // namespace + +namespace Inkscape { + +Geom::PathVector outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join, + LineCapType butt, + double tolerance) +{ + if (input.size() == 0) return Geom::PathVector(); // nope, don't even try + + Geom::PathBuilder res; + Geom::Path with_dir = half_outline(input, width/2., miter, join, tolerance); + Geom::Path against_dir = half_outline(input.reversed(), width/2., miter, join, tolerance); + res.moveTo(with_dir[0].initialPoint()); + res.append(with_dir); + + cap_func *cf; + switch (butt) { + case BUTT_ROUND: + cf = &round_cap; + break; + case BUTT_SQUARE: + cf = &square_cap; + break; + case BUTT_PEAK: + cf = &peak_cap; + break; + default: + cf = &flat_cap; + } + + // glue caps + if (!input.closed()) { + cf(res, with_dir, against_dir, width); + } else { + res.closePath(); + res.moveTo(against_dir.initialPoint()); + } + + res.append(against_dir); + + if (!input.closed()) { + cf(res, against_dir, with_dir, width); + } + + res.closePath(); + res.flush(); + return res.peek(); +} + +Geom::Path half_outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join, + double tolerance) +{ + if (tolerance <= 0) { + if (std::abs(width) > 0) { + tolerance = 5.0 * (std::abs(width)/100); + } + else { + tolerance = 1e-4; + } + } + Geom::Path res; + if (input.size() == 0) return res; + + Geom::Point tang1 = input[0].unitTangentAt(0); + Geom::Point start = input.initialPoint() + tang1 * width; + Geom::Path temp; + Geom::Point tang[2]; + + res.setStitching(true); + temp.setStitching(true); + + res.start(start); + + // Do two curves at a time for efficiency, since the join function needs to know the outgoing curve as well + const Geom::Curve &closingline = input.back_closed(); + const size_t k = (are_near(closingline.initialPoint(), closingline.finalPoint()) && input.closed() ) + ?input.size_open():input.size_default(); + + for (size_t u = 0; u < k; u += 2) { + temp.clear(); + + offset_curve(temp, &input[u], width, tolerance); + + // on the first run through, there isn't a join + if (u == 0) { + res.append(temp); + } else { + tangents(tang, input[u-1], input[u]); + outline_join(res, temp, tang[0], tang[1], width, miter, join); + } + + // odd number of paths + if (u < k - 1) { + temp.clear(); + offset_curve(temp, &input[u+1], width, tolerance); + tangents(tang, input[u], input[u+1]); + outline_join(res, temp, tang[0], tang[1], width, miter, join); + } + } + if (input.closed()) { + Geom::Curve const &c1 = res.back(); + Geom::Curve const &c2 = res.front(); + temp.clear(); + temp.append(c1); + Geom::Path temp2; + temp2.append(c2); + tangents(tang, input.back(), input.front()); + outline_join(temp, temp2, tang[0], tang[1], width, miter, join); + res.erase(res.begin()); + res.erase_last(); + res.append(temp); + res.close(); + } + return res; +} + +void outline_join(Geom::Path &res, Geom::Path const& temp, Geom::Point in_tang, Geom::Point out_tang, double width, double miter, Inkscape::LineJoinType join) +{ + if (res.size() == 0 || temp.size() == 0) + return; + Geom::Curve const& outgoing = temp.front(); + if (Geom::are_near(res.finalPoint(), outgoing.initialPoint(), 0.01)) { + // if the points are /that/ close, just ignore this one + res.setFinal(temp.initialPoint()); + res.append(temp); + return; + } + + join_data jd(res, temp, in_tang, out_tang, miter, width); + if (!(Geom::cross(in_tang, out_tang) > 0)) { + join = Inkscape::JOIN_BEVEL; + } + join_func *jf; + switch (join) { + case Inkscape::JOIN_BEVEL: + jf = &bevel_join; + break; + case Inkscape::JOIN_ROUND: + jf = &round_join; + break; + case Inkscape::JOIN_EXTRAPOLATE: + jf = &extrapolate_join; + break; + case Inkscape::JOIN_EXTRAPOLATE1: + jf = &extrapolate_join_alt1; + break; + case Inkscape::JOIN_EXTRAPOLATE2: + jf = &extrapolate_join_alt2; + break; + case Inkscape::JOIN_EXTRAPOLATE3: + jf = &extrapolate_join_alt3; + break; + case Inkscape::JOIN_MITER_CLIP: + jf = &miter_clip_join; + break; + default: + jf = &miter_join; + } + jf(jd); + } + +} // 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/helper/geom-pathstroke.h b/src/helper/geom-pathstroke.h new file mode 100644 index 0000000..73d35b4 --- /dev/null +++ b/src/helper/geom-pathstroke.h @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_PATH_STROKE_H +#define INKSCAPE_HELPER_PATH_STROKE_H + +/* Authors: + * Liam P. White + * Tavmjong Bah + * + * Copyright (C) 2014-2015 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/path.h> +#include <2geom/pathvector.h> + +namespace Inkscape { + +enum LineJoinType { + JOIN_BEVEL, + JOIN_ROUND, + JOIN_MITER, + JOIN_MITER_CLIP, + JOIN_EXTRAPOLATE, + JOIN_EXTRAPOLATE1, + JOIN_EXTRAPOLATE2, + JOIN_EXTRAPOLATE3, +}; + +enum LineCapType { + BUTT_FLAT, + BUTT_ROUND, + BUTT_SQUARE, + BUTT_PEAK, // This is not a line ending supported by the SVG standard. +}; + +/** + * Strokes the path given by @a input. + * Joins may behave oddly if the width is negative. + * + * @param[in] input Input path. + * @param[in] width Stroke width. + * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE. + * @param[in] join Line join type used during offset. Member of LineJoinType enum. + * @param[in] cap Line cap type used during stroking. Member of LineCapType enum. + * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width. + * + * @return Stroked path. + * If the input path is closed, the resultant vector will contain two paths. + * Otherwise, there should be only one in the output. + */ +Geom::PathVector outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join = JOIN_BEVEL, + LineCapType cap = BUTT_FLAT, + double tolerance = -1); + +/** + * Offset the input path by @a width. + * Joins may behave oddly if the width is negative. + * + * @param[in] input Input path. + * @param[in] width Amount to offset. + * @param[in] miter Miter limit. Only used when @a join is one of JOIN_MITER, JOIN_MITER_CLIP, and JOIN_EXTRAPOLATE. + * @param[in] join Line join type used during offset. Member of LineJoinType enum. + * @param[in] tolerance Tolerance, values smaller than 0 lead to automatic tolerance depending on width. + * + * @return Offsetted output. + */ +Geom::Path half_outline( + Geom::Path const& input, + double width, + double miter, + LineJoinType join = JOIN_BEVEL, + double tolerance = -1); + +/** + * Builds a join on the provided path. + * Joins may behave oddly if the width is negative. + * + * @param[inout] res The path to build the join on. + * The outgoing path (or a portion thereof) will be appended after the join is created. + * Previous segments may be modified as an optimization, beware! + * + * @param[in] outgoing The segment to append on the outgoing portion of the join. + * @param[in] in_tang The end tangent to consider on the input path. + * @param[in] out_tang The begin tangent to consider on the output path. + * @param[in] width + * @param[in] miter + * @param[in] join + */ +void outline_join(Geom::Path &res, Geom::Path const& outgoing, Geom::Point in_tang, Geom::Point out_tang, double width, double miter, LineJoinType join); + +} // namespace Inkscape + +#endif // INKSCAPE_HELPER_PATH_STROKE_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 : diff --git a/src/helper/geom-pathvectorsatellites.cpp b/src/helper/geom-pathvectorsatellites.cpp new file mode 100644 index 0000000..dd625b1 --- /dev/null +++ b/src/helper/geom-pathvectorsatellites.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * PathVectorSatellites a class to manage satellites -per node extra data- in a pathvector + *//* + * Authors: see git history + * Jabiertxof + * Nathan Hurst + * Johan Engelen + * Josh Andler + * suv + * Mc- + * Liam P. White + * Krzysztof KosiĆski + * This code is in public domain + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <helper/geom.h> +#include <helper/geom-pathvectorsatellites.h> +#include "util/units.h" + +Geom::PathVector PathVectorSatellites::getPathVector() const +{ + return _pathvector; +} + +void PathVectorSatellites::setPathVector(Geom::PathVector pathv) +{ + _pathvector = pathv; +} + +Satellites PathVectorSatellites::getSatellites() +{ + return _satellites; +} + +void PathVectorSatellites::setSatellites(Satellites satellites) +{ + _satellites = satellites; +} + +size_t PathVectorSatellites::getTotalSatellites() +{ + size_t counter = 0; + for (auto & _satellite : _satellites) { + counter += _satellite.size(); + } + return counter; +} + +std::pair<size_t, size_t> PathVectorSatellites::getIndexData(size_t index) +{ + size_t counter = 0; + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + if (index == counter) { + return std::make_pair(i,j); + } + counter++; + } + } + return std::make_pair(0,0); +} + +void PathVectorSatellites::setSelected(std::vector<size_t> selected) +{ + size_t counter = 0; + for (auto & _satellite : _satellites) { + for (auto & j : _satellite) { + if (find (selected.begin(), selected.end(), counter) != selected.end()) { + j.setSelected(true); + } else { + j.setSelected(false); + } + counter++; + } + } +} + +void PathVectorSatellites::updateSteps(size_t steps, bool apply_no_radius, bool apply_with_radius, bool only_selected) +{ + for (auto & _satellite : _satellites) { + for (auto & j : _satellite) { + if ((!apply_no_radius && j.amount == 0) || + (!apply_with_radius && j.amount != 0)) + { + continue; + } + if (only_selected) { + if (j.selected) { + j.steps = steps; + } + } else { + j.steps = steps; + } + } + } +} + +void PathVectorSatellites::updateAmount(double radius, bool apply_no_radius, bool apply_with_radius, bool only_selected, + bool use_knot_distance, bool flexible) +{ + double power = 0; + if (!flexible) { + power = radius; + } else { + power = radius / 100; + } + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + boost::optional<size_t> previous_index = boost::none; + if (j == 0 && _pathvector[i].closed()) { + previous_index = count_path_nodes(_pathvector[i]) - 1; + } else if (!_pathvector[i].closed() || j != 0) { + previous_index = j - 1; + } + if (!_pathvector[i].closed() && j == 0) { + _satellites[i][j].amount = 0; + continue; + } + if (count_path_nodes(_pathvector[i]) == j) { + continue; + } + if ((!apply_no_radius && _satellites[i][j].amount == 0) || + (!apply_with_radius && _satellites[i][j].amount != 0)) + { + continue; + } + + if (_satellites[i][j].selected || !only_selected) { + if (!use_knot_distance && !flexible) { + if (previous_index) { + _satellites[i][j].amount = _satellites[i][j].radToLen(power, _pathvector[i][*previous_index], _pathvector[i][j]); + if (power && !_satellites[i][j].amount) { + g_warning("Seems a too high radius value"); + } + } else { + _satellites[i][j].amount = 0.0; + } + } else { + _satellites[i][j].amount = power; + } + } + } + } +} + +void PathVectorSatellites::convertUnit(Glib::ustring in, Glib::ustring to, bool apply_no_radius, bool apply_with_radius) +{ + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + if (!_pathvector[i].closed() && j == 0) { + _satellites[i][j].amount = 0; + continue; + } + if (count_path_nodes(_pathvector[i]) == j) { + continue; + } + if ((!apply_no_radius && _satellites[i][j].amount == 0) || + (!apply_with_radius && _satellites[i][j].amount != 0)) + { + continue; + } + _satellites[i][j].amount = Inkscape::Util::Quantity::convert(_satellites[i][j].amount, in.c_str(), to.c_str()); + } + } +} + +void PathVectorSatellites::updateSatelliteType(SatelliteType satellitetype, bool apply_no_radius, bool apply_with_radius, + bool only_selected) +{ + for (size_t i = 0; i < _satellites.size(); ++i) { + for (size_t j = 0; j < _satellites[i].size(); ++j) { + if ((!apply_no_radius && _satellites[i][j].amount == 0) || + (!apply_with_radius && _satellites[i][j].amount != 0)) + { + continue; + } + if (count_path_nodes(_pathvector[i]) == j) { + if (!only_selected) { + _satellites[i][j].satellite_type = satellitetype; + } + continue; + } + if (only_selected) { + Geom::Point satellite_point = _pathvector[i].pointAt(j); + if (_satellites[i][j].selected) { + _satellites[i][j].satellite_type = satellitetype; + } + } else { + _satellites[i][j].satellite_type = satellitetype; + } + } + } +} + +void PathVectorSatellites::recalculateForNewPathVector(Geom::PathVector const pathv, Satellite const S) +{ + // pathv && _pathvector came here: + // * with diferent number of nodes + // * without empty subpats + // * _pathvector and satellites (old data) are paired + Satellites satellites; + bool found = false; + //TODO evaluate fix on nodes at same position + size_t number_nodes = count_pathvector_nodes(pathv); + size_t previous_number_nodes = getTotalSatellites(); + for (const auto & i : pathv) { + std::vector<Satellite> path_satellites; + size_t count = count_path_nodes(i); + for (size_t j = 0; j < count; j++) { + found = false; + for (size_t k = 0; k < _pathvector.size(); k++) { + size_t count2 = count_path_nodes(_pathvector[k]); + for (size_t l = 0; l < count2; l++) { + if (Geom::are_near(_pathvector[k][l].initialPoint(), i[j].initialPoint())) { + path_satellites.push_back(_satellites[k][l]); + found = true; + break; + } + } + if (found) { + break; + } + } + if (!found) { + path_satellites.push_back(S); + } + } + satellites.push_back(path_satellites); + } + setPathVector(pathv); + setSatellites(satellites); +} + +/* + 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/helper/geom-pathvectorsatellites.h b/src/helper/geom-pathvectorsatellites.h new file mode 100644 index 0000000..3009081 --- /dev/null +++ b/src/helper/geom-pathvectorsatellites.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief PathVectorSatellites a class to manage satellites -per node extra data- in a pathvector + *//* + * Authors: see git history + * Jabiertxof + * Nathan Hurst + * Johan Engelen + * Josh Andler + * suv + * Mc- + * Liam P. White + * Krzysztof KosiĆski + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#ifndef SEEN_PATHVECTORSATELLITES_H +#define SEEN_PATHVECTORSATELLITES_H + +#include <helper/geom-satellite.h> +#include <2geom/path.h> +#include <2geom/pathvector.h> + +typedef std::vector<std::vector<Satellite> > Satellites; +///@brief PathVectorSatellites a class to manage satellites in a pathvector +class PathVectorSatellites { +public: + Geom::PathVector getPathVector() const; + void setPathVector(Geom::PathVector pathv); + Satellites getSatellites(); + void setSatellites(Satellites satellites); + size_t getTotalSatellites(); + void setSelected(std::vector<size_t> selected); + void updateSteps(size_t steps, bool apply_no_radius, bool apply_with_radius, bool only_selected); + void updateAmount(double radius, bool apply_no_radius, bool apply_with_radius, bool only_selected, + bool use_knot_distance, bool flexible); + void convertUnit(Glib::ustring in, Glib::ustring to, bool apply_no_radius, bool apply_with_radius); + void updateSatelliteType(SatelliteType satellitetype, bool apply_no_radius, bool apply_with_radius, bool only_selected); + std::pair<size_t, size_t> getIndexData(size_t index); + void recalculateForNewPathVector(Geom::PathVector const pathv, Satellite const S); +private: + Geom::PathVector _pathvector; + Satellites _satellites; +}; + +#endif //SEEN_PATHVECTORSATELLITES_H +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom-satellite.cpp b/src/helper/geom-satellite.cpp new file mode 100644 index 0000000..0d8ca83 --- /dev/null +++ b/src/helper/geom-satellite.cpp @@ -0,0 +1,249 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief Satellite a per node holder of data. + *//* + * Authors: + * see git history + * 2015 Jabier Arraiza Cenoz<jabier.arraiza@marker.es> + * + * Copyright (C) 2018 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <helper/geom-satellite.h> +#include <2geom/curve.h> +#include <2geom/nearest-time.h> +#include <2geom/path-intersection.h> +#include <2geom/sbasis-to-bezier.h> +#include <2geom/ray.h> +#include <boost/optional.hpp> +// log cache +#ifdef _WIN32 +#include <Windows.h> +#else +#include <sys/time.h> +#include <ctime> +#endif + +///@brief Satellite a per node holder of data. +Satellite::Satellite() = default; + + +Satellite::Satellite(SatelliteType satellite_type) + : satellite_type(satellite_type), + is_time(false), + selected(false), + has_mirror(false), + hidden(true), + amount(0.0), + angle(0.0), + steps(0) +{} + +Satellite::~Satellite() = default; + +///Calculate the time in curve_in with a size of A +//TODO: find a better place to it +double timeAtArcLength(double const A, Geom::Curve const &curve_in) +{ + if ( A == 0 || curve_in.isDegenerate()) { + return 0; + } + + Geom::D2<Geom::SBasis> d2_in = curve_in.toSBasis(); + double t = 0; + double length_part = curve_in.length(); + if (A >= length_part || curve_in.isLineSegment()) { + if (length_part != 0) { + t = A / length_part; + } + } else if (!curve_in.isLineSegment()) { + std::vector<double> t_roots = roots(Geom::arcLengthSb(d2_in) - A); + if (!t_roots.empty()) { + t = t_roots[0]; + } + } + return t; +} + +///Calculate the size in curve_in with a point at A +//TODO: find a better place to it +double arcLengthAt(double const A, Geom::Curve const &curve_in) +{ + if ( A == 0 || curve_in.isDegenerate()) { + return 0; + } + + double s = 0; + double length_part = curve_in.length(); + if (A > length_part || curve_in.isLineSegment()) { + s = (A * length_part); + } else if (!curve_in.isLineSegment()) { + Geom::Curve *curve = curve_in.portion(0.0, A); + s = curve->length(); + delete curve; + } + return s; +} + +///Convert a arc radius of a fillet/chamfer to his satellite length -point position where fillet/chamfer knot be on original curve +double Satellite::radToLen( + double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out) const +{ + double len = 0; + Geom::D2<Geom::SBasis> d2_in = curve_in.toSBasis(); + Geom::D2<Geom::SBasis> d2_out = curve_out.toSBasis(); + Geom::Piecewise<Geom::D2<Geom::SBasis> > offset_curve0 = + Geom::Piecewise<Geom::D2<Geom::SBasis> >(d2_in) + + rot90(unitVector(derivative(d2_in))) * (A); + Geom::Piecewise<Geom::D2<Geom::SBasis> > offset_curve1 = + Geom::Piecewise<Geom::D2<Geom::SBasis> >(d2_out) + + rot90(unitVector(derivative(d2_out))) * (A); + offset_curve0[0][0].normalize(); + offset_curve0[0][1].normalize(); + Geom::Path p0 = path_from_piecewise(offset_curve0, 0.1)[0]; + offset_curve1[0][0].normalize(); + offset_curve1[0][1].normalize(); + Geom::Path p1 = path_from_piecewise(offset_curve1, 0.1)[0]; + Geom::Crossings cs = Geom::crossings(p0, p1); + if (cs.size() > 0) { + Geom::Point cp = p0(cs[0].ta); + double p0pt = nearest_time(cp, curve_out); + len = arcLengthAt(p0pt, curve_out); + } else { + if (A > 0) { + len = radToLen(A * -1, curve_in, curve_out); + } + } + return len; +} + +///Convert a satellite length -point position where fillet/chamfer knot be on original curve- to a arc radius of fillet/chamfer +double Satellite::lenToRad( + double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out, + Satellite const previousSatellite) const +{ + double time_in = (previousSatellite).time(A, true, curve_in); + double time_out = timeAtArcLength(A, curve_out); + Geom::Point start_arc_point = curve_in.pointAt(time_in); + Geom::Point end_arc_point = curve_out.pointAt(time_out); + Geom::Curve *knot_curve1 = curve_in.portion(0, time_in); + Geom::Curve *knot_curve2 = curve_out.portion(time_out, 1); + Geom::CubicBezier const *cubic1 = dynamic_cast<Geom::CubicBezier const *>(&*knot_curve1); + Geom::Ray ray1(start_arc_point, curve_in.pointAt(1)); + if (cubic1) { + ray1.setPoints((*cubic1)[2], start_arc_point); + } + Geom::CubicBezier const *cubic2 = dynamic_cast<Geom::CubicBezier const *>(&*knot_curve2); + Geom::Ray ray2(curve_out.pointAt(0), end_arc_point); + if (cubic2) { + ray2.setPoints(end_arc_point, (*cubic2)[1]); + } + bool ccw_toggle = cross(curve_in.pointAt(1) - start_arc_point, + end_arc_point - start_arc_point) < 0; + double distance_arc = + Geom::distance(start_arc_point, middle_point(start_arc_point, end_arc_point)); + double angle = angle_between(ray1, ray2, ccw_toggle); + double divisor = std::sin(angle / 2.0); + if (divisor > 0) { + return distance_arc / divisor; + } + return 0; +} + +///Get the time position of the satellite in curve_in +double Satellite::time(Geom::Curve const &curve_in, bool inverse) const +{ + double t = amount; + if (!is_time) { + t = time(t, inverse, curve_in); + } else if (inverse) { + t = 1-t; + } + if (t > 1) { + t = 1; + } + return t; +} + +///Get the time from a length A in other curve, a boolean inverse given to reverse time +double Satellite::time(double A, bool inverse, + Geom::Curve const &curve_in) const +{ + if (A == 0 && inverse) { + return 1; + } + if (A == 0 && !inverse) { + return 0; + } + if (!inverse) { + return timeAtArcLength(A, curve_in); + } + double length_part = curve_in.length(); + A = length_part - A; + return timeAtArcLength(A, curve_in); +} + +///Get the length of the satellite in curve_in +double Satellite::arcDistance(Geom::Curve const &curve_in) const +{ + double s = amount; + if (is_time) { + s = arcLengthAt(s, curve_in); + } + return s; +} + +///Get the point position of the satellite +Geom::Point Satellite::getPosition(Geom::Curve const &curve_in, bool inverse) const +{ + double t = time(curve_in, inverse); + return curve_in.pointAt(t); +} + +///Set the position of the satellite from a given point P +void Satellite::setPosition(Geom::Point const p, Geom::Curve const &curve_in, bool inverse) +{ + Geom::Curve * curve = const_cast<Geom::Curve *>(&curve_in); + if (inverse) { + curve = curve->reverse(); + } + double A = Geom::nearest_time(p, *curve); + if (!is_time) { + A = arcLengthAt(A, *curve); + } + amount = A; +} + + +///Map a satellite type with gchar +void Satellite::setSatelliteType(gchar const *A) +{ + std::map<std::string, SatelliteType> gchar_map_to_satellite_type = + boost::assign::map_list_of("F", FILLET)("IF", INVERSE_FILLET)("C", CHAMFER)("IC", INVERSE_CHAMFER)("KO", INVALID_SATELLITE); + std::map<std::string, SatelliteType>::iterator it = gchar_map_to_satellite_type.find(std::string(A)); + if (it != gchar_map_to_satellite_type.end()) { + satellite_type = it->second; + } +} + +///Map a gchar with satelliteType +gchar const *Satellite::getSatelliteTypeGchar() const +{ + std::map<SatelliteType, gchar const *> satellite_type_to_gchar_map = + boost::assign::map_list_of(FILLET, "F")(INVERSE_FILLET, "IF")(CHAMFER, "C")(INVERSE_CHAMFER, "IC")(INVALID_SATELLITE, "KO"); + return satellite_type_to_gchar_map.at(satellite_type); +} + +/* + 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/helper/geom-satellite.h b/src/helper/geom-satellite.h new file mode 100644 index 0000000..5fbcae1 --- /dev/null +++ b/src/helper/geom-satellite.h @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Satellite -- a per node holder of data. + *//* + * Authors: + * see git history + * Jabier Arraiza Cenoz<jabier.arraiza@marker.es> + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_SATELLITE_H +#define SEEN_SATELLITE_H + +#include <map> +#include <boost/assign.hpp> +#include <2geom/sbasis-geometric.h> +#include "util/enums.h" + + +enum SatelliteType { + FILLET = 0, //Fillet + INVERSE_FILLET, //Inverse Fillet + CHAMFER, //Chamfer + INVERSE_CHAMFER, //Inverse Chamfer + INVALID_SATELLITE // Invalid Satellite +}; +/** + * @brief Satellite a per node holder of data. + */ + +class Satellite { +public: + + Satellite(); + Satellite(SatelliteType satellite_type); + + virtual ~Satellite(); + void setIsTime(bool set_is_time) + { + is_time = set_is_time; + } + void setSelected(bool set_selected) + { + selected = set_selected; + } + void setHasMirror(bool set_has_mirror) + { + has_mirror = set_has_mirror; + } + void setHidden(bool set_hidden) + { + hidden = set_hidden; + } + void setAmount(double set_amount) + { + amount = set_amount; + } + void setAngle(double set_angle) + { + angle = set_angle; + } + void setSteps(size_t set_steps) + { + steps = set_steps; + } + double lenToRad(double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out, + Satellite const previousSatellite) const; + double radToLen(double const A, Geom::Curve const &curve_in, + Geom::Curve const &curve_out) const; + + double time(Geom::Curve const &curve_in, bool inverse = false) const; + double time(double A, bool inverse, Geom::Curve const &curve_in) const; + double arcDistance(Geom::Curve const &curve_in) const; + + void setPosition(Geom::Point const p, Geom::Curve const &curve_in, bool inverse = false); + Geom::Point getPosition(Geom::Curve const &curve_in, bool inverse = false) const; + + void setSatelliteType(gchar const *A); + gchar const *getSatelliteTypeGchar() const; + SatelliteType satellite_type; + //The value stored could be a time value of the satellite in the curve or a length of distance to the node from the satellite + //"is_time" tells us if it's a time or length value + bool is_time; + bool selected; + bool has_mirror; + bool hidden; + //in "amount" we store the time or distance used in the satellite + double amount; + double angle; + size_t steps; +}; + +double timeAtArcLength(double const A, Geom::Curve const &curve_in); +double arcLengthAt(double const A, Geom::Curve const &curve_in); + +#endif // SEEN_SATELLITE_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/geom.cpp b/src/helper/geom.cpp new file mode 100644 index 0000000..5e8e763 --- /dev/null +++ b/src/helper/geom.cpp @@ -0,0 +1,893 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Specific geometry functions for Inkscape, not provided my lib2geom. + * + * Author: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include "helper/geom.h" +#include "helper/geom-curves.h" +#include <2geom/curves.h> +#include <2geom/sbasis-to-bezier.h> + +using Geom::X; +using Geom::Y; + +//################################################################################# +// BOUNDING BOX CALCULATIONS + +/* Fast bbox calculation */ +/* Thanks to Nathan Hurst for suggesting it */ +static void +cubic_bbox (Geom::Coord x000, Geom::Coord y000, Geom::Coord x001, Geom::Coord y001, Geom::Coord x011, Geom::Coord y011, Geom::Coord x111, Geom::Coord y111, Geom::Rect &bbox) +{ + Geom::Coord a, b, c, D; + + bbox[0].expandTo(x111); + bbox[1].expandTo(y111); + + // It already contains (x000,y000) and (x111,y111) + // All points of the Bezier lie in the convex hull of (x000,y000), (x001,y001), (x011,y011) and (x111,y111) + // So, if it also contains (x001,y001) and (x011,y011) we don't have to compute anything else! + // Note that we compute it for the X and Y range separately to make it easier to use them below + bool containsXrange = bbox[0].contains(x001) && bbox[0].contains(x011); + bool containsYrange = bbox[1].contains(y001) && bbox[1].contains(y011); + + /* + * xttt = s * (s * (s * x000 + t * x001) + t * (s * x001 + t * x011)) + t * (s * (s * x001 + t * x011) + t * (s * x011 + t * x111)) + * xttt = s * (s2 * x000 + s * t * x001 + t * s * x001 + t2 * x011) + t * (s2 * x001 + s * t * x011 + t * s * x011 + t2 * x111) + * xttt = s * (s2 * x000 + 2 * st * x001 + t2 * x011) + t * (s2 * x001 + 2 * st * x011 + t2 * x111) + * xttt = s3 * x000 + 2 * s2t * x001 + st2 * x011 + s2t * x001 + 2st2 * x011 + t3 * x111 + * xttt = s3 * x000 + 3s2t * x001 + 3st2 * x011 + t3 * x111 + * xttt = s3 * x000 + (1 - s) 3s2 * x001 + (1 - s) * (1 - s) * 3s * x011 + (1 - s) * (1 - s) * (1 - s) * x111 + * xttt = s3 * x000 + (3s2 - 3s3) * x001 + (3s - 6s2 + 3s3) * x011 + (1 - 2s + s2 - s + 2s2 - s3) * x111 + * xttt = (x000 - 3 * x001 + 3 * x011 - x111) * s3 + + * ( 3 * x001 - 6 * x011 + 3 * x111) * s2 + + * ( 3 * x011 - 3 * x111) * s + + * ( x111) + * xttt' = (3 * x000 - 9 * x001 + 9 * x011 - 3 * x111) * s2 + + * ( 6 * x001 - 12 * x011 + 6 * x111) * s + + * ( 3 * x011 - 3 * x111) + */ + + if (!containsXrange) { + a = 3 * x000 - 9 * x001 + 9 * x011 - 3 * x111; + b = 6 * x001 - 12 * x011 + 6 * x111; + c = 3 * x011 - 3 * x111; + + /* + * s = (-b +/- sqrt (b * b - 4 * a * c)) / 2 * a; + */ + if (fabs (a) < Geom::EPSILON) { + /* s = -c / b */ + if (fabs (b) > Geom::EPSILON) { + double s; + s = -c / b; + if ((s > 0.0) && (s < 1.0)) { + double t = 1.0 - s; + double xttt = s * s * s * x000 + 3 * s * s * t * x001 + 3 * s * t * t * x011 + t * t * t * x111; + bbox[0].expandTo(xttt); + } + } + } else { + /* s = (-b +/- sqrt (b * b - 4 * a * c)) / 2 * a; */ + D = b * b - 4 * a * c; + if (D >= 0.0) { + Geom::Coord d, s, t, xttt; + /* Have solution */ + d = sqrt (D); + s = (-b + d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + xttt = s * s * s * x000 + 3 * s * s * t * x001 + 3 * s * t * t * x011 + t * t * t * x111; + bbox[0].expandTo(xttt); + } + s = (-b - d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + xttt = s * s * s * x000 + 3 * s * s * t * x001 + 3 * s * t * t * x011 + t * t * t * x111; + bbox[0].expandTo(xttt); + } + } + } + } + + if (!containsYrange) { + a = 3 * y000 - 9 * y001 + 9 * y011 - 3 * y111; + b = 6 * y001 - 12 * y011 + 6 * y111; + c = 3 * y011 - 3 * y111; + + if (fabs (a) < Geom::EPSILON) { + /* s = -c / b */ + if (fabs (b) > Geom::EPSILON) { + double s; + s = -c / b; + if ((s > 0.0) && (s < 1.0)) { + double t = 1.0 - s; + double yttt = s * s * s * y000 + 3 * s * s * t * y001 + 3 * s * t * t * y011 + t * t * t * y111; + bbox[1].expandTo(yttt); + } + } + } else { + /* s = (-b +/- sqrt (b * b - 4 * a * c)) / 2 * a; */ + D = b * b - 4 * a * c; + if (D >= 0.0) { + Geom::Coord d, s, t, yttt; + /* Have solution */ + d = sqrt (D); + s = (-b + d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + yttt = s * s * s * y000 + 3 * s * s * t * y001 + 3 * s * t * t * y011 + t * t * t * y111; + bbox[1].expandTo(yttt); + } + s = (-b - d) / (2 * a); + if ((s > 0.0) && (s < 1.0)) { + t = 1.0 - s; + yttt = s * s * s * y000 + 3 * s * s * t * y001 + 3 * s * t * t * y011 + t * t * t * y111; + bbox[1].expandTo(yttt); + } + } + } + } +} + +Geom::OptRect +bounds_fast_transformed(Geom::PathVector const & pv, Geom::Affine const & t) +{ + return bounds_exact_transformed(pv, t); //use this as it is faster for now! :) +// return Geom::bounds_fast(pv * t); +} + +Geom::OptRect +bounds_exact_transformed(Geom::PathVector const & pv, Geom::Affine const & t) +{ + if (pv.empty()) + return Geom::OptRect(); + + Geom::Point initial = pv.front().initialPoint() * t; + Geom::Rect bbox(initial, initial); // obtain well defined bbox as starting point to unionWith + + for (const auto & it : pv) { + bbox.expandTo(it.initialPoint() * t); + + // don't loop including closing segment, since that segment can never increase the bbox + for (Geom::Path::const_iterator cit = it.begin(); cit != it.end_open(); ++cit) { + Geom::Curve const &c = *cit; + + unsigned order = 0; + if (Geom::BezierCurve const* b = dynamic_cast<Geom::BezierCurve const*>(&c)) { + order = b->order(); + } + + if (order == 1) { // line segment + bbox.expandTo(c.finalPoint() * t); + + // TODO: we can make the case for quadratics faster by degree elevating them to + // cubic and then taking the bbox of that. + + } else if (order == 3) { // cubic bezier + Geom::CubicBezier const &cubic_bezier = static_cast<Geom::CubicBezier const&>(c); + Geom::Point c0 = cubic_bezier[0] * t; + Geom::Point c1 = cubic_bezier[1] * t; + Geom::Point c2 = cubic_bezier[2] * t; + Geom::Point c3 = cubic_bezier[3] * t; + cubic_bbox(c0[0], c0[1], c1[0], c1[1], c2[0], c2[1], c3[0], c3[1], bbox); + } else { + // should handle all not-so-easy curves: + Geom::Curve *ctemp = cit->transformed(t); + bbox.unionWith( ctemp->boundsExact()); + delete ctemp; + } + } + } + //return Geom::bounds_exact(pv * t); + return bbox; +} + + + +static void +geom_line_wind_distance (Geom::Coord x0, Geom::Coord y0, Geom::Coord x1, Geom::Coord y1, Geom::Point const &pt, int *wind, Geom::Coord *best) +{ + Geom::Coord Ax, Ay, Bx, By, Dx, Dy, s; + Geom::Coord dist2; + + /* Find distance */ + Ax = x0; + Ay = y0; + Bx = x1; + By = y1; + Dx = x1 - x0; + Dy = y1 - y0; + const Geom::Coord Px = pt[X]; + const Geom::Coord Py = pt[Y]; + + if (best) { + s = ((Px - Ax) * Dx + (Py - Ay) * Dy) / (Dx * Dx + Dy * Dy); + if (s <= 0.0) { + dist2 = (Px - Ax) * (Px - Ax) + (Py - Ay) * (Py - Ay); + } else if (s >= 1.0) { + dist2 = (Px - Bx) * (Px - Bx) + (Py - By) * (Py - By); + } else { + Geom::Coord Qx, Qy; + Qx = Ax + s * Dx; + Qy = Ay + s * Dy; + dist2 = (Px - Qx) * (Px - Qx) + (Py - Qy) * (Py - Qy); + } + + if (dist2 < (*best * *best)) *best = sqrt (dist2); + } + + if (wind) { + /* Find wind */ + if ((Ax >= Px) && (Bx >= Px)) return; + if ((Ay >= Py) && (By >= Py)) return; + if ((Ay < Py) && (By < Py)) return; + if (Ay == By) return; + /* Ctach upper y bound */ + if (Ay == Py) { + if (Ax < Px) *wind -= 1; + return; + } else if (By == Py) { + if (Bx < Px) *wind += 1; + return; + } else { + Geom::Coord Qx; + /* Have to calculate intersection */ + Qx = Ax + Dx * (Py - Ay) / Dy; + if (Qx < Px) { + *wind += (Dy > 0.0) ? 1 : -1; + } + } + } +} + +static void +geom_cubic_bbox_wind_distance (Geom::Coord x000, Geom::Coord y000, + Geom::Coord x001, Geom::Coord y001, + Geom::Coord x011, Geom::Coord y011, + Geom::Coord x111, Geom::Coord y111, + Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *best, + Geom::Coord tolerance) +{ + Geom::Coord x0, y0, x1, y1, len2; + int needdist, needwind; + + const Geom::Coord Px = pt[X]; + const Geom::Coord Py = pt[Y]; + + needdist = 0; + needwind = 0; + + if (bbox) cubic_bbox (x000, y000, x001, y001, x011, y011, x111, y111, *bbox); + + x0 = std::min (x000, x001); + x0 = std::min (x0, x011); + x0 = std::min (x0, x111); + y0 = std::min (y000, y001); + y0 = std::min (y0, y011); + y0 = std::min (y0, y111); + x1 = std::max (x000, x001); + x1 = std::max (x1, x011); + x1 = std::max (x1, x111); + y1 = std::max (y000, y001); + y1 = std::max (y1, y011); + y1 = std::max (y1, y111); + + if (best) { + /* Quickly adjust to endpoints */ + len2 = (x000 - Px) * (x000 - Px) + (y000 - Py) * (y000 - Py); + if (len2 < (*best * *best)) *best = (Geom::Coord) sqrt (len2); + len2 = (x111 - Px) * (x111 - Px) + (y111 - Py) * (y111 - Py); + if (len2 < (*best * *best)) *best = (Geom::Coord) sqrt (len2); + + if (((x0 - Px) < *best) && ((y0 - Py) < *best) && ((Px - x1) < *best) && ((Py - y1) < *best)) { + /* Point is inside sloppy bbox */ + /* Now we have to decide, whether subdivide */ + /* fixme: (Lauris) */ + if (((y1 - y0) > 5.0) || ((x1 - x0) > 5.0)) { + needdist = 1; + } + } + } + if (!needdist && wind) { + if ((y1 >= Py) && (y0 < Py) && (x0 < Px)) { + /* Possible intersection at the left */ + /* Now we have to decide, whether subdivide */ + /* fixme: (Lauris) */ + if (((y1 - y0) > 5.0) || ((x1 - x0) > 5.0)) { + needwind = 1; + } + } + } + + if (needdist || needwind) { + Geom::Coord x00t, x0tt, xttt, x1tt, x11t, x01t; + Geom::Coord y00t, y0tt, yttt, y1tt, y11t, y01t; + Geom::Coord s, t; + + t = 0.5; + s = 1 - t; + + x00t = s * x000 + t * x001; + x01t = s * x001 + t * x011; + x11t = s * x011 + t * x111; + x0tt = s * x00t + t * x01t; + x1tt = s * x01t + t * x11t; + xttt = s * x0tt + t * x1tt; + + y00t = s * y000 + t * y001; + y01t = s * y001 + t * y011; + y11t = s * y011 + t * y111; + y0tt = s * y00t + t * y01t; + y1tt = s * y01t + t * y11t; + yttt = s * y0tt + t * y1tt; + + geom_cubic_bbox_wind_distance (x000, y000, x00t, y00t, x0tt, y0tt, xttt, yttt, pt, nullptr, wind, best, tolerance); + geom_cubic_bbox_wind_distance (xttt, yttt, x1tt, y1tt, x11t, y11t, x111, y111, pt, nullptr, wind, best, tolerance); + } else { + geom_line_wind_distance (x000, y000, x111, y111, pt, wind, best); + } +} + +static void +geom_curve_bbox_wind_distance(Geom::Curve const & c, Geom::Affine const &m, + Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *dist, + Geom::Coord tolerance, Geom::Rect const *viewbox, + Geom::Point &p0) // pass p0 through as it represents the last endpoint added (the finalPoint of last curve) +{ + unsigned order = 0; + if (Geom::BezierCurve const* b = dynamic_cast<Geom::BezierCurve const*>(&c)) { + order = b->order(); + } + if (order == 1) { + Geom::Point pe = c.finalPoint() * m; + if (bbox) { + bbox->expandTo(pe); + } + if (dist || wind) { + if (wind) { // we need to pick fill, so do what we're told + geom_line_wind_distance (p0[X], p0[Y], pe[X], pe[Y], pt, wind, dist); + } else { // only stroke is being picked; skip this segment if it's totally outside the viewbox + Geom::Rect swept(p0, pe); + if (!viewbox || swept.intersects(*viewbox)) + geom_line_wind_distance (p0[X], p0[Y], pe[X], pe[Y], pt, wind, dist); + } + } + p0 = pe; + } + else if (order == 3) { + Geom::CubicBezier const& cubic_bezier = static_cast<Geom::CubicBezier const&>(c); + Geom::Point p1 = cubic_bezier[1] * m; + Geom::Point p2 = cubic_bezier[2] * m; + Geom::Point p3 = cubic_bezier[3] * m; + + // get approximate bbox from handles (convex hull property of beziers): + Geom::Rect swept(p0, p3); + swept.expandTo(p1); + swept.expandTo(p2); + + if (!viewbox || swept.intersects(*viewbox)) { // we see this segment, so do full processing + geom_cubic_bbox_wind_distance ( p0[X], p0[Y], + p1[X], p1[Y], + p2[X], p2[Y], + p3[X], p3[Y], + pt, + bbox, wind, dist, tolerance); + } else { + if (wind) { // if we need fill, we can just pretend it's a straight line + geom_line_wind_distance (p0[X], p0[Y], p3[X], p3[Y], pt, wind, dist); + } else { // otherwise, skip it completely + } + } + p0 = p3; + } else { + //this case handles sbasis as well as all other curve types + Geom::Path sbasis_path = Geom::cubicbezierpath_from_sbasis(c.toSBasis(), 0.1); + + //recurse to convert the new path resulting from the sbasis to svgd + for (const auto & iter : sbasis_path) { + geom_curve_bbox_wind_distance(iter, m, pt, bbox, wind, dist, tolerance, viewbox, p0); + } + } +} + +/* Calculates... + and returns ... in *wind and the distance to ... in *dist. + Returns bounding box in *bbox if bbox!=NULL. + */ +void +pathv_matrix_point_bbox_wind_distance (Geom::PathVector const & pathv, Geom::Affine const &m, Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *dist, + Geom::Coord tolerance, Geom::Rect const *viewbox) +{ + if (pathv.empty()) { + if (wind) *wind = 0; + if (dist) *dist = Geom::infinity(); + return; + } + + // remember last point of last curve + Geom::Point p0(0,0); + + // remembering the start of subpath + Geom::Point p_start(0,0); + bool start_set = false; + + for (const auto & it : pathv) { + + if (start_set) { // this is a new subpath + if (wind && (p0 != p_start)) // for correct fill picking, each subpath must be closed + geom_line_wind_distance (p0[X], p0[Y], p_start[X], p_start[Y], pt, wind, dist); + } + p0 = it.initialPoint() * m; + p_start = p0; + start_set = true; + if (bbox) { + bbox->expandTo(p0); + } + + // loop including closing segment if path is closed + for (Geom::Path::const_iterator cit = it.begin(); cit != it.end_default(); ++cit) { + geom_curve_bbox_wind_distance(*cit, m, pt, bbox, wind, dist, tolerance, viewbox, p0); + } + } + + if (start_set) { + if (wind && (p0 != p_start)) // for correct picking, each subpath must be closed + geom_line_wind_distance (p0[X], p0[Y], p_start[X], p_start[Y], pt, wind, dist); + } +} + +//################################################################################# + +/* + * Converts all segments in all paths to Geom::LineSegment or Geom::HLineSegment or + * Geom::VLineSegment or Geom::CubicBezier. + */ +Geom::PathVector +pathv_to_linear_and_cubic_beziers( Geom::PathVector const &pathv ) +{ + Geom::PathVector output; + + for (const auto & pit : pathv) { + output.push_back( Geom::Path() ); + output.back().setStitching(true); + output.back().start( pit.initialPoint() ); + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + Geom::LineSegment l(cit->initialPoint(), cit->finalPoint()); + output.back().append(l); + } else { + Geom::BezierCurve const *curve = dynamic_cast<Geom::BezierCurve const *>(&*cit); + if (curve && curve->order() == 3) { + Geom::CubicBezier b((*curve)[0], (*curve)[1], (*curve)[2], (*curve)[3]); + output.back().append(b); + } else { + // convert all other curve types to cubicbeziers + Geom::Path cubicbezier_path = Geom::cubicbezierpath_from_sbasis(cit->toSBasis(), 0.1); + cubicbezier_path.close(false); + output.back().append(cubicbezier_path); + } + } + } + + output.back().close( pit.closed() ); + } + + return output; +} + +/* + * Converts all segments in all paths to Geom::LineSegment. There is an intermediate + * stage where some may be converted to beziers. maxdisp is the maximum displacement from + * the line segment to the bezier curve; ** maxdisp is not used at this moment **. + * + * This is NOT a terribly fast method, but it should give a solution close to the one with the + * fewest points. + */ +Geom::PathVector +pathv_to_linear( Geom::PathVector const &pathv, double /*maxdisp*/) +{ + Geom::PathVector output; + Geom::PathVector tmppath = pathv_to_linear_and_cubic_beziers(pathv); + + // Now all path segments are either already lines, or they are beziers. + + for (const auto & pit : tmppath) { + output.push_back( Geom::Path() ); + output.back().start( pit.initialPoint() ); + output.back().close( pit.closed() ); + + for (Geom::Path::const_iterator cit = pit.begin(); cit != pit.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + Geom::LineSegment ls(cit->initialPoint(), cit->finalPoint()); + output.back().append(ls); + } + else { /* all others must be Bezier curves */ + Geom::BezierCurve const *curve = dynamic_cast<Geom::BezierCurve const *>(&*cit); + std::vector<Geom::Point> bzrpoints = curve->controlPoints(); + Geom::Point A = bzrpoints[0]; + Geom::Point B = bzrpoints[1]; + Geom::Point C = bzrpoints[2]; + Geom::Point D = bzrpoints[3]; + std::vector<Geom::Point> pointlist; + pointlist.push_back(A); + recursive_bezier4( + A[X], A[Y], + B[X], B[Y], + C[X], C[Y], + D[X], D[Y], + pointlist, + 0); + pointlist.push_back(D); + Geom::Point r1 = pointlist[0]; + for (unsigned int i=1; i<pointlist.size();i++){ + Geom::Point prev_r1 = r1; + r1 = pointlist[i]; + Geom::LineSegment ls(prev_r1, r1); + output.back().append(ls); + } + pointlist.clear(); + } + } + } + + return output; +} + +/* + * Converts all segments in all paths to Geom Cubic bezier. + * This is used in lattice2 LPE, maybe is better move the function to the effect + * But maybe could be usable by others, so i put here. + * The straight curve part is needed as is for the effect to work appropriately + */ +Geom::PathVector +pathv_to_cubicbezier( Geom::PathVector const &pathv) +{ + Geom::PathVector output; + double cubicGap = 0.01; + for (const auto & pit : pathv) { + if (pit.empty()) { + continue; + } + output.push_back( Geom::Path() ); + output.back().start( pit.initialPoint() ); + output.back().close( pit.closed() ); + bool end_open = false; + if (pit.closed()) { + const Geom::Curve &closingline = pit.back_closed(); + if (!are_near(closingline.initialPoint(), closingline.finalPoint())) { + end_open = true; + } + } + Geom::Path pitCubic = (Geom::Path)pit; + if(end_open && pit.closed()){ + pitCubic.close(false); + pitCubic.appendNew<Geom::LineSegment>( pitCubic.initialPoint() ); + pitCubic.close(true); + } + for (Geom::Path::iterator cit = pitCubic.begin(); cit != pitCubic.end_open(); ++cit) { + if (is_straight_curve(*cit)) { + Geom::CubicBezier b(cit->initialPoint(), cit->pointAt(0.3334) + Geom::Point(cubicGap,cubicGap), cit->finalPoint(), cit->finalPoint()); + output.back().append(b); + } else { + Geom::BezierCurve const *curve = dynamic_cast<Geom::BezierCurve const *>(&*cit); + if (curve && curve->order() == 3) { + Geom::CubicBezier b((*curve)[0], (*curve)[1], (*curve)[2], (*curve)[3]); + output.back().append(b); + } else { + // convert all other curve types to cubicbeziers + Geom::Path cubicbezier_path = Geom::cubicbezierpath_from_sbasis(cit->toSBasis(), 0.1); + output.back().append(cubicbezier_path); + } + } + } + } + + return output; +} + +//Study move to 2Geom +size_t +count_pathvector_nodes(Geom::PathVector const &pathv) { + size_t tot = 0; + for (auto subpath : pathv) { + tot += count_path_nodes(subpath); + } + return tot; +} +size_t count_path_nodes(Geom::Path const &path) +{ + size_t tot = path.size_closed(); + if (path.closed()) { + 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! + tot -= 1; + } + } + return tot; +} + +// The next routine is modified from curv4_div::recursive_bezier from file agg_curves.cpp +//---------------------------------------------------------------------------- +// Anti-Grain Geometry (AGG) - Version 2.5 +// A high quality rendering engine for C++ +// Copyright (C) 2002-2006 Maxim Shemanarev +// Contact: mcseem@antigrain.com +// mcseemagg@yahoo.com +// http://antigrain.com +// +// AGG is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// AGG is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with AGG; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +// MA 02110-1301, USA. +//---------------------------------------------------------------------------- +void +recursive_bezier4(const double x1, const double y1, + const double x2, const double y2, + const double x3, const double y3, + const double x4, const double y4, + std::vector<Geom::Point> &m_points, + int level) + { + // some of these should be parameters, but do it this way for now. + const double curve_collinearity_epsilon = 1e-30; + const double curve_angle_tolerance_epsilon = 0.01; + double m_cusp_limit = 0.0; + double m_angle_tolerance = 0.0; + double m_approximation_scale = 1.0; + double m_distance_tolerance_square = 0.5 / m_approximation_scale; + m_distance_tolerance_square *= m_distance_tolerance_square; + enum curve_recursion_limit_e { curve_recursion_limit = 32 }; +#define calc_sq_distance(A,B,C,D) ((A-C)*(A-C) + (B-D)*(B-D)) + + if(level > curve_recursion_limit) + { + return; + } + + + // Calculate all the mid-points of the line segments + //---------------------- + double x12 = (x1 + x2) / 2; + double y12 = (y1 + y2) / 2; + double x23 = (x2 + x3) / 2; + double y23 = (y2 + y3) / 2; + double x34 = (x3 + x4) / 2; + double y34 = (y3 + y4) / 2; + double x123 = (x12 + x23) / 2; + double y123 = (y12 + y23) / 2; + double x234 = (x23 + x34) / 2; + double y234 = (y23 + y34) / 2; + double x1234 = (x123 + x234) / 2; + double y1234 = (y123 + y234) / 2; + + + // Try to approximate the full cubic curve by a single straight line + //------------------ + double dx = x4-x1; + double dy = y4-y1; + + double d2 = fabs(((x2 - x4) * dy - (y2 - y4) * dx)); + double d3 = fabs(((x3 - x4) * dy - (y3 - y4) * dx)); + double da1, da2, k; + + switch((int(d2 > curve_collinearity_epsilon) << 1) + + int(d3 > curve_collinearity_epsilon)) + { + case 0: + // All collinear OR p1==p4 + //---------------------- + k = dx*dx + dy*dy; + if(k == 0) + { + d2 = calc_sq_distance(x1, y1, x2, y2); + d3 = calc_sq_distance(x4, y4, x3, y3); + } + else + { + k = 1 / k; + da1 = x2 - x1; + da2 = y2 - y1; + d2 = k * (da1*dx + da2*dy); + da1 = x3 - x1; + da2 = y3 - y1; + d3 = k * (da1*dx + da2*dy); + if(d2 > 0 && d2 < 1 && d3 > 0 && d3 < 1) + { + // Simple collinear case, 1---2---3---4 + // We can leave just two endpoints + return; + } + if(d2 <= 0) d2 = calc_sq_distance(x2, y2, x1, y1); + else if(d2 >= 1) d2 = calc_sq_distance(x2, y2, x4, y4); + else d2 = calc_sq_distance(x2, y2, x1 + d2*dx, y1 + d2*dy); + + if(d3 <= 0) d3 = calc_sq_distance(x3, y3, x1, y1); + else if(d3 >= 1) d3 = calc_sq_distance(x3, y3, x4, y4); + else d3 = calc_sq_distance(x3, y3, x1 + d3*dx, y1 + d3*dy); + } + if(d2 > d3) + { + if(d2 < m_distance_tolerance_square) + { + m_points.emplace_back(x2, y2); + return; + } + } + else + { + if(d3 < m_distance_tolerance_square) + { + m_points.emplace_back(x3, y3); + return; + } + } + break; + + case 1: + // p1,p2,p4 are collinear, p3 is significant + //---------------------- + if(d3 * d3 <= m_distance_tolerance_square * (dx*dx + dy*dy)) + { + if(m_angle_tolerance < curve_angle_tolerance_epsilon) + { + m_points.emplace_back(x23, y23); + return; + } + + // Angle Condition + //---------------------- + da1 = fabs(atan2(y4 - y3, x4 - x3) - atan2(y3 - y2, x3 - x2)); + if(da1 >= M_PI) da1 = 2*M_PI - da1; + + if(da1 < m_angle_tolerance) + { + m_points.emplace_back(x2, y2); + m_points.emplace_back(x3, y3); + return; + } + + if(m_cusp_limit != 0.0) + { + if(da1 > m_cusp_limit) + { + m_points.emplace_back(x3, y3); + return; + } + } + } + break; + + case 2: + // p1,p3,p4 are collinear, p2 is significant + //---------------------- + if(d2 * d2 <= m_distance_tolerance_square * (dx*dx + dy*dy)) + { + if(m_angle_tolerance < curve_angle_tolerance_epsilon) + { + m_points.emplace_back(x23, y23); + return; + } + + // Angle Condition + //---------------------- + da1 = fabs(atan2(y3 - y2, x3 - x2) - atan2(y2 - y1, x2 - x1)); + if(da1 >= M_PI) da1 = 2*M_PI - da1; + + if(da1 < m_angle_tolerance) + { + m_points.emplace_back(x2, y2); + m_points.emplace_back(x3, y3); + return; + } + + if(m_cusp_limit != 0.0) + { + if(da1 > m_cusp_limit) + { + m_points.emplace_back(x2, y2); + return; + } + } + } + break; + + case 3: + // Regular case + //----------------- + if((d2 + d3)*(d2 + d3) <= m_distance_tolerance_square * (dx*dx + dy*dy)) + { + // If the curvature doesn't exceed the distance_tolerance value + // we tend to finish subdivisions. + //---------------------- + if(m_angle_tolerance < curve_angle_tolerance_epsilon) + { + m_points.emplace_back(x23, y23); + return; + } + + // Angle & Cusp Condition + //---------------------- + k = atan2(y3 - y2, x3 - x2); + da1 = fabs(k - atan2(y2 - y1, x2 - x1)); + da2 = fabs(atan2(y4 - y3, x4 - x3) - k); + if(da1 >= M_PI) da1 = 2*M_PI - da1; + if(da2 >= M_PI) da2 = 2*M_PI - da2; + + if(da1 + da2 < m_angle_tolerance) + { + // Finally we can stop the recursion + //---------------------- + m_points.emplace_back(x23, y23); + return; + } + + if(m_cusp_limit != 0.0) + { + if(da1 > m_cusp_limit) + { + m_points.emplace_back(x2, y2); + return; + } + + if(da2 > m_cusp_limit) + { + m_points.emplace_back(x3, y3); + return; + } + } + } + break; + } + + // Continue subdivision + //---------------------- + recursive_bezier4(x1, y1, x12, y12, x123, y123, x1234, y1234, m_points, level + 1); + recursive_bezier4(x1234, y1234, x234, y234, x34, y34, x4, y4, m_points, level + 1); +} + +void +swap(Geom::Point &A, Geom::Point &B){ + Geom::Point tmp = A; + A = B; + B = tmp; +} + +/* + 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/helper/geom.h b/src/helper/geom.h new file mode 100644 index 0000000..58f323e --- /dev/null +++ b/src/helper/geom.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_HELPER_GEOM_H +#define INKSCAPE_HELPER_GEOM_H + +/** + * @file + * Specific geometry functions for Inkscape, not provided my lib2geom. + */ +/* + * Author: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2008 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/forward.h> +#include <2geom/rect.h> +#include <2geom/affine.h> + +Geom::OptRect bounds_fast_transformed(Geom::PathVector const & pv, Geom::Affine const & t); +Geom::OptRect bounds_exact_transformed(Geom::PathVector const & pv, Geom::Affine const & t); + +void pathv_matrix_point_bbox_wind_distance ( Geom::PathVector const & pathv, Geom::Affine const &m, Geom::Point const &pt, + Geom::Rect *bbox, int *wind, Geom::Coord *dist, + Geom::Coord tolerance, Geom::Rect const *viewbox); + +size_t count_pathvector_nodes(Geom::PathVector const &pathv ); +size_t count_path_nodes(Geom::Path const &path); +Geom::PathVector pathv_to_linear_and_cubic_beziers( Geom::PathVector const &pathv ); +Geom::PathVector pathv_to_linear( Geom::PathVector const &pathv, double maxdisp ); +Geom::PathVector pathv_to_cubicbezier( Geom::PathVector const &pathv); +void recursive_bezier4(const double x1, const double y1, const double x2, const double y2, + const double x3, const double y3, const double x4, const double y4, + std::vector<Geom::Point> &pointlist, + int level); +void swap(Geom::Point &A, Geom::Point &B); +#endif // INKSCAPE_HELPER_GEOM_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : diff --git a/src/helper/gettext.cpp b/src/helper/gettext.cpp new file mode 100644 index 0000000..4c154a7 --- /dev/null +++ b/src/helper/gettext.cpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * helper functions for gettext + *//* + * Authors: + * see git history + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2018 Authors + * 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 + +#ifdef _WIN32 +#include <windows.h> +#endif + +#ifdef ENABLE_BINRELOC +#include "prefix.h" +#endif + +#include <string> +#include <glibmm.h> +#include <glibmm/i18n.h> + +namespace Inkscape { + +/** does all required gettext initialization and takes care of the respective locale directory paths */ +void initialize_gettext() { +#ifdef _WIN32 + gchar *datadir = g_win32_get_package_installation_directory_of_module(NULL); + + // obtain short path to executable dir and pass it + // to bindtextdomain (it doesn't understand UTF-8) + gchar *shortdatadir = g_win32_locale_filename_from_utf8(datadir); + gchar *localepath = g_build_filename(shortdatadir, PACKAGE_LOCALE_DIR, NULL); + bindtextdomain(GETTEXT_PACKAGE, localepath); + g_free(shortdatadir); + g_free(localepath); + + g_free(datadir); +#else +# ifdef ENABLE_BINRELOC + bindtextdomain(GETTEXT_PACKAGE, BR_LOCALEDIR("")); +# else + bindtextdomain(GETTEXT_PACKAGE, PACKAGE_LOCALE_DIR_ABSOLUTE); +# endif +#endif + + // Allow the user to override the locale directory by setting + // the environment variable INKSCAPE_LOCALEDIR. + char const *inkscape_localedir = g_getenv("INKSCAPE_LOCALEDIR"); + if (inkscape_localedir != nullptr) { + bindtextdomain(GETTEXT_PACKAGE, inkscape_localedir); + } + + // common setup + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); + textdomain(GETTEXT_PACKAGE); +} + +/** set gettext codeset to UTF8 */ +void bind_textdomain_codeset_utf8() { + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); +} + +/** set gettext codeset to codeset of the system console + * - on *nix this is typically the current locale, + * - on Windows we don't care and simply use UTF8 + * any conversion would need to happen in our console wrappers (see winconsole.cpp) anyway + * as we have no easy way of determining console encoding from inkscape/inkview.exe process; + * for now do something even easier - switch console encoding to UTF8 and be done with it! + * this also works nicely on MSYS consoles where UTF8 encoding is used by default, too */ +void bind_textdomain_codeset_console() { +#ifdef _WIN32 + bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8"); +#else + std::string charset; + Glib::get_charset(charset); + bind_textdomain_codeset(GETTEXT_PACKAGE, charset.c_str()); +#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/helper/gettext.h b/src/helper/gettext.h new file mode 100644 index 0000000..e5745ee --- /dev/null +++ b/src/helper/gettext.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * \brief helper functions for gettext + *//* + * Authors: + * see git history + * Patrick Storz <eduard.braun2@gmx.de> + * + * Copyright (C) 2017 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_GETTEXT_HELPER_H +#define SEEN_GETTEXT_HELPER_H + +namespace Inkscape { + void initialize_gettext(); + void bind_textdomain_codeset_utf8(); + void bind_textdomain_codeset_console(); +} + +#endif // SEEN_GETTEXT_HELPER_H + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace .0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim:filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99: diff --git a/src/helper/mathfns.h b/src/helper/mathfns.h new file mode 100644 index 0000000..3fa51a0 --- /dev/null +++ b/src/helper/mathfns.h @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * ... some mathmatical functions + * + * Authors: + * Johan Engelen <goejendaagh@zonnet.nl> + * + * Copyright (C) 2007 Johan Engelen + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_INKSCAPE_UTIL_MATHFNS_H +#define SEEN_INKSCAPE_UTIL_MATHFNS_H + +#include <2geom/point.h> + +namespace Inkscape { + +namespace Util { + +/** + * Returns area in triangle given by points; may be negative. + */ +inline double +triangle_area (Geom::Point p1, Geom::Point p2, Geom::Point p3) +{ + using Geom::X; + using Geom::Y; + return (p1[X]*p2[Y] + p1[Y]*p3[X] + p2[X]*p3[Y] - p2[Y]*p3[X] - p1[Y]*p2[X] - p1[X]*p3[Y]); +} + +/** + * \return x rounded to the nearest multiple of c1 plus c0. + * + * \note + * If c1==0 (and c0 is finite), then returns +/-inf. This makes grid spacing of zero + * mean "ignore the grid in this dimension". + */ +inline double round_to_nearest_multiple_plus(double x, double const c1, double const c0) +{ + return floor((x - c0) / c1 + .5) * c1 + c0; +} + +/** + * \return x rounded to the lower multiple of c1 plus c0. + * + * \note + * If c1==0 (and c0 is finite), then returns +/-inf. This makes grid spacing of zero + * mean "ignore the grid in this dimension". + */ +inline double round_to_lower_multiple_plus(double x, double const c1, double const c0 = 0) +{ + return floor((x - c0) / c1) * c1 + c0; +} + +/** + * \return x rounded to the upper multiple of c1 plus c0. + * + * \note + * If c1==0 (and c0 is finite), then returns +/-inf. This makes grid spacing of zero + * mean "ignore the grid in this dimension". + */ +inline double round_to_upper_multiple_plus(double x, double const c1, double const c0 = 0) +{ + return ceil((x - c0) / c1) * c1 + c0; +} + +} + +} + +#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/helper/pixbuf-ops.cpp b/src/helper/pixbuf-ops.cpp new file mode 100644 index 0000000..4c5b969 --- /dev/null +++ b/src/helper/pixbuf-ops.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Helpers for SPItem -> gdk_pixbuf related stuff + * + * Authors: + * John Cliff <simarilius@yahoo.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 2008 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> + +#include "helper/png-write.h" +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "document.h" +#include "object/sp-root.h" +#include "object/sp-defs.h" +#include "object/sp-use.h" +#include "util/units.h" +#include "inkscape.h" + +#include "helper/pixbuf-ops.h" + +#include <gdk/gdk.h> + +// TODO look for copy-n-paste duplication of this function: +/** + * Hide all items except @item, recursively, skipping groups and defs. + */ +static void hide_other_items_recursively(SPObject *o, SPItem *i, unsigned dkey) +{ + SPItem *item = dynamic_cast<SPItem *>(o); + if ( item + && !dynamic_cast<SPDefs *>(item) + && !dynamic_cast<SPRoot *>(item) + && !dynamic_cast<SPGroup *>(item) + && !dynamic_cast<SPUse *>(item) + && (i != o) ) + { + item->invoke_hide(dkey); + } + + // recurse + if (i != o) { + for (auto& child: o->children) { + hide_other_items_recursively(&child, i, dkey); + } + } +} + + +// The following is a mutation of the flood fill code, the marker preview, and random other samplings. +// The dpi settings don't do anything yet, but I want them to, and was wanting to keep reasonably close +// to the call for the interface to the png writing. + +bool sp_export_jpg_file(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned width, unsigned height, double xdpi, double ydpi, + unsigned long bgcolor, double quality, SPItem* item) +{ + std::unique_ptr<Inkscape::Pixbuf> pixbuf( + sp_generate_internal_bitmap(doc, filename, x0, y0, x1, y1, + width, height, xdpi, ydpi, bgcolor, item)); + + gchar c[32]; + g_snprintf(c, 32, "%f", quality); + gboolean saved = gdk_pixbuf_save(pixbuf->getPixbufRaw(), filename, "jpeg", nullptr, "quality", c, NULL); + + return saved; +} + + +/** + generates a bitmap from given items + the bitmap is stored in RAM and not written to file + @param x0 area left in document coordinates + @param y0 area top in document coordinates + @param x1 area right in document coordinates + @param y1 area bottom in document coordinates + @param width bitmap width in pixels + @param height bitmap height in pixels + @param xdpi + @param ydpi + @return the created GdkPixbuf structure or NULL if no memory is allocable +*/ +Inkscape::Pixbuf *sp_generate_internal_bitmap(SPDocument *doc, gchar const */*filename*/, + double x0, double y0, double x1, double y1, + unsigned width, unsigned height, double xdpi, double ydpi, + unsigned long /*bgcolor*/, + SPItem *item_only) + +{ + if (width == 0 || height == 0) return nullptr; + + Inkscape::Pixbuf *inkpb = nullptr; + /* Create new drawing for offscreen rendering*/ + Inkscape::Drawing drawing; + drawing.setExact(true); + unsigned dkey = SPItem::display_key_new(1); + + doc->ensureUpToDate(); + + Geom::Rect screen=Geom::Rect(Geom::Point(x0,y0), Geom::Point(x1, y1)); + + Geom::Point origin = screen.min(); + + Geom::Scale scale(Inkscape::Util::Quantity::convert(xdpi, "px", "in"), Inkscape::Util::Quantity::convert(ydpi, "px", "in")); + Geom::Affine affine = scale * Geom::Translate(-origin * scale); + + /* Create ArenaItems and set transform */ + Inkscape::DrawingItem *root = doc->getRoot()->invoke_show( drawing, dkey, SP_ITEM_SHOW_DISPLAY); + root->setTransform(affine); + drawing.setRoot(root); + + // We show all and then hide all items we don't want, instead of showing only requested items, + // because that would not work if the shown item references something in defs + if (item_only) { + hide_other_items_recursively(doc->getRoot(), item_only, dkey); + // TODO: The following line forces 100% opacity as required by sp_asbitmap_render() in cairo-renderer.cpp + // Make it conditional if 'item_only' is ever used by other callers which need to retain opacity + item_only->get_arenaitem(dkey)->setOpacity(1.0); + } + + Geom::IntRect final_bbox = Geom::IntRect::from_xywh(0, 0, width, height); + drawing.update(final_bbox); + + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + + if (cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { + Inkscape::DrawingContext dc(surface, Geom::Point(0,0)); + + // render items + drawing.render(dc, final_bbox, Inkscape::DrawingItem::RENDER_BYPASS_CACHE); + + inkpb = new Inkscape::Pixbuf(surface); + } + else + { + long long size = (long long) height * (long long) cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + g_warning("sp_generate_internal_bitmap: not enough memory to create pixel buffer. Need %lld.", size); + cairo_surface_destroy(surface); + } + doc->getRoot()->invoke_hide(dkey); + +// gdk_pixbuf_save (pixbuf, "C:\\temp\\internal.jpg", "jpeg", NULL, "quality","100", NULL); + + return inkpb; +} + +/* + 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/helper/pixbuf-ops.h b/src/helper/pixbuf-ops.h new file mode 100644 index 0000000..d56579e --- /dev/null +++ b/src/helper/pixbuf-ops.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef __SP_PIXBUF_OPS_H__ +#define __SP_PIXBUF_OPS_H__ + +/* + * Helpers for SPItem -> gdk_pixbuf related stuff + * + * Authors: + * John Cliff <simarilius@yahoo.com> + * + * Copyright (C) 2008 John Cliff + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> + +class SPDocument; +namespace Inkscape { class Pixbuf; } + + bool sp_export_jpg_file (SPDocument *doc, gchar const *filename, double x0, double y0, double x1, double y1, + unsigned int width, unsigned int height, double xdpi, double ydpi, unsigned long bgcolor, double quality, SPItem *item_only = nullptr); + +Inkscape::Pixbuf *sp_generate_internal_bitmap(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned width, unsigned height, double xdpi, double ydpi, + unsigned long bgcolor, SPItem *item_only = nullptr); + +#endif diff --git a/src/helper/png-write.cpp b/src/helper/png-write.cpp new file mode 100644 index 0000000..ba726bf --- /dev/null +++ b/src/helper/png-write.cpp @@ -0,0 +1,508 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * PNG file format utilities + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Whoever wrote this example in libpng documentation + * Peter Bostrom + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright (C) 1999-2002 authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + + +#include <2geom/rect.h> +#include <2geom/transforms.h> + +#include <png.h> + +#include "document.h" +#include "inkscape.h" +#include "png-write.h" +#include "preferences.h" +#include "rdf.h" + +#include "display/cairo-utils.h" +#include "display/drawing-context.h" +#include "display/drawing.h" + +#include "io/sys.h" + +#include "object/sp-defs.h" +#include "object/sp-item.h" +#include "object/sp-root.h" + +#include "ui/interface.h" +#include "util/units.h" + +/* This is an example of how to use libpng to read and write PNG files. + * The file libpng.txt is much more verbose then this. If you have not + * read it, do so first. This was designed to be a starting point of an + * implementation. This is not officially part of libpng, and therefore + * does not require a copyright notice. + * + * This file does not currently compile, because it is missing certain + * parts, like allocating memory to hold an image. You will have to + * supply these parts to get it to compile. For an example of a minimal + * working PNG reader/writer, see pngtest.c, included in this distribution. + */ + +struct SPEBP { + unsigned long int width, height, sheight; + guint32 background; + Inkscape::Drawing *drawing; // it is assumed that all unneeded items are hidden + guchar *px; + unsigned (*status)(float, void *); + void *data; +}; + +/* write a png file */ + +struct SPPNGBD { + guchar const *px; + int rowstride; +}; + +/** + * A simple wrapper to list png_text. + */ +class PngTextList { +public: + PngTextList() : count(0), textItems(nullptr) {} + ~PngTextList(); + + void add(gchar const* key, gchar const* text); + gint getCount() {return count;} + png_text* getPtext() {return textItems;} + +private: + gint count; + png_text* textItems; +}; + +PngTextList::~PngTextList() { + for (gint i = 0; i < count; i++) { + if (textItems[i].key) { + g_free(textItems[i].key); + } + if (textItems[i].text) { + g_free(textItems[i].text); + } + } +} + +void PngTextList::add(gchar const* key, gchar const* text) +{ + if (count < 0) { + count = 0; + textItems = nullptr; + } + png_text* tmp = (count > 0) ? g_try_renew(png_text, textItems, count + 1): g_try_new(png_text, 1); + if (tmp) { + textItems = tmp; + count++; + + png_text* item = &(textItems[count - 1]); + item->compression = PNG_TEXT_COMPRESSION_NONE; + item->key = g_strdup(key); + item->text = g_strdup(text); + item->text_length = 0; +#ifdef PNG_iTXt_SUPPORTED + item->itxt_length = 0; + item->lang = nullptr; + item->lang_key = nullptr; +#endif // PNG_iTXt_SUPPORTED + } else { + g_warning("Unable to allocate array for %d PNG text data.", count); + textItems = nullptr; + count = 0; + } +} + +static bool +sp_png_write_rgba_striped(SPDocument *doc, + gchar const *filename, unsigned long int width, unsigned long int height, double xdpi, double ydpi, + int (* get_rows)(guchar const **rows, void **to_free, int row, int num_rows, void *data, int color_type, int bit_depth, int antialias), + void *data, bool interlace, int color_type, int bit_depth, int zlib, int antialiasing) +{ + g_return_val_if_fail(filename != nullptr, false); + g_return_val_if_fail(data != nullptr, false); + + struct SPEBP *ebp = (struct SPEBP *) data; + FILE *fp; + png_structp png_ptr; + png_infop info_ptr; + png_color_8 sig_bit; + png_uint_32 r; + + /* open the file */ + + Inkscape::IO::dump_fopen_call(filename, "M"); + fp = Inkscape::IO::fopen_utf8name(filename, "wb"); + if(fp == nullptr) return false; + + /* Create and initialize the png_struct with the desired error handler + * functions. If you want to use the default stderr and longjump method, + * you can supply NULL for the last three parameters. We also check that + * the library version is compatible with the one used at compile time, + * in case we are using dynamically linked libraries. REQUIRED. + */ + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + + if (png_ptr == nullptr) { + fclose(fp); + return false; + } + + /* Allocate/initialize the image information data. REQUIRED */ + info_ptr = png_create_info_struct(png_ptr); + if (info_ptr == nullptr) { + fclose(fp); + png_destroy_write_struct(&png_ptr, nullptr); + return false; + } + + /* Set error handling. REQUIRED if you aren't supplying your own + * error handling functions in the png_create_write_struct() call. + */ + if (setjmp(png_jmpbuf(png_ptr))) { + // If we get here, we had a problem reading the file + fclose(fp); + png_destroy_write_struct(&png_ptr, &info_ptr); + return false; + } + + /* set up the output control if you are using standard C streams */ + png_init_io(png_ptr, fp); + + /* Set the image information here. Width and height are up to 2^31, + * bit_depth is one of 1, 2, 4, 8, or 16, but valid values also depend on + * the color_type selected. color_type is one of PNG_COLOR_TYPE_GRAY, + * PNG_COLOR_TYPE_GRAY_ALPHA, PNG_COLOR_TYPE_PALETTE, PNG_COLOR_TYPE_RGB, + * or PNG_COLOR_TYPE_RGB_ALPHA. interlace is either PNG_INTERLACE_NONE or + * PNG_INTERLACE_ADAM7, and the compression_type and filter_type MUST + * currently be PNG_COMPRESSION_TYPE_BASE and PNG_FILTER_TYPE_BASE. REQUIRED + */ + + png_set_compression_level(png_ptr, zlib); + + png_set_IHDR(png_ptr, info_ptr, + width, + height, + bit_depth, + color_type, + interlace ? PNG_INTERLACE_ADAM7 : PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + + if ((color_type&2) && bit_depth == 16) { + // otherwise, if we are dealing with a color image then + sig_bit.red = 8; + sig_bit.green = 8; + sig_bit.blue = 8; + // if the image has an alpha channel then + if (color_type&4) + sig_bit.alpha = 8; + png_set_sBIT(png_ptr, info_ptr, &sig_bit); + } + + PngTextList textList; + + textList.add("Software", "www.inkscape.org"); // Made by Inkscape comment + { + const gchar* pngToDc[] = {"Title", "title", + "Author", "creator", + "Description", "description", + //"Copyright", "", + "Creation Time", "date", + //"Disclaimer", "", + //"Warning", "", + "Source", "source" + //"Comment", "" + }; + for (size_t i = 0; i < G_N_ELEMENTS(pngToDc); i += 2) { + struct rdf_work_entity_t * entity = rdf_find_entity ( pngToDc[i + 1] ); + if (entity) { + gchar const* data = rdf_get_work_entity(doc, entity); + if (data && *data) { + textList.add(pngToDc[i], data); + } + } else { + g_warning("Unable to find entity [%s]", pngToDc[i + 1]); + } + } + + + struct rdf_license_t *license = rdf_get_license(doc); + if (license) { + if (license->name && license->uri) { + gchar* tmp = g_strdup_printf("%s %s", license->name, license->uri); + textList.add("Copyright", tmp); + g_free(tmp); + } else if (license->name) { + textList.add("Copyright", license->name); + } else if (license->uri) { + textList.add("Copyright", license->uri); + } + } + } + if (textList.getCount() > 0) { + png_set_text(png_ptr, info_ptr, textList.getPtext(), textList.getCount()); + } + + /* other optional chunks like cHRM, bKGD, tRNS, tIME, oFFs, pHYs, */ + /* note that if sRGB is present the cHRM chunk must be ignored + * on read and must be written in accordance with the sRGB profile */ + if(xdpi < 0.0254 ) xdpi = 0.0255; + if(ydpi < 0.0254 ) ydpi = 0.0255; + + png_set_pHYs(png_ptr, info_ptr, unsigned(xdpi / 0.0254 ), unsigned(ydpi / 0.0254 ), PNG_RESOLUTION_METER); + + /* Write the file header information. REQUIRED */ + png_write_info(png_ptr, info_ptr); + + /* Once we write out the header, the compression type on the text + * chunks gets changed to PNG_TEXT_COMPRESSION_NONE_WR or + * PNG_TEXT_COMPRESSION_zTXt_WR, so it doesn't get written out again + * at the end. + */ + + /* set up the transformations you want. Note that these are + * all optional. Only call them if you want them. + */ + + /* --- CUT --- */ + + /* The easiest way to write the image (you may have a different memory + * layout, however, so choose what fits your needs best). You need to + * use the first method if you aren't handling interlacing yourself. + */ + + png_bytep* row_pointers = new png_bytep[ebp->sheight]; + int number_of_passes = interlace ? png_set_interlace_handling(png_ptr) : 1; + + for(int i=0;i<number_of_passes; ++i){ + r = 0; + while (r < static_cast<png_uint_32>(height)) { + void *to_free; + int n = get_rows((unsigned char const **) row_pointers, &to_free, r, height-r, data, color_type, bit_depth, antialiasing); + if (!n) break; + png_write_rows(png_ptr, row_pointers, n); + g_free(to_free); + r += n; + } + } + + delete[] row_pointers; + + /* You can write optional chunks like tEXt, zTXt, and tIME at the end + * as well. + */ + + /* It is REQUIRED to call this to finish writing the rest of the file */ + png_write_end(png_ptr, info_ptr); + + /* if you allocated any text comments, free them here */ + + /* clean up after the write, and free any memory allocated */ + png_destroy_write_struct(&png_ptr, &info_ptr); + + /* close the file */ + fclose(fp); + + /* that's it */ + return true; +} + + +/** + * + */ +static int +sp_export_get_rows(guchar const **rows, void **to_free, int row, int num_rows, void *data, int color_type, int bit_depth, int antialiasing) +{ + struct SPEBP *ebp = (struct SPEBP *) data; + + if (ebp->status) { + if (!ebp->status((float) row / ebp->height, ebp->data)) return 0; + } + + num_rows = MIN(num_rows, static_cast<int>(ebp->sheight)); + num_rows = MIN(num_rows, static_cast<int>(ebp->height - row)); + + /* Set area of interest */ + // bbox is now set to the entire image to prevent discontinuities + // in the image when blur is used (the borders may still be a bit + // off, but that's less noticeable). + Geom::IntRect bbox = Geom::IntRect::from_xywh(0, row, ebp->width, num_rows); + + /* Update to renderable state */ + ebp->drawing->update(bbox); + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, ebp->width); + unsigned char *px = g_new(guchar, num_rows * stride); + + cairo_surface_t *s = cairo_image_surface_create_for_data( + px, CAIRO_FORMAT_ARGB32, ebp->width, num_rows, stride); + Inkscape::DrawingContext dc(s, bbox.min()); + dc.setSource(ebp->background); + dc.setOperator(CAIRO_OPERATOR_SOURCE); + dc.paint(); + dc.setOperator(CAIRO_OPERATOR_OVER); + + /* Render */ + ebp->drawing->render(dc, bbox, 0, antialiasing); + cairo_surface_destroy(s); + + // PNG stores data as unpremultiplied big-endian RGBA, which means + // it's identical to the GdkPixbuf format. + convert_pixels_argb32_to_pixbuf(px, ebp->width, num_rows, stride); + + // If a custom bit depth or color type is asked, then convert rgb to grayscale, etc. + const guchar* new_data = pixbuf_to_png(rows, px, num_rows, ebp->width, stride, color_type, bit_depth); + *to_free = (void*) new_data; + free(px); + + return num_rows; +} + +/** + * Hide all items that are not listed in list, recursively, skipping groups and defs. + */ +static void hide_other_items_recursively(SPObject *o, const std::vector<SPItem*> &list, unsigned dkey) +{ + if ( SP_IS_ITEM(o) + && !SP_IS_DEFS(o) + && !SP_IS_ROOT(o) + && !SP_IS_GROUP(o) + && list.end()==find(list.begin(),list.end(),o)) + { + SP_ITEM(o)->invoke_hide(dkey); + } + + // recurse + if (list.end()==find(list.begin(),list.end(),o)) { + for (auto& child: o->children) { + hide_other_items_recursively(&child, list, dkey); + } + } +} + + +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned long int width, unsigned long int height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned int (*status) (float, void *), + void *data, bool force_overwrite, + const std::vector<SPItem*> &items_only, bool interlace, int color_type, int bit_depth, int zlib, int antialiasing) +{ + return sp_export_png_file(doc, filename, Geom::Rect(Geom::Point(x0,y0),Geom::Point(x1,y1)), + width, height, xdpi, ydpi, bgcolor, status, data, force_overwrite, items_only, interlace, color_type, bit_depth, zlib, antialiasing); +} + +/** + * Export an area to a PNG file + * + * @param area Area in document coordinates + */ +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + Geom::Rect const &area, + unsigned long width, unsigned long height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned (*status)(float, void *), + void *data, bool force_overwrite, + const std::vector<SPItem*> &items_only, bool interlace, int color_type, int bit_depth, int zlib, int antialiasing) +{ + g_return_val_if_fail(doc != nullptr, EXPORT_ERROR); + g_return_val_if_fail(filename != nullptr, EXPORT_ERROR); + g_return_val_if_fail(width >= 1, EXPORT_ERROR); + g_return_val_if_fail(height >= 1, EXPORT_ERROR); + g_return_val_if_fail(!area.hasZeroArea(), EXPORT_ERROR); + + + if (!force_overwrite && !sp_ui_overwrite_file(filename)) { + // aborted overwrite + return EXPORT_ABORTED; + } + + doc->ensureUpToDate(); + + /* Calculate translation by transforming to document coordinates (flipping Y)*/ + Geom::Point translation = -area.min(); + + /* This calculation is only valid when assumed that (x0,y0)= area.corner(0) and (x1,y1) = area.corner(2) + * 1) a[0] * x0 + a[2] * y1 + a[4] = 0.0 + * 2) a[1] * x0 + a[3] * y1 + a[5] = 0.0 + * 3) a[0] * x1 + a[2] * y1 + a[4] = width + * 4) a[1] * x0 + a[3] * y0 + a[5] = height + * 5) a[1] = 0.0; + * 6) a[2] = 0.0; + * + * (1,3) a[0] * x1 - a[0] * x0 = width + * a[0] = width / (x1 - x0) + * (2,4) a[3] * y0 - a[3] * y1 = height + * a[3] = height / (y0 - y1) + * (1) a[4] = -a[0] * x0 + * (2) a[5] = -a[3] * y1 + */ + + Geom::Affine const affine(Geom::Translate(translation) + * Geom::Scale(width / area.width(), + height / area.height())); + + struct SPEBP ebp; + ebp.width = width; + ebp.height = height; + ebp.background = bgcolor; + + /* Create new drawing */ + Inkscape::Drawing drawing; + drawing.setExact(true); // export with maximum blur rendering quality + unsigned const dkey = SPItem::display_key_new(1); + + // Create ArenaItems and set transform + drawing.setRoot(doc->getRoot()->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY)); + drawing.root()->setTransform(affine); + ebp.drawing = &drawing; + + // We show all and then hide all items we don't want, instead of showing only requested items, + // because that would not work if the shown item references something in defs + if (!items_only.empty()) { + hide_other_items_recursively(doc->getRoot(), items_only, dkey); + } + + ebp.status = status; + ebp.data = data; + + bool write_status = false;; + + ebp.sheight = 64; + ebp.px = g_try_new(guchar, 4 * ebp.sheight * width); + + if (ebp.px) { + write_status = sp_png_write_rgba_striped(doc, filename, width, height, xdpi, ydpi, sp_export_get_rows, &ebp, interlace, color_type, bit_depth, zlib, antialiasing); + g_free(ebp.px); + } + + // Hide items, this releases arenaitem + doc->getRoot()->invoke_hide(dkey); + + return write_status ? EXPORT_OK : EXPORT_ERROR; +} + + +/* + 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/helper/png-write.h b/src/helper/png-write.h new file mode 100644 index 0000000..c11ee77 --- /dev/null +++ b/src/helper/png-write.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_SP_PNG_WRITE_H +#define SEEN_SP_PNG_WRITE_H + +/* + * PNG file format utilities + * + * Authors: + * Lauris Kaplinski <lauris@kaplinski.com> + * Peter Bostrom + * Jon A. Cruz <jon@joncruz.org> + * + * Copyright (C) 1999-2002 Lauris Kaplinski + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glib.h> // Only for gchar. + +#include <2geom/forward.h> + +class SPDocument; +class SPItem; + +enum ExportResult { + EXPORT_ERROR = 0, + EXPORT_OK, + EXPORT_ABORTED +}; + +/** + * Export the given document as a Portable Network Graphics (PNG) file. + * + * @return EXPORT_OK if succeeded, EXPORT_ABORTED if no action was taken, EXPORT_ERROR (false) if an error occurred. + */ +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + double x0, double y0, double x1, double y1, + unsigned long int width, unsigned long int height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned int (*status) (float, void *), void *data, bool force_overwrite = false, const std::vector<SPItem*> &items_only = std::vector<SPItem*>(), + bool interlace = false, int color_type = 6, int bit_depth = 8, int zlib = 6, int antialiasing = 2); + +ExportResult sp_export_png_file(SPDocument *doc, gchar const *filename, + Geom::Rect const &area, + unsigned long int width, unsigned long int height, double xdpi, double ydpi, + unsigned long bgcolor, + unsigned int (*status) (float, void *), void *data, bool force_overwrite = false, const std::vector<SPItem*> &items_only = std::vector<SPItem*>(), + bool interlace = false, int color_type = 6, int bit_depth = 8, int zlib = 6, int antialiasing = 2); + +#endif // SEEN_SP_PNG_WRITE_H diff --git a/src/helper/sp-marshal.list b/src/helper/sp-marshal.list new file mode 100644 index 0000000..340d2c5 --- /dev/null +++ b/src/helper/sp-marshal.list @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# marshallers for inkscape +VOID:POINTER,UINT +BOOLEAN:POINTER +BOOLEAN:POINTER,UINT +BOOLEAN:POINTER,POINTER +INT:POINTER,POINTER +DOUBLE:POINTER,UINT +VOID:INT,INT +VOID:STRING,STRING diff --git a/src/helper/stock-items.cpp b/src/helper/stock-items.cpp new file mode 100644 index 0000000..ca96d6b --- /dev/null +++ b/src/helper/stock-items.cpp @@ -0,0 +1,275 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Stock-items + * + * Stock Item management code + * + * Authors: + * John Cliff <simarilius@yahoo.com> + * Jon A. Cruz <jon@joncruz.org> + * Abhishek Sharma + * + * Copyright 2004 John Cliff + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#define noSP_SS_VERBOSE + +#include <cstring> + +#include "path-prefix.h" + +#include <xml/repr.h> +#include "inkscape.h" + +#include "io/sys.h" +#include "stock-items.h" + +#include "object/sp-gradient.h" +#include "object/sp-pattern.h" +#include "object/sp-marker.h" +#include "object/sp-defs.h" + +static SPObject *sp_gradient_load_from_svg(gchar const *name, SPDocument *current_doc); +static SPObject *sp_marker_load_from_svg(gchar const *name, SPDocument *current_doc); +static SPObject *sp_gradient_load_from_svg(gchar const *name, SPDocument *current_doc); + + +// FIXME: these should be merged with the icon loading code so they +// can share a common file/doc cache. This function should just +// take the dir to look in, and the file to check for, and cache +// against that, rather than the existing copy/paste code seen here. + +static SPObject * sp_marker_load_from_svg(gchar const *name, SPDocument *current_doc) +{ + static SPDocument *doc = nullptr; + static unsigned int edoc = FALSE; + if (!current_doc) { + return nullptr; + } + /* Try to load from document */ + if (!edoc && !doc) { + gchar *markers = g_build_filename(INKSCAPE_MARKERSDIR, "/markers.svg", NULL); + if (Inkscape::IO::file_test(markers, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(markers, FALSE); + } + g_free(markers); + if (doc) { + doc->ensureUpToDate(); + } else { + edoc = TRUE; + } + } + if (!edoc && doc) { + /* Get the marker we want */ + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_MARKER(object)) { + SPDefs *defs = current_doc->getDefs(); + Inkscape::XML::Document *xml_doc = current_doc->getReprDoc(); + Inkscape::XML::Node *mark_repr = object->getRepr()->duplicate(xml_doc); + defs->getRepr()->addChild(mark_repr, nullptr); + SPObject *cloned_item = current_doc->getObjectByRepr(mark_repr); + Inkscape::GC::release(mark_repr); + return cloned_item; + } + } + return nullptr; +} + + +static SPObject * +sp_pattern_load_from_svg(gchar const *name, SPDocument *current_doc) +{ + static SPDocument *doc = nullptr; + static unsigned int edoc = FALSE; + if (!current_doc) { + return nullptr; + } + /* Try to load from document */ + if (!edoc && !doc) { + gchar *patterns = g_build_filename(INKSCAPE_PAINTDIR, "/patterns.svg", NULL); + if (Inkscape::IO::file_test(patterns, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(patterns, FALSE); + } + if (!doc) { + gchar *patterns = g_build_filename(CREATE_PAINTDIR, "/patterns.svg", NULL); + if (Inkscape::IO::file_test(patterns, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(patterns, FALSE); + } + g_free(patterns); + if (doc) { + doc->ensureUpToDate(); + } else { + edoc = TRUE; + } + } + } + if (!edoc && doc) { + /* Get the pattern we want */ + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_PATTERN(object)) { + SPDefs *defs = current_doc->getDefs(); + Inkscape::XML::Document *xml_doc = current_doc->getReprDoc(); + Inkscape::XML::Node *pat_repr = object->getRepr()->duplicate(xml_doc); + defs->getRepr()->addChild(pat_repr, nullptr); + Inkscape::GC::release(pat_repr); + return object; + } + } + return nullptr; +} + + +static SPObject * +sp_gradient_load_from_svg(gchar const *name, SPDocument *current_doc) +{ + static SPDocument *doc = nullptr; + static unsigned int edoc = FALSE; + if (!current_doc) { + return nullptr; + } + /* Try to load from document */ + if (!edoc && !doc) { + gchar *gradients = g_build_filename(INKSCAPE_PAINTDIR, "/gradients.svg", NULL); + if (Inkscape::IO::file_test(gradients, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(gradients, FALSE); + } + if (!doc) { + gchar *gradients = g_build_filename(CREATE_PAINTDIR, "/gradients.svg", NULL); + if (Inkscape::IO::file_test(gradients, G_FILE_TEST_IS_REGULAR)) { + doc = SPDocument::createNewDoc(gradients, FALSE); + } + g_free(gradients); + if (doc) { + doc->ensureUpToDate(); + } else { + edoc = TRUE; + } + } + } + if (!edoc && doc) { + /* Get the gradient we want */ + SPObject *object = doc->getObjectById(name); + if (object && SP_IS_GRADIENT(object)) { + SPDefs *defs = current_doc->getDefs(); + Inkscape::XML::Document *xml_doc = current_doc->getReprDoc(); + Inkscape::XML::Node *pat_repr = object->getRepr()->duplicate(xml_doc); + defs->getRepr()->addChild(pat_repr, nullptr); + Inkscape::GC::release(pat_repr); + return object; + } + } + return nullptr; +} + +// get_stock_item returns a pointer to an instance of the desired stock object in the current doc +// if necessary it will import the object. Copes with name clashes through use of the inkscape:stockid property +// This should be set to be the same as the id in the library file. + +SPObject *get_stock_item(gchar const *urn, gboolean stock) +{ + g_assert(urn != nullptr); + + /* check its an inkscape URN */ + if (!strncmp (urn, "urn:inkscape:", 13)) { + + gchar const *e = urn + 13; + int a = 0; + gchar * name = g_strdup(e); + gchar *name_p = name; + while (*name_p != ':' && *name_p != '\0'){ + name_p++; + a++; + } + + if (*name_p ==':') { + name_p++; + } + + gchar * base = g_strndup(e, a); + + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPDefs *defs = doc->getDefs(); + if (!defs) { + g_free(base); + return nullptr; + } + SPObject *object = nullptr; + if (!strcmp(base, "marker") && !stock) { + for (auto& child: defs->children) + { + if (child.getRepr()->attribute("inkscape:stockid") && + !strcmp(name_p, child.getRepr()->attribute("inkscape:stockid")) && + SP_IS_MARKER(&child)) + { + object = &child; + } + } + } + else if (!strcmp(base,"pattern") && !stock) { + for (auto& child: defs->children) + { + if (child.getRepr()->attribute("inkscape:stockid") && + !strcmp(name_p, child.getRepr()->attribute("inkscape:stockid")) && + SP_IS_PATTERN(&child)) + { + object = &child; + } + } + } + else if (!strcmp(base,"gradient") && !stock) { + for (auto& child: defs->children) + { + if (child.getRepr()->attribute("inkscape:stockid") && + !strcmp(name_p, child.getRepr()->attribute("inkscape:stockid")) && + SP_IS_GRADIENT(&child)) + { + object = &child; + } + } + } + + if (object == nullptr) { + + if (!strcmp(base, "marker")) { + object = sp_marker_load_from_svg(name_p, doc); + } + else if (!strcmp(base, "pattern")) { + object = sp_pattern_load_from_svg(name_p, doc); + } + else if (!strcmp(base, "gradient")) { + object = sp_gradient_load_from_svg(name_p, doc); + } + } + + g_free(base); + g_free(name); + + if (object) { + object->setAttribute("inkscape:isstock", "true"); + } + + return object; + } + + else { + + SPDocument *doc = SP_ACTIVE_DOCUMENT; + SPObject *object = doc->getObjectById(urn); + + return object; + } +} + +/* + 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/helper/stock-items.h b/src/helper/stock-items.h new file mode 100644 index 0000000..0d1bb20 --- /dev/null +++ b/src/helper/stock-items.h @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * TODO: insert short description here + *//* + * Authors: + * see git history + * John Cliff <simarilius@yahoo.com> + * + * Copyright (C) 2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_INK_STOCK_ITEMS_H +#define SEEN_INK_STOCK_ITEMS_H + +#include <glib.h> + +class SPObject; + +SPObject *get_stock_item(gchar const *urn, gboolean stock=FALSE); + +#endif // SEEN_INK_STOCK_ITEMS_H diff --git a/src/helper/verb-action.cpp b/src/helper/verb-action.cpp new file mode 100644 index 0000000..48fd8cd --- /dev/null +++ b/src/helper/verb-action.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * Deprecated Gtk::Action that provides a widget for an Inkscape verb + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "verb-action.h" + +#include <glibmm/i18n.h> + +#include <gtkmm/toolitem.h> + +#include "shortcuts.h" +#include "verbs.h" +#include "helper/action.h" +#include "ui/widget/button.h" +#include "widgets/toolbox.h" + +static GtkToolItem * sp_toolbox_button_item_new_from_verb_with_doubleclick( GtkWidget *t, GtkIconSize size, Inkscape::UI::Widget::ButtonType type, + Inkscape::Verb *verb, Inkscape::Verb *doubleclick_verb, + Inkscape::UI::View::View *view); + +GtkToolItem * sp_toolbox_button_item_new_from_verb_with_doubleclick(GtkWidget *t, GtkIconSize size, Inkscape::UI::Widget::ButtonType type, + Inkscape::Verb *verb, Inkscape::Verb *doubleclick_verb, + Inkscape::UI::View::View *view) +{ + SPAction *action = verb->get_action(Inkscape::ActionContext(view)); + if (!action) { + return nullptr; + } + + SPAction *doubleclick_action; + if (doubleclick_verb) { + doubleclick_action = doubleclick_verb->get_action(Inkscape::ActionContext(view)); + } else { + doubleclick_action = nullptr; + } + + /* fixme: Handle sensitive/unsensitive */ + /* fixme: Implement Inkscape::UI::Widget::Button construction from action */ + auto b = Gtk::manage(new Inkscape::UI::Widget::Button(size, type, action, doubleclick_action)); + b->show(); + auto b_toolitem = Gtk::manage(new Gtk::ToolItem()); + b_toolitem->add(*b); + + unsigned int shortcut = sp_shortcut_get_primary(verb); + if (shortcut != GDK_KEY_VoidSymbol) { + gchar *key = sp_shortcut_get_label(shortcut); + gchar *tip = g_strdup_printf ("%s (%s)", action->tip, key); + if ( t ) { + gtk_toolbar_insert(GTK_TOOLBAR(t), b_toolitem->gobj(), -1); + b->set_tooltip_text(tip); + } + g_free(tip); + g_free(key); + } else { + if ( t ) { + gtk_toolbar_insert(GTK_TOOLBAR(t), b_toolitem->gobj(), -1); + b->set_tooltip_text(action->tip); + } + } + + return GTK_TOOL_ITEM(b_toolitem->gobj()); +} + +Glib::RefPtr<VerbAction> VerbAction::create(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view) +{ + Glib::RefPtr<VerbAction> result; + SPAction *action = verb->get_action(Inkscape::ActionContext(view)); + if ( action ) { + //SPAction* action2 = verb2 ? verb2->get_action(Inkscape::ActionContext(view)) : 0; + result = Glib::RefPtr<VerbAction>(new VerbAction(verb, verb2, view)); + } + + return result; +} + +VerbAction::VerbAction(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view) : + Gtk::Action(Glib::ustring(verb->get_id()), verb->get_image(), Glib::ustring(g_dpgettext2(nullptr, "ContextVerb", verb->get_name())), Glib::ustring(_(verb->get_tip()))), + verb(verb), + verb2(verb2), + view(view), + active(false) +{ +} + +VerbAction::~VerbAction() += default; + +Gtk::Widget* VerbAction::create_menu_item_vfunc() +{ + Gtk::Widget* widg = Gtk::Action::create_menu_item_vfunc(); +// g_message("create_menu_item_vfunc() = %p for '%s'", widg, verb->get_id()); + return widg; +} + +Gtk::Widget* VerbAction::create_tool_item_vfunc() +{ +// Gtk::Widget* widg = Gtk::Action::create_tool_item_vfunc(); + GtkIconSize toolboxSize = Inkscape::UI::ToolboxFactory::prefToSize("/toolbox/tools/small"); + GtkWidget* toolbox = nullptr; + auto holder = Glib::wrap(sp_toolbox_button_item_new_from_verb_with_doubleclick( toolbox, toolboxSize, + Inkscape::UI::Widget::BUTTON_TYPE_TOGGLE, + verb, + verb2, + view )); + + auto button_widget = static_cast<Inkscape::UI::Widget::Button *>(holder->get_child()); + + if ( active ) { + button_widget->toggle_set_down(active); + } + button_widget->show_all(); + +// g_message("create_tool_item_vfunc() = %p for '%s'", holder, verb->get_id()); + return holder; +} + +void VerbAction::connect_proxy_vfunc(Gtk::Widget* proxy) +{ +// g_message("connect_proxy_vfunc(%p) for '%s'", proxy, verb->get_id()); + Gtk::Action::connect_proxy_vfunc(proxy); +} + +void VerbAction::disconnect_proxy_vfunc(Gtk::Widget* proxy) +{ +// g_message("disconnect_proxy_vfunc(%p) for '%s'", proxy, verb->get_id()); + Gtk::Action::disconnect_proxy_vfunc(proxy); +} + +void VerbAction::set_active(bool active) +{ + this->active = active; + Glib::SListHandle<Gtk::Widget*> proxies = get_proxies(); + for (auto proxie : proxies) { + Gtk::ToolItem* ti = dynamic_cast<Gtk::ToolItem*>(proxie); + if (ti) { + // *should* have one child that is the Inkscape::UI::Widget::Button + auto child = dynamic_cast<Inkscape::UI::Widget::Button *>(ti->get_child()); + if (child) { + child->toggle_set_down(active); + } + } + } +} + +void VerbAction::on_activate() +{ + if ( verb ) { + SPAction *action = verb->get_action(Inkscape::ActionContext(view)); + if ( action ) { + sp_action_perform(action, nullptr); + } + } +} + + diff --git a/src/helper/verb-action.h b/src/helper/verb-action.h new file mode 100644 index 0000000..1c0171f --- /dev/null +++ b/src/helper/verb-action.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/** + * @file + * A deprecated Gtk::Action that provides a widget for an Inkscape + * Verb. + */ +/* Authors: + * MenTaLguY <mental@rydia.net> + * Lauris Kaplinski <lauris@kaplinski.com> + * bulia byak <buliabyak@users.sf.net> + * Frank Felfe <innerspace@iname.com> + * John Cliff <simarilius@yahoo.com> + * David Turner <novalis@gnu.org> + * Josh Andler <scislac@scislac.com> + * Jon A. Cruz <jon@joncruz.org> + * Maximilian Albert <maximilian.albert@gmail.com> + * Tavmjong Bah <tavmjong@free.fr> + * Abhishek Sharma + * Kris De Gussem <Kris.DeGussem@gmail.com> + * Jabiertxo Arraiza <jabier.arraiza@marker.es> + * + * Copyright (C) 2004 David Turner + * Copyright (C) 2003 MenTaLguY + * Copyright (C) 1999-2015 authors + * Copyright (C) 2001-2002 Ximian, Inc. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef SEEN_VERB_ACTION_H +#define SEEN_VERB_ACTION_H + +#include <gtkmm/action.h> + +namespace Inkscape { +class Verb; + +namespace UI { +namespace View { +class View; +} +} +} + +/** + * \brief A deprecated Gtk::Action that provides a widget for an Inkscape Verb + * + * \deprecated In new code, you should create a Gtk::ToolItem instead of using this + */ +class VerbAction : public Gtk::Action { +public: + static Glib::RefPtr<VerbAction> create(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view); + + ~VerbAction() override; + virtual void set_active(bool active = true); + +protected: + Gtk::Widget* create_menu_item_vfunc() override; + Gtk::Widget* create_tool_item_vfunc() override; + + void connect_proxy_vfunc(Gtk::Widget* proxy) override; + void disconnect_proxy_vfunc(Gtk::Widget* proxy) override; + + void on_activate() override; + +private: + Inkscape::Verb* verb; + Inkscape::Verb* verb2; + Inkscape::UI::View::View *view; + bool active; + + VerbAction(Inkscape::Verb* verb, Inkscape::Verb* verb2, Inkscape::UI::View::View *view); +}; + +#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 : |