Blob Blame Raw
/* === S Y N F I G ========================================================= */
/*!	\file state_width.cpp
**	\brief Template File
**
**	$Id$
**
**	\legal
**	Copyright (c) 2002-2005 Robert B. Quattlebaum Jr., Adrian Bentley
**  Copyright (c) 2008 Chris Moore
**  Copyright (c) 2011 Carlos López
**
**	This package is free software; you can redistribute it and/or
**	modify it under the terms of the GNU General Public License as
**	published by the Free Software Foundation; either version 2 of
**	the License, or (at your option) any later version.
**
**	This package is distributed in the hope that it will be useful,
**	but WITHOUT ANY WARRANTY; without even the implied warranty of
**	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
**	General Public License for more details.
**	\endlegal
*/
/* ========================================================================= */

/* === H E A D E R S ======================================================= */

#ifdef USING_PCH
#	include "pch.h"
#else
#ifdef HAVE_CONFIG_H
#	include <config.h>
#endif

#include <synfig/general.h>

#include <gtkmm/dialog.h>
#include <gtkmm/entry.h>

#include <ETL/bezier>

#include <synfig/valuenodes/valuenode_dynamiclist.h>
#include <synfig/valuenodes/valuenode_wplist.h>
#include <synfig/valuenodes/valuenode_composite.h>
#include <synfig/blinepoint.h>
#include <synfigapp/action_system.h>

#include "state_width.h"
#include "state_normal.h"
#include "canvasview.h"
#include "workarea.h"
#include "app.h"

#include <synfigapp/action.h>
#include "event_mouse.h"
#include "event_layerclick.h"
#include "docks/dock_toolbox.h"
#include "docks/dialog_tooloptions.h"
#include "widgets/widget_distance.h"
#include "duck.h"

//#include <synfigapp/value_desc.h>
#include <synfigapp/main.h>

#include <ETL/clock>

#include <gui/localization.h>

#endif

/* === U S I N G =========================================================== */

using namespace std;
using namespace etl;
using namespace synfig;
using namespace synfigapp;
using namespace studio;

/* === M A C R O S ========================================================= */

/* === G L O B A L S ======================================================= */

StateWidth studio::state_width;

/* === C L A S S E S & S T R U C T S ======================================= */

class studio::StateWidth_Context : public sigc::trackable
{
	etl::handle<CanvasView> canvas_view_;
	CanvasView::IsWorking is_working;

	//Point mouse_pos;
	handle<Duck> center;
	handle<Duck> radius;
	handle<Duck> closestpoint;

	map<handle<Duck>,Real>	changetable;

	etl::clock	clocktime;
	// Real		lastt; // unused

	bool added;

	void refresh_ducks();

	WorkArea::PushState push_state;

	//Toolbox settings
	synfigapp::Settings& settings;

	//Toolbox display
	Gtk::Table options_table;

	Glib::RefPtr<Gtk::Adjustment> adj_delta;
	Gtk::SpinButton	spin_delta;

	Widget_Distance *influence_radius;

	Gtk::CheckButton check_relative;

	void AdjustWidth(handle<Duckmatic::Bezier> c, float t, Real mult, bool invert);

public:

	Real get_delta()const { return adj_delta->get_value(); }
	void set_delta(Real f) { adj_delta->set_value(f); }

	Real get_radius()const { return influence_radius->get_value().get(Distance::SYSTEM_UNITS,get_canvas_view()->get_canvas()->rend_desc());}
	void set_radius(Distance f) { influence_radius->set_value(f); }

	bool get_relative() const { return check_relative.get_active(); }
	void set_relative(bool r) { check_relative.set_active(r); }

	void refresh_tool_options(); //to refresh the toolbox

	//events
	Smach::event_result event_stop_handler(const Smach::event& x);
	Smach::event_result event_refresh_handler(const Smach::event& x);
	Smach::event_result event_mouse_handler(const Smach::event& x);
	Smach::event_result event_refresh_tool_options(const Smach::event& x);

	//constructor destructor
	StateWidth_Context(CanvasView* canvas_view);
	~StateWidth_Context();

	//Canvas interaction
	const etl::handle<CanvasView>& get_canvas_view()const{return canvas_view_;}
	etl::handle<synfigapp::CanvasInterface> get_canvas_interface()const{return canvas_view_->canvas_interface();}
	synfig::Canvas::Handle get_canvas()const{return canvas_view_->get_canvas();}
	WorkArea * get_work_area()const{return canvas_view_->get_work_area();}

	//Modifying settings etc.
	void load_settings();
	void save_settings();
	void reset();

};	// END of class StateWidth_Context

/* === M E T H O D S ======================================================= */

StateWidth::StateWidth():
	Smach::state<StateWidth_Context>("width")
{
	insert(event_def(EVENT_STOP,&StateWidth_Context::event_stop_handler));
	insert(event_def(EVENT_REFRESH,&StateWidth_Context::event_refresh_handler));
	insert(event_def(EVENT_WORKAREA_MOUSE_BUTTON_DOWN,&StateWidth_Context::event_mouse_handler));
	insert(event_def(EVENT_WORKAREA_MOUSE_BUTTON_DRAG,&StateWidth_Context::event_mouse_handler));
	insert(event_def(EVENT_WORKAREA_MOUSE_BUTTON_UP,&StateWidth_Context::event_mouse_handler));
	insert(event_def(EVENT_REFRESH_TOOL_OPTIONS,&StateWidth_Context::event_refresh_tool_options));
}

StateWidth::~StateWidth()
{
}

void
StateWidth_Context::load_settings()
{
	try
	{
		synfig::ChangeLocale change_locale(LC_NUMERIC, "C");
		String value;

		//parse the arguments yargh!
		if(settings.get_value("width.delta",value))
			set_delta(atof(value.c_str()));
		else
			set_delta(6);

		if(settings.get_value("width.radius",value))
			set_radius(Distance(atof(value.c_str()), App::distance_system));
		else
			set_radius(Distance(60, App::distance_system));

		//defaults to false
		if(settings.get_value("width.relative",value) && value == "1")
			set_relative(true);
		else
			set_relative(false);
	}
	catch(...)
	{
		synfig::warning("State Width: Caught exception when attempting to load settings.");
	}
}

void
StateWidth_Context::save_settings()
{
	try
	{
		synfig::ChangeLocale change_locale(LC_NUMERIC, "C");
		settings.set_value("width.delta",strprintf("%f",get_delta()));
		settings.set_value("width.radius",influence_radius->get_value().get_string());
		settings.set_value("width.relative",get_relative()?"1":"0");
	}
	catch(...)
	{
		synfig::warning("State Width: Caught exception when attempting to save settings.");
	}
}

void
StateWidth_Context::reset()
{
	refresh_ducks();
}

StateWidth_Context::StateWidth_Context(CanvasView* canvas_view):
	canvas_view_(canvas_view),
	is_working(*canvas_view),

	push_state(*get_work_area()),

	settings(synfigapp::Main::get_selected_input_device()->settings()),

	adj_delta(Gtk::Adjustment::create(6,0,20,0.01,0.1)),
	spin_delta(adj_delta,0.01,3),

	check_relative(_("Relative Growth"))
{
	influence_radius=manage(new Widget_Distance());
	influence_radius->show();
	influence_radius->set_digits(0);
	influence_radius->set_range(0,10000000);
	influence_radius->set_size_request(24,-1);

	load_settings();

	// Set up the tool options dialog
	options_table.attach(*manage(new Gtk::Label(_("Width Tool"))),	0, 2, 0, 1, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);

	//expand stuff
	options_table.attach(*manage(new Gtk::Label(_("Growth:"))),		0, 1, 1, 2, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);
	options_table.attach(spin_delta,								1, 2, 1, 2, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);
	options_table.attach(*manage(new Gtk::Label(_("Radius:"))),		0, 1, 2, 3, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);
	options_table.attach(*influence_radius,								1, 2, 2, 3, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);
	options_table.attach(check_relative,							0, 2, 3, 4, Gtk::EXPAND|Gtk::FILL, Gtk::EXPAND|Gtk::FILL, 0, 0);

	options_table.show_all();
	refresh_tool_options();
	App::dialog_tool_options->present();
	// Turn off layer clicking
	get_work_area()->set_allow_layer_clicks(false);
	// Refresh the work area
	get_work_area()->queue_draw();
	//Create the new ducks
	added = false;
	if(!center)
	{
		center = new Duck();
		center->set_name("p1");
		center->set_type(Duck::TYPE_POSITION);
	}
	if(!radius)
	{
		radius = new Duck();
		radius->set_origin(center);
		radius->set_radius(true);
		radius->set_type(Duck::TYPE_RADIUS);
		radius->set_name("radius");
		radius->set_point(Point(1.0,0.0));
	}
	if(!closestpoint)
	{
		closestpoint = new Duck();
		closestpoint->set_name("closest");
		closestpoint->set_type(Duck::TYPE_POSITION);
	}
	//Disable duck clicking for the maximum coolness :)
	get_work_area()->set_allow_duck_clicks(false);
	// Hide all tangent, vertex and angle ducks and show the width and
	// radius ducks
	get_work_area()->set_type_mask((get_work_area()->get_type_mask()-Duck::TYPE_TANGENT-Duck::TYPE_VERTEX-Duck::TYPE_ANGLE) | Duck::TYPE_WIDTH | Duck::TYPE_RADIUS);
	get_canvas_view()->toggle_duck_mask(Duck::TYPE_NONE);

	// Turn the mouse pointer to crosshairs
	get_work_area()->set_cursor(Gdk::CROSSHAIR);

	App::dock_toolbox->refresh();
}

void
StateWidth_Context::refresh_tool_options()
{
	App::dialog_tool_options->clear();
	App::dialog_tool_options->set_widget(options_table);
	App::dialog_tool_options->set_local_name(_("Width Tool"));
	App::dialog_tool_options->set_name("width");
}

Smach::event_result
StateWidth_Context::event_refresh_tool_options(const Smach::event& /*x*/)
{
	refresh_tool_options();
	return Smach::RESULT_ACCEPT;
}

StateWidth_Context::~StateWidth_Context()
{
	save_settings();
	//remove ducks if need be
	if(added)
	{
		get_work_area()->erase_duck(center);
		get_work_area()->erase_duck(radius);
		get_work_area()->erase_duck(closestpoint);
		added = false;
	}

	// Tool options be rid of ye!!
	App::dialog_tool_options->clear();
	// Refresh the work area
	get_work_area()->queue_draw();

	App::dock_toolbox->refresh();
}

Smach::event_result
StateWidth_Context::event_stop_handler(const Smach::event& /*x*/)
{
	throw &state_normal;
	return Smach::RESULT_OK;
}

Smach::event_result
StateWidth_Context::event_refresh_handler(const Smach::event& /*x*/)
{
	refresh_ducks();
	return Smach::RESULT_ACCEPT;
}

