From edcac219839f23a22803dbf33f808aed54af1f0c Mon Sep 17 00:00:00 2001 From: Hiren Kangad Date: Mon, 9 Mar 2026 23:23:10 +0530 Subject: [PATCH] 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. --- frontend/src/canvas/shortcut-definitions.ts | 338 +++++++++++--------- frontend/src/canvas/shortcuts.ts | 20 +- 2 files changed, 206 insertions(+), 152 deletions(-) diff --git a/frontend/src/canvas/shortcut-definitions.ts b/frontend/src/canvas/shortcut-definitions.ts index 40f50ba..0612f85 100644 --- a/frontend/src/canvas/shortcut-definitions.ts +++ b/frontend/src/canvas/shortcut-definitions.ts @@ -2,29 +2,20 @@ * All shortcut definitions — the single source of truth for keyboard shortcuts. * 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+P (browser print) → changed to Ctrl+Shift+P for arrange optimal * - '?' key (Shift+/) → e.key is '?' not '/', fixed matcher key * - ArrowUp/Down with selection → layer ordering uses ] / [ again (arrows cycle/nudge) * - 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) */ import { ShortcutDef } from './shortcuts'; -import { FabricObject } from 'fabric'; 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[] = [ // ═══════════════════════════════════════ @@ -34,22 +25,34 @@ export const shortcuts: ShortcutDef[] = [ { id: 'align-left', keys: { key: 'arrowleft', ctrl: true }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 }, 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 // ═══════════════════════════════════════ - // FIX: Changed from Ctrl+P (browser print) to Ctrl+Shift+P { id: 'arrange-optimal', keys: { key: 'p', ctrl: true, shift: true }, 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 }, 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 }, category: 'arrangement', description: 'Arrange by z-order', needsSelection: true, minSelection: 2, handler: (ctx) => { - const { objects, restore } = ops.breakSelection(ctx.canvas); - ops.arrangeByZOrder(objects, ctx.canvas.getObjects()); - restore(); - ctx.canvas.requestRenderAll(); ctx.onCanvasChange(); + ops.arrangeByZOrder(ctx.selection.getSelectedItems()); + ctx.onChange(); }, }, { id: 'arrange-random', keys: { key: 'r', ctrl: true, alt: true }, 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 }, 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 }, category: 'image', description: 'Flip horizontal', needsSelection: true, handler: (ctx) => { - ops.flipHorizontal(ctx.getActiveObjects()); - ctx.canvas.requestRenderAll(); ctx.onCanvasChange(); + ops.flipHorizontal(ctx.selection.getSelectedItems()); + ctx.onChange(); }, }, { id: 'flip-v', keys: { key: 'v', alt: true, shift: true }, category: 'image', description: 'Flip vertical', needsSelection: true, handler: (ctx) => { - ops.flipVertical(ctx.getActiveObjects()); - ctx.canvas.requestRenderAll(); ctx.onCanvasChange(); + ops.flipVertical(ctx.selection.getSelectedItems()); + ctx.onChange(); }, }, { id: 'reset-transform', keys: { key: 't', ctrl: true, shift: true }, category: 'image', description: 'Reset transform', needsSelection: true, handler: (ctx) => { - ops.resetTransform(ctx.getActiveObjects()); - ctx.canvas.requestRenderAll(); ctx.onCanvasChange(); + ops.resetTransform(ctx.selection.getSelectedItems()); + ctx.onChange(); }, }, { id: 'toggle-grayscale', keys: { key: 'g', alt: true }, category: 'image', description: 'Toggle grayscale', needsSelection: true, handler: (ctx) => { - ops.toggleGrayscale(ctx.getActiveObjects()); - ctx.canvas.requestRenderAll(); ctx.onCanvasChange(); + ops.toggleGrayscale(ctx.selection.getSelectedItems()); + ctx.onChange(); }, }, { id: 'toggle-locked', keys: { key: 'l', alt: true }, category: 'image', description: 'Toggle locked', needsSelection: true, handler: (ctx) => { - ops.toggleLocked(ctx.getActiveObjects()); - ctx.canvas.requestRenderAll(); ctx.refreshLayers(); + ops.toggleLocked(ctx.selection.getSelectedItems()); + ctx.refreshLayers(); }, }, - // FIX: Changed from Ctrl+Y (conflicts with redo) to Ctrl+Shift+Y { id: 'overlay-compare', keys: { key: 'y', ctrl: true, shift: true }, 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' }, category: 'navigation', description: 'Clear selection', handler: (ctx) => { - ctx.canvas.discardActiveObject(); - ctx.canvas.requestRenderAll(); + ctx.selection.clear(); }, }, { @@ -196,40 +227,51 @@ export const shortcuts: ShortcutDef[] = [ category: 'navigation', description: 'Fit all in view', 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). - // Layer ordering uses ] / [ (restored) to avoid arrow conflicts. + // Layer ordering uses ] / [ to avoid arrow conflicts. { id: 'cycle-next', keys: { key: 'arrowright' }, category: 'navigation', description: 'Select next object', handler: (ctx) => { - // Only cycle when nothing selected — if selected, do nothing - if (ctx.getActiveObjects().length > 0) return; - const objects = ctx.canvas.getObjects(); - if (objects.length === 0) return; - ctx.canvas.setActiveObject(objects[0]); - ctx.canvas.requestRenderAll(); + if (ctx.selection.selectedIds.size > 0) return; + const all = ctx.scene.getAllItems(); + if (all.length === 0) return; + // Sort by z to get consistent order + all.sort((a, b) => a.data.z - b.data.z); + ctx.selection.selectOnly(all[0].id); }, }, { id: 'cycle-prev', keys: { key: 'arrowleft' }, category: 'navigation', description: 'Select previous object', handler: (ctx) => { - if (ctx.getActiveObjects().length > 0) return; - const objects = ctx.canvas.getObjects(); - if (objects.length === 0) return; - ctx.canvas.setActiveObject(objects[objects.length - 1]); - ctx.canvas.requestRenderAll(); + if (ctx.selection.selectedIds.size > 0) return; + const all = ctx.scene.getAllItems(); + if (all.length === 0) return; + all.sort((a, b) => a.data.z - b.data.z); + 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: ']' }, category: 'navigation', description: 'Bring forward', needsSelection: true, handler: (ctx) => { - ctx.getActiveObjects().forEach((obj) => (ctx.canvas as any).bringObjectForward(obj)); - ctx.canvas.requestRenderAll(); ctx.onCanvasChange(); + const selected = ctx.selection.getSelectedItems(); + 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', needsSelection: true, handler: (ctx) => { - ctx.getActiveObjects().forEach((obj) => (ctx.canvas as any).sendObjectBackwards(obj)); - ctx.canvas.requestRenderAll(); ctx.onCanvasChange(); + const selected = ctx.selection.getSelectedItems(); + 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 }, category: 'editing', description: 'Select all', handler: (ctx) => { - ctx.canvas.discardActiveObject(); - 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(); + ctx.selection.selectAll(); }, }, { id: 'copy', keys: { key: 'c', ctrl: true }, category: 'editing', description: 'Copy', handler: (ctx) => { - const active = ctx.getActiveObjects(); - if (active.length > 0) { - ctx.clipboardRef.current = [...active]; - ctx.writeCanvasToClipboard(active); + const selected = ctx.selection.getSelectedItems(); + if (selected.length > 0) { + ctx.clipboardRef.current = [...selected]; + ctx.writeCanvasToClipboard(selected); ctx.showToast('Copied'); } else { ctx.writeCanvasToClipboard(); @@ -278,8 +324,8 @@ export const shortcuts: ShortcutDef[] = [ id: 'copy-as-image', keys: { key: 'c', ctrl: true, shift: true }, category: 'editing', description: 'Copy as image to clipboard', handler: (ctx) => { - const active = ctx.getActiveObjects(); - ctx.writeCanvasToClipboard(active.length > 0 ? active : undefined); + const selected = ctx.selection.getSelectedItems(); + ctx.writeCanvasToClipboard(selected.length > 0 ? selected : undefined); }, }, { @@ -287,33 +333,38 @@ export const shortcuts: ShortcutDef[] = [ category: 'editing', description: 'Paste', handler: async (ctx) => { if (ctx.clipboardRef.current.length === 0) return; - const newObjs: FabricObject[] = []; + const newItems: typeof ctx.clipboardRef.current = []; for (const original of ctx.clipboardRef.current) { - try { - const obj = await ctx.cloneFabricObject(original, 20, 20); - ctx.canvas.add(obj); - newObjs.push(obj); - } catch (err) { - console.error('Paste object failed:', err); - } + // Clone: duplicate the item data with new ID and offset position + const newData = { + ...original.data, + id: crypto.randomUUID(), + x: original.data.x + 20, + 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.canvas.requestRenderAll(); - ctx.onCanvasChange(); + ctx.clipboardRef.current = newItems; + ctx.scene._applyZOrder(); + ctx.onChange(); }, }, { id: 'cut', keys: { key: 'x', ctrl: true }, category: 'editing', description: 'Cut', handler: (ctx) => { - const active = ctx.getActiveObjects(); - if (active.length === 0) return; - ctx.clipboardRef.current = [...active]; - ctx.writeCanvasToClipboard(active); - active.forEach((obj) => ctx.canvas.remove(obj)); - ctx.canvas.discardActiveObject(); - ctx.canvas.requestRenderAll(); - ctx.onCanvasChange(); + const selected = ctx.selection.getSelectedItems(); + if (selected.length === 0) return; + ctx.clipboardRef.current = [...selected]; + ctx.writeCanvasToClipboard(selected); + for (const item of selected) { + ctx.scene.removeItem(item.id, true); + } + ctx.selection.clear(); + ctx.onChange(); ctx.showToast('Cut'); }, }, @@ -321,19 +372,21 @@ export const shortcuts: ShortcutDef[] = [ id: 'duplicate', keys: { key: 'd', ctrl: true }, category: 'editing', description: 'Duplicate', handler: async (ctx) => { - const active = ctx.getActiveObjects(); - if (active.length === 0) return; - for (const original of active) { - try { - const obj = await ctx.cloneFabricObject(original, 20, 20); - ctx.canvas.add(obj); - } catch (err) { - console.error('Duplicate failed:', err); - } + const selected = ctx.selection.getSelectedItems(); + if (selected.length === 0) return; + for (const original of selected) { + const newData = { + ...original.data, + id: crypto.randomUUID(), + x: original.data.x + 20, + y: original.data.y + 20, + z: ctx.scene.nextZ(), + }; + await ctx.scene._createItem(newData, true); } - ctx.canvas.discardActiveObject(); - ctx.canvas.requestRenderAll(); - ctx.onCanvasChange(); + ctx.selection.clear(); + ctx.scene._applyZOrder(); + ctx.onChange(); }, }, { @@ -341,11 +394,12 @@ export const shortcuts: ShortcutDef[] = [ category: 'editing', description: 'Delete selected', needsSelection: true, handler: (ctx) => { - const active = ctx.getActiveObjects(); - active.forEach((obj) => ctx.canvas.remove(obj)); - ctx.canvas.discardActiveObject(); - ctx.canvas.requestRenderAll(); - ctx.onCanvasChange(); + const selected = ctx.selection.getSelectedItems(); + for (const item of selected) { + ctx.scene.removeItem(item.id, true); + } + ctx.selection.clear(); + ctx.onChange(); }, }, { @@ -353,41 +407,42 @@ export const shortcuts: ShortcutDef[] = [ category: 'editing', description: 'Delete selected', needsSelection: true, handler: (ctx) => { - const active = ctx.getActiveObjects(); - active.forEach((obj) => ctx.canvas.remove(obj)); - ctx.canvas.discardActiveObject(); - ctx.canvas.requestRenderAll(); - ctx.onCanvasChange(); + const selected = ctx.selection.getSelectedItems(); + for (const item of selected) { + ctx.scene.removeItem(item.id, true); + } + ctx.selection.clear(); + ctx.onChange(); }, }, { id: 'undo', keys: { key: 'z', ctrl: true }, category: 'editing', description: 'Undo', handler: (ctx) => { - ctx.undoRef.current?.undo(); - ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false); - ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false); - ctx.onCanvasChange(); + ctx.history.undo(); + ctx.setCanUndo(ctx.history.canUndo()); + ctx.setCanRedo(ctx.history.canRedo()); + ctx.onChange(); }, }, { id: 'redo', keys: { key: 'z', ctrl: true, shift: true }, category: 'editing', description: 'Redo', handler: (ctx) => { - ctx.undoRef.current?.redo(); - ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false); - ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false); - ctx.onCanvasChange(); + ctx.history.redo(); + ctx.setCanUndo(ctx.history.canUndo()); + ctx.setCanRedo(ctx.history.canRedo()); + ctx.onChange(); }, }, { id: 'redo-y', keys: { key: 'y', ctrl: true }, category: 'editing', description: 'Redo (alt)', handler: (ctx) => { - ctx.undoRef.current?.redo(); - ctx.setCanUndo(ctx.undoRef.current?.canUndo() ?? false); - ctx.setCanRedo(ctx.undoRef.current?.canRedo() ?? false); - ctx.onCanvasChange(); + ctx.history.redo(); + ctx.setCanUndo(ctx.history.canUndo()); + ctx.setCanRedo(ctx.history.canRedo()); + ctx.onChange(); }, }, { @@ -400,7 +455,7 @@ export const shortcuts: ShortcutDef[] = [ category: 'editing', description: 'Ungroup', needsSelection: true, 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 }, category: 'editing', description: 'Save (auto-saves)', @@ -417,30 +472,27 @@ export const shortcuts: ShortcutDef[] = [ id: 'zoom-in', keys: { key: '=', ctrl: true }, category: 'view', description: 'Zoom in', handler: (ctx) => { - const z = Math.min(ctx.canvas.getZoom() * 1.2, 5); - const center = ctx.canvas.getCenterPoint(); - ctx.canvas.zoomToPoint(center, z); - ctx.canvas.requestRenderAll(); + const current = ctx.viewport.scale.x; + const z = Math.min(current * 1.2, 5); + ctx.viewport.setZoom(z, true); }, }, { id: 'zoom-in-plus', keys: { key: '+', ctrl: true }, category: 'view', description: 'Zoom in', handler: (ctx) => { - const z = Math.min(ctx.canvas.getZoom() * 1.2, 5); - const center = ctx.canvas.getCenterPoint(); - ctx.canvas.zoomToPoint(center, z); - ctx.canvas.requestRenderAll(); + const current = ctx.viewport.scale.x; + const z = Math.min(current * 1.2, 5); + ctx.viewport.setZoom(z, true); }, }, { id: 'zoom-out', keys: { key: '-', ctrl: true }, category: 'view', description: 'Zoom out', handler: (ctx) => { - const z = Math.max(ctx.canvas.getZoom() / 1.2, 0.1); - const center = ctx.canvas.getCenterPoint(); - ctx.canvas.zoomToPoint(center, z); - ctx.canvas.requestRenderAll(); + const current = ctx.viewport.scale.x; + const z = Math.max(current / 1.2, 0.1); + ctx.viewport.setZoom(z, true); }, }, { @@ -448,7 +500,7 @@ export const shortcuts: ShortcutDef[] = [ category: 'view', description: 'Toggle grid', 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: '?' }, category: 'view', description: 'Show shortcuts help', diff --git a/frontend/src/canvas/shortcuts.ts b/frontend/src/canvas/shortcuts.ts index e20bf36..3ebf35b 100644 --- a/frontend/src/canvas/shortcuts.ts +++ b/frontend/src/canvas/shortcuts.ts @@ -3,7 +3,10 @@ * 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'; export interface ShortcutKeys { @@ -24,17 +27,15 @@ export interface ShortcutDef { } export interface ShortcutContext { - canvas: Canvas; - getActiveObjects: () => FabricObject[]; - getActiveObject: () => FabricObject | null; - clipboardRef: React.MutableRefObject; - cloneFabricObject: (obj: any, dx: number, dy: number) => Promise; - writeCanvasToClipboard: (objects?: FabricObject[]) => Promise; - onCanvasChange: () => void; + scene: SceneManager; + selection: SelectionManager; + history: UndoManager; + viewport: Viewport; + onChange: () => void; + clipboardRef: React.MutableRefObject; showToast: (msg: string) => void; fitAll: () => void; setActiveTool: (tool: ToolType) => void; - undoRef: React.MutableRefObject; setCanUndo: (v: boolean) => void; setCanRedo: (v: boolean) => void; refreshLayers: () => void; @@ -42,6 +43,7 @@ export interface ShortcutContext { handleUngroup: () => void; toggleGrid: () => void; toggleShowHelp: () => void; + writeCanvasToClipboard: (items?: SceneItem[]) => Promise; } /**