11part of 'resizer.dart' ;
22
3- /// Handles resizing for [ResizeMode.freeform] .
3+ /// Handles freeform resizing with support for rotation.
4+ ///
5+ /// In addition to the freeform resizing logic from the non‐rotated version,
6+ /// this implementation attempts to reposition and constrain a rotated
7+ /// rectangle. The new rectangle is computed based on the user’s drag (the
8+ /// [explodedRect] ), the allowed clamping area ([clampingRect] ), size
9+ /// constraints, and rotation. It also applies a binding strategy which affects
10+ /// how the available area is computed for the resizing handle.
11+ ///
12+ /// **NOTE:** Several parts of this implementation are problematic compared to
13+ /// the non-rotated version which is flawless and passes all tests. The issues
14+ /// are marked inline with `// [ISSUE]` .
415final class FreeformResizer extends Resizer {
5- /// A default constructor for [FreeformResizer] .
16+ /// Creates a constant instance of [FreeformResizer] .
617 const FreeformResizer ();
718
19+ /// Resizes the rectangle based on user interaction, constraints, and
20+ /// rotation.
21+ ///
22+ /// Parameters:
23+ /// - [initialRect] : The original rectangle before resizing begins.
24+ /// - [explodedRect] : The rectangle produced by applying the user’s drag. It
25+ /// might temporarily exceed constraints.
26+ /// - [clampingRect] : The bounding rectangle within which the resized
27+ /// rectangle must remain.
28+ /// - [handle] : The resizing handle (e.g., a corner or side) that anchors the
29+ /// resize operation.
30+ /// - [constraints] : Minimum and maximum size constraints for the rectangle.
31+ /// - [flip] : Determines whether the rectangle’s coordinates should be
32+ /// flipped.
33+ /// - [rotation] : The rotation angle (in radians) applied to the rectangle.
34+ /// - [bindingStrategy] : A strategy indicating whether to bind to the original
35+ /// box or its bounding rectangle when computing available area.
36+ ///
37+ /// Returns a record containing:
38+ /// - [rect] : The newly computed rectangle after applying repositioning,
39+ /// constraints, and rotation.
40+ /// - [largest] : The largest available area for the handle, useful for UI hints.
41+ /// - [hasValidFlip] : A boolean indicating whether the computed rectangle meets
42+ /// the constraints (used here as a proxy for valid flipping and binding).
843 @override
944 ({Box rect, Box largest, bool hasValidFlip}) resize ({
1045 required Box initialRect,
@@ -16,6 +51,8 @@ final class FreeformResizer extends Resizer {
1651 required double rotation,
1752 required BindingStrategy bindingStrategy,
1853 }) {
54+ final HandlePosition flippedHandle = handle.flip (flip);
55+
1956 final Box effectiveInitialRect = flipRect (initialRect, flip, handle);
2057 final Box initialBoundingRect = BoxTransformer .calculateBoundingRect (
2158 rotation: rotation,
@@ -24,9 +61,13 @@ final class FreeformResizer extends Resizer {
2461 final Box effectiveInitialBoundingRect =
2562 flipRect (initialBoundingRect, flip, handle);
2663
27- final HandlePosition flippedHandle = handle.flip (flip);
28-
2964 Box newRect = explodedRect;
65+
66+ // When resizing with rotation, the box must be repositioned immediately
67+ // after resizing to ensure the opposite handle is anchored properly.
68+ // The reason this is needed is because when resizing with rotation,
69+ // the resize happens around the center of the rect, which mobilizes all
70+ // handles. This corrects that behavior by repositioning the rect.
3071 newRect = repositionRotatedResizedBox (
3172 newRect: newRect,
3273 initialRect: initialRect,
@@ -36,6 +77,10 @@ final class FreeformResizer extends Resizer {
3677 rotation: rotation,
3778 unrotatedBox: newRect,
3879 );
80+
81+ // Check if the new bounding rectangle is clamped within the allowed area.
82+ // [ISSUE] The commented-out switch below indicates that the bindingStrategy
83+ // should affect which rect is checked. Currently, only newBoundingRect is used.
3984 final bool isClamped = isRectClamped (
4085 newBoundingRect,
4186 // switch (bindingStrategy) {
@@ -44,82 +89,131 @@ final class FreeformResizer extends Resizer {
4489 // },
4590 clampingRect,
4691 );
92+
93+ // If the rectangle is not clamped, compute a corrective delta to adjust it.
4794 if (! isClamped) {
48- final Vector2 correctiveDelta = BoxTransformer .stopRectAtClampingRect (
95+ Vector2 correctiveDelta = BoxTransformer .stopRectAtClampingRect (
4996 rect: newRect,
5097 clampingRect: clampingRect,
5198 rotation: rotation,
5299 );
100+ print ('correctiveDelta: $correctiveDelta ' );
53101
54- newRect = BoxTransformer .applyDelta (
55- initialRect: newRect,
56- delta: correctiveDelta,
57- handle: handle,
58- resizeMode: ResizeMode .scale,
59- allowFlipping: false ,
60- );
61-
62- newBoundingRect = BoxTransformer .calculateBoundingRect (
63- rotation: rotation,
64- unrotatedBox: newRect,
65- );
66- }
67-
68- bool isBound = false ;
69- if (! constraints.isUnconstrained) {
70- final Dimension constrainedSize = Dimension (
71- newRect.width.clamp (constraints.minWidth, constraints.maxWidth),
72- newRect.height.clamp (constraints.minHeight, constraints.maxHeight),
73- );
74- final Dimension constrainedDelta = Dimension (
75- constrainedSize.width - newRect.width,
76- constrainedSize.height - newRect.height,
77- );
102+ if (correctiveDelta.x != 0 || correctiveDelta.y != 0 ) {
103+ // Resize
104+ if (correctiveDelta.x > 0 ) {
105+ newRect = Box .fromLTWH (
106+ newRect.left + correctiveDelta.x,
107+ newRect.top,
108+ newRect.width - correctiveDelta.x,
109+ newRect.height,
110+ );
111+ } else {
112+ newRect = Box .fromLTWH (
113+ newRect.left,
114+ newRect.top,
115+ newRect.width + correctiveDelta.x,
116+ newRect.height,
117+ );
118+ }
119+ if (correctiveDelta.y > 0 ) {
120+ newRect = Box .fromLTWH (
121+ newRect.left,
122+ newRect.top + correctiveDelta.y,
123+ newRect.width,
124+ newRect.height - correctiveDelta.y,
125+ );
126+ } else {
127+ newRect = Box .fromLTWH (
128+ newRect.left,
129+ newRect.top,
130+ newRect.width,
131+ newRect.height + correctiveDelta.y,
132+ );
133+ }
134+ }
78135
79- newRect = Box .fromHandle (
80- flippedHandle.anchor (effectiveInitialRect),
81- flippedHandle,
82- newRect.width + constrainedDelta.width,
83- newRect.height + constrainedDelta.height,
84- );
85136 newRect = repositionRotatedResizedBox (
86137 newRect: newRect,
87138 initialRect: initialRect,
88139 rotation: rotation,
89140 );
141+
142+ // Recalculate the bounding rectangle after applying the corrective delta.
90143 newBoundingRect = BoxTransformer .calculateBoundingRect (
91144 rotation: rotation,
92145 unrotatedBox: newRect,
93146 );
94-
95- isBound = isRectConstrained (
96- newRect,
97- constraints,
98- );
99-
100- if (! isBound) {
101- newRect = Box .fromHandle (
102- handle.anchor (initialRect),
103- handle,
104- handle.influencesHorizontal
105- ? constraints.minWidth
106- : constrainedSize.width,
107- handle.influencesVertical
108- ? constraints.minHeight
109- : constrainedSize.height,
110- );
111- newRect = repositionRotatedResizedBox (
112- newRect: newRect,
113- initialRect: initialRect,
114- rotation: rotation,
115- );
116- newBoundingRect = BoxTransformer .calculateBoundingRect (
117- rotation: rotation,
118- unrotatedBox: newRect,
119- );
120- }
121147 }
122148
149+ bool isBound = false ;
150+ // Apply size constraints if they are set.
151+ // if (!constraints.isUnconstrained) {
152+ // // Clamp the current width and height to within allowed limits.
153+ // final Dimension constrainedSize = Dimension(
154+ // newRect.width.clamp(constraints.minWidth, constraints.maxWidth),
155+ // newRect.height.clamp(constraints.minHeight, constraints.maxHeight),
156+ // );
157+ //
158+ // // Calculate how much adjustment is needed to reach the constrained size.
159+ // final Dimension constrainedDelta = Dimension(
160+ // constrainedSize.width - newRect.width,
161+ // constrainedSize.height - newRect.height,
162+ // );
163+ //
164+ // // Recalculate the rectangle using the flipped handle's anchor.
165+ // newRect = Box.fromHandle(
166+ // flippedHandle.anchor(effectiveInitialRect),
167+ // flippedHandle,
168+ // newRect.width + constrainedDelta.width,
169+ // newRect.height + constrainedDelta.height,
170+ // );
171+ //
172+ // // Reposition again after applying constraints.
173+ // newRect = repositionRotatedResizedBox(
174+ // newRect: newRect,
175+ // initialRect: initialRect,
176+ // rotation: rotation,
177+ // );
178+ //
179+ // // Update the bounding rectangle to reflect the constrained, repositioned rect.
180+ // newBoundingRect = BoxTransformer.calculateBoundingRect(
181+ // rotation: rotation,
182+ // unrotatedBox: newRect,
183+ // );
184+ //
185+ // // Check if the new rectangle satisfies the constraints.
186+ // isBound = isRectConstrained(
187+ // newRect,
188+ // constraints,
189+ // );
190+ //
191+ // // If the rectangle is still not properly constrained, fall back to minimum sizes.
192+ // if (!isBound) {
193+ // newRect = Box.fromHandle(
194+ // handle.anchor(initialRect),
195+ // handle,
196+ // handle.influencesHorizontal
197+ // ? constraints.minWidth
198+ // : constrainedSize.width,
199+ // handle.influencesVertical
200+ // ? constraints.minHeight
201+ // : constrainedSize.height,
202+ // );
203+ // // [ISSUE] Falling back to the unflipped handle and initialRect may ignore
204+ // // the rotation context, leading to inconsistencies.
205+ // newRect = repositionRotatedResizedBox(
206+ // newRect: newRect,
207+ // initialRect: initialRect,
208+ // rotation: rotation,
209+ // );
210+ // newBoundingRect = BoxTransformer.calculateBoundingRect(
211+ // rotation: rotation,
212+ // unrotatedBox: newRect,
213+ // );
214+ // }
215+ // }
216+
123217 final Box effectiveBindingRect = switch (bindingStrategy) {
124218 BindingStrategy .originalBox => effectiveInitialRect,
125219 BindingStrategy .boundingBox => effectiveInitialBoundingRect,
@@ -129,7 +223,6 @@ final class FreeformResizer extends Resizer {
129223 BindingStrategy .boundingBox => initialBoundingRect,
130224 };
131225
132- // Only used for calculating the correct largest box.
133226 final Box area = getAvailableAreaForHandle (
134227 rect: isBound ? effectiveBindingRect : bindingRect,
135228 handle: isBound ? flippedHandle : handle,
@@ -141,20 +234,38 @@ final class FreeformResizer extends Resizer {
141234
142235 /// Repositions a rotated and resized box back to its original unrotated
143236 /// position.
237+ ///
238+ /// This method attempts to calculate the correct position for a rectangle
239+ /// that has been both rotated and resized, returning it to the coordinate
240+ /// space of the unrotated original rectangle.
241+ ///
242+ /// Parameters:
243+ /// - [newRect] : The current, potentially rotated rectangle.
244+ /// - [initialRect] : The original rectangle before rotation and resizing.
245+ /// - [rotation] : The rotation angle (in radians).
246+ ///
247+ /// Returns a new [Box] that represents the repositioned rectangle.
144248 Box repositionRotatedResizedBox ({
145249 required Box newRect,
146250 required Box initialRect,
147251 required double rotation,
148252 }) {
253+ // If there is no rotation, no repositioning is needed.
149254 if (rotation == 0 ) return newRect;
150255
256+ // Compute the delta between the top-left corners of the new and initial
257+ // rectangles.
151258 final Vector2 positionDelta = newRect.topLeft - initialRect.topLeft;
259+
260+ // Calculate the new position in the unrotated space.
152261 final Vector2 newPos = BoxTransformer .calculateUnrotatedPos (
153262 initialRect,
154263 rotation,
155264 positionDelta,
156265 newRect.size,
157266 );
267+
268+ // Return a new Box with the repositioned top-left coordinates.
158269 return Box .fromLTWH (newPos.x, newPos.y, newRect.width, newRect.height);
159270 }
160271}
0 commit comments