Skip to content

Commit df01580

Browse files
committed
Add camera rotation support, closes #977
1 parent 29fdcb1 commit df01580

18 files changed

Lines changed: 309 additions & 166 deletions

app/lib/cubits/editor_renderer.dart

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -132,25 +132,14 @@ class RendererCubit extends Cubit<RendererRuntimeState> {
132132
final transform = transformCubit.state;
133133
final resolution = settingsCubit.state.renderResolution;
134134
final friction = transform.friction;
135-
final realWidth = size.width / transform.size;
136-
final realHeight = size.height / transform.size;
137-
Rect rect = Rect.fromLTWH(
138-
transform.position.dx,
139-
transform.position.dy,
140-
realWidth,
141-
realHeight,
142-
);
135+
Rect rect = transform.localToGlobalRect(Offset.zero & size);
143136
if (friction != null) {
144137
final beginPosition = transform.position - friction.beginOffset;
145-
final topLeft = Offset(
146-
min(transform.position.dx, beginPosition.dx),
147-
min(transform.position.dy, beginPosition.dy),
148-
);
149-
final frictionSize = Size(
150-
realWidth + (friction.beginOffset.dx * transform.size).abs(),
151-
realHeight + (friction.beginOffset.dy * transform.size).abs(),
138+
rect = rect.expandToInclude(
139+
transform
140+
.withPosition(beginPosition)
141+
.localToGlobalRect(Offset.zero & size),
152142
);
153-
rect = topLeft & frictionSize;
154143
}
155144
return _snapViewportRect(rect, size, transform, resolution);
156145
}
@@ -161,10 +150,7 @@ class RendererCubit extends Cubit<RendererRuntimeState> {
161150
CameraTransform transform,
162151
RenderResolution resolution,
163152
) {
164-
final screenRect = Rect.fromPoints(
165-
transform.globalToLocal(rect.topLeft),
166-
transform.globalToLocal(rect.bottomRight),
167-
);
153+
final screenRect = transform.globalToLocalRect(rect);
168154
final snappedRect = _expandScreenRect(
169155
Rect.fromLTRB(
170156
screenRect.left.floorToDouble(),
@@ -177,10 +163,7 @@ class RendererCubit extends Cubit<RendererRuntimeState> {
177163
(size.height * resolution.multiplier).ceilToDouble(),
178164
),
179165
);
180-
return Rect.fromPoints(
181-
transform.localToGlobal(snappedRect.topLeft),
182-
transform.localToGlobal(snappedRect.bottomRight),
183-
);
166+
return transform.localToGlobalRect(snappedRect);
184167
}
185168

186169
Rect _expandScreenRect(Rect rect, Size minimumSize) {

app/lib/cubits/editor_session.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class EditorSessionCubit extends Cubit<PersistedDocumentState> {
8181
_transformCubit.state.pixelRatio,
8282
Offset(state.camera.positionX, state.camera.positionY),
8383
state.camera.zoom,
84+
state.camera.rotation,
8485
);
8586

8687
int resolveToolIndex(DocumentInfo info) {
@@ -107,6 +108,7 @@ class EditorSessionCubit extends Cubit<PersistedDocumentState> {
107108
positionX: transform.position.dx,
108109
positionY: transform.position.dy,
109110
zoom: transform.size,
111+
rotation: transform.rotation,
110112
);
111113
if (state.camera == camera) return;
112114
emit(state.copyWith(camera: camera));

app/lib/cubits/transform.dart

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,30 +41,70 @@ sealed class CameraTransform with _$CameraTransform {
4141
@Default(1) double pixelRatio,
4242
@Default(Offset.zero) Offset position,
4343
@Default(1) double size,
44+
@Default(0) double rotation,
4445
FrictionState? friction,
4546
]) = _CameraTransform;
4647

4748
CameraTransform withPosition(Offset position) =>
48-
CameraTransform(pixelRatio, position, size);
49+
CameraTransform(pixelRatio, position, size, rotation);
4950

5051
CameraTransform withPointPosition(Point<double> position) =>
51-
CameraTransform(pixelRatio, position.toOffset(), size);
52+
CameraTransform(pixelRatio, position.toOffset(), size, rotation);
5253

5354
CameraTransform withSize(double size, [Offset cursor = Offset.zero]) {
5455
// Set size and focus on cursor if provided
5556
final double newSize = size.clamp(kMinZoom, kMaxZoom);
56-
var mx = localToGlobal(cursor);
57-
mx = (mx - position) * newSize;
58-
5957
return CameraTransform(
6058
pixelRatio,
61-
position + (mx - cursor) / newSize,
59+
localToGlobal(cursor) - cursor.rotate(Offset.zero, -rotation) / newSize,
6260
newSize,
61+
rotation,
62+
);
63+
}
64+
65+
CameraTransform withRotation(double rotation, [Offset cursor = Offset.zero]) {
66+
final normalized = (rotation + pi) % (2 * pi) - pi;
67+
return CameraTransform(
68+
pixelRatio,
69+
localToGlobal(cursor) - cursor.rotate(Offset.zero, -normalized) / size,
70+
size,
71+
normalized,
72+
);
73+
}
74+
75+
Offset localToGlobal(Offset local) =>
76+
local.rotate(Offset.zero, -rotation) / size + position;
77+
78+
Offset globalToLocal(Offset global) =>
79+
((global - position) * size).rotate(Offset.zero, rotation);
80+
81+
Offset localToGlobalDelta(Offset delta) =>
82+
delta.rotate(Offset.zero, -rotation) / size;
83+
84+
Rect localToGlobalRect(Rect rect) => _transformRect(rect, localToGlobal);
85+
86+
Rect globalToLocalRect(Rect rect) => _transformRect(rect, globalToLocal);
87+
88+
Rect _transformRect(Rect rect, Offset Function(Offset) transform) {
89+
final points = [
90+
transform(rect.topLeft),
91+
transform(rect.topRight),
92+
transform(rect.bottomLeft),
93+
transform(rect.bottomRight),
94+
];
95+
return Rect.fromLTRB(
96+
points.map((point) => point.dx).reduce(min),
97+
points.map((point) => point.dy).reduce(min),
98+
points.map((point) => point.dx).reduce(max),
99+
points.map((point) => point.dy).reduce(max),
63100
);
64101
}
65102

66-
Offset localToGlobal(Offset local) => local / size + position;
67-
Offset globalToLocal(Offset global) => (global - position) * size;
103+
void applyToCanvas(Canvas canvas) {
104+
canvas.rotate(rotation);
105+
canvas.scale(size);
106+
canvas.translate(-position.dx, -position.dy);
107+
}
68108

69109
double _getFinalTime(
70110
double velocity,
@@ -125,6 +165,7 @@ sealed class CameraTransform with _$CameraTransform {
125165
pixelRatio,
126166
finalPosition,
127167
finalScale,
168+
rotation,
128169
frictionState,
129170
);
130171
}
@@ -134,12 +175,13 @@ sealed class CameraTransform with _$CameraTransform {
134175
pixelRatio,
135176
this.position - position,
136177
this.size - size,
178+
rotation,
137179
null,
138180
);
139181
}
140182

141183
CameraTransform improve(RenderResolution resolution, Rect rect) {
142-
return CameraTransform(pixelRatio, rect.topLeft, size, friction);
184+
return CameraTransform(pixelRatio, rect.topLeft, size, 0, friction);
143185
}
144186
}
145187

@@ -149,14 +191,21 @@ class TransformCubit extends Cubit<CameraTransform> {
149191

150192
void move(Offset delta) => emit(state.withPosition(state.position + delta));
151193

152-
void teleport(Offset position, [double? scale]) =>
153-
emit(state.withPosition(position).withSize(scale ?? state.size));
194+
void teleport(Offset position, [double? scale, double? rotation]) => emit(
195+
state
196+
.withPosition(position)
197+
.withSize(scale ?? state.size)
198+
.withRotation(rotation ?? state.rotation),
199+
);
154200

155201
void zoom(double delta, [Offset cursor = Offset.zero]) =>
156202
emit(state.withSize(state.size * delta, cursor));
157203

158204
void focus(Offset cursor) => emit(state.withSize(state.size, cursor));
159205

206+
void rotate(double delta, [Offset cursor = Offset.zero]) =>
207+
emit(state.withRotation(state.rotation + delta, cursor));
208+
160209
void reset() => emit(CameraTransform(state.pixelRatio));
161210

162211
void size(double size, [Offset cursor = Offset.zero]) =>
@@ -519,6 +568,18 @@ class TransformCubit extends Cubit<CameraTransform> {
519568
teleport(clamped.position, clamped.size);
520569
}
521570

571+
void rotateConstrained(
572+
double delta, {
573+
required EditorRuntimeContext runtime,
574+
Offset cursor = Offset.zero,
575+
bool force = false,
576+
}) {
577+
if (delta == 0 || (runtime.viewCubit.state.locks.lockRotation && !force)) {
578+
return;
579+
}
580+
rotate(delta, cursor);
581+
}
582+
522583
void sizeConstrained(
523584
double size, {
524585
required EditorRuntimeContext runtime,

app/lib/cubits/transform.freezed.dart

Lines changed: 17 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/lib/models/persisted_document_state.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ sealed class PersistedCameraState with _$PersistedCameraState {
5555
@Default(0) double positionX,
5656
@Default(0) double positionY,
5757
@Default(1) double zoom,
58+
@Default(0) double rotation,
5859
}) = _PersistedCameraState;
5960

6061
factory PersistedCameraState.fromJson(Map<String, dynamic> json) =>
@@ -71,6 +72,7 @@ sealed class PersistentLockState with _$PersistentLockState {
7172
@Default(false) bool lockZoom,
7273
@Default(false) bool lockHorizontal,
7374
@Default(false) bool lockVertical,
75+
@Default(false) bool lockRotation,
7476
}) = _PersistentLockState;
7577

7678
factory PersistentLockState.fromJson(Map<String, dynamic> json) =>

0 commit comments

Comments
 (0)