From e92e0b24f61c52e6e1027d3fedde2a099829ea1b Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Apr 2026 21:43:11 +0800 Subject: [PATCH] feat: improve 3d canvas controls and points visibility --- .../[id]/CanvasEditorLegacyPageClient.tsx | 25 +- app/canvas/[id]/model3d/page.tsx | 341 ++++++++++++++++-- components/canvas/CanvasWorkspace.tsx | 3 + components/canvas/EnhanceProWorkspace.tsx | 52 ++- components/canvas/RealtimeCanvasDialog.tsx | 25 +- components/canvas/nodes/ImageNode.tsx | 24 +- components/canvas/nodes/World3DNode.tsx | 2 + components/canvas/world3d/World3DViewer.tsx | 28 +- 8 files changed, 446 insertions(+), 54 deletions(-) diff --git a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx index bb846af0b..090bdd21b 100644 --- a/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx +++ b/app/canvas/[id]/CanvasEditorLegacyPageClient.tsx @@ -1577,6 +1577,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { }>; }; }>("/api/site-settings", fetcher); + const { data: sceneFusionConfigData } = useSWR<{ + enabled?: boolean; + }>("/api/scene-fusion/config", fetcher); // 生成配置 const [prompt, setPrompt] = useState(""); @@ -1616,7 +1619,8 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { const isWorkflowMaterialStepEnabled = workflowPageFeatureSettings.materialStepEnabled !== false; const isBatchProcessingEnabled = - workflowPageFeatureSettings.batchProcessingEnabled !== false; + workflowPageFeatureSettings.batchProcessingEnabled !== false && + sceneFusionConfigData?.enabled !== false; const sanitizedWorkflowSelections = useMemo( () => sanitizeAgentWorkflowSelections({ @@ -2772,6 +2776,12 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { } }, [isBatchProcessingEnabled, shouldOpenBatchProcessing]); + useEffect(() => { + if (!isBatchProcessingEnabled) { + setSceneFusionDialogOpen(false); + } + }, [isBatchProcessingEnabled]); + const workflowStorageKey = useMemo(() => `canvas-workflow-state:${id}`, [id]); const buildPersistedCanvasData = useCallback(() => { return { @@ -8643,6 +8653,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) { key={`workspace-${id}`} > - + {isBatchProcessingEnabled ? ( + + ) : null} {loginDialogElement} diff --git a/app/canvas/[id]/model3d/page.tsx b/app/canvas/[id]/model3d/page.tsx index 6f55da7af..4a94ebd85 100644 --- a/app/canvas/[id]/model3d/page.tsx +++ b/app/canvas/[id]/model3d/page.tsx @@ -99,6 +99,13 @@ import { resolveThreeDModelDisplayName, type PublicThreeDModelAliasConfig, } from "@/lib/model3d/model-alias-config"; +import { + defaultPointsSystemConfig, + get3DTaskCreditCost, + get3DWorldCreditCost, + normalizePointsSystemConfig, + type PointsSystemConfig, +} from "@/lib/points/config"; import { canvasService, type Model3DTaskStatus, @@ -536,6 +543,72 @@ function worldModeToTaskMode(mode: WorldGenerationMode): TaskMode { } } +function modelModeToTaskType(mode: GenerationMode) { + if (mode === "text") return "text_to_model"; + if (mode === "multiview") return "multiview_to_model"; + return "image_to_model"; +} + +function formatPointAmount(value: number) { + const normalized = Number.isFinite(value) ? Math.max(0, Math.round(value)) : 0; + return normalized.toLocaleString("en-US"); +} + +function formatFeatureCostLabel({ + cost, + isHighVip, + pointsEnabled, +}: { + cost: number; + isHighVip?: boolean; + pointsEnabled: boolean; +}) { + if (!pointsEnabled) return "不扣积分"; + if (isHighVip) return "VIP免费"; + if (cost <= 0) return "免费"; + return `${formatPointAmount(cost)} 积分`; +} + +function formatFeatureExtraCostLabel({ + cost, + isHighVip, + pointsEnabled, +}: { + cost: number; + isHighVip?: boolean; + pointsEnabled: boolean; +}) { + if (!pointsEnabled) return "不扣积分"; + if (isHighVip) return "VIP免费"; + if (cost <= 0) return "免费"; + return `+${formatPointAmount(cost)} 积分`; +} + +function CostPill({ + active, + className, + label, +}: { + active?: boolean; + className?: string; + label: string; +}) { + return ( + + {label} + + ); +} + export default function CanvasModel3DPage() { const router = useRouter(); const routeParams = useParams(); @@ -688,6 +761,8 @@ export default function CanvasModel3DPage() { data?: PublicThreeDModelAliasConfig; }>("/api/canvas/model-aliases", fetcher); const { data: membershipSummary } = useSWR<{ + pointsSystemConfig?: PointsSystemConfig; + isHighVip?: boolean; summary?: { currentCredits?: number; }; @@ -705,6 +780,152 @@ export default function CanvasModel3DPage() { const normalized = Number.isFinite(rawValue) ? Math.max(0, rawValue) : 0; return normalized.toLocaleString("en-US"); }, [membershipSummary?.summary?.currentCredits]); + const pointsSystemConfig = useMemo( + () => + normalizePointsSystemConfig( + membershipSummary?.pointsSystemConfig ?? defaultPointsSystemConfig + ), + [membershipSummary?.pointsSystemConfig] + ); + const pointsEnabled = pointsSystemConfig.enabled === true; + const isHighVip = membershipSummary?.isHighVip === true; + const selectedBatchImageCount = useMemo( + () => batchImageSlots.filter((slot) => Boolean(slot.file)).length, + [batchImageSlots] + ); + const currentModelTaskType = modelModeToTaskType(mode); + const currentModelUnitCost = useMemo( + () => + get3DTaskCreditCost(pointsSystemConfig, { + taskType: currentModelTaskType, + pbr, + quad, + smartLowPoly, + }), + [currentModelTaskType, pbr, pointsSystemConfig, quad, smartLowPoly] + ); + const currentModelEstimatedCost = + mode === "batch" && selectedBatchImageCount > 0 + ? currentModelUnitCost * selectedBatchImageCount + : currentModelUnitCost; + const modelFeatureCostLabel = formatFeatureCostLabel({ + cost: currentModelEstimatedCost, + isHighVip, + pointsEnabled, + }); + const modelFeatureCostHint = (() => { + if (!pointsEnabled) return "当前未开启积分扣除"; + if (isHighVip) return "VIP 用户本次不扣积分"; + if ( + mode === "batch" && + selectedBatchImageCount > 0 && + currentModelUnitCost > 0 + ) { + return `${selectedBatchImageCount} 张 × ${formatFeatureCostLabel({ + cost: currentModelUnitCost, + isHighVip, + pointsEnabled, + })}`; + } + if (mode === "batch") { + return `每张 ${formatFeatureCostLabel({ + cost: currentModelUnitCost, + isHighVip, + pointsEnabled, + })}`; + } + return "提交成功后扣除"; + })(); + const worldFeatureCost = useMemo( + () => get3DWorldCreditCost(pointsSystemConfig, worldModel), + [pointsSystemConfig, worldModel] + ); + const worldFeatureCostLabel = formatFeatureCostLabel({ + cost: worldFeatureCost, + isHighVip, + pointsEnabled, + }); + const worldFeatureCostHint = !pointsEnabled + ? "当前未开启积分扣除" + : isHighVip + ? "VIP 用户本次不扣积分" + : "按当前生成质量扣除"; + const getModelModeCostLabel = useCallback( + (targetMode: GenerationMode) => { + const unitCost = get3DTaskCreditCost(pointsSystemConfig, { + taskType: modelModeToTaskType(targetMode), + pbr, + quad, + smartLowPoly, + }); + if (targetMode === "batch") { + return `每张 ${formatFeatureCostLabel({ + cost: unitCost, + isHighVip, + pointsEnabled, + })}`; + } + return formatFeatureCostLabel({ + cost: unitCost, + isHighVip, + pointsEnabled, + }); + }, + [isHighVip, pbr, pointsEnabled, pointsSystemConfig, quad, smartLowPoly] + ); + const getWorldModeCostLabel = useCallback( + () => + formatFeatureCostLabel({ + cost: worldFeatureCost, + isHighVip, + pointsEnabled, + }), + [isHighVip, pointsEnabled, worldFeatureCost] + ); + const getWorldModelCostLabel = useCallback( + (targetWorldModel: string) => + formatFeatureCostLabel({ + cost: get3DWorldCreditCost(pointsSystemConfig, targetWorldModel), + isHighVip, + pointsEnabled, + }), + [isHighVip, pointsEnabled, pointsSystemConfig] + ); + const refineCostLabel = formatFeatureCostLabel({ + cost: get3DTaskCreditCost(pointsSystemConfig, { taskType: "refine_model" }), + isHighVip, + pointsEnabled, + }); + const textureCostLabel = formatFeatureCostLabel({ + cost: get3DTaskCreditCost(pointsSystemConfig, { taskType: "texture_model" }), + isHighVip, + pointsEnabled, + }); + const rigCostLabel = formatFeatureCostLabel({ + cost: get3DTaskCreditCost(pointsSystemConfig, { taskType: "animate_rig" }), + isHighVip, + pointsEnabled, + }); + const convertCostLabel = formatFeatureCostLabel({ + cost: get3DTaskCreditCost(pointsSystemConfig, { taskType: "convert_model" }), + isHighVip, + pointsEnabled, + }); + const pbrExtraCostLabel = formatFeatureExtraCostLabel({ + cost: pointsSystemConfig.creditCostPer3DModelPbr, + isHighVip, + pointsEnabled, + }); + const quadExtraCostLabel = formatFeatureExtraCostLabel({ + cost: pointsSystemConfig.creditCostPer3DModelQuad, + isHighVip, + pointsEnabled, + }); + const smartLowPolyExtraCostLabel = formatFeatureExtraCostLabel({ + cost: pointsSystemConfig.creditCostPer3DModelSmartLowPoly, + isHighVip, + pointsEnabled, + }); const userAvatarFallback = useMemo(() => { const fromName = String(user?.name || "").trim(); if (fromName) return fromName.charAt(0).toUpperCase(); @@ -2567,7 +2788,7 @@ export default function CanvasModel3DPage() {

@@ -2604,7 +2833,7 @@ export default function CanvasModel3DPage() { {activeTab === "create" && ( workspaceMode === "model" ? ( <> -
+
{[ { id: "text", label: "文生", icon: PenLine }, { id: "image", label: "图生", icon: ImagePlus }, @@ -2613,7 +2842,7 @@ export default function CanvasModel3DPage() { ].map((m) => ( ))}
+
+
+ 预计消耗 + + {modelFeatureCostLabel} + +
+
+ {modelFeatureCostHint} +
+
+