mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 23:54:13 +02:00
Add an Arc tool and a handle to pull a half arc in or out (#408)
The diagram toolbar gets an "Ajouter un arc" button next to the ellipse. Click one end, then click the other end at the height the arc should reach: the result is a half arc bulging up or down from the line between the two clicks (Shift: a true half circle). It is not filled, since a fill would close it into a half disc. A selected half arc shows one more handle, in the middle of the curve. Dragging it makes the arc deeper or flatter while both ends stay put; dragging it across the line between the ends turns the arc over. The handle is hidden whenever the arc is not a half arc on a horizontal or vertical diameter, because only then is "keep the ends, move the middle" one well-defined change. Nothing new is stored: an arc stays an Ellipse with a start and end angle, exactly as the existing arc handles of an ellipse save it, so files read by older versions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,10 +34,14 @@
|
||||
Default constructor
|
||||
@param diagram : the diagram where this event must operate
|
||||
@param shape_type : the type of shape to draw
|
||||
@param half_arc : with shape_type Ellipse, draw a half arc instead of
|
||||
a whole ellipse (the Arc tool). It is still an Ellipse with a start
|
||||
and end angle, the same shape the arc handles of an ellipse produce.
|
||||
*/
|
||||
DiagramEventAddShape::DiagramEventAddShape(Diagram *diagram, QetShapeItem::ShapeType shape_type) :
|
||||
DiagramEventAddShape::DiagramEventAddShape(Diagram *diagram, QetShapeItem::ShapeType shape_type, bool half_arc) :
|
||||
DiagramEventInterface(diagram),
|
||||
m_shape_type (shape_type),
|
||||
m_half_arc (half_arc && shape_type == QetShapeItem::Ellipse),
|
||||
m_shape_item (nullptr),
|
||||
m_help_horiz (nullptr),
|
||||
m_help_verti (nullptr)
|
||||
@@ -131,6 +135,23 @@ void DiagramEventAddShape::applyPosition(const QPointF &pos, Qt::KeyboardModifie
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_half_arc)
|
||||
{
|
||||
// The first click is one end of the arc and sets the height of
|
||||
// its chord; the cursor sets the other end and how far the arc
|
||||
// bulges, up or down, from that chord. Shift makes it a true
|
||||
// half circle.
|
||||
const qreal width = qAbs(pos.x() - m_anchor_point.x());
|
||||
qreal height = pos.y() - m_anchor_point.y();
|
||||
if (mods & Qt::ShiftModifier)
|
||||
height = (height < 0 ? -width : width) / 2;
|
||||
const qreal depth = qAbs(height);
|
||||
m_shape_item->setRect(QRectF(qMin(pos.x(), m_anchor_point.x()), m_anchor_point.y() - depth,
|
||||
width, 2 * depth));
|
||||
m_shape_item->setEndAngle(height <= 0 ? 180 : -180);
|
||||
return;
|
||||
}
|
||||
|
||||
m_center_anchored = (mods & Qt::ControlModifier)
|
||||
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse);
|
||||
if (m_center_anchored)
|
||||
@@ -227,15 +248,20 @@ void DiagramEventAddShape::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
||||
// live on every subsequent move, from whatever Ctrl state is
|
||||
// held at the time.
|
||||
m_center_anchored = (event->modifiers() & Qt::ControlModifier)
|
||||
&& !m_half_arc
|
||||
&& (m_shape_type == QetShapeItem::Rectangle || m_shape_type == QetShapeItem::Ellipse);
|
||||
if (m_center_anchored)
|
||||
showCenterMarker(m_anchor_point);
|
||||
if (m_half_arc)
|
||||
m_shape_item->setEndAngle(180);
|
||||
//Start from whatever pen/brush was last applied this
|
||||
//session, rather than always the hardcoded default.
|
||||
//Not the brush for an arc: it is a line, and a fill
|
||||
//would close it into a half disc.
|
||||
if (LastUsedStyle::hasShapePen()) {
|
||||
m_shape_item->setPen(LastUsedStyle::shapePen());
|
||||
}
|
||||
if (LastUsedStyle::hasShapeBrush()) {
|
||||
if (LastUsedStyle::hasShapeBrush() && !m_half_arc) {
|
||||
m_shape_item->setBrush(LastUsedStyle::shapeBrush());
|
||||
}
|
||||
m_diagram->addItem (m_shape_item);
|
||||
@@ -461,6 +487,8 @@ QString DiagramEventAddShape::beforeClickHint() const
|
||||
return tr("Clic gauche : positionner le point de départ (Ctrl = position libre)");
|
||||
case QetShapeItem::Rectangle:
|
||||
case QetShapeItem::Ellipse:
|
||||
if (m_half_arc)
|
||||
return tr("Clic gauche : positionner une extrémité de l'arc (Ctrl = position libre)");
|
||||
return tr("Clic gauche : positionner le premier coin (Ctrl = point central, position libre)");
|
||||
case QetShapeItem::Polygon:
|
||||
return tr("Clic gauche : positionner le premier point (Ctrl = position libre)");
|
||||
@@ -479,6 +507,9 @@ QString DiagramEventAddShape::afterClickHint() const
|
||||
return tr("Clic gauche : positionner le coin opposé (Maj = carré, "
|
||||
"Ctrl = depuis le centre + position libre, Ctrl+Maj = carré centré) ; clic droit : annuler");
|
||||
case QetShapeItem::Ellipse:
|
||||
if (m_half_arc)
|
||||
return tr("Clic gauche : positionner l'autre extrémité et la hauteur de l'arc "
|
||||
"(Maj = demi-cercle, Ctrl = position libre) ; clic droit : annuler");
|
||||
return tr("Clic gauche : positionner le coin opposé (Maj = cercle, "
|
||||
"Ctrl = depuis le centre + position libre, Ctrl+Maj = cercle centré) ; clic droit : annuler");
|
||||
case QetShapeItem::Polygon:
|
||||
|
||||
@@ -32,7 +32,7 @@ class DiagramEventAddShape : public DiagramEventInterface
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DiagramEventAddShape(Diagram *diagram, QetShapeItem::ShapeType shape_type);
|
||||
DiagramEventAddShape(Diagram *diagram, QetShapeItem::ShapeType shape_type, bool half_arc = false);
|
||||
|
||||
~DiagramEventAddShape() override;
|
||||
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
|
||||
@@ -55,6 +55,7 @@ class DiagramEventAddShape : public DiagramEventInterface
|
||||
|
||||
protected:
|
||||
QetShapeItem::ShapeType m_shape_type;
|
||||
bool m_half_arc = false; // Ellipse only: draw a half arc whose chord is the first click's height (the Arc tool)
|
||||
QetShapeItem *m_shape_item;
|
||||
QGraphicsLineItem *m_help_horiz, *m_help_verti;
|
||||
QPointF m_anchor_point; // the shape's first-click point -- meaningful once m_shape_item exists
|
||||
|
||||
@@ -1048,6 +1048,7 @@ void QETDiagramEditor::setUpActions()
|
||||
QAction *add_line = m_add_item_actions_group.addAction(QET::Icons::PartLine, tr("Ajouter une ligne", "Draw line"));
|
||||
QAction *add_rectangle = m_add_item_actions_group.addAction(QET::Icons::PartRectangle, tr("Ajouter un rectangle"));
|
||||
QAction *add_ellipse = m_add_item_actions_group.addAction(QET::Icons::PartEllipse, tr("Ajouter une ellipse"));
|
||||
QAction *add_arc = m_add_item_actions_group.addAction(QET::Icons::PartArc, tr("Ajouter un arc"));
|
||||
QAction *add_polyline = m_add_item_actions_group.addAction(QET::Icons::PartPolygon, tr("Ajouter une polyligne"));
|
||||
QAction *add_path = m_add_item_actions_group.addAction(QET::Icons::PartBezier, tr("Ajouter une courbe"));
|
||||
QAction *add_terminal_strip = m_add_item_actions_group.addAction(QET::Icons::TerminalStrip, tr("Ajouter un plan de bornes"));
|
||||
@@ -1060,6 +1061,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_line ->setStatusTip(tr("Ajoute une ligne sur le folio actuel"));
|
||||
add_rectangle->setStatusTip(tr("Ajoute un rectangle sur le folio actuel"));
|
||||
add_ellipse ->setStatusTip(tr("Ajoute une ellipse sur le folio actuel"));
|
||||
add_arc ->setStatusTip(tr("Ajoute un arc sur le folio actuel"));
|
||||
add_polyline ->setStatusTip(tr("Ajoute une polyligne sur le folio actuel"));
|
||||
add_path ->setStatusTip(tr("Ajoute une courbe de Bézier sur le folio actuel"));
|
||||
add_terminal_strip->setStatusTip(tr("Ajoute un plan de bornier sur le folio actuel"));
|
||||
@@ -1072,6 +1074,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_line ->setData(QStringLiteral("line"));
|
||||
add_rectangle->setData(QStringLiteral("rectangle"));
|
||||
add_ellipse ->setData(QStringLiteral("ellipse"));
|
||||
add_arc ->setData(QStringLiteral("arc"));
|
||||
add_polyline ->setData(QStringLiteral("polyline"));
|
||||
add_path ->setData(QStringLiteral("path"));
|
||||
add_terminal_strip->setData(QStringLiteral("terminal_strip"));
|
||||
@@ -1080,6 +1083,7 @@ void QETDiagramEditor::setUpActions()
|
||||
add_line->setCheckable(true);
|
||||
add_rectangle->setCheckable(true);
|
||||
add_ellipse->setCheckable(true);
|
||||
add_arc->setCheckable(true);
|
||||
add_polyline->setCheckable(true);
|
||||
add_path->setCheckable(true);
|
||||
|
||||
@@ -2024,6 +2028,8 @@ void QETDiagramEditor::addItemGroupTriggered(QAction *action)
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Rectangle);
|
||||
else if (value == "ellipse")
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Ellipse);
|
||||
else if (value == "arc")
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Ellipse, true);
|
||||
else if (value == "polyline")
|
||||
diagram_event = new DiagramEventAddShape (d, QetShapeItem::Polygon);
|
||||
else if (value == "path")
|
||||
|
||||
@@ -398,6 +398,21 @@ void QetShapeItem::setEndAngle(qreal degrees)
|
||||
emit arcChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetShapeItem::isAxisHalfArc
|
||||
True for an Ellipse drawn as a half arc whose two ends lie on a
|
||||
horizontal or vertical diameter -- what the Arc tool draws. Only
|
||||
then does the bulge handle have one obvious meaning: move the middle
|
||||
of the arc toward or away from the line between its ends, with both
|
||||
ends staying put.
|
||||
*/
|
||||
bool QetShapeItem::isAxisHalfArc() const
|
||||
{
|
||||
if (m_shapeType != Ellipse || !qFuzzyCompare(qAbs(spanAngle()), qreal(180)))
|
||||
return false;
|
||||
return qFuzzyIsNull(std::remainder(m_startAngle, 90.0));
|
||||
}
|
||||
|
||||
void QetShapeItem::setArcClosure(ArcClosure closure)
|
||||
{
|
||||
if (m_arcClosure == closure) return;
|
||||
@@ -1435,6 +1450,8 @@ QString QetShapeItem::handleRoleTooltip(HandleRole role, int slot) const
|
||||
return tr("Glisser : arrondir les coins (Ctrl = position libre)");
|
||||
case HandleRole::ArcEndpoint:
|
||||
return tr("Glisser : ajuster l'arc (Ctrl = position libre, Maj = 15°)");
|
||||
case HandleRole::ArcBulge:
|
||||
return tr("Glisser : creuser ou aplatir l'arc, ses extrémités restent en place (Ctrl = position libre)");
|
||||
case HandleRole::PathAnchor:
|
||||
{
|
||||
QString text = tr("Glisser : déplacer le point (Ctrl = position libre");
|
||||
@@ -1469,6 +1486,7 @@ QColor QetShapeItem::colorForHandleRole(HandleRole role)
|
||||
case HandleRole::Pivot: return Qt::red;
|
||||
case HandleRole::CornerRadius: return Qt::magenta;
|
||||
case HandleRole::ArcEndpoint: return Qt::darkCyan;
|
||||
case HandleRole::ArcBulge: return Qt::darkCyan;
|
||||
case HandleRole::PathAnchor: return Qt::blue;
|
||||
case HandleRole::PathControlIn:
|
||||
case HandleRole::PathControlOut:return Qt::gray;
|
||||
@@ -1543,6 +1561,9 @@ QPointF QetShapeItem::handlePositionFor(HandleRole role, int slot) const
|
||||
case HandleRole::ArcEndpoint:
|
||||
return QetGraphicsHandlerUtility::pointsForArc(r, m_startAngle, spanAngle()).value(slot);
|
||||
|
||||
case HandleRole::ArcBulge:
|
||||
return QetGraphicsHandlerUtility::pointsForArc(r, m_startAngle + spanAngle() / 2, 0).value(0);
|
||||
|
||||
case HandleRole::PathAnchor:
|
||||
if (m_shapeType == Polygon)
|
||||
return m_polygon.value(slot);
|
||||
@@ -1653,7 +1674,14 @@ void QetShapeItem::rebuildHandles()
|
||||
addRole(HandleRole::Pivot, 0);
|
||||
}
|
||||
if (m_shapeType == Ellipse)
|
||||
{
|
||||
for (int i = 0; i < 2; ++i) addRole(HandleRole::ArcEndpoint, i);
|
||||
// Always in the set, hidden unless the shape is a half
|
||||
// arc (updateArcBulgeVisibility()): an endpoint drag can
|
||||
// make or unmake a half arc mid-drag, and changing the
|
||||
// handle set then would delete the handle being dragged.
|
||||
addRole(HandleRole::ArcBulge, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case Polygon:
|
||||
@@ -1715,6 +1743,14 @@ void QetShapeItem::rebuildHandles()
|
||||
scene()->addItem(h);
|
||||
h->installSceneEventFilter(this);
|
||||
}
|
||||
updateArcBulgeVisibility();
|
||||
}
|
||||
|
||||
void QetShapeItem::updateArcBulgeVisibility()
|
||||
{
|
||||
const int index = m_handleRoles.indexOf(HandleRole::ArcBulge);
|
||||
if (index >= 0 && index < m_handler_vector.size())
|
||||
m_handler_vector.at(index)->setVisible(isAxisHalfArc());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1741,6 +1777,7 @@ void QetShapeItem::repositionHandles()
|
||||
const QVector<QPointF> scenePositions = mapToScene(positions);
|
||||
for (int i = 0; i < scenePositions.size(); ++i)
|
||||
m_handler_vector.at(i)->setPos(scenePositions.at(i));
|
||||
updateArcBulgeVisibility();
|
||||
}
|
||||
|
||||
void QetShapeItem::insertPoint()
|
||||
@@ -2527,6 +2564,40 @@ void QetShapeItem::dragArcEndpoint(int which, const QPointF &localPos, Qt::Keybo
|
||||
which == 0 ? setStartAngle(angle) : setEndAngle(angle);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief QetShapeItem::dragArcBulge
|
||||
Pulls the middle of a half arc in or out while its two ends stay
|
||||
where they are: only the half-axis across the line between the ends
|
||||
changes. Dragging across that line turns the arc over to the other
|
||||
side, which flips the sign of the span by changing the end angle
|
||||
alone -- changing both angles one after the other would pass through
|
||||
a zero span, which setStartAngle()/setEndAngle() snap to a full
|
||||
ellipse.
|
||||
*/
|
||||
void QetShapeItem::dragArcBulge(const QPointF &localPos)
|
||||
{
|
||||
if (!isAxisHalfArc())
|
||||
return;
|
||||
|
||||
const QRectF r = localRect();
|
||||
const QPointF c = r.center();
|
||||
const bool horizontalChord = qFuzzyIsNull(std::remainder(m_startAngle, 180.0));
|
||||
// > 0: the cursor is above (horizontal chord) or left of
|
||||
// (vertical chord) the line between the two ends
|
||||
const qreal side = horizontalChord ? c.y() - localPos.y() : c.x() - localPos.x();
|
||||
const qreal depth = qMax(qAbs(side), qreal(1));
|
||||
|
||||
if (horizontalChord)
|
||||
setRect(QRectF(r.left(), c.y() - depth, r.width(), 2 * depth));
|
||||
else
|
||||
setRect(QRectF(c.x() - depth, r.top(), 2 * depth, r.height()));
|
||||
|
||||
const QPointF middle = handlePositionFor(HandleRole::ArcBulge, 0);
|
||||
const qreal middleSide = horizontalChord ? c.y() - middle.y() : c.x() - middle.x();
|
||||
if ((middleSide > 0) != (side > 0))
|
||||
setEndAngle(2 * m_startAngle - m_endAngle);
|
||||
}
|
||||
|
||||
void QetShapeItem::dragCornerRadius(int which, const QPointF &localPos)
|
||||
{
|
||||
const qreal radius = QetGraphicsHandlerUtility::radiusForPosAtIndex(localRect(), localPos, which);
|
||||
@@ -2755,6 +2826,7 @@ void QetShapeItem::handlerMouseMoveEvent(int handlerIndex, QGraphicsSceneMouseEv
|
||||
case HandleRole::Pivot: dragPivotHandle(new_pos); break;
|
||||
case HandleRole::CornerRadius: dragCornerRadius(slot, new_pos); break;
|
||||
case HandleRole::ArcEndpoint: dragArcEndpoint(slot, new_pos, mods); break;
|
||||
case HandleRole::ArcBulge: dragArcBulge(new_pos); break;
|
||||
case HandleRole::PathAnchor: dragPathAnchor(slot, new_pos, mods); break;
|
||||
case HandleRole::PathControlIn: dragPathControlHandle(false, slot, new_pos, mods); break;
|
||||
case HandleRole::PathControlOut: dragPathControlHandle(true, slot, new_pos, mods); break;
|
||||
@@ -2852,6 +2924,16 @@ void QetShapeItem::handlerMouseReleaseEvent(int handlerIndex)
|
||||
undo->setText(tr("Modifier l'angle d'un arc"));
|
||||
break;
|
||||
|
||||
case HandleRole::ArcBulge:
|
||||
if (m_P1 != m_old_P1 || m_P2 != m_old_P2 || !qFuzzyCompare(m_endAngle, m_old_endAngle))
|
||||
{
|
||||
undo = new QUndoCommand(tr("Modifier la courbure d'un arc"));
|
||||
new QPropertyUndoCommand(this, "rect", QRectF(m_old_P1, m_old_P2), QRectF(m_P1, m_P2).normalized(), undo);
|
||||
if (!qFuzzyCompare(m_endAngle, m_old_endAngle))
|
||||
new QPropertyUndoCommand(this, "endAngle", m_old_endAngle, m_endAngle, undo);
|
||||
}
|
||||
break;
|
||||
|
||||
case HandleRole::PathAnchor:
|
||||
if (m_shapeType == Polygon && m_polygon != m_old_polygon)
|
||||
{
|
||||
|
||||
@@ -125,11 +125,13 @@ class QetShapeItem : public QetGraphicsItem
|
||||
// SkewEdge 0..3, edges: N, E, S, W
|
||||
// CornerRadius 0..1, same order as QetGraphicsHandlerUtility::pointForRadiusRect
|
||||
// ArcEndpoint 0 = start angle, 1 = end angle
|
||||
// ArcBulge 0 only: the middle of a half arc
|
||||
enum class HandleRole {
|
||||
Resize, // Size mode
|
||||
Rotate, SkewEdge, Pivot, // RotateSkew mode
|
||||
CornerRadius, // Rectangle, always shown alongside Size handles
|
||||
ArcEndpoint, // Ellipse, always shown
|
||||
ArcBulge, // Ellipse, shown only on a half arc (see isAxisHalfArc())
|
||||
PathAnchor, PathControlIn, PathControlOut // Polygon/Path, node-edit mode (see setPathNodes())
|
||||
};
|
||||
|
||||
@@ -205,6 +207,7 @@ class QetShapeItem : public QetGraphicsItem
|
||||
void setEndAngle(qreal degrees);
|
||||
qreal spanAngle() const {return m_endAngle - m_startAngle;}
|
||||
bool isFullEllipse() const {return qFuzzyCompare(qAbs(spanAngle()), qreal(360));}
|
||||
bool isAxisHalfArc() const; // half arc whose two ends lie on a horizontal or vertical diameter
|
||||
ArcClosure arcClosure() const {return m_arcClosure;}
|
||||
void setArcClosure(ArcClosure closure);
|
||||
|
||||
@@ -278,6 +281,8 @@ class QetShapeItem : public QetGraphicsItem
|
||||
void dragSkewHandle (int edgeIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
|
||||
void dragPivotHandle (const QPointF &localPos);
|
||||
void dragArcEndpoint (int which, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragArcBulge (const QPointF &localPos);
|
||||
void updateArcBulgeVisibility();
|
||||
void dragCornerRadius(int which, const QPointF &localPos);
|
||||
void dragPathAnchor (int which, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
void dragPathControlHandle(bool isOutHandle, int nodeIndex, const QPointF &localPos, Qt::KeyboardModifiers mods);
|
||||
|
||||
Reference in New Issue
Block a user