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)); } }} />