抽出编辑器舞台交互状态模型
新增 ImageCanvasStageInteractionModel 承载 pointer 与拖拽状态规则 补充舞台交互状态模型单测 精简 useImageCanvasStageInteractions 的状态构造逻辑 更新 TRACKING.md 记录第四十二阶段验证
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
GenerateDialogState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
createCanvasMarqueeState,
|
||||
createGenerationFrameDragState,
|
||||
createLayerDragStart,
|
||||
createMinimapDragState,
|
||||
createPanDragState,
|
||||
getCanvasPointFromPointer,
|
||||
getPointerButton,
|
||||
getPointerClient,
|
||||
getPointerId,
|
||||
resolveLayerPointerSelection,
|
||||
updateGenerateDialogForLayerClick,
|
||||
updateGenerateDialogForLayerPointerDown,
|
||||
updateMinimapDragMovement,
|
||||
} from './ImageCanvasStageInteractionModel';
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
const id = overrides.id ?? 'layer-a';
|
||||
return {
|
||||
id,
|
||||
resourceId: `resource-${id}`,
|
||||
title: id,
|
||||
src: `data:image/png;base64,${id}`,
|
||||
x: 100,
|
||||
y: 120,
|
||||
width: 240,
|
||||
height: 160,
|
||||
originalWidth: 240,
|
||||
originalHeight: 160,
|
||||
zIndex: 1,
|
||||
sourceType: 'uploaded',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ImageCanvasStageInteractionModel', () => {
|
||||
it('normalizes pointer button, client point, and pointer id', () => {
|
||||
expect(
|
||||
getPointerButton({
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
nativeEvent: { button: 0, buttons: 4 },
|
||||
}),
|
||||
).toBe(1);
|
||||
expect(
|
||||
getPointerButton({
|
||||
button: 2,
|
||||
buttons: 0,
|
||||
nativeEvent: { button: 0, buttons: 0 },
|
||||
}),
|
||||
).toBe(2);
|
||||
expect(
|
||||
getPointerClient({
|
||||
clientX: Number.NaN,
|
||||
clientY: 42,
|
||||
nativeEvent: { clientX: 120, clientY: 80 },
|
||||
}),
|
||||
).toEqual({ x: 120, y: 42 });
|
||||
expect(
|
||||
getPointerId({
|
||||
pointerId: Number.NaN,
|
||||
nativeEvent: { pointerId: 18 },
|
||||
}),
|
||||
).toBe(18);
|
||||
expect(getPointerId({})).toBe(-1);
|
||||
});
|
||||
|
||||
it('creates pan and marquee states from normalized pointer positions', () => {
|
||||
expect(
|
||||
getCanvasPointFromPointer({
|
||||
pointer: { x: 180, y: 140 },
|
||||
rect: { left: 20, top: 30 },
|
||||
}),
|
||||
).toEqual({ x: 160, y: 110 });
|
||||
expect(
|
||||
createCanvasMarqueeState({
|
||||
pointerId: 8,
|
||||
pointer: { x: 180, y: 140 },
|
||||
rect: { left: 20, top: 30 },
|
||||
}),
|
||||
).toEqual({
|
||||
pointerId: 8,
|
||||
startX: 160,
|
||||
startY: 110,
|
||||
currentX: 160,
|
||||
currentY: 110,
|
||||
});
|
||||
expect(
|
||||
createPanDragState({
|
||||
pointerId: 9,
|
||||
pointer: { x: 320, y: 240 },
|
||||
viewport: { x: 10, y: 20, scale: 1.5 },
|
||||
}),
|
||||
).toEqual({
|
||||
kind: 'pan',
|
||||
pointerId: 9,
|
||||
startClientX: 320,
|
||||
startClientY: 240,
|
||||
startViewport: { x: 10, y: 20, scale: 1.5 },
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves multi-select layer toggles without emptying the last selection', () => {
|
||||
expect(
|
||||
resolveLayerPointerSelection({
|
||||
layerId: 'a',
|
||||
selectedLayerIds: ['b'],
|
||||
isMultiSelectGesture: false,
|
||||
}),
|
||||
).toEqual(['a']);
|
||||
expect(
|
||||
resolveLayerPointerSelection({
|
||||
layerId: 'a',
|
||||
selectedLayerIds: ['b'],
|
||||
isMultiSelectGesture: true,
|
||||
}),
|
||||
).toEqual(['b', 'a']);
|
||||
expect(
|
||||
resolveLayerPointerSelection({
|
||||
layerId: 'b',
|
||||
selectedLayerIds: ['a', 'b'],
|
||||
isMultiSelectGesture: true,
|
||||
}),
|
||||
).toEqual(['a']);
|
||||
expect(
|
||||
resolveLayerPointerSelection({
|
||||
layerId: 'a',
|
||||
selectedLayerIds: ['a'],
|
||||
isMultiSelectGesture: true,
|
||||
}),
|
||||
).toEqual(['a']);
|
||||
});
|
||||
|
||||
it('creates layer drag state for the next selected layer group', () => {
|
||||
const layers = [
|
||||
createLayer({ id: 'a', x: 40, y: 50 }),
|
||||
createLayer({ id: 'b', x: 120, y: 150 }),
|
||||
createLayer({ id: 'c', x: 220, y: 250 }),
|
||||
];
|
||||
|
||||
expect(
|
||||
createLayerDragStart({
|
||||
layer: layers[1]!,
|
||||
layers,
|
||||
selectedLayerIds: ['a'],
|
||||
isMultiSelectGesture: true,
|
||||
pointerId: 4,
|
||||
pointer: { x: 300, y: 260 },
|
||||
viewportScale: 1.25,
|
||||
}),
|
||||
).toEqual({
|
||||
selectedLayerIds: ['a', 'b'],
|
||||
dragState: {
|
||||
kind: 'layer',
|
||||
pointerId: 4,
|
||||
layerId: 'b',
|
||||
layerIds: ['a', 'b'],
|
||||
startClientX: 300,
|
||||
startClientY: 260,
|
||||
startLayerX: 120,
|
||||
startLayerY: 150,
|
||||
startLayers: [
|
||||
{ id: 'a', x: 40, y: 50 },
|
||||
{ id: 'b', x: 120, y: 150 },
|
||||
],
|
||||
startScale: 1.25,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps generation composers open only for the clicked generated layer', () => {
|
||||
const generatedDialog: GenerateDialogState = {
|
||||
id: 'dialog-1',
|
||||
mode: 'generate',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: false,
|
||||
generatedLayerId: 'layer-a',
|
||||
};
|
||||
const editDialog: GenerateDialogState = {
|
||||
mode: 'edit',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
sourceLayerId: 'layer-a',
|
||||
};
|
||||
const draftDialogWithoutId: GenerateDialogState = {
|
||||
mode: 'generate',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
};
|
||||
|
||||
expect(
|
||||
updateGenerateDialogForLayerPointerDown(generatedDialog, 'layer-a'),
|
||||
).toEqual({
|
||||
...generatedDialog,
|
||||
composerOpen: true,
|
||||
});
|
||||
expect(
|
||||
updateGenerateDialogForLayerPointerDown(generatedDialog, 'layer-b'),
|
||||
).toEqual({
|
||||
...generatedDialog,
|
||||
composerOpen: false,
|
||||
});
|
||||
expect(updateGenerateDialogForLayerPointerDown(editDialog, 'layer-a')).toBe(
|
||||
editDialog,
|
||||
);
|
||||
expect(updateGenerateDialogForLayerClick(generatedDialog)).toEqual({
|
||||
...generatedDialog,
|
||||
composerOpen: false,
|
||||
});
|
||||
expect(
|
||||
updateGenerateDialogForLayerClick(draftDialogWithoutId),
|
||||
).toMatchObject({
|
||||
mode: 'generate',
|
||||
composerOpen: false,
|
||||
});
|
||||
expect(updateGenerateDialogForLayerClick(editDialog)).toBe(editDialog);
|
||||
});
|
||||
|
||||
it('creates generation frame and minimap drag states', () => {
|
||||
const dialog: CanvasGenerationDialogState = {
|
||||
id: 'dialog-1',
|
||||
mode: 'generate',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
placeholder: {
|
||||
x: 200,
|
||||
y: 160,
|
||||
width: 420,
|
||||
height: 420,
|
||||
originalWidth: 2048,
|
||||
originalHeight: 2048,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
createGenerationFrameDragState({
|
||||
dialog,
|
||||
pointerId: 11,
|
||||
pointer: { x: 240, y: 200 },
|
||||
viewportScale: 2,
|
||||
}),
|
||||
).toEqual({
|
||||
kind: 'generation-frame',
|
||||
dialogId: 'dialog-1',
|
||||
pointerId: 11,
|
||||
startClientX: 240,
|
||||
startClientY: 200,
|
||||
startFrameX: 200,
|
||||
startFrameY: 160,
|
||||
startScale: 2,
|
||||
});
|
||||
expect(
|
||||
createGenerationFrameDragState({
|
||||
dialog: { ...dialog, placeholder: undefined },
|
||||
pointerId: 11,
|
||||
pointer: { x: 240, y: 200 },
|
||||
viewportScale: 2,
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
const minimapDrag = createMinimapDragState({
|
||||
pointerId: 12,
|
||||
pointer: { x: 120, y: 90 },
|
||||
viewport: { x: 10, y: 20, scale: 1 },
|
||||
minimapScale: 0.4,
|
||||
});
|
||||
expect(minimapDrag).toEqual({
|
||||
kind: 'minimap',
|
||||
pointerId: 12,
|
||||
startClientX: 120,
|
||||
startClientY: 90,
|
||||
startViewport: { x: 10, y: 20, scale: 1 },
|
||||
minimapScale: 0.4,
|
||||
moved: false,
|
||||
});
|
||||
expect(
|
||||
updateMinimapDragMovement(minimapDrag, { x: 121, y: 90 }),
|
||||
).toBe(minimapDrag);
|
||||
expect(updateMinimapDragMovement(minimapDrag, { x: 123, y: 90 })).toEqual({
|
||||
...minimapDrag,
|
||||
moved: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
308
src/components/image-editor/ImageCanvasStageInteractionModel.ts
Normal file
308
src/components/image-editor/ImageCanvasStageInteractionModel.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
CanvasMarqueeState,
|
||||
CanvasViewport,
|
||||
DragState,
|
||||
GenerateDialogState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import type { CanvasPoint } from './ImageCanvasInteractionModel';
|
||||
|
||||
type PointerSource = {
|
||||
button?: number;
|
||||
buttons?: number;
|
||||
clientX?: number;
|
||||
clientY?: number;
|
||||
pointerId?: number;
|
||||
nativeEvent?: {
|
||||
button?: number;
|
||||
buttons?: number;
|
||||
clientX?: number;
|
||||
clientY?: number;
|
||||
pointerId?: number;
|
||||
};
|
||||
};
|
||||
|
||||
type CanvasRectLike = {
|
||||
left?: number;
|
||||
top?: number;
|
||||
} | null | undefined;
|
||||
|
||||
const CANVAS_GENERATION_DIALOG_MODES = new Set([
|
||||
'generate',
|
||||
'spec',
|
||||
'character',
|
||||
'icon',
|
||||
]);
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function hasCanvasGenerationDialogMode(
|
||||
dialog: GenerateDialogState | null,
|
||||
): dialog is GenerateDialogState & {
|
||||
mode: CanvasGenerationDialogState['mode'];
|
||||
} {
|
||||
if (!dialog) {
|
||||
return false;
|
||||
}
|
||||
return CANVAS_GENERATION_DIALOG_MODES.has(dialog.mode);
|
||||
}
|
||||
|
||||
export function getPointerButton(event: PointerSource) {
|
||||
const nativeButtons = Number(event.nativeEvent?.buttons);
|
||||
if (Number.isFinite(nativeButtons) && (nativeButtons & 4) === 4) {
|
||||
return 1;
|
||||
}
|
||||
const syntheticButtons = Number(event.buttons);
|
||||
if (Number.isFinite(syntheticButtons) && (syntheticButtons & 4) === 4) {
|
||||
return 1;
|
||||
}
|
||||
const syntheticButton = Number(event.button);
|
||||
if (Number.isFinite(syntheticButton)) {
|
||||
return syntheticButton;
|
||||
}
|
||||
const nativeButton = Number(event.nativeEvent?.button);
|
||||
if (Number.isFinite(nativeButton)) {
|
||||
return nativeButton;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getPointerClient(event: PointerSource): CanvasPoint {
|
||||
return {
|
||||
x: isFiniteNumber(event.clientX)
|
||||
? event.clientX
|
||||
: isFiniteNumber(event.nativeEvent?.clientX)
|
||||
? event.nativeEvent.clientX
|
||||
: 0,
|
||||
y: isFiniteNumber(event.clientY)
|
||||
? event.clientY
|
||||
: isFiniteNumber(event.nativeEvent?.clientY)
|
||||
? event.nativeEvent.clientY
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function getPointerId(event: PointerSource) {
|
||||
return isFiniteNumber(event.pointerId)
|
||||
? event.pointerId
|
||||
: isFiniteNumber(event.nativeEvent?.pointerId)
|
||||
? event.nativeEvent.pointerId
|
||||
: -1;
|
||||
}
|
||||
|
||||
export function getCanvasPointFromPointer({
|
||||
pointer,
|
||||
rect,
|
||||
}: {
|
||||
pointer: CanvasPoint;
|
||||
rect: CanvasRectLike;
|
||||
}): CanvasPoint {
|
||||
return {
|
||||
x: pointer.x - (rect?.left ?? 0),
|
||||
y: pointer.y - (rect?.top ?? 0),
|
||||
};
|
||||
}
|
||||
|
||||
export function createCanvasMarqueeState({
|
||||
pointerId,
|
||||
pointer,
|
||||
rect,
|
||||
}: {
|
||||
pointerId: number;
|
||||
pointer: CanvasPoint;
|
||||
rect: CanvasRectLike;
|
||||
}): CanvasMarqueeState {
|
||||
const startPoint = getCanvasPointFromPointer({ pointer, rect });
|
||||
return {
|
||||
pointerId,
|
||||
startX: startPoint.x,
|
||||
startY: startPoint.y,
|
||||
currentX: startPoint.x,
|
||||
currentY: startPoint.y,
|
||||
};
|
||||
}
|
||||
|
||||
export function createPanDragState({
|
||||
pointerId,
|
||||
pointer,
|
||||
viewport,
|
||||
}: {
|
||||
pointerId: number;
|
||||
pointer: CanvasPoint;
|
||||
viewport: CanvasViewport;
|
||||
}): Extract<DragState, { kind: 'pan' }> {
|
||||
return {
|
||||
kind: 'pan',
|
||||
pointerId,
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startViewport: viewport,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveLayerPointerSelection({
|
||||
layerId,
|
||||
selectedLayerIds,
|
||||
isMultiSelectGesture,
|
||||
}: {
|
||||
layerId: string;
|
||||
selectedLayerIds: string[];
|
||||
isMultiSelectGesture: boolean;
|
||||
}) {
|
||||
if (!isMultiSelectGesture) {
|
||||
return [layerId];
|
||||
}
|
||||
if (!selectedLayerIds.includes(layerId)) {
|
||||
return [...selectedLayerIds, layerId];
|
||||
}
|
||||
if (selectedLayerIds.length <= 1) {
|
||||
return [layerId];
|
||||
}
|
||||
return selectedLayerIds.filter((currentLayerId) => currentLayerId !== layerId);
|
||||
}
|
||||
|
||||
export function createLayerDragStart({
|
||||
layer,
|
||||
layers,
|
||||
selectedLayerIds,
|
||||
isMultiSelectGesture,
|
||||
pointerId,
|
||||
pointer,
|
||||
viewportScale,
|
||||
}: {
|
||||
layer: CanvasLayer;
|
||||
layers: CanvasLayer[];
|
||||
selectedLayerIds: string[];
|
||||
isMultiSelectGesture: boolean;
|
||||
pointerId: number;
|
||||
pointer: CanvasPoint;
|
||||
viewportScale: number;
|
||||
}): {
|
||||
selectedLayerIds: string[];
|
||||
dragState: Extract<DragState, { kind: 'layer' }>;
|
||||
} {
|
||||
const nextSelectedLayerIds = resolveLayerPointerSelection({
|
||||
layerId: layer.id,
|
||||
selectedLayerIds,
|
||||
isMultiSelectGesture,
|
||||
});
|
||||
const dragLayerIds = nextSelectedLayerIds.includes(layer.id)
|
||||
? nextSelectedLayerIds
|
||||
: [layer.id];
|
||||
const startLayers = layers
|
||||
.filter((currentLayer) => dragLayerIds.includes(currentLayer.id))
|
||||
.map((currentLayer) => ({
|
||||
id: currentLayer.id,
|
||||
x: currentLayer.x,
|
||||
y: currentLayer.y,
|
||||
}));
|
||||
|
||||
return {
|
||||
selectedLayerIds: nextSelectedLayerIds,
|
||||
dragState: {
|
||||
kind: 'layer',
|
||||
pointerId,
|
||||
layerId: layer.id,
|
||||
layerIds: dragLayerIds,
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startLayerX: layer.x,
|
||||
startLayerY: layer.y,
|
||||
startLayers,
|
||||
startScale: viewportScale,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateGenerateDialogForLayerPointerDown(
|
||||
dialog: GenerateDialogState | null,
|
||||
layerId: string,
|
||||
): GenerateDialogState | null {
|
||||
if (!hasCanvasGenerationDialogMode(dialog)) {
|
||||
return dialog;
|
||||
}
|
||||
return {
|
||||
...dialog,
|
||||
composerOpen: dialog.generatedLayerId === layerId,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateGenerateDialogForLayerClick(
|
||||
dialog: GenerateDialogState | null,
|
||||
): GenerateDialogState | null {
|
||||
if (!hasCanvasGenerationDialogMode(dialog)) {
|
||||
return dialog;
|
||||
}
|
||||
return {
|
||||
...dialog,
|
||||
composerOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGenerationFrameDragState({
|
||||
dialog,
|
||||
pointerId,
|
||||
pointer,
|
||||
viewportScale,
|
||||
}: {
|
||||
dialog: CanvasGenerationDialogState;
|
||||
pointerId: number;
|
||||
pointer: CanvasPoint;
|
||||
viewportScale: number;
|
||||
}): Extract<DragState, { kind: 'generation-frame' }> | null {
|
||||
if (!dialog.placeholder) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
kind: 'generation-frame',
|
||||
dialogId: dialog.id,
|
||||
pointerId,
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startFrameX: dialog.placeholder.x,
|
||||
startFrameY: dialog.placeholder.y,
|
||||
startScale: viewportScale,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMinimapDragState({
|
||||
pointerId,
|
||||
pointer,
|
||||
viewport,
|
||||
minimapScale,
|
||||
}: {
|
||||
pointerId: number;
|
||||
pointer: CanvasPoint;
|
||||
viewport: CanvasViewport;
|
||||
minimapScale: number;
|
||||
}): Extract<DragState, { kind: 'minimap' }> {
|
||||
return {
|
||||
kind: 'minimap',
|
||||
pointerId,
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startViewport: { ...viewport },
|
||||
minimapScale,
|
||||
moved: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateMinimapDragMovement(
|
||||
dragState: Extract<DragState, { kind: 'minimap' }>,
|
||||
pointer: CanvasPoint,
|
||||
): Extract<DragState, { kind: 'minimap' }> {
|
||||
if (dragState.moved) {
|
||||
return dragState;
|
||||
}
|
||||
const deltaX = pointer.x - dragState.startClientX;
|
||||
const deltaY = pointer.y - dragState.startClientY;
|
||||
return Math.hypot(deltaX, deltaY) >= 2
|
||||
? {
|
||||
...dragState,
|
||||
moved: true,
|
||||
}
|
||||
: dragState;
|
||||
}
|
||||
@@ -16,6 +16,20 @@ import {
|
||||
moveViewportFromPan,
|
||||
selectLayersInsideMarquee,
|
||||
} from './ImageCanvasInteractionModel';
|
||||
import {
|
||||
createCanvasMarqueeState,
|
||||
createGenerationFrameDragState,
|
||||
createLayerDragStart,
|
||||
createMinimapDragState,
|
||||
createPanDragState,
|
||||
getCanvasPointFromPointer,
|
||||
getPointerButton,
|
||||
getPointerClient,
|
||||
getPointerId,
|
||||
updateGenerateDialogForLayerClick,
|
||||
updateGenerateDialogForLayerPointerDown,
|
||||
updateMinimapDragMovement,
|
||||
} from './ImageCanvasStageInteractionModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
@@ -27,11 +41,6 @@ import type {
|
||||
SnapGuide,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
|
||||
type CanvasPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
type UseImageCanvasStageInteractionsOptions = {
|
||||
canvasViewportRef: RefObject<HTMLDivElement | null>;
|
||||
activeTool: CanvasTool;
|
||||
@@ -70,51 +79,6 @@ type UseImageCanvasStageInteractionsOptions = {
|
||||
onCloseImageContextMenu: () => void;
|
||||
};
|
||||
|
||||
function getPointerButton(event: ReactPointerEvent<HTMLElement>) {
|
||||
const nativeEvent = event.nativeEvent as PointerEvent;
|
||||
const nativeButtons = Number(nativeEvent.buttons);
|
||||
if (Number.isFinite(nativeButtons) && (nativeButtons & 4) === 4) {
|
||||
return 1;
|
||||
}
|
||||
const syntheticButtons = Number(event.buttons);
|
||||
if (Number.isFinite(syntheticButtons) && (syntheticButtons & 4) === 4) {
|
||||
return 1;
|
||||
}
|
||||
const syntheticButton = Number(event.button);
|
||||
if (Number.isFinite(syntheticButton)) {
|
||||
return syntheticButton;
|
||||
}
|
||||
const nativeButton = Number(nativeEvent.button);
|
||||
if (Number.isFinite(nativeButton)) {
|
||||
return nativeButton;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getPointerClient(event: ReactPointerEvent<HTMLElement>): CanvasPoint {
|
||||
const nativeEvent = event.nativeEvent as PointerEvent;
|
||||
return {
|
||||
x: Number.isFinite(event.clientX)
|
||||
? event.clientX
|
||||
: Number.isFinite(nativeEvent.clientX)
|
||||
? nativeEvent.clientX
|
||||
: 0,
|
||||
y: Number.isFinite(event.clientY)
|
||||
? event.clientY
|
||||
: Number.isFinite(nativeEvent.clientY)
|
||||
? nativeEvent.clientY
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function getPointerId(event: ReactPointerEvent<HTMLElement>) {
|
||||
const nativeId = (event.nativeEvent as PointerEvent).pointerId;
|
||||
if (Number.isFinite(event.pointerId)) {
|
||||
return event.pointerId;
|
||||
}
|
||||
return Number.isFinite(nativeId) ? nativeId : -1;
|
||||
}
|
||||
|
||||
export function useImageCanvasStageInteractions({
|
||||
canvasViewportRef,
|
||||
activeTool,
|
||||
@@ -169,13 +133,11 @@ export function useImageCanvasStageInteractions({
|
||||
const pointer = getPointerClient(event);
|
||||
canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
|
||||
setIsPanning(true);
|
||||
dragStateRef.current = {
|
||||
kind: 'pan',
|
||||
dragStateRef.current = createPanDragState({
|
||||
pointerId: getPointerId(event),
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startViewport: viewport,
|
||||
};
|
||||
pointer,
|
||||
viewport,
|
||||
});
|
||||
},
|
||||
[canvasViewportRef, viewport],
|
||||
);
|
||||
@@ -199,16 +161,15 @@ export function useImageCanvasStageInteractions({
|
||||
) {
|
||||
event.preventDefault();
|
||||
const rect = canvasViewportRef.current?.getBoundingClientRect();
|
||||
const startX = event.clientX - (rect?.left ?? 0);
|
||||
const startY = event.clientY - (rect?.top ?? 0);
|
||||
const pointer = getPointerClient(event);
|
||||
canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
|
||||
setCanvasMarquee({
|
||||
pointerId: event.pointerId,
|
||||
startX,
|
||||
startY,
|
||||
currentX: startX,
|
||||
currentY: startY,
|
||||
});
|
||||
setCanvasMarquee(
|
||||
createCanvasMarqueeState({
|
||||
pointerId: getPointerId(event),
|
||||
pointer,
|
||||
rect,
|
||||
}),
|
||||
);
|
||||
clearCanvasFocus();
|
||||
return;
|
||||
}
|
||||
@@ -262,57 +223,21 @@ export function useImageCanvasStageInteractions({
|
||||
const pointer = getPointerClient(event);
|
||||
canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
|
||||
const isMultiSelectGesture = event.shiftKey || isShiftPressedRef.current;
|
||||
const nextSelectedIds = isMultiSelectGesture
|
||||
? selectedLayerIds.includes(layer.id)
|
||||
? selectedLayerIds.length > 1
|
||||
? selectedLayerIds.filter((layerId) => layerId !== layer.id)
|
||||
: [layer.id]
|
||||
: [...selectedLayerIds, layer.id]
|
||||
: [layer.id];
|
||||
setSelectedLayerId(layer.id);
|
||||
setSelectedLayerIds(nextSelectedIds);
|
||||
setGenerateDialog((currentDialog) => {
|
||||
if (
|
||||
currentDialog?.mode !== 'generate' &&
|
||||
currentDialog?.mode !== 'spec' &&
|
||||
currentDialog?.mode !== 'character' &&
|
||||
currentDialog?.mode !== 'icon'
|
||||
) {
|
||||
return currentDialog;
|
||||
}
|
||||
if (currentDialog.generatedLayerId === layer.id) {
|
||||
return {
|
||||
...currentDialog,
|
||||
composerOpen: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...currentDialog,
|
||||
composerOpen: false,
|
||||
};
|
||||
});
|
||||
const dragLayerIds = nextSelectedIds.includes(layer.id)
|
||||
? nextSelectedIds
|
||||
: [layer.id];
|
||||
const startLayers = layers
|
||||
.filter((currentLayer) => dragLayerIds.includes(currentLayer.id))
|
||||
.map((currentLayer) => ({
|
||||
id: currentLayer.id,
|
||||
x: currentLayer.x,
|
||||
y: currentLayer.y,
|
||||
}));
|
||||
dragStateRef.current = {
|
||||
kind: 'layer',
|
||||
const layerDragStart = createLayerDragStart({
|
||||
layer,
|
||||
layers,
|
||||
selectedLayerIds,
|
||||
isMultiSelectGesture,
|
||||
pointerId: getPointerId(event),
|
||||
layerId: layer.id,
|
||||
layerIds: dragLayerIds,
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startLayerX: layer.x,
|
||||
startLayerY: layer.y,
|
||||
startLayers,
|
||||
startScale: viewport.scale,
|
||||
};
|
||||
pointer,
|
||||
viewportScale: viewport.scale,
|
||||
});
|
||||
setSelectedLayerId(layer.id);
|
||||
setSelectedLayerIds(layerDragStart.selectedLayerIds);
|
||||
setGenerateDialog((currentDialog) =>
|
||||
updateGenerateDialogForLayerPointerDown(currentDialog, layer.id),
|
||||
);
|
||||
dragStateRef.current = layerDragStart.dragState;
|
||||
},
|
||||
[
|
||||
canvasViewportRef,
|
||||
@@ -358,15 +283,7 @@ export function useImageCanvasStageInteractions({
|
||||
setSelectedLayerId(layer.id);
|
||||
setSelectedLayerIds([layer.id]);
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'generate' ||
|
||||
currentDialog?.mode === 'spec' ||
|
||||
currentDialog?.mode === 'character' ||
|
||||
currentDialog?.mode === 'icon'
|
||||
? {
|
||||
...currentDialog,
|
||||
composerOpen: false,
|
||||
}
|
||||
: currentDialog,
|
||||
updateGenerateDialogForLayerClick(currentDialog),
|
||||
);
|
||||
onCloseImageContextMenu();
|
||||
},
|
||||
@@ -404,16 +321,12 @@ export function useImageCanvasStageInteractions({
|
||||
const pointer = getPointerClient(event);
|
||||
canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
|
||||
activateCanvasGenerationDialog(dialog);
|
||||
dragStateRef.current = {
|
||||
kind: 'generation-frame',
|
||||
dialogId: dialog.id,
|
||||
dragStateRef.current = createGenerationFrameDragState({
|
||||
dialog,
|
||||
pointerId: getPointerId(event),
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startFrameX: dialog.placeholder.x,
|
||||
startFrameY: dialog.placeholder.y,
|
||||
startScale: viewport.scale,
|
||||
};
|
||||
pointer,
|
||||
viewportScale: viewport.scale,
|
||||
});
|
||||
},
|
||||
[
|
||||
activateCanvasGenerationDialog,
|
||||
@@ -430,15 +343,12 @@ export function useImageCanvasStageInteractions({
|
||||
event.stopPropagation();
|
||||
const pointer = getPointerClient(event);
|
||||
canvasViewportRef.current?.setPointerCapture?.(event.pointerId);
|
||||
dragStateRef.current = {
|
||||
kind: 'minimap',
|
||||
dragStateRef.current = createMinimapDragState({
|
||||
pointerId: getPointerId(event),
|
||||
startClientX: pointer.x,
|
||||
startClientY: pointer.y,
|
||||
startViewport: { ...viewport },
|
||||
pointer,
|
||||
viewport,
|
||||
minimapScale,
|
||||
moved: false,
|
||||
};
|
||||
});
|
||||
},
|
||||
[canvasViewportRef, minimapScale, viewport],
|
||||
);
|
||||
@@ -448,20 +358,22 @@ export function useImageCanvasStageInteractions({
|
||||
if (canvasMarquee && canvasMarquee.pointerId === event.pointerId) {
|
||||
event.preventDefault();
|
||||
const rect = canvasViewportRef.current?.getBoundingClientRect();
|
||||
const currentX = event.clientX - (rect?.left ?? 0);
|
||||
const currentY = event.clientY - (rect?.top ?? 0);
|
||||
const currentPoint = getCanvasPointFromPointer({
|
||||
pointer: getPointerClient(event),
|
||||
rect,
|
||||
});
|
||||
setCanvasMarquee((currentMarquee) =>
|
||||
currentMarquee
|
||||
? {
|
||||
...currentMarquee,
|
||||
currentX,
|
||||
currentY,
|
||||
currentX: currentPoint.x,
|
||||
currentY: currentPoint.y,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const selectedIds = selectLayersInsideMarquee({
|
||||
marquee: canvasMarquee,
|
||||
currentPoint: { x: currentX, y: currentY },
|
||||
currentPoint,
|
||||
layers,
|
||||
viewport,
|
||||
});
|
||||
@@ -507,13 +419,12 @@ export function useImageCanvasStageInteractions({
|
||||
|
||||
if (dragState.kind === 'minimap') {
|
||||
const pointer = getPointerClient(event);
|
||||
const deltaX = pointer.x - dragState.startClientX;
|
||||
const deltaY = pointer.y - dragState.startClientY;
|
||||
if (!dragState.moved && Math.hypot(deltaX, deltaY) >= 2) {
|
||||
dragState.moved = true;
|
||||
const nextDragState = updateMinimapDragMovement(dragState, pointer);
|
||||
if (nextDragState !== dragState) {
|
||||
dragStateRef.current = nextDragState;
|
||||
}
|
||||
if (dragState.moved) {
|
||||
updateViewportFromMinimapDrag(dragState, pointer.x, pointer.y);
|
||||
if (nextDragState.moved) {
|
||||
updateViewportFromMinimapDrag(nextDragState, pointer.x, pointer.y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user