From fe13e864440de1a2ea86d6a829e11ba54047bc29 Mon Sep 17 00:00:00 2001 From: fm-sys <64581222+fm-sys@users.noreply.github.com> Date: Thu, 28 May 2026 12:20:07 +0200 Subject: [PATCH] support rotation snapping implements feature request https://community.signalusers.org/t/74780 --- .../core/ElementScaleEditSession.java | 13 ++++++-- .../signal/imageeditor/core/RotationSnap.java | 30 +++++++++++++++++++ .../core/ThumbDragEditSession.java | 15 ++++++++-- 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 lib/image-editor/src/main/java/org/signal/imageeditor/core/RotationSnap.java diff --git a/lib/image-editor/src/main/java/org/signal/imageeditor/core/ElementScaleEditSession.java b/lib/image-editor/src/main/java/org/signal/imageeditor/core/ElementScaleEditSession.java index ec8d8421d36..daa800cc6fb 100644 --- a/lib/image-editor/src/main/java/org/signal/imageeditor/core/ElementScaleEditSession.java +++ b/lib/image-editor/src/main/java/org/signal/imageeditor/core/ElementScaleEditSession.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.signal.imageeditor.core; import android.graphics.Matrix; @@ -9,13 +14,16 @@ final class ElementScaleEditSession extends ElementEditSession { - private ElementScaleEditSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix) { + private final float initialRotationRadians; + + private ElementScaleEditSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix, float initialRotationRadians) { super(selected, inverseMatrix); + this.initialRotationRadians = initialRotationRadians; } static ElementScaleEditSession startScale(@NonNull ElementDragEditSession session, @NonNull Matrix inverseMatrix, @NonNull PointF point, int p) { session.commit(); - ElementScaleEditSession newSession = new ElementScaleEditSession(session.selected, inverseMatrix); + ElementScaleEditSession newSession = new ElementScaleEditSession(session.selected, inverseMatrix, session.selected.getLocalRotationAngle()); newSession.setScreenStartPoint(1 - p, session.endPointScreen[0]); newSession.setScreenEndPoint(1 - p, session.endPointScreen[0]); newSession.setScreenStartPoint(p, point); @@ -38,6 +46,7 @@ public void movePoint(int p, @NonNull PointF point) { editorMatrix.postScale(scale, scale); double angle = angle(endPointElement[0], endPointElement[1]) - angle(startPointElement[0], startPointElement[1]); + angle = RotationSnap.snapToAngle(initialRotationRadians, angle); if (!selected.getFlags().isRotateLocked()) { editorMatrix.postRotate((float) Math.toDegrees(angle)); diff --git a/lib/image-editor/src/main/java/org/signal/imageeditor/core/RotationSnap.java b/lib/image-editor/src/main/java/org/signal/imageeditor/core/RotationSnap.java new file mode 100644 index 00000000000..7ccfb64411d --- /dev/null +++ b/lib/image-editor/src/main/java/org/signal/imageeditor/core/RotationSnap.java @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.imageeditor.core; + +final class RotationSnap { + + private static final double SNAP_ANGLE_RADIANS = Math.PI / 2d; // 90 degrees in radians + private static final double SNAP_THRESHOLD_RADIANS = Math.toRadians(5d); + + private RotationSnap() {} + + private static double snapToAngle(double angleRadians) { + double snappedAngle = Math.rint(angleRadians / SNAP_ANGLE_RADIANS) * SNAP_ANGLE_RADIANS; + + if (Math.abs(angleRadians - snappedAngle) <= SNAP_THRESHOLD_RADIANS) { + return snappedAngle; + } + + return angleRadians; + } + + static double snapToAngle(double baseAngleRadians, double relativeAngleRadians) { + double absoluteAngle = baseAngleRadians + relativeAngleRadians; + double snappedAbsoluteAngle = snapToAngle(absoluteAngle); + return snappedAbsoluteAngle - baseAngleRadians; + } +} diff --git a/lib/image-editor/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java b/lib/image-editor/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java index 07a2c447eff..b7a92cc36e0 100644 --- a/lib/image-editor/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java +++ b/lib/image-editor/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + package org.signal.imageeditor.core; import android.graphics.Matrix; @@ -13,6 +18,7 @@ class ThumbDragEditSession extends ElementEditSession { private final PointF oppositeControlPoint = new PointF(); private final float[] oppositeControlPointOnControlParent = new float[2]; private final float[] oppositeControlPointOnElement = new float[2]; + private final float initialRotationRadians; @NonNull private final ThumbRenderer.ControlPoint controlPoint; @@ -21,11 +27,13 @@ class ThumbDragEditSession extends ElementEditSession { private ThumbDragEditSession(@NonNull EditorElement selected, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull Matrix inverseMatrix, - @NonNull Matrix thumbContainerRelativeMatrix) + @NonNull Matrix thumbContainerRelativeMatrix, + float initialRotationRadians) { super(selected, inverseMatrix); this.controlPoint = controlPoint; this.thumbContainerRelativeMatrix = thumbContainerRelativeMatrix; + this.initialRotationRadians = initialRotationRadians; } static EditSession startDrag(@NonNull EditorElement selected, @@ -36,7 +44,7 @@ static EditSession startDrag(@NonNull EditorElement selected, { if (!selected.getFlags().isEditable()) return null; - ElementEditSession elementDragEditSession = new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix, thumbContainerRelativeMatrix); + ElementEditSession elementDragEditSession = new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix, thumbContainerRelativeMatrix, selected.getLocalRotationAngle()); elementDragEditSession.setScreenStartPoint(0, point); elementDragEditSession.setScreenEndPoint(0, point); return elementDragEditSession; @@ -72,6 +80,7 @@ public void movePoint(int p, @NonNull PointF point) { editorMatrix.postTranslate(-oppositeControlPoint.x, -oppositeControlPoint.y); editorMatrix.postScale(scale, scale); double angle = angle(endPointElement[0], oppositeControlPoint) - angle(startPointElement[0], oppositeControlPoint); + angle = RotationSnap.snapToAngle(initialRotationRadians, angle); rotate(editorMatrix, angle); editorMatrix.postTranslate(oppositeControlPoint.x, oppositeControlPoint.y); } else { @@ -142,4 +151,4 @@ private static float getDistanceSquared(@NonNull PointF a, @NonNull PointF b) { float dy = a.y - b.y; return dx * dx + dy * dy; } -} \ No newline at end of file +}