/* * Author(s): * Jabiertxo Arraiza Cenoz * * Copyright (C) 2014 Author(s) * * Special thanks to Johan Engelen for the base of the effect -powerstroke- * Also to ScislaC for point me to the idea * Also su_v for his construvtive feedback and time * Also to Mc- (IRC nick) for his important contribution to find real time * values based on * and finaly to Liam P. White for his big help on coding, that save me a lot of hours * * Released under GNU GPL, read the file 'COPYING' for more information */ #include "live_effects/lpe-fillet-chamfer.h" #include <2geom/sbasis-to-bezier.h> #include <2geom/svg-elliptical-arc.h> #include <2geom/line.h> #include "desktop.h" #include "display/curve.h" #include "helper/geom-nodetype.h" #include "live_effects/parameter/filletchamferpointarray.h" // for programmatically updating knots #include "selection.h" #include "tools-switch.h" #include "ui/tool/control-point-selection.h" #include "ui/tool/selectable-control-point.h" #include "ui/tool/node.h" #include "ui/tools/node-tool.h" // TODO due to internal breakage in glibmm headers, this must be last: #include using namespace Geom; namespace Inkscape { namespace LivePathEffect { const double tolerance = 0.001; const double gapHelper = 0.00001; LPEFilletChamfer::LPEFilletChamfer(LivePathEffectObject *lpeobject) : Effect(lpeobject), fillet_chamfer_values(_("Fillet point"), _("Fillet point"), "fillet_chamfer_values", &wr, this), hide_knots(_("Hide knots"), _("Hide knots"), "hide_knots", &wr, this, false), ignore_radius_0(_("Ignore 0 radius knots"), _("Ignore 0 radius knots"), "ignore_radius_0", &wr, this, false), only_selected(_("Change only selected nodes"), _("Change only selected nodes"), "only_selected", &wr, this, false), flexible(_("Flexible radius size (%)"), _("Flexible radius size (%)"), "flexible", &wr, this, false), unit(_("Unit"), _("Unit"), "unit", &wr, this), radius(_("Radius (unit or %)"), _("Radius, in unit or %"), "radius", &wr, this, 0.), helper_size(_("Helper size with direction"), _("Helper size with direction"), "helper_size", &wr, this, 0) { registerParameter(&fillet_chamfer_values); registerParameter(&unit); registerParameter(&radius); registerParameter(&helper_size); registerParameter(&flexible); registerParameter(&ignore_radius_0); registerParameter(&only_selected); registerParameter(&hide_knots); radius.param_set_range(0., infinity()); radius.param_set_increments(1, 1); radius.param_set_digits(4); helper_size.param_set_range(0, infinity()); helper_size.param_set_increments(5, 5); helper_size.param_set_digits(0); } LPEFilletChamfer::~LPEFilletChamfer() {} Gtk::Widget *LPEFilletChamfer::newWidget() { // use manage here, because after deletion of Effect object, others might // still be pointing to this widget. Gtk::VBox *vbox = Gtk::manage(new Gtk::VBox(Effect::newWidget())); vbox->set_border_width(5); vbox->set_homogeneous(false); vbox->set_spacing(2); std::vector::iterator it = param_vector.begin(); while (it != param_vector.end()) { if ((*it)->widget_is_visible) { Parameter *param = *it; Gtk::Widget *widg = param->param_newWidget(); if (param->param_key == "radius") { Inkscape::UI::Widget::Scalar *widgRegistered = Gtk::manage(dynamic_cast(widg)); widgRegistered->signal_value_changed().connect(sigc::mem_fun(*this, &LPEFilletChamfer::updateFillet)); widg = widgRegistered; if (widg) { Gtk::HBox *scalarParameter = dynamic_cast(widg); std::vector childList = scalarParameter->get_children(); Gtk::Entry *entryWidg = dynamic_cast(childList[1]); entryWidg->set_width_chars(6); } } else if (param->param_key == "flexible") { Gtk::CheckButton *widgRegistered = Gtk::manage(dynamic_cast(widg)); widgRegistered->signal_clicked().connect(sigc::mem_fun(*this, &LPEFilletChamfer::toggleFlexFixed)); } else if (param->param_key == "helper_size") { Inkscape::UI::Widget::Scalar *widgRegistered = Gtk::manage(dynamic_cast(widg)); widgRegistered->signal_value_changed().connect(sigc::mem_fun(*this, &LPEFilletChamfer::refreshKnots)); } else if (param->param_key == "hide_knots") { Gtk::CheckButton *widgRegistered = Gtk::manage(dynamic_cast(widg)); widgRegistered->signal_clicked().connect(sigc::mem_fun(*this, &LPEFilletChamfer::toggleHide)); } else if (param->param_key == "only_selected") { Gtk::manage(widg); } else if (param->param_key == "ignore_radius_0") { Gtk::manage(widg); } Glib::ustring *tip = param->param_getTooltip(); if (widg) { vbox->pack_start(*widg, true, true, 2); if (tip) { widg->set_tooltip_text(*tip); } else { widg->set_tooltip_text(""); widg->set_has_tooltip(false); } } } ++it; } Gtk::HBox *filletButtonsContainer = Gtk::manage(new Gtk::HBox(true, 0)); Gtk::Button *fillet = Gtk::manage(new Gtk::Button(Glib::ustring(_("Fillet")))); fillet->signal_clicked().connect(sigc::mem_fun(*this, &LPEFilletChamfer::fillet)); filletButtonsContainer->pack_start(*fillet, true, true, 2); Gtk::Button *inverse = Gtk::manage(new Gtk::Button(Glib::ustring(_("Inverse fillet")))); inverse->signal_clicked().connect(sigc::mem_fun(*this, &LPEFilletChamfer::inverse)); filletButtonsContainer->pack_start(*inverse, true, true, 2); Gtk::HBox *chamferButtonsContainer = Gtk::manage(new Gtk::HBox(true, 0)); Gtk::Button *chamfer = Gtk::manage(new Gtk::Button(Glib::ustring(_("Chamfer")))); chamfer->signal_clicked().connect(sigc::mem_fun(*this, &LPEFilletChamfer::chamfer)); chamferButtonsContainer->pack_start(*chamfer, true, true, 2); Gtk::Button *doubleChamfer = Gtk::manage(new Gtk::Button(Glib::ustring(_("Double chamfer")))); doubleChamfer->signal_clicked().connect(sigc::mem_fun(*this, &LPEFilletChamfer::doubleChamfer)); chamferButtonsContainer->pack_start(*doubleChamfer, true, true, 2); vbox->pack_start(*filletButtonsContainer, true, true, 2); vbox->pack_start(*chamferButtonsContainer, true, true, 2); return vbox; } void LPEFilletChamfer::toggleHide() { std::vector filletChamferData = fillet_chamfer_values.data(); std::vector result; for (std::vector::const_iterator point_it = filletChamferData.begin(); point_it != filletChamferData.end(); ++point_it) { if (hide_knots) { result.push_back(Point((*point_it)[X], abs((*point_it)[Y]) * -1)); } else { result.push_back(Point((*point_it)[X], abs((*point_it)[Y]))); } } fillet_chamfer_values.param_set_and_write_new_value(result); refreshKnots(); } void LPEFilletChamfer::toggleFlexFixed() { std::vector filletChamferData = fillet_chamfer_values.data(); std::vector result; unsigned int i = 0; for (std::vector::const_iterator point_it = filletChamferData.begin(); point_it != filletChamferData.end(); ++point_it) { if (flexible) { result.push_back(Point(fillet_chamfer_values.to_time(i, (*point_it)[X]), (*point_it)[Y])); } else { result.push_back(Point(fillet_chamfer_values.to_len(i, (*point_it)[X]), (*point_it)[Y])); } i++; } if (flexible) { radius.param_set_range(0., 100); radius.param_set_value(0); } else { radius.param_set_range(0., infinity()); radius.param_set_value(0); } fillet_chamfer_values.param_set_and_write_new_value(result); } void LPEFilletChamfer::updateFillet() { double power = 0; if (!flexible) { power = Inkscape::Util::Quantity::convert(radius, unit.get_abbreviation(), "px") * -1; } else { power = radius; } Piecewise > const &pwd2 = fillet_chamfer_values.get_pwd2(); doUpdateFillet(path_from_piecewise(pwd2, tolerance), power); } void LPEFilletChamfer::fillet() { Piecewise > const &pwd2 = fillet_chamfer_values.get_pwd2(); doChangeType(path_from_piecewise(pwd2, tolerance), 1); } void LPEFilletChamfer::inverse() { Piecewise > const &pwd2 = fillet_chamfer_values.get_pwd2(); doChangeType(path_from_piecewise(pwd2, tolerance), 2); } void LPEFilletChamfer::chamfer() { Piecewise > const &pwd2 = fillet_chamfer_values.get_pwd2(); doChangeType(path_from_piecewise(pwd2, tolerance), 3); } void LPEFilletChamfer::doubleChamfer() { Piecewise > const &pwd2 = fillet_chamfer_values.get_pwd2(); doChangeType(path_from_piecewise(pwd2, tolerance), 4); } void LPEFilletChamfer::refreshKnots() { Piecewise > const &pwd2 = fillet_chamfer_values.get_pwd2(); fillet_chamfer_values.recalculate_knots(pwd2); SPDesktop *desktop = inkscape_active_desktop(); if (tools_isactive(desktop, TOOLS_NODES)) { tools_switch(desktop, TOOLS_SELECT); tools_switch(desktop, TOOLS_NODES); } } bool LPEFilletChamfer::nodeIsSelected(Geom::Point nodePoint, std::vector point) { if (point.size() > 0) { for (std::vector::iterator i = point.begin(); i != point.end(); ++i) { Geom::Point p = *i; if (Geom::are_near(p, nodePoint, 2)) { return true; } } } return false; } void LPEFilletChamfer::doUpdateFillet(std::vector const& original_pathv, double power) { std::vector point; SPDesktop *desktop = inkscape_active_desktop(); if (INK_IS_NODE_TOOL(desktop->event_context)) { Inkscape::UI::Tools::NodeTool *nodeTool = INK_NODE_TOOL(desktop->event_context); Inkscape::UI::ControlPointSelection::Set &selection = nodeTool->_selected_nodes->allPoints(); std::vector::iterator pBegin; for (Inkscape::UI::ControlPointSelection::Set::iterator i = selection.begin(); i != selection.end(); ++i) { if ((*i)->selected()) { Inkscape::UI::Node *n = dynamic_cast(*i); pBegin = point.begin(); point.insert(pBegin, desktop->doc2dt(n->position())); } } } std::vector filletChamferData = fillet_chamfer_values.data(); std::vector result; int counter = 0; for (PathVector::const_iterator path_it = original_pathv.begin(); path_it != original_pathv.end(); ++path_it) { int pathCounter = 0; if (path_it->empty()) continue; Geom::Path::const_iterator curve_it1 = path_it->begin(); Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); Geom::Path::const_iterator curve_endit = path_it->end_default(); if (path_it->closed() && path_it->back_closed().isDegenerate()) { const Curve &closingline = path_it->back_closed(); if (are_near(closingline.initialPoint(), closingline.finalPoint())) { curve_endit = path_it->end_open(); } } double powerend = 0; while (curve_it1 != curve_endit) { powerend = power; if (power > 0) { powerend = counter + (power / 100); } if (ignore_radius_0 && (filletChamferData[counter][X] == 0 || filletChamferData[counter][X] == counter)) { powerend = filletChamferData[counter][X]; } if (filletChamferData[counter][Y] == 0) { powerend = filletChamferData[counter][X]; } if (only_selected && !nodeIsSelected(curve_it1->initialPoint(), point)) { powerend = filletChamferData[counter][X]; } result.push_back(Point(powerend, filletChamferData[counter][Y])); ++curve_it1; ++curve_it2; counter++; pathCounter++; } } fillet_chamfer_values.param_set_and_write_new_value(result); } void LPEFilletChamfer::doChangeType(std::vector const& original_pathv, int type) { std::vector point; SPDesktop *desktop = inkscape_active_desktop(); if (INK_IS_NODE_TOOL(desktop->event_context)) { Inkscape::UI::Tools::NodeTool *nodeTool = INK_NODE_TOOL(desktop->event_context); Inkscape::UI::ControlPointSelection::Set &selection = nodeTool->_selected_nodes->allPoints(); std::vector::iterator pBegin; for (Inkscape::UI::ControlPointSelection::Set::iterator i = selection.begin(); i != selection.end(); ++i) { if ((*i)->selected()) { Inkscape::UI::Node *n = dynamic_cast(*i); pBegin = point.begin(); point.insert(pBegin, desktop->doc2dt(n->position())); } } } std::vector filletChamferData = fillet_chamfer_values.data(); std::vector result; int counter = 0; for (PathVector::const_iterator path_it = original_pathv.begin(); path_it != original_pathv.end(); ++path_it) { int pathCounter = 0; if (path_it->empty()) continue; Geom::Path::const_iterator curve_it1 = path_it->begin(); Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); Geom::Path::const_iterator curve_endit = path_it->end_default(); if (path_it->closed() && path_it->back_closed().isDegenerate()) { const Curve &closingline = path_it->back_closed(); if (are_near(closingline.initialPoint(), closingline.finalPoint())) { curve_endit = path_it->end_open(); } } while (curve_it1 != curve_endit) { bool toggle = true; if (filletChamferData[counter][Y] == 0 || (ignore_radius_0 && (filletChamferData[counter][X] == 0 || filletChamferData[counter][X] == counter)) || (only_selected && !nodeIsSelected(curve_it1->initialPoint(), point))) { toggle = false; } if (toggle) { result.push_back(Point(filletChamferData[counter][X], type)); } else { result.push_back(filletChamferData[counter]); } ++curve_it1; if (curve_it2 != curve_endit) { ++curve_it2; } counter++; pathCounter++; } } fillet_chamfer_values.param_set_and_write_new_value(result); } void LPEFilletChamfer::doOnApply(SPLPEItem const *lpeItem) { if (SP_IS_SHAPE(lpeItem)) { std::vector point; PathVector const &original_pathv = SP_SHAPE(lpeItem)->_curve->get_pathvector(); Piecewise > pwd2_in = paths_to_pw(original_pathv); for (PathVector::const_iterator path_it = original_pathv.begin(); path_it != original_pathv.end(); ++path_it) { if (path_it->empty()) continue; Geom::Path::const_iterator curve_it1 = path_it->begin(); Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); Geom::Path::const_iterator curve_endit = path_it->end_default(); if (path_it->closed()) { const Geom::Curve &closingline = path_it->back_closed(); // the closing line segment is always of type // Geom::LineSegment. if (are_near(closingline.initialPoint(), closingline.finalPoint())) { // closingline.isDegenerate() did not work, because it only checks for // *exact* zero length, which goes wrong for relative coordinates and // rounding errors... // the closing line segment has zero-length. So stop before that one! curve_endit = path_it->end_open(); } } int counter = 0; while (curve_it1 != curve_endit) { Geom::NodeType nodetype; if (counter == 0) { if (path_it->closed()) { nodetype = get_nodetype(path_it->back_default(), *curve_it1); } else { nodetype = NODE_NONE; } } else { nodetype = get_nodetype((*path_it)[counter - 1], *curve_it1); } if (nodetype == NODE_CUSP) { point.push_back(Point(0, 1)); } else { point.push_back(Point(0, 0)); } ++curve_it1; if (curve_it2 != curve_endit) { ++curve_it2; } counter++; } } fillet_chamfer_values.param_set_and_write_new_value(point); } else { g_warning("LPE Fillet can only be applied to shapes (not groups)."); } } void LPEFilletChamfer::doBeforeEffect(SPLPEItem const *lpeItem) { if (SP_IS_SHAPE(lpeItem)) { fillet_chamfer_values.set_helper_size(helper_size); fillet_chamfer_values.set_unit(unit.get_abbreviation()); SPCurve *c = SP_IS_PATH(lpeItem) ? static_cast(lpeItem)->get_original_curve() : SP_SHAPE(lpeItem)->getCurve(); std::vector filletChamferData = fillet_chamfer_values.data(); if (!filletChamferData.empty() && getKnotsNumber(c) != (int) filletChamferData.size()) { PathVector const original_pathv = c->get_pathvector(); Piecewise > pwd2_in = paths_to_pw(original_pathv); fillet_chamfer_values.recalculate_controlpoints_for_new_pwd2(pwd2_in); } } else { g_warning("LPE Fillet can only be applied to shapes (not groups)."); } } int LPEFilletChamfer::getKnotsNumber(SPCurve const *c) { int nKnots = c->nodes_in_path(); PathVector const pv = c->get_pathvector(); for (std::vector::const_iterator path_it = pv.begin(); path_it != pv.end(); ++path_it) { if (!(*path_it).closed()) { nKnots--; } } return nKnots; } void LPEFilletChamfer::adjustForNewPath(std::vector const &path_in) { if (!path_in.empty()) { fillet_chamfer_values.recalculate_controlpoints_for_new_pwd2(path_in[0] .toPwSb()); } } std::vector LPEFilletChamfer::doEffect_path(std::vector const &path_in) { std::vector pathvector_out; Piecewise > pwd2_in = paths_to_pw(path_in); pwd2_in = remove_short_cuts(pwd2_in, .01); Piecewise > der = derivative(pwd2_in); Piecewise > n = rot90(unitVector(der)); fillet_chamfer_values.set_pwd2(pwd2_in, n); std::vector filletChamferData = fillet_chamfer_values.data(); unsigned int counter = 0; //from http://launchpadlibrarian.net/12692602/rcp.svg const double K = (4.0 / 3.0) * (sqrt(2.0) - 1.0); for (PathVector::const_iterator path_it = path_in.begin(); path_it != path_in.end(); ++path_it) { if (path_it->empty()) continue; Geom::Path path_out; Geom::Path::const_iterator curve_it1 = path_it->begin(); Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); Geom::Path::const_iterator curve_endit = path_it->end_default(); if (path_it->closed()) { const Geom::Curve &closingline = path_it->back_closed(); // the closing line segment is always of type // Geom::LineSegment. if (are_near(closingline.initialPoint(), closingline.finalPoint())) { // closingline.isDegenerate() did not work, because it only checks for // *exact* zero length, which goes wrong for relative coordinates and // rounding errors... // the closing line segment has zero-length. So stop before that one! curve_endit = path_it->end_open(); } } unsigned int counterCurves = 0; while (curve_it1 != curve_endit) { Coord it1_length = (*curve_it1).length(tolerance); double time_it1, time_it2, time_it1_B, intpart; time_it1 = modf( fillet_chamfer_values.to_time(counter, filletChamferData[counter][X]), &intpart); if (filletChamferData[counter][Y] == 0) { time_it1 = 0; } if (path_it->closed() && curve_it2 == curve_endit) { time_it2 = modf(fillet_chamfer_values.to_time( counter - counterCurves, filletChamferData[counter - counterCurves][X]), &intpart); } else if (!path_it->closed() && curve_it2 == curve_endit){ time_it2 = 0; } else { time_it2 = modf(fillet_chamfer_values.to_time( counter + 1, filletChamferData[counter + 1][X]), &intpart); } double resultLenght = 0; time_it1_B = 1; if (path_it->closed() && curve_it2 == curve_endit) { resultLenght = it1_length + fillet_chamfer_values.to_len( counter - counterCurves, filletChamferData[counter - counterCurves][X]); } else if (!path_it->closed() && curve_it2 == curve_endit){ resultLenght = 0; } else { resultLenght = it1_length + fillet_chamfer_values.to_len( counter + 1, filletChamferData[counter + 1][X]); } if (resultLenght > 0 && time_it2 != 0) { time_it1_B = modf(fillet_chamfer_values.to_time(counter, -resultLenght), &intpart); } else { if (time_it2 == 0) { time_it1_B = 1; } else { time_it1_B = gapHelper; } } if (path_it->closed() && curve_it2 == curve_endit && filletChamferData[counter - counterCurves][Y] == 0) { time_it1_B = 1; time_it2 = 0; } else if (path_it->size() > counterCurves + 1 && filletChamferData[counter + 1][Y] == 0) { time_it1_B = 1; time_it2 = 0; } if (time_it1_B < time_it1) { time_it1_B = time_it1 + gapHelper; } Curve *knotCurve1 = curve_it1->portion(time_it1, time_it1_B); if (counterCurves > 0) { knotCurve1->setInitial(path_out.finalPoint()); } else { path_out.start((*curve_it1).pointAt(time_it1)); } Curve *knotCurve2 = (*path_it).front().portion(time_it2, 1); if (curve_it2 != curve_endit) { knotCurve2 = (*curve_it2).portion(time_it2, 1); } Point startArcPoint = knotCurve1->finalPoint(); Point endArcPoint = (*path_it).front().pointAt(time_it2); if (curve_it2 != curve_endit) { endArcPoint = (*curve_it2).pointAt(time_it2); } double k1 = distance(startArcPoint, curve_it1->finalPoint()) * K; double k2 = distance(endArcPoint, curve_it1->finalPoint()) * K; Geom::CubicBezier const *cubic1 = dynamic_cast(&*knotCurve1); Ray ray1(startArcPoint, curve_it1->finalPoint()); if (cubic1) { ray1.setPoints((*cubic1)[2], startArcPoint); } Point handle1 = Point::polar(ray1.angle(),k1) + startArcPoint; Geom::CubicBezier const *cubic2 = dynamic_cast(&*knotCurve2); Ray ray2(curve_it1->finalPoint(), endArcPoint); if (cubic2) { ray2.setPoints(endArcPoint, (*cubic2)[1]); } Point handle2 = endArcPoint - Point::polar(ray2.angle(),k2); bool ccwToggle = cross(curve_it1->finalPoint() - startArcPoint, endArcPoint - startArcPoint) < 0; double angle = angle_between(ray1, ray2, ccwToggle); double handleAngle = ray1.angle() - angle; if (ccwToggle) { handleAngle = ray1.angle() + angle; } Point inverseHandle1 = Point::polar(handleAngle,k1) + startArcPoint; handleAngle = ray2.angle() + angle; if (ccwToggle) { handleAngle = ray2.angle() - angle; } Point inverseHandle2 = endArcPoint - Point::polar(handleAngle,k2); if (time_it1_B != 1) { if (time_it1_B != gapHelper && time_it1_B != time_it1 + gapHelper) { path_out.append(*knotCurve1); } int type = 0; if(path_it->closed() && curve_it2 == curve_endit){ type = abs(filletChamferData[counter - counterCurves][Y]); } else if (!path_it->closed() && curve_it2 == curve_endit){ //0 } else { type = abs(filletChamferData[counter + 1][Y]); } if (type == 3 || type == 4) { if (type == 4) { Geom::Point central = middle_point(startArcPoint, endArcPoint); LineSegment chamferCenter(central, curve_it1->finalPoint()); path_out.appendNew(chamferCenter.pointAt(0.5)); } path_out.appendNew(endArcPoint); } else if (type == 2) { path_out.appendNew(inverseHandle1, inverseHandle2, endArcPoint); } else { path_out.appendNew(handle1, handle2, endArcPoint); } } else { path_out.append(*knotCurve1); } if (path_it->closed() && curve_it2 == curve_endit) { path_out.close(); } ++curve_it1; if (curve_it2 != curve_endit) { ++curve_it2; } counter++; counterCurves++; } pathvector_out.push_back(path_out); } return pathvector_out; } }; //namespace LivePathEffect }; /* namespace Inkscape */ /* Local Variables: mode:c++ c-file-style:"stroustrup" c-file-offset:((innamespace . 0)(inline-open . 0)(case-label . +)) indent-tabs-mode:nil fill-column:99 End: */ // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :