Fix canvas toolbar tone and placeholder scaling

This commit is contained in:
134 2026-06-24 17:24:18 +08:00
parent 1aad39b588
commit 41522cc481
3 changed files with 62 additions and 16 deletions

View File

@ -9251,6 +9251,7 @@ export function ImageNode({
onImageUpload={handleImageUpload}
onSelectFile={handleSelectFile}
status={data.status}
viewportScale={viewportZoom}
/>
)}
@ -9261,6 +9262,7 @@ export function ImageNode({
<ImageNodeProcessingOverlay
detailProgressLabel={detailProgressLabel || undefined}
generationProgress={generationProgress}
viewportScale={viewportZoom}
/>
)}
@ -9575,7 +9577,7 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
<div
className="fixed z-[9999] flex items-center gap-2 rounded-[24px] border border-slate-200 bg-white px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-slate-700 dark:bg-slate-900 dark:text-gray-100"
className="fixed z-[9999] flex items-center gap-2 rounded-[24px] border border-slate-200 bg-white px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100 dark:shadow-[0_16px_44px_rgba(0,0,0,0.45)]"
data-local-edit-control="true"
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
@ -9641,7 +9643,7 @@ export function ImageNode({
</Button>
</div>
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-zinc-800" />
<Button
className="mr-1 inline-flex h-10 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-xl bg-slate-950 px-3 font-semibold text-[13px] text-white hover:bg-slate-800 hover:text-white disabled:bg-slate-300 disabled:text-slate-500 dark:border dark:border-white/14 dark:bg-black dark:text-white dark:disabled:border-transparent dark:disabled:bg-slate-700 dark:disabled:text-slate-400 dark:hover:bg-zinc-900"
@ -9935,7 +9937,7 @@ export function ImageNode({
createPortal(
<TooltipProvider delayDuration={200}>
<div
className="fixed z-[9999] flex items-center gap-0.5 rounded-[24px] border border-slate-200 bg-white px-2 py-1.5 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-slate-700 dark:bg-slate-900 dark:text-gray-100"
className="fixed z-[9999] flex items-center gap-0.5 rounded-[24px] border border-slate-200 bg-white px-2 py-1.5 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100 dark:shadow-[0_16px_44px_rgba(0,0,0,0.45)]"
onClick={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
onPointerDown={(e) => e.stopPropagation()}
@ -9979,7 +9981,7 @@ export function ImageNode({
{imageAddToChatEnabled &&
(hasSelectedImagePrimaryToolbarActions ||
hasSelectedImageSecondaryToolbarActions) ? (
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-zinc-800" />
) : null}
{isSelectedImageActionInline("pickObject") ? (
@ -10352,7 +10354,7 @@ export function ImageNode({
{hasSelectedImagePrimaryToolbarActions &&
hasSelectedImageSecondaryToolbarActions ? (
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-zinc-800" />
) : null}
{hasSelectedImageMoreMenu ? (
@ -10374,7 +10376,7 @@ export function ImageNode({
</DropdownMenuTrigger>
<DropdownMenuContent
align="center"
className="bg-white opacity-100 backdrop-blur-none dark:bg-slate-900 dark:text-slate-100"
className="bg-white opacity-100 backdrop-blur-none dark:bg-zinc-950 dark:text-zinc-100"
side="bottom"
>
{isSelectedImageActionInMore("boxCutout") ? (
@ -10594,7 +10596,7 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
<div
className="fixed z-[9999] flex items-center rounded-[24px] border border-slate-200 bg-white px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-slate-700 dark:bg-slate-900 dark:text-gray-100"
className="fixed z-[9999] flex items-center rounded-[24px] border border-slate-200 bg-white px-2 py-2 text-gray-800 shadow-[0_14px_38px_rgba(15,23,42,0.16)] dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100 dark:shadow-[0_16px_44px_rgba(0,0,0,0.45)]"
onClick={(event) => event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
@ -10622,7 +10624,7 @@ export function ImageNode({
<span className="whitespace-nowrap">{boxCutoutHintText}</span>
</div>
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-slate-700" />
<div className="mx-1 h-6 w-px bg-slate-200 dark:bg-zinc-800" />
<Button
className="mr-1 inline-flex h-10 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-xl bg-slate-900 px-3 font-semibold text-[13px] text-white hover:bg-slate-800 hover:text-white disabled:bg-slate-300 disabled:text-slate-500 dark:bg-slate-100 dark:text-slate-900 dark:disabled:bg-slate-700 dark:disabled:text-slate-400 dark:hover:bg-slate-200"

View File

@ -11,6 +11,7 @@ interface ImageNodeEmptyStateProps {
onImageUpload: (event: React.ChangeEvent<HTMLInputElement>) => void;
onSelectFile: (event: React.MouseEvent<HTMLButtonElement>) => void;
status?: string;
viewportScale?: number;
}
export function ImageNodeDeferredLoadState() {
@ -27,9 +28,11 @@ export function ImageNodeDeferredLoadState() {
export function ImageNodeProcessingOverlay({
detailProgressLabel,
generationProgress,
viewportScale,
}: {
detailProgressLabel?: string;
generationProgress?: number | null;
viewportScale?: number;
}) {
return (
<div className="absolute inset-0 overflow-hidden rounded-[inherit] bg-white/96 backdrop-blur-[1px]">
@ -39,6 +42,7 @@ export function ImageNodeProcessingOverlay({
phase="processing"
progress={generationProgress}
title={detailProgressLabel || "正在处理图片"}
viewportScale={viewportScale}
/>
</div>
);
@ -51,6 +55,7 @@ export function ImageNodeEmptyState({
onImageUpload,
onSelectFile,
status,
viewportScale,
}: ImageNodeEmptyStateProps) {
if (status === "generating") {
return (
@ -60,6 +65,7 @@ export function ImageNodeEmptyState({
phase="processing"
progress={generationProgress}
title={detailProgressLabel || undefined}
viewportScale={viewportScale}
/>
);
}

View File

@ -12,6 +12,7 @@ interface GenerationDotPlaceholderProps {
progress?: number | null;
compact?: boolean;
showProgress?: boolean;
viewportScale?: number;
}
const PHASE_TITLES: Record<GenerationPhase, string> = {
@ -242,6 +243,17 @@ function clampProgress(value: number) {
return Math.max(0, Math.min(100, Math.round(value)));
}
function normalizeViewportScale(value: number | undefined) {
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
return 1;
}
return Math.max(0.1, Math.min(4, value));
}
function getParticlePointSize(compact: boolean, viewportScale: number) {
return (compact ? 76 : 94) * viewportScale;
}
function estimateProgress(phase: GenerationPhase, elapsedMs: number) {
if (phase === "completed") return 100;
if (phase === "failed") return 100;
@ -259,11 +271,25 @@ function estimateProgress(phase: GenerationPhase, elapsedMs: number) {
function ScatterParticleLayer({
compact,
opacity,
viewportScale,
}: {
compact: boolean;
opacity: number;
viewportScale: number;
}) {
const containerRef = useRef<HTMLDivElement | null>(null);
const pointSizeUniformRef = useRef<{ value: number } | null>(null);
const viewportScaleRef = useRef(viewportScale);
useEffect(() => {
viewportScaleRef.current = viewportScale;
if (pointSizeUniformRef.current) {
pointSizeUniformRef.current.value = getParticlePointSize(
compact,
viewportScale
);
}
}, [compact, viewportScale]);
useEffect(() => {
let cancelled = false;
@ -286,12 +312,12 @@ function ScatterParticleLayer({
antialias: true,
powerPreference: "high-performance",
});
renderer.setClearColor(0xffffff, 0);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2.5));
renderer.domElement.style.display = "block";
renderer.domElement.style.height = "100%";
renderer.domElement.style.width = "100%";
container.appendChild(renderer.domElement);
renderer.setClearColor(0xffffff, 0);
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2.5));
renderer.domElement.style.display = "block";
renderer.domElement.style.height = "100%";
renderer.domElement.style.width = "100%";
container.appendChild(renderer.domElement);
const particleCount = compact ? 7200 : 12800;
const positions = new Float32Array(particleCount * 3);
@ -323,11 +349,16 @@ function ScatterParticleLayer({
blending: THREE.NormalBlending,
uniforms: {
uTime: { value: 0 },
uPointSize: { value: compact ? 76 : 94 },
uPointSize: {
value: getParticlePointSize(compact, viewportScaleRef.current),
},
uEffectIntensity: { value: 1 },
uOpacity: { value: opacity },
},
});
pointSizeUniformRef.current = material.uniforms.uPointSize as {
value: number;
};
const points = new THREE.Points(geometry, material);
points.rotation.x = -0.06;
@ -371,6 +402,7 @@ function ScatterParticleLayer({
material.dispose();
renderer.dispose();
renderer.domElement.remove();
pointSizeUniformRef.current = null;
};
}
@ -391,8 +423,10 @@ export function GenerationDotPlaceholder({
progress,
compact = false,
showProgress = true,
viewportScale,
}: GenerationDotPlaceholderProps) {
const [elapsedMs, setElapsedMs] = useState(0);
const resolvedViewportScale = normalizeViewportScale(viewportScale);
useEffect(() => {
const startedAt = Date.now();
@ -489,7 +523,11 @@ export function GenerationDotPlaceholder({
</div>
) : null}
</div>
<ScatterParticleLayer compact={compact} opacity={dotOpacity} />
<ScatterParticleLayer
compact={compact}
opacity={dotOpacity}
viewportScale={resolvedViewportScale}
/>
<style jsx>{`
@keyframes generationCursorBlink {
0%,