chore: upgrade sparkjs to 2.x and three to 0.184 (#12396)

## Summary
- Spark 2.x requires SparkRenderer in scene tree; add it in SceneManager
and protect it in clearModel so model reloads don't dispose the splat
renderer.
- three 0.184 OrbitControls listens on ownerDocument; drop redundant
pointermove/up .stop in Load3D containers so the document listener can
receive events.
- Narrow Texture.image type for 0.184 strict typing.
This commit is contained in:
Terry Jia
2026-05-21 10:06:20 -04:00
committed by GitHub
parent f1f65cff61
commit 52d77e6ee0
10 changed files with 94 additions and 51 deletions

View File

@@ -113,7 +113,7 @@
"primevue": "catalog:",
"reka-ui": "catalog:",
"semver": "^7.7.2",
"three": "^0.170.0",
"three": "catalog:",
"tiptap-markdown": "^0.8.10",
"typegpu": "catalog:",
"vee-validate": "catalog:",

78
pnpm-lock.yaml generated
View File

@@ -91,8 +91,8 @@ catalogs:
specifier: ^10.32.1
version: 10.32.1
'@sparkjsdev/spark':
specifier: ^0.1.10
version: 0.1.10
specifier: ^2.1.0
version: 2.1.0
'@storybook/addon-docs':
specifier: ^10.2.10
version: 10.2.10
@@ -157,8 +157,8 @@ catalogs:
specifier: ^7.7.0
version: 7.7.0
'@types/three':
specifier: ^0.170.0
version: 0.170.0
specifier: ^0.184.1
version: 0.184.1
'@vee-validate/zod':
specifier: ^4.15.1
version: 4.15.1
@@ -337,8 +337,8 @@ catalogs:
specifier: ^0.6.1
version: 0.6.1
three:
specifier: ^0.170.0
version: 0.170.0
specifier: ^0.184.0
version: 0.184.0
tsx:
specifier: ^4.15.6
version: 4.19.4
@@ -438,7 +438,7 @@ importers:
version: link:packages/design-system
'@comfyorg/fbx-exporter-three':
specifier: ^1.0.1
version: 1.0.1(@types/three@0.170.0)(three@0.170.0)
version: 1.0.1(@types/three@0.184.1)(three@0.184.0)
'@comfyorg/object-info-parser':
specifier: workspace:*
version: link:packages/object-info-parser
@@ -483,7 +483,7 @@ importers:
version: 10.32.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))
'@sparkjsdev/spark':
specifier: 'catalog:'
version: 0.1.10
version: 2.1.0(three@0.184.0)
'@tanstack/vue-virtual':
specifier: 'catalog:'
version: 3.13.12(vue@3.5.13(typescript@5.9.3))
@@ -596,8 +596,8 @@ importers:
specifier: ^7.7.2
version: 7.7.4
three:
specifier: ^0.170.0
version: 0.170.0
specifier: 'catalog:'
version: 0.184.0
tiptap-markdown:
specifier: ^0.8.10
version: 0.8.10(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
@@ -621,7 +621,7 @@ importers:
version: 3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.3))
wwobjloader2:
specifier: 'catalog:'
version: 6.2.1(three@0.170.0)
version: 6.2.1(three@0.184.0)
yjs:
specifier: 'catalog:'
version: 13.6.27
@@ -706,7 +706,7 @@ importers:
version: 7.7.0
'@types/three':
specifier: 'catalog:'
version: 0.170.0
version: 0.184.1
'@vitejs/plugin-vue':
specifier: 'catalog:'
version: 6.0.3(vite@8.0.0(@types/node@24.10.4)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.9.0))(vue@3.5.13(typescript@5.9.3))
@@ -980,7 +980,7 @@ importers:
version: 1.358.1
three:
specifier: 'catalog:'
version: 0.170.0
version: 0.184.0
vue:
specifier: 'catalog:'
version: 3.5.13(typescript@5.9.3)
@@ -1850,6 +1850,9 @@ packages:
'@cyberalien/svg-utils@1.1.1':
resolution: {integrity: sha512-i05Cnpzeezf3eJAXLx7aFirTYYoq5D1XUItp1XsjqkerNJh//6BG9sOYHbiO7v0KYMvJAx3kosrZaRcNlQPdsA==}
'@dimforge/rapier3d-compat@0.12.0':
resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
'@dual-bundle/import-meta-resolve@4.2.1':
resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==}
@@ -3948,8 +3951,10 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@sparkjsdev/spark@0.1.10':
resolution: {integrity: sha512-CiijdZQuj7KPDUqIZPiEqyUkJCYo1JqR05vq/V+ElxMwqR7L70ZuZDyIKcasjZHSiPB8pGRMH8HZGqUKO9aRPQ==}
'@sparkjsdev/spark@2.1.0':
resolution: {integrity: sha512-BRw+MuMzx0B3K8fDLQygt2OHEhYUV+41RX7btq9pZ3rCVrq42o57jW34VAIvC7JO/84DJh/1AutACV9ym6BfVg==}
peerDependencies:
three: '>=0.180.0'
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
@@ -4424,8 +4429,8 @@ packages:
'@types/stats.js@0.17.3':
resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==}
'@types/three@0.170.0':
resolution: {integrity: sha512-CUm2uckq+zkCY7ZbFpviRttY+6f9fvwm6YqSqPfA5K22s9w7R4VnA3rzJse8kHVvuzLcTx+CjNCs2NYe0QFAyg==}
'@types/three@0.184.1':
resolution: {integrity: sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==}
'@types/tough-cookie@4.0.5':
resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==}
@@ -7441,8 +7446,8 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
meshoptimizer@0.18.1:
resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
meshoptimizer@1.1.1:
resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==}
micromark-core-commonmark@2.0.3:
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
@@ -8823,8 +8828,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
three@0.170.0:
resolution: {integrity: sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==}
three@0.184.0:
resolution: {integrity: sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==}
tiny-inflate@1.0.3:
resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
@@ -10875,12 +10880,12 @@ snapshots:
'@comfyorg/comfyui-electron-types@0.6.2': {}
'@comfyorg/fbx-exporter-three@1.0.1(@types/three@0.170.0)(three@0.170.0)':
'@comfyorg/fbx-exporter-three@1.0.1(@types/three@0.184.1)(three@0.184.0)':
dependencies:
fflate: 0.8.2
three: 0.170.0
three: 0.184.0
optionalDependencies:
'@types/three': 0.170.0
'@types/three': 0.184.1
'@csstools/color-helpers@5.1.0': {}
@@ -10917,6 +10922,8 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@dimforge/rapier3d-compat@0.12.0': {}
'@dual-bundle/import-meta-resolve@4.2.1': {}
'@emmetio/abbreviation@2.3.3':
@@ -12880,9 +12887,10 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
'@sparkjsdev/spark@0.1.10':
'@sparkjsdev/spark@2.1.0(three@0.184.0)':
dependencies:
fflate: 0.8.2
three: 0.184.0
'@standard-schema/spec@1.1.0': {}
@@ -13405,14 +13413,14 @@ snapshots:
'@types/stats.js@0.17.3': {}
'@types/three@0.170.0':
'@types/three@0.184.1':
dependencies:
'@dimforge/rapier3d-compat': 0.12.0
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.3
'@types/webxr': 0.5.20
'@webgpu/types': 0.1.66
fflate: 0.8.2
meshoptimizer: 0.18.1
meshoptimizer: 1.1.1
'@types/tough-cookie@4.0.5': {}
@@ -16910,7 +16918,7 @@ snapshots:
merge2@1.4.1: {}
meshoptimizer@0.18.1: {}
meshoptimizer@1.1.1: {}
micromark-core-commonmark@2.0.3:
dependencies:
@@ -18772,7 +18780,7 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
three@0.170.0: {}
three@0.184.0: {}
tiny-inflate@1.0.3: {}
@@ -19831,16 +19839,16 @@ snapshots:
wtd-core@3.0.0: {}
wtd-three-ext@3.0.0(three@0.170.0):
wtd-three-ext@3.0.0(three@0.184.0):
dependencies:
three: 0.170.0
three: 0.184.0
wtd-core: 3.0.0
wwobjloader2@6.2.1(three@0.170.0):
wwobjloader2@6.2.1(three@0.184.0):
dependencies:
three: 0.170.0
three: 0.184.0
wtd-core: 3.0.0
wtd-three-ext: 3.0.0(three@0.170.0)
wtd-three-ext: 3.0.0(three@0.184.0)
xdg-basedir@5.1.0: {}

