Skip to content

bugfix(rotation): improve rotation logic #106

Open
BenAuerDev wants to merge 6 commits into
mainfrom
bugfix/beau/rotation
Open

bugfix(rotation): improve rotation logic #106
BenAuerDev wants to merge 6 commits into
mainfrom
bugfix/beau/rotation

Conversation

@BenAuerDev

@BenAuerDev BenAuerDev commented Feb 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Summary

  • Rewrote rotation drag handle math to use direct center-to-cursor angle calculation instead of unreliable delta accumulation
  • Fixed image position shifting after rotation by preserving canvas scale factor and compensating for trim center offset
  • Rewrote SVG rotation to compose multiple rotations into a single transform, preventing bounding box inflation
  • Removed dead code (rotationSignal, onRotate state mutation) that leaked rotation state when switching tools
  • Added 39 tests covering bitmap and SVG rotation

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore
  • 🧪 Tests

Summary by CodeRabbit

  • Bug Fixes

    • Image rotation preserves scale and center to prevent jumping
    • SVG rotation preserves original content size so repeated rotations keep consistent bounds
    • Improved error handling for image operations
  • Refactor

    • Rotation controls simplified and made internal to the widget
    • Resizing always shows corner handles with symmetric scaling
  • Tests

    • Added comprehensive unit tests for image rotation and SVG transformations

Replaced unreliable delta-accumulation drag math with direct
center-to-cursor angle calculation. Added degree display during
rotation. Removed dead rotationSignal and onRotate state mutation
that leaked rotation state when switching tools.
…osition

Refactored processRotation to return typed RotationResult instead of
Map. rotateImage() now preserves canvas scale factor and centers image
after rotation by compensating for trim center offset.
…ing box

Rotations are now composed into a single transform using data-orig-*
attributes to track original dimensions. Prevents bounding box from
growing with repeated rotations. Fixed svgStringToPngBytes canvas
drawing and exception catches.
Covers processRotation, trimPngAsBytes, getImageFromBytes, SVG parsing
utilities, rotation composition, bounding box stability, and rotation
helper methods. 39 tests total.
@BenAuerDev BenAuerDev self-assigned this Feb 23, 2026
@coderabbitai

coderabbitai Bot commented Feb 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Rotation state was centralized inside RotationWrapper (removing external rotationSignal/onRotate/initialRotation), raster rotation now returns trim offsets, SVG rotation composes prior rotations using preserved original-size attributes, move/resize tool was simplified to rotation-agnostic rendering, and unit tests for raster and SVG rotation utilities were added.

Changes

Cohort / File(s) Summary
Rotation UI & orchestration
apps/flites/lib/widgets/rotation/rotation_wrapper.dart, apps/flites/lib/feature_kits/tools/rotate_tool.dart, apps/flites/lib/feature_kits/tools/move_resize_tool.dart
Removed external rotationSignal/onRotate/initialRotation API surface; RotationWrapper now manages rotation internally via angle-from-center pan gestures; rotate/move tools simplified to rotation-agnostic TransformableBox usage.
Raster image processing
apps/flites/lib/utils/image_processing_utils.dart, apps/flites/lib/types/flites_image.dart
Added RotationResult (bytes + trim-center offsets); rotateInIsolates/processRotation return offsets; FlitesImage.rotateImage preserves scale/center, compensates for asymmetric trimming, resets transient rotation, and saves changes.
SVG rotation logic
apps/flites/lib/utils/svg_utils.dart
Rewrote rotateAndTrimSvg to reuse original content size, detect/compose prior rotations via data-orig-* attrs, compute combined rotation & center, and added helpers: getDataAttribute, ensureOriginalSizeAttributes, extractExistingRotationDegrees, stripRotationWrapper.
Editor minor cleanup
apps/flites/lib/feature_kits/tools/canvas_helpers/image_editor.dart
Removed an unused import and reformatted builder/watch blocks; no behavioral changes.
Tests
apps/flites/test/utils/image_rotation_test.dart, apps/flites/test/utils/svg_rotation_test.dart
Added unit tests covering raster rotation trimming/offsets and comprehensive SVG utility behavior (parsing, rotation composition, bounding-box stability, edge cases).

