Skip to content

Commit a6aa6b7

Browse files
committed
Rotation #9
1 parent 452d812 commit a6aa6b7

File tree

10 files changed

+661
-165
lines changed

10 files changed

+661
-165
lines changed

packages/box_transform/lib/src/geometry.dart

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -689,20 +689,20 @@ class Box {
689689

690690
/// Returns the relevant corner of this [Box] based on the given [quadrant].
691691
Vector2 pointFromQuadrant(Quadrant quadrant) => switch (quadrant) {
692-
Quadrant.topLeft => topLeft,
693-
Quadrant.topRight => topRight,
694-
Quadrant.bottomRight => bottomRight,
695-
Quadrant.bottomLeft => bottomLeft,
696-
};
692+
Quadrant.topLeft => topLeft,
693+
Quadrant.topRight => topRight,
694+
Quadrant.bottomRight => bottomRight,
695+
Quadrant.bottomLeft => bottomLeft,
696+
};
697697

698698
/// Returns a value that represents the distances of the passed
699699
/// [point] relative to the closest edge of this [Box]. If the point is
700700
/// inside the box, the distance will be positive. If the point is outside
701701
/// the box, the distance will be negative.
702702
///
703-
/// Returns the [side] that the point is closest to and the distance to that
703+
/// Returns the [side] that the point is closest to and the [distance] to that
704704
/// side.
705-
(Side side, double) distanceOfPoint(Vector2 point) {
705+
(Side side, double distance) closestSideTo(Vector2 point) {
706706
final double left = point.x - this.left;
707707
final double right = this.right - point.x;
708708
final double top = point.y - this.top;

packages/box_transform/lib/src/helpers.dart

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -584,7 +584,6 @@ bool isRectClamped(
584584
clampingRect.right.roundToPrecision(4) < rect.right.roundToPrecision(4) ||
585585
clampingRect.bottom.roundToPrecision(4) <
586586
rect.bottom.roundToPrecision(4)) {
587-
print('Hit clamping rect.');
588587
return false;
589588
}
590589

@@ -617,7 +616,6 @@ bool isRectBound(
617616
checkClamp.right.roundToPrecision(4) ||
618617
clampingRect.bottom.roundToPrecision(4) <
619618
checkClamp.bottom.roundToPrecision(4)) {
620-
print('Hit clamping rect.');
621619
return false;
622620
}
623621
if (!constraints.isUnconstrained) {
@@ -630,7 +628,6 @@ bool isRectBound(
630628
constraints.minHeight.roundToPrecision(4) ||
631629
box.height.roundToPrecision(4) >
632630
constraints.maxHeight.roundToPrecision(4)) {
633-
print('Hit constraints.');
634631
return false;
635632
}
636633
}
Lines changed: 174 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,45 @@
11
part 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]`.
415
final 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

Comments
 (0)