完成素材无限画布最终审计与恢复安全收口
同步资源管理最新稳定的 manifest relay、Runner 重挂与 MCP 域名修复 统一网站与 Tauri 的共享画布 history、缩放恢复和宿主边界 移除生成账本中的临时上传凭证与 Provider 敏感字段 补齐事务快照发布前故障恢复和同幂等身份重放 更新最终 PRD、技术方案、decision-log 与 pitfalls 补齐前端、Tauri Rust、竞态、锁、恢复和原生壳门禁
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
|
||||
import type { CanvasTool } from '@genarrative/image-canvas-core';
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
|
||||
export type CanvasViewportProps = HTMLAttributes<HTMLDivElement> & {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
CANVAS_WORLD_SIZE,
|
||||
type CanvasViewport,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
|
||||
export function CanvasWorld({
|
||||
children,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { HTMLAttributes, KeyboardEventHandler, ReactNode } from 'react';
|
||||
|
||||
import type { CanvasLayer } from '@genarrative/image-canvas-core';
|
||||
import type { HTMLAttributes, KeyboardEventHandler, ReactNode } from 'react';
|
||||
|
||||
export function LayerRenderer({
|
||||
as = 'button',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PointerEventHandler, Ref } from 'react';
|
||||
|
||||
import type { StageMinimapModel } from '@genarrative/image-canvas-core';
|
||||
import type { PointerEventHandler, Ref } from 'react';
|
||||
|
||||
export function Minimap({
|
||||
className = '',
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import {
|
||||
canvasDisplayScaleToViewportScale,
|
||||
formatCanvasDisplayScalePercent,
|
||||
type CanvasViewport,
|
||||
formatCanvasDisplayScalePercent,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type CanvasZoomActions = {
|
||||
displayPercent: string;
|
||||
|
||||
@@ -4,8 +4,8 @@ export * from './CanvasWorld';
|
||||
export * from './LayerRenderer';
|
||||
export * from './Minimap';
|
||||
export * from './SelectionOverlay';
|
||||
export * from './ZoomControls';
|
||||
export * from './useCanvasHistory';
|
||||
export * from './useImageCanvasStageController';
|
||||
export * from './useImageCanvasStageInteractions';
|
||||
export * from './useImageCanvasViewportControls';
|
||||
export * from './ZoomControls';
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import type { CanvasLayer } from '@genarrative/image-canvas-core';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { createRef } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { CanvasLayer } from '@genarrative/image-canvas-core';
|
||||
|
||||
import { useImageCanvasViewportControls } from './useImageCanvasViewportControls';
|
||||
|
||||
function layer(id: string, x: number): CanvasLayer {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { type RefObject, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasHistoryAction,
|
||||
@@ -11,27 +9,28 @@ import type {
|
||||
GenerateDialogState,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
MAX_HISTORY_STEPS,
|
||||
canRestoreCanvasHistorySnapshotWithoutContentLoss,
|
||||
isProtectedCanvasHistoryAction,
|
||||
MAX_HISTORY_STEPS,
|
||||
mergeCanvasHistorySnapshotForRestore,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import { type RefObject, useCallback, useRef, useState } from 'react';
|
||||
|
||||
type CanvasHistoryRefs = {
|
||||
layersRef: RefObject<CanvasLayer[]>;
|
||||
viewportRef: RefObject<CanvasViewport>;
|
||||
generateDialogRef: RefObject<GenerateDialogState | null>;
|
||||
inactiveGenerateDialogsRef: RefObject<CanvasGenerationDialogState[]>;
|
||||
selectedLayerIdRef: RefObject<string | null>;
|
||||
generateDialogRef?: RefObject<GenerateDialogState | null>;
|
||||
inactiveGenerateDialogsRef?: RefObject<CanvasGenerationDialogState[]>;
|
||||
selectedLayerIdRef?: RefObject<string | null>;
|
||||
selectedLayerIdsRef: RefObject<string[]>;
|
||||
};
|
||||
|
||||
type CanvasHistorySetters = {
|
||||
setLayers: (layers: CanvasLayer[]) => void;
|
||||
setViewport: (viewport: CanvasViewport) => void;
|
||||
setGenerateDialog: (dialog: GenerateDialogState | null) => void;
|
||||
setInactiveGenerateDialogs: (dialogs: CanvasGenerationDialogState[]) => void;
|
||||
setSelectedLayerId: (layerId: string | null) => void;
|
||||
setGenerateDialog?: (dialog: GenerateDialogState | null) => void;
|
||||
setInactiveGenerateDialogs?: (dialogs: CanvasGenerationDialogState[]) => void;
|
||||
setSelectedLayerId?: (layerId: string | null) => void;
|
||||
setSelectedLayerIds: (layerIds: string[]) => void;
|
||||
};
|
||||
|
||||
@@ -69,11 +68,13 @@ export function useCanvasHistory({
|
||||
setters,
|
||||
resetters,
|
||||
effects,
|
||||
allowContentRemovalOnRestore = false,
|
||||
}: {
|
||||
refs: CanvasHistoryRefs;
|
||||
setters: CanvasHistorySetters;
|
||||
resetters: CanvasHistoryResetters;
|
||||
resetters?: CanvasHistoryResetters;
|
||||
effects?: CanvasHistoryEffects;
|
||||
allowContentRemovalOnRestore?: boolean;
|
||||
}) {
|
||||
const undoStackRef = useRef<CanvasHistoryEntry[]>([]);
|
||||
const redoStackRef = useRef<CanvasHistoryEntry[]>([]);
|
||||
@@ -84,13 +85,13 @@ export function useCanvasHistory({
|
||||
(): CanvasHistorySnapshot => ({
|
||||
layers: refs.layersRef.current.map((layer) => ({ ...layer })),
|
||||
viewport: { ...refs.viewportRef.current },
|
||||
generateDialog: refs.generateDialogRef.current
|
||||
generateDialog: refs.generateDialogRef?.current
|
||||
? cloneGenerateDialog(refs.generateDialogRef.current)
|
||||
: null,
|
||||
inactiveGenerateDialogs: refs.inactiveGenerateDialogsRef.current.map(
|
||||
cloneCanvasGenerationDialog,
|
||||
),
|
||||
selectedLayerId: refs.selectedLayerIdRef.current,
|
||||
inactiveGenerateDialogs: (
|
||||
refs.inactiveGenerateDialogsRef?.current ?? []
|
||||
).map(cloneCanvasGenerationDialog),
|
||||
selectedLayerId: refs.selectedLayerIdRef?.current ?? null,
|
||||
selectedLayerIds: [...refs.selectedLayerIdsRef.current],
|
||||
}),
|
||||
[refs],
|
||||
@@ -112,22 +113,28 @@ export function useCanvasHistory({
|
||||
? new Set(action.layerIds)
|
||||
: undefined
|
||||
: new Set(),
|
||||
layerSizeLayerIds:
|
||||
action?.type === 'resize-image'
|
||||
? action.layerIds?.length
|
||||
? new Set(action.layerIds)
|
||||
: new Set(snapshot.layers.map((layer) => layer.id))
|
||||
: new Set(),
|
||||
});
|
||||
setters.setLayers(mergedSnapshot.layers.map((layer) => ({ ...layer })));
|
||||
setters.setViewport({ ...mergedSnapshot.viewport });
|
||||
setters.setGenerateDialog(
|
||||
setters.setGenerateDialog?.(
|
||||
mergedSnapshot.generateDialog
|
||||
? cloneGenerateDialog(mergedSnapshot.generateDialog)
|
||||
: null,
|
||||
);
|
||||
setters.setInactiveGenerateDialogs(
|
||||
setters.setInactiveGenerateDialogs?.(
|
||||
mergedSnapshot.inactiveGenerateDialogs.map(cloneCanvasGenerationDialog),
|
||||
);
|
||||
setters.setSelectedLayerId(mergedSnapshot.selectedLayerId);
|
||||
setters.setSelectedLayerId?.(mergedSnapshot.selectedLayerId);
|
||||
setters.setSelectedLayerIds([...mergedSnapshot.selectedLayerIds]);
|
||||
resetters.setHoveredLayerId(null);
|
||||
resetters.setMetadataLayer(null);
|
||||
resetters.resetCanvasInteractionState();
|
||||
resetters?.setHoveredLayerId(null);
|
||||
resetters?.setMetadataLayer(null);
|
||||
resetters?.resetCanvasInteractionState();
|
||||
|
||||
if (action?.type === 'change-asset-kind') {
|
||||
const currentLayerById = new Map(
|
||||
@@ -215,11 +222,12 @@ export function useCanvasHistory({
|
||||
}
|
||||
const currentSnapshot = getCanvasHistorySnapshot();
|
||||
if (
|
||||
isProtectedCanvasHistoryAction(previousEntry.action) ||
|
||||
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
|
||||
current: currentSnapshot,
|
||||
target: previousEntry.snapshot,
|
||||
})
|
||||
!allowContentRemovalOnRestore &&
|
||||
(isProtectedCanvasHistoryAction(previousEntry.action) ||
|
||||
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
|
||||
current: currentSnapshot,
|
||||
target: previousEntry.snapshot,
|
||||
}))
|
||||
) {
|
||||
return {
|
||||
status: 'blocked',
|
||||
@@ -244,7 +252,11 @@ export function useCanvasHistory({
|
||||
);
|
||||
setHistoryVersion((version) => version + 1);
|
||||
return { status: 'success', action: previousEntry.action };
|
||||
}, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);
|
||||
}, [
|
||||
allowContentRemovalOnRestore,
|
||||
getCanvasHistorySnapshot,
|
||||
restoreCanvasHistorySnapshot,
|
||||
]);
|
||||
|
||||
const redoCanvasChange = useCallback((): CanvasHistoryApplyResult => {
|
||||
const nextEntry = redoStackRef.current.at(-1);
|
||||
@@ -253,6 +265,7 @@ export function useCanvasHistory({
|
||||
}
|
||||
const currentSnapshot = getCanvasHistorySnapshot();
|
||||
if (
|
||||
!allowContentRemovalOnRestore &&
|
||||
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
|
||||
current: currentSnapshot,
|
||||
target: nextEntry.snapshot,
|
||||
@@ -281,7 +294,17 @@ export function useCanvasHistory({
|
||||
);
|
||||
setHistoryVersion((version) => version + 1);
|
||||
return { status: 'success', action: nextEntry.action };
|
||||
}, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);
|
||||
}, [
|
||||
allowContentRemovalOnRestore,
|
||||
getCanvasHistorySnapshot,
|
||||
restoreCanvasHistorySnapshot,
|
||||
]);
|
||||
|
||||
const resetCanvasHistory = useCallback(() => {
|
||||
undoStackRef.current = [];
|
||||
redoStackRef.current = [];
|
||||
setHistoryVersion((version) => version + 1);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
canUndo: undoStackRef.current.length > 0,
|
||||
@@ -294,5 +317,6 @@ export function useCanvasHistory({
|
||||
discardCanvasHistoryEntriesById,
|
||||
undoCanvasChange,
|
||||
redoCanvasChange,
|
||||
resetCanvasHistory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
import type {
|
||||
CanvasContextMenuState,
|
||||
CanvasGenerationDialogState,
|
||||
@@ -13,6 +5,13 @@ import type {
|
||||
CanvasViewport,
|
||||
ImageContextMenuState,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
type Dispatch,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
|
||||
function getBrowserViewportBounds() {
|
||||
return typeof window === 'undefined'
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasHistoryAction,
|
||||
@@ -44,6 +32,17 @@ import {
|
||||
updateGenerateDialogForLayerPointerDown,
|
||||
updateMinimapDragMovement,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
type Dispatch,
|
||||
type MouseEvent as ReactMouseEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
const CLICK_COLLAPSE_MOVEMENT_THRESHOLD_PX = 3;
|
||||
|
||||
|
||||
@@ -1,22 +1,11 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type {
|
||||
CanvasLayer,
|
||||
CanvasViewport,
|
||||
DragState,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
DEFAULT_CANVAS_SIZE,
|
||||
createMinimapModel,
|
||||
DEFAULT_CANVAS_SIZE,
|
||||
fitViewportToLayers,
|
||||
getCanvasDropPoint as resolveCanvasDropPoint,
|
||||
getCanvasPointFromClient as resolveCanvasPointFromClient,
|
||||
@@ -27,6 +16,16 @@ import {
|
||||
scaleViewportFromScreenPoint,
|
||||
zoomViewportFromWheel,
|
||||
} from '@genarrative/image-canvas-core';
|
||||
import {
|
||||
type Dispatch,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
export const DEFAULT_IMAGE_CANVAS_VIEWPORT: CanvasViewport = {
|
||||
x: -260,
|
||||
|
||||
Reference in New Issue
Block a user