Skip to content

Commit 41ad9e2

Browse files
committed
Fix placement when flipping rotated elements
1 parent 1c8a856 commit 41ad9e2

10 files changed

Lines changed: 184 additions & 71 deletions

File tree

app/lib/handlers/select.dart

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,14 +114,8 @@ class SelectHandler extends Handler<SelectTool> {
114114
final rotation = transform.rotation;
115115
final rotationRad = rotation * pi / 180;
116116

117-
final Offset selectionTopLeft = selectionRect.topLeft;
118-
final Offset movedSelectionTopLeft = selectionTopLeft + transform.position;
119-
120-
Offset applyScaleAndTranslate(Offset original) {
121-
final relative = original - selectionTopLeft;
122-
return Offset(relative.dx * scaleX, relative.dy * scaleY) +
123-
movedSelectionTopLeft;
124-
}
117+
Offset applyScaleAndTranslate(Offset original) =>
118+
transform.scalePoint(original, selectionRect);
125119

126120
final Offset transformedPivot = applyScaleAndTranslate(pivot);
127121

@@ -134,13 +128,14 @@ class SelectHandler extends Handler<SelectTool> {
134128
final elementRect = renderer.rect ?? Rect.zero;
135129

136130
final originalExpandedTopLeft = elementExpandedRect.topLeft;
137-
final translatedExpandedTopLeft = applyScaleAndTranslate(
138-
originalExpandedTopLeft,
131+
final transformedExpandedRect = transform.scaleRect(
132+
elementExpandedRect,
133+
selectionRect,
139134
);
140135
// Delta relative to expandedRect.topLeft so it's zero at identity.
141136
// The position compensation inside transform() converts from the
142137
// expandedRect reference frame to the rect reference frame.
143-
var delta = translatedExpandedTopLeft - originalExpandedTopLeft;
138+
var delta = transformedExpandedRect.topLeft - originalExpandedTopLeft;
144139

145140
if (rotation != 0) {
146141
final originalTopLeft = elementRect.topLeft;
@@ -163,6 +158,7 @@ class SelectHandler extends Handler<SelectTool> {
163158
rotation: rotation,
164159
rotatePosition: false,
165160
relative: true,
161+
positionIsBounds: true,
166162
) ??
167163
renderer;
168164
}).toList();

app/lib/renderers/elements/pen.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,10 @@ class PenRenderer extends Renderer<PenElement> {
322322
bool expanded = false,
323323
]) {
324324
final rect = expanded ? expandedRect : this.rect;
325-
final size = Size(rect.width * scaleX, rect.height * scaleY);
326-
return position & size;
325+
return Rect.fromPoints(
326+
position,
327+
position + Offset(rect.width * scaleX, rect.height * scaleY),
328+
);
327329
}
328330

329331
@override

app/lib/renderers/elements/polygon.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,10 @@ class PolygonRenderer extends Renderer<PolygonElement> {
224224
}
225225

226226
Rect moveRect(Offset position, double scaleX, double scaleY) {
227-
final size = Size(rect.width * scaleX, rect.height * scaleY);
228-
return position & size;
227+
return Rect.fromPoints(
228+
position,
229+
position + Offset(rect.width * scaleX, rect.height * scaleY),
230+
);
229231
}
230232

231233
@override

app/lib/renderers/elements/shape.dart

Lines changed: 27 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
part of '../renderer.dart';
22

3+
({Offset tip, Offset left, Offset right}) _trianglePoints(
4+
Rect rect,
5+
ShapeElement element,
6+
) {
7+
final flippedVertically = element.secondPosition.y < element.firstPosition.y;
8+
return flippedVertically
9+
? (tip: rect.bottomCenter, left: rect.topLeft, right: rect.topRight)
10+
: (tip: rect.topCenter, left: rect.bottomLeft, right: rect.bottomRight);
11+
}
12+
313
class ShapeRenderer extends Renderer<ShapeElement> {
414
final _strokePaint = ElementPaintRenderer();
515
final _fillPaint = ElementPaintRenderer();
@@ -161,11 +171,11 @@ class ShapeRenderer extends Renderer<ShapeElement> {
161171
..lineTo(element.secondPosition.x, element.secondPosition.y);
162172
_drawStyledPath(canvas, path, paint);
163173
} else if (shape is TriangleShape) {
164-
final topCenter = drawRect.topCenter;
174+
final points = _trianglePoints(drawRect, element);
165175
final path = Path()
166-
..moveTo(topCenter.dx, topCenter.dy)
167-
..lineTo(drawRect.right, drawRect.bottom)
168-
..lineTo(drawRect.left, drawRect.bottom)
176+
..moveTo(points.tip.dx, points.tip.dy)
177+
..lineTo(points.right.dx, points.right.dy)
178+
..lineTo(points.left.dx, points.left.dy)
169179
..close();
170180
canvas.drawPath(
171181
path,
@@ -305,11 +315,11 @@ class ShapeRenderer extends Renderer<ShapeElement> {
305315
},
306316
);
307317
} else if (shape is TriangleShape) {
308-
final topCenter = drawRect.topCenter;
318+
final points = _trianglePoints(drawRect, element);
309319
final d =
310-
'M${topCenter.dx} ${topCenter.dy} '
311-
'L${drawRect.right} ${drawRect.bottom} '
312-
'L${drawRect.left} ${drawRect.bottom} Z';
320+
'M${points.tip.dx} ${points.tip.dy} '
321+
'L${points.right.dx} ${points.right.dy} '
322+
'L${points.left.dx} ${points.left.dy} Z';
313323
xml
314324
.getElement('svg')
315325
?.createElement(
@@ -322,23 +332,6 @@ class ShapeRenderer extends Renderer<ShapeElement> {
322332
'stroke-dasharray': ?dashArray,
323333
},
324334
);
325-
} else if (shape is TriangleShape) {
326-
final topCenter = drawRect.topCenter;
327-
final d =
328-
'M${topCenter.dx} ${topCenter.dy} '
329-
'L${drawRect.right} ${drawRect.bottom} '
330-
'L${drawRect.left} ${drawRect.bottom} Z';
331-
xml
332-
.getElement('svg')
333-
?.createElement(
334-
'path',
335-
attributes: {
336-
'd': d,
337-
'fill': shape.fillPaint.previewColor.toHexString(),
338-
'stroke': element.property.paint.previewColor.toHexString(),
339-
'stroke-width': '${element.property.strokeWidth}px',
340-
},
341-
);
342335
}
343336
}
344337

@@ -354,16 +347,13 @@ class ShapeRenderer extends Renderer<ShapeElement> {
354347
final previous = rect.topLeft;
355348
final localFirst = element.firstPosition.toOffset() - previous;
356349
final localSecond = element.secondPosition.toOffset() - previous;
357-
final nextRotation = element.property.shape is TriangleShape && scaleY < 0
358-
? (rotation + 180) % 360
359-
: rotation;
360350
return ShapeRenderer(
361351
element.copyWith(
362352
shear: shear,
363353
firstPosition: (localFirst.scale(scaleX, scaleY) + position).toPoint(),
364354
secondPosition: (localSecond.scale(scaleX, scaleY) + position)
365355
.toPoint(),
366-
rotation: nextRotation,
356+
rotation: rotation,
367357
),
368358
layer,
369359
);
@@ -503,9 +493,10 @@ class ShapeHitCalculator extends HitCalculator {
503493
}
504494

505495
bool hitTriangle() {
506-
final triTop = this.rect.topCenter.rotate(center, rotation);
507-
final triLeft = this.rect.bottomLeft.rotate(center, rotation);
508-
final triRight = this.rect.bottomRight.rotate(center, rotation);
496+
final points = _trianglePoints(this.rect, element);
497+
final triTop = points.tip.rotate(center, rotation);
498+
final triLeft = points.left.rotate(center, rotation);
499+
final triRight = points.right.rotate(center, rotation);
509500

510501
return switch (hitElementMode) {
511502
HitElementMode.full =>
@@ -600,9 +591,10 @@ class ShapeHitCalculator extends HitCalculator {
600591
_ => false, // this shouldn't happen
601592
};
602593
case TriangleShape():
603-
final topCenter = rect.topCenter.rotate(center, rotation);
604-
final bottomLeft = rect.bottomLeft.rotate(center, rotation);
605-
final bottomRight = rect.bottomRight.rotate(center, rotation);
594+
final points = _trianglePoints(rect, element);
595+
final topCenter = points.tip.rotate(center, rotation);
596+
final bottomLeft = points.left.rotate(center, rotation);
597+
final bottomRight = points.right.rotate(center, rotation);
606598
var triPoints = [topCenter, bottomLeft, bottomRight];
607599
final inside = isPolygonInPolygon(polygon, triPoints);
608600
return switch (hitElementMode) {

app/lib/renderers/foregrounds/select.dart

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ typedef TransformResult = ({
5151
double scaleY,
5252
});
5353

54+
extension TransformResultGeometry on TransformResult {
55+
Offset scalePoint(Offset point, Rect selection) {
56+
final relative = point - selection.topLeft;
57+
return selection.topLeft +
58+
position +
59+
Offset(relative.dx * scaleX, relative.dy * scaleY);
60+
}
61+
62+
Rect scaleRect(Rect rect, Rect selection) => Rect.fromPoints(
63+
scalePoint(rect.topLeft, selection),
64+
scalePoint(rect.bottomRight, selection),
65+
);
66+
}
67+
5468
class RectSelectionForegroundManager {
5569
final bool enableRotation;
5670
Rect _selection = Rect.zero;
@@ -270,15 +284,7 @@ class RectSelectionForegroundManager {
270284
Rect getTransformedSelection() {
271285
final transform = getTransform();
272286
if (transform == null) return _selection;
273-
final topLeft = _selection.topLeft + transform.position;
274-
return Rect.fromPoints(
275-
topLeft,
276-
topLeft +
277-
Offset(
278-
_selection.width * transform.scaleX,
279-
_selection.height * transform.scaleY,
280-
),
281-
);
287+
return transform.scaleRect(_selection, _selection);
282288
}
283289

284290
RectSelectionForegroundRenderer get renderer =>

app/lib/renderers/renderer.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,7 @@ abstract class Renderer<T> {
676676
double? rotation,
677677
bool relative = true,
678678
bool rotatePosition = false,
679+
bool positionIsBounds = false,
679680
}) {
680681
final rect = this.rect ?? Rect.zero;
681682
rotation ??= relative ? 0 : this.rotation;
@@ -748,9 +749,10 @@ abstract class Renderer<T> {
748749
nextShear,
749750
);
750751
if (rotationDelta == 0) {
752+
final scaledOrigin = positionIsBounds ? rect.topLeft : scaledRect.topLeft;
751753
nextPosition +=
752754
(oldExpanded.topLeft - rect.topLeft) -
753-
(newExpanded.topLeft - scaledRect.topLeft);
755+
(newExpanded.topLeft - scaledOrigin);
754756
}
755757

756758
return _transform(

app/pubspec.lock

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -252,10 +252,10 @@ packages:
252252
dependency: "direct main"
253253
description:
254254
name: connectivity_plus
255-
sha256: cad0e811a289ea2a941119dc483c204ec1684cbb9a8fc7351fe4a230b8313160
255+
sha256: "25cebb79dfe304022550e0c3c893ae051ded07183d2607f7b88473cb3ade33cb"
256256
url: "https://pub.dev"
257257
source: hosted
258-
version: "7.2.0"
258+
version: "7.3.0"
259259
connectivity_plus_platform_interface:
260260
dependency: transitive
261261
description:
@@ -893,10 +893,10 @@ packages:
893893
dependency: "direct main"
894894
description:
895895
name: network_info_plus
896-
sha256: "4a1217d16644ed59f88e415e2777a4c81f4da5bffb724566825e5551c6379567"
896+
sha256: "096a700e2321507e2e587487a442b9baa7ba6cd4e70e3cc1ebbb8327d2632c2d"
897897
url: "https://pub.dev"
898898
source: hosted
899-
version: "8.2.0"
899+
version: "8.2.1"
900900
network_info_plus_platform_interface:
901901
dependency: transitive
902902
description:
@@ -977,10 +977,10 @@ packages:
977977
dependency: "direct main"
978978
description:
979979
name: package_info_plus
980-
sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad
980+
sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481"
981981
url: "https://pub.dev"
982982
source: hosted
983-
version: "10.2.0"
983+
version: "10.2.1"
984984
package_info_plus_platform_interface:
985985
dependency: transitive
986986
description:
@@ -1283,10 +1283,10 @@ packages:
12831283
dependency: "direct main"
12841284
description:
12851285
name: share_plus
1286-
sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8"
1286+
sha256: "02180b01c1237b9706b663d9402b2cf2402b3407f48cce99cc19e3200f095b8a"
12871287
url: "https://pub.dev"
12881288
source: hosted
1289-
version: "13.2.0"
1289+
version: "13.2.1"
12901290
share_plus_platform_interface:
12911291
dependency: transitive
12921292
description:
@@ -1601,10 +1601,10 @@ packages:
16011601
dependency: transitive
16021602
description:
16031603
name: uuid
1604-
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
1604+
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
16051605
url: "https://pub.dev"
16061606
source: hosted
1607-
version: "4.5.3"
1607+
version: "4.6.0"
16081608
vector_graphics:
16091609
dependency: transitive
16101610
description:

app/test/renderers/polygon_renderer_test.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@ import 'package:flutter_test/flutter_test.dart';
66
import 'package:material_leap/helpers.dart';
77

88
void main() {
9+
test('negative scale keeps valid polygon bounds', () {
10+
final points = [
11+
PolygonPoint(0, 0),
12+
PolygonPoint(100, 0),
13+
PolygonPoint(100, 50),
14+
];
15+
final renderer = PolygonRenderer(
16+
PolygonElement(points: points),
17+
null,
18+
calculatePolygonRect(points),
19+
);
20+
21+
final mirrored = renderer.transform(scaleX: -1)!;
22+
23+
expect(mirrored.rect, const Rect.fromLTWH(-100, 0, 100, 50));
24+
expect(mirrored.rect!.isEmpty, isFalse);
25+
});
26+
927
group('No Bezier', () {
1028
late PolygonHitCalculator calculator;
1129

app/test/renderers/select_foreground_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,22 @@ void main() {
9191
const Rect.fromLTWH(100, 50, 50, 25),
9292
);
9393
});
94+
95+
test('negative scale uses the opposite element corner for placement', () {
96+
const selection = Rect.fromLTWH(0, 0, 200, 100);
97+
const element = Rect.fromLTWH(50, 20, 100, 40);
98+
const transform = (
99+
position: Offset.zero,
100+
rotation: 0.0,
101+
scaleX: -1.0,
102+
scaleY: 1.0,
103+
);
104+
105+
expectRectCloseTo(
106+
transform.scaleRect(element, selection),
107+
const Rect.fromLTWH(-150, 20, 100, 40),
108+
);
109+
});
94110
}
95111

96112
void expectRectCloseTo(Rect actual, Rect expected) {

0 commit comments

Comments
 (0)