feat: refine generation workspace experience
This commit is contained in:
@@ -398,6 +398,8 @@ function CanvasPage({
|
||||
const canvasRef = useRef<HTMLElement>(null);
|
||||
const videoGenerationInFlightRef = useRef(new Set<string>());
|
||||
const canvasReferenceUploadPromisesRef = useRef(new Map<string, Promise<string | null>>());
|
||||
const canvasDragCounterRef = useRef(0);
|
||||
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
|
||||
const suppressNextPaneClickRef = useRef(false);
|
||||
const canvasAutoSaveTimerRef = useRef<number | null>(null);
|
||||
const canvasAutoSaveIdleHandleRef = useRef<number | null>(null);
|
||||
@@ -1335,7 +1337,7 @@ function CanvasPage({
|
||||
model: defaultVideoModel,
|
||||
aspectRatio: "16:9",
|
||||
resolution: getDefaultVideoQuality(defaultVideoModel),
|
||||
duration: "4",
|
||||
duration: "5",
|
||||
videoMode: "text2video",
|
||||
sourceTextNodeId: source.id,
|
||||
position: {
|
||||
@@ -1359,7 +1361,7 @@ function CanvasPage({
|
||||
model: defaultVideoModel,
|
||||
aspectRatio: "16:9",
|
||||
resolution: getDefaultVideoQuality(defaultVideoModel),
|
||||
duration: "4",
|
||||
duration: "5",
|
||||
videoMode: "text2video",
|
||||
sourceTextNodeId: "",
|
||||
position,
|
||||
@@ -1415,7 +1417,7 @@ function CanvasPage({
|
||||
imageUrl = "",
|
||||
fileName = "本地图片",
|
||||
position = { x: 0, y: 0 },
|
||||
options?: { title?: string; sourceImageNodeId?: string }
|
||||
options?: { title?: string; sourceImageNodeId?: string; sourceTextNodeId?: string }
|
||||
) => {
|
||||
const nodeNumber = imageNodeIdRef.current;
|
||||
imageNodeIdRef.current += 1;
|
||||
@@ -1429,6 +1431,7 @@ function CanvasPage({
|
||||
imageSize: getDefaultImageQuality(fallbackVisibleImageModel),
|
||||
fileName,
|
||||
sourceImageNodeId: options?.sourceImageNodeId,
|
||||
sourceTextNodeId: options?.sourceTextNodeId,
|
||||
position,
|
||||
size: createCanvasNodeSize("image"),
|
||||
};
|
||||
@@ -2034,6 +2037,120 @@ function CanvasPage({
|
||||
setNodeMenu(null);
|
||||
};
|
||||
|
||||
// ── Canvas drag-and-drop file upload ──────────────────────────────
|
||||
const handleCanvasDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
canvasDragCounterRef.current += 1;
|
||||
if (canvasDragCounterRef.current === 1) {
|
||||
setIsCanvasDragging(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCanvasDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
canvasDragCounterRef.current -= 1;
|
||||
if (canvasDragCounterRef.current <= 0) {
|
||||
canvasDragCounterRef.current = 0;
|
||||
setIsCanvasDragging(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleCanvasDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleCanvasDrop = useCallback(
|
||||
(e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
canvasDragCounterRef.current = 0;
|
||||
setIsCanvasDragging(false);
|
||||
const files = Array.from(e.dataTransfer.files).filter(
|
||||
(f) => f.type.startsWith("image/")
|
||||
);
|
||||
if (files.length === 0) return;
|
||||
|
||||
const rect = canvasRef.current?.getBoundingClientRect();
|
||||
if (!rect) return;
|
||||
const dropPosition = {
|
||||
x: (e.clientX - rect.left - canvasViewport.x) / canvasViewport.zoom,
|
||||
y: (e.clientY - rect.top - canvasViewport.y) / canvasViewport.zoom,
|
||||
};
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
for (const file of files) {
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
addImageNode(imageUrl, file.name, {
|
||||
x: dropPosition.x + offsetX,
|
||||
y: dropPosition.y + offsetY,
|
||||
});
|
||||
offsetX += 60;
|
||||
offsetY += 60;
|
||||
}
|
||||
setContextMenu(null);
|
||||
setNodeMenu(null);
|
||||
},
|
||||
[canvasViewport.x, canvasViewport.y, canvasViewport.zoom, addImageNode],
|
||||
);
|
||||
|
||||
// ── Text composer drag-and-drop ──────────────────────────────────
|
||||
const [textComposerDragNodeId, setTextComposerDragNodeId] = useState<string | null>(null);
|
||||
const textComposerDragCounterRef = useRef(0);
|
||||
|
||||
const handleTextComposerDragEnter = useCallback((_e: React.DragEvent, nodeId: string) => {
|
||||
_e.preventDefault();
|
||||
_e.stopPropagation();
|
||||
textComposerDragCounterRef.current += 1;
|
||||
if (textComposerDragCounterRef.current === 1) {
|
||||
setTextComposerDragNodeId(nodeId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTextComposerDragLeave = useCallback((_e: React.DragEvent) => {
|
||||
_e.preventDefault();
|
||||
_e.stopPropagation();
|
||||
textComposerDragCounterRef.current -= 1;
|
||||
if (textComposerDragCounterRef.current <= 0) {
|
||||
textComposerDragCounterRef.current = 0;
|
||||
setTextComposerDragNodeId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleTextComposerDragOver = useCallback((_e: React.DragEvent) => {
|
||||
_e.preventDefault();
|
||||
_e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleTextComposerDrop = useCallback(
|
||||
(e: React.DragEvent, sourceNode: CanvasTextNode) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
textComposerDragCounterRef.current = 0;
|
||||
setTextComposerDragNodeId(null);
|
||||
const files = Array.from(e.dataTransfer.files).filter(
|
||||
(f) => f.type.startsWith("image/")
|
||||
);
|
||||
if (files.length === 0) return;
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
for (const file of files) {
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
addImageNode(imageUrl, file.name, {
|
||||
x: sourceNode.position.x + sourceNode.size.width + 40 + offsetX,
|
||||
y: sourceNode.position.y + offsetY,
|
||||
}, { sourceTextNodeId: sourceNode.id });
|
||||
offsetX += 60;
|
||||
offsetY += 60;
|
||||
}
|
||||
},
|
||||
[addImageNode],
|
||||
);
|
||||
|
||||
const activeTextNode = textNodeMenu
|
||||
? textNodes.find((node) => node.id === textNodeMenu.nodeId) ?? null
|
||||
: null;
|
||||
@@ -3632,7 +3749,7 @@ function CanvasPage({
|
||||
<WorkspacePageShell title="画布" fullWidth className="canvas-page page-motion">
|
||||
<div className={`studio-tool-layout studio-tool-layout--no-top studio-tool-layout--no-left studio-tool-layout--no-right studio-tool-layout--canvas${(shouldShowEmptyProjectState || isWaitingForProjects) ? " studio-tool-layout--canvas-empty" : ""}`}>
|
||||
<section
|
||||
className={`studio-canvas${pendingLinkPort ? " is-linking" : ""}${(shouldShowEmptyProjectState || isWaitingForProjects) ? " is-empty-projects" : ""}`}
|
||||
className={`studio-canvas${pendingLinkPort ? " is-linking" : ""}${isCanvasDragging ? " is-canvas-dragging" : ""}${(shouldShowEmptyProjectState || isWaitingForProjects) ? " is-empty-projects" : ""}`}
|
||||
ref={canvasRef}
|
||||
onAuxClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasAuxClick}
|
||||
onContextMenu={(shouldShowEmptyProjectState || isWaitingForProjects) ? (event) => event.preventDefault() : handleCanvasContextMenu}
|
||||
@@ -3640,6 +3757,10 @@ function CanvasPage({
|
||||
onDoubleClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDoubleClick}
|
||||
onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove}
|
||||
onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel}
|
||||
onDragEnter={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragEnter}
|
||||
onDragOver={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragOver}
|
||||
onDragLeave={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragLeave}
|
||||
onDrop={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDrop}
|
||||
style={{
|
||||
"--canvas-bg-size": `${34 * canvasViewport.zoom}px`,
|
||||
"--canvas-bg-dot": `${1.35 * canvasViewport.zoom}px`,
|
||||
|
||||
@@ -140,7 +140,6 @@ export const videoRatioOptions: CanvasOption[] = [
|
||||
];
|
||||
|
||||
export const videoDurationOptions: CanvasOption[] = [
|
||||
{ value: "4", label: "4s" },
|
||||
{ value: "5", label: "5s" },
|
||||
{ value: "6", label: "6s" },
|
||||
{ value: "7", label: "7s" },
|
||||
|
||||
Reference in New Issue
Block a user