Skip to content

Cache glTF model scenes#909

Open
Finesssee wants to merge 2 commits into
tscircuit:mainfrom
Finesssee:cache-gltf-model-scenes
Open

Cache glTF model scenes#909
Finesssee wants to merge 2 commits into
tscircuit:mainfrom
Finesssee:cache-gltf-model-scenes

Conversation

@Finesssee
Copy link
Copy Markdown

Summary

  • cache loaded GLTF/GLB scenes by normalized model URL so duplicate model instances share the same in-flight/completed load
  • strip only cachebust_origin from cache keys while preserving meaningful query params
  • clone cached scenes with independent material instances so translucency, env-map, and hover mutations do not leak between components
  • switch GltfModel to use the shared cache helper

Tests

  • bun test tests/gltf-model-cache.test.ts
  • bun run build

/claim #93

@vercel
Copy link
Copy Markdown

vercel Bot commented May 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
3d-viewer Ready Ready Preview, Comment May 21, 2026 12:46am

Request Review

Comment thread tests/gltf-model-cache.test.ts Outdated
Comment on lines +8 to +33
test("normalizes cachebust_origin without dropping meaningful query params", () => {
expect(
normalizeGltfModelCacheKey(
"https://assets.tscircuit.com/chip.glb?foo=1&cachebust_origin=abc&bar=2",
),
).toBe("https://assets.tscircuit.com/chip.glb?foo=1&bar=2")

expect(
normalizeGltfModelCacheKey("/models/chip.glb?cachebust_origin=abc"),
).toBe("/models/chip.glb")
})

test("clones cached scenes with independent material instances", () => {
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 })
const original = new THREE.Group()
original.add(new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material))

const firstClone = cloneSceneWithIndependentMaterials(original)
const secondClone = cloneSceneWithIndependentMaterials(original)
const firstMesh = firstClone.children[0] as THREE.Mesh
const secondMesh = secondClone.children[0] as THREE.Mesh

expect(firstMesh.material).not.toBe(material)
expect(secondMesh.material).not.toBe(material)
expect(firstMesh.material).not.toBe(secondMesh.material)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file contains two test(...) calls (one at line 8 and another at line 20), which violates the rule that a *.test.ts file may have AT MOST one test(...). Please split this into two separate numbered files, e.g. gltf-model-cache1.test.ts (for the cache key normalization test) and gltf-model-cache2.test.ts (for the independent material cloning test).

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +73 to +82
const loader = new GLTFLoader()
const promise = loader.loadAsync(gltfUrl).then((gltf) => {
cache.set(cacheKey, { promise, scene: gltf.scene })
return gltf.scene
})

cache.set(cacheKey, { promise, scene: null })

const scene = await promise
return cloneSceneWithIndependentMaterials(scene)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Failed model loads are permanently cached, preventing retries. When loader.loadAsync(gltfUrl) rejects (network error, 404, invalid file, etc.), the rejected promise is stored in the cache. All subsequent calls with the same URL will hit line 68-70 and re-throw the cached rejection forever.

Fix by removing failed entries from cache:

const loader = new GLTFLoader()
const promise = loader.loadAsync(gltfUrl)
  .then((gltf) => {
    cache.set(cacheKey, { promise, scene: gltf.scene })
    return gltf.scene
  })
  .catch((error) => {
    cache.delete(cacheKey)
    throw error
  })

cache.set(cacheKey, { promise, scene: null })

const scene = await promise
return cloneSceneWithIndependentMaterials(scene)
Suggested change
const loader = new GLTFLoader()
const promise = loader.loadAsync(gltfUrl).then((gltf) => {
cache.set(cacheKey, { promise, scene: gltf.scene })
return gltf.scene
})
cache.set(cacheKey, { promise, scene: null })
const scene = await promise
return cloneSceneWithIndependentMaterials(scene)
const loader = new GLTFLoader()
const promise = loader.loadAsync(gltfUrl).then((gltf) => {
cache.set(cacheKey, { promise, scene: gltf.scene })
return gltf.scene
}).catch((error) => {
cache.delete(cacheKey)
throw error
})
cache.set(cacheKey, { promise, scene: null })
const scene = await promise
return cloneSceneWithIndependentMaterials(scene)

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Finesssee
Copy link
Copy Markdown
Author

Addressed the Graphite review feedback in f380a8b:

  • Split the GLTF cache tests so each *.test.ts file has one test(...).
  • Removed failed GLTF loads from the cache so transient failures can retry instead of replaying a cached rejection forever.

Validation is green on the PR now: Format Check, Test Node Bundle Load, Type Check, and Vercel.

Preview/demo links:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant