Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
transformation.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
6/* Authors:
7 * Bryce W. Harrington <bryce@bryceharrington.org>
8 * buliabyak@gmail.com
9 * Abhishek Sharma
10 *
11 * Copyright (C) 2004, 2005 Authors
12 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
13 */
14
15#include "transformation.h"
16
17#include <sigc++/functors/mem_fun.h>
18#include <gtkmm/box.h>
19#include <gtkmm/button.h>
20#include <gtkmm/entry.h>
21#include <gtkmm/grid.h>
22#include <gtkmm/label.h>
23#include <gtkmm/version.h>
24
25#include <2geom/transforms.h>
26
27#include "desktop.h"
28#include "document-undo.h"
29#include "message-stack.h"
30#include "preferences.h"
31#include "selection.h"
32
35#include "object/sp-namedview.h"
36#include "ui/icon-loader.h"
37#include "ui/icon-names.h"
38#include "ui/pack.h"
40
41namespace Inkscape::UI::Dialog {
42
43/*########################################################################
44# C O N S T R U C T O R
45########################################################################*/
46
48 : DialogBase("/dialogs/transformation", "Transform"),
49
50 _page_move (4, 2),
51 _page_scale (4, 2),
52 _page_rotate (4, 2),
53 _page_skew (4, 2),
54 _page_transform (3, 3),
55
56 _scalar_move_horizontal (_("_Horizontal:"), _("Horizontal displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR,
57 "transform-move-horizontal", &_units_move),
58 _scalar_move_vertical (_("_Vertical:"), _("Vertical displacement (relative) or position (absolute)"), UNIT_TYPE_LINEAR,
59 "transform-move-vertical", &_units_move),
60 _scalar_scale_horizontal(_("_Width:"), _("Horizontal size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS,
61 "transform-scale-horizontal", &_units_scale),
62 _scalar_scale_vertical (_("_Height:"), _("Vertical size (absolute or percentage of current)"), UNIT_TYPE_DIMENSIONLESS,
63 "transform-scale-vertical", &_units_scale),
64 _scalar_rotate (_("A_ngle:"), _("Rotation angle (positive = counterclockwise)"), UNIT_TYPE_RADIAL,
65 "transform-rotate", &_units_rotate),
66 _scalar_skew_horizontal (_("_Horizontal:"), _("Horizontal skew angle (positive = counterclockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR,
67 "transform-skew-horizontal", &_units_skew),
68 _scalar_skew_vertical (_("_Vertical:"), _("Vertical skew angle (positive = clockwise), or absolute displacement, or percentage displacement"), UNIT_TYPE_LINEAR,
69 "transform-skew-vertical", &_units_skew),
70
71 _scalar_transform_a ({}, _("Transformation matrix element A")),
72 _scalar_transform_b ({}, _("Transformation matrix element B")),
73 _scalar_transform_c ({}, _("Transformation matrix element C")),
74 _scalar_transform_d ({}, _("Transformation matrix element D")),
75 _scalar_transform_e ({}, _("Transformation matrix element E"),
76 UNIT_TYPE_LINEAR, {}, &_units_transform),
77 _scalar_transform_f ({}, _("Transformation matrix element F"),
78 UNIT_TYPE_LINEAR, {}, &_units_transform),
79
80 _check_move_relative (_("Rela_tive move")),
81 _check_scale_proportional(_("_Scale proportionally")),
82 _check_apply_separately (_("Apply to each _object separately")),
83 _check_replace_matrix (_("Edit c_urrent matrix")),
84
85 resetButton{Gtk::make_managed<Gtk::Button>()},
86 applyButton{Gtk::make_managed<Gtk::Button>(_("_Apply"))}
87{
88 _scalar_move_horizontal.getLabel()->set_hexpand();
89 _scalar_move_vertical.getLabel()->set_hexpand();
90 _scalar_scale_horizontal.getLabel()->set_hexpand();
91 _scalar_scale_vertical.getLabel()->set_hexpand();
92 _scalar_skew_horizontal.getLabel()->set_hexpand();
93 _scalar_skew_vertical.getLabel()->set_hexpand();
94
95 _check_move_relative.set_use_underline();
96 _check_move_relative.set_tooltip_text(_("Add the specified relative displacement to the current position; otherwise, edit the current absolute position directly"));
97
98 _check_scale_proportional.set_use_underline();
99 _check_scale_proportional.set_tooltip_text(_("Preserve the width/height ratio of the scaled objects"));
100
101 _check_apply_separately.set_use_underline();
102 _check_apply_separately.set_tooltip_text(_("Apply the scale/rotate/skew to each selected object separately; otherwise, transform the selection as a whole"));
103
104 _check_replace_matrix.set_use_underline();
105 _check_replace_matrix.set_tooltip_text(_("Edit the current transform= matrix; otherwise, post-multiply transform= by this matrix"));
106
107 // Notebook for individual transformations
108 UI::pack_start(*this, _notebook, false, false);
109
110 _page_move.set_halign(Gtk::Align::START);
111 _notebook.append_page(_page_move, _("_Move"), true);
112 layoutPageMove();
113
114 _page_scale.set_halign(Gtk::Align::START);
115 _notebook.append_page(_page_scale, _("_Scale"), true);
116 layoutPageScale();
117
118 _page_rotate.set_halign(Gtk::Align::START);
119 _notebook.append_page(_page_rotate, _("_Rotate"), true);
120 layoutPageRotate();
121
122 _page_skew.set_halign(Gtk::Align::START);
123 _notebook.append_page(_page_skew, _("Ske_w"), true);
124 layoutPageSkew();
125
126 _page_transform.set_halign(Gtk::Align::START);
127 _notebook.append_page(_page_transform, _("Matri_x"), true);
128 layoutPageTransform();
129
130 _tabSwitchConn = _notebook.signal_switch_page().connect(sigc::mem_fun(*this, &Transformation::onSwitchPage));
131
132 // Apply separately
133 UI::pack_start(*this, _check_apply_separately, false, false);
135 _check_apply_separately.set_active(prefs->getBool("/dialogs/transformation/applyseparately"));
136 _check_apply_separately.signal_toggled().connect(sigc::mem_fun(*this, &Transformation::onApplySeparatelyToggled));
137
138#if GTKMM_CHECK_VERSION(4, 14, 0)
139 // make sure all spinbuttons activate Apply on pressing Enter
140 auto const apply_on_activate = [this](UI::Widget::ScalarUnit &scalar) {
141 scalar.getSpinButton().signal_activate().connect([this] { _apply(); });
142 };
143 apply_on_activate(_scalar_move_horizontal );
144 apply_on_activate(_scalar_move_vertical );
145 apply_on_activate(_scalar_scale_horizontal);
146 apply_on_activate(_scalar_scale_vertical );
147 apply_on_activate(_scalar_rotate );
148 apply_on_activate(_scalar_skew_horizontal );
149 apply_on_activate(_scalar_skew_vertical );
150#endif
151
152 resetButton->set_image_from_icon_name("reset-settings-symbolic");
153 resetButton->set_size_request(30, -1);
154 resetButton->set_halign(Gtk::Align::CENTER);
155 resetButton->set_use_underline();
156 resetButton->set_tooltip_text(_("Reset the values on the current tab to defaults"));
157 resetButton->set_sensitive(true);
158 resetButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onClear));
159
160 applyButton->set_use_underline();
161 applyButton->set_halign(Gtk::Align::CENTER);
162 applyButton->set_tooltip_text(_("Apply transformation to selection"));
163 applyButton->set_sensitive(false);
164 applyButton->signal_clicked().connect(sigc::mem_fun(*this, &Transformation::_apply));
165 applyButton->add_css_class("wide-apply-button");
166
167 auto const button_box = Gtk::make_managed<Gtk::Box>();
168 button_box->set_margin_top(4);
169 button_box->set_spacing(8);
170 button_box->set_halign(Gtk::Align::CENTER);
171 UI::pack_start(*button_box, *applyButton);
172 UI::pack_start(*button_box, *resetButton);
173 UI::pack_start(*this, *button_box, UI::PackOptions::shrink);
174}
175
184
185/*########################################################################
186# U T I L I T Y
187########################################################################*/
188
190{
191 _notebook.set_current_page(page);
192 set_visible(true);
193}
194
195/*########################################################################
196# S E T U P L A Y O U T
197########################################################################*/
198
200{
201 _units_move.setUnitType(UNIT_TYPE_LINEAR);
202
206 _scalar_move_horizontal.set_hexpand();
208
212 _scalar_move_vertical.set_hexpand();
214
215 //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_HOR );
216
217 _page_move.table().attach(_scalar_move_horizontal, 0, 0, 2, 1);
218 _page_move.table().attach(_units_move, 2, 0, 1, 1);
219
221 .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged));
222
223 //_scalar_move_vertical.set_label_image( INKSCAPE_STOCK_ARROWS_VER );
224 _page_move.table().attach(_scalar_move_vertical, 0, 1, 2, 1);
225
227 .connect(sigc::mem_fun(*this, &Transformation::onMoveValueChanged));
228
229 // Relative moves
230 _page_move.table().attach(_check_move_relative, 0, 2, 2, 1);
231
232 _check_move_relative.set_active(true);
233 _check_move_relative.signal_toggled()
234 .connect(sigc::mem_fun(*this, &Transformation::onMoveRelativeToggled));
235}
236
238{
239 _units_scale.setUnitType(UNIT_TYPE_DIMENSIONLESS);
240 _units_scale.setUnitType(UNIT_TYPE_LINEAR);
241
248 _scalar_scale_horizontal.set_hexpand();
250
257 _scalar_scale_vertical.set_hexpand();
259
260 _page_scale.table().attach(_scalar_scale_horizontal, 0, 0, 2, 1);
261
263 .connect(sigc::mem_fun(*this, &Transformation::onScaleXValueChanged));
264
265 _page_scale.table().attach(_units_scale, 2, 0, 1, 1);
266 _page_scale.table().attach(_scalar_scale_vertical, 0, 1, 2, 1);
267
269 .connect(sigc::mem_fun(*this, &Transformation::onScaleYValueChanged));
270
271 _page_scale.table().attach(_check_scale_proportional, 0, 2, 2, 1);
272
273 _check_scale_proportional.set_active(false);
274 _check_scale_proportional.signal_toggled()
275 .connect(sigc::mem_fun(*this, &Transformation::onScaleProportionalToggled));
276
277 //TODO: add a widget for selecting the fixed point in scaling, or honour rotation center?
278}
279
281{
282 _units_rotate.setUnitType(UNIT_TYPE_RADIAL);
283
284 _scalar_rotate.initScalar(-360.0, 360.0);
287 _scalar_rotate.set_hexpand();
288
289 _counterclockwise_rotate.set_icon_name("object-rotate-left");
290 _counterclockwise_rotate.set_has_frame(false);
291 _counterclockwise_rotate.set_tooltip_text(_("Rotate in a counterclockwise direction"));
292
293 _clockwise_rotate.set_icon_name("object-rotate-right");
294 _clockwise_rotate.set_has_frame(false);
295 _clockwise_rotate.set_tooltip_text(_("Rotate in a clockwise direction"));
297
298 auto const box = Gtk::make_managed<Gtk::Box>();
299 _counterclockwise_rotate.set_halign(Gtk::Align::START);
300 _clockwise_rotate.set_halign(Gtk::Align::START);
303
304 _page_rotate.table().attach(_scalar_rotate, 0, 0, 1, 1);
305 _page_rotate.table().attach(_units_rotate, 1, 0, 1, 1);
306 _page_rotate.table().attach(*box, 1, 1, 1, 1);
307
309 .connect(sigc::mem_fun(*this, &Transformation::onRotateValueChanged));
310
311 _counterclockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateCounterclockwiseClicked));
312 _clockwise_rotate.signal_clicked().connect(sigc::mem_fun(*this, &Transformation::onRotateClockwiseClicked));
313
314 //TODO: honour rotation center?
315}
316
318{
319 _units_skew.setUnitType(UNIT_TYPE_LINEAR);
320 _units_skew.setUnitType(UNIT_TYPE_DIMENSIONLESS);
321 _units_skew.setUnitType(UNIT_TYPE_RADIAL);
322
326 _scalar_skew_horizontal.set_hexpand();
328
332 _scalar_skew_vertical.set_hexpand();
334
335 _page_skew.table().attach(_scalar_skew_horizontal, 0, 0, 2, 1);
336 _page_skew.table().attach(_units_skew, 2, 0, 1, 1);
337 _page_skew.table().attach(_scalar_skew_vertical, 0, 1, 2, 1);
338
340 .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged));
342 .connect(sigc::mem_fun(*this, &Transformation::onSkewValueChanged));
343
344 //TODO: honour rotation center?
345}
346
348{
349 _units_transform.setUnitType(UNIT_TYPE_LINEAR);
350 _units_transform.set_tooltip_text(_("E and F units"));
351 _units_transform.set_halign(Gtk::Align::END);
352 _units_transform.set_margin_top(3);
353 _units_transform.set_margin_bottom(3);
354
356 for (auto label : labels) {
357 label->hide_label();
358 label->set_margin_start(2);
359 label->set_margin_end(2);
360 }
361 _page_transform.table().set_column_spacing(0);
362 _page_transform.table().set_row_spacing(1);
363 _page_transform.table().set_column_homogeneous(true);
364
365 _scalar_transform_a.getWidget()->set_size_request(65, -1);
366 _scalar_transform_a.setRange(-1e10, 1e10);
371 _scalar_transform_a.set_hexpand();
372
373 _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("A:"), 0, 0, 1, 1);
374 _page_transform.table().attach(_scalar_transform_a, 0, 1, 1, 1);
375
377 .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
378
379 _scalar_transform_b.getWidget()->set_size_request(65, -1);
380 _scalar_transform_b.setRange(-1e10, 1e10);
385 _scalar_transform_b.set_hexpand();
386
387 _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("B:"), 0, 2, 1, 1);
388 _page_transform.table().attach(_scalar_transform_b, 0, 3, 1, 1);
389
391 .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
392
393 _scalar_transform_c.getWidget()->set_size_request(65, -1);
394 _scalar_transform_c.setRange(-1e10, 1e10);
399 _scalar_transform_c.set_hexpand();
400
401 _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("C:"), 1, 0, 1, 1);
402 _page_transform.table().attach(_scalar_transform_c, 1, 1, 1, 1);
403
405 .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
406
407 _scalar_transform_d.getWidget()->set_size_request(65, -1);
408 _scalar_transform_d.setRange(-1e10, 1e10);
413 _scalar_transform_d.set_hexpand();
414
415 _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("D:"), 1, 2, 1, 1);
416 _page_transform.table().attach(_scalar_transform_d, 1, 3, 1, 1);
417
419 .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
420
421 _scalar_transform_e.getWidget()->set_size_request(65, -1);
422 _scalar_transform_e.setRange(-1e10, 1e10);
427 _scalar_transform_e.set_hexpand();
428
429 _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("E:"), 2, 0, 1, 1);
430 _page_transform.table().attach(_scalar_transform_e, 2, 1, 1, 1);
431
433 .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
434
435 _scalar_transform_f.getWidget()->set_size_request(65, -1);
436 _scalar_transform_f.setRange(-1e10, 1e10);
441 _scalar_transform_f.set_hexpand();
442
443 _page_transform.table().attach(*Gtk::make_managed<Gtk::Label>("F:"), 2, 2, 1, 1);
444 _page_transform.table().attach(_scalar_transform_f, 2, 3, 1, 1);
445
446 auto const img = Gtk::make_managed<Gtk::Image>();
447 img->set_from_icon_name("matrix-2d");
448 img->set_pixel_size(52);
449 img->set_margin_top(4);
450 img->set_margin_bottom(4);
451 _page_transform.table().attach(*img, 0, 5, 1, 1);
452
453 auto const descr = Gtk::make_managed<Gtk::Label>();
454 descr->set_wrap();
455 descr->set_wrap_mode(Pango::WrapMode::WORD);
456 descr->set_text(
457 _("<small>"
458 "<a href=\"https://www.w3.org/TR/SVG11/coords.html#TransformMatrixDefined\">"
459 "2D transformation matrix</a> that combines translation (E,F), scaling (A,D),"
460 " rotation (A-D) and shearing (B,C)."
461 "</small>")
462 );
463 descr->set_use_markup();
464 _page_transform.table().attach(*descr, 1, 5, 2, 1);
465
466 _page_transform.table().attach(_units_transform, 2, 4, 1, 1);
467
469 .connect(sigc::mem_fun(*this, &Transformation::onTransformValueChanged));
470
471 // Edit existing matrix
472 _page_transform.table().attach(_check_replace_matrix, 0, 4, 2, 1);
473
474 _check_replace_matrix.set_active(false);
475 _check_replace_matrix.signal_toggled()
476 .connect(sigc::mem_fun(*this, &Transformation::onReplaceMatrixToggled));
477}
478
479/*########################################################################
480# U P D A T E
481########################################################################*/
482
484{
485 applyButton->set_sensitive(selection && !selection->isEmpty());
486
487 if (!selection || selection->isEmpty())
488 return;
489
490 switch (page) {
491 case PAGE_MOVE: {
493 break;
494 }
495 case PAGE_SCALE: {
497 break;
498 }
499 case PAGE_ROTATE: {
501 break;
502 }
503 case PAGE_SKEW: {
505 break;
506 }
507 case PAGE_TRANSFORM: {
509 break;
510 }
511 case PAGE_QTY: {
512 break;
513 }
514 }
515}
516
517void Transformation::onSwitchPage(Gtk::Widget * /*page*/, guint pagenum)
518{
519 if (!getDesktop()) {
520 return;
521 }
522
524}
525
527{
528 if (selection && !selection->isEmpty()) {
529 if (!_check_move_relative.get_active()) {
531 if (bbox) {
532 double x = bbox->min()[Geom::X];
533 double y = bbox->min()[Geom::Y];
534
535 double conversion = _units_move.getConversion("px");
536 _scalar_move_horizontal.setValue(x / conversion);
537 _scalar_move_vertical.setValue(y / conversion);
538 }
539 } else {
540 // do nothing, so you can apply the same relative move to many objects in turn
541 }
542 _page_move.set_sensitive(true);
543 } else {
544 _page_move.set_sensitive(false);
545 }
546}
547
549{
550 if (selection && !selection->isEmpty()) {
552 if (bbox) {
553 double w = bbox->dimensions()[Geom::X];
554 double h = bbox->dimensions()[Geom::Y];
557 onScaleXValueChanged(); // to update x/y proportionality if switch is on
558 _page_scale.set_sensitive(true);
559 } else {
560 _page_scale.set_sensitive(false);
561 }
562 } else {
563 _page_scale.set_sensitive(false);
564 }
565}
566
568{
569 if (selection && !selection->isEmpty()) {
570 _page_rotate.set_sensitive(true);
571 } else {
572 _page_rotate.set_sensitive(false);
573 }
574}
575
577{
578 if (selection && !selection->isEmpty()) {
580 if (bbox) {
581 double w = bbox->dimensions()[Geom::X];
582 double h = bbox->dimensions()[Geom::Y];
585 _page_skew.set_sensitive(true);
586 } else {
587 _page_skew.set_sensitive(false);
588 }
589 } else {
590 _page_skew.set_sensitive(false);
591 }
592}
593
595{
596 if (selection && !selection->isEmpty()) {
597 if (_check_replace_matrix.get_active()) {
598 Geom::Affine current (selection->items().front()->transform); // take from the first item in selection
599
600 Geom::Affine new_displayed = current;
601
602 _scalar_transform_a.setValue(new_displayed[0]);
603 _scalar_transform_b.setValue(new_displayed[1]);
604 _scalar_transform_c.setValue(new_displayed[2]);
605 _scalar_transform_d.setValue(new_displayed[3]);
606 _scalar_transform_e.setValue(new_displayed[4], "px");
607 _scalar_transform_f.setValue(new_displayed[5], "px");
608 } else {
609 // do nothing, so you can apply the same matrix to many objects in turn
610 }
611 _page_transform.set_sensitive(true);
612 } else {
613 _page_transform.set_sensitive(false);
614 }
615}
616
617/*########################################################################
618# A P P L Y
619########################################################################*/
620
622{
623 auto selection = getSelection();
624 if (!selection || selection->isEmpty())
625 return;
626
627 int const page = _notebook.get_current_page();
628
629 switch (page) {
630 case PAGE_MOVE: {
632 break;
633 }
634 case PAGE_ROTATE: {
636 break;
637 }
638 case PAGE_SCALE: {
640 break;
641 }
642 case PAGE_SKEW: {
644 break;
645 }
646 case PAGE_TRANSFORM: {
648 break;
649 }
650 }
651
652 // Let's play with never turning this off
653 applyButton->set_sensitive(false);
654}
655
657{
658 double x = _scalar_move_horizontal.getValue("px");
659 double y = _scalar_move_vertical.getValue("px");
660 if (_check_move_relative.get_active()) {
661 y *= getDesktop()->yaxisdir();
662 }
663
665 if (!prefs->getBool("/dialogs/transformation/applyseparately")) {
666 // move selection as a whole
667 if (_check_move_relative.get_active()) {
668 selection->moveRelative(x, y);
669 } else {
671 if (bbox) {
672 selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]);
673 }
674 }
675 } else {
676
677 if (_check_move_relative.get_active()) {
678 // shift each object relatively to the previous one
679 std::vector<SPItem*> selected(selection->items().begin(), selection->items().end());
680 if (selected.empty()) return;
681
682 if (fabs(x) > 1e-6) {
683 std::vector< BBoxSort > sorted;
684 for (auto item : selected)
685 {
687 if (bbox) {
688 sorted.emplace_back(item, *bbox, Geom::X, x > 0? 1. : 0., x > 0? 0. : 1.);
689 }
690 }
691 //sort bbox by anchors
692 std::stable_sort(sorted.begin(), sorted.end());
693
694 double move = x;
695 for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
696 it < sorted.end();
697 ++it )
698 {
699 it->item->move_rel(Geom::Translate(move, 0));
700 // move each next object by x relative to previous
701 move += x;
702 }
703 }
704 if (fabs(y) > 1e-6) {
705 std::vector< BBoxSort > sorted;
706 for (auto item : selected)
707 {
709 if (bbox) {
710 sorted.emplace_back(item, *bbox, Geom::Y, y > 0? 1. : 0., y > 0? 0. : 1.);
711 }
712 }
713 //sort bbox by anchors
714 std::stable_sort(sorted.begin(), sorted.end());
715
716 double move = y;
717 for ( std::vector<BBoxSort> ::iterator it (sorted.begin());
718 it < sorted.end();
719 ++it )
720 {
721 it->item->move_rel(Geom::Translate(0, move));
722 // move each next object by x relative to previous
723 move += y;
724 }
725 }
726 } else {
728 if (bbox) {
729 selection->moveRelative(x - bbox->min()[Geom::X], y - bbox->min()[Geom::Y]);
730 }
731 }
732 }
733
734 DocumentUndo::done( selection->desktop()->getDocument(), _("Move"), INKSCAPE_ICON("dialog-transform"));
735}
736
738{
739 double scaleX = _scalar_scale_horizontal.getValue("px");
740 double scaleY = _scalar_scale_vertical.getValue("px");
741
743 bool transform_stroke = prefs->getBool("/options/transform/stroke", true);
744 bool preserve = prefs->getBool("/options/preservetransform/value", false);
745 if (prefs->getBool("/dialogs/transformation/applyseparately")) {
746 auto tmp= selection->items();
747 for(auto i=tmp.begin();i!=tmp.end();++i){
748 SPItem *item = *i;
751 if (bbox_pref && bbox_geom) {
752 double new_width = scaleX;
753 double new_height = scaleY;
754 // the values are increments!
755 if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent
756 new_width = scaleX/100 * bbox_pref->width();
757 new_height = scaleY/100 * bbox_pref->height();
758 }
759 if (fabs(new_width) < 1e-6) new_width = 1e-6; // not 0, as this would result in a nasty no-bbox object
760 if (fabs(new_height) < 1e-6) new_height = 1e-6;
761
762 double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2;
763 double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2;
764 double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2;
765 double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2;
766
767 Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1);
768 item->set_i2d_affine(item->i2dt_affine() * scaler);
770 }
771 }
772 } else {
775 if (bbox_pref && bbox_geom) {
776 // the values are increments!
777 double new_width = scaleX;
778 double new_height = scaleY;
779 if (!_units_scale.isAbsolute()) { // Relative scaling, i.e in percent
780 new_width = scaleX/100 * bbox_pref->width();
781 new_height = scaleY/100 * bbox_pref->height();
782 }
783 if (fabs(new_width) < 1e-6) new_width = 1e-6;
784 if (fabs(new_height) < 1e-6) new_height = 1e-6;
785
786 double x0 = bbox_pref->midpoint()[Geom::X] - new_width/2;
787 double y0 = bbox_pref->midpoint()[Geom::Y] - new_height/2;
788 double x1 = bbox_pref->midpoint()[Geom::X] + new_width/2;
789 double y1 = bbox_pref->midpoint()[Geom::Y] + new_height/2;
790 Geom::Affine scaler = get_scale_transform_for_variable_stroke (*bbox_pref, *bbox_geom, transform_stroke, preserve, x0, y0, x1, y1);
791
792 selection->applyAffine(scaler);
793 }
794 }
795
796 DocumentUndo::done(selection->desktop()->getDocument(), _("Scale"), INKSCAPE_ICON("dialog-transform"));
797}
798
800{
801 double angle = _scalar_rotate.getValue(DEG);
802
804 if (!prefs->getBool("/dialogs/transformation/rotateCounterClockwise", TRUE)) {
805 angle *= -1;
806 }
807
808 if (prefs->getBool("/dialogs/transformation/applyseparately")) {
809 auto tmp= selection->items();
810 for(auto i=tmp.begin();i!=tmp.end();++i){
811 SPItem *item = *i;
812 item->rotate_rel(Geom::Rotate (angle*M_PI/180.0));
813 }
814 } else {
815 std::optional<Geom::Point> center = selection->center();
816 if (center) {
817 selection->rotateRelative(*center, angle);
818 }
819 }
820
821 DocumentUndo::done(selection->desktop()->getDocument(), _("Rotate"), INKSCAPE_ICON("dialog-transform"));
822}
823
825{
827 if (prefs->getBool("/dialogs/transformation/applyseparately")) {
828 auto items = selection->items();
829 for(auto i = items.begin();i!=items.end();++i){
830 SPItem *item = *i;
831
832 if (!_units_skew.isAbsolute()) { // percentage
833 double skewX = _scalar_skew_horizontal.getValue("%");
834 double skewY = _scalar_skew_vertical.getValue("%");
835 skewY *= getDesktop()->yaxisdir();
836 if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) {
837 getDesktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
838 return;
839 }
840 item->skew_rel(0.01*skewX, 0.01*skewY);
841 } else if (_units_skew.isRadial()) { //deg or rad
842 double angleX = _scalar_skew_horizontal.getValue("rad");
843 double angleY = _scalar_skew_vertical.getValue("rad");
844 if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON)
845 || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON)
846 || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON)
847 || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) {
848 getDesktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
849 return;
850 }
851 double skewX = tan(angleX);
852 double skewY = tan(angleY);
853 skewX *= getDesktop()->yaxisdir();
854 skewY *= getDesktop()->yaxisdir();
855 item->skew_rel(skewX, skewY);
856 } else { // absolute displacement
857 double skewX = _scalar_skew_horizontal.getValue("px");
858 double skewY = _scalar_skew_vertical.getValue("px");
859 skewY *= getDesktop()->yaxisdir();
861 if (bbox) {
862 double width = bbox->dimensions()[Geom::X];
863 double height = bbox->dimensions()[Geom::Y];
864 if (fabs(skewX*skewY - width*height) < Geom::EPSILON) {
865 getDesktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
866 return;
867 }
868 item->skew_rel(skewX/height, skewY/width);
869 }
870 }
871 }
872 } else { // transform whole selection
874 std::optional<Geom::Point> center = selection->center();
875
876 if ( bbox && center ) {
877 double width = bbox->dimensions()[Geom::X];
878 double height = bbox->dimensions()[Geom::Y];
879
880 if (!_units_skew.isAbsolute()) { // percentage
881 double skewX = _scalar_skew_horizontal.getValue("%");
882 double skewY = _scalar_skew_vertical.getValue("%");
883 skewY *= getDesktop()->yaxisdir();
884 if (fabs(0.01*skewX*0.01*skewY - 1.0) < Geom::EPSILON) {
885 getDesktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
886 return;
887 }
888 selection->skewRelative(*center, 0.01 * skewX, 0.01 * skewY);
889 } else if (_units_skew.isRadial()) { //deg or rad
890 double angleX = _scalar_skew_horizontal.getValue("rad");
891 double angleY = _scalar_skew_vertical.getValue("rad");
892 if ((fabs(angleX - angleY + M_PI/2) < Geom::EPSILON)
893 || (fabs(angleX - angleY - M_PI/2) < Geom::EPSILON)
894 || (fabs((angleX - angleY)/3 + M_PI/2) < Geom::EPSILON)
895 || (fabs((angleX - angleY)/3 - M_PI/2) < Geom::EPSILON)) {
896 getDesktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
897 return;
898 }
899 double skewX = tan(angleX);
900 double skewY = tan(angleY);
901 skewX *= getDesktop()->yaxisdir();
902 skewY *= getDesktop()->yaxisdir();
903 selection->skewRelative(*center, skewX, skewY);
904 } else { // absolute displacement
905 double skewX = _scalar_skew_horizontal.getValue("px");
906 double skewY = _scalar_skew_vertical.getValue("px");
907 skewY *= getDesktop()->yaxisdir();
908 if (fabs(skewX*skewY - width*height) < Geom::EPSILON) {
909 getDesktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
910 return;
911 }
912 selection->skewRelative(*center, skewX / height, skewY / width);
913 }
914 }
915 }
916
917 DocumentUndo::done(selection->desktop()->getDocument(), _("Skew"), INKSCAPE_ICON("dialog-transform"));
918}
919
921{
922 double a = _scalar_transform_a.getValue();
923 double b = _scalar_transform_b.getValue();
924 double c = _scalar_transform_c.getValue();
925 double d = _scalar_transform_d.getValue();
926 double e = _scalar_transform_e.getValue("px");
927 double f = _scalar_transform_f.getValue("px");
928
929 Geom::Affine displayed(a, b, c, d, e, f);
930 if (displayed.isSingular()) {
931 getDesktop()->messageStack()->flash(Inkscape::WARNING_MESSAGE, _("Transform matrix is singular, <b>not used</b>."));
932 return;
933 }
934
935 if (_check_replace_matrix.get_active()) {
936 auto tmp = selection->items();
937 for(auto i=tmp.begin();i!=tmp.end();++i){
938 SPItem *item = *i;
939 item->set_item_transform(displayed);
940 item->updateRepr();
941 }
942 } else {
943 selection->applyAffine(displayed); // post-multiply each object's transform
944 }
945
946 DocumentUndo::done(selection->desktop()->getDocument(), _("Edit transformation matrix"), INKSCAPE_ICON("dialog-transform"));
947}
948
949/*########################################################################
950# V A L U E - C H A N G E D C A L L B A C K S
951########################################################################*/
952
954{
955 applyButton->set_sensitive(true);
956}
957
959{
960 auto selection = getSelection();
961 if (!selection || selection->isEmpty())
962 return;
963
964 double x = _scalar_move_horizontal.getValue("px");
965 double y = _scalar_move_vertical.getValue("px");
966
967 double conversion = _units_move.getConversion("px");
968
969 //g_message("onMoveRelativeToggled: %f, %f px\n", x, y);
970
972
973 if (bbox) {
974 if (_check_move_relative.get_active()) {
975 // From absolute to relative
976 _scalar_move_horizontal.setValue((x - bbox->min()[Geom::X]) / conversion);
977 _scalar_move_vertical.setValue(( y - bbox->min()[Geom::Y]) / conversion);
978 } else {
979 // From relative to absolute
980 _scalar_move_horizontal.setValue((bbox->min()[Geom::X] + x) / conversion);
981 _scalar_move_vertical.setValue(( bbox->min()[Geom::Y] + y) / conversion);
982 }
983 }
984
985 applyButton->set_sensitive(true);
986}
987
989{
992 return;
993 }
994
995 applyButton->set_sensitive(true);
996
997 if (_check_scale_proportional.get_active()) {
998 if (!_units_scale.isAbsolute()) { // percentage, just copy over
1000 } else {
1001 double scaleXPercentage = _scalar_scale_horizontal.getAsPercentage();
1002 _scalar_scale_vertical.setFromPercentage (scaleXPercentage);
1003 }
1004 }
1005}
1006
1008{
1011 return;
1012 }
1013
1014 applyButton->set_sensitive(true);
1015
1016 if (_check_scale_proportional.get_active()) {
1017 if (!_units_scale.isAbsolute()) { // percentage, just copy over
1019 } else {
1020 double scaleYPercentage = _scalar_scale_vertical.getAsPercentage();
1022 }
1023 }
1024}
1025
1027{
1028 applyButton->set_sensitive(true);
1029}
1030
1032{
1033 _scalar_rotate.set_tooltip_text(_("Rotation angle (positive = counterclockwise)"));
1035 prefs->setBool("/dialogs/transformation/rotateCounterClockwise", !getDesktop()->is_yaxisdown());
1036}
1037
1039{
1040 _scalar_rotate.set_tooltip_text(_("Rotation angle (positive = clockwise)"));
1042 prefs->setBool("/dialogs/transformation/rotateCounterClockwise", getDesktop()->is_yaxisdown());
1043}
1044
1046{
1047 applyButton->set_sensitive(true);
1048}
1049
1051{
1052
1053 /*
1054 double a = _scalar_transform_a.getValue();
1055 double b = _scalar_transform_b.getValue();
1056 double c = _scalar_transform_c.getValue();
1057 double d = _scalar_transform_d.getValue();
1058 double e = _scalar_transform_e.getValue();
1059 double f = _scalar_transform_f.getValue();
1060
1061 //g_message("onTransformValueChanged: (%f, %f, %f, %f, %f, %f)\n",
1062 // a, b, c, d, e ,f);
1063 */
1064
1065 applyButton->set_sensitive(true);
1066}
1067
1069{
1070 auto selection = getSelection();
1071 if (!selection || selection->isEmpty())
1072 return;
1073
1074 double a = _scalar_transform_a.getValue();
1075 double b = _scalar_transform_b.getValue();
1076 double c = _scalar_transform_c.getValue();
1077 double d = _scalar_transform_d.getValue();
1078 double e = _scalar_transform_e.getValue("px");
1079 double f = _scalar_transform_f.getValue("px");
1080
1081 Geom::Affine displayed (a, b, c, d, e, f);
1082 Geom::Affine current = selection->items().front()->transform; // take from the first item in selection
1083
1084 Geom::Affine new_displayed;
1085 if (_check_replace_matrix.get_active()) {
1086 new_displayed = current;
1087 } else {
1088 new_displayed = current.inverse() * displayed;
1089 }
1090
1091 _scalar_transform_a.setValue(new_displayed[0]);
1092 _scalar_transform_b.setValue(new_displayed[1]);
1093 _scalar_transform_c.setValue(new_displayed[2]);
1094 _scalar_transform_d.setValue(new_displayed[3]);
1095 _scalar_transform_e.setValue(new_displayed[4], "px");
1096 _scalar_transform_f.setValue(new_displayed[5], "px");
1097}
1098
1106
1108{
1109 int const page = _notebook.get_current_page();
1110
1111 switch (page) {
1112 case PAGE_MOVE: {
1113 auto selection = getSelection();
1114 if (!selection || selection->isEmpty() || _check_move_relative.get_active()) {
1117 } else {
1119 if (bbox) {
1120 _scalar_move_horizontal.setValue(bbox->min()[Geom::X], "px");
1121 _scalar_move_vertical.setValue(bbox->min()[Geom::Y], "px");
1122 }
1123 }
1124 break;
1125 }
1126 case PAGE_ROTATE: {
1128 break;
1129 }
1130 case PAGE_SCALE: {
1133 break;
1134 }
1135 case PAGE_SKEW: {
1138 break;
1139 }
1140 case PAGE_TRANSFORM: {
1147 break;
1148 }
1149 }
1150}
1151
1153{
1155 prefs->setBool("/dialogs/transformation/applyseparately", _check_apply_separately.get_active());
1156}
1157
1159{
1160 // Setting default unit to document unit
1161 if (auto desktop = getDesktop()) {
1163 if (nv->display_units) {
1166 }
1167
1169 if (prefs->getBool("/dialogs/transformation/rotateCounterClockwise", true) != desktop->is_yaxisdown()) {
1170 _counterclockwise_rotate.set_active();
1172 } else {
1173 _clockwise_rotate.set_active();
1175 }
1176
1178 }
1179}
1180
1181} // namespace Inkscape::UI::Dialog
1182
1183/*
1184 Local Variables:
1185 mode:c++
1186 c-file-style:"stroustrup"
1187 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1188 indent-tabs-mode:nil
1189 fill-column:99
1190 End:
1191*/
1192// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
Simple helper class for sorting objects based on their bounding boxes.
uint64_t page
Definition canvas.cpp:171
3x3 matrix representing an affine transformation.
Definition affine.h:70
bool isSingular(Coord eps=EPSILON) const
Check whether this matrix is singular.
Definition affine.cpp:377
Affine inverse() const
Compute the inverse matrix.
Definition affine.cpp:388
Axis-aligned rectangle that can be empty.
Definition rect.h:203
Rotation around the origin.
Definition transforms.h:187
Translation by a vector.
Definition transforms.h:115
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
MessageId flash(MessageType type, char const *message)
Temporarily pushes a message onto the stack.
SPDesktop * desktop()
Returns the desktop the selection is bound to.
Definition object-set.h:390
SPItemRange items()
Returns a range of selected SPItems.
Definition object-set.h:255
Geom::OptRect preferredBounds() const
Returns either the visual or geometric bounding rectangle of the selection, based on the preferences ...
void rotateRelative(const Geom::Point &, double)
void applyAffine(Geom::Affine const &affine, bool set_i2d=true, bool compensate=true, bool adjust_transf_center=true)
Apply matrix to the selection.
void moveRelative(const Geom::Point &move, bool compensate=true)
std::optional< Geom::Point > center() const
Returns the rotation/skew center of the selection.
Geom::OptRect geometricBounds() const
void skewRelative(const Geom::Point &, double, double)
bool isEmpty()
Returns true if no items are selected.
Preference storage class.
Definition preferences.h:66
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
void setBool(Glib::ustring const &pref_path, bool value)
Set a Boolean value.
The set of selected SPObjects for a given document and layer model.
Definition selection.h:80
DialogBase is the base class for the dialog system.
Definition dialog-base.h:42
Selection * getSelection() const
Definition dialog-base.h:84
SPDesktop * getDesktop() const
Definition dialog-base.h:79
void updatePageRotate(Inkscape::Selection *)
UI::Widget::NotebookPage _page_transform
UI::Widget::ScalarUnit _scalar_transform_e
UI::Widget::NotebookPage _page_scale
Transformation()
Constructor for Transformation.
void selectionModified(Inkscape::Selection *selection, unsigned flags) final
UI::Widget::NotebookPage _page_rotate
void updatePageTransform(Inkscape::Selection *)
UI::Widget::ScalarUnit _scalar_rotate
void applyPageMove(Inkscape::Selection *)
Called when the Apply button is pushed Dialog-—>editor.
UI::Widget::NotebookPage _page_skew
void updatePageMove(Inkscape::Selection *)
Called when the selection is updated, to make the panel(s) show the new values.
UI::Widget::ScalarUnit _scalar_transform_f
void layoutPageMove()
Layout the GUI components, and prepare for use.
void applyPageSkew(Inkscape::Selection *)
UI::Widget::NotebookPage _page_move
void updateSelection(PageType page, Inkscape::Selection *selection)
UI::Widget::ScalarUnit _scalar_skew_horizontal
void selectionChanged(Inkscape::Selection *selection) final
UI::Widget::ScalarUnit _scalar_skew_vertical
void onSwitchPage(Gtk::Widget *page, guint pagenum)
void onMoveValueChanged()
Callbacks for when a user changes values on the panels.
UI::Widget::ScalarUnit _scalar_scale_vertical
void applyPageTransform(Inkscape::Selection *)
void updatePageScale(Inkscape::Selection *)
void desktopReplaced() final
Called when the desktop has certainly changed.
UI::Widget::ScalarUnit _scalar_scale_horizontal
void applyPageRotate(Inkscape::Selection *)
void updatePageSkew(Inkscape::Selection *)
UI::Widget::ScalarUnit _scalar_move_vertical
UI::Widget::ScalarUnit _scalar_move_horizontal
void applyPageScale(Inkscape::Selection *)
Gtk::Widget const * getWidget() const
Definition labelled.h:48
void setValue(double number, Glib::ustring const &units)
Sets the number and unit system.
void setAbsoluteIsIncrement(bool value)
void initScalar(double min_value, double max_value)
Initializes the scalar based on the settings in _unit_menu.
void setPercentageIsIncrement(bool value)
void setFromPercentage(double value)
Assuming the current unit is absolute, set the value corresponding to a given %.
void setHundredPercent(double number)
double getAsPercentage()
Assuming the current unit is absolute, get the corresponding % value.
double getValue(Glib::ustring const &units) const
Returns the value in the given unit system.
A labelled text box, with spin buttons and optional icon, for entering arbitrary number values.
Definition scalar.h:34
void setIncrements(double step, double page)
Sets the step and page increments for the spin button.
Definition scalar.cpp:123
void setWidthChars(unsigned chars)
Sets the width of the spin button by number of characters.
Definition scalar.cpp:142
Glib::SignalProxy< void()> signal_value_changed()
Signal raised when the spin button's value changes.
Definition scalar.cpp:159
void setDigits(unsigned digits)
Sets the precision to be displayed by the spin button.
Definition scalar.cpp:94
void setRange(double min, double max)
Sets the minimum and maximum range allowed for the spin button.
Definition scalar.cpp:128
void setValue(double value, bool setProg=true)
Sets the value of the spin button.
Definition scalar.cpp:133
bool setProgrammatically
true if the value was set by setValue, not changed by the user; if a callback checks it,...
Definition scalar.h:180
double getValue() const
Get the value in the spin_button.
Definition scalar.cpp:83
bool isRadial() const
Returns true if the selected unit is radial (deg or rad).
double getConversion(Glib::ustring const &new_unit_abbr, Glib::ustring const &old_unit_abbr="no_unit") const
Returns the conversion factor required to convert values of the currently selected unit into units of...
bool isAbsolute() const
Returns true if the selected unit is not dimensionless (false for %, true for px, pt,...
bool setUnit(Glib::ustring const &unit)
Sets the dropdown widget to the given unit abbreviation.
Definition unit-menu.cpp:75
bool setUnitType(UnitType unit_type, bool svg_length=false)
Adds the unit type to the widget.
Definition unit-menu.cpp:33
Glib::ustring abbr
Definition units.h:76
SPDocument * getDocument() const
Definition desktop.h:189
Inkscape::MessageStack * messageStack() const
Definition desktop.h:160
SPNamedView * getNamedView() const
Definition desktop.h:191
double yaxisdir() const
Definition desktop.h:426
bool is_yaxisdown() const
Definition desktop.h:427
Base class for visual SVG elements.
Definition sp-item.h:109
Geom::OptRect desktopPreferredBounds() const
Definition sp-item.cpp:1058
void set_i2d_affine(Geom::Affine const &transform)
Definition sp-item.cpp:1827
void set_item_transform(Geom::Affine const &transform_matrix)
Sets item private transform (not propagated to repr), without compensating stroke widths,...
Definition sp-item.cpp:1772
Geom::Affine i2dt_affine() const
Returns the transformation from item to desktop coords.
Definition sp-item.cpp:1821
void skew_rel(double skewX, double skewY)
Definition sp-item.cpp:1933
Geom::Affine transform
Definition sp-item.h:138
void rotate_rel(Geom::Rotate const &rotation)
Definition sp-item.cpp:1905
Geom::OptRect desktopGeometricBounds() const
Get item's geometric bbox in desktop coordinate system.
Definition sp-item.cpp:1044
void doWriteTransform(Geom::Affine const &transform, Geom::Affine const *adv=nullptr, bool compensate=true)
Set a new transform on an object.
Definition sp-item.cpp:1658
Inkscape::Util::Unit const * display_units
Inkscape::XML::Node * updateRepr(unsigned int flags=SP_OBJECT_WRITE_EXT)
Updates the object's repr based on the object's state.
const double w
Definition conic-4.cpp:19
double c[8][4]
Editable view implementation.
static char const *const current
Definition dir-util.cpp:71
TODO: insert short description here.
constexpr Coord EPSILON
Default "acceptably small" value.
Definition coord.h:84
@ Y
Definition coord.h:48
@ X
Definition coord.h:48
Icon Loader.
Macro for icon names used in Inkscape.
SPItem * item
static const unsigned int DEG
Glib::ustring label
Raw stack of active status messages.
Dialog code.
Definition desktop.h:117
static constexpr int height
void pack_start(Gtk::Box &box, Gtk::Widget &child, bool const expand, bool const fill, unsigned const padding)
Adds child to box, packed with reference to the start of box.
Definition pack.cpp:141
@ UNIT_TYPE_LINEAR
Definition units.h:34
@ WARNING_MESSAGE
Definition message.h:28
Helpers for using Gtk::Boxes, encapsulating large changes between GTK3 & GTK4.
Singleton class to access the preferences file in a convenient way.
GList * items
Geom::Affine get_scale_transform_for_variable_stroke(Geom::Rect const &bbox_visual, Geom::Rect const &bbox_geom, bool transform_stroke, bool preserve, gdouble x0, gdouble y0, gdouble x1, gdouble y1)
Calculate the affine transformation required to transform one visual bounding box into another,...
TODO: insert short description here.
double width
Transform dialog.
Affine transformation classes.