fix(model3d): preserve textures with qiniu storage

This commit is contained in:
134 2026-08-10 22:16:05 +08:00
parent cd1639b4d7
commit a3e3938f7b
4 changed files with 59 additions and 1 deletions

View File

@ -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",

View File

@ -300,6 +300,7 @@ function configureMaterialTextures(
function cloneSceneWithIndependentMaterials(scene: Object3D) {
const clonedScene = scene.clone(true);
const clonedTextures = new Map<THREE.Texture, THREE.Texture>();
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<THREE.Texture>();
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 (
<group rotation={modelRotation || [0, 0, 0]}>
<primitive object={scene} scale={1} />
<primitive dispose={null} object={scene} scale={1} />
</group>
);
}

View File

@ -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",

View File

@ -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("<primitive dispose={null}");
expect(viewer).toContain(
"configureMaterialTextures(clonedMaterial, node.geometry)"
);
});
test("loads Qiniu-hosted GLB assets through the authenticated same-origin proxy", () => {
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");