diff --git a/frontend/src/canvas/TransformBox.ts b/frontend/src/canvas/TransformBox.ts
index 25921a5..2fc072f 100644
--- a/frontend/src/canvas/TransformBox.ts
+++ b/frontend/src/canvas/TransformBox.ts
@@ -1,9 +1,9 @@
/**
- * TransformBox — resize/rotate handles around selected scene items.
+ * TransformBox — resize handles around selected scene items.
*
- * Shows 8 resize handles (corners + edge midpoints) + 1 rotation handle
- * around the combined bounding rect of selected items.
- * Each handle is draggable and applies scale/rotation transforms to the items.
+ * Shows 8 resize handles (corners + edge midpoints) around the combined
+ * bounding rect of selected items. Each handle is draggable and applies
+ * scale transforms to the items.
*/
import { Container, Graphics, FederatedPointerEvent, Text, TextStyle } from 'pixi.js';
diff --git a/frontend/src/canvas/context-menu-items.ts b/frontend/src/canvas/context-menu-items.ts
index 26a68a2..b6c6cea 100644
--- a/frontend/src/canvas/context-menu-items.ts
+++ b/frontend/src/canvas/context-menu-items.ts
@@ -155,6 +155,8 @@ export function buildContextMenuItems(ctx: MenuContext): MenuItem[] {
// -- Image --
{ label: 'Flip Horizontal', shortcut: 'Alt+Shift+H', onClick: () => { ops.flipHorizontal(selected); selection?.transformBox.update(selected); ctx.onChange(ids); }, disabled: !hasSel },
{ label: 'Flip Vertical', shortcut: 'Alt+Shift+V', onClick: () => { ops.flipVertical(selected); selection?.transformBox.update(selected); ctx.onChange(ids); }, disabled: !hasSel },
+ { label: 'Rotate Clockwise', shortcut: 'R', onClick: () => { ops.rotate90(selected, true); selection?.transformBox.update(selected); ctx.onChange(ids); }, disabled: !hasSel },
+ { label: 'Rotate Counter-Clockwise', shortcut: 'Shift+R', onClick: () => { ops.rotate90(selected, false); selection?.transformBox.update(selected); ctx.onChange(ids); }, disabled: !hasSel },
{ label: 'Crop', shortcut: 'C', onClick: () => ctx.startCrop?.(), disabled: selected.length !== 1 || selected[0]?.data.type !== 'image' },
{ label: 'Reset Transform', shortcut: 'Ctrl+Shift+T', onClick: () => { ops.resetTransform(selected); selection?.transformBox.update(selected); ctx.onChange(ids); }, disabled: !hasSel },
{ label: '', shortcut: '', onClick: () => {}, divider: true },
diff --git a/frontend/src/components/SelectionToolbar.tsx b/frontend/src/components/SelectionToolbar.tsx
index fe37b77..772cea0 100644
--- a/frontend/src/components/SelectionToolbar.tsx
+++ b/frontend/src/components/SelectionToolbar.tsx
@@ -20,6 +20,8 @@ interface SelectionToolbarProps {
onStack: () => void;
onFlipH: () => void;
onFlipV: () => void;
+ onRotateCW: () => void;
+ onRotateCCW: () => void;
onGroup: () => void;
onNormSize: () => void;
}
@@ -70,6 +72,12 @@ function IcoFlipH() {
function IcoFlipV() {
return ;
}
+function IcoRotateCW() {
+ return ;
+}
+function IcoRotateCCW() {
+ return ;
+}
function IcoGroup() {
return ;
}
@@ -122,6 +130,8 @@ export default function SelectionToolbar(props: SelectionToolbarProps) {
items: [
{ icon: IcoFlipH, label: 'Flip H', shortcut: 'Alt+Shift+H', onClick: props.onFlipH },
{ icon: IcoFlipV, label: 'Flip V', shortcut: 'Alt+Shift+V', onClick: props.onFlipV },
+ { icon: IcoRotateCW, label: 'Rotate CW', shortcut: 'R', onClick: props.onRotateCW },
+ { icon: IcoRotateCCW, label: 'Rotate CCW', shortcut: 'Shift+R', onClick: props.onRotateCCW },
{ icon: IcoGroup, label: 'Group', shortcut: 'Ctrl+G', onClick: props.onGroup },
{ icon: IcoNormSize, label: 'Same size', shortcut: '', onClick: props.onNormSize },
],
diff --git a/frontend/src/pages/Editor.tsx b/frontend/src/pages/Editor.tsx
index 754f363..3a03ea4 100644
--- a/frontend/src/pages/Editor.tsx
+++ b/frontend/src/pages/Editor.tsx
@@ -1060,6 +1060,8 @@ export default function Editor({ isPublicView }: EditorProps) {
onStack={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.stackObjects(s); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }}
onFlipH={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.flipHorizontal(s); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }}
onFlipV={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.flipVertical(s); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }}
+ onRotateCW={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.rotate90(s, true); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }}
+ onRotateCCW={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.rotate90(s, false); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }}
onGroup={handleGroup}
onNormSize={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.normalizeSize(s); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }}
/>