CATROID-1647 Feat: Resize and rotate object when placing in stage#5189
Conversation
…ate feature (IDE-217)
… in ProjectActivity.kt (IDE-217)
…rCloud Reliability bug (IDE-217)
There was a problem hiding this comment.
Pull request overview
This PR adds multi-touch transformation support to the visual placement flow so users can resize and rotate a sprite while placing it on the stage, with a bounding-box overlay for visual feedback and persistence of the result back into the project.
Changes:
- Introduces a new two-finger gesture detector for pinch-to-scale and rotation, and integrates it into visual placement touch handling.
- Adds a bounding box overlay view (with corner-handle vector drawable) that updates during placement interactions.
- Persists placement scale/rotation back to the calling activities and (for new sprites) appends corresponding bricks to the generated script.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| catroid/src/main/java/org/catrobat/catroid/visualplacement/ResizeRotateGestureDetector.kt | New detector for scaling/rotation math and gesture state. |
| catroid/src/main/java/org/catrobat/catroid/visualplacement/BoundingBoxOverlay.kt | New overlay view rendering a rotated bounding box + handles around the placed sprite. |
| catroid/src/main/res/drawable/ic_corner_handle.xml | New corner-handle vector drawable used by the overlay. |
| catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementTouchListener.java | Adds multi-touch routing and overlay updates in the touch pipeline. |
| catroid/src/main/java/org/catrobat/catroid/visualplacement/VisualPlacementActivity.java | Wires detector + overlay into placement UI and returns scale/rotation via result extras. |
| catroid/src/main/java/org/catrobat/catroid/visualplacement/TransformationInterface.kt | Extends placement callback contract to include scale/rotation setters. |
| catroid/src/main/java/org/catrobat/catroid/ui/SpriteActivity.java | Applies returned scale/rotation from visual placement to the current sprite look. |
| catroid/src/main/java/org/catrobat/catroid/ui/ProjectActivity.kt | For new sprites, creates a StartScript and appends PlaceAt/SetSize/PointInDirection bricks from placement results. |
| catroid/src/test/java/org/catrobat/catroid/test/visualplacement/ResizeRotateGestureDetectorTest.kt | Unit tests for detector helper math and initial state. |
| catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementResizeRotateTest.kt | Tests for integration expectations (drag still works, overlay update, detector state). |
| catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementTouchListenerTest.java | Updates mocks for new pointer-count logic. |
| catroid/src/test/java/org/catrobat/catroid/test/visualplacement/VisualPlacementPositionCorrectionTest.java | Updates mocks for new pointer-count/pointer-id checks. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
wslany
left a comment
There was a problem hiding this comment.
Thanks for the feature. I found a couple of interaction edge cases around rotation handling and the transition back from multi-touch to single-touch, plus two smaller cleanup items. The generated APK builds and the focused visual-placement unit tests pass locally, but I think the P2 items should be fixed before merge.
…uch baseline reset, and direction comparison; update copyrights and merge nested ifs
wslany
left a comment
There was a problem hiding this comment.
Thanks for the quick follow-up. I still see a couple of actionable issues in the latest head, mainly around the pointer-up reset path and missing regression coverage for the review fixes.
- Resolved P2 issue in VisualPlacementTouchListener where pointer-up events caused stale touch baselines. - Removed unused DEFAULT_ROTATION in ProjectActivity and fixed default rotation extra read. - Added regression tests for angle normalization and multi-touch transition resets.
wslany
left a comment
There was a problem hiding this comment.
One remaining coverage request: we want full coverage for new behavior from now on. The focused unit coverage improved for the gesture/touch code, but the activity result/script-creation path still lacks direct regression coverage.
…ton (IDE-217) - Fix SpriteActivity: default placementRotation to DEGREE_UI_OFFSET (90°) and only apply rotation when changed - Fix VisualPlacementActivity: set correct defaults for rotationMode and initialLookRotation when loading from look list - Fix default initialSpriteRotation to DEGREE_UI_OFFSET to prevent unwanted PointInDirectionBrick - Add ScreenValueHandler.updateScreenWidthAndHeight() for correct layout sizing - Use existing LayoutParams instead of creating new ones - Add two-finger panning with frame-to-frame delta tracking - Add velocity-based rotation snapping to 90° increments - Add Reset toolbar button to restore initial transformations
| private var lastRotationTime: Long = 0 | ||
| private var lastRotationAngle: Float = 0f | ||
|
|
||
| fun onTouchEvent(event: MotionEvent): Boolean { |
| } | ||
|
|
||
| private fun getSnappedAngle(angle: Float): Float { | ||
| val normalizedRotation = (angle % 360 + 360) % 360 |
| } | ||
|
|
||
| private fun getSnappedAngle(angle: Float): Float { | ||
| val normalizedRotation = (angle % 360 + 360) % 360 |
| } | ||
|
|
||
| private fun getSnappedAngle(angle: Float): Float { | ||
| val normalizedRotation = (angle % 360 + 360) % 360 |
|
|
||
| private fun getSnappedAngle(angle: Float): Float { | ||
| val normalizedRotation = (angle % 360 + 360) % 360 | ||
| val snapAngles = floatArrayOf(0f, 90f, 180f, 270f, 360f) |
|
|
||
| private fun getSnappedAngle(angle: Float): Float { | ||
| val normalizedRotation = (angle % 360 + 360) % 360 | ||
| val snapAngles = floatArrayOf(0f, 90f, 180f, 270f, 360f) |
|
|
||
| private fun getSnappedAngle(angle: Float): Float { | ||
| val normalizedRotation = (angle % 360 + 360) % 360 | ||
| val snapAngles = floatArrayOf(0f, 90f, 180f, 270f, 360f) |
|
|
||
| private fun getSnappedAngle(angle: Float): Float { | ||
| val normalizedRotation = (angle % 360 + 360) % 360 | ||
| val snapAngles = floatArrayOf(0f, 90f, 180f, 270f, 360f) |
- Add ic_rotate_90 drawable (circular arrow icon) - Add rotate_90 menu item between reset and confirm - Implement rotateBy90Degrees() in VisualPlacementActivity - Add rotate_90 string resource - Add testCumulativeRotationForRotate90Feature test - Add testSetCumulativeRotationAndScaleForResetScenario test - Update test listeners to implement onPan interface
|
| case R.id.confirm: | ||
| finishWithResult(); | ||
| break; | ||
| case R.id.reset: |
| case R.id.reset: | ||
| resetTransformations(); | ||
| break; | ||
| case R.id.rotate_90: |



Related Jira Ticket
(https://catrobat.atlassian.net/browse/CATROID-1647)
Summary
This PR introduces multi-touch support to the
VisualPlacementActivity, allowing users to intuitively resize and rotate a sprite while positioning it on the stage.To provide clear visual feedback, a new bounding box overlay with corner handles has been implemented. Additionally, the IDE now automatically handles data persistence by inserting
SetSizeToBrickandPointInDirectionBrickinto the sprite's script upon confirmation, streamlining the creation workflow.Technical Changes
ResizeRotateGestureDetector.kt: New gesture detector for handling pinch-to-scale and two-finger rotation math.BoundingBoxOverlay.kt: Custom View that draws a dynamic blue outline and curved-arrow handles around the active sprite.VisualPlacementTouchListener.java: Updated to coordinate between single-finger dragging and multi-finger transformations.TransformationInterface.kt: Extends the existing interface to include scale and rotation setters.ProjectActivity.kt: Added logic to automatically append the relevant bricks to the script based on the visual placement results.Screen Recording
Screenrecorder-2026-04-07-15-56-57-229.mp4
Your checklist for this pull request
Please review the contributing guidelines and wiki pages of this repository.