Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rotation Support #24

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions packages/box_transform/lib/src/enums.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,94 @@
import 'package:vector_math/vector_math.dart';

import '../box_transform.dart';
import 'geometry.dart';

/// Represents a cardinal side of a quadrilateral.
enum Side {
/// The top side of the rect.
top,

/// The right side of the rect.
right,

/// The bottom side of the rect.
bottom,

/// The left side of the rect.
left;

/// Whether the side is top or not.
bool get isTop => this == Side.top;

/// Whether the side is right or not.
bool get isRight => this == Side.right;

/// Whether the side is bottom or not.
bool get isBottom => this == Side.bottom;

/// Whether the side is left or not.
bool get isLeft => this == Side.left;

/// Returns the handle position of the side.
HandlePosition get handlePosition => switch (this) {
Side.top => HandlePosition.top,
Side.right => HandlePosition.right,
Side.bottom => HandlePosition.bottom,
Side.left => HandlePosition.left,
};

/// Returns the opposite side of the given side.
Side get opposite => switch (this) {
Side.top => Side.bottom,
Side.right => Side.left,
Side.bottom => Side.top,
Side.left => Side.right,
};

/// Returns the side that is clockwise to the given side.
Side get clockWise => switch (this) {
Side.top => Side.right,
Side.right => Side.bottom,
Side.bottom => Side.left,
Side.left => Side.top,
};

/// Returns the side that is counter-clockwise to the given side.
Side get counterClockWise => switch (this) {
Side.top => Side.left,
Side.right => Side.top,
Side.bottom => Side.right,
Side.left => Side.bottom,
};

/// Whether the side is horizontal or not.
bool get isHorizontal => this == Side.left || this == Side.right;

/// Whether the side is vertical or not.
bool get isVertical => this == Side.top || this == Side.bottom;

/// Returns the length of the [rect] along the side.
double getLengthOf(Box rect) => switch (this) {
Side.top || Side.bottom => rect.height,
Side.left || Side.right => rect.width,
};
}

/// Represents the different quadrants of a rectangle.
enum Quadrant {
/// The top left quadrant.
topLeft,

/// The top right quadrant.
topRight,

/// The bottom left quadrant.
bottomLeft,

/// The bottom right quadrant.
bottomRight,
}

