refboard: add visible rotate controls

This commit is contained in:
Hiren Kangad
2026-03-13 15:01:25 +05:30
parent cb59fbb150
commit 98fb7be77e
4 changed files with 18 additions and 4 deletions
+4 -4
View File
@@ -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 * Shows 8 resize handles (corners + edge midpoints) around the combined
* around the combined bounding rect of selected items. * bounding rect of selected items. Each handle is draggable and applies
* Each handle is draggable and applies scale/rotation transforms to the items. * scale transforms to the items.
*/ */
import { Container, Graphics, FederatedPointerEvent, Text, TextStyle } from 'pixi.js'; import { Container, Graphics, FederatedPointerEvent, Text, TextStyle } from 'pixi.js';
@@ -155,6 +155,8 @@ export function buildContextMenuItems(ctx: MenuContext): MenuItem[] {
// -- Image -- // -- Image --
{ label: 'Flip Horizontal', shortcut: 'Alt+Shift+H', onClick: () => { ops.flipHorizontal(selected); selection?.transformBox.update(selected); ctx.onChange(ids); }, disabled: !hasSel }, { 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: '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: '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: 'Reset Transform', shortcut: 'Ctrl+Shift+T', onClick: () => { ops.resetTransform(selected); selection?.transformBox.update(selected); ctx.onChange(ids); }, disabled: !hasSel },
{ label: '', shortcut: '', onClick: () => {}, divider: true }, { label: '', shortcut: '', onClick: () => {}, divider: true },
@@ -20,6 +20,8 @@ interface SelectionToolbarProps {
onStack: () => void; onStack: () => void;
onFlipH: () => void; onFlipH: () => void;
onFlipV: () => void; onFlipV: () => void;
onRotateCW: () => void;
onRotateCCW: () => void;
onGroup: () => void; onGroup: () => void;
onNormSize: () => void; onNormSize: () => void;
} }
@@ -70,6 +72,12 @@ function IcoFlipH() {
function IcoFlipV() { function IcoFlipV() {
return <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.3"><line x1="1" y1="7" x2="13" y2="7" strokeDasharray="2 1" /><path d="M4 5V2L10 5H4Z" fill="currentColor" opacity="0.3" /><path d="M4 9V12L10 9H4Z" fill="currentColor" opacity="0.15" /></svg>; return <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.3"><line x1="1" y1="7" x2="13" y2="7" strokeDasharray="2 1" /><path d="M4 5V2L10 5H4Z" fill="currentColor" opacity="0.3" /><path d="M4 9V12L10 9H4Z" fill="currentColor" opacity="0.15" /></svg>;
} }
function IcoRotateCW() {
return <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"><path d="M10.5 4.5V1.75H7.75" /><path d="M10.2 7A4.2 4.2 0 1 1 8.7 3.8L10.5 5.6" /></svg>;
}
function IcoRotateCCW() {
return <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"><path d="M3.5 4.5V1.75H6.25" /><path d="M3.8 7A4.2 4.2 0 1 0 5.3 3.8L3.5 5.6" /></svg>;
}
function IcoGroup() { function IcoGroup() {
return <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.2"><rect x="1" y="1" width="12" height="12" rx="1.5" strokeDasharray="2 1.5" /><rect x="3" y="3" width="4" height="4" rx="0.5" fill="currentColor" opacity="0.25" /><rect x="7" y="7" width="4" height="4" rx="0.5" fill="currentColor" opacity="0.25" /></svg>; return <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.2"><rect x="1" y="1" width="12" height="12" rx="1.5" strokeDasharray="2 1.5" /><rect x="3" y="3" width="4" height="4" rx="0.5" fill="currentColor" opacity="0.25" /><rect x="7" y="7" width="4" height="4" rx="0.5" fill="currentColor" opacity="0.25" /></svg>;
} }
@@ -122,6 +130,8 @@ export default function SelectionToolbar(props: SelectionToolbarProps) {
items: [ items: [
{ icon: IcoFlipH, label: 'Flip H', shortcut: 'Alt+Shift+H', onClick: props.onFlipH }, { 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: 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: IcoGroup, label: 'Group', shortcut: 'Ctrl+G', onClick: props.onGroup },
{ icon: IcoNormSize, label: 'Same size', shortcut: '', onClick: props.onNormSize }, { icon: IcoNormSize, label: 'Same size', shortcut: '', onClick: props.onNormSize },
], ],
+2
View File
@@ -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)); } }} 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)); } }} 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)); } }} 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} onGroup={handleGroup}
onNormSize={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.normalizeSize(s); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }} onNormSize={() => { const s = selectionRef.current?.getSelectedItems(); if (s) { ops.normalizeSize(s); selectionRef.current?.transformBox.update(s); onCanvasChange(s.map(i => i.id)); } }}
/> />