Sequence Diagram

sequenceDiagram
    participant User
    participant RW as RotationWrapper
    participant FI as FlitesImage
    participant IPU as ImageProcessingUtils
    participant SvgU as SvgUtils

    User->>RW: Drag (pan) around center to rotate
    RW->>RW: compute angle from center, update internal rotation
    User->>RW: Release (apply)
    RW->>FI: request rotateImage(angle)
    alt Raster image
        FI->>IPU: rotateInIsolates / processRotation(bytes, angle)
        IPU-->>FI: RotationResult { imageBytes, trimOffsetX, trimOffsetY }
    else SVG image
        FI->>SvgU: rotateAndTrimSvg(svgData, angle)
        SvgU-->>FI: rotated SVG bytes (composed with any prior rotation, original-size attrs preserved)
    end
    FI->>FI: preserve scale & center, apply trim offsets, reset transient rotation, save changes
    FI-->>RW: updated image state
    RW->>User: UI reflects rotated & re-centered image
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • SimonErich

Poem

🐰 I twirled a carrot in a neat little arc,
Angles live inside now — no signal, no spark.
Trimmed edges tucked close, center holds tight,
Rotations compose gently, the canvas feels right.
Hop, spin, and smile — the picture's just smart. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'bugfix(rotation): improve rotation logic' is directly related to the main objectives of the PR, which center on rewriting rotation math, fixing image position issues, and improving rotation composition. It accurately summarizes the primary change focus.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/beau/rotation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/flites/lib/utils/svg_utils.dart (1)

232-234: Regex injection risk is low but worth noting.

getDataAttribute interpolates name directly into a regex pattern. The current callers all pass literal data-orig-* strings which are safe. If this method is ever called with user-controlled input, the unescaped string could break the regex or match unintended content. Consider using RegExp.escape(name) for defense-in-depth.

