Skip to content

Conversation

@mateuszJS
Copy link
Owner

@mateuszJS mateuszJS commented Aug 24, 2025

Summary by CodeRabbit

  • Bug Fixes

    • Prevents elements from collapsing below a 1x1 minimum and reduces flip/mirroring during resize.
    • Rotation handle rotates more accurately around element center, including mirrored cases.
    • More consistent pointer tracking for drag/rotate interactions.
  • Refactor

    • New matrix-based transform pipeline for smoother, more stable interactions and reduced visual jitter.
    • Introduces a stable default bounding rectangle used when none is provided.

Copilot AI review requested due to automatic review settings August 24, 2025 21:25
@coderabbitai
Copy link

coderabbitai bot commented Aug 24, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

Refactors transformPoints to use a forward, matrix-centric transform built from rectangle bounds; changes the API to accept bounds and a Point pointer; adds matrix pivot/rotate utilities and a DEFAULT_BOUNDS constant; updates call sites and clamps resulting bounds to avoid sub-1x1 geometry.

Changes

Cohort / File(s) Summary
Transform core
src/logic/transform_ui.zig
Signature changed to transformPoints(ui_component_id: u32, bounds: *[4]PointUV, raw_pointer: Point). Rewrote to build a matrix from bounds, compute pointer via inverse, apply per-corner/edge updates using matrix.pivotScale, handle rotation via rotateScaled (with mirror detection), map DEFAULT_BOUNDS through the matrix back into bounds, and clamp small extents.
Call site
src/logic/index.zig
Updated onPointerMove to call TransformUI.transformPoints with types.Point{ .x = x, .y = y } instead of separate x, y floats.
Matrix API
src/logic/matrix.zig
Added pivotScale(sx, sy, px, py), rotateScaled(angle_rad, aspect), and isMirrored() on Matrix3x3 to support pivoted scaling, anisotropic rotation, and mirror detection.
Defaults const
src/logic/consts.zig
Added pub const DEFAULT_BOUNDS = [4]PointUV{ ... } representing a unit rectangle with UVs.
Shapes usage
src/logic/shapes/shapes.zig
Replaced local DEFAULT_BOUNDS with imported DEFAULT_BOUNDS and use input_bounds orelse DEFAULT_BOUNDS.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant User
  participant Index as onPointerMove
  participant TransformUI as transformPoints
  participant Matrix as Matrix3x3
  participant Geometry

  User->>Index: pointer move (x,y)
  Index->>TransformUI: transformPoints(ui_id, bounds, Point{x,y})
  TransformUI->>Matrix: m = Matrix3x3.getMatrixFromRectangle(bounds.*)
  TransformUI->>Matrix: i_m = m.inverse()
  TransformUI->>Geometry: raw_in_bounds = i_m.get(raw_pointer)
  alt corner/edge adjustments
    TransformUI->>Matrix: matrix.pivotScale(...) per-case
  else rotation
    TransformUI->>Geometry: center = bounds[0].mid(bounds[2])
    TransformUI->>Geometry: compute asset_new_angle (center.angleTo(raw_pointer) - ...) 
    TransformUI->>Matrix: if matrix.isMirrored() adjust angle
    TransformUI->>Matrix: matrix.rotateScaled(asset_new_angle, aspect)
  end
  TransformUI->>Geometry: map DEFAULT_BOUNDS via matrix -> new_bounds
  TransformUI->>Geometry: clamp extents to >=1.0 using angle cos/sin
  TransformUI->>Index: write updated bounds
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

released on @next

Poem

I hop through matrices, quick and bright,
Pivot, scale, then spin just right.
DEFAULT_BOUNDS I map and trace,
Clamp each corner, keep its place.
A rabbit’s tweak to shape and light. 🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 37c21d3 and 5e037f2.

📒 Files selected for processing (3)
  • src/logic/consts.zig (1 hunks)
  • src/logic/shapes/shapes.zig (1 hunks)
  • src/logic/transform_ui.zig (2 hunks)
✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor-transform-ui

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@mateuszJS mateuszJS changed the title Refactor transform UI fix: Refactor transform UI Aug 24, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the transform UI code to improve readability and maintainability by simplifying the transformation logic and reducing code duplication. The changes focus on streamlining how UI component transformations are applied to point bounds.

  • Renamed parameter from points to bounds for clarity
  • Simplified matrix transformation logic by combining rotation and translation operations
  • Replaced manual point transformation with a loop-based approach

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (7)
src/logic/transform_ui.zig (7)

57-62: Confirm Matrix3x3.get accepts PointUV inputs (type interop).

