Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
actions-object-align.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Gio::Actions for aligning and distributing objects without GUI.
4 *
5 * Copyright (C) 2020 Tavmjong Bah
6 *
7 * Some code and ideas from src/ui/dialogs/align-and-distribute.cpp
8 * Authors: Bryce Harrington
9 * Martin Owens
10 * John Smith
11 * Patrick Storz
12 * Jabier Arraiza
13 *
14 * The contents of this file may be used under the GNU General Public License Version 2 or later.
15 *
16 */
17
18#include "actions-helper.h"
20
21#include <limits>
22
23#include <giomm.h> // Not <gtkmm.h>! To eventually allow a headless version!
24#include <glibmm/i18n.h>
25
26#include "document-undo.h"
27#include "enums.h" // Clones
28#include "filter-chemistry.h" // LPE bool
30#include "preferences.h"
31#include "selection.h"
32#include "text-editing.h"
33
34#include "object/sp-text.h"
35#include "object/sp-flowtext.h"
36
37#include "object/algorithms/graphlayout.h" // Graph layout objects.
38#include "object/algorithms/removeoverlap.h" // Remove overlaps between objects.
39#include "object/algorithms/unclump.h" // Rearrange objects.
40#include "object/algorithms/bboxsort.h" // Sort based on bounding box.
41
43#include "live_effects/effect.h"
44
45#include "object/sp-root.h" // "Desktop Bounds"
46
47#include "ui/icon-names.h" // Icon macro used in undo.
48
50 LAST,
51 FIRST,
52 BIGGEST,
54 PAGE,
55 DRAWING,
57};
58
59void
61{
62 // Get Action
63 auto *gapp = app->gio_app();
64 auto action = gapp->lookup_action("object-align-on-canvas");
65 if (!action) {
66 show_output("object_align_on_canvas: action missing!");
67 return;
68 }
69
70 auto saction = std::dynamic_pointer_cast<Gio::SimpleAction>(action);
71 if (!saction) {
72 show_output("object_align_on_canvas: action not SimpleAction!");
73 return;
74 }
75
76 // Toggle state
77 bool state = false;
78 saction->get_state(state);
79 state = !state;
80 saction->change_state(state);
81
82 // Toggle action
84 prefs->setBool("/dialogs/align/oncanvas", state);
85}
86
87void
88object_align(const Glib::VariantBase& value, InkscapeApplication *app)
89{
91 Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
92 std::vector<Glib::ustring> tokens = Glib::Regex::split_simple(" ", s.get());
93
94 // Find out if we are using an anchor.
95 bool anchor = std::find(tokens.begin(), tokens.end(), "anchor") != tokens.end();
96
97 // Default values:
98 auto target = ObjectAlignTarget::SELECTION;
99
100 bool group = false;
101 double mx0 = 0;
102 double mx1 = 0;
103 double my0 = 0;
104 double my1 = 0;
105 double sx0 = 0;
106 double sx1 = 0;
107 double sy0 = 0;
108 double sy1 = 0;
109
110 // Preference request allows alignment action to remember for key-presses
111 if (std::find(tokens.begin(), tokens.end(), "pref") != tokens.end()) {
112 group = prefs->getBool("/dialogs/align/sel-as-groups", false);
113 tokens.push_back(prefs->getString("/dialogs/align/objects-align-to", "selection"));
114 }
115
116 // clang-format off
117 for (auto const &token : tokens) {
118
119 // Target
120 if (token == "last" ) target = ObjectAlignTarget::LAST;
121 else if (token == "first" ) target = ObjectAlignTarget::FIRST;
122 else if (token == "biggest" ) target = ObjectAlignTarget::BIGGEST;
123 else if (token == "smallest" ) target = ObjectAlignTarget::SMALLEST;
124 else if (token == "page" ) target = ObjectAlignTarget::PAGE;
125 else if (token == "drawing" ) target = ObjectAlignTarget::DRAWING;
126 else if (token == "selection") target = ObjectAlignTarget::SELECTION;
127
128 // Group
129 else if (token == "group") group = true;
130
131 // Position
132 if (!anchor) {
133 if (token == "left" ) { mx0 = 1.0; mx1 = 0.0; sx0 = 1.0; sx1 = 0.0; }
134 else if (token == "hcenter" ) { mx0 = 0.5; mx1 = 0.5; sx0 = 0.5; sx1 = 0.5; }
135 else if (token == "right" ) { mx0 = 0.0; mx1 = 1.0; sx0 = 0.0; sx1 = 1.0; }
136
137 else if (token == "top" ) { my0 = 1.0; my1 = 0.0; sy0 = 1.0; sy1 = 0.0; }
138 else if (token == "vcenter" ) { my0 = 0.5; my1 = 0.5; sy0 = 0.5; sy1 = 0.5; }
139 else if (token == "bottom" ) { my0 = 0.0; my1 = 1.0; sy0 = 0.0; sy1 = 1.0; }
140 } else {
141 if (token == "left" ) { mx0 = 0.0; mx1 = 1.0; sx0 = 1.0; sx1 = 0.0; }
142 else if (token == "hcenter" ) show_output("'anchor' cannot be used with 'hcenter'");
143 else if (token == "right" ) { mx0 = 1.0; mx1 = 0.0; sx0 = 0.0; sx1 = 1.0; }
144
145 else if (token == "top" ) { my0 = 0.0; my1 = 1.0; sy0 = 1.0; sy1 = 0.0; }
146 else if (token == "vcenter" ) show_output("'anchor' cannot be used with 'vcenter'");
147 else if (token == "bottom" ) { my0 = 1.0; my1 = 0.0; sy0 = 0.0; sy1 = 1.0; }
148 }
149 }
150 // clang-format on
151
152 auto selection = app->get_active_selection();
153
154 // We should not have to do this!
155 auto document = app->get_active_document();
156 selection->setDocument(document);
157
158 // We force unselect operand in bool LPE. TODO: See if we can use "selected" from below.
159 auto list = selection->items();
160 std::size_t total = std::distance(list.begin(), list.end());
161 std::vector<SPItem *> selected;
162 std::vector<Inkscape::LivePathEffect::Effect *> bools;
163 for (auto item : list) {
164 if (total == 2) {
165 auto lpeitem = cast<SPLPEItem>(item);
166 if (lpeitem) {
167 for (auto lpe : lpeitem->getPathEffectsOfType(Inkscape::LivePathEffect::EffectType::BOOL_OP)) {
168 if (!g_strcmp0(lpe->getRepr()->attribute("is_visible"), "true")) {
169 lpe->getRepr()->setAttribute("is_visible", "false");
170 bools.emplace_back(lpe);
172 }
173 }
174 }
175 }
176 if (!(item && has_hidder_filter(item) && total > 2)) {
177 selected.emplace_back(item);
178 }
179 }
180
181 if (selected.empty()) return;
182
183 // Find alignment rectangle. This can come from:
184 // - The bounding box of an object
185 // - The bounding box of a group of objects
186 // - The bounding box of the page, drawing, or selection.
187 SPItem *focus = nullptr;
190
191 switch (target) {
193 focus = selected.back();
194 break;
196 focus = selected.front();
197 break;
199 focus = selection->largestItem(direction);
200 break;
202 focus = selection->smallestItem(direction);
203 break;
205 b = document->pageBounds();
206 break;
208 b = document->getRoot()->desktopPreferredBounds();
209 break;
211 b = selection->preferredBounds();
212 break;
213 default:
214 g_assert_not_reached ();
215 break;
216 };
217
218 if (focus) {
219 b = focus->desktopPreferredBounds();
220 }
221
222 g_return_if_fail(b);
223
224 if (auto desktop = selection->desktop(); desktop && !desktop->is_yaxisdown()) {
225 std::swap(my0, my1);
226 std::swap(sy0, sy1);
227 }
228
229 // Generate the move point from the selected bounding box
230 Geom::Point mp = Geom::Point(mx0 * b->min()[Geom::X] + mx1 * b->max()[Geom::X],
231 my0 * b->min()[Geom::Y] + my1 * b->max()[Geom::Y]);
232
233 if (group) {
234 if (focus) {
235 // Use bounding box of all selected elements except the "focused" element.
236 Inkscape::ObjectSet copy(document);
237 copy.add(selection->objects().begin(), selection->objects().end());
238 copy.remove(focus);
239 b = copy.preferredBounds();
240 } else {
241 // Use bounding box of all selected elements.
242 b = selection->preferredBounds();
243 }
244 }
245
246 // Move each item in the selected list separately.
247 bool changed = false;
248 for (auto item : selected) {
249 document->ensureUpToDate();
250
251 if (!group) {
252 b = (item)->desktopPreferredBounds();
253 }
254
255 if (b && (!focus || (item) != focus)) {
256 Geom::Point const sp(sx0 * b->min()[Geom::X] + sx1 * b->max()[Geom::X],
257 sy0 * b->min()[Geom::Y] + sy1 * b->max()[Geom::Y]);
258 Geom::Point const mp_rel( mp - sp );
259 if (LInfty(mp_rel) > 1e-9) {
260 item->move_rel(Geom::Translate(mp_rel));
261 changed = true;
262 }
263 }
264 }
265
266 if (changed) {
267 Inkscape::DocumentUndo::done(document, _("Align"), INKSCAPE_ICON("dialog-align-and-distribute"));
268 }
269}
270
271void
272object_distribute(const Glib::VariantBase& value, InkscapeApplication *app)
273{
274 Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
275 auto token = s.get();
276
277 auto selection = app->get_active_selection();
278
279 // We should not have to do this!
280 auto document = app->get_active_document();
281 selection->setDocument(document);
282
283 std::vector<SPItem*> selected(selection->items().begin(), selection->items().end());
284 if (selected.size() < 2) {
285 return;
286 }
287
288 // clang-format off
289 double a = 0.0;
290 double b = 0.0;
291 bool gap = false;
292 auto orientation = Geom::X;
293 if (token == "hgap" ) { gap = true; orientation = Geom::X; a = 0.5, b = 0.5; }
294 else if (token == "left" ) { gap = false; orientation = Geom::X; a = 1.0, b = 0.0; }
295 else if (token == "hcenter" ) { gap = false; orientation = Geom::X; a = 0.5, b = 0.5; }
296 else if (token == "right" ) { gap = false; orientation = Geom::X; a = 0.0, b = 1.0; }
297 else if (token == "vgap" ) { gap = true; orientation = Geom::Y; a = 0.5, b = 0.5; }
298 else if (token == "top" ) { gap = false; orientation = Geom::Y; a = 1.0, b = 0.0; }
299 else if (token == "vcenter" ) { gap = false; orientation = Geom::Y; a = 0.5, b = 0.5; }
300 else if (token == "bottom" ) { gap = false; orientation = Geom::Y; a = 0.0, b = 1.0; }
301 // clang-format on
302
303
305 int prefs_bbox = prefs->getBool("/tools/bounding_box");
306
307 // Make a list of objects, sorted by anchors.
308 std::vector<BBoxSort> sorted;
309 for (auto item : selected) {
310 Geom::OptRect bbox = !prefs_bbox ? (item)->desktopVisualBounds() : (item)->desktopGeometricBounds();
311 if (bbox) {
312 sorted.emplace_back(item, *bbox, orientation, a, b);
313 }
314 }
315 std::stable_sort(sorted.begin(), sorted.end());
316
317 // See comment in ActionAlign above (MISSING).
318 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
319 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
320
321 bool changed = false;
322 if (gap) {
323 // Evenly spaced.
324
325 // Overall bboxes span.
326 double dist = (sorted.back().bbox.max()[orientation] - sorted.front().bbox.min()[orientation]);
327
328 // Space eaten by bboxes.
329 double span = 0.0;
330 for (auto bbox : sorted) {
331 span += bbox.bbox[orientation].extent();
332 }
333
334 // New distance between each bbox.
335 double step = (dist - span) / (sorted.size() - 1);
336 double pos = sorted.front().bbox.min()[orientation];
337 for (auto bbox : sorted) {
338
339 // Don't move if we are really close.
340 if (!Geom::are_near(pos, bbox.bbox.min()[orientation], 1e-6)) {
341
342 // Compute translation.
343 Geom::Point t(0.0, 0.0);
344 t[orientation] = pos - bbox.bbox.min()[orientation];
345
346 // Translate
347 bbox.item->move_rel(Geom::Translate(t));
348 changed = true;
349 }
350
351 pos += bbox.bbox[orientation].extent();
352 pos += step;
353 }
354
355 } else {
356
357 // Overall anchor span.
358 double dist = sorted.back().anchor - sorted.front().anchor;
359
360 // Distance between anchors.
361 double step = dist / (sorted.size() - 1);
362
363 for (unsigned int i = 0; i < sorted.size() ; i++) {
364 BBoxSort & it(sorted[i]);
365
366 // New anchor position.
367 double pos = sorted.front().anchor + i * step;
368
369 // Don't move if we are really close.
370 if (!Geom::are_near(pos, it.anchor, 1e-6)) {
371
372 // Compute translation.
373 Geom::Point t(0.0, 0.0);
374 t[orientation] = pos - it.anchor;
375
376 // Translate
378 changed = true;
379 }
380 }
381 }
382
383 // Restore compensation setting.
384 prefs->setInt("/options/clonecompensation/value", saved_compensation);
385
386 if (changed) {
387 Inkscape::DocumentUndo::done( document, _("Distribute"), INKSCAPE_ICON("dialog-align-and-distribute"));
388 }
389}
390
391class Baseline
392{
393public:
394 Baseline(SPItem *item, Geom::Point base, Geom::Dim2 orientation)
395 : _item (item)
396 , _base (base)
397 , _orientation (orientation)
398 {}
399 SPItem *_item = nullptr;
400 Geom::Point _base;
401 Geom::Dim2 _orientation;
402};
403
404static bool operator< (const Baseline &a, const Baseline &b)
405{
406 return (a._base[a._orientation] < b._base[b._orientation]);
407}
408
409void
410object_distribute_text(const Glib::VariantBase& value, InkscapeApplication *app)
411{
412 Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
413 auto token = s.get();
414
415 Geom::Dim2 orientation = Geom::Dim2::X;
416 if (token.find("vertical") != Glib::ustring::npos) {
417 orientation = Geom::Dim2::Y;
418 }
419
420 auto selection = app->get_active_selection();
421 if (selection->size() < 2) {
422 return;
423 }
424
425 // We should not have to do this!
426 auto document = app->get_active_document();
427 selection->setDocument(document);
428
429 std::vector<Baseline> baselines;
430 Geom::Point b_min = Geom::Point ( HUGE_VAL, HUGE_VAL);
431 Geom::Point b_max = Geom::Point (-HUGE_VAL, -HUGE_VAL);
432
433 for (auto item : selection->items()) {
434 if (is<SPText>(item) || cast<SPFlowtext>(item)) {
436 std::optional<Geom::Point> pt = layout->baselineAnchorPoint();
437 if (pt) {
438 Geom::Point base = *pt * item->i2dt_affine();
439 if (base[Geom::X] < b_min[Geom::X]) b_min[Geom::X] = base[Geom::X];
440 if (base[Geom::Y] < b_min[Geom::Y]) b_min[Geom::Y] = base[Geom::Y];
441 if (base[Geom::X] > b_max[Geom::X]) b_max[Geom::X] = base[Geom::X];
442 if (base[Geom::Y] > b_max[Geom::Y]) b_max[Geom::Y] = base[Geom::Y];
443 baselines.emplace_back(item, base, orientation);
444 }
445 }
446 }
447
448 if (baselines.size() < 2) {
449 return;
450 }
451
452 std::stable_sort(baselines.begin(), baselines.end());
453
454 double step = (b_max[orientation] - b_min[orientation])/(baselines.size() - 1);
455 int i = 0;
456 for (auto& baseline : baselines) {
457 Geom::Point t(0.0, 0.0);
458 t[orientation] = b_min[orientation] + (step * i) - baseline._base[orientation];
459 baseline._item->move_rel(Geom::Translate(t));
460 ++i;
461 }
462
463 Inkscape::DocumentUndo::done( document, _("Distribute"), INKSCAPE_ICON("dialog-align-and-distribute"));
464}
465
466void
467object_align_text(const Glib::VariantBase& value, InkscapeApplication *app)
468{
469
470 Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
471 std::vector<Glib::ustring> tokens = Glib::Regex::split_simple(" ", s.get());
472
473 // Defaults
474 auto target = ObjectAlignTarget::SELECTION;
475 auto orientation = Geom::Dim2::X;
476 auto direction = Inkscape::Selection::HORIZONTAL;
477
478 // Preference request allows alignment action to remember for key-presses
480 if (std::find(tokens.begin(), tokens.end(), "pref") != tokens.end()) {
481 tokens.push_back(prefs->getString("/dialogs/align/objects-align-to", "selection"));
482 }
483
484 for (auto const &token : tokens) {
485
486 // Target
487 if (token == "last" ) target = ObjectAlignTarget::LAST;
488 else if (token == "first" ) target = ObjectAlignTarget::FIRST;
489 else if (token == "biggest" ) target = ObjectAlignTarget::BIGGEST;
490 else if (token == "smallest" ) target = ObjectAlignTarget::SMALLEST;
491 else if (token == "page" ) target = ObjectAlignTarget::PAGE;
492 else if (token == "drawing" ) target = ObjectAlignTarget::DRAWING;
493 else if (token == "selection") target = ObjectAlignTarget::SELECTION;
494
495 // Direction
496 if (token == "vertical" ) {
497 orientation = Geom::Dim2::Y;
499 }
500 }
501
502 auto selection = app->get_active_selection();
503
504 if (selection->items().empty()) {
505 return;
506 }
507
508 // We should not have to do this!
509 auto document = app->get_active_document();
510 selection->setDocument(document);
511
512 // Find alignment rectangle. This can come from:
513 // - The bounding box of an object
514 // - The bounding box of a group of objects
515 // - The bounding box of the page, drawing, or selection.
516 SPItem *focus = nullptr;
518
519 switch (target) {
521 focus = selection->items().back();
522 break;
524 focus = selection->items().front();
525 break;
527 focus = selection->largestItem(direction);
528 break;
530 focus = selection->smallestItem(direction);
531 break;
533 b = document->pageBounds();
534 break;
536 b = document->getRoot()->desktopPreferredBounds();
537 break;
539 b = selection->preferredBounds();
540 break;
541 default:
542 g_assert_not_reached ();
543 break;
544 };
545
546 Geom::Point ref_point;
547 if (focus) {
548 if (is<SPText>(focus) || cast<SPFlowtext>(focus)) {
549 ref_point = *(te_get_layout(focus)->baselineAnchorPoint())*(focus->i2dt_affine());
550 } else {
551 ref_point = focus->desktopPreferredBounds()->min();
552 }
553 } else {
554 ref_point = b->min();
555 }
556
557 for (auto item : selection->items()) {
558 if (is<SPText>(item) || cast<SPFlowtext>(item)) {
560 std::optional<Geom::Point> pt = layout->baselineAnchorPoint();
561 if (pt) {
562 Geom::Point base = *pt * (item)->i2dt_affine();
563 Geom::Point t(0.0, 0.0);
564 t[orientation] = ref_point[orientation] - base[orientation];
566 }
567 }
568 }
569
570 Inkscape::DocumentUndo::done( document, _("Align"), INKSCAPE_ICON("dialog-align-and-distribute"));
571}
572
573/* --------------- Rearrange ----------------- */
574
575class RotateCompare
576{
577public:
578 RotateCompare(Geom::Point& center) : center(center) {}
579
580 bool operator()(const SPItem* a, const SPItem* b) {
581 Geom::Point point_a = a->getCenter() - (center);
582 Geom::Point point_b = b->getCenter() - (center);
583
584 // Sort according to angle.
585 double angle_a = Geom::atan2(point_a);
586 double angle_b = Geom::atan2(point_b);
587 if (angle_a != angle_b) return (angle_a < angle_b);
588
589 // Sort by distance
590 return point_a.length() < point_b.length();
591 }
592
593private:
594 Geom::Point center;
595};
596
602
603static bool PositionCompare(const SPItem* a, const SPItem* b) {
604 return sp_item_repr_compare_position(a, b) < 0;
605}
606
608{
609 std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
610
611 // Reorder items.
612 switch (order) {
613 case SelectionOrder:
614 break;
615 case ZOrder:
616 std::sort(items.begin(), items.end(), PositionCompare);
617 break;
618 case Rotate:
619 auto center = selection->center();
620 if (center) {
621 std::sort(items.begin(), items.end(), RotateCompare(*center));
622 }
623 break;
624 }
625
626 // Move items.
627 Geom::Point p1 = items.back()->getCenter();
628 for (SPItem *item : items) {
629 Geom::Point p2 = item->getCenter();
630 Geom::Point delta = p1 - p2;
632 p1 = p2;
633 }
634}
635
636/*
637 * The algorithm keeps the size of the bounding box of the centers of all items constant. This
638 * ensures there is no growth or shrinking or drift of the overall area of the items on sequential
639 * randomizations.
640 */
642{
643 std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
644
645 // Do 'x' and 'y' independently.
646 for (int i = 0; i < 2; i++) {
647
648 // First, find maximum and minimum centers.
649 double min = std::numeric_limits<double>::max();
650 double max = std::numeric_limits<double>::min();
651
652 for (auto item : items) {
653 double center = item->getCenter()[i];
654 if (min > center) {
655 min = center;
656 }
657 if (max < center) {
658 max = center;
659 }
660 }
661
662
663 // Second, assign minimum/maximum values to two different items randomly.
664 int nitems = items.size();
665 int imin = rand() % nitems;
666 int imax = rand() % nitems;
667 while (imin == imax) {
668 imax = rand() % nitems;
669 }
670
671
672 // Third, find new positions of item centers.
673 int index = 0;
674 for (auto item : items) {
675 double z = 0.0;
676 if (index == imin) {
677 z = min;
678 } else if (index == imax) {
679 z = max;
680 } else {
681 z = g_random_double_range(min, max);
682 }
683
684 double delta = z - item->getCenter()[i];
685 Geom::Point t;
686 t[i] = delta;
688
689 ++index;
690 }
691 }
692}
693
694
695void
696object_rearrange(const Glib::VariantBase& value, InkscapeApplication *app)
697{
698 Glib::Variant<Glib::ustring> s = Glib::VariantBase::cast_dynamic<Glib::Variant<Glib::ustring> >(value);
699 auto token = s.get();
700
701 auto selection = app->get_active_selection();
702
703 // We should not have to do this!
704 auto document = app->get_active_document();
705 selection->setDocument(document);
706
707 std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
708 if (items.size() < 2) {
709 return;
710 }
711
713 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
714 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
715
716 // clang-format off
717 if (token == "graph" ) { graphlayout(items); }
718 else if (token == "exchange" ) { exchange(selection, SortOrder::SelectionOrder); }
719 else if (token == "exchangez" ) { exchange(selection, SortOrder::ZOrder); }
720 else if (token == "rotate" ) { exchange(selection, SortOrder::Rotate); }
721 else if (token == "randomize" ) { randomize(selection); }
722 else if (token == "unclump" ) { unclump(items); }
723 else {
724 show_output(Glib::ustring("object_rearrange: unhandled argument: ") + token.raw());
725 }
726 // clang-format on
727
728 // Restore compensation setting.
729 prefs->setInt("/options/clonecompensation/value", saved_compensation);
730
731 Inkscape::DocumentUndo::done( document, _("Rearrange"), INKSCAPE_ICON("dialog-align-and-distribute"));
732}
733
734
735void
736object_remove_overlaps(const Glib::VariantBase& value, InkscapeApplication *app)
737{
738 auto selection = app->get_active_selection();
739
740 // We should not have to do this!
741 auto document = app->get_active_document();
742 selection->setDocument(document);
743
744 std::vector<SPItem*> items(selection->items().begin(), selection->items().end());
745 if (items.size() < 2) {
746 return;
747 }
748
749 // We used tuple so as not to convert from double to string and back again (from Align and Distribute dialog).
750 if (value.get_type_string() != "(dd)") {
751 show_output(Glib::ustring("object_remove_overlaps: wrong variant type: ") + Glib::ustring::format(value.get_type_string()) + " (should be '(dd)')");
752 }
753
754 auto tuple = Glib::VariantBase::cast_dynamic<Glib::Variant<std::tuple<double, double>>>(value);
755 auto [hgap, vgap] = tuple.get();
756
758 int saved_compensation = prefs->getInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
759 prefs->setInt("/options/clonecompensation/value", SP_CLONE_COMPENSATION_UNMOVED);
760
761 removeoverlap(items, hgap, vgap);
762
763 // Restore compensation setting.
764 prefs->setInt("/options/clonecompensation/value", saved_compensation);
765
766 Inkscape::DocumentUndo::done( document, _("Remove overlaps"), INKSCAPE_ICON("dialog-align-and-distribute"));
767}
768
769const Glib::ustring SECTION = NC_("Action Section", "Object");
770
771std::vector<std::vector<Glib::ustring>> raw_data_object_align =
772{
773 // clang-format off
774 {"app.object-align-on-canvas", N_("Enable on-canvas alignment"), SECTION, N_("Enable on-canvas alignment handles" )},
775
776 {"app.object-align", N_("Align objects"), SECTION, N_("Align selected objects; usage: [[left|hcenter|right] || [top|vcenter|bottom]] [last|first|biggest|smallest|page|drawing|selection|pref]? group? anchor?")},
777
778 {"app.object-align('left pref')", N_("Align to left edge"), SECTION, N_("Align selection horizontally to left edge" )},
779 {"app.object-align('hcenter pref')", N_("Align to horizontal center"), SECTION, N_("Align selection horizontally to the center" )},
780 {"app.object-align('right pref')", N_("Align to right edge"), SECTION, N_("Align selection horizontally to right edge" )},
781 {"app.object-align('top pref')", N_("Align to top edge"), SECTION, N_("Align selection vertically to top edge" )},
782 {"app.object-align('bottom pref')", N_("Align to bottom edge"), SECTION, N_("Align selection vertically to bottom edge" )},
783 {"app.object-align('vcenter pref')", N_("Align to vertical center"), SECTION, N_("Align selection vertically to the center" )},
784 {"app.object-align('hcenter vcenter pref')", N_("Align to center"), SECTION, N_("Align selection to the center" )},
785 {"app.object-align-text", N_("Align text objects"), SECTION, N_("Align selected text anchors; usage: [[vertical | horizontal] [last|first|biggest|smallest|page|drawing|selection]?" )},
786
787 {"app.object-distribute", N_("Distribute objects"), SECTION, N_("Distribute selected objects; usage: [hgap | left | hcenter | right | vgap | top | vcenter | bottom]" )},
788 {"app.object-distribute('hgap')", N_("Even horizontal gaps"), SECTION, N_("Distribute horizontally with even horizontal gaps" )},
789 {"app.object-distribute('left')", N_("Even left edges"), SECTION, N_("Distribute horizontally with even spacing between left edges" )},
790 {"app.object-distribute('hcenter')", N_("Even horizontal centers"), SECTION, N_("Distribute horizontally with even spacing between centers" )},
791 {"app.object-distribute('right')", N_("Even right edges"), SECTION, N_("Distribute horizontally with even spacing between right edges" )},
792 {"app.object-distribute('vgap')", N_("Even vertical gaps"), SECTION, N_("Distribute vertically with even vertical gaps" )},
793 {"app.object-distribute('top')", N_("Even top edges"), SECTION, N_("Distribute vertically with even spacing between top edges" )},
794 {"app.object-distribute('vcenter')", N_("Even vertical centers"), SECTION, N_("Distribute vertically with even spacing between centers" )},
795 {"app.object-distribute('bottom')", N_("Even bottom edges"), SECTION, N_("Distribute vertically with even spacing between bottom edges" )},
796
797 {"app.object-distribute-text", N_("Distribute text objects"), SECTION, N_("Distribute text anchors; usage [vertical | horizontal]" )},
798 {"app.object-distribute-text('horizontal')", N_("Distribute text objects"), SECTION, N_("Distribute text anchors horizontally" )},
799 {"app.object-distribute-text('vertical')", N_("Distribute text objects"), SECTION, N_("Distribute text anchors vertically" )},
800
801 {"app.object-rearrange", N_("Rearrange objects"), SECTION, N_("Rearrange selected objects; usage: [graph | exchange | exchangez | rotate | randomize | unclump]" )},
802 {"app.object-rearrange('graph')", N_("Rearrange as graph"), SECTION, N_("Nicely arrange selected connector network" )},
803 {"app.object-rearrange('exchange')", N_("Exchange in selection order"), SECTION, N_("Exchange positions of selected objects - selection order" )},
804 {"app.object-rearrange('exchangez')", N_("Exchange in z-order"), SECTION, N_("Exchange positions of selected objects - stacking order" )},
805 {"app.object-rearrange('rotate')", N_("Exchange around center"), SECTION, N_("Exchange positions of selected objects - rotate around center point" )},
806 {"app.object-rearrange('randomize')", N_("Random exchange"), SECTION, N_("Randomize centers in both dimensions" )},
807 {"app.object-rearrange('unclump')", N_("Unclump"), SECTION, N_("Unclump objects: try to equalize edge-to-edge distances" )},
808
809 {"app.object-remove-overlaps", N_("Remove overlaps"), SECTION, N_("Remove overlaps between objects: requires two comma separated numbers (horizontal and vertical gaps)" )},
810 // clang-format on
811};
812
813std::vector<std::vector<Glib::ustring>> hint_data_object_align =
814{
815 // clang-format off
816 {"app.object-align", N_("Enter anchor<space>alignment<space>optional second alignment. Possible anchors: last, first, biggest, smallest, page, drawing, selection, pref; possible alignments: left, hcenter, right, top, vcenter, bottom.")},
817 {"app.object-distribute", N_("Enter distribution type. Possible values: left, hcenter, right, top, vcenter, bottom, hgap, vgap.") },
818 {"app.object-rearrange", N_("Enter arrange method. Possible values: graph, exchange, exchangez, rotate, randomize, unclump.") },
819 {"app.object-remove-overlaps", N_("Enter two comma-separated numbers: horizontal,vertical") },
820 // clang-format on
821};
822
823void
825{
826 Glib::VariantType String(Glib::VARIANT_TYPE_STRING);
827 std::vector<Glib::VariantType> dd = {Glib::VARIANT_TYPE_DOUBLE, Glib::VARIANT_TYPE_DOUBLE};
828 Glib::VariantType Tuple_DD = Glib::VariantType::create_tuple(dd);
829
830 auto *gapp = app->gio_app();
831
832 auto prefs = Inkscape::Preferences::get();
833 bool on_canvas = prefs->getBool("/dialogs/align/oncanvas");
834
835 // clang-format off
836 gapp->add_action_bool( "object-align-on-canvas", sigc::bind(sigc::ptr_fun(&object_align_on_canvas), app), on_canvas);
837 gapp->add_action_with_parameter( "object-align", String, sigc::bind(sigc::ptr_fun(&object_align), app));
838 gapp->add_action_with_parameter( "object-align-text", String, sigc::bind(sigc::ptr_fun(&object_align_text), app));
839 gapp->add_action_with_parameter( "object-distribute", String, sigc::bind(sigc::ptr_fun(&object_distribute), app));
840 gapp->add_action_with_parameter( "object-distribute-text", String, sigc::bind(sigc::ptr_fun(&object_distribute_text), app));
841 gapp->add_action_with_parameter( "object-rearrange", String, sigc::bind(sigc::ptr_fun(&object_rearrange), app));
842 gapp->add_action_with_parameter( "object-remove-overlaps", Tuple_DD, sigc::bind(sigc::ptr_fun(&object_remove_overlaps), app));
843 // clang-format on
844
847}
848
849/*
850 Local Variables:
851 mode:c++
852 c-file-style:"stroustrup"
853 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
854 indent-tabs-mode:nil
855 fill-column:99
856 End:
857*/
858// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
void show_output(Glib::ustring const &data, bool const is_cerr)
void object_rearrange(const Glib::VariantBase &value, InkscapeApplication *app)
std::vector< std::vector< Glib::ustring > > hint_data_object_align
void randomize(Inkscape::Selection *selection)
const Glib::ustring SECTION
void exchange(Inkscape::Selection *selection, SortOrder order)
std::vector< std::vector< Glib::ustring > > raw_data_object_align
static bool operator<(const Baseline &a, const Baseline &b)
void object_distribute(const Glib::VariantBase &value, InkscapeApplication *app)
static bool PositionCompare(const SPItem *a, const SPItem *b)
void object_align_text(const Glib::VariantBase &value, InkscapeApplication *app)
void object_align(const Glib::VariantBase &value, InkscapeApplication *app)
void object_distribute_text(const Glib::VariantBase &value, InkscapeApplication *app)
void object_remove_overlaps(const Glib::VariantBase &value, InkscapeApplication *app)
void object_align_on_canvas(InkscapeApplication *app)
void add_actions_object_align(InkscapeApplication *app)
Simple helper class for sorting objects based on their bounding boxes.
double anchor
Definition bboxsort.h:34
SPItem * item
Definition bboxsort.h:35
Axis-aligned rectangle that can be empty.
Definition rect.h:203
Two-dimensional point that doubles as a vector.
Definition point.h:66
constexpr Coord get() const
Definition point.h:110
Coord length() const
Compute the distance from origin.
Definition point.h:118
Translation by a vector.
Definition transforms.h:115
void add_data(std::vector< std::vector< Glib::ustring > > const &raw_data)
void add_data(std::vector< std::vector< Glib::ustring > > &raw_data)
InkActionExtraData & get_action_extra_data()
Gio::Application * gio_app()
The Gio application instance, never NULL.
SPDocument * get_active_document()
InkActionHintData & get_action_hint_data()
Inkscape::Selection * get_active_selection()
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
SPItemRange items()
Returns a range of selected SPItems.
Definition object-set.h:255
std::optional< Geom::Point > center() const
Returns the rotation/skew center of the selection.
Preference storage class.
Definition preferences.h:66
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
Glib::ustring getString(Glib::ustring const &pref_path, Glib::ustring const &def="")
Retrieve an UTF-8 string.
static Preferences * get()
Access the singleton Preferences object.
int getInt(Glib::ustring const &pref_path, int def=0)
Retrieve an integer.
void setInt(Glib::ustring const &pref_path, int value)
Set an integer value.
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
Generates the layout for either wrapped or non-wrapped text and stores the result.
Definition Layout-TNG.h:144
std::optional< Geom::Point > baselineAnchorPoint() const
For left aligned text, the leftmost end of the baseline For rightmost text, the rightmost....
bool is_yaxisdown() const
Definition desktop.h:427
int ensureUpToDate(unsigned int object_modified_tag=0)
Repeatedly works on getting the document updated, since sometimes it takes more than one pass to get ...
Base class for visual SVG elements.
Definition sp-item.h:109
Geom::OptRect desktopPreferredBounds() const
Definition sp-item.cpp:1074
Geom::Affine i2dt_affine() const
Returns the transformation from item to desktop coords.
Definition sp-item.cpp:1837
Geom::Point getCenter(bool ensure_uptodate=true) const
Definition sp-item.cpp:384
void move_rel(Geom::Translate const &tr)
Definition sp-item.cpp:1967
SPDocument * document
Definition sp-object.h:188
const unsigned order
TODO: insert short description here.
@ SP_CLONE_COMPENSATION_UNMOVED
Definition enums.h:90
bool has_hidder_filter(SPObject const *item)
void graphlayout(std::vector< SPItem * > const &items)
Takes a list of inkscape items, extracts the graph defined by connectors between them,...
graph layout functions.
Dim2
2D axis enumeration (X or Y).
Definition coord.h:48
@ Y
Definition coord.h:48
@ X
Definition coord.h:48
Macro for icon names used in Inkscape.
SPItem * item
double atan2(Point const &p)
bool are_near(Affine const &a1, Affine const &a2, Coord eps=EPSILON)
Singleton class to access the preferences file in a convenient way.
void removeoverlap(std::vector< SPItem * > const &items, double const xGap, double const yGap)
Takes a list of inkscape items and moves them as little as possible such that rectangular bounding bo...
Remove overlaps function.
GList * items
TODO: insert short description here.
int sp_item_repr_compare_position(SPItem const *first, SPItem const *second)
Definition sp-item.cpp:1872
SPRoot: SVG <svg> implementation.
int delta
int index
SPDesktop * desktop
Inkscape::Text::Layout const * te_get_layout(SPItem const *item)
void unclump(std::vector< SPItem * > &items)
Unclumps the items in items, reducing local unevenness in their distribution.
Definition unclump.cpp:337
Unclumping objects.