Port shortcut definitions from Fabric.js to PixiJS SceneManager/SelectionManager
Replace all Fabric.js Canvas references with SceneManager, SelectionManager, UndoManager, and Viewport. Remove withBreak/breakSelection helper (no longer needed without ActiveSelection). Implement z-swap layer ordering for ] and [ keys, clone-based paste/duplicate via SceneManager._createItem, and direct history.undo()/redo() calls instead of undoRef indirection.
This commit is contained in:
@@ -2,29 +2,20 @@
|
|||||||
* All shortcut definitions — the single source of truth for keyboard shortcuts.
|
* All shortcut definitions — the single source of truth for keyboard shortcuts.
|
||||||
* Used by the keyboard handler AND the ShortcutsHelp overlay.
|
* Used by the keyboard handler AND the ShortcutsHelp overlay.
|
||||||
*
|
*
|
||||||
* BUGS FIXED in this revision:
|
* Ported to PixiJS: uses SceneManager/SelectionManager instead of Fabric.js Canvas.
|
||||||
|
*
|
||||||
|
* BUGS FIXED in previous revision (preserved):
|
||||||
* - Ctrl+Y conflict (was both redo AND overlay-compare) → overlay moved to Ctrl+Shift+Y
|
* - Ctrl+Y conflict (was both redo AND overlay-compare) → overlay moved to Ctrl+Shift+Y
|
||||||
* - Ctrl+P (browser print) → changed to Ctrl+Shift+P for arrange optimal
|
* - Ctrl+P (browser print) → changed to Ctrl+Shift+P for arrange optimal
|
||||||
* - '?' key (Shift+/) → e.key is '?' not '/', fixed matcher key
|
* - '?' key (Shift+/) → e.key is '?' not '/', fixed matcher key
|
||||||
* - ArrowUp/Down with selection → layer ordering uses ] / [ again (arrows cycle/nudge)
|
* - ArrowUp/Down with selection → layer ordering uses ] / [ again (arrows cycle/nudge)
|
||||||
* - ArrowLeft/Right bare → only cycle when nothing selected, otherwise no-op
|
* - ArrowLeft/Right bare → only cycle when nothing selected, otherwise no-op
|
||||||
* - ActiveSelection coordinates → use withBreak() for position-dependent ops
|
|
||||||
* - Added Ctrl+S (prevent browser save-as)
|
* - Added Ctrl+S (prevent browser save-as)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ShortcutDef } from './shortcuts';
|
import { ShortcutDef } from './shortcuts';
|
||||||
import { FabricObject } from 'fabric';
|
|
||||||
import * as ops from './operations';
|
import * as ops from './operations';
|
||||||
|
|
||||||
/** Helper: break ActiveSelection, run operation on canvas-level coords, restore selection */
|
|
||||||
function withBreak(ctx: { canvas: any; onCanvasChange: () => void }, fn: (objs: FabricObject[]) => void) {
|
|
||||||
const { objects, restore } = ops.breakSelection(ctx.canvas);
|
|
||||||
fn(objects);
|
|
||||||
restore();
|
|
||||||
ctx.canvas.requestRenderAll();
|
|
||||||
ctx.onCanvasChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const shortcuts: ShortcutDef[] = [
|
export const shortcuts: ShortcutDef[] = [
|
||||||
|
|
||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
@@ -34,22 +25,34 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
{
|
{
|
||||||
id: 'align-left', keys: { key: 'arrowleft', ctrl: true },
|
id: 'align-left', keys: { key: 'arrowleft', ctrl: true },
|
||||||
category: 'alignment', description: 'Align left', needsSelection: true, minSelection: 2,
|
category: 'alignment', description: 'Align left', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.alignLeft(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.alignLeft(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'align-right', keys: { key: 'arrowright', ctrl: true },
|
id: 'align-right', keys: { key: 'arrowright', ctrl: true },
|
||||||
category: 'alignment', description: 'Align right', needsSelection: true, minSelection: 2,
|
category: 'alignment', description: 'Align right', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.alignRight(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.alignRight(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'align-top', keys: { key: 'arrowup', ctrl: true },
|
id: 'align-top', keys: { key: 'arrowup', ctrl: true },
|
||||||
category: 'alignment', description: 'Align top', needsSelection: true, minSelection: 2,
|
category: 'alignment', description: 'Align top', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.alignTop(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.alignTop(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'align-bottom', keys: { key: 'arrowdown', ctrl: true },
|
id: 'align-bottom', keys: { key: 'arrowdown', ctrl: true },
|
||||||
category: 'alignment', description: 'Align bottom', needsSelection: true, minSelection: 2,
|
category: 'alignment', description: 'Align bottom', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.alignBottom(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.alignBottom(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
@@ -59,12 +62,18 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
{
|
{
|
||||||
id: 'distribute-h', keys: { key: 'arrowup', ctrl: true, alt: true, shift: true },
|
id: 'distribute-h', keys: { key: 'arrowup', ctrl: true, alt: true, shift: true },
|
||||||
category: 'alignment', description: 'Distribute horizontal', needsSelection: true, minSelection: 3,
|
category: 'alignment', description: 'Distribute horizontal', needsSelection: true, minSelection: 3,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.distributeHorizontal(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.distributeHorizontal(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'distribute-v', keys: { key: 'arrowdown', ctrl: true, alt: true, shift: true },
|
id: 'distribute-v', keys: { key: 'arrowdown', ctrl: true, alt: true, shift: true },
|
||||||
category: 'alignment', description: 'Distribute vertical', needsSelection: true, minSelection: 3,
|
category: 'alignment', description: 'Distribute vertical', needsSelection: true, minSelection: 3,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.distributeVertical(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.distributeVertical(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
@@ -74,58 +83,79 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
{
|
{
|
||||||
id: 'normalize-size', keys: { key: 'arrowup', ctrl: true, alt: true },
|
id: 'normalize-size', keys: { key: 'arrowup', ctrl: true, alt: true },
|
||||||
category: 'normalize', description: 'Normalize size (same area)', needsSelection: true, minSelection: 2,
|
category: 'normalize', description: 'Normalize size (same area)', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeSize(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.normalizeSize(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'normalize-scale', keys: { key: 'arrowdown', ctrl: true, alt: true },
|
id: 'normalize-scale', keys: { key: 'arrowdown', ctrl: true, alt: true },
|
||||||
category: 'normalize', description: 'Normalize scale', needsSelection: true, minSelection: 2,
|
category: 'normalize', description: 'Normalize scale', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeScale(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.normalizeScale(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'normalize-height', keys: { key: 'arrowleft', ctrl: true, alt: true },
|
id: 'normalize-height', keys: { key: 'arrowleft', ctrl: true, alt: true },
|
||||||
category: 'normalize', description: 'Normalize height', needsSelection: true, minSelection: 2,
|
category: 'normalize', description: 'Normalize height', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeHeight(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.normalizeHeight(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'normalize-width', keys: { key: 'arrowright', ctrl: true, alt: true },
|
id: 'normalize-width', keys: { key: 'arrowright', ctrl: true, alt: true },
|
||||||
category: 'normalize', description: 'Normalize width', needsSelection: true, minSelection: 2,
|
category: 'normalize', description: 'Normalize width', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.normalizeWidth(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.normalizeWidth(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
// ARRANGEMENT
|
// ARRANGEMENT
|
||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
|
|
||||||
// FIX: Changed from Ctrl+P (browser print) to Ctrl+Shift+P
|
|
||||||
{
|
{
|
||||||
id: 'arrange-optimal', keys: { key: 'p', ctrl: true, shift: true },
|
id: 'arrange-optimal', keys: { key: 'p', ctrl: true, shift: true },
|
||||||
category: 'arrangement', description: 'Arrange optimal (pack)', needsSelection: true, minSelection: 2,
|
category: 'arrangement', description: 'Arrange optimal (pack)', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.arrangeOptimal(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.arrangeOptimal(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'arrange-by-name', keys: { key: 'n', ctrl: true, alt: true },
|
id: 'arrange-by-name', keys: { key: 'n', ctrl: true, alt: true },
|
||||||
category: 'arrangement', description: 'Arrange by name', needsSelection: true, minSelection: 2,
|
category: 'arrangement', description: 'Arrange by name', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.arrangeByName(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.arrangeByName(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'arrange-by-order', keys: { key: 'o', ctrl: true, alt: true },
|
id: 'arrange-by-order', keys: { key: 'o', ctrl: true, alt: true },
|
||||||
category: 'arrangement', description: 'Arrange by z-order', needsSelection: true, minSelection: 2,
|
category: 'arrangement', description: 'Arrange by z-order', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const { objects, restore } = ops.breakSelection(ctx.canvas);
|
ops.arrangeByZOrder(ctx.selection.getSelectedItems());
|
||||||
ops.arrangeByZOrder(objects, ctx.canvas.getObjects());
|
ctx.onChange();
|
||||||
restore();
|
|
||||||
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'arrange-random', keys: { key: 'r', ctrl: true, alt: true },
|
id: 'arrange-random', keys: { key: 'r', ctrl: true, alt: true },
|
||||||
category: 'arrangement', description: 'Arrange randomly', needsSelection: true, minSelection: 2,
|
category: 'arrangement', description: 'Arrange randomly', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.arrangeRandomly(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.arrangeRandomly(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stack', keys: { key: 's', ctrl: true, alt: true },
|
id: 'stack', keys: { key: 's', ctrl: true, alt: true },
|
||||||
category: 'arrangement', description: 'Stack (pile on top)', needsSelection: true, minSelection: 2,
|
category: 'arrangement', description: 'Stack (pile on top)', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.stackObjects(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.stackObjects(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
@@ -136,47 +166,49 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
id: 'flip-h', keys: { key: 'h', alt: true, shift: true },
|
id: 'flip-h', keys: { key: 'h', alt: true, shift: true },
|
||||||
category: 'image', description: 'Flip horizontal', needsSelection: true,
|
category: 'image', description: 'Flip horizontal', needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ops.flipHorizontal(ctx.getActiveObjects());
|
ops.flipHorizontal(ctx.selection.getSelectedItems());
|
||||||
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'flip-v', keys: { key: 'v', alt: true, shift: true },
|
id: 'flip-v', keys: { key: 'v', alt: true, shift: true },
|
||||||
category: 'image', description: 'Flip vertical', needsSelection: true,
|
category: 'image', description: 'Flip vertical', needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ops.flipVertical(ctx.getActiveObjects());
|
ops.flipVertical(ctx.selection.getSelectedItems());
|
||||||
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'reset-transform', keys: { key: 't', ctrl: true, shift: true },
|
id: 'reset-transform', keys: { key: 't', ctrl: true, shift: true },
|
||||||
category: 'image', description: 'Reset transform', needsSelection: true,
|
category: 'image', description: 'Reset transform', needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ops.resetTransform(ctx.getActiveObjects());
|
ops.resetTransform(ctx.selection.getSelectedItems());
|
||||||
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'toggle-grayscale', keys: { key: 'g', alt: true },
|
id: 'toggle-grayscale', keys: { key: 'g', alt: true },
|
||||||
category: 'image', description: 'Toggle grayscale', needsSelection: true,
|
category: 'image', description: 'Toggle grayscale', needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ops.toggleGrayscale(ctx.getActiveObjects());
|
ops.toggleGrayscale(ctx.selection.getSelectedItems());
|
||||||
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'toggle-locked', keys: { key: 'l', alt: true },
|
id: 'toggle-locked', keys: { key: 'l', alt: true },
|
||||||
category: 'image', description: 'Toggle locked', needsSelection: true,
|
category: 'image', description: 'Toggle locked', needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ops.toggleLocked(ctx.getActiveObjects());
|
ops.toggleLocked(ctx.selection.getSelectedItems());
|
||||||
ctx.canvas.requestRenderAll(); ctx.refreshLayers();
|
ctx.refreshLayers();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// FIX: Changed from Ctrl+Y (conflicts with redo) to Ctrl+Shift+Y
|
|
||||||
{
|
{
|
||||||
id: 'overlay-compare', keys: { key: 'y', ctrl: true, shift: true },
|
id: 'overlay-compare', keys: { key: 'y', ctrl: true, shift: true },
|
||||||
category: 'image', description: 'Overlay / compare', needsSelection: true, minSelection: 2,
|
category: 'image', description: 'Overlay / compare', needsSelection: true, minSelection: 2,
|
||||||
handler: (ctx) => withBreak(ctx, (objs) => ops.overlayCompare(objs)),
|
handler: (ctx) => {
|
||||||
|
ops.overlayCompare(ctx.selection.getSelectedItems());
|
||||||
|
ctx.onChange();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// ═══════════════════════════════════════
|
// ═══════════════════════════════════════
|
||||||
@@ -187,8 +219,7 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
id: 'clear-selection', keys: { key: 'escape' },
|
id: 'clear-selection', keys: { key: 'escape' },
|
||||||
category: 'navigation', description: 'Clear selection',
|
category: 'navigation', description: 'Clear selection',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ctx.canvas.discardActiveObject();
|
ctx.selection.clear();
|
||||||
ctx.canvas.requestRenderAll();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -196,40 +227,51 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
category: 'navigation', description: 'Fit all in view',
|
category: 'navigation', description: 'Fit all in view',
|
||||||
handler: (ctx) => ctx.fitAll(),
|
handler: (ctx) => ctx.fitAll(),
|
||||||
},
|
},
|
||||||
// FIX: Bare arrow Left/Right only cycle when nothing is selected.
|
// Bare arrow Left/Right only cycle when nothing is selected.
|
||||||
// When something IS selected, they're no-ops (prevent accidental cycling).
|
// When something IS selected, they're no-ops (prevent accidental cycling).
|
||||||
// Layer ordering uses ] / [ (restored) to avoid arrow conflicts.
|
// Layer ordering uses ] / [ to avoid arrow conflicts.
|
||||||
{
|
{
|
||||||
id: 'cycle-next', keys: { key: 'arrowright' },
|
id: 'cycle-next', keys: { key: 'arrowright' },
|
||||||
category: 'navigation', description: 'Select next object',
|
category: 'navigation', description: 'Select next object',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
// Only cycle when nothing selected — if selected, do nothing
|
if (ctx.selection.selectedIds.size > 0) return;
|
||||||
if (ctx.getActiveObjects().length > 0) return;
|
const all = ctx.scene.getAllItems();
|
||||||
const objects = ctx.canvas.getObjects();
|
if (all.length === 0) return;
|
||||||
if (objects.length === 0) return;
|
// Sort by z to get consistent order
|
||||||
ctx.canvas.setActiveObject(objects[0]);
|
all.sort((a, b) => a.data.z - b.data.z);
|
||||||
ctx.canvas.requestRenderAll();
|
ctx.selection.selectOnly(all[0].id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cycle-prev', keys: { key: 'arrowleft' },
|
id: 'cycle-prev', keys: { key: 'arrowleft' },
|
||||||
category: 'navigation', description: 'Select previous object',
|
category: 'navigation', description: 'Select previous object',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
if (ctx.getActiveObjects().length > 0) return;
|
if (ctx.selection.selectedIds.size > 0) return;
|
||||||
const objects = ctx.canvas.getObjects();
|
const all = ctx.scene.getAllItems();
|
||||||
if (objects.length === 0) return;
|
if (all.length === 0) return;
|
||||||
ctx.canvas.setActiveObject(objects[objects.length - 1]);
|
all.sort((a, b) => a.data.z - b.data.z);
|
||||||
ctx.canvas.requestRenderAll();
|
ctx.selection.selectOnly(all[all.length - 1].id);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// FIX: Layer ordering restored to ] / [ (not arrow keys — those conflict with cycle/nudge)
|
// Layer ordering: ] brings forward, [ sends backward
|
||||||
{
|
{
|
||||||
id: 'send-to-front', keys: { key: ']' },
|
id: 'send-to-front', keys: { key: ']' },
|
||||||
category: 'navigation', description: 'Bring forward',
|
category: 'navigation', description: 'Bring forward',
|
||||||
needsSelection: true,
|
needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ctx.getActiveObjects().forEach((obj) => (ctx.canvas as any).bringObjectForward(obj));
|
const selected = ctx.selection.getSelectedItems();
|
||||||
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
|
const all = ctx.scene.getAllItems().sort((a, b) => a.data.z - b.data.z);
|
||||||
|
// For each selected item, swap z with the next non-selected item above it
|
||||||
|
const selectedIds = new Set(selected.map((s) => s.id));
|
||||||
|
for (let i = all.length - 2; i >= 0; i--) {
|
||||||
|
if (selectedIds.has(all[i].id) && !selectedIds.has(all[i + 1].id)) {
|
||||||
|
const tmp = all[i].data.z;
|
||||||
|
all[i].data.z = all[i + 1].data.z;
|
||||||
|
all[i + 1].data.z = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.scene._applyZOrder();
|
||||||
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -237,8 +279,18 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
category: 'navigation', description: 'Send backward',
|
category: 'navigation', description: 'Send backward',
|
||||||
needsSelection: true,
|
needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ctx.getActiveObjects().forEach((obj) => (ctx.canvas as any).sendObjectBackwards(obj));
|
const selected = ctx.selection.getSelectedItems();
|
||||||
ctx.canvas.requestRenderAll(); ctx.onCanvasChange();
|
const all = ctx.scene.getAllItems().sort((a, b) => a.data.z - b.data.z);
|
||||||
|
const selectedIds = new Set(selected.map((s) => s.id));
|
||||||
|
for (let i = 1; i < all.length; i++) {
|
||||||
|
if (selectedIds.has(all[i].id) && !selectedIds.has(all[i - 1].id)) {
|
||||||
|
const tmp = all[i].data.z;
|
||||||
|
all[i].data.z = all[i - 1].data.z;
|
||||||
|
all[i - 1].data.z = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.scene._applyZOrder();
|
||||||
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -250,23 +302,17 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
id: 'select-all', keys: { key: 'a', ctrl: true },
|
id: 'select-all', keys: { key: 'a', ctrl: true },
|
||||||
category: 'editing', description: 'Select all',
|
category: 'editing', description: 'Select all',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ctx.canvas.discardActiveObject();
|
ctx.selection.selectAll();
|
||||||
const fabricNs = (window as any).fabric;
|
|
||||||
if (fabricNs?.ActiveSelection) {
|
|
||||||
const sel = new fabricNs.ActiveSelection(ctx.canvas.getObjects(), { canvas: ctx.canvas });
|
|
||||||
ctx.canvas.setActiveObject(sel);
|
|
||||||
}
|
|
||||||
ctx.canvas.requestRenderAll();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'copy', keys: { key: 'c', ctrl: true },
|
id: 'copy', keys: { key: 'c', ctrl: true },
|
||||||
category: 'editing', description: 'Copy',
|
category: 'editing', description: 'Copy',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const active = ctx.getActiveObjects();
|
const selected = ctx.selection.getSelectedItems();
|
||||||
if (active.length > 0) {
|
if (selected.length > 0) {
|
||||||
ctx.clipboardRef.current = [...active];
|
ctx.clipboardRef.current = [...selected];
|
||||||
ctx.writeCanvasToClipboard(active);
|
ctx.writeCanvasToClipboard(selected);
|
||||||
ctx.showToast('Copied');
|
ctx.showToast('Copied');
|
||||||
} else {
|
} else {
|
||||||
ctx.writeCanvasToClipboard();
|
ctx.writeCanvasToClipboard();
|
||||||
@@ -278,8 +324,8 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
id: 'copy-as-image', keys: { key: 'c', ctrl: true, shift: true },
|
id: 'copy-as-image', keys: { key: 'c', ctrl: true, shift: true },
|
||||||
category: 'editing', description: 'Copy as image to clipboard',
|
category: 'editing', description: 'Copy as image to clipboard',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const active = ctx.getActiveObjects();
|
const selected = ctx.selection.getSelectedItems();
|
||||||
ctx.writeCanvasToClipboard(active.length > 0 ? active : undefined);
|
ctx.writeCanvasToClipboard(selected.length > 0 ? selected : undefined);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -287,33 +333,38 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
category: 'editing', description: 'Paste',
|
category: 'editing', description: 'Paste',
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
if (ctx.clipboardRef.current.length === 0) return;
|
if (ctx.clipboardRef.current.length === 0) return;
|
||||||
const newObjs: FabricObject[] = [];
|
const newItems: typeof ctx.clipboardRef.current = [];
|
||||||
for (const original of ctx.clipboardRef.current) {
|
for (const original of ctx.clipboardRef.current) {
|
||||||
try {
|
// Clone: duplicate the item data with new ID and offset position
|
||||||
const obj = await ctx.cloneFabricObject(original, 20, 20);
|
const newData = {
|
||||||
ctx.canvas.add(obj);
|
...original.data,
|
||||||
newObjs.push(obj);
|
id: crypto.randomUUID(),
|
||||||
} catch (err) {
|
x: original.data.x + 20,
|
||||||
console.error('Paste object failed:', err);
|
y: original.data.y + 20,
|
||||||
}
|
z: ctx.scene.nextZ(),
|
||||||
|
};
|
||||||
|
await ctx.scene._createItem(newData, true);
|
||||||
|
const newItem = ctx.scene.getById(newData.id);
|
||||||
|
if (newItem) newItems.push(newItem);
|
||||||
}
|
}
|
||||||
ctx.clipboardRef.current = newObjs;
|
ctx.clipboardRef.current = newItems;
|
||||||
ctx.canvas.requestRenderAll();
|
ctx.scene._applyZOrder();
|
||||||
ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cut', keys: { key: 'x', ctrl: true },
|
id: 'cut', keys: { key: 'x', ctrl: true },
|
||||||
category: 'editing', description: 'Cut',
|
category: 'editing', description: 'Cut',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const active = ctx.getActiveObjects();
|
const selected = ctx.selection.getSelectedItems();
|
||||||
if (active.length === 0) return;
|
if (selected.length === 0) return;
|
||||||
ctx.clipboardRef.current = [...active];
|
ctx.clipboardRef.current = [...selected];
|
||||||
ctx.writeCanvasToClipboard(active);
|
ctx.writeCanvasToClipboard(selected);
|
||||||
active.forEach((obj) => ctx.canvas.remove(obj));
|
for (const item of selected) {
|
||||||
ctx.canvas.discardActiveObject();
|
ctx.scene.removeItem(item.id, true);
|
||||||
ctx.canvas.requestRenderAll();
|
}
|
||||||
ctx.onCanvasChange();
|
ctx.selection.clear();
|
||||||
|
ctx.onChange();
|
||||||
ctx.showToast('Cut');
|
ctx.showToast('Cut');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -321,19 +372,21 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
id: 'duplicate', keys: { key: 'd', ctrl: true },
|
id: 'duplicate', keys: { key: 'd', ctrl: true },
|
||||||
category: 'editing', description: 'Duplicate',
|
category: 'editing', description: 'Duplicate',
|
||||||
handler: async (ctx) => {
|
handler: async (ctx) => {
|
||||||
const active = ctx.getActiveObjects();
|
const selected = ctx.selection.getSelectedItems();
|
||||||
if (active.length === 0) return;
|
if (selected.length === 0) return;
|
||||||
for (const original of active) {
|
for (const original of selected) {
|
||||||
try {
|
const newData = {
|
||||||
const obj = await ctx.cloneFabricObject(original, 20, 20);
|
...original.data,
|
||||||
ctx.canvas.add(obj);
|
id: crypto.randomUUID(),
|
||||||
} catch (err) {
|
x: original.data.x + 20,
|
||||||
console.error('Duplicate failed:', err);
|
y: original.data.y + 20,
|
||||||
}
|
z: ctx.scene.nextZ(),
|
||||||
|
};
|
||||||
|
await ctx.scene._createItem(newData, true);
|
||||||
}
|
}
|
||||||
ctx.canvas.discardActiveObject();
|
ctx.selection.clear();
|
||||||
ctx.canvas.requestRenderAll();
|
ctx.scene._applyZOrder();
|
||||||
ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -341,11 +394,12 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
category: 'editing', description: 'Delete selected',
|
category: 'editing', description: 'Delete selected',
|
||||||
needsSelection: true,
|
needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const active = ctx.getActiveObjects();
|
const selected = ctx.selection.getSelectedItems();
|
||||||
active.forEach((obj) => ctx.canvas.remove(obj));
|
for (const item of selected) {
|
||||||
ctx.canvas.discardActiveObject();
|
ctx.scene.removeItem(item.id, true);
|
||||||
ctx.canvas.requestRenderAll();
|
}
|
||||||
ctx.onCanvasChange();
|
ctx.selection.clear();
|
||||||
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -353,41 +407,42 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
category: 'editing', description: 'Delete selected',
|
category: 'editing', description: 'Delete selected',
|
||||||
needsSelection: true,
|
needsSelection: true,
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const active = ctx.getActiveObjects();
|
const selected = ctx.selection.getSelectedItems();
|
||||||
active.forEach((obj) => ctx.canvas.remove(obj));
|
for (const item of selected) {
|
||||||
ctx.canvas.discardActiveObject();
|
ctx.scene.removeItem(item.id, true);
|
||||||
ctx.canvas.requestRenderAll();
|
}
|
||||||
ctx.onCanvasChange();
|
ctx.selection.clear();
|
||||||
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'undo', keys: { key: 'z', ctrl: true },
|
id: 'undo', keys: { key: 'z', ctrl: true },
|
||||||
category: 'editing', description: 'Undo',
|
category: 'editing', description: 'Undo',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ctx.undoRef.current?.undo();
|
ctx.history.undo();
|
||||||
ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false);
|
ctx.setCanUndo(ctx.history.canUndo());
|
||||||
ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false);
|
ctx.setCanRedo(ctx.history.canRedo());
|
||||||
ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'redo', keys: { key: 'z', ctrl: true, shift: true },
|
id: 'redo', keys: { key: 'z', ctrl: true, shift: true },
|
||||||
category: 'editing', description: 'Redo',
|
category: 'editing', description: 'Redo',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ctx.undoRef.current?.redo();
|
ctx.history.redo();
|
||||||
ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false);
|
ctx.setCanUndo(ctx.history.canUndo());
|
||||||
ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false);
|
ctx.setCanRedo(ctx.history.canRedo());
|
||||||
ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'redo-y', keys: { key: 'y', ctrl: true },
|
id: 'redo-y', keys: { key: 'y', ctrl: true },
|
||||||
category: 'editing', description: 'Redo (alt)',
|
category: 'editing', description: 'Redo (alt)',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
ctx.undoRef.current?.redo();
|
ctx.history.redo();
|
||||||
ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false);
|
ctx.setCanUndo(ctx.history.canUndo());
|
||||||
ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false);
|
ctx.setCanRedo(ctx.history.canRedo());
|
||||||
ctx.onCanvasChange();
|
ctx.onChange();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -400,7 +455,7 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
category: 'editing', description: 'Ungroup', needsSelection: true,
|
category: 'editing', description: 'Ungroup', needsSelection: true,
|
||||||
handler: (ctx) => ctx.handleUngroup(),
|
handler: (ctx) => ctx.handleUngroup(),
|
||||||
},
|
},
|
||||||
// FIX: Block Ctrl+S from opening browser save-as dialog
|
// Block Ctrl+S from opening browser save-as dialog
|
||||||
{
|
{
|
||||||
id: 'save', keys: { key: 's', ctrl: true },
|
id: 'save', keys: { key: 's', ctrl: true },
|
||||||
category: 'editing', description: 'Save (auto-saves)',
|
category: 'editing', description: 'Save (auto-saves)',
|
||||||
@@ -417,30 +472,27 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
id: 'zoom-in', keys: { key: '=', ctrl: true },
|
id: 'zoom-in', keys: { key: '=', ctrl: true },
|
||||||
category: 'view', description: 'Zoom in',
|
category: 'view', description: 'Zoom in',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const z = Math.min(ctx.canvas.getZoom() * 1.2, 5);
|
const current = ctx.viewport.scale.x;
|
||||||
const center = ctx.canvas.getCenterPoint();
|
const z = Math.min(current * 1.2, 5);
|
||||||
ctx.canvas.zoomToPoint(center, z);
|
ctx.viewport.setZoom(z, true);
|
||||||
ctx.canvas.requestRenderAll();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'zoom-in-plus', keys: { key: '+', ctrl: true },
|
id: 'zoom-in-plus', keys: { key: '+', ctrl: true },
|
||||||
category: 'view', description: 'Zoom in',
|
category: 'view', description: 'Zoom in',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const z = Math.min(ctx.canvas.getZoom() * 1.2, 5);
|
const current = ctx.viewport.scale.x;
|
||||||
const center = ctx.canvas.getCenterPoint();
|
const z = Math.min(current * 1.2, 5);
|
||||||
ctx.canvas.zoomToPoint(center, z);
|
ctx.viewport.setZoom(z, true);
|
||||||
ctx.canvas.requestRenderAll();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'zoom-out', keys: { key: '-', ctrl: true },
|
id: 'zoom-out', keys: { key: '-', ctrl: true },
|
||||||
category: 'view', description: 'Zoom out',
|
category: 'view', description: 'Zoom out',
|
||||||
handler: (ctx) => {
|
handler: (ctx) => {
|
||||||
const z = Math.max(ctx.canvas.getZoom() / 1.2, 0.1);
|
const current = ctx.viewport.scale.x;
|
||||||
const center = ctx.canvas.getCenterPoint();
|
const z = Math.max(current / 1.2, 0.1);
|
||||||
ctx.canvas.zoomToPoint(center, z);
|
ctx.viewport.setZoom(z, true);
|
||||||
ctx.canvas.requestRenderAll();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -448,7 +500,7 @@ export const shortcuts: ShortcutDef[] = [
|
|||||||
category: 'view', description: 'Toggle grid',
|
category: 'view', description: 'Toggle grid',
|
||||||
handler: (ctx) => ctx.toggleGrid(),
|
handler: (ctx) => ctx.toggleGrid(),
|
||||||
},
|
},
|
||||||
// FIX: '?' key — when Shift+/ is pressed, e.key is '?' not '/'
|
// '?' key — when Shift+/ is pressed, e.key is '?' not '/'
|
||||||
{
|
{
|
||||||
id: 'show-help', keys: { key: '?' },
|
id: 'show-help', keys: { key: '?' },
|
||||||
category: 'view', description: 'Show shortcuts help',
|
category: 'view', description: 'Show shortcuts help',
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
* Actual shortcut definitions live in shortcut-definitions.ts.
|
* Actual shortcut definitions live in shortcut-definitions.ts.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Canvas, FabricObject } from 'fabric';
|
import type { SceneManager, SceneItem } from './SceneManager';
|
||||||
|
import type { SelectionManager } from './SelectionManager';
|
||||||
|
import type { UndoManager } from './history';
|
||||||
|
import type { Viewport } from 'pixi-viewport';
|
||||||
import { ToolType } from './tools';
|
import { ToolType } from './tools';
|
||||||
|
|
||||||
export interface ShortcutKeys {
|
export interface ShortcutKeys {
|
||||||
@@ -24,17 +27,15 @@ export interface ShortcutDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ShortcutContext {
|
export interface ShortcutContext {
|
||||||
canvas: Canvas;
|
scene: SceneManager;
|
||||||
getActiveObjects: () => FabricObject[];
|
selection: SelectionManager;
|
||||||
getActiveObject: () => FabricObject | null;
|
history: UndoManager;
|
||||||
clipboardRef: React.MutableRefObject<FabricObject[]>;
|
viewport: Viewport;
|
||||||
cloneFabricObject: (obj: any, dx: number, dy: number) => Promise<any>;
|
onChange: () => void;
|
||||||
writeCanvasToClipboard: (objects?: FabricObject[]) => Promise<void>;
|
clipboardRef: React.MutableRefObject<SceneItem[]>;
|
||||||
onCanvasChange: () => void;
|
|
||||||
showToast: (msg: string) => void;
|
showToast: (msg: string) => void;
|
||||||
fitAll: () => void;
|
fitAll: () => void;
|
||||||
setActiveTool: (tool: ToolType) => void;
|
setActiveTool: (tool: ToolType) => void;
|
||||||
undoRef: React.MutableRefObject<any>;
|
|
||||||
setCanUndo: (v: boolean) => void;
|
setCanUndo: (v: boolean) => void;
|
||||||
setCanRedo: (v: boolean) => void;
|
setCanRedo: (v: boolean) => void;
|
||||||
refreshLayers: () => void;
|
refreshLayers: () => void;
|
||||||
@@ -42,6 +43,7 @@ export interface ShortcutContext {
|
|||||||
handleUngroup: () => void;
|
handleUngroup: () => void;
|
||||||
toggleGrid: () => void;
|
toggleGrid: () => void;
|
||||||
toggleShowHelp: () => void;
|
toggleShowHelp: () => void;
|
||||||
|
writeCanvasToClipboard: (items?: SceneItem[]) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user