/// Represents a resizing handle on corners.
enum HandlePosition {
/// Represents no handle. An empty resize operation.
Expand Down Expand Up @@ -106,6 +193,15 @@ enum HandlePosition {
HandlePosition.right,
];

/// returns the quadrant of a corner handle.
Quadrant get quadrant => switch (this) {
HandlePosition.topLeft => Quadrant.topLeft,
HandlePosition.topRight => Quadrant.topRight,
HandlePosition.bottomLeft => Quadrant.bottomLeft,
HandlePosition.bottomRight => Quadrant.bottomRight,
_ => throw Exception('Invalid handle position. Corners only.'),
};

/// Returns the opposite handle position on the horizontal axis.
HandlePosition flipY() {
switch (this) {
Expand Down Expand Up @@ -413,3 +509,21 @@ enum ResizeMode {
/// w.r.t the center.
bool get hasSymmetry => isSymmetric || isSymmetricScale;
}

/// An enum that defines how a box should be constrained and/or clamped when
/// undergoing a box transformation operation.
enum BindingStrategy {
/// When a box transformation occurs, clamps and constraints are considered
/// on the original unrotated dimensions of the box. Rotation is not
/// considered with this strategy, and therefore, if a box is rotated,
/// its vertices may leak out of its terminal dimensions and positions.
originalBox,

/// When a box transformation occurs, clamps and constraints are considered
/// on the entire bounding box of the box. The bounding box is the smallest
/// box that can contain all the vertices of its rotated box.
///
/// This is the default strategy to ensure that the box does not
/// leak out of its terminal dimensions and positions.
boundingBox,
}
102 changes: 75 additions & 27 deletions packages/box_transform/lib/src/geometry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ class Dimension {
/// smallest integer values.
Dimension floor() => Dimension(width.floorToDouble(), height.floorToDouble());

/// Returns a [Vector2] representation of this [Dimension].
Vector2 toVector() => Vector2(width, height);

/// Linearly interpolate between two sizes
///
/// If either size is null, this function interpolates from [Dimension.zero].
Expand Down Expand Up @@ -412,28 +415,26 @@ class Box {
/// Construct a rectangle from given handle and its origin.
factory Box.fromHandle(
Vector2 origin, HandlePosition handle, double width, double height) {
switch (handle) {
case HandlePosition.none:
throw ArgumentError('HandlePosition.none is not supported!');
case HandlePosition.topLeft:
return Box.fromLTWH(origin.x - width, origin.y - height, width, height);
case HandlePosition.topRight:
return Box.fromLTWH(origin.x, origin.y - height, width, height);
case HandlePosition.bottomLeft:
return Box.fromLTWH(origin.x - width, origin.y, width, height);
case HandlePosition.bottomRight:
return Box.fromLTWH(origin.x, origin.y, width, height);
case HandlePosition.left:
return Box.fromLTWH(
origin.x - width, origin.y - height / 2, width, height);
case HandlePosition.top:
return Box.fromLTWH(
origin.x - width / 2, origin.y - height, width, height);
case HandlePosition.right:
return Box.fromLTWH(origin.x, origin.y - height / 2, width, height);
case HandlePosition.bottom:
return Box.fromLTWH(origin.x - width / 2, origin.y, width, height);
}
return switch (handle) {
HandlePosition.none =>
throw ArgumentError('HandlePosition.none is not supported!'),
HandlePosition.topLeft =>
Box.fromLTWH(origin.x - width, origin.y - height, width, height),
HandlePosition.topRight =>
Box.fromLTWH(origin.x, origin.y - height, width, height),
HandlePosition.bottomLeft =>
Box.fromLTWH(origin.x - width, origin.y, width, height),
HandlePosition.bottomRight =>
Box.fromLTWH(origin.x, origin.y, width, height),
HandlePosition.left =>
Box.fromLTWH(origin.x - width, origin.y - height / 2, width, height),
HandlePosition.top =>
Box.fromLTWH(origin.x - width / 2, origin.y - height, width, height),
HandlePosition.right =>
Box.fromLTWH(origin.x, origin.y - height / 2, width, height),
HandlePosition.bottom =>
Box.fromLTWH(origin.x - width / 2, origin.y, width, height)
};
}

/// The Vector2 of the left edge of this rectangle from the x axis.
Expand Down Expand Up @@ -474,8 +475,9 @@ class Box {
static const Box largest =
Box.fromLTRB(-_giantScalar, -_giantScalar, _giantScalar, _giantScalar);

/// Whether any of the coordinates of this rectangle are equal to positive infinity.
// included for consistency with Vector2 and Dimension
/// Whether any of the coordinates of this rectangle are equal to positive
/// infinity.
/// Included for consistency with Vector2 and Dimension
bool get isInfinite =>
left >= double.infinity ||
top >= double.infinity ||
Expand Down Expand Up @@ -606,6 +608,23 @@ class Box {
_ => size.aspectRatio,
};

/// Returns a list of the four corners of this rectangle.
List<Vector2> get points => [
topLeft,
topRight,
bottomRight,
bottomLeft,
];

/// Returns a map of the four corners of this rectangle mapped as
/// a [Quadrant] to a [Vector2].
Map<Quadrant, Vector2> get sidedPoints => {
Quadrant.topLeft: topLeft,
Quadrant.topRight: topRight,
Quadrant.bottomRight: bottomRight,
Quadrant.bottomLeft: bottomLeft,
};

/// Whether the point specified by the given Vector2 (which is assumed to be
/// relative to the origin) lies between the left and right and the top and
/// bottom edges of this rectangle.
Expand Down Expand Up @@ -641,8 +660,8 @@ class Box {

final double newLeft = math.max(left, clampedLeft);
final double newTop = math.max(top, clampedTop);
double newWidth = math.min(width, childWidth);
double newHeight = math.min(height, childHeight);
final double newWidth = math.min(width, childWidth);
final double newHeight = math.min(height, childHeight);

return Box.fromLTWH(
newLeft,
Expand All @@ -668,6 +687,35 @@ class Box {
bottom.floorToDouble(),
);

/// Returns the relevant corner of this [Box] based on the given [quadrant].
Vector2 pointFromQuadrant(Quadrant quadrant) => switch (quadrant) {
Quadrant.topLeft => topLeft,
Quadrant.topRight => topRight,
Quadrant.bottomRight => bottomRight,
Quadrant.bottomLeft => bottomLeft,
};

/// Returns a value that represents the distances of the passed
/// [point] relative to the closest edge of this [Box]. If the point is
/// inside the box, the distance will be positive. If the point is outside
/// the box, the distance will be negative.
///
/// Returns the [side] that the point is closest to and the distance to that
/// side.
(Side side, double) distanceOfPoint(Vector2 point) {
final double left = point.x - this.left;
final double right = this.right - point.x;
final double top = point.y - this.top;
final double bottom = this.bottom - point.y;

final double min = math.min(left, math.min(right, math.min(top, bottom)));

if (min == left) return (Side.left, left);
if (min == right) return (Side.right, right);
if (min == top) return (Side.top, top);
return (Side.bottom, bottom);
}

/// Linearly interpolate between two rectangles.
///
/// If either rect is null, [Box.zero] is used as a substitute.
Expand Down Expand Up @@ -721,5 +769,5 @@ class Box {

@override
String toString() =>
'Box.fromLTRB(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)})';
'Box.fromLTWH(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${width.toStringAsFixed(1)}, ${height.toStringAsFixed(1)})';
}
83 changes: 76 additions & 7 deletions packages/box_transform/lib/src/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -543,25 +543,94 @@ Box getMinRectForScaling({
);
}

/// [returns] whether the given [rect] is properly confined within its
/// [constraints] but at the same time is not outside of the [clampingRect].
bool isValidRect(Box rect, Constraints constraints, Box clampingRect) {
/// Returns the largest intersection amount between the given [rect] and the
/// [clampingRect]. If more than one side of [rect] intersects with
/// [clampingRect], only the largest intersection is returned.
///
/// The returned value is made positive.
({Side side, double amount, bool singleIntersection})
getLargestIntersectionDelta(Box rect, Box clampingRect) {
final Map<Side, double> intersections = {
Side.left: rect.left - clampingRect.left,
Side.top: rect.top - clampingRect.top,
Side.right: clampingRect.right - rect.right,
Side.bottom: clampingRect.bottom - rect.bottom,
};

// The largest intersection is represents by the smallest negative value.
final MapEntry<Side, double> largestIntersection =
intersections.entries.reduce((a, b) {
if (a.value < b.value) {
return a;
} else {
return b;
}
});

return (
side: largestIntersection.key,
amount: min(0.0, largestIntersection.value).abs(),
singleIntersection:
intersections.values.where((amount) => amount < 0).length <= 1,
);
}

bool isRectClamped(
Box rect,
Box clampingRect,
) {
if (clampingRect.left.roundToPrecision(4) > rect.left.roundToPrecision(4) ||
clampingRect.top.roundToPrecision(4) > rect.top.roundToPrecision(4) ||
clampingRect.right.roundToPrecision(4) < rect.right.roundToPrecision(4) ||
clampingRect.bottom.roundToPrecision(4) <
rect.bottom.roundToPrecision(4)) {
print('Hit clamping rect.');
return false;
}

return true;
}

bool isRectConstrained(
Box rect,
Constraints constraints,
) =>
constraints.isUnconstrained ||
(rect.width >= constraints.minWidth &&
rect.width <= constraints.maxWidth &&
rect.height >= constraints.minHeight &&
rect.height <= constraints.maxHeight);

/// [returns] whether the given [checkClamp] is properly confined within its
/// [constraints] but at the same time is not outside of the [clampingRect].
bool isRectBound(
Box checkClamp,
Constraints constraints,
Box clampingRect, {
Box? checkConstraints,
}) {
if (clampingRect.left.roundToPrecision(4) >
checkClamp.left.roundToPrecision(4) ||
clampingRect.top.roundToPrecision(4) >
checkClamp.top.roundToPrecision(4) ||
clampingRect.right.roundToPrecision(4) <
checkClamp.right.roundToPrecision(4) ||
clampingRect.bottom.roundToPrecision(4) <
checkClamp.bottom.roundToPrecision(4)) {
print('Hit clamping rect.');
return false;
}
if (!constraints.isUnconstrained) {
if (rect.width.roundToPrecision(4) <
final box = checkConstraints ?? checkClamp;
if (box.width.roundToPrecision(4) <
constraints.minWidth.roundToPrecision(4) ||
rect.width.roundToPrecision(4) >
box.width.roundToPrecision(4) >
constraints.maxWidth.roundToPrecision(4) ||
rect.height.roundToPrecision(4) <
box.height.roundToPrecision(4) <
constraints.minHeight.roundToPrecision(4) ||
rect.height.roundToPrecision(4) >
box.height.roundToPrecision(4) >
constraints.maxHeight.roundToPrecision(4)) {
print('Hit constraints.');
return false;
}
}
Expand Down
Loading
Loading