diff --git a/src/three-components/STLModel.tsx b/src/three-components/STLModel.tsx index e5c41e9d..2295629f 100644 --- a/src/three-components/STLModel.tsx +++ b/src/three-components/STLModel.tsx @@ -1,13 +1,13 @@ -import { useState, useEffect, useMemo } from "react" +import { useEffect, useMemo, useState } from "react" +import { useThree } from "src/react-three/ThreeContext" +import { loadCachedStlDataGeometry } from "src/utils/load-cached-stl-data-geometry" import * as THREE from "three" import { STLLoader } from "three-stdlib" -import { useThree } from "src/react-three/ThreeContext" import type { LayerType } from "../hooks/use-stls-from-geom" export function STLModel({ stlUrl, stlData, - mtlUrl, color, opacity = 1, layerType, @@ -23,10 +23,11 @@ export function STLModel({ const [geom, setGeom] = useState(null) useEffect(() => { - const loader = new STLLoader() if (stlData) { try { - const geometry = loader.parse(stlData) + const geometry = loadCachedStlDataGeometry(stlData, (data) => + new STLLoader().parse(data), + ) setGeom(geometry) } catch (e) { console.error("Failed to parse STL data", e) @@ -35,6 +36,7 @@ export function STLModel({ return } if (stlUrl) { + const loader = new STLLoader() loader.load(stlUrl, (geometry) => { setGeom(geometry) }) @@ -66,7 +68,9 @@ export function STLModel({ rootObject.remove(mesh) mesh.geometry.dispose() if (Array.isArray(mesh.material)) { - mesh.material.forEach((m) => m.dispose()) + for (const material of mesh.material) { + material.dispose() + } } else { mesh.material.dispose() } diff --git a/src/utils/load-cached-stl-data-geometry.ts b/src/utils/load-cached-stl-data-geometry.ts new file mode 100644 index 00000000..0dc90e13 --- /dev/null +++ b/src/utils/load-cached-stl-data-geometry.ts @@ -0,0 +1,16 @@ +import type { BufferGeometry } from "three" + +const stlDataGeometryCache = new WeakMap() + +export function loadCachedStlDataGeometry( + stlData: ArrayBuffer, + parseStlData: (stlData: ArrayBuffer) => BufferGeometry, +): BufferGeometry { + let cachedGeometry = stlDataGeometryCache.get(stlData) + if (!cachedGeometry) { + cachedGeometry = parseStlData(stlData) + stlDataGeometryCache.set(stlData, cachedGeometry) + } + + return cachedGeometry.clone() +} diff --git a/tests/load-cached-stl-data-geometry.test.ts b/tests/load-cached-stl-data-geometry.test.ts new file mode 100644 index 00000000..51316a8e --- /dev/null +++ b/tests/load-cached-stl-data-geometry.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "bun:test" +import { BufferGeometry, Float32BufferAttribute } from "three" +import { loadCachedStlDataGeometry } from "../src/utils/load-cached-stl-data-geometry" + +test("caches parsed inline STL data while returning disposable clones", () => { + const stlData = new ArrayBuffer(8) + let parseCalls = 0 + const parsedGeometry = new BufferGeometry() + parsedGeometry.setAttribute( + "position", + new Float32BufferAttribute([0, 0, 0, 1, 0, 0, 0, 1, 0], 3), + ) + + const firstGeometry = loadCachedStlDataGeometry(stlData, () => { + parseCalls += 1 + return parsedGeometry + }) + const secondGeometry = loadCachedStlDataGeometry(stlData, () => { + parseCalls += 1 + return new BufferGeometry() + }) + + expect(parseCalls).toBe(1) + expect(firstGeometry).not.toBe(parsedGeometry) + expect(secondGeometry).not.toBe(parsedGeometry) + expect(firstGeometry).not.toBe(secondGeometry) + + firstGeometry.dispose() + expect(secondGeometry.getAttribute("position")).toBeDefined() +})