void
StateWidth_Context::AdjustWidth(handle<Duckmatic::Bezier> c, float t, Real mult, bool invert)
{
	//Leave the function if there is no curve
	if(!c)return;
	Real amount1=0,amount2=0;
	//decide how much to change each width
	/*
	t \in [0,1]
	both pressure and multiply amount are in mult
		(may want to change this to allow different types of falloff)
	rsq is the squared distance from the point on the curve (also part of the falloff)
	*/
	//may want to provide a different falloff function...
	if(t <= 0.2)
		amount1 = mult;
	else if(t >= 0.8)
		amount2 = mult;
	else
	{
		float u = (t-0.2)/0.6;
		amount1 = (1-u)*mult;
		amount2 = u*mult;
	}
	// change sign if we are decreasing widths
	if(invert)
	{
		amount1 *= -1;
		amount2 *= -1;
	}
	// ducks for the bezier vertexes
	handle<Duck>	p1 = c->p1;
	handle<Duck>	p2 = c->p2;
	// ducks for the widths of the bezier vertexes
	handle<Duck>	w1,w2;
	//find w1,w2
	{
		const DuckList dl = get_work_area()->get_duck_list();
		DuckList::const_iterator i = dl.begin();
		for(;i != dl.end(); ++i)
		{
			if((*i)->get_type() == Duck::TYPE_WIDTH)
			{
				if((*i)->get_origin_duck() == p1)
				{
					w1 = *i;
				}
				if((*i)->get_origin_duck() == p2)
				{
					w2 = *i;
				}
			}
		}
	}
	// change the widths of the affected BLine of Outlines
	if(amount1 != 0 && w1)
	{
		Real width = w1->get_point().mag();
		width += amount1;
		w1->set_point(Vector(width,0));
		//log in the list of changes...
		//to truly be changed after everything is said and done
		changetable[w1] = width;
	}
	if(amount2 != 0 && w2)
	{
		Real width = w2->get_point().mag();
		width += amount2;
		w2->set_point(Vector(width,0));
		//log in the list of changes...
		//to truly be changed after everything is said and done
		changetable[w2] = width;
	}
	///////
	// Change the widths of the affected BLine of Advance Outlines
	// Parents value nodes of the value node of the p1 and p2 ducks.
	synfig::ValueNode::Handle p1pvn(p1->get_value_desc().get_parent_value_node());
	synfig::ValueNode::Handle p2pvn(p2->get_value_desc().get_parent_value_node());
	// if the bezier position ducks are linkable valuenode children
	if(p1pvn && p2pvn && p1pvn==p2pvn)
	{
		// we guess that the parent value node is a bline value node
		synfig::ValueNode::Handle bezier_bline=p1pvn;
		// Positions of the points on the bline
		Real p1_pos, bezier_size;
		// index of the first point on the bezier
		int p1_i;
		// retrieve the number of blinepoints on the bline and the loop of the bline
		int bline_size((*(bezier_bline))(get_canvas()->get_time()).get_list().size());
		bool loop((*(bezier_bline))(get_canvas()->get_time()).get_loop());
		p1_i = p1->get_value_desc().get_index();
		// bezier size depends on loop status
		bezier_size = 1.0/(loop?bline_size:(bline_size-1));
		if(loop)
		{
			// if looped and the we are in the first bezier
			if(p1_i == (bline_size -1))
				p1_i = 0;
			else
				p1_i++;
		}
		// the position is based on the index and the bezier size
		p1_pos = Real(p1_i)*bezier_size;
		// find all the widthpoints
		const DuckList dl = get_work_area()->get_duck_list();
		DuckList::const_iterator i = dl.begin();
		for(;i != dl.end(); ++i)
		{
			handle<Duck> iduck(*i);
			handle<Duck> iduck_origin(iduck->get_origin_duck());
			// If we find a width duck
			if(iduck->get_type() == Duck::TYPE_WIDTH && iduck_origin)
			{
				// if it has an origin duck
				synfigapp::ValueDesc origin_value_desc(iduck_origin->get_value_desc());
				ValueNode::Handle wpvn(ValueNode::Handle::cast_dynamic(origin_value_desc.get_value_node()));
				// if the origin duck is widthpoint type and it belongs to a list
				if(wpvn && wpvn->get_type() == type_width_point && origin_value_desc.parent_is_linkable_value_node())
				{
					// and if the width point list that it belongs to...
					ValueNode_WPList::Handle wplist(ValueNode_WPList::Handle::cast_dynamic(origin_value_desc.get_parent_value_node()));
					if(wplist)
					{
						// ... has a bline valid and is the same as the bline
						// we found for the bezier previously caught...
						ValueNode::Handle bline(wplist->get_bline());
						if(bline && (bline==bezier_bline))
						{
							// ... then update the values properly
							synfig::WidthPoint wpoint((*wpvn)(get_canvas()->get_time()).get(synfig::WidthPoint()));
							Real pos(wpoint.get_norm_position(wplist->get_loop()));
							Real tpos(p1_pos+t*bezier_size);
							// The factor of 20 can be modified by the user as a preference.
							// The higher value the more local effect has the
							// Width Tool around the widths points.
							Real amount(mult*exp(-20.0*(fabs(pos-tpos)+0.000001)));
							amount*=invert?-1.0:1.0;
							Real width = iduck->get_point().mag();
							width += amount;
							iduck->set_point(Vector(width,0));
							changetable[iduck] = width;
						}
					}
				}
			}
		}
	}
}

