Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 37 additions & 8 deletions src/hooks/use-global-obj-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useEffect } from "react"
import { useEffect, useState } from "react"
import * as THREE from "three"
import type { Object3D } from "three"
import { MTLLoader, OBJLoader } from "three-stdlib"
import { loadVrml } from "src/utils/vrml"
Expand All @@ -9,6 +10,38 @@ interface CacheItem {
result: Object3D | null
}

export function cloneLoadedObject(source: Object3D): Object3D {
const clone = source.clone(true)
const sourceObjects: Object3D[] = []
const clonedObjects: Object3D[] = []

source.traverse((object) => sourceObjects.push(object))
clone.traverse((object) => clonedObjects.push(object))

clonedObjects.forEach((object, index) => {
const sourceObject = sourceObjects[index]
if (
!(sourceObject instanceof THREE.Mesh) ||
!(object instanceof THREE.Mesh)
) {
return
}

if (sourceObject.geometry) {
object.geometry = sourceObject.geometry
}
if (Array.isArray(sourceObject.material)) {
object.material = sourceObject.material.map((material) =>
material.clone(),
)
} else if (sourceObject.material) {
object.material = sourceObject.material.clone()
}
})

return clone
}

declare global {
interface Window {
TSCIRCUIT_OBJ_LOADER_CACHE: Map<string, CacheItem>
Expand Down Expand Up @@ -82,23 +115,19 @@ export function useGlobalObjLoader(
if (cache.has(cleanUrl)) {
const cacheItem = cache.get(cleanUrl)!
if (cacheItem.result) {
// If we have a result, clone it
return Promise.resolve(cacheItem.result.clone())
return Promise.resolve(cloneLoadedObject(cacheItem.result))
}
// If we're still loading, return the existing promise
return cacheItem.promise.then((result) => {
if (result instanceof Error) return result
return result.clone()
return cloneLoadedObject(result)
})
}
// If it's not in the cache, create a new promise and cache it
const promise = loadAndParseObj().then((result) => {
if (result instanceof Error) {
// If the result is an Error, return it
return result
}
cache.set(cleanUrl, { ...cache.get(cleanUrl)!, result })
return result
return cloneLoadedObject(result)
})
cache.set(cleanUrl, { promise, result: null })
return promise
Expand Down
27 changes: 27 additions & 0 deletions tests/global-obj-loader-cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { expect, test } from "bun:test"
import * as THREE from "three"
import { cloneLoadedObject } from "../src/hooks/use-global-obj-loader"

test("cloneLoadedObject deep-clones cached meshes while reusing geometry", () => {
const geometry = new THREE.BoxGeometry(1, 2, 3)
const material = new THREE.MeshStandardMaterial({ color: "red" })
const mesh = new THREE.Mesh(geometry, material)
mesh.name = "cached-mesh"

const source = new THREE.Group()
source.add(mesh)

const firstClone = cloneLoadedObject(source) as THREE.Group
const secondClone = cloneLoadedObject(source) as THREE.Group
const firstMesh = firstClone.children[0] as THREE.Mesh
const secondMesh = secondClone.children[0] as THREE.Mesh

expect(firstClone).not.toBe(source)
expect(firstMesh).not.toBe(mesh)
expect(secondMesh).not.toBe(firstMesh)
expect(firstMesh.geometry).toBe(geometry)
expect(secondMesh.geometry).toBe(geometry)
expect(firstMesh.material).not.toBe(material)
expect(secondMesh.material).not.toBe(material)
expect(secondMesh.material).not.toBe(firstMesh.material)
})
Loading