points is built as [4]Point while bounds is [4]PointUV, and matrix.get(bounds[i]) implies get is generic over both or can accept PointUV. If get returns a Point, this is fine; if it returns a PointUV, the explicit [4]Point type on points may be unnecessary or could mismatch.

  • If get is generic, consider declaring points as [4]@TypeOf(matrix.get(bounds[0])) for clarity.
  • Otherwise, be explicit about the conversion to avoid accidental copies or method resolution surprises.

67-92: Corner/edge handlers look correct in local space.

Assignments for cases 1–8 are consistent and minimal. Good symmetry and no off-by-one pitfalls observed. Consider a small helper to reduce duplication if you plan to extend constraints (e.g., aspect-ratio lock with Shift, symmetric scale with Alt), but not required for this PR.

Also applies to: 95-114


118-123: Rotation semantics: verify “absolute vs. delta” and pivot; simplify loop.

  • Angle: Using atan2(pointer.y, pointer.x) + π/2 as the new rotation in local space is plausible, but it effectively sets an absolute local-space angle, not a delta from the current one. Given you first aligned to the object’s frame, this should behave as intended. Still, please verify expected behavior when the user rotates, stops, then resumes rotation from a different handle position—the result should not “snap” unexpectedly.
  • Pivot: The loop applies new_rotation around the origin. This is OK only if the center is at (0,0) in local space (see the earlier comment). If center is not at origin due to composition order, rotation will orbit the rectangle.

Minor clean-up: you can assign the whole struct rather than per-field.

Apply this tiny refactor:

-            for (&points) |*p| {
-                const new_point = new_rotation.get(p);
-                p.x = new_point.x;
-                p.y = new_point.y;
-            }
+            for (&points) |*p| {
+                p.* = new_rotation.get(p.*);
+            }

Also applies to: 126-130


135-143: Min-size clamp: use axis deltas instead of Euclidean distance.

Because the local-space rect is axis-aligned, checking Euclidean distance is overkill and slightly less explicit. Using axis deltas removes a sqrt and better communicates intent.

Apply this refactor:

-    // make sure bounds is not smaller tan 1x1(it removed tons of edge cases)
-    if (points[0].distance(points[1]) < 1.0) {
+    // make sure bounds is not smaller than 1x1 (this removed tons of edge cases)
+    if (@abs(points[1].x - points[0].x) < 1.0) {
         points[1].x = points[0].x + 1.0;
         points[2].x = points[3].x + 1.0;
     }
-    if (points[0].distance(points[3]) < 1.0) {
+    if (@abs(points[3].y - points[0].y) < 1.0) {
         points[3].y = points[0].y + 1.0;
         points[2].y = points[1].y + 1.0;
     }

49-51: Fix typos in comments.

Nit-level, but worth cleaning up in a refactor PR.

-    var matrix = Matrix3x3.rotation(-asset_angle_y); // transfor matrix
-    matrix.translate(-asset_center.x, -asset_center.y); // useufl for angle and mirrored scaling(with shift/alt)
+    var matrix = Matrix3x3.rotation(-asset_angle_y); // transform matrix
+    matrix.translate(-asset_center.x, -asset_center.y); // useful for angle and mirrored scaling (with Shift/Alt)
@@
-    // make sure bounds is not smaller tan 1x1(it removed tons of edge cases)
+    // make sure bounds is not smaller than 1x1 (this removed tons of edge cases)

Also applies to: 135-135


146-151: Inverse on an orthonormal + translation matrix: consider a specialized inverse and NaN guard.

matrix is rotation + translation, so it should be orthonormal. A specialized inverse (transpose of rotation and translated by the negative rotated offset) is faster and numerically stable if available, e.g., inverseOrthonormal(). If not implemented, current inverse() is fine.

Also consider guarding against NaNs/Infs during interactive drags to avoid corrupting bounds on rare numerical spikes.

Optionally:

-    const i_matrix = matrix.inverse();
+    const i_matrix = matrix.inverse(); // or matrix.inverseOrthonormal() if available
     for (bounds, points) |*b, p| {
         const t_p = i_matrix.get(p);
+        if (!std.math.isFinite(t_p.x) or !std.math.isFinite(t_p.y)) continue;
         b.x = t_p.x;
         b.y = t_p.y;
     }

44-151: Add quick tests for degenerate and reflection edge cases.

The integration tests cover rotation, scaling, reflect-X via top-middle, and rotate-after-reflect (great). To lock in this refactor, add cases for:

  • Reflect-Y via middle-left/middle-right, then rotate.
  • Drag to within <1px width/height to exercise the clamp branches.
  • Rotate, then scale with a corner handle, then rotate again (accumulated transforms).

I can draft Playwright steps mirroring your existing integration-tests/init.ts utilities if helpful.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 09821ab and 2b2b137.

📒 Files selected for processing (1)
  • src/logic/transform_ui.zig (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/logic/transform_ui.zig (2)
integration-tests/init.ts (2)
  • resizeAsset (127-162)
  • getRotationHandle (99-113)
integration-tests/tests/asset-basic-transform.spec.ts (1)
  • testinfo (6-36)
🔇 Additional comments (1)
src/logic/transform_ui.zig (1)

44-63: Matrix composition order confirmed correct (pivot at center)

I verified that Matrix3x3.translate does a post-multiplication (self.* = multiply(self.*, translation)), so your code:

var matrix = Matrix3x3.rotation(-asset_angle_y);
matrix.translate(-asset_center.x, -asset_center.y);

produces R * T. When you call matrix.get(p), it effectively does R * (T * p), i.e. translate first, then rotate—exactly what you want for a rotation about asset_center. No swap in the call order is needed.

Optional safeguard: you can still include the debug-only assertions to catch any future regressions, but functionally the current composition is correct.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@mateuszJS mateuszJS merged commit 36d4467 into next Aug 25, 2025
4 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (2)
src/logic/matrix.zig (2)

332-335: Tests won’t compile: inverse() returns Matrix3x3, not ?Matrix3x3

inverse() panics on non-invertible matrices; it does not return an optional. The test uses orelse, which is invalid and breaks the build.

Apply this diff to fix the test:

-    const inv = m.inverse() orelse @panic("Matrix not invertible");
+    const inv = m.inverse();

346-355: Tests won’t compile: getTransformBetween() returns Matrix3x3, not ?Matrix3x3

Similar to the previous comment, the test incorrectly expects an optional.

Apply this diff to fix the test:

-    const transform = Matrix3x3.getTransformBetween(matrix1, matrix2) orelse @panic("Transform failed");
+    const transform = Matrix3x3.getTransformBetween(matrix1, matrix2);
🧹 Nitpick comments (5)
src/logic/matrix.zig (1)

223-231: Guard against aspect = 0 and document behavior for extreme aspect ratios

rotateScaled divides by aspect; if height collapses to ~0 the call site will feed in aspect ≈ ∞ or 0, leading to Inf/NaN. Even if upstream clamps after the transform, a degenerate input can still happen (e.g., newly imported zero-height bounds). Add a small epsilon guard to keep the matrix well-defined.

Apply this diff to make rotateScaled numerically robust:

 pub fn rotateScaled(self: *Matrix3x3, angle_rad: f32, aspect: f32) void {
-        const c = std.math.cos(angle_rad);
-        const s = std.math.sin(angle_rad);
+        const c = std.math.cos(angle_rad);
+        const s = std.math.sin(angle_rad);
+        const eps: f32 = 1e-6;
+        const safe_aspect = if (@abs(aspect) < eps) (if (aspect < 0) -eps else eps) else aspect;
         self.* = Matrix3x3.multiply(self.*, Matrix3x3.from([_]f32{
-            c,          -s / aspect, 0.0,
-            s * aspect, c,           0.0,
+            c,              -s / safe_aspect, 0.0,
+            s * safe_aspect, c,               0.0,
             0.0,        0.0,         1.0,
         }));
 }

Note: The anisotropic rotation form [c, -s/aspect; s*aspect, c] matches S(w,h)^-1 · R(θ) · S(w,h) with aspect = w/h.

src/logic/transform_ui.zig (4)

44-47: Pointer mapping via inverse() is elegant—add a safety net for singular bounds

Building the local-space pointer with matrix.inverse().get(raw_pointer) is the right approach. However, if bounds become near-degenerate (e.g., imported as 0×H, or from a prior bug), inverse() will panic. Suggest an early guard or a fallback.

Minimal guard idea:

-    var matrix = Matrix3x3.getMatrixFromRectangle(bounds.*);
-    const pointer = matrix.inverse().get(raw_pointer);
+    var matrix = Matrix3x3.getMatrixFromRectangle(bounds.*);
+    // Avoid inverse panic on degenerate rectangles.
+    const w = bounds[0].distance(bounds[1]);
+    const h = bounds[0].distance(bounds[3]);
+    if (w < 1e-6 or h < 1e-6) return; // or: skip update / clamp to minimal safe size
+    const pointer = matrix.inverse().get(raw_pointer);

If you prefer not to early-return, we can clamp w/h to eps and still proceed.


48-53: Hoist DEFAULT_RECT to file scope (comptime) to avoid reinitialization

DEFAULT_RECT is constant and used on each pointer move. Moving it to file scope (comptime const) avoids repeated initialization and makes intent explicit.

Example:

comptime const DEFAULT_RECT: [4]PointUV = .{
    .{ .x = 0.0, .y = 1.0, .u = 0.0, .v = 1.0 },
    .{ .x = 1.0, .y = 1.0, .u = 1.0, .v = 1.0 },
    .{ .x = 1.0, .y = 0.0, .u = 1.0, .v = 0.0 },
    .{ .x = 0.0, .y = 0.0, .u = 0.0, .v = 0.0 },
};

Then use it directly in the function.


66-78: Rotation path: good approach—make aspect robust and confirm mirrored semantics

The center-based translate → rotateScaled → translate flow is solid, and the mirrored sign flip is a pragmatic fix for reflected states. Two suggestions:

  • Use a safe aspect to avoid 1/0 in rotateScaled when height is ~0.
  • Add a quick comment clarifying why angle is measured vs the Y-axis (0→3), which makes the top rotation handle behavior easier to reason about after reflections.

Apply this diff to harden aspect:

-            const aspect = bounds[0].distance(bounds[1]) / bounds[0].distance(bounds[3]);
+            const width = bounds[0].distance(bounds[1]);
+            const height = bounds[0].distance(bounds[3]);
+            const aspect = width / @max(height, 1e-6);

94-106: Min-size clamp is helpful—consider centralizing the threshold and preserving new edge angles

The clamp enforces ≥1px along both axes by nudging endpoints using pre-transform angles. It works, but:

  • Extract the 1.0 literal to a const MIN_SIZE: f32 = 1.0; at file scope for consistency and future tuning.
  • Using pre-transform angles can produce slight visual discontinuities after large flips. Optionally recompute the current edge directions post-transform to nudge along the final orientation.

Potential tweak:

-    if (bounds[0].distance(bounds[1]) < 1.0) {
+    const MIN_SIZE: f32 = 1.0;
+    if (bounds[0].distance(bounds[1]) < MIN_SIZE) {
         bounds[1].x = bounds[0].x + @cos(angle_x);
         bounds[1].y = bounds[0].y + @sin(angle_x);
         bounds[2].x = bounds[3].x + @cos(angle_x);
         bounds[2].y = bounds[3].y + @sin(angle_x);
     }
-    if (bounds[0].distance(bounds[3]) < 1.0) {
+    if (bounds[0].distance(bounds[3]) < MIN_SIZE) {
         bounds[3].x = bounds[0].x + @cos(angle_y);
         bounds[3].y = bounds[0].y + @sin(angle_y);
         bounds[2].x = bounds[1].x + @cos(angle_y);
         bounds[2].y = bounds[1].y + @sin(angle_y);
     }

If you want, I can provide a variant that derives the nudge direction from the post-transform edges instead.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2b2b137 and 37c21d3.

📒 Files selected for processing (3)
  • src/logic/index.zig (1 hunks)
  • src/logic/matrix.zig (1 hunks)
  • src/logic/transform_ui.zig (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/logic/matrix.zig (1)
src/WebGPU/m3.ts (4)
  • rotate (88-90)
  • scale (92-94)
  • rotation (67-75)
  • scaling (77-82)
src/logic/index.zig (2)
src/WebGPU/pointer.ts (2)
  • updatePointer (84-87)
  • updatePointer (47-52)
src/logic/index.d.ts (1)
  • PointUV (6-11)
src/logic/transform_ui.zig (2)
integration-tests/tests/asset-basic-transform.spec.ts (1)
  • testinfo (6-36)
integration-tests/init.ts (2)
  • getRotationHandle (99-113)
  • resizeAsset (127-162)
🔇 Additional comments (5)
src/logic/matrix.zig (2)

212-219: pivotScale math is correct (T(p) * S(sx, sy) * T(-p))

The composition encodes scaling around an arbitrary pivot via px*(1 - sx) / py*(1 - sy). Clean and side-effect free.


233-237: isMirrored check is appropriate

Using the 2x2 linear-part determinant (m00m11 - m01m10) is the standard reflection test for 2D affine transforms. Good choice.

src/logic/transform_ui.zig (2)

55-64: Corner/edge scale formulas and pivots look correct

Nice use of pivotScale: each handler maps to the expected unit-space pivot and scale factors (e.g., TL uses sx=1 - pointer.x, sy=pointer.y, pivot=(1,0)). This matches the derivation x' = px + sx(x - px), y' = py + sy(y - py).


82-92: Mapping DEFAULT_RECT through the updated matrix is the right, rotation-safe write-back

This avoids per-corner mutation bugs and keeps the transform pipeline consistent. Good call.

src/logic/index.zig (1)

365-369: API callsite updated correctly to pass a Point

The new signature transformPoints(u32, *[4]PointUV, Point) is respected. No other callsites appear in this file. LGTM.

@github-actions
Copy link

github-actions bot commented Oct 6, 2025

🎉 This PR is included in version 0.1.0-next.22 🎉

The release is available on:

Your semantic-release bot 📦🚀

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.

2 participants