diff --git a/sources/editor/customelementpart.cpp b/sources/editor/customelementpart.cpp index b357d967d..1917a3de9 100644 --- a/sources/editor/customelementpart.cpp +++ b/sources/editor/customelementpart.cpp @@ -59,6 +59,17 @@ void CustomElementPart::setDecorator(ElementPrimitiveDecorator *decorator) { Q_UNUSED(decorator) } +/** + This method is called by the decorator when it needs to determine the best + way to interactively scale a primitive. It is typically called when only a + single primitive is being scaled. + The default implementation systematically returns + QET::SnapScalingPointToGrid +*/ +QET::ScalingMethod CustomElementPart::preferredScalingMethod() const { + return(QET::SnapScalingPointToGrid); +} + /** This method is called by the decorator when it manages only a single primitive and it received a mouse press event. diff --git a/sources/editor/customelementpart.h b/sources/editor/customelementpart.h index 9de3bd4ea..37ee4425d 100644 --- a/sources/editor/customelementpart.h +++ b/sources/editor/customelementpart.h @@ -20,6 +20,7 @@ #include #include #include +#include "qet.h" class CustomElement; class ElementPrimitiveDecorator; class ElementScene; @@ -101,6 +102,7 @@ class CustomElementPart { virtual QGraphicsItem *toItem(); virtual void setDecorator(ElementPrimitiveDecorator *); + virtual QET::ScalingMethod preferredScalingMethod() const; virtual bool singleItemPressEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); virtual bool singleItemMoveEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); virtual bool singleItemReleaseEvent(ElementPrimitiveDecorator *, QGraphicsSceneMouseEvent *); diff --git a/sources/editor/elementprimitivedecorator.cpp b/sources/editor/elementprimitivedecorator.cpp index 384e80040..81ba08491 100644 --- a/sources/editor/elementprimitivedecorator.cpp +++ b/sources/editor/elementprimitivedecorator.cpp @@ -245,15 +245,30 @@ void ElementPrimitiveDecorator::mouseMoveEvent(QGraphicsSceneMouseEvent *event) QPointF scene_pos = event -> scenePos(); QPointF movement = scene_pos - latest_pos_; - // snap the movement to a non-visible grid when scaling selection - if (mustSnapToGrid(event)) { - // We now want some point belonging to the selection to be snapped to this rounded position - if (current_operation_square_ > QET::NoOperation) { + if (current_operation_square_ > QET::NoOperation) { + // This is a scaling operation. + + // For convenience purposes, we may need to adjust mouse movements. + QET::ScalingMethod scaling_method = scalingMethod(event); + if (scaling_method > QET::FreeScaling) { // real, non-rounded movement from the mouse press event QPointF global_movement = scene_pos - first_pos_; - // real, rounded movement from the mouse press event - QPointF rounded_global_movement = snapConstPointToGrid(global_movement); + QPointF rounded_global_movement; + if (scaling_method == QET::SnapScalingPointToGrid) { + // real, rounded movement from the mouse press event + rounded_global_movement = snapConstPointToGrid(global_movement); + } + else { + QRectF new_bounding_rect = original_bounding_rect_; + applyMovementToRect(current_operation_square_, global_movement, new_bounding_rect); + + const qreal scale_epsilon = 20.0; // rounds to 0.05 + QPointF delta = deltaForRoundScaling(original_bounding_rect_, new_bounding_rect, scale_epsilon); + + // real, rounded movement from the mouse press event + rounded_global_movement = global_movement + delta; + } // rounded position of the current mouse move event QPointF rounded_scene_pos = first_pos_ + rounded_global_movement; @@ -262,18 +277,22 @@ void ElementPrimitiveDecorator::mouseMoveEvent(QGraphicsSceneMouseEvent *event) QPointF current_position = mapToScene(rects.at(current_operation_square_).center()); // determine the final, effective movement movement = rounded_scene_pos - current_position; - } else if (current_operation_square_ == QET::MoveArea) { - // when moving the selection, consider the position of the first selected item - QPointF current_position = scene_pos - mouse_offset_; - QPointF rounded_current_position = snapConstPointToGrid(current_position); - movement = rounded_current_position - decorated_items_.at(0) -> toItem() -> scenePos(); - } else { - if (CustomElementPart *single_item = singleItem()) { - bool event_accepted = single_item -> singleItemMoveEvent(this, event); - if (event_accepted) { - event -> ignore(); - return; - } + } + } + else if (current_operation_square_ == QET::MoveArea) { + // When moving the selection, consider the position of the first selected item + QPointF current_position = scene_pos - mouse_offset_; + QPointF rounded_current_position = snapConstPointToGrid(current_position); + movement = rounded_current_position - decorated_items_.at(0) -> toItem() -> scenePos(); + } + else { + // Neither a movement nor a scaling operation -- perhaps the underlying item + // is interested in the mouse event for custom operations? + if (CustomElementPart *single_item = singleItem()) { + bool event_accepted = single_item -> singleItemMoveEvent(this, event); + if (event_accepted) { + event -> ignore(); + return; } } } @@ -627,6 +646,30 @@ int ElementPrimitiveDecorator::resizingSquareAtPos(const QPointF &position) { return(current_square); } +/** + Receive two rects, assuming they share a common corner and current is a \a + scaled version of \a original. + Calculate the scale ratios implied by this assumption, round them to the + nearest multiple of \a epsilon, then return the horizontal and vertical + offsets to be applied in order to pass from \a current to \a original scaled + by the rounded factors. + This method can be used to adjust a mouse movement so that it inputs a + round scaling operation. +*/ +QPointF ElementPrimitiveDecorator::deltaForRoundScaling(const QRectF &original, const QRectF ¤t, qreal epsilon) { + qreal sx = current.width() / original.width(); + qreal sy = current.height() / original.height(); + + qreal sx_rounded = QET::round(sx, epsilon); + qreal sy_rounded = QET::round(sy, epsilon); + + QPointF delta( + original.width() * (sx_rounded - sx), + original.height() * (sy_rounded - sy) + ); + return(delta); +} + /** Round the coordinates of \a point so it is snapped to the grid defined by the grid_step_x_ and grid_step_y_ attributes. @@ -656,3 +699,19 @@ void ElementPrimitiveDecorator::snapPointToGrid(QPointF &point) const { bool ElementPrimitiveDecorator::mustSnapToGrid(QGraphicsSceneMouseEvent *event) { return(!(event -> modifiers() & Qt::ControlModifier)); } + +/** + @param event Mouse event during the scale operations -- simply passed to mustSnapToGrid() + @return the scaling method to be used for the currently decorated items. + @see QET::ScalingMethod + @see mustSnapToGrid() +*/ +QET::ScalingMethod ElementPrimitiveDecorator::scalingMethod(QGraphicsSceneMouseEvent *event) { + if (event && !mustSnapToGrid(event)) { + return(QET::FreeScaling); + } + if (CustomElementPart *single_item = singleItem()) { + return single_item -> preferredScalingMethod(); + } + return QET::RoundScaleRatios; +} diff --git a/sources/editor/elementprimitivedecorator.h b/sources/editor/elementprimitivedecorator.h index c000ac362..6146492a8 100644 --- a/sources/editor/elementprimitivedecorator.h +++ b/sources/editor/elementprimitivedecorator.h @@ -1,6 +1,7 @@ #ifndef ELEMENTPRIMITIVEDECORATOR_H #define ELEMENTPRIMITIVEDECORATOR_H #include +#include "qet.h" class ElementEditionCommand; class ElementScene; class CustomElementPart; @@ -49,9 +50,11 @@ class ElementPrimitiveDecorator : public QGraphicsObject { void mouseReleaseEvent(QGraphicsSceneMouseEvent *); void keyPressEvent(QKeyEvent *); void keyReleaseEvent(QKeyEvent *); + QPointF deltaForRoundScaling(const QRectF &, const QRectF &, qreal); QPointF snapConstPointToGrid(const QPointF &) const; void snapPointToGrid(QPointF &) const; bool mustSnapToGrid(QGraphicsSceneMouseEvent *); + QET::ScalingMethod scalingMethod(QGraphicsSceneMouseEvent *); private: void init(); diff --git a/sources/editor/partpolygon.cpp b/sources/editor/partpolygon.cpp index ae6e163bd..90de2c5eb 100644 --- a/sources/editor/partpolygon.cpp +++ b/sources/editor/partpolygon.cpp @@ -190,6 +190,17 @@ void PartPolygon::handleUserTransformation(const QRectF &initial_selection_rect, setPolygon(mapFromScene(QPolygonF(mapped_points.toVector()))); } +/** + @reimp CustomElementPart::preferredScalingMethod + This method is called by the decorator when it needs to determine the best + way to interactively scale a primitive. It is typically called when only a + single primitive is being scaled. + This reimplementation systematically returns QET::RoundScaleRatios. +*/ +QET::ScalingMethod PartPolygon::preferredScalingMethod() const { + return(QET::RoundScaleRatios); +} + /** @return le rectangle delimitant cette partie. */ diff --git a/sources/editor/partpolygon.h b/sources/editor/partpolygon.h index 9993fa0c9..fe3cd4106 100644 --- a/sources/editor/partpolygon.h +++ b/sources/editor/partpolygon.h @@ -59,6 +59,7 @@ class PartPolygon : public QGraphicsPolygonItem, public CustomElementGraphicPart virtual QRectF sceneGeometricRect() const; virtual void startUserTransformation(const QRectF &); virtual void handleUserTransformation(const QRectF &, const QRectF &); + virtual QET::ScalingMethod preferredScalingMethod() const; protected: QVariant itemChange(GraphicsItemChange, const QVariant &); diff --git a/sources/qet.h b/sources/qet.h index 743d73864..0ab044c77 100644 --- a/sources/qet.h +++ b/sources/qet.h @@ -59,6 +59,13 @@ namespace QET { ResizeFromBottomRightCorner = 7 }; + /// Supported types of interactive scaling, typically for a single element primitive + enum ScalingMethod { + FreeScaling, ///< do not interfer with the default scaling process + SnapScalingPointToGrid, ///< snap the point used to define the new bounding rectangle to the grid + RoundScaleRatios ///< adjust the scaling movement so that the induced scaling ratios are rounded + }; + /// Known kinds of conductor segments enum ConductorSegmentType { Horizontal = 1, ///< Horizontal segment