diff --git a/app/api/canvas/3d/model-proxy/route.ts b/app/api/canvas/3d/model-proxy/route.ts index f7be306e6..3d2b93d99 100644 --- a/app/api/canvas/3d/model-proxy/route.ts +++ b/app/api/canvas/3d/model-proxy/route.ts @@ -14,6 +14,11 @@ const ALLOWED_MODEL_HOST_DOMAINS = [ "worldlabs.ai", "s3.1080p.hk", "img.yydd.ai", + "yybb.top", + "qiniucs.com", + "qiniudn.com", + "clouddn.com", + "qbox.me", "blob.vercel-storage.com", "aliyuncs.com", "amazonaws.com", diff --git a/components/canvas/model3d/Model3DViewer.tsx b/components/canvas/model3d/Model3DViewer.tsx index a58791365..02e4f7593 100644 --- a/components/canvas/model3d/Model3DViewer.tsx +++ b/components/canvas/model3d/Model3DViewer.tsx @@ -300,6 +300,7 @@ function configureMaterialTextures( function cloneSceneWithIndependentMaterials(scene: Object3D) { const clonedScene = scene.clone(true); + const clonedTextures = new Map(); clonedScene.traverse((node) => { if (!isMesh(node)) return; @@ -309,6 +310,24 @@ function cloneSceneWithIndependentMaterials(scene: Object3D) { : [node.material]; const clonedMaterials = sourceMaterials.map((material) => { const clonedMaterial = material.clone(); + const sourceMaterialRecord = material as any; + const clonedMaterialRecord = clonedMaterial as any; + + for (const key of MATERIAL_TEXTURE_KEYS) { + const sourceTexture = sourceMaterialRecord[key]; + if (!sourceTexture?.isTexture) continue; + + const typedSourceTexture = sourceTexture as THREE.Texture; + let clonedTexture = clonedTextures.get(typedSourceTexture); + if (!clonedTexture) { + const newTexture = typedSourceTexture.clone(); + newTexture.needsUpdate = true; + clonedTextures.set(typedSourceTexture, newTexture); + clonedTexture = newTexture; + } + clonedMaterialRecord[key] = clonedTexture; + } + configureMaterialTextures(clonedMaterial, node.geometry); return clonedMaterial; }); @@ -407,12 +426,20 @@ function ModelScene({ useEffect(() => { return () => { + const disposedTextures = new Set(); scene.traverse((node) => { if (!isMesh(node)) return; const materials = Array.isArray(node.material) ? node.material : [node.material]; for (const material of materials) { + const materialRecord = material as any; + for (const key of MATERIAL_TEXTURE_KEYS) { + const texture = materialRecord[key]; + if (!texture?.isTexture || disposedTextures.has(texture)) continue; + disposedTextures.add(texture); + texture.dispose(); + } material.dispose(); } }); @@ -437,7 +464,7 @@ function ModelScene({ return ( - + ); } diff --git a/lib/canvas/three-asset-url.ts b/lib/canvas/three-asset-url.ts index cd8788a2d..c81a9b366 100644 --- a/lib/canvas/three-asset-url.ts +++ b/lib/canvas/three-asset-url.ts @@ -7,6 +7,11 @@ const PROXY_HOST_KEYWORDS = [ "marble", "s3.1080p.hk", "img.yydd.ai", + "yybb.top", + "qiniucs.com", + "qiniudn.com", + "clouddn.com", + "qbox.me", "blob.vercel-storage.com", "aliyuncs.com", "amazonaws.com", diff --git a/tests/routes/model3d-style-pro-regressions.test.ts b/tests/routes/model3d-style-pro-regressions.test.ts index 56624c323..9df978016 100644 --- a/tests/routes/model3d-style-pro-regressions.test.ts +++ b/tests/routes/model3d-style-pro-regressions.test.ts @@ -28,11 +28,32 @@ test.describe("3D workspace and Style Pro regressions", () => { expect(viewer).toContain("cloneSceneWithIndependentMaterials"); expect(viewer).toContain("const clonedMaterial = material.clone()"); + expect(viewer).toContain("typedSourceTexture.clone()"); + expect(viewer).toContain("clonedMaterialRecord[key] = clonedTexture"); + expect(viewer).toContain("disposedTextures.add(texture)"); + expect(viewer).toContain("texture.dispose()"); + expect(viewer).toContain(" { + const assetUrl = readSource("lib/canvas/three-asset-url.ts"); + const proxy = readSource("app/api/canvas/3d/model-proxy/route.ts"); + + for (const host of [ + "yybb.top", + "qiniucs.com", + "qiniudn.com", + "clouddn.com", + "qbox.me", + ]) { + expect(assetUrl).toContain(`\"${host}\"`); + expect(proxy).toContain(`\"${host}\"`); + } + }); + test("uses the same hidden-model access rule in Style Pro as canvas", () => { const route = readSource("app/api/style-pro/model-options/route.ts"); const canvasModelsRoute = readSource("app/(chat)/api/models/route.ts");