// SPDX-License-Identifier: GPL-2.0-or-later /** \file * LPE Curve Stitching implementation, used as an example for a base starting class * when implementing new LivePathEffects. * */ /* * Authors: * JF Barraud. * * Copyright (C) Johan Engelen 2007 * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "ui/widget/scalar.h" #include "live_effects/lpe-rough-hatches.h" #include "object/sp-item.h" #include "xml/repr.h" #include <2geom/sbasis-math.h> #include <2geom/bezier-to-sbasis.h> // TODO due to internal breakage in glibmm headers, this must be last: #include namespace Inkscape { namespace LivePathEffect { using namespace Geom; //------------------------------------------------ // Some goodies to navigate through curve's levels. //------------------------------------------------ struct LevelCrossing{ Point pt; double t; bool sign; bool used; std::pair next_on_curve; std::pair prev_on_curve; }; struct LevelCrossingOrder { bool operator()(LevelCrossing a, LevelCrossing b) { return ( a.pt[Y] < b.pt[Y] );// a.pt[X] == b.pt[X] since we are supposed to be on the same level... //return ( a.pt[X] < b.pt[X] || ( a.pt[X] == b.pt[X] && a.pt[Y] < b.pt[Y] ) ); } }; struct LevelCrossingInfo{ double t; unsigned level; unsigned idx; }; struct LevelCrossingInfoOrder { bool operator()(LevelCrossingInfo a, LevelCrossingInfo b) { return a.t < b.t; } }; typedef std::vector LevelCrossings; static std::vector discontinuities(Piecewise > const &f){ std::vector result; if (f.size()==0) return result; result.push_back(f.cuts[0]); Point prev_pt = f.segs[0].at1(); //double old_t = f.cuts[0]; for(unsigned i=1; i{ public: LevelsCrossings():std::vector(){}; LevelsCrossings(std::vector > const ×, Piecewise > const &f, Piecewise const &dx){ for (const auto & time : times){ LevelCrossings lcs; for (double j : time){ LevelCrossing lc; lc.pt = f.valueAt(j); lc.t = j; lc.sign = ( dx.valueAt(j)>0 ); lc.used = false; lcs.push_back(lc); } std::sort(lcs.begin(), lcs.end(), LevelCrossingOrder()); push_back(lcs); } //Now create time ordering. std::vectortemp; for (unsigned i=0; i jumps = discontinuities(f); unsigned jump_idx = 0; unsigned first_in_comp = 0; for (unsigned i=0; i jumps[jump_idx+1]){ std::pairnext_data(temp[first_in_comp].level,temp[first_in_comp].idx); (*this)[lvl][idx].next_on_curve = next_data; first_in_comp = i+1; jump_idx += 1; }else{ std::pair next_data(temp[i+1].level,temp[i+1].idx); (*this)[lvl][idx].next_on_curve = next_data; } } for (unsigned i=0; i next = (*this)[i][j].next_on_curve; (*this)[next.first][next.second].prev_on_curve = std::pair(i,j); } } } void findFirstUnused(unsigned &level, unsigned &idx){ level = size(); idx = 0; for (unsigned i=0; i= (*this)[level].size()-1 || (*this)[level][idx+1].used ) { level = size(); return; } idx += 1; }else{ if ( idx <= 0 || (*this)[level][idx-1].used ) { level = size(); return; } idx -= 1; } direction += 1; return; } //double t = (*this)[level][idx].t; double sign = ((*this)[level][idx].sign ? 1 : -1); //---double next_t = t; //level += 1; direction = (direction + 1)%4; if (level == size()){ return; } std::pair next; if ( sign > 0 ){ next = (*this)[level][idx].next_on_curve; }else{ next = (*this)[level][idx].prev_on_curve; } if ( level+1 != next.first || (*this)[next.first][next.second].used ) { level = size(); return; } level = next.first; idx = next.second; return; } }; //------------------------------------------------------- // Bend a path... //------------------------------------------------------- static Piecewise > bend(Piecewise > const &f, Piecewise bending){ D2 > ff = make_cuts_independent(f); ff[X] += compose(bending, ff[Y]); return sectionize(ff); } //-------------------------------------------------------- // The RoughHatches lpe. //-------------------------------------------------------- LPERoughHatches::LPERoughHatches(LivePathEffectObject *lpeobject) : Effect(lpeobject), hatch_dist(0), dist_rdm(_("Frequency randomness:"), _("Variation of distance between hatches, in %."), "dist_rdm", &wr, this, 75), growth(_("Growth:"), _("Growth of distance between hatches."), "growth", &wr, this, 0.), //FIXME: top/bottom names are inverted in the UI/svg and in the code!! scale_tf(_("Half-turns smoothness: 1st side, in:"), _("Set smoothness/sharpness of path when reaching a 'bottom' half-turn. 0=sharp, 1=default"), "scale_bf", &wr, this, 1.), scale_tb(_("1st side, out:"), _("Set smoothness/sharpness of path when leaving a 'bottom' half-turn. 0=sharp, 1=default"), "scale_bb", &wr, this, 1.), scale_bf(_("2nd side, in:"), _("Set smoothness/sharpness of path when reaching a 'top' half-turn. 0=sharp, 1=default"), "scale_tf", &wr, this, 1.), scale_bb(_("2nd side, out:"), _("Set smoothness/sharpness of path when leaving a 'top' half-turn. 0=sharp, 1=default"), "scale_tb", &wr, this, 1.), top_edge_variation(_("Magnitude jitter: 1st side:"), _("Randomly moves 'bottom' half-turns to produce magnitude variations."), "bottom_edge_variation", &wr, this, 0), bot_edge_variation(_("2nd side:"), _("Randomly moves 'top' half-turns to produce magnitude variations."), "top_edge_variation", &wr, this, 0), top_tgt_variation(_("Parallelism jitter: 1st side:"), _("Add direction randomness by moving 'bottom' half-turns tangentially to the boundary."), "bottom_tgt_variation", &wr, this, 0), bot_tgt_variation(_("2nd side:"), _("Add direction randomness by randomly moving 'top' half-turns tangentially to the boundary."), "top_tgt_variation", &wr, this, 0), top_smth_variation(_("Variance: 1st side:"), _("Randomness of 'bottom' half-turns smoothness"), "top_smth_variation", &wr, this, 0), bot_smth_variation(_("2nd side:"), _("Randomness of 'top' half-turns smoothness"), "bottom_smth_variation", &wr, this, 0), // fat_output(_("Generate thick/thin path"), _("Simulate a stroke of varying width"), "fat_output", &wr, this, true), do_bend(_("Bend hatches"), _("Add a global bend to the hatches (slower)"), "do_bend", &wr, this, true), stroke_width_top(_("Thickness: at 1st side:"), _("Width at 'bottom' half-turns"), "stroke_width_top", &wr, this, 1.), stroke_width_bot(_("At 2nd side:"), _("Width at 'top' half-turns"), "stroke_width_bottom", &wr, this, 1.), // front_thickness(_("From 2nd to 1st side:"), _("Width from 'top' to 'bottom'"), "front_thickness", &wr, this, 1.), back_thickness(_("From 1st to 2nd side:"), _("Width from 'bottom' to 'top'"), "back_thickness", &wr, this, .25), direction(_("Hatches width and dir"), _("Defines hatches frequency and direction"), "direction", &wr, this, Geom::Point(50,0)), // bender(_("Global bending"), _("Relative position to a reference point defines global bending direction and amount"), "bender", &wr, this, Geom::Point(-5,0)) { registerParameter(&direction); registerParameter(&dist_rdm); registerParameter(&growth); registerParameter(&do_bend); registerParameter(&bender); registerParameter(&top_edge_variation); registerParameter(&bot_edge_variation); registerParameter(&top_tgt_variation); registerParameter(&bot_tgt_variation); registerParameter(&scale_tf); registerParameter(&scale_tb); registerParameter(&scale_bf); registerParameter(&scale_bb); registerParameter(&top_smth_variation); registerParameter(&bot_smth_variation); registerParameter(&fat_output); registerParameter(&stroke_width_top); registerParameter(&stroke_width_bot); registerParameter(&front_thickness); registerParameter(&back_thickness); //hatch_dist.param_set_range(0.1, Geom::infinity()); growth.param_set_range(0, std::numeric_limits::max()); dist_rdm.param_set_range(0, 99.); stroke_width_top.param_set_range(0, std::numeric_limits::max()); stroke_width_bot.param_set_range(0, std::numeric_limits::max()); front_thickness.param_set_range(0, std::numeric_limits::max()); back_thickness.param_set_range(0, std::numeric_limits::max()); // hide the widgets for direction and bender vectorparams direction.widget_is_visible = false; bender.widget_is_visible = false; // give distinguishing colors to direction and bender on-canvas params direction.set_oncanvas_color(0x00ff7d00); bender.set_oncanvas_color(0xffffb500); concatenate_before_pwd2 = false; show_orig_path = true; } LPERoughHatches::~LPERoughHatches() = default; void LPERoughHatches::doOnApply(SPLPEItem const *lpeitem) { lpeversion.param_setValue("1.2", true); } Geom::Piecewise > LPERoughHatches::doEffect_pwd2 (Geom::Piecewise > const & pwd2_in){ //std::cout<<"doEffect_pwd2:\n"; Piecewise > result; Piecewise > transformed_pwd2_in = pwd2_in; Point start = pwd2_in.segs.front().at0(); Point end = pwd2_in.segs.back().at1(); if (end != start ){ transformed_pwd2_in.push_cut( transformed_pwd2_in.cuts.back() + 1 ); D2 stitch( SBasis( 1, Linear(end[X],start[X]) ), SBasis( 1, Linear(end[Y],start[Y]) ) ); transformed_pwd2_in.push_seg( stitch ); } Point transformed_org = direction.getOrigin(); Piecewise tilter;//used to bend the hatches Affine bend_mat;//used to bend the hatches if (do_bend.get_value()){ Point bend_dir = -rot90(unit_vector(bender.getVector())); double bend_amount = L2(bender.getVector()); bend_mat = Affine(-bend_dir[Y], bend_dir[X], bend_dir[X], bend_dir[Y],0,0); transformed_pwd2_in = transformed_pwd2_in * bend_mat; tilter = Piecewise(shift(Linear(-bend_amount),1)); OptRect bbox = bounds_exact( transformed_pwd2_in ); if (!(bbox)) return pwd2_in; tilter.setDomain((*bbox)[Y]); transformed_pwd2_in = bend(transformed_pwd2_in, tilter); transformed_pwd2_in = transformed_pwd2_in * bend_mat.inverse(); } hatch_dist = Geom::L2(direction.getVector())/5; Point hatches_dir = rot90(unit_vector(direction.getVector())); Affine mat(-hatches_dir[Y], hatches_dir[X], hatches_dir[X], hatches_dir[Y],0,0); transformed_pwd2_in = transformed_pwd2_in * mat; transformed_org *= mat; std::vector > snakePoints; snakePoints = linearSnake(transformed_pwd2_in, transformed_org); if (!snakePoints.empty()){ Piecewise >smthSnake = smoothSnake(snakePoints); smthSnake = smthSnake*mat.inverse(); if (do_bend.get_value()){ smthSnake = smthSnake*bend_mat; smthSnake = bend(smthSnake, -tilter); smthSnake = smthSnake*bend_mat.inverse(); } return (smthSnake); } return pwd2_in; } //------------------------------------------------ // Generate the levels with random, growth... //------------------------------------------------ std::vector LPERoughHatches::generateLevels(Interval const &domain, double x_org){ std::vector result; int n = int((domain.min()-x_org)/hatch_dist); double x = x_org + n * hatch_dist; //double x = domain.min() + double(hatch_dist)/2.; double step = double(hatch_dist); double scale = 1+(hatch_dist*growth/domain.extent()); while (x < domain.max()){ result.push_back(x); double rdm = 1; if (dist_rdm.get_value() != 0) rdm = 1.+ double((2*dist_rdm - dist_rdm.get_value()))/100.; x+= step*rdm; step*=scale;//(1.+double(growth)); } return result; } //------------------------------------------------------- // Walk through the intersections to create linear hatches //------------------------------------------------------- std::vector > LPERoughHatches::linearSnake(Piecewise > const &f, Point const &org){ //std::cout<<"linearSnake:\n"; std::vector > result; Piecewise x = make_cuts_independent(f)[X]; //Remark: derivative is computed twice in the 2 lines below!! Piecewise dx = derivative(x); OptInterval range = bounds_exact(x); if (!range) return result; std::vector levels = generateLevels(*range, org[X]); std::vector > times; times = multi_roots(x,levels); //TODO: fix multi_roots!!!***************************************** //remove doubles :-( std::vector > cleaned_times(levels.size(),std::vector()); for (unsigned i=0; i0 ){ double last_t = times[i][0]-1;//ugly hack!! for (unsigned j=0; j0.000001){ last_t = times[i][j]; cleaned_times[i].push_back(last_t); } } } } times = cleaned_times; //******************************************************************* LevelsCrossings lscs(times,f,dx); unsigned i,j; lscs.findFirstUnused(i,j); std::vector result_component; int n = int((range->min()-org[X])/hatch_dist); while ( i < lscs.size() ){ int dir = 0; //switch orientation of first segment according to starting point. if ((static_cast(i) % 2 == n % 2) && ((j + 1) < lscs[i].size()) && !lscs[i][j].used){ j += 1; dir = 2; } while ( i < lscs.size() ){ result_component.push_back(lscs[i][j].pt); lscs[i][j].used = true; lscs.step(i,j, dir); } result.push_back(result_component); result_component = std::vector(); lscs.findFirstUnused(i,j); } return result; } //------------------------------------------------------- // Smooth the linear hatches according to params... //------------------------------------------------------- Piecewise > LPERoughHatches::smoothSnake(std::vector > const &linearSnake){ Piecewise > result; for (const auto & comp : linearSnake){ if (comp.size()>=2){ Point last_pt = comp[0]; //Point last_top = linearSnake[comp][0]; //Point last_bot = linearSnake[comp][0]; Point last_hdle = comp[0]; Point last_top_hdle = comp[0]; Point last_bot_hdle = comp[0]; Geom::Path res_comp(last_pt); Geom::Path res_comp_top(last_pt); Geom::Path res_comp_bot(last_pt); unsigned i=1; //bool is_top = true;//Inversion here; due to downward y? bool is_top = ( comp[0][Y] < comp[1][Y] ); while( i+1 inside[X]) inside_hdle_in = inside; //if (inside_hdle_out[X] < inside[X]) inside_hdle_out = inside; if (is_top){ res_comp_top.appendNew(last_top_hdle,new_hdle_in,new_pt); res_comp_bot.appendNew(last_bot_hdle,inside_hdle_in,inside); last_top_hdle = new_hdle_out; last_bot_hdle = inside_hdle_out; }else{ res_comp_top.appendNew(last_top_hdle,inside_hdle_in,inside); res_comp_bot.appendNew(last_bot_hdle,new_hdle_in,new_pt); last_top_hdle = inside_hdle_out; last_bot_hdle = new_hdle_out; } }else{ res_comp.appendNew(last_hdle,new_hdle_in,new_pt); } last_hdle = new_hdle_out; i+=2; is_top = !is_top; } if ( i(last_top_hdle,comp[i],comp[i]); res_comp_bot.appendNew(last_bot_hdle,comp[i],comp[i]); }else{ res_comp.appendNew(last_hdle,comp[i],comp[i]); } } if ( fat_output.get_value() ){ res_comp = res_comp_bot; res_comp.setStitching(true); res_comp.append(res_comp_top.reversed()); } result.concat(res_comp.toPwSb()); } } return result; } void LPERoughHatches::doBeforeEffect (SPLPEItem const*/*lpeitem*/) { using namespace Geom; top_edge_variation.resetRandomizer(); bot_edge_variation.resetRandomizer(); top_tgt_variation.resetRandomizer(); bot_tgt_variation.resetRandomizer(); top_smth_variation.resetRandomizer(); bot_smth_variation.resetRandomizer(); dist_rdm.resetRandomizer(); //original_bbox(lpeitem); } void LPERoughHatches::resetDefaults(SPItem const* item) { Effect::resetDefaults(item); Geom::OptRect bbox = item->geometricBounds(); Geom::Point origin(0.,0.); Geom::Point vector(50.,0.); if (bbox) { origin = bbox->midpoint(); vector = Geom::Point((*bbox)[X].extent()/4, 0.); top_edge_variation.param_set_value( (*bbox)[Y].extent()/10, 0 ); bot_edge_variation.param_set_value( (*bbox)[Y].extent()/10, 0 ); top_edge_variation.write_to_SVG(); bot_edge_variation.write_to_SVG(); } //direction.set_and_write_new_values(origin, vector); //bender.param_set_and_write_new_value( origin + Geom::Point(5,0) ); direction.set_and_write_new_values(origin + Geom::Point(0,-5), vector); bender.set_and_write_new_values( origin, Geom::Point(5,0) ); hatch_dist = Geom::L2(vector)/2; } } //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 :