Smach::event_result
StateWidth_Context::event_mouse_handler(const Smach::event& x)
{
	const EventMouse& event(*reinterpret_cast<const EventMouse*>(&x));

	//handle the click
	if( (event.key == EVENT_WORKAREA_MOUSE_BUTTON_DOWN || event.key == EVENT_WORKAREA_MOUSE_BUTTON_DRAG)
			&& event.button == BUTTON_LEFT )
	{
		const Real pw = get_work_area()->get_pw();
		const Real ph = get_work_area()->get_ph();
		const Real scale = sqrt(pw*pw+ph*ph);
		const Real rad = get_relative() ? scale * get_radius() : get_radius();

		bool invert = (event.modifier&Gdk::CONTROL_MASK);

		const Real threshold = 0.08;

		float t = 0;
		Real rsq = 0;

		Real dtime = 1/60.0;

		//if we're dragging get the difference in time between now and then
		if(event.key == EVENT_WORKAREA_MOUSE_BUTTON_DRAG)
		{
			dtime = min((float)(1/15.0),(float)(clocktime()));
		}
		clocktime.reset();

		center->set_point(event.pos);
		if(!added)get_work_area()->add_duck(center);

		radius->set_scalar(rad);
		if(!added)get_work_area()->add_duck(radius);
		//the other duck is at the current duck
		closestpoint->set_point(event.pos);
		if(!added)get_work_area()->add_duck(closestpoint);
		//get the closest curve...
		handle<Duckmatic::Bezier>	c;
		if(event.pressure >= threshold)
			c = get_work_area()->find_bezier(event.pos,scale*8,rad,&t);
		//run algorithm on event.pos to get 2nd placement
		if(!c.empty())
		{
			bezier<Point> curve;
			Point p;

			curve[0] = c->p1->get_trans_point();
			curve[1] = c->c1->get_trans_point();
			curve[2] = c->c2->get_trans_point();
			curve[3] = c->p2->get_trans_point();
			curve.sync();
			p = curve(t);
			rsq = (p-event.pos).mag_squared();
			const Real r = rad*rad;
			if(rsq < r)
			{
				closestpoint->set_point(p);
				//adjust the width...
				//squared falloff for radius... [0,1]
				Real ri = (r - rsq)/r;
				AdjustWidth(c,t,ri*event.pressure*get_delta()*dtime,invert);
			}
		}
		//the points have been added
		added = true;
		//draw where it is yo!
		get_work_area()->queue_draw();
		return Smach::RESULT_ACCEPT;
	}

	if(event.key == EVENT_WORKAREA_MOUSE_BUTTON_UP && event.button == BUTTON_LEFT)
	{
		if(added)
		{
			get_work_area()->erase_duck(center);
			get_work_area()->erase_duck(radius);
			get_work_area()->erase_duck(closestpoint);
			added = false;
		}
		//Affect the width changes here...
		map<handle<Duck>,Real>::iterator i = changetable.begin();
		synfigapp::Action::PassiveGrouper group(get_canvas_interface()->get_instance().get(),_("Sketch Width"));
		for(; i != changetable.end(); ++i)
		{
			//for each duck modify IT!!!
			ValueDesc desc = i->first->get_value_desc();
			if (desc.get_value_type() == type_real) {
				Action::Handle action(Action::create("ValueDescSet"));
				assert(action);

				action->set_param("canvas",get_canvas());
				action->set_param("canvas_interface",get_canvas_interface());

				action->set_param("value_desc",desc);
				action->set_param("new_value",ValueBase(i->second));
				action->set_param("time",get_canvas_view()->get_time());

				if(!action->is_ready() || !get_canvas_view()->get_instance()->perform_action(action))
				{
					group.cancel();
					synfig::warning("Changing the width action has failed");
					return Smach::RESULT_ERROR;
				}
			}
			else
			if (desc.get_value_type() == type_bline_point
			 && desc.parent_is_value_desc()
			 && desc.get_sub_name() == "width")
			{
				BLinePoint p;
				p.set_width(i->second);

				Action::Handle action(Action::create("ValueDescSet"));
				assert(action);

				action->set_param("canvas",get_canvas());
				action->set_param("canvas_interface",get_canvas_interface());

				action->set_param("value_desc",desc);
				action->set_param("new_value",ValueBase(p));
				action->set_param("time",get_canvas_view()->get_time());

				if(!action->is_ready() || !get_canvas_view()->get_instance()->perform_action(action))
				{
					group.cancel();
					synfig::warning("Changing the width action has failed");
					return Smach::RESULT_ERROR;
				}
			}
		}

		changetable.clear();
		get_work_area()->queue_draw();
		return Smach::RESULT_ACCEPT;
	}

	return Smach::RESULT_OK;
}


void
StateWidth_Context::refresh_ducks()
{
	get_work_area()->clear_ducks();
	get_work_area()->queue_draw();
}