diff --git a/package.json b/package.json index 4808eb4fca..6a52d1d793 100644 --- a/package.json +++ b/package.json @@ -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:", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04b3d47564..76da91581d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bde3769004..04e53c6837 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -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 diff --git a/src/components/load3d/Load3D.vue b/src/components/load3d/Load3D.vue index a0244a4fe8..b145fb8464 100644 --- a/src/components/load3d/Load3D.vue +++ b/src/components/load3d/Load3D.vue @@ -4,8 +4,6 @@ @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave" @pointerdown.stop - @pointermove.stop - @pointerup.stop > { 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) diff --git a/src/extensions/core/load3d/SceneManager.ts b/src/extensions/core/load3d/SceneManager.ts index 86c30200d2..8434f65b4d 100644 --- a/src/extensions/core/load3d/SceneManager.ts +++ b/src/extensions/core/load3d/SceneManager.ts @@ -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) { diff --git a/src/extensions/core/load3d/SceneModelManager.test.ts b/src/extensions/core/load3d/SceneModelManager.test.ts index 8db052d23f..62442caeb5 100644 --- a/src/extensions/core/load3d/SceneModelManager.test.ts +++ b/src/extensions/core/load3d/SceneModelManager.test.ts @@ -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', () => { diff --git a/src/extensions/core/load3d/SceneModelManager.ts b/src/extensions/core/load3d/SceneModelManager.ts index 766fd0b243..2d4179ad93 100644 --- a/src/extensions/core/load3d/SceneModelManager.ts +++ b/src/extensions/core/load3d/SceneModelManager.ts @@ -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) { diff --git a/vitest.setup.ts b/vitest.setup.ts index a22e823b49..9d5cc0095d 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -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 {