// SPDX-License-Identifier: GPL-2.0-or-later /** \file * implementation */ /* * Authors: * Mitsuru Oka * Lauris Kaplinski * Abhishek Sharma * Jon A. Cruz * * 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 #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(); #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 &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 :