View File

@@ -36,7 +36,7 @@ catalog:
'@primevue/themes': ^4.2.5
'@sentry/vite-plugin': ^4.6.0
'@sentry/vue': ^10.32.1
'@sparkjsdev/spark': ^0.1.10
'@sparkjsdev/spark': ^2.1.0
'@storybook/addon-docs': ^10.2.10
'@storybook/addon-mcp': 0.1.6
'@storybook/vue3': ^10.2.10
@@ -59,7 +59,7 @@ catalog:
'@types/jsdom': ^21.1.7
'@types/node': ^24.1.0
'@types/semver': ^7.7.0
'@types/three': ^0.170.0
'@types/three': ^0.184.1
'@vee-validate/zod': ^4.15.1
'@vercel/analytics': ^2.0.1
'@vitejs/plugin-vue': ^6.0.0
@@ -118,7 +118,7 @@ catalog:
storybook: ^10.2.10
stylelint: ^16.26.1
tailwindcss: ^4.3.0
three: ^0.170.0
three: ^0.184.0
tailwindcss-primeui: ^0.6.1
tsx: ^4.15.6
tw-animate-css: ^1.3.8

View File

@@ -4,8 +4,6 @@
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
>
<Load3DScene
v-if="node"

View File

@@ -5,11 +5,7 @@
data-capture-wheel="true"
tabindex="-1"
@pointerdown.stop="focusContainer"
@pointermove.stop
@pointerup.stop
@mousedown.stop
@mousemove.stop
@mouseup.stop
@contextmenu.stop.prevent
@dragover.prevent.stop="handleDragOver"
@dragleave.stop="handleDragLeave"

