mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 21:34:12 +02:00
91804d5f5e
A group whose only unlocked member is a shape (its other members locked) kept a centre of (0,0), so Centrer horizontalement and Centrer verticalement pulled every item toward the folio origin and the shape itself landed off the line. The rule that turns a group's members into one item now lives in Alignment::combined(), where every member, shapes included, brings its own centre, and tst_alignment covers it. Also comments why unitFor()'s returned reference is safe, as asked in the review of #1087. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG
169 lines
5.2 KiB
C++
169 lines
5.2 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 @a items taken as one piece, the way a group lines up:
|
|
their edges together, and the middle of that box as its centre.
|
|
A single item keeps its own centre.
|
|
*/
|
|
inline Item combined(const QList<Item> &items)
|
|
{
|
|
Item result;
|
|
for (const Item &item : items)
|
|
result.edges = result.edges.isNull() ? item.edges
|
|
: result.edges.united(item.edges);
|
|
if (items.size() == 1)
|
|
result.ref = items.first().ref;
|
|
else
|
|
result.ref = result.edges.center();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
@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
|