修复图片画布新增素材持久化
新增画布图层资源创建后的即时布局保存 补充素材库图片加入画布的持久化回归测试 更新图片画布回归验证记录
This commit is contained in:
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
Download,
|
||||
Pencil,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Check, ChevronLeft, Download, Pencil, X } from 'lucide-react';
|
||||
import JSZip from 'jszip';
|
||||
import {
|
||||
type CSSProperties,
|
||||
@@ -272,7 +266,10 @@ export function ImageCanvasEditorView() {
|
||||
const pendingProjectResourceLayersRef = useRef<
|
||||
Array<{
|
||||
layer: CanvasLayer;
|
||||
options: { onCreated?: (resourceId: string) => void };
|
||||
options: {
|
||||
onCreated?: (resourceId: string) => void;
|
||||
snapshotLayers?: CanvasLayer[];
|
||||
};
|
||||
}>
|
||||
>([]);
|
||||
const selectedLayerIdRef = useRef<string | null>(null);
|
||||
@@ -373,8 +370,9 @@ export function ImageCanvasEditorView() {
|
||||
useState(false);
|
||||
const [imageContextMenu, setImageContextMenu] =
|
||||
useState<ImageContextMenuState | null>(null);
|
||||
const [contextMenu, setContextMenu] =
|
||||
useState<CanvasContextMenuState | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(
|
||||
null,
|
||||
);
|
||||
const [canvasClipboard, setCanvasClipboard] =
|
||||
useState<CanvasClipboard | null>(null);
|
||||
const [historyVersion, setHistoryVersion] = useState(0);
|
||||
@@ -447,12 +445,11 @@ export function ImageCanvasEditorView() {
|
||||
: null,
|
||||
[activeCanvasGenerationDialog, layers],
|
||||
);
|
||||
const generationAnchor =
|
||||
activeCanvasGenerationDialog
|
||||
? (activeGenerationLayer ??
|
||||
activeCanvasGenerationDialog.placeholder ??
|
||||
null)
|
||||
: null;
|
||||
const generationAnchor = activeCanvasGenerationDialog
|
||||
? (activeGenerationLayer ??
|
||||
activeCanvasGenerationDialog.placeholder ??
|
||||
null)
|
||||
: null;
|
||||
const generationComposerStyle =
|
||||
activeCanvasGenerationDialog?.status !== 'generating' &&
|
||||
activeCanvasGenerationDialog?.composerOpen !== false &&
|
||||
@@ -630,8 +627,7 @@ export function ImageCanvasEditorView() {
|
||||
) => CanvasGenerationDialogState | null,
|
||||
) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
isCanvasGenerationDialog(currentDialog) &&
|
||||
currentDialog.id === dialogId
|
||||
isCanvasGenerationDialog(currentDialog) && currentDialog.id === dialogId
|
||||
? updater(currentDialog)
|
||||
: currentDialog,
|
||||
);
|
||||
@@ -707,7 +703,9 @@ export function ImageCanvasEditorView() {
|
||||
inactiveGenerateDialogs: inactiveGenerateDialogsRef.current.map(
|
||||
(dialog) => ({
|
||||
...dialog,
|
||||
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
|
||||
placeholder: dialog.placeholder
|
||||
? { ...dialog.placeholder }
|
||||
: undefined,
|
||||
}),
|
||||
),
|
||||
selectedLayerId: selectedLayerIdRef.current,
|
||||
@@ -733,7 +731,9 @@ export function ImageCanvasEditorView() {
|
||||
setInactiveGenerateDialogs(
|
||||
snapshot.inactiveGenerateDialogs.map((dialog) => ({
|
||||
...dialog,
|
||||
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
|
||||
placeholder: dialog.placeholder
|
||||
? { ...dialog.placeholder }
|
||||
: undefined,
|
||||
})),
|
||||
);
|
||||
setSelectedLayerId(snapshot.selectedLayerId);
|
||||
@@ -859,7 +859,10 @@ export function ImageCanvasEditorView() {
|
||||
const createProjectResourceForLayer = useCallback(
|
||||
(
|
||||
layer: CanvasLayer,
|
||||
options: { onCreated?: (resourceId: string) => void } = {},
|
||||
options: {
|
||||
onCreated?: (resourceId: string) => void;
|
||||
snapshotLayers?: CanvasLayer[];
|
||||
} = {},
|
||||
) => {
|
||||
const readyProjectId = projectIdRef.current;
|
||||
if (!readyProjectId) {
|
||||
@@ -882,16 +885,40 @@ export function ImageCanvasEditorView() {
|
||||
})
|
||||
.then((resource) => {
|
||||
options.onCreated?.(resource.resourceId);
|
||||
setLayers((currentLayers) =>
|
||||
currentLayers.map((currentLayer) =>
|
||||
currentLayer.id === layer.id
|
||||
? {
|
||||
...currentLayer,
|
||||
resourceId: resource.resourceId,
|
||||
}
|
||||
: currentLayer,
|
||||
),
|
||||
);
|
||||
const layerWithResourceId = {
|
||||
...layer,
|
||||
resourceId: resource.resourceId,
|
||||
};
|
||||
const currentLayers = layersRef.current;
|
||||
const nextLayers = currentLayers.some(
|
||||
(currentLayer) => currentLayer.id === layer.id,
|
||||
)
|
||||
? currentLayers.map((currentLayer) =>
|
||||
currentLayer.id === layer.id
|
||||
? layerWithResourceId
|
||||
: currentLayer,
|
||||
)
|
||||
: options.snapshotLayers?.some(
|
||||
(snapshotLayer) => snapshotLayer.id === layer.id,
|
||||
)
|
||||
? options.snapshotLayers.map((snapshotLayer) =>
|
||||
snapshotLayer.id === layer.id
|
||||
? layerWithResourceId
|
||||
: snapshotLayer,
|
||||
)
|
||||
: currentLayers;
|
||||
layersRef.current = nextLayers;
|
||||
setLayers(nextLayers);
|
||||
if (nextLayers.length) {
|
||||
void saveEditorProjectLayout(readyProjectId, {
|
||||
viewport: viewportRef.current,
|
||||
layers: nextLayers.map(serializeLayer),
|
||||
}).catch((error: unknown) => {
|
||||
if (isEditorAuthError(error)) {
|
||||
openEditorLoginModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (isEditorAuthError(error)) {
|
||||
@@ -1019,8 +1046,8 @@ export function ImageCanvasEditorView() {
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.code === 'KeyZ' &&
|
||||
@@ -1034,9 +1061,9 @@ export function ImageCanvasEditorView() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Shift') {
|
||||
isShiftPressedRef.current = true;
|
||||
}
|
||||
if (event.key === 'Shift') {
|
||||
isShiftPressedRef.current = true;
|
||||
}
|
||||
if (
|
||||
(event.key === 'Backspace' || event.key === 'Delete') &&
|
||||
!event.repeat &&
|
||||
@@ -1070,9 +1097,9 @@ export function ImageCanvasEditorView() {
|
||||
if (event.key === 'Escape') {
|
||||
setActiveSidebarPanel(null);
|
||||
setIsZoomMenuOpen(false);
|
||||
setIsBackgroundSettingsOpen(false);
|
||||
setIsSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
setIsBackgroundSettingsOpen(false);
|
||||
setIsSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
setContextMenu(null);
|
||||
setQuickEditPanel((currentPanel) =>
|
||||
currentPanel?.status === 'generating' ? currentPanel : null,
|
||||
@@ -1325,9 +1352,13 @@ export function ImageCanvasEditorView() {
|
||||
if (!canvasClipboard?.layers.length) {
|
||||
return;
|
||||
}
|
||||
const nextLayers = duplicateLayersToPoint(canvasClipboard.layers, canvasPoint, {
|
||||
renameCopies: canvasClipboard.mode !== 'cut',
|
||||
});
|
||||
const nextLayers = duplicateLayersToPoint(
|
||||
canvasClipboard.layers,
|
||||
canvasPoint,
|
||||
{
|
||||
renameCopies: canvasClipboard.mode !== 'cut',
|
||||
},
|
||||
);
|
||||
if (!nextLayers.length) {
|
||||
return;
|
||||
}
|
||||
@@ -1352,7 +1383,9 @@ export function ImageCanvasEditorView() {
|
||||
setCanvasClipboard(clipboard);
|
||||
if (options.cut) {
|
||||
captureCanvasHistory();
|
||||
setLayers((currentLayers) => removeCanvasLayers(currentLayers, targetIds));
|
||||
setLayers((currentLayers) =>
|
||||
removeCanvasLayers(currentLayers, targetIds),
|
||||
);
|
||||
selectSingleLayer(null);
|
||||
setMetadataLayer((currentLayer) =>
|
||||
currentLayer && targetIds.includes(currentLayer.id)
|
||||
@@ -1534,13 +1567,29 @@ export function ImageCanvasEditorView() {
|
||||
const listRect = listElement?.getBoundingClientRect();
|
||||
const headerRect = header?.getBoundingClientRect();
|
||||
setPinnedAssetMoveFolderId(
|
||||
listRect && headerRect &&
|
||||
listRect &&
|
||||
headerRect &&
|
||||
(headerRect.bottom < listRect.top || headerRect.top > listRect.bottom)
|
||||
? folderId
|
||||
: null,
|
||||
);
|
||||
};
|
||||
|
||||
const appendCanvasLayersWithResources = useCallback(
|
||||
(nextLayers: CanvasLayer[]) => {
|
||||
if (!nextLayers.length) {
|
||||
return;
|
||||
}
|
||||
const snapshotLayers = [...layersRef.current, ...nextLayers];
|
||||
layersRef.current = snapshotLayers;
|
||||
setLayers(snapshotLayers);
|
||||
nextLayers.forEach((layer) =>
|
||||
createProjectResourceForLayer(layer, { snapshotLayers }),
|
||||
);
|
||||
},
|
||||
[createProjectResourceForLayer],
|
||||
);
|
||||
|
||||
const addAssetLayer = (
|
||||
asset: EditorAsset,
|
||||
position?: { x: number; y: number },
|
||||
@@ -1557,10 +1606,9 @@ export function ImageCanvasEditorView() {
|
||||
},
|
||||
);
|
||||
captureCanvasHistory();
|
||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
||||
appendCanvasLayersWithResources([nextLayer]);
|
||||
selectSingleLayer(nextLayer.id);
|
||||
setHoveredLayerId(null);
|
||||
createProjectResourceForLayer(nextLayer);
|
||||
};
|
||||
addAssetLayerRef.current = addAssetLayer;
|
||||
|
||||
@@ -1609,7 +1657,10 @@ export function ImageCanvasEditorView() {
|
||||
|
||||
try {
|
||||
const blob = await readLayerImageBlob(layer);
|
||||
const extension = getImageExtensionFromTypeOrSrc(blob.type, layer.src);
|
||||
const extension = getImageExtensionFromTypeOrSrc(
|
||||
blob.type,
|
||||
layer.src,
|
||||
);
|
||||
const file = `images/${indexedFileName}.${extension}`;
|
||||
imageByKey.set(key, {
|
||||
key,
|
||||
@@ -1872,7 +1923,8 @@ export function ImageCanvasEditorView() {
|
||||
setSelectedLayerIds((currentIds) =>
|
||||
currentIds.filter((layerId) =>
|
||||
layers.every(
|
||||
(layer) => layer.id !== layerId || !isLayerLinkedToAsset(layer, asset),
|
||||
(layer) =>
|
||||
layer.id !== layerId || !isLayerLinkedToAsset(layer, asset),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1978,7 +2030,9 @@ export function ImageCanvasEditorView() {
|
||||
|
||||
const deleteSelectedAssets = () => {
|
||||
const ids = [...selectedAssetIds];
|
||||
const deletedAssets = assets.filter((asset) => selectedAssetIds.has(asset.id));
|
||||
const deletedAssets = assets.filter((asset) =>
|
||||
selectedAssetIds.has(asset.id),
|
||||
);
|
||||
setAssets((currentAssets) =>
|
||||
currentAssets.filter((asset) => !selectedAssetIds.has(asset.id)),
|
||||
);
|
||||
@@ -2363,7 +2417,7 @@ export function ImageCanvasEditorView() {
|
||||
};
|
||||
|
||||
if (options.addToCanvas) {
|
||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
||||
appendCanvasLayersWithResources([nextLayer]);
|
||||
}
|
||||
if (options.addToCanvas) {
|
||||
selectSingleLayer(nextLayer.id);
|
||||
@@ -2446,10 +2500,6 @@ export function ImageCanvasEditorView() {
|
||||
);
|
||||
});
|
||||
|
||||
if (options.addToCanvas) {
|
||||
createProjectResourceForLayer(nextLayer);
|
||||
}
|
||||
|
||||
if (imageSrc) {
|
||||
const uploadedImage = new Image();
|
||||
uploadedImage.onload = () => {
|
||||
@@ -2825,7 +2875,7 @@ export function ImageCanvasEditorView() {
|
||||
generationInputs: options.generationInputs,
|
||||
};
|
||||
|
||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
||||
appendCanvasLayersWithResources([nextLayer]);
|
||||
selectSingleLayer(nextLayer.id);
|
||||
setActiveSidebarPanel('layers');
|
||||
if (options.sourceLayer) {
|
||||
@@ -2848,7 +2898,6 @@ export function ImageCanvasEditorView() {
|
||||
if (options.sourceLayer) {
|
||||
fitLayers([options.sourceLayer, nextLayer]);
|
||||
}
|
||||
createProjectResourceForLayer(nextLayer);
|
||||
};
|
||||
|
||||
const addQuickEditResultLayer = (
|
||||
@@ -2859,7 +2908,8 @@ export function ImageCanvasEditorView() {
|
||||
layerCounterRef.current += 1;
|
||||
const generatedIndex = layerCounterRef.current;
|
||||
const originalWidth = generated.width || sourceLayer.originalWidth || 1024;
|
||||
const originalHeight = generated.height || sourceLayer.originalHeight || 1024;
|
||||
const originalHeight =
|
||||
generated.height || sourceLayer.originalHeight || 1024;
|
||||
const { width, height } = resolveLayerResolutionSize(
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
@@ -2894,13 +2944,12 @@ export function ImageCanvasEditorView() {
|
||||
generationInputs,
|
||||
};
|
||||
|
||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
||||
appendCanvasLayersWithResources([nextLayer]);
|
||||
selectSingleLayer(nextLayer.id);
|
||||
setActiveSidebarPanel('layers');
|
||||
setQuickEditPanel(null);
|
||||
setActiveTool('select');
|
||||
fitLayers([sourceLayer, nextLayer]);
|
||||
createProjectResourceForLayer(nextLayer);
|
||||
};
|
||||
|
||||
const addIconSpritesheetResultLayers = (
|
||||
@@ -2970,14 +3019,13 @@ export function ImageCanvasEditorView() {
|
||||
if (!nextLayers.length) {
|
||||
return;
|
||||
}
|
||||
setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
|
||||
appendCanvasLayersWithResources(nextLayers);
|
||||
selectSingleLayer(nextLayers[0]?.id ?? null);
|
||||
setActiveSidebarPanel('layers');
|
||||
if (dialogId) {
|
||||
removeCanvasGenerationDialogById(dialogId);
|
||||
}
|
||||
setActiveTool('select');
|
||||
nextLayers.forEach((layer) => createProjectResourceForLayer(layer));
|
||||
};
|
||||
|
||||
const updateIconDescription = (index: number, value: string) => {
|
||||
@@ -3101,8 +3149,9 @@ export function ImageCanvasEditorView() {
|
||||
});
|
||||
|
||||
try {
|
||||
const referenceImageSrc =
|
||||
await resolveEditorImageReferenceDataUrl(quickEditSourceLayer.src);
|
||||
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||
quickEditSourceLayer.src,
|
||||
);
|
||||
const generated = await generateEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
size: quickEditPanel.size,
|
||||
@@ -3388,9 +3437,7 @@ export function ImageCanvasEditorView() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleCanvasContextMenu = (
|
||||
event: ReactMouseEvent<HTMLDivElement>,
|
||||
) => {
|
||||
const handleCanvasContextMenu = (event: ReactMouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const position = resolveContextMenuPosition(
|
||||
@@ -4241,14 +4288,18 @@ export function ImageCanvasEditorView() {
|
||||
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
||||
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
||||
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
||||
setIsPickingCharacterSpecFromCanvas={setIsPickingCharacterSpecFromCanvas}
|
||||
setIsPickingCharacterSpecFromCanvas={
|
||||
setIsPickingCharacterSpecFromCanvas
|
||||
}
|
||||
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
||||
onOpenSpecDialog={openSpecDialog}
|
||||
onRequestUpload={(target) => {
|
||||
setUploadTarget(target);
|
||||
uploadInputRef.current?.click();
|
||||
}}
|
||||
onSubmitImageGeneration={(dialog) => void submitImageGeneration(dialog)}
|
||||
onSubmitImageGeneration={(dialog) =>
|
||||
void submitImageGeneration(dialog)
|
||||
}
|
||||
onSubmitIconSpritesheetGeneration={(dialog) =>
|
||||
void submitIconSpritesheetGeneration(dialog)
|
||||
}
|
||||
@@ -4268,9 +4319,10 @@ export function ImageCanvasEditorView() {
|
||||
onUpdateSpecFormValue={updateSpecFormValue}
|
||||
onUpdateIconDescription={updateIconDescription}
|
||||
onAddIconDescription={addIconDescription}
|
||||
onUpdateCharacterAnimationDuration={updateCharacterAnimationDuration}
|
||||
onUpdateCharacterAnimationDuration={
|
||||
updateCharacterAnimationDuration
|
||||
}
|
||||
/>
|
||||
|
||||
</ImageCanvasStageView>
|
||||
</div>
|
||||
|
||||
@@ -4311,7 +4363,11 @@ export function ImageCanvasEditorView() {
|
||||
key={`${reference.title}-${reference.label}-${reference.src}`}
|
||||
className="image-canvas-editor__metadata-reference-card"
|
||||
>
|
||||
<img src={reference.src} alt="" aria-hidden="true" />
|
||||
<img
|
||||
src={reference.src}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="image-canvas-editor__metadata-reference-copy">
|
||||
<span className="image-canvas-editor__metadata-input-title">
|
||||
{reference.title}
|
||||
|
||||
Reference in New Issue
Block a user