/* 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 . */ #ifndef QETSHAPEITEM_H #define QETSHAPEITEM_H #include "../QetGraphicsItemModeler/qetgraphicshandleritem.h" #include "qetgraphicsitem.h" #include "shapetransform.h" #include #include #include #include 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(); void uuidChanged(); // 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 inHandle; std::optional 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 // 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, Size mode, shown only on a half arc (see isAxisHalfArc()) 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; QUuid uuid() const {return m_uuid;} void setUuid(const QUuid &uuid) {m_uuid = uuid; emit uuidChanged();} void newUuid() {setUuid(QUuid::createUuid());} //create new uuid for this item 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));} 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); //Path (Bezier): only meaningful when shapeType() == Path. const QVector &pathNodes() const {return m_nodes;} void setPathNodes(const QVector &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; QRectF sceneOutlineRect() const; using QetGraphicsItem::setPos; void setPos(const QPointF &p) 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 bezierNodesForArc(const QRectF &rect, qreal startAngleDeg, qreal spanAngleDeg); static QVector 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 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); 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 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 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: QUuid m_uuid = QUuid::createUuid(); 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 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 m_handleRoles; // parallel to m_handler_vector, one role per handle QVector 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 m_nodes; QVector 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