Files
qelectrotech-source-mirror/sources/alignment.h
T
ispyisail 3c4d23bb4e Add Align left/centre/right/top/middle/bottom for the selected items
Edit > Aligner gains six commands, also in the selection's context menu
and the command search: Aligner à gauche, Centrer horizontalement,
Aligner à droite, Aligner en haut, Centrer verticalement, Aligner en bas.
One undo step; wires follow their symbols. Second stage of discussion
#1069, on top of "Aligner sur la grille".

Left/right/top/bottom line the edges up on the outermost one. The two
centre commands line the items up on the mean of their centres, and a
symbol's centre is its origin point, not the middle of its drawing: in
the collection, vertical two-terminal symbols almost always have their
terminals on the origin's axis, so this puts their wires on one line.
A picture is aligned by the picture itself, without its caption
(imageRect() becomes public for this).

A group (#1070) lines up as one piece: its edges are its members'
together, and every member moves by the same amount, so the group keeps
its shape; shapes inside a group come along.

Each item moves only across the line it is aligned on, and lands on the
grid its drag uses, so aligning never takes a symbol off the grid. Two
symbols whose edges sit at different distances from their origins
cannot both be exactly on the line and on the grid; they end up within
half a grid step of it.

The commands need two items (Aligner sur la grille still needs one).
Locked items stay put and the status bar says so. If nothing moves, no
undo step is pushed and the status bar says the selection is already
aligned as far as the grid allows. The geometry is in alignment.h,
tested by tst_alignment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 07:52:17 +13:00

151 lines
4.7 KiB
C++

/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech 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.
QElectroTech 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.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef ALIGNMENT_H
#define ALIGNMENT_H
#include "textgrid.h"
#include <QList>
#include <QPointF>
#include <QRectF>
#include <algorithm>
/**
The geometry behind the align commands, kept free of any scene so it
can be tested on its own.
*/
namespace Alignment
{
/**
What the items of a selection are lined up on.
*/
enum Edge {
Left, ///< left edges, on the left-most one
HCenter, ///< reference points, on the mean of their x
Right, ///< right edges, on the right-most one
Top, ///< top edges, on the top-most one
VCenter, ///< reference points, on the mean of their y
Bottom ///< bottom edges, on the bottom-most one
};
/**
One item to align: its edges, and the point that counts as its
centre. For a symbol that point is its origin, where its wires
usually leave, not the middle of its drawn shape.
*/
struct Item {
QRectF edges;
QPointF ref;
};
/**
@return true if aligning on @a edge moves items along x
*/
inline bool isHorizontal(Edge edge)
{
return edge == Left || edge == HCenter || edge == Right;
}
/**
@return the movement of each of @a items that lines them up on
@a edge, in the same order. Items only move across the line they
are aligned on: aligning left never moves anything up or down.
*/
inline QList<QPointF> alignOffsets(const QList<Item> &items, Edge edge)
{
QList<qreal> values;
for (const Item &item : items)
{
switch (edge) {
case Left: values << item.edges.left(); break;
case HCenter: values << item.ref.x(); break;
case Right: values << item.edges.right(); break;
case Top: values << item.edges.top(); break;
case VCenter: values << item.ref.y(); break;
case Bottom: values << item.edges.bottom(); break;
}
}
QList<QPointF> offsets;
if (values.isEmpty())
return offsets;
qreal target = 0;
switch (edge) {
case Left:
case Top:
target = *std::min_element(values.cbegin(), values.cend());
break;
case Right:
case Bottom:
target = *std::max_element(values.cbegin(), values.cend());
break;
case HCenter:
case VCenter:
for (qreal v : std::as_const(values))
target += v;
target /= values.size();
break;
}
for (qreal v : std::as_const(values))
offsets << (isHorizontal(edge) ? QPointF(target - v, 0)
: QPointF(0, target - v));
return offsets;
}
/**
@return the movement that puts p on a grid of x_grid by y_grid,
divided by divisor as TextGrid::snap() does, or a null point when
p is already on it. Unlike Diagram::snapToGrid(), this never looks
at the keyboard: a command run from a shortcut with Ctrl in it must
not quietly round to the pixel instead.
Less than a millionth of a pixel counts as on the grid: positions
that went through arithmetic carry residues of that size, and
qFuzzyIsNull() (1e-12) is too strict to absorb them.
*/
inline QPointF gridOffset(const QPointF &p, int x_grid, int y_grid, qreal divisor = 1)
{
const QPointF offset = TextGrid::snap(p, x_grid, y_grid, divisor) - p;
auto clean = [](qreal v) { return qAbs(v) < 1e-6 ? 0.0 : v; };
return QPointF(clean(offset.x()), clean(offset.y()));
}
/**
@return @a offset, the movement alignOffsets() gave an item at
@a pos, rounded so the item lands on the grid along the line it is
aligned on. The other coordinate is left as it is, even off the
grid: aligning left must not also move items up or down.
Edges of items whose width is not a whole number of grid steps
apart from their origin cannot all land on one line; they end up as
close to it as the grid allows.
*/
inline QPointF snappedOffset(const QPointF &pos, const QPointF &offset, Edge edge,
int x_grid, int y_grid, qreal divisor = 1)
{
const QPointF snap = gridOffset(pos + offset, x_grid, y_grid, divisor);
QPointF result = offset + (isHorizontal(edge) ? QPointF(snap.x(), 0)
: QPointF(0, snap.y()));
auto clean = [](qreal v) { return qAbs(v) < 1e-6 ? 0.0 : v; };
return QPointF(clean(result.x()), clean(result.y()));
}
}
#endif // ALIGNMENT_H