View File

@@ -1,3 +1,4 @@
import { SparkRenderer } from '@sparkjsdev/spark'
import * as THREE from 'three'
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -137,6 +138,13 @@ describe('SceneManager', () => {
expect(manager.scene.children).toContain(manager.gridHelper)
})
it('adds a SparkRenderer to the scene so SplatMesh instances render', () => {
const sparkRenderers = manager.scene.children.filter(
(child) => child instanceof SparkRenderer
)
expect(sparkRenderers).toHaveLength(1)
})
it('builds a separate background scene with a tiled mesh', () => {
expect(manager.backgroundScene).toBeInstanceOf(THREE.Scene)
expect(manager.backgroundMesh).toBeInstanceOf(THREE.Mesh)

View File

@@ -1,3 +1,4 @@
import { SparkRenderer } from '@sparkjsdev/spark'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
@@ -11,6 +12,7 @@ import {
export class SceneManager implements SceneManagerInterface {
scene!: THREE.Scene
gridHelper: THREE.GridHelper
private sparkRenderer: SparkRenderer
backgroundScene!: THREE.Scene
backgroundCamera: THREE.OrthographicCamera
@@ -42,6 +44,12 @@ export class SceneManager implements SceneManagerInterface {
this.getActiveCamera = getActiveCamera
// Spark 2.x requires a SparkRenderer in the scene tree to render SplatMesh
// (Gaussian splat) instances; without it splats are silent no-ops. Kept
// alive across model reloads by SceneModelManager.clearModel.
this.sparkRenderer = new SparkRenderer({ renderer })
this.scene.add(this.sparkRenderer)
this.gridHelper = new THREE.GridHelper(20, 20)
this.gridHelper.position.set(0, 0, 0)
this.scene.add(this.gridHelper)
@@ -277,8 +285,8 @@ export class SceneManager implements SceneManagerInterface {
if (!material.map) return
const imageAspect =
backgroundTexture.image.width / backgroundTexture.image.height
const image = backgroundTexture.image as { width: number; height: number }
const imageAspect = image.width / image.height
const targetAspect = targetWidth / targetHeight
if (imageAspect > targetAspect) {

View File

@@ -1,3 +1,4 @@
import { SparkRenderer } from '@sparkjsdev/spark'
import * as THREE from 'three'
import { describe, expect, it, vi } from 'vitest'
@@ -355,6 +356,20 @@ describe('SceneModelManager', () => {
expect(geoDispose).toHaveBeenCalled()
expect(matDispose).toHaveBeenCalled()
})
it('preserves SparkRenderer across model reloads', async () => {
const { manager, scene } = createManager()
const sparkRenderer = new SparkRenderer({
renderer: {} as THREE.WebGLRenderer
})
scene.add(sparkRenderer)
const model = createMeshModel()
await manager.setupModel(model)
manager.clearModel()
expect(scene.children).toContain(sparkRenderer)
})
})
describe('reset', () => {

View File

@@ -1,3 +1,4 @@
import { SparkRenderer } from '@sparkjsdev/spark'
import * as THREE from 'three'
import type { GLTF } from 'three/examples/jsm/loaders/GLTFLoader'
@@ -317,6 +318,7 @@ export class SceneModelManager implements ModelManagerInterface {
object instanceof THREE.GridHelper ||
object instanceof THREE.Light ||
object instanceof THREE.Camera ||
object instanceof SparkRenderer ||
object.name === 'GizmoTransformControls'
if (!isEnvironmentObject) {

View File

@@ -3,11 +3,19 @@ import { vi } from 'vitest'
import 'vue'
// Mock @sparkjsdev/spark which uses WASM that doesn't work in Node.js
vi.mock('@sparkjsdev/spark', () => ({
SplatMesh: class SplatMesh {
constructor() {}
vi.mock('@sparkjsdev/spark', async () => {
const three = await import('three')
return {
SplatMesh: class SplatMesh {
constructor() {}
},
SparkRenderer: class SparkRenderer extends three.Object3D {
constructor() {
super()
}
}
}
}))
})
// Augment Window interface for tests
declare global {