feat: implement MoveCommand for undoable layer movement
Critical Fix - Move Tool Undo/Redo: - Create MoveCommand class to track layer position changes - Capture initial and final positions during move operation - Only add to history if position actually changed - Real-time visual feedback during drag (via updateLayer) - Single undo point per move operation (not per pixel) Command Pattern: - MoveCommand extends BaseCommand - Implements execute() and undo() methods - captureAfterPosition() called on pointer up - hasChanged() prevents no-op entries in history Files: - core/commands/move-command.ts - New command class - tools/move-tool.ts - Updated to use MoveCommand - core/commands/index.ts - Export MoveCommand This fixes the critical issue where moving layers had no undo support. Users can now undo/redo layer movements as expected. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
export * from './base-command';
|
export * from './base-command';
|
||||||
export * from './layer-commands';
|
export * from './layer-commands';
|
||||||
export * from './draw-command';
|
export * from './draw-command';
|
||||||
|
export * from './move-command';
|
||||||
|
|||||||
60
core/commands/move-command.ts
Normal file
60
core/commands/move-command.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { BaseCommand } from './base-command';
|
||||||
|
import { useLayerStore } from '@/store/layer-store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command for moving a layer
|
||||||
|
* Captures start and end positions for undo/redo
|
||||||
|
*/
|
||||||
|
export class MoveCommand extends BaseCommand {
|
||||||
|
private layerId: string;
|
||||||
|
private beforePosition: { x: number; y: number };
|
||||||
|
private afterPosition: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
|
constructor(layerId: string, beforePosition: { x: number; y: number }) {
|
||||||
|
super('Move Layer');
|
||||||
|
this.layerId = layerId;
|
||||||
|
this.beforePosition = beforePosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture the final position after the move is complete
|
||||||
|
*/
|
||||||
|
captureAfterPosition(x: number, y: number): void {
|
||||||
|
this.afterPosition = { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(): void {
|
||||||
|
if (!this.afterPosition) {
|
||||||
|
// No-op on first execute (position already set during drag)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore after position (for redo)
|
||||||
|
const { updateLayer } = useLayerStore.getState();
|
||||||
|
updateLayer(this.layerId, {
|
||||||
|
x: this.afterPosition.x,
|
||||||
|
y: this.afterPosition.y,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(): void {
|
||||||
|
const { updateLayer } = useLayerStore.getState();
|
||||||
|
updateLayer(this.layerId, {
|
||||||
|
x: this.beforePosition.x,
|
||||||
|
y: this.beforePosition.y,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this move actually changed position
|
||||||
|
*/
|
||||||
|
hasChanged(): boolean {
|
||||||
|
if (!this.afterPosition) return false;
|
||||||
|
return (
|
||||||
|
this.beforePosition.x !== this.afterPosition.x ||
|
||||||
|
this.beforePosition.y !== this.afterPosition.y
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { BaseTool } from './base-tool';
|
import { BaseTool } from './base-tool';
|
||||||
import type { PointerState } from '@/types';
|
import type { PointerState } from '@/types';
|
||||||
import { useLayerStore } from '@/store/layer-store';
|
import { useLayerStore } from '@/store/layer-store';
|
||||||
|
import { useHistoryStore } from '@/store/history-store';
|
||||||
|
import { MoveCommand } from '@/core/commands';
|
||||||
|
|
||||||
export class MoveTool extends BaseTool {
|
export class MoveTool extends BaseTool {
|
||||||
private startX = 0;
|
private startX = 0;
|
||||||
private startY = 0;
|
private startY = 0;
|
||||||
private layerStartX = 0;
|
private layerStartX = 0;
|
||||||
private layerStartY = 0;
|
private layerStartY = 0;
|
||||||
|
private moveCommand: MoveCommand | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('Move');
|
super('Move');
|
||||||
@@ -23,6 +26,9 @@ export class MoveTool extends BaseTool {
|
|||||||
this.startY = pointer.y;
|
this.startY = pointer.y;
|
||||||
this.layerStartX = layer.x;
|
this.layerStartX = layer.x;
|
||||||
this.layerStartY = layer.y;
|
this.layerStartY = layer.y;
|
||||||
|
|
||||||
|
// Create move command with initial position
|
||||||
|
this.moveCommand = new MoveCommand(layer.id, { x: layer.x, y: layer.y });
|
||||||
}
|
}
|
||||||
|
|
||||||
onPointerMove(pointer: PointerState): void {
|
onPointerMove(pointer: PointerState): void {
|
||||||
@@ -34,6 +40,7 @@ export class MoveTool extends BaseTool {
|
|||||||
const dx = pointer.x - this.startX;
|
const dx = pointer.x - this.startX;
|
||||||
const dy = pointer.y - this.startY;
|
const dy = pointer.y - this.startY;
|
||||||
|
|
||||||
|
// Update position in real-time (for visual feedback)
|
||||||
const { updateLayer } = useLayerStore.getState();
|
const { updateLayer } = useLayerStore.getState();
|
||||||
updateLayer(layer.id, {
|
updateLayer(layer.id, {
|
||||||
x: this.layerStartX + dx,
|
x: this.layerStartX + dx,
|
||||||
@@ -42,6 +49,21 @@ export class MoveTool extends BaseTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onPointerUp(): void {
|
onPointerUp(): void {
|
||||||
|
if (this.moveCommand) {
|
||||||
|
const layer = this.getActiveLayer();
|
||||||
|
if (layer) {
|
||||||
|
// Capture final position
|
||||||
|
this.moveCommand.captureAfterPosition(layer.x, layer.y);
|
||||||
|
|
||||||
|
// Only add to history if position actually changed
|
||||||
|
if (this.moveCommand.hasChanged()) {
|
||||||
|
const { executeCommand } = useHistoryStore.getState();
|
||||||
|
executeCommand(this.moveCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.moveCommand = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.isDrawing = false;
|
this.isDrawing = false;
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user