Files
qelectrotech-source-mirror/sources/qetgraphicsitem/qetshapeitem.h
T

343 lines
16 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 QETSHAPEITEM_H
#define QETSHAPEITEM_H
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
#include "qetgraphicsitem.h"
#include "shapetransform.h"
#include <QPen>
#include <optional>
#include <utility>
class QDomElement;
class QDomDocument;
class QetGraphicsHandlerItem;
class QAction;
/**
@brief The QetShapeItem class
this class is used to draw a basic shape (line, rectangle, ellipse,
polygon or free-form path) into a diagram, that can be saved to a
.qet file. Beyond its local geometry, a shape may also carry a
rotation/skew/scale (see ShapeTransform) around an arbitrary pivot.
*/
class QetShapeItem : public QetGraphicsItem
{
Q_OBJECT
Q_PROPERTY(QPen pen READ pen WRITE setPen NOTIFY penChanged)
Q_PROPERTY(QBrush brush READ brush WRITE setBrush NOTIFY brushChanged)
Q_PROPERTY(QRectF rect READ rect WRITE setRect)
Q_PROPERTY(QLineF line READ line WRITE setLine)
Q_PROPERTY(QPolygonF polygon READ polygon WRITE setPolygon)
Q_PROPERTY(bool close READ isClosed WRITE setClosed NOTIFY closeChanged)
Q_PROPERTY(qreal xRadius READ XRadius WRITE setXRadius NOTIFY XRadiusChanged)
Q_PROPERTY(qreal yRadius READ YRadius WRITE setYRadius NOTIFY YRadiusChanged)
// One property per ShapeTransform scalar -- this is what lets a plain
// QPropertyUndoCommand(this, "rotation", oldValue, newValue) work for
// every handle, exactly like xRadius/yRadius already do for corner
// rounding.
Q_PROPERTY(qreal rotation READ rotation WRITE setRotation NOTIFY transformChanged)
Q_PROPERTY(qreal skewX READ skewX WRITE setSkewX NOTIFY transformChanged)
Q_PROPERTY(qreal skewY READ skewY WRITE setSkewY NOTIFY transformChanged)
Q_PROPERTY(qreal scaleFactorX READ scaleFactorX WRITE setScaleFactorX NOTIFY transformChanged)
Q_PROPERTY(qreal scaleFactorY READ scaleFactorY WRITE setScaleFactorY NOTIFY transformChanged)
Q_PROPERTY(QPointF pivot READ pivot WRITE setPivot NOTIFY transformChanged)
Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY arcChanged)
Q_PROPERTY(qreal endAngle READ endAngle WRITE setEndAngle NOTIFY arcChanged)
signals:
void penChanged();
void brushChanged();
void closeChanged();
void XRadiusChanged();
void YRadiusChanged();
void transformChanged();
void arcChanged();
void geometryChanged(); // P1/P2, polygon points, or path nodes changed -- lets the properties panel stay in sync while a handle is dragged, not just when it's typed into
public:
enum ShapeType {Line =1,
Rectangle =2,
Ellipse =4,
Polygon =8,
Path =16 };
Q_ENUM (ShapeType)
enum ArcClosure {NoClosure = 0, Chord = 1, Pie = 2};
Q_ENUM (ArcClosure)
// Point of a Path shape. Anchors are in the same local coordinate
// frame as an ordinary Polygon's points; handle offsets are stored
// *relative to the anchor*, so moving a node never has to rewrite
// its own handle coordinates.
enum class NodeKind {Corner, Smooth, Symmetric};
struct PathNode {
QPointF anchor;
NodeKind kind = NodeKind::Corner;
std::optional<QPointF> inHandle;
std::optional<QPointF> outHandle;
bool operator==(const PathNode &other) const {
return anchor == other.anchor && kind == other.kind
&& inHandle == other.inHandle && outHandle == other.outHandle;
}
};
// Orthogonal to ShapeType: which handle set is currently shown.
// Cycled by clicking an already-selected shape without dragging:
// Size -> Corner -> RotateSkew -> Size for Rectangle (the only
// type with a corner-radius concept); Size -> NodeEdit ->
// RotateSkew -> Size for Path (reveals control handles for
// every node that has any); Size -> RotateSkew -> Size for
// everything else. One unified click-cycle for every shape
// type, rather than a separate, less discoverable gesture
// (e.g. double-click) for any one shape's extra mode.
enum class HandleMode {Size, Corner, NodeEdit, RotateSkew};
// index conventions, deliberately matched to what already exists
// rather than invented fresh:
// Resize 0..7, same order as QetGraphicsHandlerUtility::pointsForRect
// (this is exactly today's Rectangle/Ellipse handle set --
// only the Ctrl/Shift dispatch around it is new)
// Rotate 0..3, corners: NW, NE, SE, SW
// SkewEdge 0..3, edges: N, E, S, W
// CornerRadius 0..1, same order as QetGraphicsHandlerUtility::pointForRadiusRect
// ArcEndpoint 0 = start angle, 1 = end angle
enum class HandleRole {
Resize, // Size mode
Rotate, SkewEdge, Pivot, // RotateSkew mode
CornerRadius, // Rectangle, always shown alongside Size handles
ArcEndpoint, // Ellipse, always shown
PathAnchor, PathControlIn, PathControlOut // Polygon/Path, node-edit mode (see setPathNodes())
};
enum { Type = UserType + 1008 };
QetShapeItem(
QPointF,
QPointF = QPointF(0,0),
ShapeType = Line,
QGraphicsItem *parent = nullptr);
~QetShapeItem() override;
//Enable the use of qgraphicsitem_cast to safely cast a
//QGraphicsItem into a QetShapeItem return the QGraphicsItem type
int type() const override { return Type; }
///METHODS
QPen pen() const {return m_pen;}
void setPen(const QPen &pen);
QBrush brush() const {return m_brush;}
void setBrush(const QBrush &brush);
ShapeType shapeType() const {return m_shapeType;}
virtual bool fromXml (const QDomElement &);
virtual QDomElement toXml (QDomDocument &document) const;
virtual bool toDXF (const QString &filepath,const QPen &pen);
void editProperty() override;
QString name() const override;
void setP2 (const QPointF &P2);
QLineF line() const{return QLineF(m_P1, m_P2);}
bool setLine (const QLineF &line);
QRectF rect() const{return QRectF(m_P1, m_P2);}
bool setRect (const QRectF &rect);
QPolygonF polygon() const {return m_polygon;}
bool setPolygon (const QPolygonF &polygon);
bool isClosed() const {return m_closed;}
void setClosed (bool close);
qreal XRadius() const {return m_xRadius;}
void setXRadius(qreal X);
qreal YRadius() const {return m_yRadius;}
void setYRadius(qreal Y);
//Transform: one accessor pair per ShapeTransform scalar (see
//the Q_PROPERTY block above for why they are not grouped into
//a single property).
const ShapeTransform &shapeTransform() const {return m_transform;}
qreal rotation() const {return m_transform.rotation;}
void setRotation(qreal degrees);
qreal skewX() const {return m_transform.skewX;}
void setSkewX(qreal degrees);
qreal skewY() const {return m_transform.skewY;}
void setSkewY(qreal degrees);
qreal scaleFactorX() const {return m_transform.scaleX;}
void setScaleFactorX(qreal factor);
qreal scaleFactorY() const {return m_transform.scaleY;}
void setScaleFactorY(qreal factor);
QPointF pivot() const {return m_transform.pivot;}
void setPivot(const QPointF &pivot); // moves the pivot handle: compensates pos() so the shape does not jump
void resetPivotToBoundingRectCenter();
void enableNodeEditMode(); // Path only: switches to NodeEdit mode, so every node's control handles become visible
//Arc: only meaningful when shapeType() == Ellipse. A full
//ellipse is just an arc with span 360 -- there is no separate
//Arc shape type.
qreal startAngle() const {return m_startAngle;}
void setStartAngle(qreal degrees);
qreal endAngle() const {return m_endAngle;}
void setEndAngle(qreal degrees);
qreal spanAngle() const {return m_endAngle - m_startAngle;}
bool isFullEllipse() const {return qFuzzyCompare(qAbs(spanAngle()), qreal(360));}
ArcClosure arcClosure() const {return m_arcClosure;}
void setArcClosure(ArcClosure closure);
//Path (Bezier): only meaningful when shapeType() == Path.
const QVector<PathNode> &pathNodes() const {return m_nodes;}
void setPathNodes(const QVector<PathNode> &nodes);
//Methods available for polygon shape
int pointsCount () const;
void setNextPoint (QPointF P);
void removePoints (int number = 1);
QRectF boundingRect() const override;
QPainterPath shape() const override;
protected:
void paint(
QPainter *painter,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
void hoverEnterEvent (QGraphicsSceneHoverEvent *event) override;
void hoverLeaveEvent (QGraphicsSceneHoverEvent *event) override;
void mousePressEvent (QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent (QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent (QGraphicsSceneMouseEvent *event) override;
QVariant itemChange(
GraphicsItemChange change,
const QVariant &value) override;
bool sceneEventFilter(
QGraphicsItem *watched,
QEvent *event) override;
void contextMenuEvent(
QGraphicsSceneContextMenuEvent *event) override;
private:
void toggleHandleMode();
HandleMode nextHandleMode() const; // what a click would switch to from here -- shared by toggleHandleMode() and the tooltip
void updateModeHint(); // keeps the tooltip in sync with nextHandleMode()
void refreshInteractionHints(); // updateModeHint(), plus an immediate re-show of tooltip/status bar if currently hovered
void showStatusHint(const QString &text) const;
void clearStatusHint() const;
QString currentModeStatusHint() const; // richer, one-line gesture/modifier reference for the status bar, shown when hovering the shape body
QString handleRoleTooltip(HandleRole role, int slot) const; // shown natively by Qt when hovering that specific handle, and pushed to the status bar too
static QString handleModeLabel(HandleMode mode);
static bool isResizeCornerSlot(int slot); // true for the 4 corner slots (of 8) in QetGraphicsHandlerUtility::pointsForRect's ordering
void rebuildHandles(); // (re)creates handler items -- only when the *set* of handles changes
void repositionHandles(); // moves existing handler items -- safe to call every frame of a live drag
void insertPoint();
void removePoint();
void convertToPathOrPolygon(); // context-menu action; see promoteRectangleOrEllipseToPolygon() for the separate Alt+drag mechanism
static QVector<PathNode> bezierNodesForArc(const QRectF &rect, qreal startAngleDeg, qreal spanAngleDeg);
static QVector<PathNode> bezierNodesForRoundedRect(const QRectF &rect, qreal xRadius, qreal yRadius);
void mirror(bool horizontal); // context-menu action: flips scaleFactorX (horizontal) or scaleFactorY (vertical) around the current pivot
void setNodeKind(int nodeIndex, NodeKind kind); // context-menu action on a Path node
void handlerMousePressEvent(int handlerIndex);
void handlerMouseMoveEvent(int handlerIndex, QGraphicsSceneMouseEvent *event);
void handlerMouseReleaseEvent(int handlerIndex);
// One dispatch function per handle role -- called from
// handlerMouseMoveEvent() with the mouse position already
// mapped to local coordinates and any grid snap applied.
// dragResize() covers both the plain and Ctrl/Shift/Alt-modified
// interpretations of the whole Size handle set.
void dragResize (int index, const QPointF &localPos, Qt::KeyboardModifiers mods);
void dragRotateHandle(int cornerIndex, const QPointF &scenePos, Qt::KeyboardModifiers mods);
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 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);
void mirrorOppositeHandle(PathNode &node, bool justChangedIsOut); // shared by dragPathControlHandle() and dragCurveSegment()
void dragCurveSegment(int segmentIndex, qreal t, const QPointF &localPos); // Inkscape-style "grab the curve itself"
void promoteRectangleOrEllipseToPolygon(int detachedResizeIndex, const QPointF &newLocalPos);
std::pair<int, qreal> nearestPathSegment(const QPointF &localPos) const; // {segmentIndex, t}, -1 if fewer than 2 nodes
void insertPathPoint(int segmentIndex, qreal t); // De Casteljau split -- see .cpp for why
void removePathPoint(int nodeIndex);
QDomElement snapshotXml() const; // helper for PromoteShapeCommand: toXml() into a throwaway document
QRectF localRect() const;
QPainterPath outline() const; // raw, unstroked path for the current type; shared by shape() and paint()
QVector<QPointF> currentHandlePositions() const; // in m_handleRoles/m_handleSlot order, local coordinates
QPointF handlePositionFor(HandleRole role, int slot) const;
QPointF rotateHandleReference(int slot) const; // the point a Rotate handle tracks, before rotation is applied
QPointF scaleOnlyOffset(const QPointF &localPoint) const; // (localPoint - pivot), scaled, in the pre-shear frame
QPointF scaleAndShearOffset(const QPointF &localPoint) const; // same, with current shear also applied -- the pre-rotation frame
static QColor colorForHandleRole(HandleRole role);
static QRectF lockAspectRatio(const QRectF &oldRect, QRectF newRect, int resizeIndex, bool mirrored);
static QPointF cornerPoint(const QRectF &rect, int cornerIndex); // 0=NW,1=NE,2=SE,3=SW
static QPointF edgeMidpoint(const QRectF &rect, int edgeIndex); // 0=N, 1=E, 2=S, 3=W
///ATTRIBUTES
private:
ShapeType m_shapeType;
QPen m_pen;
QBrush m_brush;
QPointF m_P1,
m_P2,
m_old_P1,
m_old_P2,
m_context_menu_pos;
QPolygonF m_polygon, m_old_polygon;
bool m_hovered;
int m_vector_index;
bool m_closed = false,
m_modifie_radius_equaly = false;
QVector<QetGraphicsHandlerItem *> m_handler_vector;
QAction *m_insert_point,
*m_remove_point;
qreal m_xRadius = 0,
m_yRadius = 0,
m_old_xRadius,
m_old_yRadius,
m_old_startAngle,
m_old_endAngle;
ShapeTransform m_transform;
ShapeTransform m_old_transform;
HandleMode m_handleMode = HandleMode::Size;
QVector<HandleRole> m_handleRoles; // parallel to m_handler_vector, one role per handle
QVector<int> m_handleSlot; // parallel to m_handler_vector, meaning depends on role (see HandleRole)
QPointF m_old_pos;
qreal m_startAngle = 0;
qreal m_endAngle = 360;
ArcClosure m_arcClosure = NoClosure;
bool m_pivotIsCustom = false; // false: pivot auto-follows the bounding-rect center on every geometry edit
bool m_deferHandleReposition = false; // true while setPivot() is applying its two related updates together
QVector<PathNode> m_nodes;
QVector<PathNode> m_old_nodes; // saved at handle press, for undo
int m_curveDragSegment = -1; // >=0 while dragging the curve itself (not a handle) between two nodes
qreal m_curveDragT = 0; // parameter along that segment where the drag started
QPointF m_curveDragOriginalP1, m_curveDragOriginalP2; // absolute control points at drag start, fixed for the whole drag
QPointF m_curveDragPressPos; // local position at press, to tell a plain click from a real drag
bool m_curveDragEngaged = false; // false until the drag actually exceeds a small threshold
};
#endif // QETSHAPEITEM_H