Defensive fix
  static double? getDataAttribute(String attributes, String name) {
-   final match = RegExp('$name="([^"]*)"').firstMatch(attributes);
+   final match = RegExp('${RegExp.escape(name)}="([^"]*)"').firstMatch(attributes);
    return match != null ? double.tryParse(match.group(1)!) : null;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/utils/svg_utils.dart` around lines 232 - 234, The
getDataAttribute function interpolates the name parameter directly into a RegExp
which can be broken or exploited if name ever comes from user input; update
getDataAttribute to escape the name before building the pattern (use
RegExp.escape(name)) so the generated regex matches the literal attribute name
safely and still extracts the captured value for double.tryParse.
apps/flites/lib/utils/image_processing_utils.dart (1)

50-51: Nit: comment about isolate type restrictions is slightly inaccurate.

Dart isolates via compute() can pass many types beyond primitives (including Uint8List, List, custom classes). The Map approach works fine, but the comment overstates the constraint. Consider softening to something like "we use a Map for simplicity across the isolate boundary."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/utils/image_processing_utils.dart` around lines 50 - 51, The
comment stating "Isolate entry point — accepts and returns Maps because Dart
isolates can only pass primitive types across the boundary." overstates Dart's
restrictions; replace it with a softer, accurate note such as: "Isolate entry
point — we use a Map for simplicity when passing data across the isolate
boundary (other types like Uint8List and Lists are supported by compute())."
Locate and update the comment above the isolate entry function / compute usage
in image_processing_utils.dart so it references the Map choice as a convenience
rather than a language limitation.
apps/flites/lib/types/flites_image.dart (1)

88-89: Weak unique ID generation.

The ID is constructed from millisecondsSinceEpoch plus two small random ints. In a batch-import scenario (e.g., importing multiple files in a loop), DateTime.now().millisecondsSinceEpoch can repeat within the same millisecond, and Random().nextInt(14000) / Random().nextInt(15000) has a non-trivial collision probability (~1 in 210M per pair). Consider using a UUID package or at least a single Random.secure() instance with a wider range.

This is pre-existing code, not introduced in this PR, so feel free to defer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/types/flites_image.dart` around lines 88 - 89, The current ID
assignment (the id field set with
'${DateTime.now().millisecondsSinceEpoch}-${Random().nextInt(14000)}-${Random().nextInt(15000)}')
is weak and can collide; replace it with a proper UUID or a cryptographically
strong random generator: either import and use a UUID library (e.g. uuid.v4())
to set the id, or create a single Random.secure() instance and generate a larger
bit-width value (e.g. 64+ bits) instead of two small nextInt calls; update the
code where id is assigned (the id field/constructor in the FlitesImage class or
the method that currently builds this string) to use the chosen approach and
remove the current DateTime+small-random concatenation.
apps/flites/lib/widgets/rotation/rotation_wrapper.dart (1)

31-37: circleRadius is computed once in initState and won't update if widget.rect changes.

If the parent rebuilds this widget with a different rect (e.g., after resizing the image), circleRadius will still reflect the stale value. Consider moving the calculation to build() or adding a didUpdateWidget override.

Option: use didUpdateWidget
+ `@override`
+ void didUpdateWidget(covariant RotationWrapper oldWidget) {
+   super.didUpdateWidget(oldWidget);
+   if (oldWidget.rect.size != widget.rect.size) {
+     circleRadius = (longestSideSize(widget.rect.size) / 2) * 1.5;
+   }
+ }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart` around lines 31 - 37,
circleRadius is computed once in initState so it won't update when widget.rect
changes; move the computation out of initState or update it in didUpdateWidget:
either recompute circleRadius inside build() using
longestSideSize(widget.rect.size) / 2 * 1.5, or override
didUpdateWidget(oldWidget) to detect changes to widget.rect and recalculate
circleRadius there (and call setState if needed) so the value stays in sync with
updates to widget.rect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/flites/lib/utils/svg_utils.dart`:
- Around line 155-189: The code uses previouslyRotated to branch but
storedW/storedH remain nullable, causing Size(storedW, storedH) to fail; update
the if to directly check storedW != null && storedH != null (so Dart will
promote them) or explicitly unwrap/coalesce when assigning
originalContentSize—e.g., replace the previouslyRotated branch condition with if
(storedW != null && storedH != null) { originalContentSize = Size(storedW!,
storedH!); ... } or use Size(storedW ?? 0, storedH ?? 0) if a default is
acceptable; adjust references to rotationCenter and other uses accordingly.

In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart`:
- Around line 47-51: The current _rotationDegrees getter uses (rotation * 180 /
pi) % 360 which yields wrong results for large negative rotations because Dart's
% preserves the left operand sign; update the normalization to always map into
(-180, 180] by computing degrees = rotation * 180 / pi and then normalized =
(((degrees + 180) % 360) + 360) % 360 - 180 (then format
normalized.toStringAsFixed(1)°); replace the existing normalization line in
_rotationDegrees with this stable expression.

In `@apps/flites/test/utils/image_rotation_test.dart`:
- Around line 133-149: The test currently redefines longestSideSize locally
instead of importing the production helper; remove the local definition of
longestSideSize from the test file and add an import for rotation_wrapper.dart
so the tests exercise the actual rotation_wrapper.dart::longestSideSize used
elsewhere (e.g., rotate_tool.dart). Ensure the test file imports the same symbol
(longestSideSize) from rotation_wrapper.dart and delete the duplicate function
at the file end.

---

Nitpick comments:
In `@apps/flites/lib/types/flites_image.dart`:
- Around line 88-89: The current ID assignment (the id field set with
'${DateTime.now().millisecondsSinceEpoch}-${Random().nextInt(14000)}-${Random().nextInt(15000)}')
is weak and can collide; replace it with a proper UUID or a cryptographically
strong random generator: either import and use a UUID library (e.g. uuid.v4())
to set the id, or create a single Random.secure() instance and generate a larger
bit-width value (e.g. 64+ bits) instead of two small nextInt calls; update the
code where id is assigned (the id field/constructor in the FlitesImage class or
the method that currently builds this string) to use the chosen approach and
remove the current DateTime+small-random concatenation.

In `@apps/flites/lib/utils/image_processing_utils.dart`:
- Around line 50-51: The comment stating "Isolate entry point — accepts and
returns Maps because Dart isolates can only pass primitive types across the
boundary." overstates Dart's restrictions; replace it with a softer, accurate
note such as: "Isolate entry point — we use a Map for simplicity when passing
data across the isolate boundary (other types like Uint8List and Lists are
supported by compute())." Locate and update the comment above the isolate entry
function / compute usage in image_processing_utils.dart so it references the Map
choice as a convenience rather than a language limitation.

In `@apps/flites/lib/utils/svg_utils.dart`:
- Around line 232-234: The getDataAttribute function interpolates the name
parameter directly into a RegExp which can be broken or exploited if name ever
comes from user input; update getDataAttribute to escape the name before
building the pattern (use RegExp.escape(name)) so the generated regex matches
the literal attribute name safely and still extracts the captured value for
double.tryParse.

In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart`:
- Around line 31-37: circleRadius is computed once in initState so it won't
update when widget.rect changes; move the computation out of initState or update
it in didUpdateWidget: either recompute circleRadius inside build() using
longestSideSize(widget.rect.size) / 2 * 1.5, or override
didUpdateWidget(oldWidget) to detect changes to widget.rect and recalculate
circleRadius there (and call setState if needed) so the value stays in sync with
updates to widget.rect.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 351fbb4 and ee19d66.

📒 Files selected for processing (9)
  • apps/flites/lib/feature_kits/tools/canvas_helpers/image_editor.dart
  • apps/flites/lib/feature_kits/tools/move_resize_tool.dart
  • apps/flites/lib/feature_kits/tools/rotate_tool.dart
  • apps/flites/lib/types/flites_image.dart
  • apps/flites/lib/utils/image_processing_utils.dart
  • apps/flites/lib/utils/svg_utils.dart
  • apps/flites/lib/widgets/rotation/rotation_wrapper.dart
  • apps/flites/test/utils/image_rotation_test.dart
  • apps/flites/test/utils/svg_rotation_test.dart
💤 Files with no reviewable changes (1)
  • apps/flites/lib/feature_kits/tools/rotate_tool.dart

Comment thread apps/flites/lib/utils/svg_utils.dart
Comment thread apps/flites/lib/widgets/rotation/rotation_wrapper.dart
Comment thread apps/flites/test/utils/image_rotation_test.dart Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/flites/lib/widgets/rotation/rotation_wrapper.dart (1)

203-205: Move longestSideSize out of the widget layer.

This helper is now shared outside RotationWrapper, but it lives in a file that also pulls in widget/app-state dependencies. A small geometry utility would keep non-UI callers from importing the whole widget layer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart` around lines 203 -
205, Extract the pure-geometry helper longestSideSize out of
rotation_wrapper.dart into a new non-UI utility (e.g., a geometry or math utils
file) so callers that need only geometry won’t import widget/app-state code;
create a new dart file (e.g., geometry_utils.dart) with the
longestSideSize(Size) implementation (keeping it independent of any Flutter
widget or app-state imports), update all call sites (including rotate_tool.dart
and RotationWrapper) to import the new utility, and remove the helper from
rotation_wrapper.dart so the widget layer no longer exposes non-UI utilities.
apps/flites/lib/utils/svg_utils.dart (1)

230-234: Accept both quote styles for data-orig-* parsing.

getAttributes operates on arbitrary SVG markup, but this helper only matches double-quoted attributes. Supporting single quotes too would make the stored-rotation path as tolerant as getSvgSize and getViewBox.

Suggested fix
   static double? getDataAttribute(String attributes, String name) {
-    final match = RegExp('$name="([^"]*)"').firstMatch(attributes);
+    final escapedName = RegExp.escape(name);
+    final match =
+        RegExp('$escapedName="([^"]*)"').firstMatch(attributes) ??
+        RegExp("$escapedName='([^']*)'").firstMatch(attributes);
     return match != null ? double.tryParse(match.group(1)!) : null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/utils/svg_utils.dart` around lines 230 - 234,
getDataAttribute currently only matches double-quoted attributes; change its
RegExp to accept both double- and single-quoted attribute values (e.g., match
name="..." OR name='...') and then use whichever capture group matched (or
combine into one capture by using an alternation) before calling
double.tryParse; update the RegExp and the return logic in getDataAttribute so
it tolerates single quotes like getSvgSize/getViewBox do.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart`:
- Around line 121-127: The rotation snap occurs because the raw delta
(currentAngle - _dragStartAngle) can jump by ~±2π across the ±π seam in
onPanUpdate; to fix, compute the delta using _angleFromCenter and then normalize
it into the (-π, π] range before applying it: calculate delta = currentAngle -
_dragStartAngle, then while (delta > π) subtract 2π and while (delta < -π) add
2π, and finally set rotation = _rotationAtDragStart + delta (keep references to
onPanUpdate, _angleFromCenter, _rotationAtDragStart, _dragStartAngle, and
rotation).
- Around line 65-67: circleRadius is computed once in initState() from
widget.rect.size causing the gesture/overlay SizedBox to use a stale radius when
the parent supplies a new widget.rect; update the computation to always reflect
current widget.rect by either moving the circleRadius calculation into build()
(so SizedBox uses the latest value) or override didUpdateWidget() in
RotationWrapper to recompute and setState() when oldWidget.rect != widget.rect,
ensuring the SizedBox height/width and any gesture bounds stay in sync with the
rotated content that uses widget.rect directly.

---

Nitpick comments:
In `@apps/flites/lib/utils/svg_utils.dart`:
- Around line 230-234: getDataAttribute currently only matches double-quoted
attributes; change its RegExp to accept both double- and single-quoted attribute
values (e.g., match name="..." OR name='...') and then use whichever capture
group matched (or combine into one capture by using an alternation) before
calling double.tryParse; update the RegExp and the return logic in
getDataAttribute so it tolerates single quotes like getSvgSize/getViewBox do.

In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart`:
- Around line 203-205: Extract the pure-geometry helper longestSideSize out of
rotation_wrapper.dart into a new non-UI utility (e.g., a geometry or math utils
file) so callers that need only geometry won’t import widget/app-state code;
create a new dart file (e.g., geometry_utils.dart) with the
longestSideSize(Size) implementation (keeping it independent of any Flutter
widget or app-state imports), update all call sites (including rotate_tool.dart
and RotationWrapper) to import the new utility, and remove the helper from
rotation_wrapper.dart so the widget layer no longer exposes non-UI utilities.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef987445-4ec4-475d-9703-d217efee61fe

📥 Commits

Reviewing files that changed from the base of the PR and between ee19d66 and 7c6a8fb.

📒 Files selected for processing (3)
  • apps/flites/lib/utils/svg_utils.dart
  • apps/flites/lib/widgets/rotation/rotation_wrapper.dart
  • apps/flites/test/utils/image_rotation_test.dart

Comment thread apps/flites/lib/widgets/rotation/rotation_wrapper.dart Outdated
Comment thread apps/flites/lib/widgets/rotation/rotation_wrapper.dart
- Normalize _rotationDegrees to (-180, 180]
- Compute circle radius from current rect (getter) instead of initState
- Normalize pan delta to (-π, π] to avoid snap when crossing seam

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/flites/lib/widgets/rotation/rotation_wrapper.dart (1)

180-193: ⚠️ Potential issue | 🟠 Major

Guard the async apply path against re-entry and disposal.

The apply button remains enabled while rotateImage awaits on line 185, allowing rapid taps to enqueue multiple rotation operations. Additionally, setState runs unconditionally after the await completes, causing an error if the user navigates away during the async operation (setState after dispose).

Suggested fix
 class _RotationWrapperState extends State<RotationWrapper> {
   double rotation = 0;
   double _dragStartAngle = 0;
   double _rotationAtDragStart = 0;
+  bool _isApplyingRotation = false;
...
                   IconButton(
                     icon: Icon(
                       Icons.check_circle_outlined,
                       color: context.colors.surfaceContainer,
                     ),
-                    onPressed: () async {
-                      final currentImage =
-                          getFliteImage(SelectedImageState.selectedImageId);
-
-                      if (currentImage != null) {
-                        await currentImage.rotateImage(rotation);
-                      }
-
-                      setState(() {
-                        rotation = 0;
-                      });
-
-                      toolController.selectedTool = Tool.canvas;
+                    onPressed: _isApplyingRotation
+                        ? null
+                        : () async {
+                            setState(() {
+                              _isApplyingRotation = true;
+                            });
+                            try {
+                              final currentImage = getFliteImage(
+                                SelectedImageState.selectedImageId,
+                              );
+
+                              if (currentImage != null) {
+                                await currentImage.rotateImage(rotation);
+                              }
+
+                              if (!mounted) return;
+                              setState(() {
+                                rotation = 0;
+                              });
+                              toolController.selectedTool = Tool.canvas;
+                            } finally {
+                              if (mounted) {
+                                setState(() {
+                                  _isApplyingRotation = false;
+                                });
+                              }
+                            }
+                          },
                   ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart` around lines 180 -
193, Protect the async apply path by adding a re-entry guard boolean (e.g.
_isApplying) checked at the start of the onPressed handler and set true before
awaiting SelectedImageState.selectedImageId ->
getFliteImage(...).rotateImage(rotation), and set false at the end; also disable
the apply button when _isApplying is true. After the await, only call setState
and update toolController.selectedTool if the widget is still mounted (check
mounted) to avoid setState after dispose; ensure rotation is reset inside the
guarded mounted check and that the guard is cleared in finally so the button
becomes usable again.
♻️ Duplicate comments (1)
apps/flites/lib/widgets/rotation/rotation_wrapper.dart (1)

42-45: ⚠️ Potential issue | 🟡 Minor

Exact 180° still formats as -180.0°.

Line 44 normalizes into [-180, 180), so both pi and -pi render as -180.0°. The PR summary says the display should be (-180, 180], so this edge case is still flipped.

Suggested fix
 String get _rotationDegrees {
-  final degrees = rotation * 180 / pi;
-  final normalized = (((degrees + 180) % 360) + 360) % 360 - 180;
+  var normalized = (rotation * 180 / pi) % 360;
+  if (normalized <= -180) normalized += 360;
+  if (normalized > 180) normalized -= 360;
   return '${normalized.toStringAsFixed(1)}°';
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart` around lines 42 - 45,
The _rotationDegrees getter currently normalizes degrees into [-180, 180)
causing both +pi and -pi to format as "-180.0°"; update the normalization in
_rotationDegrees (variables degrees/normalized) to produce the range (-180, 180]
instead — e.g., keep the existing normalization but after computing normalized,
if normalized == -180.0 then set normalized = 180.0 so the edge case renders as
"180.0°".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart`:
- Around line 180-193: Protect the async apply path by adding a re-entry guard
boolean (e.g. _isApplying) checked at the start of the onPressed handler and set
true before awaiting SelectedImageState.selectedImageId ->
getFliteImage(...).rotateImage(rotation), and set false at the end; also disable
the apply button when _isApplying is true. After the await, only call setState
and update toolController.selectedTool if the widget is still mounted (check
mounted) to avoid setState after dispose; ensure rotation is reset inside the
guarded mounted check and that the guard is cleared in finally so the button
becomes usable again.

---

Duplicate comments:
In `@apps/flites/lib/widgets/rotation/rotation_wrapper.dart`:
- Around line 42-45: The _rotationDegrees getter currently normalizes degrees
into [-180, 180) causing both +pi and -pi to format as "-180.0°"; update the
normalization in _rotationDegrees (variables degrees/normalized) to produce the
range (-180, 180] instead — e.g., keep the existing normalization but after
computing normalized, if normalized == -180.0 then set normalized = 180.0 so the
edge case renders as "180.0°".

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9589c231-c2bb-4cdc-8506-1701f1bddfee

📥 Commits

Reviewing files that changed from the base of the PR and between 7c6a8fb and e3eb3c6.

📒 Files selected for processing (1)
  • apps/flites/lib/widgets/rotation/rotation_wrapper.dart

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant