// SPDX-License-Identifier: GPL-2.0-or-later /** \file * LPE "Points to Ellipse" implementation */ /* * Authors: * Markus Schwienbacher * * Copyright (C) Markus Schwienbacher 2013 * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "live_effects/lpe-pts2ellipse.h" #include #include #include #include #include #include #include <2geom/path.h> #include <2geom/circle.h> #include <2geom/ellipse.h> #include <2geom/pathvector.h> #include <2geom/elliptical-arc.h> #include using namespace Geom; namespace Inkscape { namespace LivePathEffect { static const Util::EnumData EllipseMethodData[] = { { EM_AUTO, N_("Auto ellipse"), "auto" }, //!< (2..4 points: circle, from 5 points: ellipse) { EM_CIRCLE, N_("Force circle"), "circle" }, { EM_ISONOMETRIC_CIRCLE, N_("Isometric circle"), "iso_circle" } }; static const Util::EnumDataConverter EMConverter(EllipseMethodData, EM_END); LPEPts2Ellipse::LPEPts2Ellipse(LivePathEffectObject *lpeobject) : Effect(lpeobject), method(_("Method:"), _("Methods to generate the ellipse"), "method", EMConverter, &wr, this, EM_AUTO), gen_isometric_frame(_("_Frame (isometric rectangle)"), _("Draw Parallelogram around the ellipse"), "gen_isometric_frame", &wr, this, false), gen_arc(_("_Arc"), _("Generate open arc (open ellipse)"), "gen_arc", &wr, this, false), other_arc(_("_Other Arc side"), _("switch sides of the arc"), "arc_other", &wr, this, false), slice_arc(_("_Slice Arc"), _("slice the arc"), "slice_arc", &wr, this, false), draw_axes(_("A_xes"), _("Draw both semi-major and semi-minor axes"), "draw_axes", &wr, this, false), rot_axes(_("Axes Rotation"), _("Axes rotation angle [deg]"), "rot_axes", &wr, this, 0), draw_ori_path(_("Source _Path"), _("Show the original source path"), "draw_ori_path", &wr, this, false) { registerParameter(&method); registerParameter(&gen_arc); registerParameter(&other_arc); registerParameter(&slice_arc); registerParameter(&gen_isometric_frame); registerParameter(&draw_axes); registerParameter(&rot_axes); registerParameter(&draw_ori_path); rot_axes.param_set_range(-360,360); rot_axes.param_set_increments(1,10); show_orig_path=true; } LPEPts2Ellipse::~LPEPts2Ellipse() = default; // helper function, transforms a given value into range [0, 2pi] inline double range2pi(double a) { a = fmod(a, 2*M_PI); if(a<0) a+=2*M_PI; return a; } inline double deg2rad(double a) { return a*M_PI/180.0; } inline double rad2deg(double a) { return a*180.0/M_PI; } // helper function, calculates the angle between a0 and a1 in ccw sense // examples: 0..1->1, -1..1->2, pi/4..-pi/4->1.5pi // full rotations: 0..2pi->2pi, -pi..pi->2pi, pi..-pi->0, 2pi..0->0 inline double calc_delta_angle(const double a0, const double a1) { double da=range2pi(a1-a0); if((fabs(da)<1e-9) && (a0moveto(cos(start), sin(start)); double s = start; for (int i=0; i < nda; s = (++i)*da+start) { double e = s + da; if (e > end) e = end; const double len = 4*tan((e - s)/4)/3; const double x0 = cos(s); const double y0 = sin(s); const double x1 = x0 + len * cos(s + M_PI_2); const double y1 = y0 + len * sin(s + M_PI_2); const double x3 = cos(e); const double y3 = sin(e); const double x2 = x3 + len * cos(e - M_PI_2); const double y2 = y3 + len * sin(e - M_PI_2); curve->curveto(x1,y1, x2,y2, x3,y3); } if (slice && !closed) { curve->lineto(0., 0.); } curve->transform(affine); path.append(*curve->first_path()); if ((slice && !closed) || closed) { path.close(true); } // give to GC curve->unref(); return 0; } void gen_iso_frame_paths(Geom::PathVector &path_out, const Geom::Affine &affine) { Geom::Path rect; SPCurve curve; // unit rectangle curve.moveto(-1, -1); curve.lineto(1, -1); curve.lineto(1, 1); curve.lineto(-1, 1); //curve.transform(Rotate(-rot_angle)*affine); curve.transform(affine); rect.append(*curve.first_path()); rect.close(true); path_out.push_back(rect); } void gen_axes_paths(Geom::PathVector &path_out, const Geom::Affine &affine) { LineSegment clx(Point(-1,0),Point(1,0)); LineSegment cly(Point(0,-1),Point(0,1)); Geom::Path plx, ply; plx.append(clx); ply.append(cly); plx*=affine; ply*=affine; path_out.push_back(plx); path_out.push_back(ply); } bool is_ccw(const std::vector & pts) { // method: sum up the angles between edges size_t n=pts.size(); // edges about vertex 0 Point e0=pts.front()-pts.back(); Point e1=pts[1]-pts[0]; Coord sum=cross(e0,e1); // the rest for(size_t i=1;i pts; for(const auto & pit : path_in) { // extract first point of this path pts.push_back(pit.initialPoint()); // iterate over all curves for (const auto & cit : pit) { pts.push_back(cit.finalPoint()); } } // avoid identical start-point and end-point if(pts.front() == pts.back()) { pts.pop_back(); } // special mode: Use first two edges, interpret them as two sides of a parallelogram and // generate an ellipse residing inside the parallelogram. This effect is quite useful when // generating isometric views. Hence, the name. //if(gen_isometric.get_value()) if(method == EM_ISONOMETRIC_CIRCLE) { if(0!=genIsometricEllipse (pts, path_out)) return path_in; } else { if(0!=genFitEllipse(pts, path_out)) return path_in; } return path_out; } /** * Generates an ellipse (or circle) from the vertices of a given path. Thereby, using fitting * algorithms from 2geom. Depending on the settings made by the user regarding things like arc, * slice, circle etc. the final result will be different. We need at least 5 points to fit an * ellipse. With 5 points each point is on the ellipse. For less points we get a circle. */ int LPEPts2Ellipse::genFitEllipse (std::vector const & pts, Geom::PathVector & path_out) { // rotation angle based on user provided rot_axes to position the vertices const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation Affine affine; affine*=Rotate(rot_angle); Coord a0=0; Coord a1=2*M_PI; if(pts.size()<2) { return -1; } else if(pts.size()==2) { // simple line: circle in the middle of the line to the vertices Point line=pts.front()-pts.back(); double radius=line.length()*0.5; if(radius<1e-9) return -1; Point center=middle_point(pts.front(),pts.back()); Circle circle(center[0],center[1],radius); affine*=Scale(circle.radius()); affine*=Translate(circle.center()); Geom::Path path; unit_arc_path(path,affine); path_out.push_back(path); } else if(pts.size()>=5 && EM_AUTO == method) { //!only_circle.get_value()) { // do ellipse try { Ellipse ellipse; ellipse.fit(pts); affine*=Scale(ellipse.ray(X),ellipse.ray(Y)); affine*=Rotate(ellipse.rotationAngle()); affine*=Translate(ellipse.center()); if(gen_arc.get_value()) { Affine inv_affine=affine.inverse(); Point p0=pts.front()*inv_affine; Point p1=pts.back()*inv_affine; const bool ccw_wind=is_ccw(pts); endpoints2angles(ccw_wind,other_arc.get_value(),p0,p1,a0,a1); } Geom::Path path; unit_arc_path(path,affine,a0,a1,slice_arc.get_value()); path_out.push_back(path); if(draw_axes.get_value()) { gen_axes_paths(path_out,affine); } } catch(...) { return -1; } } else { // do a circle (3,4 points, or only_circle set) try { Circle circle; circle.fit(pts); affine*=Scale(circle.radius()); affine*=Translate(circle.center()); if(gen_arc.get_value()) { Point p0=pts.front()-circle.center(); Point p1=pts.back()-circle.center(); const bool ccw_wind=is_ccw(pts); endpoints2angles(ccw_wind,other_arc.get_value(),p0,p1,a0,a1); } Geom::Path path; unit_arc_path(path,affine,a0,a1,slice_arc.get_value()); path_out.push_back(path); } catch(...) { return -1; } } // draw frame? if(gen_isometric_frame.get_value()) { gen_iso_frame_paths(path_out,affine); } // draw axes? if(draw_axes.get_value()) { gen_axes_paths(path_out,affine); } return 0; } int LPEPts2Ellipse::genIsometricEllipse (std::vector const & pts, Geom::PathVector & path_out) { // take the first 3 vertices for the edges if(pts.size() < 3) return -1; // calc edges Point e0=pts[0]-pts[1]; Point e1=pts[2]-pts[1]; Coord ce=cross(e0,e1); // parallel or one is zero? if(fabs(ce)<1e-9) return -1; // unit vectors along edges Point u0=unit_vector(e0); Point u1=unit_vector(e1); // calc angles Coord a0=atan2(e0); // Coord a1=M_PI_2-atan2(e1)-a0; Coord a1=acos(dot(u0,u1))-M_PI_2; // if(fabs(a1)<1e-9) return -1; if(ce<0) a1=-a1; // lengths: l0= length of edge 0; l1= height of parallelogram Coord l0=e0.length()*0.5; Point e0n=e1-dot(u0,e1)*u0; Coord l1=e0n.length()*0.5; // center of the ellipse Point pos=pts[1]+0.5*(e0+e1); // rotation angle based on user provided rot_axes to position the vertices const double rot_angle = -deg2rad(rot_axes); // negative for ccw rotation // build up the affine transformation Affine affine; affine*=Rotate(rot_angle); affine*=Scale(l0,l1); affine*=HShear(-tan(a1)); affine*=Rotate(a0); affine*=Translate(pos); Geom::Path path; unit_arc_path(path,affine); path_out.push_back(path); // draw frame? if(gen_isometric_frame.get_value()) { gen_iso_frame_paths(path_out,affine); } // draw axes? if(draw_axes.get_value()) { gen_axes_paths(path_out,affine); } return 0; } /* ######################## */ } //namespace LivePathEffect } /* namespace Inkscape */ /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :