refboard: add visible rotate controls
This commit is contained in:
@@ -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 },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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)); } }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user