summaryrefslogtreecommitdiffstats
path: root/src/object/sp-spiral.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/object/sp-spiral.cpp')
-rw-r--r--src/object/sp-spiral.cpp569
1 files changed, 569 insertions, 0 deletions
diff --git a/src/object/sp-spiral.cpp b/src/object/sp-spiral.cpp
new file mode 100644
index 0000000..f8b8065
--- /dev/null
+++ b/src/object/sp-spiral.cpp
@@ -0,0 +1,569 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/** \file
+ * <sodipodi:spiral> implementation
+ */
+/*
+ * Authors:
+ * Mitsuru Oka <oka326@parkcity.ne.jp>
+ * Lauris Kaplinski <lauris@kaplinski.com>
+ * Abhishek Sharma
+ * Jon A. Cruz <jon@joncruz.org>
+ *
+ * Copyright (C) 1999-2002 Lauris Kaplinski
+ * Copyright (C) 2000-2001 Ximian, Inc.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include "live_effects/effect.h"
+#include "svg/svg.h"
+#include "attributes.h"
+#include <2geom/bezier-utils.h>
+#include <2geom/pathvector.h>
+#include "display/curve.h"
+#include <glibmm/i18n.h>
+#include "xml/repr.h"
+#include "document.h"
+
+#include "sp-spiral.h"
+
+SPSpiral::SPSpiral()
+ : SPShape()
+ , cx(0)
+ , cy(0)
+ , exp(1)
+ , revo(3)
+ , rad(1)
+ , arg(0)
+ , t0(0)
+{
+}
+
+SPSpiral::~SPSpiral() = default;
+
+void SPSpiral::build(SPDocument * document, Inkscape::XML::Node * repr) {
+ SPShape::build(document, repr);
+
+ this->readAttr(SPAttr::SODIPODI_CX);
+ this->readAttr(SPAttr::SODIPODI_CY);
+ this->readAttr(SPAttr::SODIPODI_EXPANSION);
+ this->readAttr(SPAttr::SODIPODI_REVOLUTION);
+ this->readAttr(SPAttr::SODIPODI_RADIUS);
+ this->readAttr(SPAttr::SODIPODI_ARGUMENT);
+ this->readAttr(SPAttr::SODIPODI_T0);
+}
+
+Inkscape::XML::Node* SPSpiral::write(Inkscape::XML::Document *xml_doc, Inkscape::XML::Node *repr, guint flags) {
+ if ((flags & SP_OBJECT_WRITE_BUILD) && !repr) {
+ repr = xml_doc->createElement("svg:path");
+ }
+
+ if (flags & SP_OBJECT_WRITE_EXT) {
+ /* Fixme: we may replace these attributes by
+ * sodipodi:spiral="cx cy exp revo rad arg t0"
+ */
+ repr->setAttribute("sodipodi:type", "spiral");
+ repr->setAttributeSvgDouble("sodipodi:cx", this->cx);
+ repr->setAttributeSvgDouble("sodipodi:cy", this->cy);
+ repr->setAttributeSvgDouble("sodipodi:expansion", this->exp);
+ repr->setAttributeSvgDouble("sodipodi:revolution", this->revo);
+ repr->setAttributeSvgDouble("sodipodi:radius", this->rad);
+ repr->setAttributeSvgDouble("sodipodi:argument", this->arg);
+ repr->setAttributeSvgDouble("sodipodi:t0", this->t0);
+ }
+
+ // make sure the curve is rebuilt with all up-to-date parameters
+ this->set_shape();
+
+ // Nulls might be possible if this called iteratively
+ if (!this->_curve) {
+ //g_warning("sp_spiral_write(): No path to copy\n");
+ return nullptr;
+ }
+
+ repr->setAttribute("d", sp_svg_write_path(this->_curve->get_pathvector()));
+
+ SPShape::write(xml_doc, repr, flags | SP_SHAPE_WRITE_PATH);
+
+ return repr;
+}
+
+void SPSpiral::set(SPAttr key, gchar const* value) {
+ /// \todo fixme: we should really collect updates
+ switch (key) {
+ case SPAttr::SODIPODI_CX:
+ if (!sp_svg_length_read_computed_absolute (value, &this->cx)) {
+ this->cx = 0.0;
+ }
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ break;
+
+ case SPAttr::SODIPODI_CY:
+ if (!sp_svg_length_read_computed_absolute (value, &this->cy)) {
+ this->cy = 0.0;
+ }
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ break;
+
+ case SPAttr::SODIPODI_EXPANSION:
+ if (value) {
+ /** \todo
+ * FIXME: check that value looks like a (finite)
+ * number. Create a routine that uses strtod, and
+ * accepts a default value (if strtod finds an error).
+ * N.B. atof/sscanf/strtod consider "nan" and "inf"
+ * to be valid numbers.
+ */
+ this->exp = g_ascii_strtod (value, nullptr);
+ this->exp = CLAMP (this->exp, 0.0, 1000.0);
+ } else {
+ this->exp = 1.0;
+ }
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ break;
+
+ case SPAttr::SODIPODI_REVOLUTION:
+ if (value) {
+ this->revo = g_ascii_strtod (value, nullptr);
+ this->revo = CLAMP (this->revo, 0.05, 1024.0);
+ } else {
+ this->revo = 3.0;
+ }
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ break;
+
+ case SPAttr::SODIPODI_RADIUS:
+ if (!sp_svg_length_read_computed_absolute (value, &this->rad)) {
+ this->rad = MAX (this->rad, 0.001);
+ }
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ break;
+
+ case SPAttr::SODIPODI_ARGUMENT:
+ if (value) {
+ this->arg = g_ascii_strtod (value, nullptr);
+ /** \todo
+ * FIXME: We still need some bounds on arg, for
+ * numerical reasons. E.g., we don't want inf or NaN,
+ * nor near-infinite numbers. I'm inclined to take
+ * modulo 2*pi. If so, then change the knot editors,
+ * which use atan2 - revo*2*pi, which typically
+ * results in very negative arg.
+ */
+ } else {
+ this->arg = 0.0;
+ }
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ break;
+
+ case SPAttr::SODIPODI_T0:
+ if (value) {
+ this->t0 = g_ascii_strtod (value, nullptr);
+ this->t0 = CLAMP (this->t0, 0.0, 0.999);
+ /** \todo
+ * Have shared constants for the allowable bounds for
+ * attributes. There was a bug here where we used -1.0
+ * as the minimum (which leads to NaN via, e.g.,
+ * pow(-1.0, 0.5); see sp_spiral_get_xy for
+ * requirements.
+ */
+ } else {
+ this->t0 = 0.0;
+ }
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+ break;
+
+ default:
+ SPShape::set(key, value);
+ break;
+ }
+}
+
+void SPSpiral::update(SPCtx *ctx, guint flags) {
+ if (flags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_STYLE_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG)) {
+ this->set_shape();
+ }
+
+ SPShape::update(ctx, flags);
+}
+
+const char* SPSpiral::typeName() const {
+ return "spiral";
+}
+
+const char* SPSpiral::displayName() const {
+ return _("Spiral");
+}
+
+gchar* SPSpiral::description() const {
+ // TRANSLATORS: since turn count isn't an integer, please adjust the
+ // string as needed to deal with an localized plural forms.
+ return g_strdup_printf (_("with %3f turns"), this->revo);
+}
+
+/**
+ * Fit beziers together to spiral and draw it.
+ *
+ * \pre dstep \> 0.
+ * \pre is_unit_vector(*hat1).
+ * \post is_unit_vector(*hat2).
+ **/
+void SPSpiral::fitAndDraw(SPCurve* c, double dstep, Geom::Point darray[], Geom::Point const& hat1, Geom::Point& hat2, double* t) const {
+#define BEZIER_SIZE 4
+#define FITTING_MAX_BEZIERS 4
+#define BEZIER_LENGTH (BEZIER_SIZE * FITTING_MAX_BEZIERS)
+
+ g_assert (dstep > 0);
+ g_assert (is_unit_vector (hat1));
+
+ Geom::Point bezier[BEZIER_LENGTH];
+ double d;
+ int depth, i;
+
+ for (d = *t, i = 0; i <= SAMPLE_SIZE; d += dstep, i++) {
+ darray[i] = this->getXY(d);
+
+ /* Avoid useless adjacent dups. (Otherwise we can have all of darray filled with
+ the same value, which upsets chord_length_parameterize.) */
+ if ((i != 0) && (darray[i] == darray[i - 1]) && (d < 1.0)) {
+ i--;
+ d += dstep;
+ /** We mustn't increase dstep for subsequent values of
+ * i: for large spiral.exp values, rate of growth
+ * increases very rapidly.
+ */
+ /** \todo
+ * Get the function itself to decide what value of d
+ * to use next: ensure that we move at least 0.25 *
+ * stroke width, for example. The derivative (as used
+ * for get_tangent before normalization) would be
+ * useful for estimating the appropriate d value. Or
+ * perhaps just start with a small dstep and scale by
+ * some small number until we move >= 0.25 *
+ * stroke_width. Must revert to the original dstep
+ * value for next iteration to avoid the problem
+ * mentioned above.
+ */
+ }
+ }
+
+ double const next_t = d - 2 * dstep;
+ /* == t + (SAMPLE_SIZE - 1) * dstep, in absence of dups. */
+
+ hat2 = -this->getTangent(next_t);
+
+ /** \todo
+ * We should use better algorithm to specify maximum error.
+ */
+ depth = Geom::bezier_fit_cubic_full (bezier, nullptr, darray, SAMPLE_SIZE,
+ hat1, hat2,
+ SPIRAL_TOLERANCE*SPIRAL_TOLERANCE,
+ FITTING_MAX_BEZIERS);
+
+ g_assert(depth * BEZIER_SIZE <= gint(G_N_ELEMENTS(bezier)));
+
+#ifdef SPIRAL_DEBUG
+ if (*t == spiral->t0 || *t == 1.0)
+ g_print ("[%s] depth=%d, dstep=%g, t0=%g, t=%g, arg=%g\n",
+ debug_state, depth, dstep, spiral->t0, *t, spiral->arg);
+#endif
+
+ if (depth != -1) {
+ for (i = 0; i < 4*depth; i += 4) {
+ c->curveto(bezier[i + 1],
+ bezier[i + 2],
+ bezier[i + 3]);
+ }
+ } else {
+#ifdef SPIRAL_VERBOSE
+ g_print ("cant_fit_cubic: t=%g\n", *t);
+#endif
+ for (i = 1; i < SAMPLE_SIZE; i++)
+ c->lineto(darray[i]);
+ }
+
+ *t = next_t;
+
+ g_assert (is_unit_vector (hat2));
+}
+
+void SPSpiral::set_shape() {
+ if (checkBrokenPathEffect()) {
+ return;
+ }
+
+ Geom::Point darray[SAMPLE_SIZE + 1];
+
+ this->requestModified(SP_OBJECT_MODIFIED_FLAG);
+
+ auto c = std::make_unique<SPCurve>();
+
+#ifdef SPIRAL_VERBOSE
+ g_print ("cx=%g, cy=%g, exp=%g, revo=%g, rad=%g, arg=%g, t0=%g\n",
+ this->cx,
+ this->cy,
+ this->exp,
+ this->revo,
+ this->rad,
+ this->arg,
+ this->t0);
+#endif
+
+ /* Initial moveto. */
+ c->moveto(this->getXY(this->t0));
+
+ double const tstep = SAMPLE_STEP / this->revo;
+ double const dstep = tstep / (SAMPLE_SIZE - 1);
+
+ Geom::Point hat1 = this->getTangent(this->t0);
+ Geom::Point hat2;
+
+ double t;
+ for (t = this->t0; t < (1.0 - tstep);) {
+ this->fitAndDraw(c.get(), dstep, darray, hat1, hat2, &t);
+
+ hat1 = -hat2;
+ }
+
+ if ((1.0 - t) > SP_EPSILON) {
+ this->fitAndDraw(c.get(), (1.0 - t) / (SAMPLE_SIZE - 1.0), darray, hat1, hat2, &t);
+ }
+
+ if (prepareShapeForLPE(c.get())) {
+ return;
+ }
+
+ // This happends on undo, fix bug:#1791784
+ setCurveInsync(std::move(c));
+}
+
+/**
+ * Set spiral properties and update display.
+ */
+void SPSpiral::setPosition(gdouble cx, gdouble cy, gdouble exp, gdouble revo, gdouble rad, gdouble arg, gdouble t0) {
+ /** \todo
+ * Consider applying CLAMP or adding in-bounds assertions for
+ * some of these parameters.
+ */
+ this->cx = cx;
+ this->cy = cy;
+ this->exp = exp;
+ this->revo = revo;
+ this->rad = MAX (rad, 0.0);
+ this->arg = arg;
+ this->t0 = CLAMP(t0, 0.0, 0.999);
+
+ this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG);
+}
+
+void SPSpiral::snappoints(std::vector<Inkscape::SnapCandidatePoint> &p, Inkscape::SnapPreferences const *snapprefs) const {
+ // We will determine the spiral's midpoint ourselves, instead of trusting on the base class
+ // Therefore snapping to object midpoints is temporarily disabled
+ Inkscape::SnapPreferences local_snapprefs = *snapprefs;
+ local_snapprefs.setTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT, false);
+
+ SPShape::snappoints(p, &local_snapprefs);
+
+ if (snapprefs->isTargetSnappable(Inkscape::SNAPTARGET_OBJECT_MIDPOINT)) {
+ Geom::Affine const i2dt (this->i2dt_affine ());
+
+ p.emplace_back(Geom::Point(this->cx, this->cy) * i2dt, Inkscape::SNAPSOURCE_OBJECT_MIDPOINT, Inkscape::SNAPTARGET_OBJECT_MIDPOINT);
+ // This point is the start-point of the spiral, which is also returned when _snap_to_itemnode has been set
+ // in the object snapper. In that case we will get a duplicate!
+ }
+}
+
+/**
+ * Set spiral transform
+ */
+Geom::Affine SPSpiral::set_transform(Geom::Affine const &xform)
+{
+ if (pathEffectsEnabled() && !optimizeTransforms()) {
+ return xform;
+ }
+ // Only set transform with proportional scaling
+ if (!xform.withoutTranslation().isUniformScale()) {
+ return xform;
+ }
+ /* Calculate spiral start in parent coords. */
+ Geom::Point pos( Geom::Point(this->cx, this->cy) * xform );
+
+ /* This function takes care of translation and scaling, we return whatever parts we can't
+ handle. */
+ Geom::Affine ret(Geom::Affine(xform).withoutTranslation());
+ gdouble const s = hypot(ret[0], ret[1]);
+ if (s > 1e-9) {
+ ret[0] /= s;
+ ret[1] /= s;
+ ret[2] /= s;
+ ret[3] /= s;
+ } else {
+ ret[0] = 1.0;
+ ret[1] = 0.0;
+ ret[2] = 0.0;
+ ret[3] = 1.0;
+ }
+
+ this->rad *= s;
+
+ /* Find start in item coords */
+ pos = pos * ret.inverse();
+ this->cx = pos[Geom::X];
+ this->cy = pos[Geom::Y];
+
+ this->set_shape();
+
+ // Adjust stroke width
+ this->adjust_stroke(s);
+
+ // Adjust pattern fill
+ this->adjust_pattern(xform * ret.inverse());
+
+ // Adjust gradient fill
+ this->adjust_gradient(xform * ret.inverse());
+
+ return ret;
+}
+
+void SPSpiral::update_patheffect(bool write) {
+ SPShape::update_patheffect(write);
+}
+
+/**
+ * Return one of the points on the spiral.
+ *
+ * \param t specifies how far along the spiral.
+ * \pre \a t in [0.0, 2.03]. (It doesn't make sense for t to be much more
+ * than 1.0, though some callers go slightly beyond 1.0 for curve-fitting
+ * purposes.)
+ */
+Geom::Point SPSpiral::getXY(gdouble t) const {
+ g_assert (this->exp >= 0.0);
+ /* Otherwise we get NaN for t==0. */
+ g_assert (this->exp <= 1000.0);
+ /* Anything much more results in infinities. Even allowing 1000 is somewhat overkill. */
+ g_assert (t >= 0.0);
+ /* Any callers passing -ve t will have a bug for non-integral values of exp. */
+
+ double const rad = this->rad * pow(t, (double)this->exp);
+ double const arg = 2.0 * M_PI * this->revo * t + this->arg;
+
+ return Geom::Point(rad * cos(arg) + this->cx, rad * sin(arg) + this->cy);
+}
+
+
+/**
+ * Returns the derivative of sp_spiral_get_xy with respect to t,
+ * scaled to a unit vector.
+ *
+ * \pre spiral != 0.
+ * \pre 0 \<= t.
+ * \pre p != NULL.
+ * \post is_unit_vector(*p).
+ */
+Geom::Point SPSpiral::getTangent(gdouble t) const {
+ Geom::Point ret(1.0, 0.0);
+
+ g_assert (t >= 0.0);
+ g_assert (this->exp >= 0.0);
+ /* See above for comments on these assertions. */
+
+ double const t_scaled = 2.0 * M_PI * this->revo * t;
+ double const arg = t_scaled + this->arg;
+ double const s = sin(arg);
+ double const c = cos(arg);
+
+ if (this->exp == 0.0) {
+ ret = Geom::Point(-s, c);
+ } else if (t_scaled == 0.0) {
+ ret = Geom::Point(c, s);
+ } else {
+ Geom::Point unrotated(this->exp, t_scaled);
+ double const s_len = L2 (unrotated);
+ g_assert (s_len != 0);
+ /** \todo
+ * Check that this isn't being too hopeful of the hypot
+ * function. E.g. test with numbers around 2**-1070
+ * (denormalized numbers), preferably on a few different
+ * platforms. However, njh says that the usual implementation
+ * does handle both very big and very small numbers.
+ */
+ unrotated /= s_len;
+
+ /* ret = spiral->exp * (c, s) + t_scaled * (-s, c);
+ alternatively ret = (spiral->exp, t_scaled) * (( c, s),
+ (-s, c)).*/
+ ret = Geom::Point(dot(unrotated, Geom::Point(c, -s)),
+ dot(unrotated, Geom::Point(s, c)));
+ /* ret should already be approximately normalized: the
+ matrix ((c, -s), (s, c)) is orthogonal (it just
+ rotates by arg), and unrotated has been normalized,
+ so ret is already of unit length other than numerical
+ error in the above matrix multiplication. */
+
+ /** \todo
+ * I haven't checked how important it is for ret to be very
+ * near unit length; we could get rid of the below.
+ */
+
+ ret.normalize();
+ /* Proof that ret length is non-zero: see above. (Should be near 1.) */
+ }
+
+ g_assert (is_unit_vector(ret));
+ return ret;
+}
+
+/**
+ * Compute rad and/or arg for point on spiral.
+ */
+void SPSpiral::getPolar(gdouble t, gdouble* rad, gdouble* arg) const {
+ if (rad) {
+ *rad = this->rad * pow(t, (double)this->exp);
+ }
+
+ if (arg) {
+ *arg = 2.0 * M_PI * this->revo * t + this->arg;
+ }
+}
+
+/**
+ * Return true if spiral has properties that make it invalid.
+ */
+bool SPSpiral::isInvalid() const {
+ gdouble rad;
+
+ this->getPolar(0.0, &rad, nullptr);
+
+ if (rad < 0.0 || rad > SP_HUGE) {
+ g_print("rad(t=0)=%g\n", rad);
+ return true;
+ }
+
+ this->getPolar(1.0, &rad, nullptr);
+
+ if (rad < 0.0 || rad > SP_HUGE) {
+ g_print("rad(t=1)=%g\n", rad);
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ 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 :