fix(canvas): resolve compare source by upstream original and preserve ctrl+a
This commit is contained in:
parent
eba188116a
commit
22493ce1a2
@ -474,8 +474,20 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// 如果正在输入则不触发快捷键(包括 input、textarea 和 contentEditable 元素)
|
||||
const target = e.target as HTMLElement;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
const inputType = String(target.type || "").toLowerCase();
|
||||
const isTextInput =
|
||||
inputType === "text" ||
|
||||
inputType === "search" ||
|
||||
inputType === "url" ||
|
||||
inputType === "email" ||
|
||||
inputType === "password" ||
|
||||
inputType === "number";
|
||||
if (isTextInput) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLTextAreaElement ||
|
||||
target.isContentEditable ||
|
||||
target.closest('[contenteditable="true"]')
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from "react";
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { ImagePlus, Loader2, AlertCircle, MoreVertical, MoreHorizontal, Scissors, Eraser, Sparkles, Expand, Maximize2, Move3D, Download, Trash2, RefreshCw, Pencil, MessageSquarePlus, Crosshair, Check, X, Type, StickyNote } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -28,6 +28,7 @@ import { imageGenerationService } from "@/services/canvas";
|
||||
import type { CanvasTextEditPayloadItem } from "@/services/canvas";
|
||||
import type { CanvasNode, ImageNodeData, ImageTextEditItem } from "@/types/canvas";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { inferOriginalImageUrl } from "@/lib/image-url";
|
||||
import { AngleEditDialog } from "@/components/angle-edit-dialog";
|
||||
import { SceneFusionDialog } from "@/components/scene-fusion-dialog";
|
||||
import { ImageAnnotator } from "@/components/image-annotator";
|
||||
@ -61,6 +62,29 @@ const normalizeTextEditItems = (items: ImageTextEditItem[] | undefined): ImageTe
|
||||
|
||||
const isHttpImageUrl = (url?: string) => !!url && /^https?:\/\//i.test(url);
|
||||
|
||||
const normalizeMediaUrl = (url: string): string => {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
|
||||
try {
|
||||
const parsed = new URL(raw);
|
||||
return `${parsed.origin}${parsed.pathname}`.replace(/\/+$/, "").toLowerCase();
|
||||
} catch {
|
||||
return raw
|
||||
.split("#")[0]
|
||||
.split("?")[0]
|
||||
.replace(/\/+$/, "")
|
||||
.toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeImageIdentityUrl = (url: string): string => {
|
||||
const raw = String(url || "").trim();
|
||||
if (!raw) return "";
|
||||
const canonical = inferOriginalImageUrl(raw);
|
||||
return normalizeMediaUrl(canonical || raw);
|
||||
};
|
||||
|
||||
const normalizeImageTitle = (raw?: string) => {
|
||||
const title = (raw || "").trim();
|
||||
return title || "";
|
||||
@ -270,6 +294,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
const addNode = useCanvasStore((state) => state.addNode);
|
||||
const addPendingChatAttachment = useCanvasStore((state) => state.addPendingChatAttachment);
|
||||
const setPickerMode = useCanvasStore((state) => state.setPickerMode);
|
||||
const allNodes = useCanvasStore((state) => state.nodes);
|
||||
|
||||
// 🆕 物品拾取相关状态
|
||||
const isPickerMode = useCanvasStore((state) => state.isPickerMode);
|
||||
@ -277,18 +302,6 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
const isPickerLoading = useCanvasStore((state) => state.isPickerLoading);
|
||||
const setPickerLoading = useCanvasStore((state) => state.setPickerLoading);
|
||||
const addPendingPickedObject = useCanvasStore((state) => state.addPendingPickedObject);
|
||||
const fallbackCompareSource = useCanvasStore((state) => {
|
||||
if (data.originalSrc) return "";
|
||||
if (!Array.isArray(node.connections) || node.connections.length === 0) return "";
|
||||
for (const sourceNodeId of node.connections) {
|
||||
const sourceNode = state.nodes.find((item) => item.id === sourceNodeId);
|
||||
if (sourceNode?.type !== "image") continue;
|
||||
const sourceUrl = String(((sourceNode.data as any) || {}).src || "").trim();
|
||||
if (sourceUrl) return sourceUrl;
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
// 当前节点是否处于拾取模式
|
||||
const isThisNodePicking = isPickerMode && pickerTargetNodeId === node.id;
|
||||
|
||||
@ -296,14 +309,54 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
|
||||
const [currentClick, setCurrentClick] = useState<{ x: number; y: number } | null>(null);
|
||||
const displayTitle = getImageNodeTitle(data, node.id);
|
||||
const isMultiSelected = isSelected && selectedNodeIds.length > 1;
|
||||
const metadataCompareSource = String(data.metadata?.compareSourceUrl || "").trim();
|
||||
const compareSource = String(
|
||||
data.originalSrc || metadataCompareSource || fallbackCompareSource || ""
|
||||
).trim();
|
||||
const hasCompareSource =
|
||||
Boolean(data.src) &&
|
||||
Boolean(compareSource) &&
|
||||
compareSource !== data.src;
|
||||
const compareSource = useMemo(() => {
|
||||
const targetIdentity = normalizeImageIdentityUrl(data.src || "");
|
||||
if (!targetIdentity) return "";
|
||||
|
||||
const candidates: string[] = [];
|
||||
const pushCandidate = (value: unknown) => {
|
||||
const raw = String(value || "").trim();
|
||||
if (!raw) return;
|
||||
candidates.push(raw);
|
||||
};
|
||||
|
||||
const visited = new Set<string>([node.id]);
|
||||
const queue = Array.isArray(node.connections) ? [...node.connections] : [];
|
||||
let safety = 0;
|
||||
|
||||
while (queue.length > 0 && safety < 120) {
|
||||
safety += 1;
|
||||
const sourceNodeId = queue.shift();
|
||||
if (!sourceNodeId || visited.has(sourceNodeId)) continue;
|
||||
visited.add(sourceNodeId);
|
||||
|
||||
const sourceNode = allNodes.find((item) => item.id === sourceNodeId);
|
||||
if (!sourceNode || sourceNode.type !== "image") continue;
|
||||
const sourceData = (sourceNode.data as any) || {};
|
||||
pushCandidate(sourceData.src);
|
||||
pushCandidate(sourceData.originalSrc);
|
||||
pushCandidate(sourceData.metadata?.compareSourceUrl);
|
||||
|
||||
if (Array.isArray(sourceNode.connections) && sourceNode.connections.length > 0) {
|
||||
queue.push(...sourceNode.connections);
|
||||
}
|
||||
}
|
||||
|
||||
pushCandidate(data.originalSrc);
|
||||
pushCandidate(data.metadata?.compareSourceUrl);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const identity = normalizeImageIdentityUrl(candidate);
|
||||
if (!identity) continue;
|
||||
if (identity !== targetIdentity) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}, [allNodes, data.metadata, data.originalSrc, data.src, node.connections, node.id]);
|
||||
|
||||
const hasCompareSource = Boolean(data.src) && Boolean(compareSource);
|
||||
|
||||
// 退出拾取模式时清空状态
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user