Workspace has no image rotate, flip, or crop UI — UploadedImage type has rotation field and the canvas honors it, but no controls #174

Closed
opened 2026-10-03 17:34:26 +00:00 by Mark · 1 comment
Owner

Finding

src/components/workspace/types.ts:93 defines the UploadedImage type with a rotation: number field. src/components/workspace/canvasRenderer.ts:274 honors it: ctx.rotate((image.rotation * Math.PI) / 180);.

But there is no UI to set the rotation. grep -rn 'Rotate\b\|RotateCw\|rotate.*90\|rotate.*45' src/components/workspace/ returns zero matches. A user who uploads a sideways photo of their sketch can't fix it; they have to re-take the photo.

The Help page at line 143 promises: "You can work on designs larger than your target hoop and crop or resize before exporting." The workspace has neither crop nor resize UI.

A power user wants:

  • Rotate 90° CW / 90° CCW — for sideways phone photos
  • Flip horizontal / vertical — for mirrored scans
  • Crop to selection — for "I only want the bottom half of this image"
  • Resize (scale up/down) — for "the source is too small to digitize well"

Where

  • UploadedImage type: src/components/workspace/types.ts:93 (rotation is there)
  • Canvas honors rotation: src/components/workspace/canvasRenderer.ts:274
  • No UI controls: anywhere in src/components/workspace/
  • Help page claim: src/pages/Help.tsx:143

Suggested fix shape

Add an image-editing toolbar that appears when an image is uploaded but not yet digitized. The toolbar can be a small floating overlay on the canvas (top-right of the image bounds) with 4 buttons:

<button onClick={() => updateImage({ rotation: (image.rotation + 90) % 360 })}>
  <RotateCw />
</button>
<button onClick={() => updateImage({ rotation: (image.rotation + 270) % 360 })}>
  <RotateCcw />
</button>
<button onClick={() => updateImage({ flipped: !image.flipped })}>
  <FlipHorizontal />
</button>

For crop, the toolbar opens a selection rectangle mode: the user drags a rectangle on the image, clicks "Crop to selection", the image.src and image.width/image.height are updated.

For resize, a simple slider that scales the image (0.5× to 2×) without changing the source.

The reducer needs a new UPDATE_IMAGE action that takes a partial update and applies it.

Severity

Medium. A user with a sideways photo can't fix it without re-uploading. The Help page promise is unfulfilled.

Discovered by

Gap analysis (2026-10-02) of workspace image affordances.

  • The rotation field is in the type and the canvas renders it correctly — the UI is the only missing piece.
  • #158 (open) — Help page fabrications. This is a similar Help/UI mismatch.
## Finding `src/components/workspace/types.ts:93` defines the `UploadedImage` type with a `rotation: number` field. `src/components/workspace/canvasRenderer.ts:274` honors it: `ctx.rotate((image.rotation * Math.PI) / 180);`. But there is **no UI to set the rotation**. `grep -rn 'Rotate\b\|RotateCw\|rotate.*90\|rotate.*45' src/components/workspace/` returns zero matches. A user who uploads a sideways photo of their sketch can't fix it; they have to re-take the photo. The Help page at line 143 promises: "You can work on designs larger than your target hoop and **crop or resize** before exporting." The workspace has neither crop nor resize UI. A power user wants: - **Rotate 90° CW / 90° CCW** — for sideways phone photos - **Flip horizontal / vertical** — for mirrored scans - **Crop to selection** — for "I only want the bottom half of this image" - **Resize (scale up/down)** — for "the source is too small to digitize well" ## Where - `UploadedImage` type: `src/components/workspace/types.ts:93` (rotation is there) - Canvas honors rotation: `src/components/workspace/canvasRenderer.ts:274` - No UI controls: anywhere in `src/components/workspace/` - Help page claim: `src/pages/Help.tsx:143` ## Suggested fix shape Add an image-editing toolbar that appears when an image is uploaded but not yet digitized. The toolbar can be a small floating overlay on the canvas (top-right of the image bounds) with 4 buttons: ```tsx <button onClick={() => updateImage({ rotation: (image.rotation + 90) % 360 })}> <RotateCw /> </button> <button onClick={() => updateImage({ rotation: (image.rotation + 270) % 360 })}> <RotateCcw /> </button> <button onClick={() => updateImage({ flipped: !image.flipped })}> <FlipHorizontal /> </button> ``` For crop, the toolbar opens a selection rectangle mode: the user drags a rectangle on the image, clicks "Crop to selection", the `image.src` and `image.width`/`image.height` are updated. For resize, a simple slider that scales the image (0.5× to 2×) without changing the source. The reducer needs a new `UPDATE_IMAGE` action that takes a partial update and applies it. ## Severity Medium. A user with a sideways photo can't fix it without re-uploading. The Help page promise is unfulfilled. ## Discovered by Gap analysis (2026-10-02) of workspace image affordances. ## Related - The rotation field is in the type and the canvas renders it correctly — the UI is the only missing piece. - #158 (open) — Help page fabrications. This is a similar Help/UI mismatch.
Author
Owner

Fixed in 5fa763d on main.

What landed

  • New ImageEditToolbar (src/components/workspace/ImageEditToolbar.tsx) floating at the top of the canvas during appState='image-uploaded'. Five controls:
    • Rotate 90° CCW / CW (snap)
    • Flip horizontal (toggle)
    • Resize slider (0.25×–2×)
    • Remove image (× → empty state)
  • New reducer action UPDATE_IMAGE { updates: Partial<UploadedImage> } that merges into state.uploadedImage, wrapped in pushHistory so every edit round-trips through undo/redo. id/src are intentionally not patchable through this action — replacing the source remains a SET_IMAGE.
  • HistoryState gained an uploadedImage slot; UNDO/REDO now copy it back alongside the rest of the workspace. The previous gap (image edits silently lost on undo) was the same shape as #158's History/UI mismatch.
  • Help page FAQ rephrased to advertise only the controls the toolbar actually has — "crop or resize before exporting" is gone; the new line names rotate / flip / resize. Crop filed as a follow-up on this issue (it needs a new render-time state and selection-rect UX that wasn't in scope for this fix).

Scope explicitly deferred

  • Crop — needs selection rectangle mode and a canvas re-render of width/height/src. Filed as a follow-up.
  • Vertical flip — needs a second-axis sign or a new field on the type. Filed as a follow-up.
  • Fine rotation — the snap-to-90° matches the casual "sideways phone photo" case; a free-angle slider is a follow-up.

Verification

  • tsc -b && vite build — exit 0.
  • eslint src/ — clean.
  • vitest run — 776/779 (3 pre-existing EditorCanvas (#170) failures unrelated to this change). New tests: src/components/workspace/issue-174.test.tsx (16 tests) + src/pages/Help.test.tsx (+1 test).
Fixed in 5fa763d on `main`. **What landed** - New `ImageEditToolbar` (`src/components/workspace/ImageEditToolbar.tsx`) floating at the top of the canvas during `appState='image-uploaded'`. Five controls: - Rotate 90° CCW / CW (snap) - Flip horizontal (toggle) - Resize slider (0.25×–2×) - Remove image (× → empty state) - New reducer action `UPDATE_IMAGE { updates: Partial<UploadedImage> }` that merges into `state.uploadedImage`, wrapped in `pushHistory` so every edit round-trips through undo/redo. `id`/`src` are intentionally not patchable through this action — replacing the source remains a `SET_IMAGE`. - `HistoryState` gained an `uploadedImage` slot; UNDO/REDO now copy it back alongside the rest of the workspace. The previous gap (image edits silently lost on undo) was the same shape as #158's History/UI mismatch. - Help page FAQ rephrased to advertise only the controls the toolbar actually has — "crop or resize before exporting" is gone; the new line names rotate / flip / resize. Crop filed as a follow-up on this issue (it needs a new render-time state and selection-rect UX that wasn't in scope for this fix). **Scope explicitly deferred** - **Crop** — needs selection rectangle mode and a canvas re-render of `width`/`height`/`src`. Filed as a follow-up. - **Vertical flip** — needs a second-axis sign or a new field on the type. Filed as a follow-up. - **Fine rotation** — the snap-to-90° matches the casual "sideways phone photo" case; a free-angle slider is a follow-up. **Verification** - `tsc -b && vite build` — exit 0. - `eslint src/` — clean. - `vitest run` — 776/779 (3 pre-existing `EditorCanvas (#170)` failures unrelated to this change). New tests: `src/components/workspace/issue-174.test.tsx` (16 tests) + `src/pages/Help.test.tsx` (+1 test).
Mark closed this issue 2026-10-03 19:30:27 +00:00
Sign in to join this conversation.
No description provided.