"use client"; import { useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react"; import { BoxSelect, Brush, Check, ImageIcon, Loader2, MapPin, Redo2, Trash2, Undo2, Upload, X } from "lucide-react"; import clsx from "clsx"; import { materialPreviewUrl } from "@/lib/client/asset-urls"; import type { PromptMaterial } from "@/lib/prompt/assembler"; import { seedreamAnnotationToken, type SeedreamAnnotation, type SeedreamCreationMode, type SeedreamPoint } from "@/lib/seedream/creation"; type AnnotationTool = "box" | "point" | "brush"; type BoxDraft = { start: SeedreamPoint; current: SeedreamPoint; }; type StrokeDraft = { points: SeedreamPoint[]; }; const maximumInteractiveSources = 10; export function SeedreamWorkspace({ mode, sources, selectedSources, source, uploading, annotations, onAnnotationsChange, onSelectSource, onToggleSource, onUpload, onInsertAnnotation }: { mode: Exclude; sources: PromptMaterial[]; selectedSources: PromptMaterial[]; source?: PromptMaterial; uploading: boolean; annotations: SeedreamAnnotation[]; onAnnotationsChange: (annotations: SeedreamAnnotation[]) => void; onSelectSource: (material: PromptMaterial) => void; onToggleSource: (material: PromptMaterial) => void; onUpload: (files: FileList | null) => void; onInsertAnnotation: (token: string) => void; }) { const [tool, setTool] = useState("box"); const [boxDraft, setBoxDraft] = useState(null); const [strokeDraft, setStrokeDraft] = useState(null); const [selectedAnnotationId, setSelectedAnnotationId] = useState(null); const [redoStack, setRedoStack] = useState([]); const canvasRef = useRef(null); const sourceKey = materialKey(source); const selectedSourceKeys = useMemo(() => new Set(selectedSources.map(materialKey)), [selectedSources]); const sourceIndexByKey = useMemo(() => new Map(selectedSources.map((material, index) => [materialKey(material), index + 1])), [selectedSources]); const activeAnnotations = useMemo( () => annotations.filter((annotation) => !annotation.sourceKey || annotation.sourceKey === sourceKey), [annotations, sourceKey] ); const selectedAnnotation = activeAnnotations.find((annotation) => annotation.id === selectedAnnotationId); const nextIndex = useMemo(() => annotations.reduce((maximum, annotation) => Math.max(maximum, annotation.index), 0) + 1, [annotations]); const selectionLimitReached = selectedSources.length >= maximumInteractiveSources; useEffect(() => { setBoxDraft(null); setStrokeDraft(null); setSelectedAnnotationId(null); setRedoStack([]); }, [sourceKey, mode]); useEffect(() => { if (mode === "layers" && tool === "point") setTool("box"); }, [mode, tool]); function addAnnotation(annotation: SeedreamAnnotation) { onAnnotationsChange([...annotations, { ...annotation, sourceKey }]); setSelectedAnnotationId(annotation.id); setRedoStack([]); } function undo() { const removed = activeAnnotations.at(-1); if (!removed) return; onAnnotationsChange(annotations.filter((annotation) => annotation.id !== removed.id)); setRedoStack((items) => [...items, removed]); if (selectedAnnotationId === removed.id) setSelectedAnnotationId(null); } function redo() { const restored = redoStack.at(-1); if (!restored) return; onAnnotationsChange([...annotations, restored]); setRedoStack((items) => items.slice(0, -1)); setSelectedAnnotationId(restored.id); } function removeSelected() { if (!selectedAnnotation) return; onAnnotationsChange(annotations.filter((annotation) => annotation.id !== selectedAnnotation.id)); setRedoStack((items) => [...items, selectedAnnotation]); setSelectedAnnotationId(null); } function handlePointerDown(event: ReactPointerEvent) { if (!source) return; const point = pointerCoordinate(event, canvasRef.current); if (!point) return; if (tool === "point" && mode === "interactive") { addAnnotation({ id: annotationID(), index: nextIndex, kind: "point", point }); return; } event.currentTarget.setPointerCapture(event.pointerId); setSelectedAnnotationId(null); if (tool === "brush") { setStrokeDraft({ points: [point] }); return; } setBoxDraft({ start: point, current: point }); } function handlePointerMove(event: ReactPointerEvent) { const point = pointerCoordinate(event, canvasRef.current); if (!point) return; if (boxDraft) { setBoxDraft((current) => current ? { ...current, current: point } : null); return; } if (strokeDraft) { setStrokeDraft((current) => { if (!current) return null; const previous = current.points.at(-1); if (previous && pointDistance(previous, point) < 3) return current; return { points: [...current.points, point] }; }); } } function handlePointerUp(event: ReactPointerEvent) { if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId); if (strokeDraft) { const points = strokeDraft.points.length === 1 ? [strokeDraft.points[0], { x: Math.min(1000, strokeDraft.points[0].x + 1), y: Math.min(1000, strokeDraft.points[0].y + 1) }] : strokeDraft.points; setStrokeDraft(null); addAnnotation({ id: annotationID(), index: nextIndex, kind: "stroke", points }); return; } if (!boxDraft) return; const box = normalizedBox(boxDraft.start, boxDraft.current); setBoxDraft(null); if (box.right - box.left < 8 || box.bottom - box.top < 8) return; addAnnotation({ id: annotationID(), index: nextIndex, kind: "box", box }); } function cancelDraft() { setBoxDraft(null); setStrokeDraft(null); } const currentImageIndex = sourceIndexByKey.get(sourceKey) || 1; return (
{mode === "interactive" ? "精准编辑画布" : "指定拆分区域"} {mode === "interactive" ? "可在最多 10 张图片间切换,使用坐标或蓝色涂鸦描述编辑位置" : "可留空自动全拆,也可框选或涂鸦指定希望拆出的元素"}
Seedream 5.0 Pro
{!source ? (
{sources.length ? "选择参与生成的图片" : "先上传一张图片"} {mode === "interactive" ? "可以选择多张图片进行跨图定位编辑" : "模板效果预览图不会自动参与拆分"}
) : (
图{currentImageIndex} {mode === "interactive" ? ( ) : null}
{source.name {activeAnnotations.map((annotation) => ( setSelectedAnnotationId(annotation.id)} /> ))} {boxDraft ? : null} {strokeDraft ? : null}
{mode === "interactive" ? "蓝色涂鸦会在提交时自动合成到对应输入图;框选和点选会转换成官方坐标标签。" : "蓝色涂鸦会合成到待拆图片;框选会转换为 坐标,不会修改原始素材。"}
{activeAnnotations.length ? activeAnnotations.map((annotation) => ( )) : 尚未标记。可以框选、{mode === "interactive" ? "点选或" : "或"}使用蓝色涂鸦。}
)} {sources.length ? (
{mode === "interactive" ? `输入图片 ${selectedSources.length}/${maximumInteractiveSources}` : "选择待拆图片"} {mode === "interactive" ? 图片编号按选中顺序生成,用于“图1 / 图2”坐标提示词 : null}
{sources.map((material) => { const key = materialKey(material); const included = selectedSourceKeys.has(key); const imageIndex = sourceIndexByKey.get(key); return (
{mode === "interactive" ? ( ) : null}
); })}
) : null}
); } function AnnotationShape({ annotation, selected, onSelect }: { annotation: SeedreamAnnotation; selected: boolean; onSelect: () => void }) { if (annotation.kind === "point" && annotation.point) { return ( { event.stopPropagation(); onSelect(); }}> ); } if (annotation.kind === "box" && annotation.box) { const width = annotation.box.right - annotation.box.left; const height = annotation.box.bottom - annotation.box.top; return ( { event.stopPropagation(); onSelect(); }}> ); } if (annotation.kind === "stroke" && annotation.points?.length) { const first = annotation.points[0]; return ( { event.stopPropagation(); onSelect(); }}> ); } return null; } function AnnotationLabel({ x, y, index }: { x: number; y: number; index: number }) { const safeX = Math.min(955, Math.max(12, x)); const safeY = Math.min(955, Math.max(34, y)); return ( {index} ); } function DraftBox({ draft }: { draft: BoxDraft }) { const box = normalizedBox(draft.start, draft.current); return ; } function DraftStroke({ draft }: { draft: StrokeDraft }) { return ; } function annotationIcon(kind: SeedreamAnnotation["kind"]) { if (kind === "point") return ; if (kind === "stroke") return ; return ; } function normalizedBox(start: SeedreamPoint, end: SeedreamPoint) { return { left: Math.min(start.x, end.x), top: Math.min(start.y, end.y), right: Math.max(start.x, end.x), bottom: Math.max(start.y, end.y) }; } function pointerCoordinate(event: ReactPointerEvent, canvas: SVGSVGElement | null): SeedreamPoint | null { if (!canvas) return null; const bounds = canvas.getBoundingClientRect(); if (!bounds.width || !bounds.height) return null; return { x: clamp((event.clientX - bounds.left) / bounds.width * 1000), y: clamp((event.clientY - bounds.top) / bounds.height * 1000) }; } function pointsPath(points: SeedreamPoint[]) { return points.map((point, index) => `${index === 0 ? "M" : "L"}${point.x} ${point.y}`).join(" "); } function pointDistance(a: SeedreamPoint, b: SeedreamPoint) { return Math.hypot(a.x - b.x, a.y - b.y); } function clamp(value: number) { return Math.max(0, Math.min(1000, Math.round(value))); } function materialKey(material?: PromptMaterial) { return material ? material.id || material.url : ""; } function annotationID() { return `annotation-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; }