diff --git a/AGENTS.md b/AGENTS.md index 516a72fb..d553397a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,12 +13,45 @@ - Global Vitest setup mocks were removed; tests now mock dependencies locally per file - Shared test mock modules like `src/test/mocks/three.ts` and `src/test/mocks/three-spritetext.ts` were removed; tests should inline only the mocks they actually need - Old global-mock cleanup can leave behind no-op local shims like `vi.mock('three', () => importActual('three'))`; remove them when a test does not override Three behavior +- After the WebGPU import migration, tests must mock `three/webgpu` when the production module imports from `three/webgpu`; mocking `three` does not affect those modules +- `TransformTool` must add and traverse `TransformControls.getHelper()`, not the `TransformControls` instance itself, because current Three typings and runtime treat the control as non-`Object3D` - Local mocks for `three/examples/jsm/*` should use the exact runtime specifier including the `.js` suffix when the source import does - `ARQuickLook` tests must mock `@shopware-ag/dive/assetloader` and `@shopware-ag/dive/assetexporter` in addition to `AssetConverter`, because `new AssetLoader()` and `new AssetExporter()` are evaluated before the mocked `AssetConverter` constructor runs - `DIVEGizmo` tests should mock child gizmo classes as real `Object3D` instances with spied methods to avoid `THREE.Object3D.add` warnings from plain-object stand-ins - `DIVEPrimitive` tests are more stable with real `Box3` plus per-test spies on `Box3.prototype`/`Raycaster`, instead of mocking the full `three` module surface +- `OrientationDisplayAxes` tests should locally stub `three-spritetext` because jsdom does not implement the canvas text context that the real package needs +- `DIVERoot` should detach both legacy scene-level `TransformControls` objects and modern `TransformControlsRoot.controls` helper roots when cleaning up transform controls +- `DIVERoot` POV update/delete coverage requires manually seeding a matching `Object3D` in tests because `addSceneObject` intentionally skips creating POV scene nodes - Plugins live in `src/plugins//` and are auto-discovered by looking for `index.ts` in subdirectories - Plugins are exported as subpath exports: `@shopware-ag/dive/` (e.g. `@shopware-ag/dive/shader`, `@shopware-ag/dive/state`) -- The shader plugin (`src/plugins/shader/`) exports `DIVEShaderMaterial` (extends three.js `ShaderMaterial`) and `DIVEShaderLib` +- The shader plugin (`src/plugins/shader/`) now exposes node-based building blocks like `GridNode` and `GridNodeUniforms`; legacy `DIVEShaderLib`/`DIVEShaderMaterial` shader-lib wrappers are being removed +- `DIVEGrid` custom shader code must use TSL/node materials for WebGPU; plain `ShaderMaterial` triggers `THREE.NodeMaterial: Material "ShaderMaterial" is not compatible` +- `DIVEGrid` owns its `MeshBasicNodeMaterial` setup and creates its grid uniform nodes locally; `GridNode` only provides the TSL output-node implementation - `DIVEGrid` component uses the shader plugin; it is imported transitively via `Scene` → `Grid` → `@shopware-ag/dive/shader` +- Shader plugin public docs must describe the new node-based API: `GridNode` plus `GridNodeUniforms`; legacy `DIVEShaderLib`/`DIVEShaderMaterial` docs are outdated +- Tests that mock `@shopware-ag/dive/shader` must provide a `GridNode` constructor stub after the shader plugin migration; legacy `DIVEShaderLib`-only mocks break transitive imports +- Most tests do not need to mock `@shopware-ag/dive/shader` at all; after the WebGPU migration the only current direct need is `src/components/grid/__test__/Grid.test.ts`, which asserts `DIVEGrid` constructs `GridNode` +- When partially mocking `three/webgpu`, base the mock on `importOriginal()`; using `vi.importActual('three')` drops WebGPU-only exports like `Node` and breaks transitive shader imports +- `DIVEGrid` tests or other `MeshBasicNodeMaterial` mocks must preserve the constructor `outputNode` param because production code passes `new GridNode(uniforms)` directly into material creation +- `GridNode` unit tests are best written with local `three/tsl` and `three/webgpu` mocks plus `vi.hoisted(...)`; plain top-level mock helpers break because `vi.mock(...)` factories are hoisted +- `GridNode` returns the final `vec4(...)` TSL node from its constructor while still naming the underlying base `Node` instance `GridNode` +- In `GridNode` tests, keep a raw mock-uniform object separate from the `GridNodeUniforms` cast; casting too early hides Vitest `.mock` metadata from TypeScript +- `DIVEEnvironment` no longer applies HDR state from the constructor alone; tests should wait for the async HDR load and call `env.init()` before asserting environment/background updates +- `DIVEEnvironment` concurrent-load cleanup is best covered by spying on the private `loadHDRImage` method and resolving overlapping promises out of order; stale textures should be disposed +- `DIVE` tests must mock `mainView.renderer.initialized` and `mainView.renderer.init()` because `DIVE.start()` now guards rendering via renderer initialization +- On the v3 branch, deprecated compatibility APIs should not be kept alive just to satisfy tests; remove the matching legacy test coverage instead of restoring `DIVE.QuickView()`, `engine`, `createView()`, `disposeView()`, `AnimationSystem.animate()`, `Toolbox.useTool()`, `Toolbox.getActiveTool()`, or old environment no-op methods +- `DIVERenderer` tests must mock `three/webgpu` `WebGPURenderer`; old `three` `WebGLRenderer` expectations are outdated +- `DIVERenderer` stale-init behavior after `setCanvas()` is best tested with a deferred first `init()` promise; the old renderer must not trigger a second environment init after the swap +- `MediaCreator` fallback coverage is easiest by overriding test canvas `width`/`height` to `undefined` and `writable: true`, then letting `drawCanvas()` fall back through `clientWidth` to the renderer canvas dimensions +- `MediaCreator.drawCanvas()` must restore the previous WebGPU render target and camera layer mask before awaiting `readRenderTargetPixelsAsync()`; otherwise the live `View.tick()` render can keep drawing into the offscreen target and trigger `WebGPUTextureUtils: Texture already initialized.` - Demo fixture `/Users/f.frank/Public/Repos/dive-demo/public/model_reverse_animation_order_long_name_blank_name.glb` is used for animation edge cases; it contains a blank clip name, an overlong clip name, and a `Walk` clip that now hard-fails loading via an invalid animation accessor reference +- `yarn build` can still exit successfully while `vite-plugin-dts` reports TypeScript API migration errors, so WebGPU refactors need explicit grep/type-review and not just a green build exit code +- `DIVE.start()` is now a fire-and-forget wrapper around `startAsync()`, so tests that need renderer readiness should await `startAsync()` or a microtask before asserting downstream effects +- `DIVE.dispose()` must dispose the `DIVEClock` before tearing down views/renderers, and `startAsync()` must bail out after late renderer init if the instance was disposed; otherwise demo route switches can leave stale RAF ticks calling `DIVEView.tick()` on dead WebGPU renderers +- Demo views in `/Users/f.frank/Public/Repos/dive-demo/src/views/` that create `QuickView` instances must dispose them in `onUnmounted`; missing route-leave cleanup leaves old WebGPU render loops alive across example switches +- Deprecated `BaseTool` coverage was removed entirely; if `src/plugins/toolbox/src/BaseTool.ts` is gone in a future major, delete the legacy suite instead of recreating the class for tests +- `MediaCreator` screenshot generation is async under WebGPU and uses `RenderTarget` plus `readRenderTargetPixelsAsync`; it no longer swaps `renderer.domElement` +- `DIVEXRLightRoot` currently guards `XREstimatedLight` off under WebGPU and falls back to the existing scene light until a dedicated WebGPU-compatible light-estimation path exists +- Library builds must externalize `three` with a pattern that also matches subpaths like `three/webgpu`, `three/tsl`, and `three/examples/jsm/*`; externalizing only bare `three` bundles a second Three runtime into `build/` and triggers `THREE.WARNING: Multiple instances of Three.js being imported.` in consumers +- State action migrations must use `AnimationSystem.fromTargets(...).play()` and `Toolbox.enableTool()`; lingering `animate()` or `useTool()` calls can still let `yarn build` exit 0 while `vite-plugin-dts` reports TS2339 API drift +- When swapping canvases under WebGPU, `DIVEEnvironment.setRenderer()` must run before disposing the previous `WebGPURenderer`; disposing the old renderer first can crash `PMREMGenerator.dispose()` inside Three's `NodeManager.delete` with `usedTimes` access errors diff --git a/package.json b/package.json index 27b0d877..6e38821e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shopware-ag/dive", - "version": "2.3.9", + "version": "3.0.0", "description": "Shopware Spatial Framework", "type": "module", "main": "build/dive.cjs", @@ -107,7 +107,7 @@ "> 1%, not dead, not ie 11, not op_mini all" ], "dependencies": { - "three": "^0.163.0", + "three": "^0.183.0", "@tweenjs/tween.js": "^25.0.0", "three-spritetext": "^1.8.2", "lodash": "^4.17.21", @@ -119,7 +119,7 @@ "@types/jest": "^29.5.12", "@types/lodash": "^4.17.12", "@types/node": "^20.12.7", - "@types/three": "^0.163.0", + "@types/three": "^0.183.0", "@vitest/coverage-v8": "3.1.2", "acorn": "^8.14.1", "acorn-walk": "^8.3.4", @@ -153,10 +153,11 @@ "lint": "eslint", "prettier:check": "prettier --check .", "prettier:fix": "prettier --write .", + "typecheck": "tsc --noEmit --project tsconfig.json", "unit": "vitest --run", "coverage": "vitest --coverage --run", "docs": "yarn docs:actions", "docs:actions": "tsx docs/generators/generate-actions-docs.ts", - "ci": "yarn && yarn lint && yarn prettier:check && yarn coverage && yarn build" + "ci": "yarn && yarn lint && yarn prettier:check && yarn typecheck && yarn coverage && yarn build" } } diff --git a/scripts/build/vite/vite-plugin-exports.ts b/scripts/build/vite/vite-plugin-exports.ts index fd8aceed..7e74558c 100644 --- a/scripts/build/vite/vite-plugin-exports.ts +++ b/scripts/build/vite/vite-plugin-exports.ts @@ -10,6 +10,12 @@ interface pluginRegistration { buildPath: string; // Path in the build output } +const externalDependencies = [ + /^three(?:\/.*)?$/, + '@tweenjs/tween.js', + 'three-spritetext', +]; + // Function to update package.json exports function updatePackageJsonExports(registrations: pluginRegistration[]): void { const packageJsonPath = pathResolve(process.cwd(), 'package.json'); @@ -142,11 +148,7 @@ export default function pluginBuildPlugin(): Plugin { exports: 'named', }, ], - external: [ - 'three', - '@tweenjs/tween.js', - 'three-spritetext', - ], + external: externalDependencies, }, }, }; diff --git a/src/components/boundingbox/BoundingBox.ts b/src/components/boundingbox/BoundingBox.ts index ce263792..9ca85778 100644 --- a/src/components/boundingbox/BoundingBox.ts +++ b/src/components/boundingbox/BoundingBox.ts @@ -8,7 +8,7 @@ import { MeshBasicMaterial, SphereGeometry, ColorRepresentation, -} from 'three'; +} from 'three/webgpu'; import { DIVENode } from '../node/Node.ts'; /** diff --git a/src/components/boundingbox/__test__/BoundingBox.test.ts b/src/components/boundingbox/__test__/BoundingBox.test.ts index 0c7aaeb7..de7a0c81 100644 --- a/src/components/boundingbox/__test__/BoundingBox.test.ts +++ b/src/components/boundingbox/__test__/BoundingBox.test.ts @@ -9,7 +9,7 @@ import { MeshBasicMaterial, BoxGeometry, SphereGeometry, -} from 'three'; +} from 'three/webgpu'; describe('BoundingBox', () => { let mockObject: Object3D; diff --git a/src/components/floor/Floor.ts b/src/components/floor/Floor.ts index ab643af4..e04c5f7b 100644 --- a/src/components/floor/Floor.ts +++ b/src/components/floor/Floor.ts @@ -5,7 +5,7 @@ import { Mesh, MeshStandardMaterial, PlaneGeometry, -} from 'three'; +} from 'three/webgpu'; import { PRODUCT_LAYER_MASK } from '../../constants/VisibilityLayerMask.ts'; /** diff --git a/src/components/gizmo/Gizmo.ts b/src/components/gizmo/Gizmo.ts index 614d464b..184b28f6 100644 --- a/src/components/gizmo/Gizmo.ts +++ b/src/components/gizmo/Gizmo.ts @@ -1,4 +1,4 @@ -import { Euler, Object3D, Vector3 } from 'three'; +import { Euler, Object3D, Vector3 } from 'three/webgpu'; import { DIVERotateGizmo } from './rotate/RotateGizmo.ts'; import { DIVETranslateGizmo } from './translate/TranslateGizmo.ts'; import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; diff --git a/src/components/gizmo/__test__/Gizmo.test.ts b/src/components/gizmo/__test__/Gizmo.test.ts index fb9faa8d..34a56c67 100644 --- a/src/components/gizmo/__test__/Gizmo.test.ts +++ b/src/components/gizmo/__test__/Gizmo.test.ts @@ -1,7 +1,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { DIVEGizmo, DIVEGizmoMode } from '../Gizmo.ts'; import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; -import { Object3D, Vector3, Euler } from 'three'; +import { Object3D, Vector3, Euler } from 'three/webgpu'; import { DIVESelectable } from '../../../interfaces/Selectable.ts'; // Mock the OrbitController diff --git a/src/components/gizmo/handles/AxisHandle.ts b/src/components/gizmo/handles/AxisHandle.ts index 5d943a39..2bdb0dd7 100644 --- a/src/components/gizmo/handles/AxisHandle.ts +++ b/src/components/gizmo/handles/AxisHandle.ts @@ -6,7 +6,7 @@ import { MeshBasicMaterial, Object3D, Vector3, -} from 'three'; +} from 'three/webgpu'; import { UI_LAYER_MASK } from '../../../constants/VisibilityLayerMask.ts'; import { DIVEHoverable } from '../../../interfaces/Hoverable.ts'; import { DIVETranslateGizmo } from '../translate/TranslateGizmo.ts'; diff --git a/src/components/gizmo/handles/RadialHandle.ts b/src/components/gizmo/handles/RadialHandle.ts index d3033cfb..b336952e 100644 --- a/src/components/gizmo/handles/RadialHandle.ts +++ b/src/components/gizmo/handles/RadialHandle.ts @@ -6,7 +6,7 @@ import { Object3D, TorusGeometry, Vector3, -} from 'three'; +} from 'three/webgpu'; import { UI_LAYER_MASK } from '../../../constants/VisibilityLayerMask.ts'; import { DIVEHoverable } from '../../../interfaces/Hoverable.ts'; import { DraggableEvent } from '@shopware-ag/dive/toolbox'; diff --git a/src/components/gizmo/handles/ScaleHandle.ts b/src/components/gizmo/handles/ScaleHandle.ts index 5d28e14a..36ccc371 100644 --- a/src/components/gizmo/handles/ScaleHandle.ts +++ b/src/components/gizmo/handles/ScaleHandle.ts @@ -7,7 +7,7 @@ import { MeshBasicMaterial, Object3D, Vector3, -} from 'three'; +} from 'three/webgpu'; import { UI_LAYER_MASK } from '../../../constants/VisibilityLayerMask.ts'; import { DIVEHoverable } from '../../../interfaces/Hoverable.ts'; import { DIVEScaleGizmo } from '../scale/ScaleGizmo.ts'; diff --git a/src/components/gizmo/handles/__test__/AxisHandle.test.ts b/src/components/gizmo/handles/__test__/AxisHandle.test.ts index b74e17b0..a8dc1ce8 100644 --- a/src/components/gizmo/handles/__test__/AxisHandle.test.ts +++ b/src/components/gizmo/handles/__test__/AxisHandle.test.ts @@ -2,7 +2,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { DIVEAxisHandle } from '../AxisHandle.ts'; import { DIVETranslateGizmo } from '../../translate/TranslateGizmo.ts'; import { DraggableEvent } from '@shopware-ag/dive/toolbox'; -import { Vector3, Color } from 'three'; +import { Vector3, Color } from 'three/webgpu'; // Mock the TranslateGizmo vi.mock('../../translate/TranslateGizmo', () => ({ diff --git a/src/components/gizmo/handles/__test__/RadialHandle.test.ts b/src/components/gizmo/handles/__test__/RadialHandle.test.ts index d888305c..bf661775 100644 --- a/src/components/gizmo/handles/__test__/RadialHandle.test.ts +++ b/src/components/gizmo/handles/__test__/RadialHandle.test.ts @@ -2,7 +2,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { DIVERadialHandle } from '../RadialHandle.ts'; import { DIVERotateGizmo } from '../../rotate/RotateGizmo.ts'; import { DraggableEvent } from '@shopware-ag/dive/toolbox'; -import { Vector3, Color } from 'three'; +import { Vector3, Color } from 'three/webgpu'; // Mock the RotateGizmo vi.mock('../../rotate/RotateGizmo', () => ({ diff --git a/src/components/gizmo/handles/__test__/ScaleHandle.test.ts b/src/components/gizmo/handles/__test__/ScaleHandle.test.ts index 4757e2d0..42789034 100644 --- a/src/components/gizmo/handles/__test__/ScaleHandle.test.ts +++ b/src/components/gizmo/handles/__test__/ScaleHandle.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { DIVEScaleHandle } from '../ScaleHandle.ts'; import { DIVEScaleGizmo } from '../../scale/ScaleGizmo.ts'; import { DraggableEvent } from '@shopware-ag/dive/toolbox'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; import { vi } from 'vitest'; vi.mock('../../../constants/VisibilityLayerMask', () => ({ UI_LAYER_MASK: 1 })); diff --git a/src/components/gizmo/plane/GizmoPlane.ts b/src/components/gizmo/plane/GizmoPlane.ts index ad415d42..965eb6f9 100644 --- a/src/components/gizmo/plane/GizmoPlane.ts +++ b/src/components/gizmo/plane/GizmoPlane.ts @@ -1,4 +1,4 @@ -import { Mesh, MeshBasicMaterial, Object3D, PlaneGeometry } from 'three'; +import { Mesh, MeshBasicMaterial, Object3D, PlaneGeometry } from 'three/webgpu'; import { UI_LAYER_MASK } from '../../../constants/VisibilityLayerMask.ts'; import { DIVEGizmoAxis, DIVEGizmoMode } from '../Gizmo.ts'; diff --git a/src/components/gizmo/rotate/RotateGizmo.ts b/src/components/gizmo/rotate/RotateGizmo.ts index cc948872..5a2447a1 100644 --- a/src/components/gizmo/rotate/RotateGizmo.ts +++ b/src/components/gizmo/rotate/RotateGizmo.ts @@ -1,4 +1,4 @@ -import { Euler, Object3D, Vector3 } from 'three'; +import { Euler, Object3D, Vector3 } from 'three/webgpu'; import { AxesColorBlue, AxesColorGreen, diff --git a/src/components/gizmo/rotate/__test__/RotateGizmo.test.ts b/src/components/gizmo/rotate/__test__/RotateGizmo.test.ts index 24e4ce1a..2028ae9f 100644 --- a/src/components/gizmo/rotate/__test__/RotateGizmo.test.ts +++ b/src/components/gizmo/rotate/__test__/RotateGizmo.test.ts @@ -4,7 +4,7 @@ import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; import { DIVERadialHandle } from '../../handles/RadialHandle.ts'; import { DIVEGizmo } from '../../Gizmo.ts'; import { DraggableEvent } from '@shopware-ag/dive/toolbox'; -import { Vector3, Euler } from 'three'; +import { Vector3, Euler } from 'three/webgpu'; import { DIVEMath } from '../../../../helpers/math/index.ts'; // Mock the OrbitController diff --git a/src/components/gizmo/scale/ScaleGizmo.ts b/src/components/gizmo/scale/ScaleGizmo.ts index afde0a1d..0a6c19e9 100644 --- a/src/components/gizmo/scale/ScaleGizmo.ts +++ b/src/components/gizmo/scale/ScaleGizmo.ts @@ -1,4 +1,4 @@ -import { Object3D, Vector3 } from 'three'; +import { Object3D, Vector3 } from 'three/webgpu'; import { AxesColorBlue, AxesColorGreen, diff --git a/src/components/gizmo/scale/__test__/ScaleGizmo.test.ts b/src/components/gizmo/scale/__test__/ScaleGizmo.test.ts index 433c3c40..e59cd9d3 100644 --- a/src/components/gizmo/scale/__test__/ScaleGizmo.test.ts +++ b/src/components/gizmo/scale/__test__/ScaleGizmo.test.ts @@ -4,7 +4,7 @@ import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; import { DIVEScaleHandle } from '../../handles/ScaleHandle.ts'; import { DIVEGizmo } from '../../Gizmo.ts'; import { DraggableEvent } from '@shopware-ag/dive/toolbox'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; // Mock the OrbitController vi.mock('@shopware-ag/dive/orbitcontroller', () => ({ diff --git a/src/components/gizmo/translate/TranslateGizmo.ts b/src/components/gizmo/translate/TranslateGizmo.ts index 58538518..c205c3b2 100644 --- a/src/components/gizmo/translate/TranslateGizmo.ts +++ b/src/components/gizmo/translate/TranslateGizmo.ts @@ -1,4 +1,4 @@ -import { Object3D, Vector3 } from 'three'; +import { Object3D, Vector3 } from 'three/webgpu'; import { AxesColorBlue, AxesColorGreen, diff --git a/src/components/gizmo/translate/__test__/TranslateGizmo.test.ts b/src/components/gizmo/translate/__test__/TranslateGizmo.test.ts index 4a426b28..12126c19 100644 --- a/src/components/gizmo/translate/__test__/TranslateGizmo.test.ts +++ b/src/components/gizmo/translate/__test__/TranslateGizmo.test.ts @@ -4,7 +4,7 @@ import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; import { DIVEAxisHandle } from '../../handles/AxisHandle.ts'; import { DIVEGizmo } from '../../Gizmo.ts'; import { DraggableEvent } from '@shopware-ag/dive/toolbox'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; // Mock the OrbitController vi.mock('@shopware-ag/dive/orbitcontroller', () => ({ diff --git a/src/components/grid/Grid.ts b/src/components/grid/Grid.ts index 0fb6f5ea..c68b47b9 100644 --- a/src/components/grid/Grid.ts +++ b/src/components/grid/Grid.ts @@ -1,8 +1,4 @@ -import { - DIVEShaderLib, - DIVEShaderMaterial, - GridShader, -} from '@shopware-ag/dive/shader'; +import { GridNode, type GridNodeUniforms } from '@shopware-ag/dive/shader'; import { GRID_MINOR_LINE_COLOR, GRID_MAJOR_LINE_COLOR, @@ -12,10 +8,11 @@ import { Color, DoubleSide, Mesh, + MeshBasicNodeMaterial, Object3D, PlaneGeometry, - ShaderMaterial, -} from 'three'; +} from 'three/webgpu'; +import { uniform } from 'three/tsl'; const PLANE_SIZE = 50; const GRID_SIZE = 1; @@ -35,7 +32,8 @@ export interface DIVEGridSettings { */ export class DIVEGrid extends Object3D { private _mesh: Mesh; - private _material: ShaderMaterial; + private _material: MeshBasicNodeMaterial; + private _uniforms: GridNodeUniforms; private _gridSize: number; constructor(settings?: DIVEGridSettings) { @@ -48,18 +46,19 @@ export class DIVEGrid extends Object3D { const geometry = new PlaneGeometry(PLANE_SIZE, PLANE_SIZE); geometry.rotateX(-Math.PI / 2); - this._material = new DIVEShaderMaterial({ - ...DIVEShaderLib.grid, - uniforms: { - uGridSize: { value: this._gridSize }, - uMajorLineEvery: { value: majorLineEvery }, - uMinorLineColor: { value: new Color(GRID_MINOR_LINE_COLOR) }, - uMajorLineColor: { value: new Color(GRID_MAJOR_LINE_COLOR) }, - uFadeDistance: { value: PLANE_SIZE / 2 }, - }, + this._uniforms = { + uGridSize: uniform(this._gridSize), + uMajorLineEvery: uniform(majorLineEvery), + uMinorLineColor: uniform(new Color(GRID_MINOR_LINE_COLOR)), + uMajorLineColor: uniform(new Color(GRID_MAJOR_LINE_COLOR)), + uFadeDistance: uniform(PLANE_SIZE / 2), + }; + + this._material = new MeshBasicNodeMaterial({ transparent: true, depthWrite: false, side: DoubleSide, + outputNode: new GridNode(this._uniforms), }); this._mesh = new Mesh(geometry, this._material); @@ -83,11 +82,11 @@ export class DIVEGrid extends Object3D { public setGridSize(size: number): void { this._gridSize = size; - this._material.uniforms.uGridSize.value = size; + this._uniforms.uGridSize.value = size; } public setMajorLineEvery(n: number): void { - this._material.uniforms.uMajorLineEvery.value = n; + this._uniforms.uMajorLineEvery.value = n; } public dispose(): void { diff --git a/src/components/grid/__test__/Grid.test.ts b/src/components/grid/__test__/Grid.test.ts index 1c167a4c..8e4ab62c 100644 --- a/src/components/grid/__test__/Grid.test.ts +++ b/src/components/grid/__test__/Grid.test.ts @@ -1,36 +1,38 @@ -vi.mock('three', () => { +vi.mock('@shopware-ag/dive/shader', () => { + const GridNode = vi.fn(function (this: any, uniforms) { + this.uniforms = uniforms; + return this; + }); + + return { + GridNode, + }; +}); + +vi.mock('three/webgpu', () => { class MockVector3 { x: number; y: number; z: number; + constructor(x = 0, y = 0, z = 0) { this.x = x; this.y = y; this.z = z; } + set(x: number, y: number, z: number) { this.x = x; this.y = y; this.z = z; return this; } - copy(v: MockVector3) { - this.x = v.x; - this.y = v.y; - this.z = v.z; - return this; - } - clone() { - return new MockVector3(this.x, this.y, this.z); - } } class MockEuler { x = 0; y = 0; z = 0; - set = vi.fn(); - copy = vi.fn(); } class MockQuaternion { @@ -38,7 +40,6 @@ vi.mock('three', () => { y = 0; z = 0; w = 1; - set = vi.fn(); } class MockObject3D { @@ -62,12 +63,14 @@ vi.mock('three', () => { this.children.push(child); return this; } + remove(child: MockObject3D) { - const idx = this.children.indexOf(child); - if (idx !== -1) this.children.splice(idx, 1); + const index = this.children.indexOf(child); + if (index !== -1) this.children.splice(index, 1); child.parent = null; return this; } + dispatchEvent = vi.fn(); updateMatrixWorld = vi.fn(); } @@ -77,43 +80,39 @@ vi.mock('three', () => { dispose = vi.fn(); } - class MockShaderMaterial { - uniforms: Record; - vertexShader: string; - fragmentShader: string; + class MockMesh extends MockObject3D { + isMesh = true; + geometry: MockPlaneGeometry; + material: any; + + constructor(geometry?: any, material?: any) { + super(); + this.geometry = geometry ?? new MockPlaneGeometry(); + this.material = material; + } + } + + class MockMeshBasicNodeMaterial { + outputNode: unknown = null; transparent: boolean; - depthTest: boolean; depthWrite: boolean; side: number; constructor(params: any = {}) { - this.uniforms = params.uniforms ?? {}; - this.vertexShader = params.vertexShader ?? ''; - this.fragmentShader = params.fragmentShader ?? ''; this.transparent = params.transparent ?? false; - this.depthTest = params.depthTest ?? true; this.depthWrite = params.depthWrite ?? true; this.side = params.side ?? 0; + this.outputNode = params.outputNode ?? null; } - dispose = vi.fn(); - } - - class MockMesh extends MockObject3D { - isMesh = true; - geometry: MockPlaneGeometry; - material: MockShaderMaterial; - constructor(geometry?: any, material?: any) { - super(); - this.geometry = geometry ?? new MockPlaneGeometry(); - this.material = material ?? new MockShaderMaterial(); - } + dispose = vi.fn(); } class MockColor { r = 0; g = 0; b = 0; + constructor(color?: string | number) { if (typeof color === 'string') { const hex = color.replace('#', ''); @@ -134,17 +133,22 @@ vi.mock('three', () => { return { Object3D: MockObject3D, Mesh: MockMesh, + MeshBasicNodeMaterial: MockMeshBasicNodeMaterial, PlaneGeometry: MockPlaneGeometry, - ShaderMaterial: MockShaderMaterial, Color: MockColor, PerspectiveCamera: MockPerspectiveCamera, DoubleSide: 2, }; }); +vi.mock('three/tsl', () => ({ + uniform: vi.fn((value) => ({ value })), +})); + +import { GridNode } from '@shopware-ag/dive/shader'; import { DIVEGrid } from '../Grid.ts'; import { HELPER_LAYER_MASK } from '../../../constants/VisibilityLayerMask.ts'; -import { Mesh, ShaderMaterial, PerspectiveCamera } from 'three'; +import { Mesh, MeshBasicNodeMaterial, PerspectiveCamera } from 'three/webgpu'; let grid: DIVEGrid; @@ -160,9 +164,10 @@ describe('dive/grid/DIVEGrid', () => { const mesh = grid.children[0] as Mesh; expect(mesh).toBeInstanceOf(Mesh); - expect(mesh.material).toBeInstanceOf(ShaderMaterial); - expect((mesh.material as ShaderMaterial).depthWrite).toBe(false); - expect((mesh.material as ShaderMaterial).transparent).toBe(true); + expect(mesh.material).toBeInstanceOf(MeshBasicNodeMaterial as any); + expect((mesh.material as any).depthWrite).toBe(false); + expect((mesh.material as any).transparent).toBe(true); + expect(GridNode).toHaveBeenCalledTimes(1); expect(mesh.layers.mask).toBe(HELPER_LAYER_MASK); expect(mesh.frustumCulled).toBe(false); }); @@ -170,9 +175,9 @@ describe('dive/grid/DIVEGrid', () => { it('should accept custom settings', () => { const customGrid = new DIVEGrid({ gridSize: 2, majorLineEvery: 10 }); const mesh = customGrid.children[0] as Mesh; - const material = mesh.material as ShaderMaterial; - expect(material.uniforms.uGridSize.value).toBe(2); - expect(material.uniforms.uMajorLineEvery.value).toBe(10); + const uniforms = (mesh.material as any).outputNode.uniforms; + expect(uniforms.uGridSize.value).toBe(2); + expect(uniforms.uMajorLineEvery.value).toBe(10); }); it('should set visibility', () => { @@ -185,15 +190,15 @@ describe('dive/grid/DIVEGrid', () => { it('should update grid size via setter', () => { grid.setGridSize(3); const mesh = grid.children[0] as Mesh; - const material = mesh.material as ShaderMaterial; - expect(material.uniforms.uGridSize.value).toBe(3); + const uniforms = (mesh.material as any).outputNode.uniforms; + expect(uniforms.uGridSize.value).toBe(3); }); it('should update major line interval via setter', () => { grid.setMajorLineEvery(10); const mesh = grid.children[0] as Mesh; - const material = mesh.material as ShaderMaterial; - expect(material.uniforms.uMajorLineEvery.value).toBe(10); + const uniforms = (mesh.material as any).outputNode.uniforms; + expect(uniforms.uMajorLineEvery.value).toBe(10); }); it('should snap position to camera in onBeforeRender', () => { @@ -236,10 +241,7 @@ describe('dive/grid/DIVEGrid', () => { it('should dispose geometry and material', () => { const mesh = grid.children[0] as Mesh; const geometryDispose = vi.spyOn(mesh.geometry, 'dispose'); - const materialDispose = vi.spyOn( - mesh.material as ShaderMaterial, - 'dispose', - ); + const materialDispose = vi.spyOn(mesh.material as any, 'dispose'); grid.dispose(); diff --git a/src/components/group/Group.ts b/src/components/group/Group.ts index 417340e8..698a1884 100644 --- a/src/components/group/Group.ts +++ b/src/components/group/Group.ts @@ -5,7 +5,7 @@ import { Vector3, Vector3Like, Object3D, -} from 'three'; +} from 'three/webgpu'; import { DIVENode } from '../node/Node.ts'; import { type DIVESceneObject } from '../../types/index.ts'; diff --git a/src/components/group/__test__/Group.test.ts b/src/components/group/__test__/Group.test.ts index 20990c25..68378612 100644 --- a/src/components/group/__test__/Group.test.ts +++ b/src/components/group/__test__/Group.test.ts @@ -1,11 +1,4 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - -import { Object3D, type Vector3Like } from 'three'; +import { Object3D, type Vector3Like } from 'three/webgpu'; import { State } from '@shopware-ag/dive/state'; import { type DIVENode } from '../../node/Node.ts'; import { DIVEGroup } from '../Group.ts'; diff --git a/src/components/light/AmbientLight.ts b/src/components/light/AmbientLight.ts index 9a531d25..55cef9d4 100644 --- a/src/components/light/AmbientLight.ts +++ b/src/components/light/AmbientLight.ts @@ -1,4 +1,4 @@ -import { AmbientLight, Color, Object3D } from 'three'; +import { AmbientLight, Color, Object3D } from 'three/webgpu'; import { PRODUCT_LAYER_MASK } from '../../constants/VisibilityLayerMask.ts'; import { DIVESelectable } from '@shopware-ag/dive'; diff --git a/src/components/light/PointLight.ts b/src/components/light/PointLight.ts index 0dc908e8..1190b9a3 100644 --- a/src/components/light/PointLight.ts +++ b/src/components/light/PointLight.ts @@ -6,7 +6,7 @@ import { Mesh, FrontSide, Object3D, -} from 'three'; +} from 'three/webgpu'; import { PRODUCT_LAYER_MASK, UI_LAYER_MASK, diff --git a/src/components/light/SceneLight.ts b/src/components/light/SceneLight.ts index 16078d46..9449cd85 100644 --- a/src/components/light/SceneLight.ts +++ b/src/components/light/SceneLight.ts @@ -1,6 +1,11 @@ import { DIVESelectable } from '@shopware-ag/dive'; import { PRODUCT_LAYER_MASK } from '../../constants/VisibilityLayerMask.ts'; -import { Color, DirectionalLight, HemisphereLight, Object3D } from 'three'; +import { + Color, + DirectionalLight, + HemisphereLight, + Object3D, +} from 'three/webgpu'; /** * A complex scene light. diff --git a/src/components/light/__test__/AmbientLight.test.ts b/src/components/light/__test__/AmbientLight.test.ts index 7ba9df70..ff055dd2 100644 --- a/src/components/light/__test__/AmbientLight.test.ts +++ b/src/components/light/__test__/AmbientLight.test.ts @@ -1,4 +1,4 @@ -import { type AmbientLight, type Color } from 'three'; +import { type AmbientLight, type Color } from 'three/webgpu'; import { DIVEAmbientLight } from '../AmbientLight.ts'; describe('dive/light/DIVEAmbientLight', () => { diff --git a/src/components/light/__test__/PointLight.test.ts b/src/components/light/__test__/PointLight.test.ts index 08575caa..a26d2ab5 100644 --- a/src/components/light/__test__/PointLight.test.ts +++ b/src/components/light/__test__/PointLight.test.ts @@ -1,13 +1,6 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - import { DIVEPointLight } from '../PointLight.ts'; import { State } from '@shopware-ag/dive/state'; -import { type Color, type PointLight } from 'three'; +import { type Color, type PointLight } from 'three/webgpu'; vi.mock('../../../modules/state/State', () => { return { diff --git a/src/components/light/__test__/SceneLight.test.ts b/src/components/light/__test__/SceneLight.test.ts index 8c5cdb10..ac1b812d 100644 --- a/src/components/light/__test__/SceneLight.test.ts +++ b/src/components/light/__test__/SceneLight.test.ts @@ -1,12 +1,5 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - import { State } from '@shopware-ag/dive/state'; -import { type Color } from 'three'; +import { type Color } from 'three/webgpu'; import { DIVESceneLight } from '../SceneLight.ts'; vi.mock('../../../modules/state/State', () => { diff --git a/src/components/model/Model.ts b/src/components/model/Model.ts index b85e6760..384573f7 100644 --- a/src/components/model/Model.ts +++ b/src/components/model/Model.ts @@ -5,7 +5,7 @@ import { Object3D, Raycaster, Vector3, -} from 'three'; +} from 'three/webgpu'; import { PRODUCT_LAYER_MASK } from '../../constants/VisibilityLayerMask.ts'; import { findSceneRecursive } from '../../helpers/findSceneRecursive/findSceneRecursive.ts'; import { DIVENode } from '../node/Node.ts'; diff --git a/src/components/model/__test__/Model.test.ts b/src/components/model/__test__/Model.test.ts index 5a89b3bc..81813a4d 100644 --- a/src/components/model/__test__/Model.test.ts +++ b/src/components/model/__test__/Model.test.ts @@ -7,7 +7,7 @@ import { MeshStandardMaterial, type Texture, Object3D, -} from 'three'; +} from 'three/webgpu'; import { DIVENode } from '../../node/Node.ts'; import { type MaterialSchema } from '../../../types/schema/MaterialSchema.ts'; import { BoundingBox } from '../../boundingbox/BoundingBox.ts'; @@ -19,7 +19,7 @@ import { BoundingBox } from '../../boundingbox/BoundingBox.ts'; // Mock for Raycaster.intersectObjects - exposed for test assertions const RaycasterIntersectObjectMock = vi.fn().mockReturnValue([]); -vi.mock('three', async (importOriginal) => { +vi.mock('three/webgpu', async (importOriginal) => { const actual = await importOriginal(); // Vector3 mock with essential methods diff --git a/src/components/node/Node.ts b/src/components/node/Node.ts index d30485b0..5b299502 100644 --- a/src/components/node/Node.ts +++ b/src/components/node/Node.ts @@ -1,4 +1,4 @@ -import { Box3, Object3D, Vector3, type Vector3Like } from 'three'; +import { Box3, Object3D, Vector3, type Vector3Like } from 'three/webgpu'; import { PRODUCT_LAYER_MASK } from '../../constants/VisibilityLayerMask.ts'; import { DIVEMovable } from '../../interfaces/Movable.ts'; diff --git a/src/components/node/__test__/Node.test.ts b/src/components/node/__test__/Node.test.ts index dcd7c661..49a5fa10 100644 --- a/src/components/node/__test__/Node.test.ts +++ b/src/components/node/__test__/Node.test.ts @@ -1,13 +1,6 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - import { DIVENode } from '../Node.ts'; import { State } from '@shopware-ag/dive/state'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; vi.mock('../../../modules/state/State', () => { return { diff --git a/src/components/primitive/Primitive.ts b/src/components/primitive/Primitive.ts index 025db272..f7921497 100644 --- a/src/components/primitive/Primitive.ts +++ b/src/components/primitive/Primitive.ts @@ -7,7 +7,7 @@ import { Mesh, MeshStandardMaterial, SphereGeometry, -} from 'three'; +} from 'three/webgpu'; import { PRODUCT_LAYER_MASK } from '../../constants/VisibilityLayerMask.ts'; import { DIVEModel } from '../model/Model.ts'; import { type GeometrySchema } from '../../types/index.ts'; diff --git a/src/components/primitive/__test__/Primitive.test.ts b/src/components/primitive/__test__/Primitive.test.ts index 83010651..8b32f981 100644 --- a/src/components/primitive/__test__/Primitive.test.ts +++ b/src/components/primitive/__test__/Primitive.test.ts @@ -5,14 +5,14 @@ import { Mesh, type Texture, type MeshStandardMaterial, -} from 'three'; +} from 'three/webgpu'; import { type State } from '@shopware-ag/dive/state'; import { DIVEScene } from 'src/engine/scene/Scene.ts'; import { GeometrySchema } from 'src/types/schema/GeometrySchema.ts'; import { MaterialSchema } from 'src/types/schema/MaterialSchema.ts'; import { GeometryTypeSchema } from 'src/types/schema/GeometryTypeSchema.ts'; -const RaycasterIntersectObjectMock = vi.fn(() => []); +const RaycasterIntersectObjectMock = vi.fn().mockReturnValue([]); vi.mock('three', async () => { const actual = await vi.importActual('three'); diff --git a/src/components/root/Root.ts b/src/components/root/Root.ts index 21346024..b6dbd898 100644 --- a/src/components/root/Root.ts +++ b/src/components/root/Root.ts @@ -1,4 +1,4 @@ -import { Box3, Color, Object3D } from 'three'; +import { Box3, Color, Object3D } from 'three/webgpu'; import { DIVEAmbientLight } from '../light/AmbientLight.ts'; import { DIVEPointLight } from '../light/PointLight.ts'; import { DIVESceneLight } from '../light/SceneLight.ts'; @@ -6,7 +6,7 @@ import { DIVEModel } from '../model/Model.ts'; import { DIVEPrimitive } from '../primitive/Primitive.ts'; import { type DIVEScene } from '../../engine/scene/Scene.ts'; -import { type TransformControls } from 'three/examples/jsm/controls/TransformControls.ts'; +import { type TransformControls } from 'three/examples/jsm/controls/TransformControls.js'; import { LightSchema, ModelSchema, @@ -354,10 +354,26 @@ export class DIVERoot extends Object3D { private _detachTransformControls(object: Object3D): void { // this is only neccessary due to using the old TransformControls instead of the new DIVEGizmo - this._findScene(object).children.find((object) => { - if ('isTransformControls' in object) { - (object as TransformControls).detach(); + this._findScene(object).children.find((sceneChild) => { + const helperRoot = sceneChild as Object3D & { + isTransformControlsRoot?: boolean; + controls?: TransformControls; + }; + if (helperRoot.isTransformControlsRoot && helperRoot.controls) { + helperRoot.controls.detach(); + return true; } + + const controls = sceneChild as Object3D & { + isTransformControls?: boolean; + detach?: () => void; + }; + if (controls.isTransformControls && controls.detach) { + controls.detach(); + return true; + } + + return false; }); } diff --git a/src/components/root/__test__/Root.test.ts b/src/components/root/__test__/Root.test.ts index ce8c92f3..47312fdf 100644 --- a/src/components/root/__test__/Root.test.ts +++ b/src/components/root/__test__/Root.test.ts @@ -9,10 +9,11 @@ import { EntityTypeSchema, GeometryTypeSchema, } from '@shopware-ag/dive'; -import { Object3D, Vector3, Box3 } from 'three'; +import { Object3D, Vector3, Box3 } from 'three/webgpu'; -vi.mock('three', async () => { - const actual = await vi.importActual('three'); +vi.mock('three/webgpu', async () => { + const actual = + await vi.importActual('three/webgpu'); const Object3D = vi.fn(function (this: any) { this.isObject3D = true; @@ -752,6 +753,29 @@ describe('components/root/DIVERoot', () => { 'DIVERoot.addSceneObject: Unknown entity type: unknown', ); }); + + it('should warn and return the existing object when adding a duplicate id', () => { + const modelData: ModelSchema = { + id: 'model-duplicate', + entityType: 'model', + name: 'Test Model', + visible: true, + uri: 'test.glb', + position: { x: 1, y: 2, z: 3 }, + rotation: { x: 0, y: 0, z: 0 }, + scale: { x: 1, y: 1, z: 1 }, + loaded: false, + }; + + const root = new DIVERoot(); + const firstObject = root.addSceneObject(modelData); + const duplicateObject = root.addSceneObject(modelData); + + expect(duplicateObject).toBe(firstObject); + expect(spyConsoleWarn).toHaveBeenCalledWith( + 'DIVERoot.addSceneObject: Scene object with id model-duplicate already exists', + ); + }); }); describe('updateSceneObject', () => { @@ -905,6 +929,25 @@ describe('components/root/DIVERoot', () => { ); }); + it('should no-op when updating a found POV object', () => { + const povData = { + id: 'pov-1', + entityType: 'pov' as EntityTypeSchema, + name: 'Test POV', + visible: true, + }; + + const root = new DIVERoot(); + const povObject = new Object3D(); + povObject.userData.id = povData.id; + root.add(povObject); + + root.updateSceneObject(povData); + + expect(spyConsoleWarn).not.toHaveBeenCalled(); + expect(root.getSceneObject(povData)).toBe(povObject); + }); + it('should warn for unknown entity type in update', () => { const unknownData = { id: 'unknown', @@ -919,6 +962,23 @@ describe('components/root/DIVERoot', () => { 'DIVERoot.updateSceneObject: Scene object with id unknown does not exist', ); }); + + it('should throw for unknown entity type when the object exists', () => { + const unknownData = { + id: 'unknown', + entityType: 'unknown' as EntityTypeSchema, + name: 'Unknown', + }; + + const root = new DIVERoot(); + const existingObject = new Object3D(); + existingObject.userData.id = unknownData.id; + root.add(existingObject); + + expect(() => root.updateSceneObject(unknownData)).toThrow( + 'DIVERoot.updateSceneObject: Unknown entity type: unknown', + ); + }); }); describe('deleteSceneObject', () => { @@ -982,6 +1042,27 @@ describe('components/root/DIVERoot', () => { ); }); + it('should no-op when deleting a found POV object', () => { + const povData: PovSchema = { + id: 'pov-1', + entityType: 'pov', + name: 'Test POV', + visible: true, + position: { x: 1, y: 2, z: 3 }, + target: { x: 0, y: 0, z: 0 }, + }; + + const root = new DIVERoot(); + const povObject = new Object3D(); + povObject.userData.id = povData.id; + root.add(povObject); + + root.deleteSceneObject(povData); + + expect(spyConsoleWarn).not.toHaveBeenCalled(); + expect(root.getSceneObject(povData)).toBe(povObject); + }); + it('should warn for unknown entity type in deletion', () => { const unknownData = { id: 'unknown', @@ -1647,6 +1728,25 @@ describe('components/root/DIVERoot', () => { expect(mockTransformControls.detach).toHaveBeenCalled(); }); + it('should detach controls from transform control helper roots', () => { + const mockObject = new Object3D(); + const detach = vi.fn(); + const mockHelperRoot = Object.assign(new Object3D(), { + isTransformControlsRoot: true, + controls: { + detach, + }, + }); + + const mockScene = new Object3D(); + mockScene.children = [mockHelperRoot]; + mockObject.parent = mockScene; + + const root = new DIVERoot(); + root['_detachTransformControls'](mockObject); + expect(detach).toHaveBeenCalled(); + }); + it('should handle object without transform controls', () => { const mockObject = new Object3D(); const mockScene = new Object3D(); diff --git a/src/engine/Dive.ts b/src/engine/Dive.ts index e21d746b..65cbcd28 100644 --- a/src/engine/Dive.ts +++ b/src/engine/Dive.ts @@ -1,4 +1,4 @@ -import { MathUtils } from 'three'; +import { MathUtils } from 'three/webgpu'; import { DIVEClock } from './clock/Clock.ts'; import { DIVEView } from './view/View.ts'; import { @@ -12,7 +12,6 @@ import { DIVEPerspectiveCameraSettings, } from './camera/PerspectiveCamera.ts'; import { - DIVERenderer, DIVERendererDefaultSettings, DIVERendererSettings, } from './renderer/Renderer.ts'; @@ -95,23 +94,10 @@ export const DIVEDefaultSettings: Required = { */ export class DIVE { - /** - * @deprecated This static method will be removed in a future version. Please use `import { QuickView, QuickViewSettings, QuickViewDefaultSettings } from '@shopware-ag/dive/quickview'` instead. - */ - public static async QuickView( - uri: string, - settings?: Partial< - import('@shopware-ag/dive/quickview').QuickViewSettings - >, - ): Promise { - return import('@shopware-ag/dive/quickview').then(({ QuickView }) => - QuickView(uri, settings), - ); - } - // descriptive members private _instanceId: string = MathUtils.generateUUID(); private _settings: DIVESettings; + private _disposed: boolean = false; private _views: DIVEView[]; private _mainView: DIVEView; @@ -154,6 +140,10 @@ export class DIVE { if (this._settings.displayAxes) { import('@shopware-ag/dive/orientationdisplay').then( ({ OrientationDisplay }) => { + if (this._disposed) { + return; + } + this._orientationDisplay = new OrientationDisplay( this.mainView.renderer, this.scene, @@ -177,39 +167,6 @@ export class DIVE { window.DIVE.instances.push(this); } - /** - * @deprecated This property will be removed in a future version. Please use properties on the DIVE instance and mainView directly. - */ - public get engine(): { - scene: DIVEScene; - camera: DIVEPerspectiveCamera; - renderer: DIVERenderer; - setCanvas: (canvas: HTMLCanvasElement) => void; - clock: DIVEClock; - start: () => void; - stop: () => void; - dispose: () => void; - } { - return { - scene: this.scene, - camera: this.mainView.camera, - renderer: this.mainView.renderer, - setCanvas: (canvas: HTMLCanvasElement) => { - this.mainView.setCanvas(canvas); - }, - clock: this.clock, - start: () => { - this.start(); - }, - stop: () => { - this.stop(); - }, - dispose: () => { - this.dispose(); - }, - }; - } - public get views(): DIVEView[] { return this._views; } @@ -231,6 +188,31 @@ export class DIVE { } public start(): void { + if (this._disposed) { + return; + } + + void this.startAsync().catch((error) => { + console.error( + 'DIVE.start: Failed to initialize the WebGPU renderer.', + error, + ); + }); + } + + public async startAsync(): Promise { + if (this._disposed) { + return; + } + + if (!this.mainView.renderer.initialized) { + await this.mainView.renderer.init(); + } + + if (this._disposed) { + return; + } + this._clock.start(); } @@ -239,15 +221,19 @@ export class DIVE { } public async dispose(): Promise { + this._disposed = true; + return new Promise((resolve) => { + this._clock.dispose(); + this._views.forEach((view) => { view.dispose(); }); this._views = []; if (this._orientationDisplay) { - this._clock.removeTicker(this._orientationDisplay); this._orientationDisplay.dispose(); + this._orientationDisplay = null; } this.scene.dispose(); @@ -259,46 +245,4 @@ export class DIVE { resolve(); }); } - - /** - * @deprecated This method will be removed in a future version. To create a new view, use `QuickView` instead. - */ - public createView(camera?: DIVEPerspectiveCamera): DIVEView { - const view = new DIVEView( - this._scene, - camera ?? new DIVEPerspectiveCamera(), - { - ...this._settings, - canvas: undefined, // instantiate new canvas for created view - }, - ); - - this._views.push(view); - this._clock.addTicker(view); - - if (this._views.length === 1) { - this._mainView = view; - } - - return view; - } - - /** - * @deprecated This method will be removed in a future version. - */ - public disposeView(view: DIVEView): void { - this._views = this._views.filter((v) => v !== view); - this._clock.removeTicker(view); - - if (this._mainView === view) { - this._mainView = this._views[0]; - } - - view.dispose(); - } } - -/** - * @deprecated Use `import { DIVE } from '@shopware-ag/dive'` instead. - */ -export const DIVEEngine = DIVE; diff --git a/src/engine/__test__/Dive.test.ts b/src/engine/__test__/Dive.test.ts index 35f4df4e..4c9a8d2d 100644 --- a/src/engine/__test__/Dive.test.ts +++ b/src/engine/__test__/Dive.test.ts @@ -4,10 +4,10 @@ import { vi } from 'vitest'; import { DIVE, DIVESettings } from '../Dive.ts'; -import { MathUtils } from 'three'; -import { DIVEClock } from '../clock/Clock.ts'; -import { DIVERenderer } from '../renderer/Renderer.ts'; -import { DIVEScene } from '../scene/Scene.ts'; +import { MathUtils } from 'three/webgpu'; + +const waitForAsync = () => new Promise((resolve) => setTimeout(resolve, 0)); + // Mock ResizeObserver class MockResizeObserver { observe() {} @@ -16,13 +16,6 @@ class MockResizeObserver { } global.ResizeObserver = MockResizeObserver as any; -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - vi.mock('../../components/boundingbox/BoundingBox.ts', () => ({ BoundingBox: vi.fn(), })); @@ -32,22 +25,35 @@ vi.mock('../view/View.ts', async (importOriginal) => { return { ...actual, DIVEView: vi.fn(function (this: any) { + const renderer = { + initialized: false, + canvas: { + parentElement: document.createElement('div'), + getBoundingClientRect: vi.fn().mockReturnValue({ + width: 100, + height: 100, + }), + }, + init: vi.fn(() => { + renderer.initialized = true; + return Promise.resolve(); + }), + dispose: vi.fn(), + onResize: vi.fn(), + render: vi.fn(), + setCanvas: vi.fn(), + }; this.dispose = vi.fn(); this.onResize = vi.fn(); this.tick = vi.fn(); this.setCanvas = vi.fn(); + this.renderer = renderer; this.camera = { position: { set: vi.fn(), }, }; - this.canvas = { - parentElement: document.createElement('div'), - getBoundingClientRect: vi.fn().mockReturnValue({ - width: 100, - height: 100, - }), - }; + this.canvas = renderer.canvas; return this; }), }; @@ -284,12 +290,13 @@ describe('DIVE', () => { expect(() => dive.mainView.onResize(800, 600)).not.toThrow(); }); - it('should initialize with axis camera when displayAxes is true', () => { + it('should initialize with axis camera when displayAxes is true', async () => { const settings = { displayAxes: true, } as DIVESettings; const dive = new DIVE(settings); + await waitForAsync(); expect(dive['_orientationDisplay']).toBeDefined(); }); @@ -309,13 +316,15 @@ describe('DIVE', () => { const dive = new DIVE(settings); - dive['_orientationDisplay'] = { + const orientationDisplay = { dispose: vi.fn(), } as any; + dive['_orientationDisplay'] = orientationDisplay; await dive.dispose(); - expect(dive['_orientationDisplay']?.dispose).toHaveBeenCalled(); + expect(orientationDisplay.dispose).toHaveBeenCalled(); + expect(dive.clock.dispose).toHaveBeenCalled(); }); it('should handle dispose when animation system pipeline is not initialized', () => { @@ -333,36 +342,41 @@ describe('DIVE', () => { expect(window.DIVE.instances).toContain(dive); }); - it('should create a new view', () => { + it('should start the clock', () => { const dive = new DIVE(); - const view = dive.createView(); - expect(view).toBeDefined(); - expect(dive.views).toContain(view); + dive.start(); + return waitForAsync().then(() => { + expect(dive.clock.start).toHaveBeenCalled(); + }); }); - it('should dispose a view', () => { - const dive = new DIVE(); - const view = dive.createView(); - dive.disposeView(view); - expect(dive.views).not.toContain(view); - }); + it('should log renderer initialization failures from start', async () => { + const error = new Error('renderer failed'); + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + const dive = new DIVE({ + autoStart: false, + }); - it('should get the engine', () => { - const dive = new DIVE(); - const engine = dive.engine; - expect(engine).toBeDefined(); - }); + dive.mainView.renderer.init.mockRejectedValueOnce(error); + dive.start(); + await waitForAsync(); - it('should set the canvas', () => { - const dive = new DIVE(); - const canvas = document.createElement('canvas'); - dive.engine.setCanvas(canvas); - expect(dive.mainView.setCanvas).toHaveBeenCalledWith(canvas); + expect(errorSpy).toHaveBeenCalledWith( + 'DIVE.start: Failed to initialize the WebGPU renderer.', + error, + ); }); - it('should start the clock', () => { - const dive = new DIVE(); - dive.start(); + it('should expose an explicit async start path', async () => { + const dive = new DIVE({ + autoStart: false, + }); + + await dive.startAsync(); + + expect(dive.mainView.renderer.init).toHaveBeenCalled(); expect(dive.clock.start).toHaveBeenCalled(); }); @@ -372,33 +386,26 @@ describe('DIVE', () => { expect(dive.clock.stop).toHaveBeenCalled(); }); - it('should set a new mainView when the current one is disposed', () => { - const dive = new DIVE(); - const firstMainView = dive.mainView; - const newView = dive.createView(); - - dive.disposeView(firstMainView); + it('should not start the clock after dispose when renderer init resolves late', async () => { + const dive = new DIVE({ + autoStart: false, + }); + let resolveInit: (() => void) | undefined; - expect(dive.mainView).toBe(newView); - expect(dive.views).not.toContain(firstMainView); - }); - - it('should handle disposing the only view', () => { - const dive = new DIVE(); - const onlyView = dive.mainView; - - dive.disposeView(onlyView); - - expect(dive.mainView).toBeUndefined(); - expect(dive.views.length).toBe(0); - }); + dive.mainView.renderer.init.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveInit = resolve; + }), + ); - it('should set mainView when creating a view after all views were disposed', async () => { - const dive = new DIVE(); + const pendingStart = dive.startAsync(); await dive.dispose(); - const view = dive.createView(); - expect(dive.mainView).toBe(view); + resolveInit?.(); + await pendingStart; + + expect(dive.clock.start).not.toHaveBeenCalled(); }); it('should get the canvas', () => { diff --git a/src/engine/camera/PerspectiveCamera.ts b/src/engine/camera/PerspectiveCamera.ts index 62612657..ce670cad 100644 --- a/src/engine/camera/PerspectiveCamera.ts +++ b/src/engine/camera/PerspectiveCamera.ts @@ -1,4 +1,4 @@ -import { PerspectiveCamera } from 'three'; +import { PerspectiveCamera } from 'three/webgpu'; import { DEFAULT_LAYER_MASK, HELPER_LAYER_MASK, diff --git a/src/engine/environment/Environment.ts b/src/engine/environment/Environment.ts index e55f667f..266c250f 100644 --- a/src/engine/environment/Environment.ts +++ b/src/engine/environment/Environment.ts @@ -6,16 +6,16 @@ import { PMREMGenerator, Scene, Texture, - WebGLCubeRenderTarget, - WebGLRenderTarget, - WebGLRenderer, + CubeRenderTarget, + RenderTarget, + WebGPURenderer, BackSide, SphereGeometry, NoToneMapping, LinearSRGBColorSpace, HalfFloatType, -} from 'three'; -import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'; +} from 'three/webgpu'; +import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js'; import defaultEnvUrl from '../../../assets/maps/env/default.hdr?url'; export type DIVEEnvironmentSettings = { @@ -23,8 +23,6 @@ export type DIVEEnvironmentSettings = { * Whether to enable the image-based lighting. * * @default true - * - * @deprecated enabled defaults to true. */ enabled: boolean; /** @@ -43,8 +41,6 @@ export type DIVEEnvironmentSettings = { * The intensity of the environment lighting. * * @default 1 - * - * @deprecated envIntensity defaults to 1.0. */ globalEnvIntensity: number; /** @@ -63,8 +59,6 @@ export type DIVEEnvironmentSettings = { * Whether to replace the existing lights (can be restored via `restoreLights`). * * @default false - * - * @deprecated replaceLights defaults to false. Remove lights manually instead. */ replaceLights?: boolean; }; @@ -89,16 +83,21 @@ export const DIVEEnvironmentDefaultSettings: DIVEEnvironmentSettings = { export class DIVEEnvironment { private originalBackground: typeof Scene.prototype.background; - private renderer: WebGLRenderer; + private renderer: WebGPURenderer; private scene: Scene; private pmrem: PMREMGenerator; - private currentEnvRT: WebGLRenderTarget | null = null; - private currentBackgroundCube: WebGLCubeRenderTarget | null = null; + private currentEnvRT: RenderTarget | null = null; + private currentBackgroundCube: CubeRenderTarget | null = null; private sourceImage: Texture | null = null; private options: DIVEEnvironmentSettings; + private _loadPromise: Promise; + private _initPromise: Promise | null = null; + private _sourceImageLoadId = 0; + private _initRequested = false; + private _disposed = false; constructor( - renderer: WebGLRenderer, + renderer: WebGPURenderer, scene: Scene, options: Partial = {}, ) { @@ -112,16 +111,34 @@ export class DIVEEnvironment { ...options, }; - this.loadHDRImage(this.options.imageUrl).then((image) => { - this.sourceImage = image; + this._loadPromise = this._loadSourceImage(this.options.imageUrl); + } + + public async init(): Promise { + this._initRequested = true; + + if (this._initPromise) { + return this._initPromise; + } + + this._initPromise = (async () => { + await this._loadPromise; + + if (this._disposed || !this.renderer.initialized) return; + this.update(); + })().finally(() => { + this._initPromise = null; }); + + return this._initPromise; } /** * Disposes the environment. */ public dispose(): void { + this._disposed = true; this.pmrem.dispose(); this.sourceImage?.dispose(); this.sourceImage = null; @@ -155,6 +172,8 @@ export class DIVEEnvironment { * - Early-returns if the source image is not loaded. */ public update(): void { + if (!this.renderer.initialized) return; + if (!this.sourceImage) { this.clearEnvironment(); return; @@ -179,7 +198,7 @@ export class DIVEEnvironment { this.renderer.toneMapping = NoToneMapping; this.renderer.outputColorSpace = LinearSRGBColorSpace; - const cubeRT = new WebGLCubeRenderTarget(1024, { + const cubeRT = new CubeRenderTarget(1024, { type: HalfFloatType, }); const cubeCamera = new CubeCamera(0.1, 1000, cubeRT); @@ -208,7 +227,7 @@ export class DIVEEnvironment { this.currentEnvRT = pmremRT; this.scene.environment = pmremRT.texture; - // keep unfiltered capture as background (matches RGBELoader brightness) + // keep unfiltered capture as background (matches HDRLoader brightness) if (this.options.useAsBackground) { this.scene.background = cubeRT.texture; this.currentBackgroundCube = cubeRT; @@ -225,15 +244,18 @@ export class DIVEEnvironment { } /** - * Sets the renderer and updates the environment. Use this only when rebuilding the webgl renderer. + * Sets the renderer and rebinds the PMREM generator. Use this only when rebuilding the renderer. * - * @param renderer - The webglrenderer. + * @param renderer - The renderer. */ - public setRenderer(renderer: WebGLRenderer): void { - this.renderer = renderer; + public setRenderer(renderer: WebGPURenderer): void { this.pmrem.dispose(); + this.renderer = renderer; this.pmrem = new PMREMGenerator(renderer); - this.update(); + + if (this._initRequested && renderer.initialized) { + this.update(); + } } /** @@ -243,11 +265,13 @@ export class DIVEEnvironment { */ public async setImageUrl(url: string | null): Promise { this.options.imageUrl = url ?? defaultEnvUrl; - this.sourceImage?.dispose(); - this.sourceImage = null; - this.sourceImage = await this.loadHDRImage(this.options.imageUrl); - this.update(); + this._loadPromise = this._loadSourceImage(this.options.imageUrl); + await this._loadPromise; + + if (this._initRequested && this.renderer.initialized) { + this.update(); + } } /** @@ -277,40 +301,21 @@ export class DIVEEnvironment { * @returns The loaded equirectangular HDR texture. */ private async loadHDRImage(url: string): Promise { - const image = await new RGBELoader().loadAsync(url); + const image = await new HDRLoader().loadAsync(url); image.mapping = EquirectangularReflectionMapping; return image; } - /** - * @deprecated setGlobalEnvIntensity does nothing. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public setGlobalEnvIntensity(intensity: number): void { - console.warn('setGlobalEnvIntensity is deprecated and does nothing.'); - } - - /** - * @deprecated setExposure does nothing. - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public setExposure(exposure: number): void { - console.warn('setExposure is deprecated and does nothing.'); - } + private async _loadSourceImage(url: string): Promise { + const loadId = ++this._sourceImageLoadId; + const image = await this.loadHDRImage(url); - /** - * @deprecated disable does nothing. Environment is enabled by default. - */ - public disable(): void { - console.warn( - 'disable is deprecated and does nothing. Environment is enabled by default.', - ); - } + if (this._disposed || loadId !== this._sourceImageLoadId) { + image.dispose(); + return; + } - /** - * @deprecated Use update() instead. - */ - public async enable(): Promise { - this.update(); + this.sourceImage?.dispose(); + this.sourceImage = image; } } diff --git a/src/engine/environment/__test__/Environment.test.ts b/src/engine/environment/__test__/Environment.test.ts index 1faabc98..c5d4f5eb 100644 --- a/src/engine/environment/__test__/Environment.test.ts +++ b/src/engine/environment/__test__/Environment.test.ts @@ -2,54 +2,55 @@ * @jest-environment jsdom */ -import { vi, describe, it, expect, beforeEach } from 'vitest'; -import { WebGLRenderer, Scene, Color } from 'three'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + Color, + CubeRenderTarget as CubeRenderTargetOriginal, + Scene, + WebGPURenderer, +} from 'three/webgpu'; import { DIVEEnvironment } from '../Environment.ts'; -vi.mock('three', async () => { - const actual = await vi.importActual('three'); - - const WebGLRenderer = vi.fn(function (this: any) { - this.toneMapping = actual.NoToneMapping; - this.outputColorSpace = actual.LinearSRGBColorSpace; - return this; - }); - - const WebGLCubeRenderTarget = vi.fn(function (this: any) { - this.texture = { - dispose: vi.fn(), - }; - this.dispose = vi.fn(); - return this; - }); - - const CubeCamera = vi.fn(function (this: any) { - this.update = vi.fn(); - return this; - }); - - const PMREMGenerator = vi.fn(function (this: any) { - this.dispose = vi.fn(); - this.fromCubemap = vi.fn(() => ({ - texture: { - dispose: vi.fn(), - }, - dispose: vi.fn(), - })); - return this; - }); +vi.mock('three/webgpu', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, - WebGLRenderer, - WebGLCubeRenderTarget, - CubeCamera, - PMREMGenerator, + WebGPURenderer: vi.fn(function (this: any) { + this.initialized = true; + this.toneMapping = actual.NoToneMapping; + this.outputColorSpace = actual.LinearSRGBColorSpace; + this.render = vi.fn(); + this.init = vi.fn(async () => {}); + this.dispose = vi.fn(); + return this; + }), + CubeRenderTarget: vi.fn(function (this: any) { + this.texture = { + dispose: vi.fn(), + }; + this.dispose = vi.fn(); + return this; + }), + CubeCamera: vi.fn(function (this: any, _near: number, _far: number) { + this.update = vi.fn(); + return this; + }), + PMREMGenerator: vi.fn(function (this: any) { + this.dispose = vi.fn(); + this.fromCubemap = vi.fn(() => ({ + texture: { + dispose: vi.fn(), + }, + dispose: vi.fn(), + })); + return this; + }), }; }); -vi.mock('three/examples/jsm/loaders/RGBELoader.js', () => ({ - RGBELoader: vi.fn(function (this: any) { +vi.mock('three/examples/jsm/loaders/HDRLoader.js', () => ({ + HDRLoader: vi.fn(function (this: any) { this.loadAsync = vi.fn(async () => ({ mapping: undefined, dispose: vi.fn(), @@ -59,30 +60,67 @@ vi.mock('three/examples/jsm/loaders/RGBELoader.js', () => ({ }), })); -const waitForAsync = () => new Promise((resolve) => setTimeout(resolve, 0)); +const CubeRenderTarget = CubeRenderTargetOriginal as any; +const createDeferred = () => { + let resolve!: (value: T) => void; + const promise = new Promise((resolver) => { + resolve = resolver; + }); + + return { + promise, + resolve, + }; +}; describe('HDREnvironment', () => { let renderer: any; - let scene: any; + let scene: Scene; beforeEach(() => { - renderer = new WebGLRenderer(); + vi.clearAllMocks(); + renderer = new WebGPURenderer(); scene = new Scene(); }); it('loads default image when no imageUrl provided', async () => { const env = new DIVEEnvironment(renderer, scene, { enabled: true }); - await waitForAsync(); + + await env.init(); + expect(scene.environment).toBeDefined(); }); + it('reuses the in-flight init promise when init is called twice', async () => { + const env = new DIVEEnvironment(renderer, scene, { + imageUrl: 'hdr.hdr', + }); + const updateSpy = vi.spyOn(env, 'update'); + const deferred = createDeferred(); + + (env as any)._loadPromise = deferred.promise; + + const firstInit = env.init(); + const secondInit = env.init(); + + expect(updateSpy).not.toHaveBeenCalled(); + + deferred.resolve(); + await Promise.all([ + firstInit, + secondInit, + ]); + + expect(updateSpy).toHaveBeenCalledTimes(1); + }); + it('enables IBL with equirect source and sets background', async () => { const env = new DIVEEnvironment(renderer, scene, { imageUrl: 'hdr.hdr', useAsBackground: true, }); - await waitForAsync(); + await env.init(); expect(scene.background).toBeDefined(); expect(scene.environment).toBeDefined(); @@ -95,26 +133,42 @@ describe('HDREnvironment', () => { useAsBackground: true, }); - await waitForAsync(); + await env.init(); expect(scene.environment).toBeDefined(); expect(scene.background).toBeDefined(); }); - it('can set image URL after construction and re-enable', async () => { + it('can set image URL after construction and initialize again', async () => { const env = new DIVEEnvironment(renderer, scene, { enabled: true }); + + await env.init(); await env.setImageUrl('later.hdr'); + expect(scene.environment).toBeDefined(); }); + it('does not update during init when the renderer is not initialized', async () => { + const uninitializedRenderer = new WebGPURenderer(); + uninitializedRenderer.initialized = false; + const env = new DIVEEnvironment(uninitializedRenderer, scene, { + imageUrl: 'hdr.hdr', + }); + const updateSpy = vi.spyOn(env, 'update'); + + await env.init(); + + expect(updateSpy).not.toHaveBeenCalled(); + }); + it('disposes resources and clears source image', async () => { const env = new DIVEEnvironment(renderer, scene, { imageUrl: 'hdr.hdr', }); - await waitForAsync(); - await env.dispose(); - // After dispose the scene env should be null + await env.init(); + env.dispose(); + expect(scene.environment).toBeNull(); }); @@ -123,12 +177,12 @@ describe('HDREnvironment', () => { imageUrl: 'hdr.hdr', useAsBackground: true, }); - await waitForAsync(); - // Check that background cube exists + await env.init(); + expect((env as any).currentBackgroundCube).toBeDefined(); - await env.dispose(); + env.dispose(); expect(scene.environment).toBeNull(); expect((env as any).currentBackgroundCube).toBeNull(); @@ -138,7 +192,8 @@ describe('HDREnvironment', () => { const env = new DIVEEnvironment(renderer, scene, { imageUrl: 'hdr.hdr', }); - await waitForAsync(); + + await env.init(); const spy = vi.spyOn(env, 'update'); env.setRotationY(1.23); @@ -153,14 +208,17 @@ describe('HDREnvironment', () => { rotateY: 0.1, useAsBackground: true, }); - await waitForAsync(); + + await env.init(); const firstCube = (env as any).currentBackgroundCube; expect(firstCube).toBeDefined(); + const texDispose = firstCube.texture.dispose as any; const rtDispose = firstCube.dispose as any; env.update(); + expect(texDispose).toHaveBeenCalled(); expect(rtDispose).toHaveBeenCalled(); }); @@ -171,19 +229,37 @@ describe('HDREnvironment', () => { rotateY: 0.5, useAsBackground: false, }); - await waitForAsync(); - // capture current instances count, then expect the next one to be disposed - const { WebGLCubeRenderTarget } = await import('three'); - const baseLen = (WebGLCubeRenderTarget as any).mock.instances.length; + await env.init(); + + const baseLen = CubeRenderTarget.mock.instances.length; env.update(); - const created = (WebGLCubeRenderTarget as any).mock.instances[baseLen]; + const created = CubeRenderTarget.mock.instances[baseLen]; expect(created.texture.dispose).toHaveBeenCalled(); expect(created.dispose).toHaveBeenCalled(); }); + it('clears the environment when the source image is missing', async () => { + const originalBg = new Color(0x123456); + scene.background = originalBg; + + const env = new DIVEEnvironment(renderer, scene, { + imageUrl: 'hdr.hdr', + useAsBackground: true, + }); + + await env.init(); + + (env as any).sourceImage = null; + env.update(); + + expect(scene.environment).toBeNull(); + expect(scene.background).toBe(originalBg); + expect((env as any).currentBackgroundCube).toBeNull(); + }); + it('restores original background when background option toggled off', async () => { const originalBg = new Color(0xff0000); scene.background = originalBg; @@ -192,14 +268,14 @@ describe('HDREnvironment', () => { imageUrl: 'hdr.hdr', useAsBackground: true, }); - await waitForAsync(); - // 1. Enable -> replaces background + await env.init(); + expect(scene.background).not.toBe(originalBg); - expect(scene.background).toBeDefined(); // Should be the texture + expect(scene.background).toBeDefined(); - // 2. Toggle useAsBackground off -> restores background env.setUseAsBackground(false); + expect(scene.background).toBe(originalBg); }); @@ -207,61 +283,86 @@ describe('HDREnvironment', () => { const originalBg = new Color(0x00ff00); scene.background = originalBg; - // constructor saves original background const env = new DIVEEnvironment(renderer, scene, { imageUrl: 'hdr.hdr', useAsBackground: true, }); - await waitForAsync(); + await env.init(); const hdrBg = scene.background; expect(hdrBg).not.toBe(originalBg); env.setUseAsBackground(false); - expect(scene.background).toBe(originalBg); + + env.setUseAsBackground(true); + expect(scene.background).not.toBe(originalBg); }); it('updates renderer and regenerates PMREM', async () => { const env = new DIVEEnvironment(renderer, scene, { imageUrl: 'hdr.hdr', }); - await waitForAsync(); - const newRenderer = new WebGLRenderer(); + await env.init(); + + const newRenderer = new WebGPURenderer(); const pmremDisposeSpy = vi.spyOn((env as any).pmrem, 'dispose'); env.setRenderer(newRenderer); expect((env as any).renderer).toBe(newRenderer); expect(pmremDisposeSpy).toHaveBeenCalled(); - // It should also trigger update expect(scene.environment).toBeDefined(); }); - it('deprecated methods do not throw', () => { - const env = new DIVEEnvironment(renderer, scene); - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + it('falls back to the default HDR URL when setting a null image URL', async () => { + const env = new DIVEEnvironment(renderer, scene, { + imageUrl: 'hdr.hdr', + }); + const updateSpy = vi.spyOn(env, 'update'); + + await env.init(); + await env.setImageUrl(null); + + expect((env as any).options.imageUrl).toBeTruthy(); + expect(updateSpy).toHaveBeenCalled(); + }); + + it('disposes stale source images when a newer HDR load replaces them', async () => { + const env = new DIVEEnvironment(renderer, scene, { + imageUrl: 'hdr.hdr', + }); + const firstTexture = { + mapping: undefined, + dispose: vi.fn(), + } as any; + const secondTexture = { + mapping: undefined, + dispose: vi.fn(), + } as any; + const firstLoad = createDeferred(); + const secondLoad = createDeferred(); - expect(() => env.setGlobalEnvIntensity(1)).not.toThrow(); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('deprecated'), - ); + await env.init(); - expect(() => env.setExposure(1)).not.toThrow(); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('deprecated'), - ); + vi.spyOn(env as any, 'loadHDRImage') + .mockImplementationOnce(() => firstLoad.promise) + .mockImplementationOnce(() => secondLoad.promise); - expect(() => env.disable()).not.toThrow(); - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('deprecated'), - ); + const firstUpdate = env.setImageUrl('first.hdr'); + const secondUpdate = env.setImageUrl('second.hdr'); - // enable is async - expect(async () => await env.enable()).not.toThrow(); + firstLoad.resolve(firstTexture); + await Promise.resolve(); + secondLoad.resolve(secondTexture); + await Promise.all([ + firstUpdate, + secondUpdate, + ]); - warnSpy.mockRestore(); + expect(firstTexture.dispose).toHaveBeenCalledTimes(1); + expect((env as any).sourceImage).toBe(secondTexture); }); }); diff --git a/src/engine/renderer/Renderer.ts b/src/engine/renderer/Renderer.ts index 7e34fceb..e1eceb1f 100644 --- a/src/engine/renderer/Renderer.ts +++ b/src/engine/renderer/Renderer.ts @@ -4,8 +4,8 @@ import { PCFShadowMap, PCFSoftShadowMap, SRGBColorSpace, - WebGLRenderer, -} from 'three'; + WebGPURenderer, +} from 'three/webgpu'; import { DIVEScene } from '../scene/Scene.ts'; import { DIVEPerspectiveCamera } from '../camera/PerspectiveCamera.ts'; import { DIVEEnvironment } from '../environment/Environment.ts'; @@ -94,8 +94,9 @@ export const DIVERendererDefaultSettings: Required = { export class DIVERenderer { public readonly isDIVERenderer: true = true; - private _webglrenderer: WebGLRenderer; + private _webgpurenderer: WebGPURenderer; private _environment: DIVEEnvironment; + private _initPromise: Promise | null = null; private _settings: DIVERendererSettings; @@ -109,16 +110,16 @@ export class DIVERenderer { ...(settings ?? {}), }; - this._webglrenderer = this._createWebGLRenderer(); + this._webgpurenderer = this._createWebGPURenderer(); this._environment = new DIVEEnvironment( - this._webglrenderer, + this._webgpurenderer, this._scene, ); } - public get webglrenderer(): WebGLRenderer { - return this._webglrenderer; + public get webgpurenderer(): WebGPURenderer { + return this._webgpurenderer; } public get environment(): DIVEEnvironment { @@ -126,41 +127,78 @@ export class DIVERenderer { } public get canvas(): HTMLCanvasElement { - return this._webglrenderer.domElement; + return this._webgpurenderer.domElement; + } + + public get initialized(): boolean { + return this._webgpurenderer.initialized; + } + + public async init(): Promise { + if (this._webgpurenderer.initialized) { + await this._environment.init(); + return; + } + + if (this._initPromise) { + return this._initPromise; + } + + const renderer = this._webgpurenderer; + this._initPromise = (async () => { + await renderer.init(); + + if (renderer !== this._webgpurenderer) { + return; + } + + await this._environment.init(); + })().finally(() => { + if (renderer === this._webgpurenderer) { + this._initPromise = null; + } + }); + + return this._initPromise; } public render(): void { - this._webglrenderer.render(this._scene, this._camera); + if (!this._webgpurenderer.initialized) return; + + this._webgpurenderer.render(this._scene, this._camera); } public onResize(width: number, height: number): void { - this._webglrenderer.setSize(width, height); + this._webgpurenderer.setSize(width, height); } public dispose(): void { this._environment.dispose(); - this._webglrenderer.dispose(); + this._webgpurenderer.dispose(); } public setCanvas(canvas: HTMLCanvasElement): void { - // dispose old renderer and hdr environment - this._webglrenderer.dispose(); + const previousRenderer = this._webgpurenderer; + const shouldReinitialize = + previousRenderer.initialized || this._initPromise !== null; + + this._initPromise = null; // create new renderer with canvas this._settings.canvas = canvas; - this._webglrenderer = this._createWebGLRenderer(); + this._webgpurenderer = this._createWebGPURenderer(); - this._environment.setRenderer(this._webglrenderer); - } + this._environment.setRenderer(this._webgpurenderer); + previousRenderer.dispose(); - private _createWebGLRenderer(): WebGLRenderer { - // reset GL state - const gl = this._settings.canvas?.getContext('webgl2'); - gl?.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); - gl?.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false); + if (shouldReinitialize) { + void this.init(); + } + } + private _createWebGPURenderer(): WebGPURenderer { // create new renderer - const renderer = new WebGLRenderer(this._settings); + const renderer = new WebGPURenderer(this._settings); renderer.shadowMap.enabled = this._settings.shadows; renderer.shadowMap.type = this._settings.shadowQuality === 'high' @@ -177,8 +215,3 @@ export class DIVERenderer { return renderer; } } - -/** - * @deprecated Use `import { DIVERenderer } from '@shopware-ag/dive'` instead. - */ -export const DIVERenderPipeline = DIVERenderer; diff --git a/src/engine/renderer/__test__/Renderer.test.ts b/src/engine/renderer/__test__/Renderer.test.ts index a20fbb14..ebdd57c4 100644 --- a/src/engine/renderer/__test__/Renderer.test.ts +++ b/src/engine/renderer/__test__/Renderer.test.ts @@ -2,81 +2,84 @@ * @jest-environment jsdom */ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + BasicShadowMap, + LinearToneMapping, + PCFShadowMap, + PCFSoftShadowMap, + SRGBColorSpace, + WebGPURenderer as WebGPURendererOriginal, +} from 'three/webgpu'; +import { DIVEEnvironment } from '../../environment/Environment.ts'; import { DIVERenderer, DIVERendererDefaultSettings } from '../Renderer.ts'; -import { DIVEScene } from '../../scene/Scene.ts'; -import { DIVEPerspectiveCamera } from '../../camera/PerspectiveCamera.ts'; -import { vi } from 'vitest'; -import { WebGLRenderer as WebGLRendererOriginal } from 'three'; -vi.mock('three', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('three/webgpu', async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, - WebGLRenderer: vi.fn(function (this: { - domElement: { - clientWidth: number; - clientHeight: number; - style: { position: string }; - parentElement: typeof this.domElement; - }; - setSize: ReturnType; - render: ReturnType; - compile: ReturnType; - dispose: ReturnType; - setPixelRatio: ReturnType; - setAnimationLoop: ReturnType; - shadowMap: { enabled: boolean; type: number }; - debug: { checkShaderErrors: boolean }; - }) { - const dom = { - clientWidth: 800, - clientHeight: 600, - style: { position: 'absolute' }, - } as typeof this.domElement; - dom.parentElement = dom; - this.domElement = dom; + WebGPURenderer: vi.fn(function (this: any, settings: any = {}) { + const domElement = + settings.canvas ?? + ({ + clientWidth: 800, + clientHeight: 600, + style: { position: 'absolute' }, + } as HTMLCanvasElement); + + if (!settings.canvas) { + (domElement as any).parentElement = domElement; + } + + this.domElement = domElement; + this.settings = settings; + this.initialized = false; this.setSize = vi.fn(); this.setPixelRatio = vi.fn(); this.render = vi.fn(); - this.compile = vi.fn(); - this.setAnimationLoop = vi.fn(); - this.shadowMap = { enabled: false, type: 0 }; - this.debug = { checkShaderErrors: true }; this.dispose = vi.fn(); + this.init = vi.fn(async () => { + this.initialized = true; + }); + this.shadowMap = { enabled: false, type: 0 }; + this.outputColorSpace = actual.LinearSRGBColorSpace; + this.toneMapping = actual.NoToneMapping; + this.toneMappingExposure = 0; return this; }), }; }); vi.mock('../../environment/Environment.ts', () => ({ - DIVEEnvironment: vi.fn(function (this: { - dispose: ReturnType; - setRenderer: ReturnType; - }) { + DIVEEnvironment: vi.fn(function (this: any) { this.dispose = vi.fn(); this.setRenderer = vi.fn(); + this.init = vi.fn(async () => {}); return this; }), })); -// cast to any so we can call .mock on the mocked WebGLRenderer -const WebGLRenderer = WebGLRendererOriginal as any; +const WebGPURenderer = WebGPURendererOriginal as any; +const MockedDIVEEnvironment = DIVEEnvironment as any; describe('DIVERenderPipeline', () => { let renderer: DIVERenderer; - let scene: DIVEScene; - let camera: DIVEPerspectiveCamera; + let scene: any; + let camera: any; beforeEach(() => { vi.clearAllMocks(); - scene = new DIVEScene(); - camera = new DIVEPerspectiveCamera(); + scene = { isScene: true }; + camera = { isCamera: true }; renderer = new DIVERenderer(scene, camera); }); it('should instantiate with default settings', () => { + const instance = WebGPURenderer.mock.results[0].value; + expect(renderer).toBeDefined(); - expect(WebGLRenderer).toHaveBeenCalledWith( + expect(WebGPURenderer).toHaveBeenCalledWith( expect.objectContaining({ antialias: DIVERendererDefaultSettings.antialias, alpha: DIVERendererDefaultSettings.alpha, @@ -86,8 +89,18 @@ describe('DIVERenderPipeline', () => { depth: DIVERendererDefaultSettings.depth, logarithmicDepthBuffer: DIVERendererDefaultSettings.logarithmicDepthBuffer, + shadows: DIVERendererDefaultSettings.shadows, + shadowQuality: DIVERendererDefaultSettings.shadowQuality, }), ); + expect(instance.shadowMap.enabled).toBe(true); + expect(instance.shadowMap.type).toBe(PCFSoftShadowMap); + expect(instance.setPixelRatio).toHaveBeenCalledWith( + window.devicePixelRatio, + ); + expect(instance.outputColorSpace).toBe(SRGBColorSpace); + expect(instance.toneMapping).toBe(LinearToneMapping); + expect(MockedDIVEEnvironment).toHaveBeenCalledWith(instance, scene); }); it('should instantiate with custom settings', () => { @@ -98,47 +111,196 @@ describe('DIVERenderPipeline', () => { precision: 'mediump' as const, stencil: true, depth: false, - logarithmicDepthBuffer: true, + logarithmicDepthBuffer: false, + shadows: false, + shadowQuality: 'low' as const, }; + renderer = new DIVERenderer(scene, camera, customSettings); - expect(WebGLRenderer).toHaveBeenCalledWith( + const instance = WebGPURenderer.mock.results.at(-1)?.value; + + expect(WebGPURenderer).toHaveBeenLastCalledWith( expect.objectContaining(customSettings), ); + expect(instance.shadowMap.enabled).toBe(false); + expect(instance.shadowMap.type).toBe(BasicShadowMap); }); - it('should provide access to domElement', () => { - expect(renderer.webglrenderer.domElement).toBeDefined(); + it('should expose the current renderer and canvas', () => { + const instance = WebGPURenderer.mock.results[0].value; + + expect(renderer.webgpurenderer).toBe(instance); + expect(renderer.canvas).toBe(instance.domElement); }); - it('should set domElement', () => { - const newCanvas = document.createElement('canvas'); - renderer.webglrenderer.domElement = newCanvas; - const mockInstance = WebGLRenderer.mock.results[0].value; - expect(mockInstance.domElement).toBe(newCanvas); + it('should expose the environment instance', () => { + const environment = MockedDIVEEnvironment.mock.results[0].value; + + expect(renderer.environment).toBe(environment); }); - it('should provide access to webglrenderer', () => { - const mockInstance = WebGLRenderer.mock.results[0].value; - expect(renderer.webglrenderer).toBe(mockInstance); + it('should initialize renderer and environment', async () => { + const instance = WebGPURenderer.mock.results[0].value; + const environment = MockedDIVEEnvironment.mock.results[0].value; + + await renderer.init(); + + expect(instance.init).toHaveBeenCalled(); + expect(environment.init).toHaveBeenCalled(); + expect(renderer.initialized).toBe(true); }); - it('should render scene and camera', () => { + it('should initialize the environment immediately when the renderer is already initialized', async () => { + const instance = WebGPURenderer.mock.results[0].value; + const environment = MockedDIVEEnvironment.mock.results[0].value; + + instance.initialized = true; + + await renderer.init(); + + expect(instance.init).not.toHaveBeenCalled(); + expect(environment.init).toHaveBeenCalledTimes(1); + }); + + it('should reuse the pending init when init is called twice concurrently', async () => { + const instance = WebGPURenderer.mock.results[0].value; + const environment = MockedDIVEEnvironment.mock.results[0].value; + let resolveInit: (() => void) | undefined; + + instance.init = vi.fn( + () => + new Promise((resolve) => { + resolveInit = () => { + instance.initialized = true; + resolve(); + }; + }), + ); + + const firstInit = renderer.init(); + const secondInit = renderer.init(); + + expect(instance.init).toHaveBeenCalledTimes(1); + expect(environment.init).not.toHaveBeenCalled(); + + resolveInit?.(); + await Promise.all([ + firstInit, + secondInit, + ]); + + expect(environment.init).toHaveBeenCalledTimes(1); + }); + + it('should render only after initialization', () => { + const instance = WebGPURenderer.mock.results[0].value; + + renderer.render(); + expect(instance.render).not.toHaveBeenCalled(); + + instance.initialized = true; renderer.render(); - const mockInstance = WebGLRenderer.mock.results[0].value; - expect(mockInstance.render).toHaveBeenCalledWith(scene, camera); + expect(instance.render).toHaveBeenCalledWith(scene, camera); }); it('should handle resize', () => { - const width = 800; - const height = 600; - renderer.onResize(width, height); - const mockInstance = WebGLRenderer.mock.results[0].value; - expect(mockInstance.setSize).toHaveBeenCalledWith(width, height); + const instance = WebGPURenderer.mock.results[0].value; + + renderer.onResize(800, 600); + + expect(instance.setSize).toHaveBeenCalledWith(800, 600); }); - it('should dispose WebGLRenderer', () => { + it('should recreate the renderer when setting a canvas', () => { + const firstInstance = WebGPURenderer.mock.results[0].value; + const environment = MockedDIVEEnvironment.mock.results[0].value; + const newCanvas = document.createElement('canvas'); + + renderer.setCanvas(newCanvas); + + const secondInstance = WebGPURenderer.mock.results.at(-1)?.value; + expect(firstInstance.dispose).toHaveBeenCalled(); + expect(WebGPURenderer).toHaveBeenLastCalledWith( + expect.objectContaining({ canvas: newCanvas }), + ); + expect(environment.setRenderer).toHaveBeenCalledWith(secondInstance); + expect(renderer.canvas).toBe(secondInstance.domElement); + }); + + it('should swap the environment to the new renderer before disposing the previous renderer', () => { + const firstInstance = WebGPURenderer.mock.results[0].value; + const environment = MockedDIVEEnvironment.mock.results[0].value; + const newCanvas = document.createElement('canvas'); + + renderer.setCanvas(newCanvas); + + expect(environment.setRenderer).toHaveBeenCalledTimes(1); + expect(firstInstance.dispose).toHaveBeenCalledTimes(1); + expect( + environment.setRenderer.mock.invocationCallOrder[0], + ).toBeLessThan(firstInstance.dispose.mock.invocationCallOrder[0]); + }); + + it('should reinitialize after canvas swap when the previous renderer was active', () => { + const firstInstance = WebGPURenderer.mock.results[0].value; + const newCanvas = document.createElement('canvas'); + + firstInstance.initialized = true; + + renderer.setCanvas(newCanvas); + + const secondInstance = WebGPURenderer.mock.results.at(-1)?.value; + expect(secondInstance.init).toHaveBeenCalled(); + }); + + it('should ignore stale init completions after a canvas swap', async () => { + const firstInstance = WebGPURenderer.mock.results[0].value; + const environment = MockedDIVEEnvironment.mock.results[0].value; + const newCanvas = document.createElement('canvas'); + let resolveFirstInit: (() => void) | undefined; + + firstInstance.init = vi.fn( + () => + new Promise((resolve) => { + resolveFirstInit = () => { + firstInstance.initialized = true; + resolve(); + }; + }), + ); + + const pendingInit = renderer.init(); + renderer.setCanvas(newCanvas); + + const secondInstance = WebGPURenderer.mock.results.at(-1)?.value; + expect(secondInstance.init).toHaveBeenCalledTimes(1); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(environment.init).toHaveBeenCalledTimes(1); + + resolveFirstInit?.(); + await pendingInit; + + expect(environment.init).toHaveBeenCalledTimes(1); + }); + + it('should dispose environment and renderer', () => { + const instance = WebGPURenderer.mock.results[0].value; + const environment = MockedDIVEEnvironment.mock.results[0].value; + renderer.dispose(); - const mockInstance = WebGLRenderer.mock.results[0].value; - expect(mockInstance.dispose).toHaveBeenCalled(); + + expect(environment.dispose).toHaveBeenCalled(); + expect(instance.dispose).toHaveBeenCalled(); + }); + + it('should map medium shadow quality to PCFShadowMap', () => { + renderer = new DIVERenderer(scene, camera, { + shadowQuality: 'medium', + }); + + const instance = WebGPURenderer.mock.results.at(-1)?.value; + + expect(instance.shadowMap.type).toBe(PCFShadowMap); }); }); diff --git a/src/engine/scene/Scene.ts b/src/engine/scene/Scene.ts index 919c200c..498c1470 100644 --- a/src/engine/scene/Scene.ts +++ b/src/engine/scene/Scene.ts @@ -1,4 +1,9 @@ -import { Color, Scene, type Box3, type ColorRepresentation } from 'three'; +import { + Color, + Scene, + type Box3, + type ColorRepresentation, +} from 'three/webgpu'; import { DIVERoot } from '../../components/root/Root.ts'; import { DIVEGrid } from '../../components/grid/Grid.ts'; diff --git a/src/engine/scene/__test__/Scene.test.ts b/src/engine/scene/__test__/Scene.test.ts index b0130191..b6f6f5bf 100644 --- a/src/engine/scene/__test__/Scene.test.ts +++ b/src/engine/scene/__test__/Scene.test.ts @@ -1,4 +1,4 @@ -import { Color } from 'three'; +import { Color } from 'three/webgpu'; import { DIVEScene, DIVESceneDefaultSettings } from '../Scene.ts'; const mock_GetSceneObject = vi.fn(); diff --git a/src/engine/scene/xrroot/XRRoot.ts b/src/engine/scene/xrroot/XRRoot.ts index ba80fb85..346a86a6 100644 --- a/src/engine/scene/xrroot/XRRoot.ts +++ b/src/engine/scene/xrroot/XRRoot.ts @@ -1,6 +1,5 @@ -import { Mesh, Object3D, PlaneGeometry, ShadowMaterial } from 'three'; +import { Mesh, Object3D, PlaneGeometry, ShadowMaterial } from 'three/webgpu'; import { DIVERoot } from '../../../components/root/Root.ts'; -import { type DIVERenderer } from '../../renderer/Renderer.ts'; import { DIVEXRLightRoot } from './xrlightroot/XRLightRoot.ts'; import { type DIVEScene } from '../Scene.ts'; @@ -46,8 +45,8 @@ export class DIVEXRRoot extends Object3D { this.add(this._xrHandNode); } - public initLightEstimation(renderer: DIVERenderer): void { - this._xrLightRoot.initLightEstimation(renderer); + public initLightEstimation(): void { + this._xrLightRoot.initLightEstimation(); } public disposeLightEstimation(): void { diff --git a/src/engine/scene/xrroot/xrlightroot/XRLightRoot.ts b/src/engine/scene/xrroot/xrlightroot/XRLightRoot.ts index da0c8916..d17b1aae 100644 --- a/src/engine/scene/xrroot/xrlightroot/XRLightRoot.ts +++ b/src/engine/scene/xrroot/xrlightroot/XRLightRoot.ts @@ -1,8 +1,6 @@ import { XREstimatedLight } from 'three/examples/jsm/webxr/XREstimatedLight.ts'; -import { type DIVERenderer } from '../../../renderer/Renderer.ts'; -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; import { type DIVEScene } from '../../Scene.ts'; -import { PRODUCT_LAYER_MASK } from '../../../../constants/VisibilityLayerMask.ts'; import { DIVERoot } from '../../../../components/root/Root.ts'; export class DIVEXRLightRoot extends Object3D { @@ -10,6 +8,7 @@ export class DIVEXRLightRoot extends Object3D { private _xrLight: XREstimatedLight | null; private _lightRoot: DIVERoot; + private _warnedUnsupported = false; constructor(scene: DIVEScene) { super(); @@ -35,19 +34,13 @@ export class DIVEXRLightRoot extends Object3D { this.add(this._lightRoot); } - public initLightEstimation(renderer: DIVERenderer): void { - if (!this._xrLight) { - this._xrLight = new XREstimatedLight(renderer.webglrenderer, true); - this._xrLight.layers.mask = PRODUCT_LAYER_MASK; - this.add(this._xrLight); + public initLightEstimation(): void { + if (!this._warnedUnsupported) { + console.warn( + 'DIVEXRLightRoot.initLightEstimation: XREstimatedLight is not supported with WebGPURenderer yet. Falling back to the scene light.', + ); + this._warnedUnsupported = true; } - - this._xrLight.addEventListener('estimationstart', () => { - this.onEstimationStart(); - }); - this._xrLight.addEventListener('estimationend', () => { - this.onEstimationEnd(); - }); } public disposeLightEstimation(): void { diff --git a/src/engine/view/View.ts b/src/engine/view/View.ts index 0fcb3b91..439a5272 100644 --- a/src/engine/view/View.ts +++ b/src/engine/view/View.ts @@ -1,4 +1,4 @@ -import { MathUtils } from 'three'; +import { MathUtils } from 'three/webgpu'; import { DIVETicker } from '../clock/Clock.ts'; import { DIVEPerspectiveCamera } from '../camera/PerspectiveCamera.ts'; import { DIVERenderer } from '../renderer/Renderer.ts'; diff --git a/src/engine/view/__test__/View.test.ts b/src/engine/view/__test__/View.test.ts index 8d5d2c97..15051e87 100644 --- a/src/engine/view/__test__/View.test.ts +++ b/src/engine/view/__test__/View.test.ts @@ -1,6 +1,6 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -vi.mock('three', async (importOriginal) => { +vi.mock('three/webgpu', async (importOriginal) => { const actual = await importOriginal(); return { ...actual }; }); diff --git a/src/helpers/deepClone/__test__/deepClone.test.ts b/src/helpers/deepClone/__test__/deepClone.test.ts index 3c61ba56..107a957f 100644 --- a/src/helpers/deepClone/__test__/deepClone.test.ts +++ b/src/helpers/deepClone/__test__/deepClone.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi } from 'vitest'; import { deepClone } from '../deepClone.ts'; -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; describe('deepClone', () => { describe('primitive types', () => { diff --git a/src/helpers/findInterface/__test__/findInterface.test.ts b/src/helpers/findInterface/__test__/findInterface.test.ts index 8b96d7cd..95094400 100644 --- a/src/helpers/findInterface/__test__/findInterface.test.ts +++ b/src/helpers/findInterface/__test__/findInterface.test.ts @@ -1,4 +1,4 @@ -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; import { findInterface } from '../findInterface.ts'; describe('dive/helper/findInterface', () => { diff --git a/src/helpers/findInterface/findInterface.ts b/src/helpers/findInterface/findInterface.ts index d41e8f82..534f45fa 100644 --- a/src/helpers/findInterface/findInterface.ts +++ b/src/helpers/findInterface/findInterface.ts @@ -1,5 +1,5 @@ import { implementsInterface } from '../implementsInterface/implementsInterface.ts'; -import { type Object3D } from 'three'; +import { type Object3D } from 'three/webgpu'; export function findInterface( object: Object3D | null | undefined, diff --git a/src/helpers/findSceneRecursive/__test__/findSceneRecursive.test.ts b/src/helpers/findSceneRecursive/__test__/findSceneRecursive.test.ts index 8b95b1a3..00101599 100644 --- a/src/helpers/findSceneRecursive/__test__/findSceneRecursive.test.ts +++ b/src/helpers/findSceneRecursive/__test__/findSceneRecursive.test.ts @@ -1,4 +1,4 @@ -import type { Object3D } from 'three'; +import type { Object3D } from 'three/webgpu'; import { findSceneRecursive } from '../findSceneRecursive.ts'; describe('dive/helper/findSceneRecursive', () => { diff --git a/src/helpers/findSceneRecursive/findSceneRecursive.ts b/src/helpers/findSceneRecursive/findSceneRecursive.ts index 341906cd..68419d9a 100644 --- a/src/helpers/findSceneRecursive/findSceneRecursive.ts +++ b/src/helpers/findSceneRecursive/findSceneRecursive.ts @@ -1,4 +1,4 @@ -import { type Object3D } from 'three'; +import { type Object3D } from 'three/webgpu'; import { type DIVEScene } from '../../engine/scene/Scene.ts'; /** diff --git a/src/helpers/implementsInterface/__test__/implementsInterface.test.ts b/src/helpers/implementsInterface/__test__/implementsInterface.test.ts index 87a22d33..3246ef78 100644 --- a/src/helpers/implementsInterface/__test__/implementsInterface.test.ts +++ b/src/helpers/implementsInterface/__test__/implementsInterface.test.ts @@ -1,4 +1,4 @@ -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; import { implementsInterface } from '../implementsInterface.ts'; describe('dive/helper/implementsInterface', () => { diff --git a/src/helpers/implementsInterface/implementsInterface.ts b/src/helpers/implementsInterface/implementsInterface.ts index 048d1736..65c64d77 100644 --- a/src/helpers/implementsInterface/implementsInterface.ts +++ b/src/helpers/implementsInterface/implementsInterface.ts @@ -1,4 +1,4 @@ -import { type Object3D } from 'three'; +import { type Object3D } from 'three/webgpu'; export function implementsInterface( object: Object3D | null | undefined, diff --git a/src/helpers/math/degToRad/__test__/degToRad.test.ts b/src/helpers/math/degToRad/__test__/degToRad.test.ts index 8a601c1c..454d5e4d 100644 --- a/src/helpers/math/degToRad/__test__/degToRad.test.ts +++ b/src/helpers/math/degToRad/__test__/degToRad.test.ts @@ -1,8 +1,9 @@ import degToRad from '../degToRad'; -import { MathUtils } from 'three'; +import { MathUtils } from 'three/webgpu'; -vi.mock('three', async () => { - const actual = await vi.importActual('three'); +vi.mock('three/webgpu', async () => { + const actual = + await vi.importActual('three/webgpu'); return { ...actual, @@ -14,7 +15,7 @@ vi.mock('three', async () => { }); // Type assertion for the mocked MathUtils.degToRad -const mockedDegToRad = MathUtils.degToRad as vi.Mock; +const mockedDegToRad = vi.mocked(MathUtils.degToRad); /** * Test Suite for degToRad Function diff --git a/src/helpers/math/degToRad/degToRad.ts b/src/helpers/math/degToRad/degToRad.ts index 0b36cb1d..ad659f0e 100644 --- a/src/helpers/math/degToRad/degToRad.ts +++ b/src/helpers/math/degToRad/degToRad.ts @@ -1,4 +1,4 @@ -import { MathUtils } from 'three'; +import { MathUtils } from 'three/webgpu'; export default function degToRad(degrees: number): number { return MathUtils.degToRad(degrees); diff --git a/src/helpers/math/radToDeg/__test__/radToDeg.test.ts b/src/helpers/math/radToDeg/__test__/radToDeg.test.ts index c461a986..ac7c7dc3 100644 --- a/src/helpers/math/radToDeg/__test__/radToDeg.test.ts +++ b/src/helpers/math/radToDeg/__test__/radToDeg.test.ts @@ -1,8 +1,9 @@ import radToDeg from '../radToDeg'; -import { MathUtils } from 'three'; +import { MathUtils } from 'three/webgpu'; -vi.mock('three', async () => { - const actual = await vi.importActual('three'); +vi.mock('three/webgpu', async () => { + const actual = + await vi.importActual('three/webgpu'); return { ...actual, @@ -14,7 +15,7 @@ vi.mock('three', async () => { }); // Type assertion for the mocked MathUtils.radToDeg -const mockedRadToDeg = MathUtils.radToDeg as vi.Mock; +const mockedRadToDeg = vi.mocked(MathUtils.radToDeg); /** * Test Suite for radToDeg Function diff --git a/src/helpers/math/radToDeg/radToDeg.ts b/src/helpers/math/radToDeg/radToDeg.ts index b7be4082..41d11ed1 100644 --- a/src/helpers/math/radToDeg/radToDeg.ts +++ b/src/helpers/math/radToDeg/radToDeg.ts @@ -1,4 +1,4 @@ -import { MathUtils } from 'three'; +import { MathUtils } from 'three/webgpu'; export default function radToDeg(radians: number): number { return (MathUtils.radToDeg(radians) + 360) % 360; diff --git a/src/helpers/math/signedAngleTo/__test__/signedAngleTo.test.ts b/src/helpers/math/signedAngleTo/__test__/signedAngleTo.test.ts index 8c73deb2..3b24934a 100644 --- a/src/helpers/math/signedAngleTo/__test__/signedAngleTo.test.ts +++ b/src/helpers/math/signedAngleTo/__test__/signedAngleTo.test.ts @@ -1,4 +1,4 @@ -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; import signedAngleTo from '../signedAngleTo'; describe('dive/math/signedAngleTo', () => { diff --git a/src/helpers/math/signedAngleTo/signedAngleTo.ts b/src/helpers/math/signedAngleTo/signedAngleTo.ts index ea6fd770..b4caa77d 100644 --- a/src/helpers/math/signedAngleTo/signedAngleTo.ts +++ b/src/helpers/math/signedAngleTo/signedAngleTo.ts @@ -1,4 +1,4 @@ -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; /** * Calculate the signed angle between two vectors. Only works when the vectors are on the same plane. diff --git a/src/interfaces/Hoverable.ts b/src/interfaces/Hoverable.ts index d2e5236a..c79050d1 100644 --- a/src/interfaces/Hoverable.ts +++ b/src/interfaces/Hoverable.ts @@ -4,7 +4,7 @@ * @module */ -import { type Intersection } from 'three'; +import { type Intersection } from 'three/webgpu'; export interface DIVEHoverable { isHoverable: true; diff --git a/src/plugins/animation/src/animator/Animator.ts b/src/plugins/animation/src/animator/Animator.ts index f8593a80..e77bc535 100644 --- a/src/plugins/animation/src/animator/Animator.ts +++ b/src/plugins/animation/src/animator/Animator.ts @@ -1,5 +1,5 @@ -import { MathUtils } from 'three'; -import { EventDispatcher } from 'three/src/core/EventDispatcher.js'; +import { MathUtils } from 'three/webgpu'; +import { EventDispatcher } from 'three/webgpu'; import { TAnimatorEventMap, TAnimatorLoopMode, diff --git a/src/plugins/animation/src/animator/ClipAnimator.ts b/src/plugins/animation/src/animator/ClipAnimator.ts index 97cc3c3d..9ff0173a 100644 --- a/src/plugins/animation/src/animator/ClipAnimator.ts +++ b/src/plugins/animation/src/animator/ClipAnimator.ts @@ -6,7 +6,7 @@ import { LoopRepeat, LoopPingPong, Object3D, -} from 'three'; +} from 'three/webgpu'; import { Animator } from './Animator.ts'; import { TAnimatorLoopMode, TAnimatorState } from '../types/AnimatorTypes.ts'; diff --git a/src/plugins/animation/src/animator/__test__/ClipAnimator.test.ts b/src/plugins/animation/src/animator/__test__/ClipAnimator.test.ts index 2eaec252..7fa7827c 100644 --- a/src/plugins/animation/src/animator/__test__/ClipAnimator.test.ts +++ b/src/plugins/animation/src/animator/__test__/ClipAnimator.test.ts @@ -1,6 +1,6 @@ import { ClipAnimator } from '../ClipAnimator.ts'; -vi.mock('three', async (importOriginal) => { +vi.mock('three/webgpu', async (importOriginal) => { const actual = (await importOriginal()) as any; return { ...actual, diff --git a/src/plugins/animation/src/system/AnimationSystem.ts b/src/plugins/animation/src/system/AnimationSystem.ts index 498d73ef..9c5229f1 100644 --- a/src/plugins/animation/src/system/AnimationSystem.ts +++ b/src/plugins/animation/src/system/AnimationSystem.ts @@ -1,5 +1,5 @@ import { Easing as TWEENEasing } from '@tweenjs/tween.js'; -import { AnimationClip, MathUtils, Object3D } from 'three'; +import { AnimationClip, MathUtils, Object3D } from 'three/webgpu'; import { DIVETicker } from '@shopware-ag/dive'; type Animator = import('../animator/Animator.ts').Animator; @@ -37,20 +37,6 @@ export class AnimationSystem implements DIVETicker { } } - /** - * @deprecated Use `fromTargets()` instead. - * @note This method also calls .play() on the animator automatically. This has been removed in fromTargets(). You have to call .play() independently after creating the animator. - */ - public async animate( - targets: AnimationTarget | AnimationTarget[], - duration: number, - options?: TargetAnimatorOptions, - ): Promise { - const animator = await this.fromTargets(targets, duration, options); - animator.play(); - return animator; - } - /** * Creates a TargetAnimator and returns it asynchronously. * diff --git a/src/plugins/animation/src/system/__test__/AnimationSystem.test.ts b/src/plugins/animation/src/system/__test__/AnimationSystem.test.ts index fb0d80df..d45a3174 100644 --- a/src/plugins/animation/src/system/__test__/AnimationSystem.test.ts +++ b/src/plugins/animation/src/system/__test__/AnimationSystem.test.ts @@ -71,7 +71,7 @@ vi.mock('@tweenjs/tween.js', () => { }; }); -vi.mock('three', async (importOriginal) => { +vi.mock('three/webgpu', async (importOriginal) => { const actual = (await importOriginal()) as any; return { ...actual, @@ -178,27 +178,6 @@ describe('AnimationSystem', () => { }); }); - describe('animate()', () => { - it('should create and auto-play a TargetAnimator', async () => { - const animator = await animationSystem.animate( - [{ object: { x: 0 }, to: { x: 100 } }], - 1000, - ); - expect(animator).toBeDefined(); - expect(animator.state).toBe('playing'); - }); - - it('should register the animator internally', async () => { - const animator = await animationSystem.animate( - [{ object: { x: 0 }, to: { x: 100 } }], - 1000, - ); - expect( - animationSystem['_animators'].has(animator.uuid), - ).toBeTruthy(); - }); - }); - describe('ClipAnimator Creation', () => { it('should create a ClipAnimator via fromClips()', async () => { const root = {}; diff --git a/src/plugins/animation/src/types/AnimatorTypes.ts b/src/plugins/animation/src/types/AnimatorTypes.ts index a8bf8dad..082265c9 100644 --- a/src/plugins/animation/src/types/AnimatorTypes.ts +++ b/src/plugins/animation/src/types/AnimatorTypes.ts @@ -1,4 +1,4 @@ -import { Event } from 'three/src/core/EventDispatcher.js'; +import { Event } from 'three/webgpu'; export type TAnimatorState = 'idle' | 'playing' | 'paused'; diff --git a/src/plugins/ar/src/__test__/ARSystem.test.ts b/src/plugins/ar/src/__test__/ARSystem.test.ts index c2d5df02..9efde5b7 100644 --- a/src/plugins/ar/src/__test__/ARSystem.test.ts +++ b/src/plugins/ar/src/__test__/ARSystem.test.ts @@ -1,10 +1,3 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - import { ARSystem, type ARSystemOptions } from '../ARSystem.ts'; import { SystemInfo, ESystem } from '@shopware-ag/dive/systeminfo'; // This will be mocked import { ARQuickLook } from '../arquicklook/ARQuickLook.ts'; // This will be mocked diff --git a/src/plugins/ar/src/arquicklook/__test__/ARQuickLook.test.ts b/src/plugins/ar/src/arquicklook/__test__/ARQuickLook.test.ts index be3ef63f..a09807fa 100644 --- a/src/plugins/ar/src/arquicklook/__test__/ARQuickLook.test.ts +++ b/src/plugins/ar/src/arquicklook/__test__/ARQuickLook.test.ts @@ -1,4 +1,3 @@ -import { Object3D } from 'three'; import { type ARSystemOptions } from '../../ARSystem.ts'; import { ARQuickLook } from '../ARQuickLook.ts'; import { AssetConverter } from '@shopware-ag/dive/assetconverter'; diff --git a/src/plugins/ar/src/webxr/WebXR.ts b/src/plugins/ar/src/webxr/WebXR.ts index e48586e9..199f2090 100644 --- a/src/plugins/ar/src/webxr/WebXR.ts +++ b/src/plugins/ar/src/webxr/WebXR.ts @@ -1,4 +1,4 @@ -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; import { type DIVERenderer, type DIVEScene, DIVE } from '@shopware-ag/dive'; import { Overlay } from './overlay/Overlay.ts'; @@ -21,7 +21,7 @@ export class DIVEWebXR { private static _session: XRSession | null = null; private static _referenceSpaceType: XRReferenceSpaceType = 'local'; private static _overlay: Overlay | null = null; - private static _options = { + private static _options: XRSessionInit = { requiredFeatures: [ 'local', 'hit-test', @@ -34,7 +34,7 @@ export class DIVEWebXR { ], depthSensing: { usagePreference: ['gpu-optimized'], - dataFormatPreference: [], + dataFormatPreference: [] as XRDepthDataFormat[], }, domOverlay: { root: {} as HTMLElement }, }; @@ -59,7 +59,7 @@ export class DIVEWebXR { } // setup current instance - this._renderer.webglrenderer.xr.enabled = true; + this._renderer.webgpurenderer.xr.enabled = true; // this._scene.InitXR(renderer); // creating overlay @@ -80,10 +80,10 @@ export class DIVEWebXR { }); // build up session - this._renderer.webglrenderer.xr.setReferenceSpaceType( + this._renderer.webgpurenderer.xr.setReferenceSpaceType( this._referenceSpaceType, ); - await this._renderer.webglrenderer.xr.setSession(session); + await this._renderer.webgpurenderer.xr.setSession(session); DIVEWebXR._overlay.element.style.display = ''; this._session = session; @@ -147,11 +147,11 @@ export class DIVEWebXR { } // disable XR on renderer to restore canvas rendering - this._renderer.webglrenderer.xr.enabled = false; + this._renderer.webgpurenderer.xr.enabled = false; // resize renderer const canvasWrapper = - this._renderer.webglrenderer.domElement.parentElement; + this._renderer.webgpurenderer.domElement.parentElement; if (canvasWrapper) { const { clientWidth, clientHeight } = canvasWrapper; this._renderer.onResize(clientWidth, clientHeight); diff --git a/src/plugins/ar/src/webxr/controller/WebXRController.ts b/src/plugins/ar/src/webxr/controller/WebXRController.ts index 48e9f591..f8239015 100644 --- a/src/plugins/ar/src/webxr/controller/WebXRController.ts +++ b/src/plugins/ar/src/webxr/controller/WebXRController.ts @@ -4,8 +4,8 @@ import { Object3D, Quaternion, Vector3, - WebXRArrayCamera, -} from 'three'; + ArrayCamera, +} from 'three/webgpu'; import { DIVERenderer } from '../../../../../engine/renderer/Renderer.ts'; import { DIVEScene } from '../../../../../engine/scene/Scene.ts'; import { DIVEWebXRCrosshair } from '../crosshair/WebXRCrosshair.ts'; @@ -36,7 +36,7 @@ export class DIVEWebXRController extends Object3D { // controller members private _touchscreenControls: DIVEWebXRTouchscreenControls; private _handNodeInitialPosition = new Vector3(); - private _xrCamera: WebXRArrayCamera; + private _xrCamera: ArrayCamera; private _placed: boolean = false; // grabbing @@ -72,7 +72,7 @@ export class DIVEWebXRController extends Object3D { this._crosshair = new DIVEWebXRCrosshair(); this._crosshair.visible = false; - this._xrCamera = this._renderer.webglrenderer.xr.getCamera(); + this._xrCamera = this._renderer.webgpurenderer.xr.getCamera(); // this._scene.XRRoot.XRHandNode.position.set(0, -0.05, -0.25); // this._handNodeInitialPosition = diff --git a/src/plugins/ar/src/webxr/crosshair/WebXRCrosshair.ts b/src/plugins/ar/src/webxr/crosshair/WebXRCrosshair.ts index f2ba39f1..ce998ba6 100644 --- a/src/plugins/ar/src/webxr/crosshair/WebXRCrosshair.ts +++ b/src/plugins/ar/src/webxr/crosshair/WebXRCrosshair.ts @@ -1,4 +1,4 @@ -import { Mesh, MeshBasicMaterial, Object3D, RingGeometry } from 'three'; +import { Mesh, MeshBasicMaterial, Object3D, RingGeometry } from 'three/webgpu'; export class DIVEWebXRCrosshair extends Object3D { public set mesh(mesh: Mesh | undefined) { diff --git a/src/plugins/ar/src/webxr/origin/WebXROrigin.ts b/src/plugins/ar/src/webxr/origin/WebXROrigin.ts index 37c0d8ba..a187241c 100644 --- a/src/plugins/ar/src/webxr/origin/WebXROrigin.ts +++ b/src/plugins/ar/src/webxr/origin/WebXROrigin.ts @@ -1,4 +1,4 @@ -import { Matrix4, Quaternion, Vector3 } from 'three'; +import { Matrix4, Quaternion, Vector3 } from 'three/webgpu'; import { DIVERenderer } from '../../../../../engine/renderer/Renderer.ts'; export class DIVEWebXROrigin { @@ -154,7 +154,7 @@ export class DIVEWebXROrigin { if (this._hitTestResultBuffer.length > 0) { // hit found this._referenceSpaceBuffer = - this._renderer.webglrenderer.xr.getReferenceSpace(); + this._renderer.webgpurenderer.xr.getReferenceSpace(); // if there is no reference space, hit will be counted as lost for this frame if (!this._referenceSpaceBuffer) { diff --git a/src/plugins/ar/src/webxr/raycaster/WebXRRaycaster.ts b/src/plugins/ar/src/webxr/raycaster/WebXRRaycaster.ts index c62619f1..16feb07e 100644 --- a/src/plugins/ar/src/webxr/raycaster/WebXRRaycaster.ts +++ b/src/plugins/ar/src/webxr/raycaster/WebXRRaycaster.ts @@ -1,4 +1,4 @@ -import { Matrix4, Mesh, Vector3 } from 'three'; +import { Matrix4, Mesh, Vector3 } from 'three/webgpu'; import { DIVERenderer } from '../../../../../engine/renderer/Renderer.ts'; import { DIVEWebXRRaycasterAR } from './ar/WebXRRaycasterAR.ts'; import { DIVEWebXRRaycasterTHREE } from './three/WebXRRaycasterTHREE.ts'; diff --git a/src/plugins/ar/src/webxr/raycaster/ar/WebXRRaycasterAR.ts b/src/plugins/ar/src/webxr/raycaster/ar/WebXRRaycasterAR.ts index 83832579..cdd6c11e 100644 --- a/src/plugins/ar/src/webxr/raycaster/ar/WebXRRaycasterAR.ts +++ b/src/plugins/ar/src/webxr/raycaster/ar/WebXRRaycasterAR.ts @@ -1,4 +1,4 @@ -import { Matrix4, Vector3 } from 'three'; +import { Matrix4, Vector3 } from 'three/webgpu'; import { type DIVERenderer } from '../../../../../../engine/renderer/Renderer.ts'; import { type DIVEHitResult } from '../WebXRRaycaster.ts'; @@ -56,7 +56,7 @@ export class DIVEWebXRRaycasterAR { profile: 'generic-touchscreen', }); this._referenceSpaceBuffer = - this._renderer.webglrenderer.xr.getReferenceSpace(); + this._renderer.webgpurenderer.xr.getReferenceSpace(); this._requesting = false; if (!this._transientHitTestSource) { diff --git a/src/plugins/ar/src/webxr/raycaster/three/WebXRRaycasterTHREE.ts b/src/plugins/ar/src/webxr/raycaster/three/WebXRRaycasterTHREE.ts index 2129e4cb..d731ad14 100644 --- a/src/plugins/ar/src/webxr/raycaster/three/WebXRRaycasterTHREE.ts +++ b/src/plugins/ar/src/webxr/raycaster/three/WebXRRaycasterTHREE.ts @@ -3,7 +3,7 @@ import { type Mesh, Raycaster, type XRTargetRaySpace, -} from 'three'; +} from 'three/webgpu'; import { type DIVERenderer } from '../../../../../../engine/renderer/Renderer.ts'; import { type DIVEScene } from '../../../../../../engine/scene/Scene.ts'; import { type DIVEHitResult } from '../WebXRRaycaster.ts'; @@ -21,7 +21,7 @@ export class DIVEWebXRRaycasterTHREE { this._renderer = renderer; this._scene = scene; - this._controller = this._renderer.webglrenderer.xr.getController(0); + this._controller = this._renderer.webgpurenderer.xr.getController(0); } public async init(): Promise { diff --git a/src/plugins/ar/src/webxr/touchscreencontrols/WebXRTouchscreenControls.ts b/src/plugins/ar/src/webxr/touchscreencontrols/WebXRTouchscreenControls.ts index 12a72d34..2f17dec4 100644 --- a/src/plugins/ar/src/webxr/touchscreencontrols/WebXRTouchscreenControls.ts +++ b/src/plugins/ar/src/webxr/touchscreencontrols/WebXRTouchscreenControls.ts @@ -1,4 +1,4 @@ -import { Vector2 } from 'three'; +import { Vector2 } from 'three/webgpu'; import { EventDispatcher } from '../../../../../events/index.ts'; export type DIVETouchscreenEvents = { diff --git a/src/plugins/assetcache/src/cache/__test__/AssetCache.test.ts b/src/plugins/assetcache/src/cache/__test__/AssetCache.test.ts index 89174b07..f28457fa 100644 --- a/src/plugins/assetcache/src/cache/__test__/AssetCache.test.ts +++ b/src/plugins/assetcache/src/cache/__test__/AssetCache.test.ts @@ -1,4 +1,3 @@ -import { Group } from 'three'; import { AssetCache } from '../AssetCache.ts'; import { Chunk } from '../../chunk/Chunk.ts'; diff --git a/src/plugins/assetcache/src/chunk/__test__/Chunk.test.ts b/src/plugins/assetcache/src/chunk/__test__/Chunk.test.ts index 4b118fb8..7b593816 100644 --- a/src/plugins/assetcache/src/chunk/__test__/Chunk.test.ts +++ b/src/plugins/assetcache/src/chunk/__test__/Chunk.test.ts @@ -1,10 +1,3 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - import { Chunk } from '../Chunk.ts'; import { FileContentError, NetworkError } from '@shopware-ag/dive'; diff --git a/src/plugins/assetconverter/src/__test__/AssetConverter.test.ts b/src/plugins/assetconverter/src/__test__/AssetConverter.test.ts index bc1dd6f0..05b63438 100644 --- a/src/plugins/assetconverter/src/__test__/AssetConverter.test.ts +++ b/src/plugins/assetconverter/src/__test__/AssetConverter.test.ts @@ -1,7 +1,7 @@ import { AssetConverter } from '../AssetConverter.ts'; import { AssetLoader } from '@shopware-ag/dive/assetloader'; import { AssetExporter } from '@shopware-ag/dive/assetexporter'; -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; const mockLoaderLoad = vi.fn(); vi.mock('@shopware-ag/dive/assetloader', () => { diff --git a/src/plugins/assetexporter/src/AssetExporter.ts b/src/plugins/assetexporter/src/AssetExporter.ts index 4bcae650..34529096 100644 --- a/src/plugins/assetexporter/src/AssetExporter.ts +++ b/src/plugins/assetexporter/src/AssetExporter.ts @@ -1,4 +1,4 @@ -import { Object3D, Mesh } from 'three'; +import { Object3D, Mesh } from 'three/webgpu'; import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'; import type { GLTFExporterOptions as THREEGLTFExporterOptions } from 'three/examples/jsm/exporters/GLTFExporter.js'; import { USDZExporter } from 'three/examples/jsm/exporters/USDZExporter.js'; @@ -91,10 +91,7 @@ export class AssetExporter { ): Promise { try { const json = await this._gltfExporter.parseAsync(object, { - animations: - object.animations.length > 0 - ? object.animations - : undefined, + animations: object.animations || [], ...options, binary: false, }); diff --git a/src/plugins/assetexporter/src/__test__/AssetExporter.test.ts b/src/plugins/assetexporter/src/__test__/AssetExporter.test.ts index 902c979f..e61bb6e3 100644 --- a/src/plugins/assetexporter/src/__test__/AssetExporter.test.ts +++ b/src/plugins/assetexporter/src/__test__/AssetExporter.test.ts @@ -1,6 +1,6 @@ import { AssetExporter } from '../AssetExporter.ts'; -import { Object3D, Mesh } from 'three'; -import { ParseError } from '../../../../error/parse/parse-error.ts'; +import { Object3D, Mesh } from 'three/webgpu'; +import { ParseError } from '@shopware-ag/dive'; // Mock TextEncoder class MockTextEncoder { diff --git a/src/plugins/assetloader/src/loader/AssetLoader.ts b/src/plugins/assetloader/src/loader/AssetLoader.ts index 4d13698e..a205e78b 100644 --- a/src/plugins/assetloader/src/loader/AssetLoader.ts +++ b/src/plugins/assetloader/src/loader/AssetLoader.ts @@ -1,6 +1,6 @@ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; -import { USDZLoader } from 'three/examples/jsm/loaders/USDZLoader.js'; -import { type Object3D } from 'three'; +import { USDLoader } from 'three/examples/jsm/loaders/USDLoader.js'; +import { type Object3D } from 'three/webgpu'; import { type FileType, SUPPORTED_FILE_TYPES, @@ -15,7 +15,7 @@ import { AssetCache } from '@shopware-ag/dive/assetcache'; export class AssetLoader { private _gltfLoader: GLTFLoader; - private _usdzLoader: USDZLoader; + private _usdLoader: USDLoader; private _stepLoader: STEPLoader; constructor() { @@ -30,7 +30,7 @@ export class AssetLoader { this._gltfLoader.setDRACOLoader(dracoLoader); // create usdz loader - this._usdzLoader = new USDZLoader(); + this._usdLoader = new USDLoader(); // create step/iges loader (CAD formats) this._stepLoader = new STEPLoader(); @@ -191,7 +191,7 @@ export class AssetLoader { return gltf.scene; } case 'usdz': { - const usdz = this._usdzLoader.parse(arrayBuffer); + const usdz = this._usdLoader.parse(arrayBuffer); usdz.animations = []; return usdz; } diff --git a/src/plugins/assetloader/src/loader/__test__/AssetLoader.test.ts b/src/plugins/assetloader/src/loader/__test__/AssetLoader.test.ts index 06d629eb..6f0cc42d 100644 --- a/src/plugins/assetloader/src/loader/__test__/AssetLoader.test.ts +++ b/src/plugins/assetloader/src/loader/__test__/AssetLoader.test.ts @@ -1,17 +1,8 @@ import { AssetLoader } from '../AssetLoader.ts'; import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js'; -import { Group } from 'three'; +import { Group } from 'three/webgpu'; +import { FileTypeError, NetworkError, ParseError } from '@shopware-ag/dive'; import { AssetCache } from '@shopware-ag/dive/assetcache'; -import { FileTypeError } from '../../../../../error/file-type/file-type-error.ts'; -import { NetworkError } from '../../../../../error/network/network-error.ts'; -import { ParseError } from '../../../../../error/parse/parse-error.ts'; - -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); // Mock the Three.js loaders const mockParseAsyncGLTF = vi.fn(); @@ -22,10 +13,10 @@ vi.mock('three/examples/jsm/loaders/GLTFLoader.js', () => ({ })), })); -const mockParseUSDZ = vi.fn(); -vi.mock('three/examples/jsm/loaders/USDZLoader.js', () => ({ - USDZLoader: vi.fn().mockImplementation(() => ({ - parse: mockParseUSDZ, +const mockParseUSD = vi.fn(); +vi.mock('three/examples/jsm/loaders/USDLoader.js', () => ({ + USDLoader: vi.fn().mockImplementation(() => ({ + parse: mockParseUSD, })), })); @@ -255,27 +246,27 @@ describe('AssetLoader', () => { it('should parse USDZ files correctly', async () => { const mockObject = new Group(); const mockArrayBuffer = new ArrayBuffer(1024); - mockParseUSDZ.mockReturnValue(mockObject); + mockParseUSD.mockReturnValue(mockObject); mockChunk.load.mockResolvedValue(mockArrayBuffer); const result = await loader.load('model.usdz'); expect(MockedAssetCache.create).toHaveBeenCalledWith('model.usdz'); expect(mockChunk.load).toHaveBeenCalled(); - expect(mockParseUSDZ).toHaveBeenCalledWith(mockArrayBuffer); + expect(mockParseUSD).toHaveBeenCalledWith(mockArrayBuffer); expect(result).toBe(mockObject); expect(result.animations).toEqual([]); }); it('should throw ParseError when USDZ parsing fails', async () => { const mockArrayBuffer = new ArrayBuffer(1024); - mockParseUSDZ.mockImplementation(() => { + mockParseUSD.mockImplementation(() => { throw new Error('Invalid USDZ'); }); mockChunk.load.mockResolvedValue(mockArrayBuffer); await expect(loader.load('model.usdz')).rejects.toThrow(ParseError); - expect(mockParseUSDZ).toHaveBeenCalledWith(mockArrayBuffer); + expect(mockParseUSD).toHaveBeenCalledWith(mockArrayBuffer); }); }); @@ -311,14 +302,14 @@ describe('AssetLoader', () => { MockedAssetCache.read.mockReturnValue(null); mockChunk.load.mockResolvedValue(mockArrayBuffer); - mockParseUSDZ.mockReturnValue(mockObject); + mockParseUSD.mockReturnValue(mockObject); const result = await loader.load(uri); expect(MockedAssetCache.read).toHaveBeenCalledWith(uri); expect(MockedAssetCache.create).toHaveBeenCalledWith(uri); expect(mockChunk.load).toHaveBeenCalled(); - expect(mockParseUSDZ).toHaveBeenCalledWith(mockArrayBuffer); + expect(mockParseUSD).toHaveBeenCalledWith(mockArrayBuffer); expect(result).toBe(mockObject); expect(result.animations).toEqual([]); }); @@ -342,7 +333,7 @@ describe('AssetLoader', () => { mockParseAsyncGLTF.mockResolvedValue({ scene: new Group(), } as GLTF); - mockParseUSDZ.mockResolvedValue(new Group()); + mockParseUSD.mockResolvedValue(new Group()); mockStepParseAsync.mockResolvedValue(new Group()); }); diff --git a/src/plugins/assetloader/src/step/STEPLoader.ts b/src/plugins/assetloader/src/step/STEPLoader.ts index c2e29f9f..6bcb41e2 100644 --- a/src/plugins/assetloader/src/step/STEPLoader.ts +++ b/src/plugins/assetloader/src/step/STEPLoader.ts @@ -5,7 +5,7 @@ import { Mesh, MeshStandardMaterial, Object3D, -} from 'three'; +} from 'three/webgpu'; import { STEPWorker } from '../step/worker/StepWorker.js'; /** diff --git a/src/plugins/assetloader/src/step/__test__/STEPLoader.test.ts b/src/plugins/assetloader/src/step/__test__/STEPLoader.test.ts index 20881a71..0760fb6a 100644 --- a/src/plugins/assetloader/src/step/__test__/STEPLoader.test.ts +++ b/src/plugins/assetloader/src/step/__test__/STEPLoader.test.ts @@ -1,8 +1,14 @@ -import { BufferGeometry, Color, Group, MeshStandardMaterial } from 'three'; +import { + BufferGeometry, + Color, + Group, + MeshStandardMaterial, +} from 'three/webgpu'; import { STEPLoader } from '../STEPLoader.ts'; -vi.mock('three', async () => { - const actual = await vi.importActual('three'); +vi.mock('three/webgpu', async () => { + const actual = + await vi.importActual('three/webgpu'); const Object3D = vi.fn(function (this: any) { this.children = []; diff --git a/src/plugins/mediacreator/README.md b/src/plugins/mediacreator/README.md index 486c892f..9caec4ea 100644 --- a/src/plugins/mediacreator/README.md +++ b/src/plugins/mediacreator/README.md @@ -13,10 +13,12 @@ import { MediaCreator } from '@shopware-ag/dive/mediacreator'; const mediaCreator = new MediaCreator(renderer, scene, controller); // Generate a screenshot -const screenshot = await mediaCreator.generateMedia( - { x: 0, y: 0, z: 0 }, // camera position - { x: 0, y: 0, z: 0 }, // camera target - 1920, // width - 1080 // height -); -``` \ No newline at end of file +const screenshot = await mediaCreator.generateMedia({ + position: { x: 0, y: 0, z: 0 }, + target: { x: 0, y: 0, z: 0 }, + resolution: { + width: 1920, + height: 1080, + }, +}); +``` diff --git a/src/plugins/mediacreator/src/MediaCreator.ts b/src/plugins/mediacreator/src/MediaCreator.ts index b6679329..bad80c43 100644 --- a/src/plugins/mediacreator/src/MediaCreator.ts +++ b/src/plugins/mediacreator/src/MediaCreator.ts @@ -1,8 +1,12 @@ +import { RenderTarget, SRGBColorSpace } from 'three/webgpu'; import { DIVEPerspectiveCamera } from '../../../engine/camera/PerspectiveCamera.ts'; import { type DIVEScene } from '../../../engine/scene/Scene.ts'; import { type DIVERenderer } from '../../../engine/renderer/Renderer.ts'; import { type OrbitController } from '@shopware-ag/dive/orbitcontroller'; -import { MediaGenerationByPosition } from '../types/index.ts'; +import { + type MediaGenerationByPosition, + type MediaGenerationResolution, +} from '../types/index.ts'; /** * @internal @@ -22,54 +26,113 @@ export class MediaCreator { this._controller = controller; } - public generateMedia(options: MediaGenerationByPosition): string { + public async generateMedia( + options: MediaGenerationByPosition, + ): Promise { const { position, target, resolution } = options; const { width, height } = resolution; const resetPosition = this._controller.object.position.clone(); const resetRotation = this._controller.object.quaternion.clone(); + const resetTarget = this._controller.target.clone(); + const restoreWidth = this._renderer.canvas.clientWidth || width; + const restoreHeight = this._renderer.canvas.clientHeight || height; - this._renderer.onResize(width, height); - // will be removed in the future when DIVEOrthographicCamera will be implemented - if ('onResize' in this._controller.object) { - this._controller.object.onResize(width, height); - } + try { + if ('onResize' in this._controller.object) { + this._controller.object.onResize(width, height); + } - this._controller.object.position.copy(position); - this._controller.target.copy(target); - this._controller.update(); + this._controller.object.position.copy(position); + this._controller.target.copy(target); + this._controller.update(); - const dataUri = this.drawCanvas().toDataURL(); + const dataUri = ( + await this.drawCanvas(undefined, resolution) + ).toDataURL(); - this._controller.object.position.copy(resetPosition); - this._controller.object.quaternion.copy(resetRotation); + return dataUri; + } finally { + this._controller.object.position.copy(resetPosition); + this._controller.object.quaternion.copy(resetRotation); + this._controller.target.copy(resetTarget); + this._controller.update(); - return dataUri; + if ('onResize' in this._controller.object) { + this._controller.object.onResize(restoreWidth, restoreHeight); + } + } } - public drawCanvas(canvasElement?: HTMLCanvasElement): HTMLCanvasElement { - // save current canvas - const restore = this._renderer.webglrenderer.domElement; - if (canvasElement) { - this._renderer.webglrenderer.domElement = canvasElement; - } + public async drawCanvas( + canvasElement?: HTMLCanvasElement, + resolution?: MediaGenerationResolution, + ): Promise { + await this._renderer.init(); - // draw canvas - this._controller.object.layers.mask = - DIVEPerspectiveCamera.LIVE_VIEW_LAYER_MASK; - this._renderer.webglrenderer.render( - this._scene, - this._controller.object, + const renderer = this._renderer.webgpurenderer; + const width = Math.max( + 1, + resolution?.width ?? + canvasElement?.width ?? + canvasElement?.clientWidth ?? + this._renderer.canvas.clientWidth, ); - this._controller.object.layers.mask = - DIVEPerspectiveCamera.EDITOR_VIEW_LAYER_MASK; + const height = Math.max( + 1, + resolution?.height ?? + canvasElement?.height ?? + canvasElement?.clientHeight ?? + this._renderer.canvas.clientHeight, + ); + const renderTarget = new RenderTarget(width, height, { + colorSpace: SRGBColorSpace, + }); + const restoreRenderTarget = renderer.getRenderTarget(); + const restoreLayerMask = this._controller.object.layers.mask; + + try { + renderer.setRenderTarget(renderTarget); + this._controller.object.layers.mask = + DIVEPerspectiveCamera.LIVE_VIEW_LAYER_MASK; + renderer.render(this._scene, this._controller.object); - const returnCanvas = this._renderer.webglrenderer.domElement; + const pixels = await renderer.readRenderTargetPixelsAsync( + renderTarget, + 0, + 0, + width, + height, + ); + + const outputCanvas = + canvasElement ?? document.createElement('canvas'); + outputCanvas.width = width; + outputCanvas.height = height; + this._writePixelsToCanvas(outputCanvas, pixels, width, height); + + return outputCanvas; + } finally { + this._controller.object.layers.mask = restoreLayerMask; + renderer.setRenderTarget(restoreRenderTarget); + renderTarget.dispose(); + } + } - // restore canvas - if (canvasElement) { - this._renderer.webglrenderer.domElement = restore; + private _writePixelsToCanvas( + canvas: HTMLCanvasElement, + pixels: ArrayLike, + width: number, + height: number, + ): void { + const context = canvas.getContext('2d'); + if (!context) { + throw new Error( + 'MediaCreator.drawCanvas: 2D canvas context is not available.', + ); } - return returnCanvas; + const imageData = context.createImageData(width, height); + imageData.data.set(new Uint8ClampedArray(Array.from(pixels))); + context.putImageData(imageData, 0, 0); } } diff --git a/src/plugins/mediacreator/src/__test__/MediaCreator.test.ts b/src/plugins/mediacreator/src/__test__/MediaCreator.test.ts index 4718573c..33a352e0 100644 --- a/src/plugins/mediacreator/src/__test__/MediaCreator.test.ts +++ b/src/plugins/mediacreator/src/__test__/MediaCreator.test.ts @@ -1,16 +1,9 @@ -import { MediaCreator } from '../MediaCreator.ts'; -import { - DIVEPerspectiveCamera, - DIVERenderer, - DIVEScene, -} from '@shopware-ag/dive'; -import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; -import { type MediaGenerationByPosition } from '../../types/index.ts'; - /** * @jest-environment jsdom */ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + // Mock ResizeObserver class MockResizeObserver { observe() {} @@ -19,57 +12,130 @@ class MockResizeObserver { } global.ResizeObserver = MockResizeObserver as any; -const mock_render = vi.fn(); -const mock_toDataURL = vi.fn(); +const originalCreateElement = document.createElement.bind(document); + +const mockRender = vi.fn(); +const mockToDataURL = vi.fn().mockReturnValue('data:image/png;base64,test'); +const mockSetRenderTarget = vi.fn(); +const mockGetRenderTarget = vi.fn().mockReturnValue(null); +const mockReadRenderTargetPixelsAsync = vi.fn().mockResolvedValue( + new Uint8Array([ + 255, + 0, + 0, + 255, + ]), +); +const mockRendererInit = vi.fn().mockResolvedValue(undefined); +const mockCreateImageData = vi.fn((width: number, height: number) => ({ + data: new Uint8ClampedArray(width * height * 4), +})); +const mockPutImageData = vi.fn(); + +const createMockCanvas = (): HTMLCanvasElement => { + const canvas = originalCreateElement('canvas'); + + Object.defineProperty(canvas, 'clientWidth', { + configurable: true, + value: 800, + }); + Object.defineProperty(canvas, 'clientHeight', { + configurable: true, + value: 600, + }); + Object.defineProperty(canvas, 'getContext', { + configurable: true, + value: vi.fn(() => ({ + createImageData: mockCreateImageData, + putImageData: mockPutImageData, + })), + }); + Object.defineProperty(canvas, 'toDataURL', { + configurable: true, + value: mockToDataURL, + }); + + return canvas; +}; + +vi.mock('three/webgpu', async (importOriginal) => { + const actual = await importOriginal(); -vi.mock('@shopware-ag/dive', () => { return { - DIVERenderer: vi.fn(function (this: any) { - this.webglrenderer = { - domElement: { - toDataURL: mock_toDataURL, - }, - render: mock_render, - }; - this.onResize = vi.fn(); - return this; - }), - DIVEPerspectiveCamera: vi.fn(function (this: any) { - this.position = { - clone: vi.fn(), - copy: vi.fn(), - }; - this.quaternion = { - clone: vi.fn(), - copy: vi.fn(), - }; - this.layers = { - mask: 0, - }; - this.onResize = vi.fn(); - return this; - }), - DIVEScene: vi.fn(function (this: any) { - this.add = vi.fn(); - this.children = []; - this.root = { - children: [], - }; + ...actual, + RenderTarget: vi.fn(function ( + this: any, + width: number, + height: number, + ) { + this.width = width; + this.height = height; + this.dispose = vi.fn(); return this; }), }; }); +vi.mock('@shopware-ag/dive', () => { + const MockDIVERenderer = vi.fn(function (this: any) { + const nativeRenderer = { + domElement: createMockCanvas(), + render: mockRender, + setRenderTarget: mockSetRenderTarget, + getRenderTarget: mockGetRenderTarget, + readRenderTargetPixelsAsync: mockReadRenderTargetPixelsAsync, + }; + + this.canvas = nativeRenderer.domElement; + this.init = mockRendererInit; + this.webgpurenderer = nativeRenderer; + return this; + }); + + const MockPerspectiveCamera = vi.fn(function (this: any) { + this.position = { + clone: vi.fn(() => ({ copy: vi.fn() })), + copy: vi.fn(), + }; + this.quaternion = { + clone: vi.fn(() => ({ copy: vi.fn() })), + copy: vi.fn(), + }; + this.layers = { + mask: 0, + }; + this.onResize = vi.fn(); + return this; + }); + (MockPerspectiveCamera as any).LIVE_VIEW_LAYER_MASK = 1; + (MockPerspectiveCamera as any).EDITOR_VIEW_LAYER_MASK = 2; + + const MockScene = vi.fn(function (this: any) { + this.add = vi.fn(); + this.children = []; + this.root = { + children: [], + }; + return this; + }); + + return { + DIVERenderer: MockDIVERenderer, + DIVEPerspectiveCamera: MockPerspectiveCamera, + DIVEScene: MockScene, + }; +}); + vi.mock('@shopware-ag/dive/orbitcontroller', () => { return { OrbitController: vi.fn(function (this: any) { this.object = { position: { - clone: vi.fn(), + clone: vi.fn(() => ({ copy: vi.fn() })), copy: vi.fn(), }, quaternion: { - clone: vi.fn(), + clone: vi.fn(() => ({ copy: vi.fn() })), copy: vi.fn(), }, layers: { @@ -79,7 +145,7 @@ vi.mock('@shopware-ag/dive/orbitcontroller', () => { }; this.target = { - clone: vi.fn(), + clone: vi.fn(() => ({ copy: vi.fn() })), copy: vi.fn(), }; @@ -90,35 +156,38 @@ vi.mock('@shopware-ag/dive/orbitcontroller', () => { }; }); -vi.mock('@shopware-ag/dive/animation', () => { - return { - DIVEAnimationSystem: vi.fn(function (this: any) { - this.domElement = { - toDataURL: mock_toDataURL, - }; - this.render = mock_render; - this.OnResize = vi.fn(); - this.AddPreRenderCallback = vi.fn((callback) => { - callback(); - return 'id'; - }); - return this; - }), - }; -}); +import { MediaCreator } from '../MediaCreator.ts'; +import { + DIVEPerspectiveCamera, + DIVERenderer, + DIVEScene, +} from '@shopware-ag/dive'; +import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; +import { type MediaGenerationByPosition } from '../../types/index.ts'; const mockScene = new DIVEScene(); const mockCamera = new DIVEPerspectiveCamera(); const mockRenderer = new DIVERenderer(mockScene, mockCamera); const mockOrbitController = new OrbitController( mockCamera, - mockRenderer.webglrenderer.domElement, + mockRenderer.webgpurenderer.domElement, ); -let mediaCreator: MediaCreator; describe('MediaCreator', () => { + let mediaCreator: MediaCreator; + beforeEach(() => { vi.clearAllMocks(); + vi.spyOn(document, 'createElement').mockImplementation((( + tagName: string, + ) => { + if (tagName === 'canvas') { + return createMockCanvas(); + } + + return originalCreateElement(tagName); + }) as typeof document.createElement); + mediaCreator = new MediaCreator( mockRenderer, mockScene, @@ -130,27 +199,131 @@ describe('MediaCreator', () => { expect(mediaCreator).toBeDefined(); }); - it('should generate media by position', () => { + it('should generate media by position', async () => { const options: MediaGenerationByPosition = { position: { x: 0, y: 0, z: 0 }, target: { x: 0, y: 0, z: 0 }, resolution: { - width: 800, - height: 600, + width: 2, + height: 1, }, }; - expect(() => { - mediaCreator.generateMedia(options); - }).not.toThrow(); + const dataUri = await mediaCreator.generateMedia(options); + + expect(mockRendererInit).toHaveBeenCalledTimes(1); + expect(mockRender).toHaveBeenCalledTimes(1); + expect(mockReadRenderTargetPixelsAsync).toHaveBeenCalledWith( + expect.anything(), + 0, + 0, + 2, + 1, + ); + expect(mockToDataURL).toHaveBeenCalledTimes(1); + expect(dataUri).toBe('data:image/png;base64,test'); + }); + + it('should draw into a custom canvas', async () => { + const canvas = createMockCanvas(); + + const result = await mediaCreator.drawCanvas(canvas, { + width: 4, + height: 4, + }); + + expect(result).toBe(canvas); + expect(mockSetRenderTarget).toHaveBeenCalled(); + expect(mockPutImageData).toHaveBeenCalledTimes(1); + }); + + it('should restore the requested resolution when the renderer canvas has no client size', async () => { + Object.defineProperty(mockRenderer.canvas, 'clientWidth', { + configurable: true, + value: 0, + }); + Object.defineProperty(mockRenderer.canvas, 'clientHeight', { + configurable: true, + value: 0, + }); + + await mediaCreator.generateMedia({ + position: { x: 1, y: 2, z: 3 }, + target: { x: 4, y: 5, z: 6 }, + resolution: { + width: 16, + height: 9, + }, + }); + + expect(mockOrbitController.object.onResize).toHaveBeenNthCalledWith( + 1, + 16, + 9, + ); + expect(mockOrbitController.object.onResize).toHaveBeenLastCalledWith( + 16, + 9, + ); + }); + + it('should fall back to the renderer canvas size when no resolution is provided', async () => { + const canvas = createMockCanvas(); - expect(mock_render).toHaveBeenCalledTimes(1); - expect(mock_toDataURL).toHaveBeenCalledTimes(1); + Object.defineProperty(canvas, 'width', { + configurable: true, + value: undefined, + writable: true, + }); + Object.defineProperty(canvas, 'height', { + configurable: true, + value: undefined, + writable: true, + }); + Object.defineProperty(canvas, 'clientWidth', { + configurable: true, + value: undefined, + }); + Object.defineProperty(canvas, 'clientHeight', { + configurable: true, + value: undefined, + }); + Object.defineProperty(mockRenderer.canvas, 'clientWidth', { + configurable: true, + value: 321, + }); + Object.defineProperty(mockRenderer.canvas, 'clientHeight', { + configurable: true, + value: 123, + }); + + const result = await mediaCreator.drawCanvas(canvas); + + expect(result.width).toBe(321); + expect(result.height).toBe(123); + expect(mockReadRenderTargetPixelsAsync).toHaveBeenCalledWith( + expect.anything(), + 0, + 0, + 321, + 123, + ); }); - it('should draw canvas with custom canvas', () => { - const canvas = document.createElement('canvas'); - mediaCreator.drawCanvas(canvas); - expect(mock_render).toHaveBeenCalledTimes(1); + it('should throw when the output canvas has no 2D context', async () => { + const canvas = createMockCanvas(); + Object.defineProperty(canvas, 'getContext', { + configurable: true, + value: vi.fn(() => null), + }); + + await expect( + mediaCreator.drawCanvas(canvas, { + width: 1, + height: 1, + }), + ).rejects.toThrow( + 'MediaCreator.drawCanvas: 2D canvas context is not available.', + ); }); }); diff --git a/src/plugins/mediacreator/types/MediaGenerationByPosition.ts b/src/plugins/mediacreator/types/MediaGenerationByPosition.ts index 31f8734c..01dd293a 100644 --- a/src/plugins/mediacreator/types/MediaGenerationByPosition.ts +++ b/src/plugins/mediacreator/types/MediaGenerationByPosition.ts @@ -1,4 +1,4 @@ -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; import { type MediaGenerationResolution } from './MediaGenerationResolution.ts'; export type MediaGenerationByPosition = { diff --git a/src/plugins/orbitcontroller/src/OrbitController.ts b/src/plugins/orbitcontroller/src/OrbitController.ts index 4bae4950..3641a020 100644 --- a/src/plugins/orbitcontroller/src/OrbitController.ts +++ b/src/plugins/orbitcontroller/src/OrbitController.ts @@ -12,7 +12,7 @@ import { Vector2, Vector3, Vector3Like, -} from 'three'; +} from 'three/webgpu'; import { DIVEPerspectiveCamera, DIVETicker, diff --git a/src/plugins/orbitcontroller/src/__test__/OrbitController.test.ts b/src/plugins/orbitcontroller/src/__test__/OrbitController.test.ts index 87e9a05a..a103b12a 100644 --- a/src/plugins/orbitcontroller/src/__test__/OrbitController.test.ts +++ b/src/plugins/orbitcontroller/src/__test__/OrbitController.test.ts @@ -14,7 +14,7 @@ import { OrthographicCamera, Matrix4, TOUCH, -} from 'three'; +} from 'three/webgpu'; // Add a real canvas for the controls domElement const canvas = document.createElement('canvas'); diff --git a/src/plugins/orbitcontroller/types/OrbitControllerState.types.ts b/src/plugins/orbitcontroller/types/OrbitControllerState.types.ts index f01caf03..94f6c24b 100644 --- a/src/plugins/orbitcontroller/types/OrbitControllerState.types.ts +++ b/src/plugins/orbitcontroller/types/OrbitControllerState.types.ts @@ -1,5 +1,4 @@ -import { Vector3Like } from 'three'; -import { QuaternionLike } from 'three'; +import { Vector3Like, QuaternionLike } from 'three/webgpu'; export type OrbitControllerState = { target: Vector3Like; diff --git a/src/plugins/orientationdisplay/src/OrientationDisplay.ts b/src/plugins/orientationdisplay/src/OrientationDisplay.ts index 2e8cf4ad..3c49e9a7 100644 --- a/src/plugins/orientationdisplay/src/OrientationDisplay.ts +++ b/src/plugins/orientationdisplay/src/OrientationDisplay.ts @@ -1,4 +1,4 @@ -import { MathUtils, Vector4 } from 'three'; +import { MathUtils, Vector4, OrthographicCamera } from 'three/webgpu'; import { type DIVERenderer, DIVETicker, @@ -7,7 +7,6 @@ import { COORDINATE_LAYER_MASK, } from '@shopware-ag/dive'; import { OrientationDisplayAxes } from './axes/Axes.ts'; -import { OrthographicCamera } from 'three'; /** * Shows the scene axes in the bottom left corner of the screen. @@ -50,22 +49,22 @@ export class OrientationDisplay implements DIVETicker { this._scene.background = null; // save current viewport and set it to desired size - this._renderer.webglrenderer.getViewport(this._restoreViewport); - this._renderer.webglrenderer.setViewport(0, 0, 150, 150); - this._renderer.webglrenderer.autoClear = false; + this._renderer.webgpurenderer.getViewport(this._restoreViewport); + this._renderer.webgpurenderer.setViewport(0, 0, 150, 150); + this._renderer.webgpurenderer.autoClear = false; // set axes rotation to camera rotation this._axes.setFromCameraMatrix(this._camera.matrix); // render scene to orthographic camera - this._renderer.webglrenderer.render( + this._renderer.webgpurenderer.render( this._scene, this._orthographicCamera, ); // restore viewport and background - this._renderer.webglrenderer.setViewport(this._restoreViewport); - this._renderer.webglrenderer.autoClear = true; + this._renderer.webgpurenderer.setViewport(this._restoreViewport); + this._renderer.webgpurenderer.autoClear = true; this._scene.background = restoreBackground; } diff --git a/src/plugins/orientationdisplay/src/__test__/OrientationDisplay.test.ts b/src/plugins/orientationdisplay/src/__test__/OrientationDisplay.test.ts index f704c204..448a79a8 100644 --- a/src/plugins/orientationdisplay/src/__test__/OrientationDisplay.test.ts +++ b/src/plugins/orientationdisplay/src/__test__/OrientationDisplay.test.ts @@ -1,11 +1,4 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - -import { Matrix4, Vector4, Color, Material } from 'three'; +import { Matrix4, Vector4, Color, Material } from 'three/webgpu'; import { OrientationDisplay } from '../OrientationDisplay.ts'; import { DIVERenderer, @@ -44,7 +37,7 @@ const mockCamera = { const mockRenderer = { render: vi.fn(), - webglrenderer: { + webgpurenderer: { getViewport: vi.fn().mockReturnValue(new Vector4(0, 0, 800, 600)), setViewport: vi.fn(), render: vi.fn(), @@ -57,7 +50,7 @@ describe('OrientationDisplay', () => { beforeEach(() => { vi.clearAllMocks(); - mockRenderer.webglrenderer.autoClear = true; + mockRenderer.webgpurenderer.autoClear = true; orientationDisplay = new OrientationDisplay( mockRenderer, mockScene, @@ -110,22 +103,19 @@ describe('OrientationDisplay', () => { orientationDisplay.tick(); - expect(mockRenderer.webglrenderer.getViewport).toHaveBeenCalledWith( - orientationDisplay['_restoreViewport'], - ); - expect(mockRenderer.webglrenderer.setViewport).toHaveBeenCalledWith( - 0, - 0, - 150, - 150, - ); - expect(mockRenderer.webglrenderer.render).toHaveBeenCalledWith( + expect( + mockRenderer.webgpurenderer.getViewport, + ).toHaveBeenCalledWith(orientationDisplay['_restoreViewport']); + expect( + mockRenderer.webgpurenderer.setViewport, + ).toHaveBeenCalledWith(0, 0, 150, 150); + expect(mockRenderer.webgpurenderer.render).toHaveBeenCalledWith( mockScene, orientationDisplay['_orthographicCamera'], ); - expect(mockRenderer.webglrenderer.setViewport).toHaveBeenCalledWith( - orientationDisplay['_restoreViewport'], - ); + expect( + mockRenderer.webgpurenderer.setViewport, + ).toHaveBeenCalledWith(orientationDisplay['_restoreViewport']); expect(mockScene.background).toBe(originalBackground); }); @@ -153,7 +143,7 @@ describe('OrientationDisplay', () => { it('should manage autoClear property correctly', () => { orientationDisplay.tick(); - expect(mockRenderer.webglrenderer.autoClear).toBe(true); + expect(mockRenderer.webgpurenderer.autoClear).toBe(true); }); }); }); diff --git a/src/plugins/orientationdisplay/src/axes/Axes.ts b/src/plugins/orientationdisplay/src/axes/Axes.ts index a388c81c..43f51441 100644 --- a/src/plugins/orientationdisplay/src/axes/Axes.ts +++ b/src/plugins/orientationdisplay/src/axes/Axes.ts @@ -7,7 +7,7 @@ import { AxesColorRedLetter, COORDINATE_LAYER_MASK, } from '@shopware-ag/dive'; -import { AxesHelper, Color, Material, Matrix4, Object3D } from 'three'; +import { AxesHelper, Color, Material, Matrix4, Object3D } from 'three/webgpu'; import SpriteText from 'three-spritetext'; export class OrientationDisplayAxes extends Object3D { diff --git a/src/plugins/orientationdisplay/src/axes/__test__/Axes.test.ts b/src/plugins/orientationdisplay/src/axes/__test__/Axes.test.ts index e0de12f9..6cca4e2a 100644 --- a/src/plugins/orientationdisplay/src/axes/__test__/Axes.test.ts +++ b/src/plugins/orientationdisplay/src/axes/__test__/Axes.test.ts @@ -1,25 +1,19 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); +import { Matrix4 } from 'three/webgpu'; +import { OrientationDisplayAxes } from '../Axes.ts'; vi.mock('three-spritetext', async () => { - const { Object3D } = await vi.importActual('three'); + const actual = + await vi.importActual('three/webgpu'); return { - default: vi.fn(function (this: any) { - const sprite = new Object3D(); - sprite.layers.mask = 0; - return sprite; - }), + default: vi.fn((text: string, textHeight: number, color: unknown) => + Object.assign(new actual.Object3D(), { + userData: { text, textHeight, color }, + }), + ), }; }); -import { Matrix4 } from 'three'; -import { OrientationDisplayAxes } from '../Axes.ts'; - describe('OrientationDisplayAxes', () => { it('should construct without errors', () => { const axes = new OrientationDisplayAxes(); diff --git a/src/plugins/quickview/src/__test__/QuickView.test.ts b/src/plugins/quickview/src/__test__/QuickView.test.ts index bf07dd92..ae2865a9 100644 --- a/src/plugins/quickview/src/__test__/QuickView.test.ts +++ b/src/plugins/quickview/src/__test__/QuickView.test.ts @@ -17,9 +17,7 @@ vi.mock('@shopware-ag/dive', () => { set: vi.fn(), }, }, - renderer: { - webglrenderer: vi.fn(), - }, + renderer: {}, }, scene: { setBackground: vi.fn(), diff --git a/src/plugins/shader/README.md b/src/plugins/shader/README.md index 5034661e..2a15df3d 100644 --- a/src/plugins/shader/README.md +++ b/src/plugins/shader/README.md @@ -1,36 +1,46 @@ # Shader -DIVEShaderLib is a shared shader library. It contains all shaders from the three.js library as well as custom shaders defined in the DIVE shader plugin. +The shader plugin provides reusable TSL node building blocks for WebGPU materials. -## Features: -- DIVEShaderMaterial wrapper class for correct types -- define custom shaders in the DIVE shader plugin -- access and extend from outside the DIVE package for runtime -- reuse default shaders from three.js +## Features +- exports node classes instead of legacy shader-lib objects +- callers own `NodeMaterial` creation and uniform defaults +- runtime updates happen on caller-owned `UniformNode`s ## Usage ```ts -import { Color } from 'three'; -import { DIVEShaderMaterial, DIVEShaderLib, type GridShader } from '@shopware-ag/dive/shader'; - -// create DIVEShaderMaterial with custom uniforms -const gridMaterial = new DIVEShaderMaterial({ - ...DIVEShaderLib.grid, - uniforms: { - uGridSize: { value: 10 }, - uMajorLineEvery: { value: 2 }, - uMinorLineColor: { value: new Color('green') }, - uMajorLineColor: { value: new Color('red') }, - uFadeDistance: { value: 25 }, - }, +import { GridNode, type GridNodeUniforms } from '@shopware-ag/dive/shader'; +import { Color, DoubleSide, MeshBasicNodeMaterial } from 'three/webgpu'; +import { uniform } from 'three/tsl'; + +const uniforms: GridNodeUniforms = { + uGridSize: uniform(10), + uMajorLineEvery: uniform(2), + uMinorLineColor: uniform(new Color('green')), + uMajorLineColor: uniform(new Color('red')), + uFadeDistance: uniform(25), +}; + +const material = new MeshBasicNodeMaterial({ + transparent: true, + depthWrite: false, + side: DoubleSide, + outputNode: new GridNode(uniforms), }); +``` + +## Runtime Updates +Update the same uniform nodes after material creation to change the shader at runtime. + +```ts +uniforms.uGridSize.value = 12; +uniforms.uMajorLineEvery.value = 4; -// change uniform in runtime window.addEventListener('keydown', (event: KeyboardEvent) => { - if(event.key === 'ArrowUp') { - gridMaterial.uniforms.uGridSize.value += 1; - } else if(event.key === 'ArrowDown') { - gridMaterial.uniforms.uGridSize.value -= 1; + if (event.key === 'ArrowUp') { + uniforms.uGridSize.value += 1; + } else if (event.key === 'ArrowDown') { + uniforms.uGridSize.value -= 1; } }); ``` diff --git a/src/plugins/shader/src/index.ts b/src/plugins/shader/src/index.ts index 65538904..7acfd928 100644 --- a/src/plugins/shader/src/index.ts +++ b/src/plugins/shader/src/index.ts @@ -1,5 +1 @@ -export * from './shaders/DIVEShaderLib.ts'; -export * from './material/DIVEShaderMaterial.ts'; - -// DIVE shader types -export type { GridShader } from './shaders/grid/index.ts'; +export { GridNode, type GridNodeUniforms } from './shaders/GridNode.ts'; diff --git a/src/plugins/shader/src/material/DIVEShaderMaterial.ts b/src/plugins/shader/src/material/DIVEShaderMaterial.ts deleted file mode 100644 index 4332da8b..00000000 --- a/src/plugins/shader/src/material/DIVEShaderMaterial.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type ShaderLibShader } from 'three/src/renderers/shaders/ShaderLib.js'; -import { ShaderMaterial } from 'three'; - -export class DIVEShaderMaterial< - TShader extends ShaderLibShader, -> extends ShaderMaterial { - declare uniforms: TShader['uniforms']; -} diff --git a/src/plugins/shader/src/shaders/DIVEShaderLib.ts b/src/plugins/shader/src/shaders/DIVEShaderLib.ts deleted file mode 100644 index 9a334b65..00000000 --- a/src/plugins/shader/src/shaders/DIVEShaderLib.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ShaderLib } from 'three/src/renderers/shaders/ShaderLib.js'; - -// DIVE shaders -import { grid } from './grid/index.ts'; - -export const DIVEShaderLib: typeof ShaderLib & DIVEShaders = { - ...ShaderLib, - grid, -}; diff --git a/src/plugins/shader/src/shaders/GridNode.ts b/src/plugins/shader/src/shaders/GridNode.ts new file mode 100644 index 00000000..fa78489e --- /dev/null +++ b/src/plugins/shader/src/shaders/GridNode.ts @@ -0,0 +1,74 @@ +import { type Color, Node, type UniformNode } from 'three/webgpu'; +import { + abs, + cameraPosition, + float, + fract, + fwidth, + max, + min, + mix, + positionWorld, + smoothstep, + step, + vec4, +} from 'three/tsl'; + +export type GridNodeUniforms = { + /** World-space size of each grid cell in meters. */ + uGridSize: UniformNode<'float', number>; + /** Draw a major line every N cells. */ + uMajorLineEvery: UniformNode<'float', number>; + /** Color of minor grid lines. */ + uMinorLineColor: UniformNode<'color', Color>; + /** Color of major grid lines. */ + uMajorLineColor: UniformNode<'color', Color>; + /** Distance at which the grid fades out in meters. */ + uFadeDistance: UniformNode<'float', number>; +}; + +export class GridNode extends Node { + constructor(uniforms: GridNodeUniforms) { + super(); + + this.name = 'GridNode'; + + const coord = positionWorld.xz; + + const minorCoord = coord.div(uniforms.uGridSize); + const minorGrid = abs(fract(minorCoord.sub(0.5)).sub(0.5)).div( + fwidth(minorCoord), + ); + const lineMinor = min(minorGrid.x, minorGrid.y); + + const majorSize = uniforms.uGridSize.mul(uniforms.uMajorLineEvery); + const majorCoord = coord.div(majorSize); + const majorGrid = abs(fract(majorCoord.sub(0.5)).sub(0.5)).div( + fwidth(majorCoord), + ); + const lineMajor = min(majorGrid.x, majorGrid.y); + + const minorAlpha = float(1).sub(min(lineMinor, 1)); + const majorAlpha = float(1).sub(min(lineMajor.div(2), 1)); + + const alpha = max(minorAlpha, majorAlpha).mul( + float(1).sub( + smoothstep( + uniforms.uFadeDistance.mul(0.5), + uniforms.uFadeDistance, + positionWorld.xz.sub(cameraPosition.xz).length(), + ), + ), + ); + + alpha.lessThan(float(0.001)).discard(); + + const color = mix( + uniforms.uMinorLineColor, + uniforms.uMajorLineColor, + step(minorAlpha, majorAlpha), + ); + + return vec4(color, alpha); + } +} diff --git a/src/plugins/shader/src/shaders/__test__/GridNode.test.ts b/src/plugins/shader/src/shaders/__test__/GridNode.test.ts new file mode 100644 index 00000000..201aec0f --- /dev/null +++ b/src/plugins/shader/src/shaders/__test__/GridNode.test.ts @@ -0,0 +1,217 @@ +type MockTSLNode = { + label: string; + x: MockTSLNode; + y: MockTSLNode; + xz: MockTSLNode; + div: ReturnType; + sub: ReturnType; + mul: ReturnType; + length: ReturnType; + lessThan: ReturnType; + discard: ReturnType; +}; + +const mockState = vi.hoisted(() => { + const describeNode = (value: unknown): string => { + if (typeof value === 'number') return value.toString(); + return (value as { label?: string })?.label ?? 'unknown'; + }; + + const createMockNode = (label: string, depth = 2): MockTSLNode => { + const node = { + label, + x: null as unknown as MockTSLNode, + y: null as unknown as MockTSLNode, + xz: null as unknown as MockTSLNode, + div: vi.fn((value: unknown) => + createMockNode( + `${label}.div(${describeNode(value)})`, + depth - 1, + ), + ), + sub: vi.fn((value: unknown) => + createMockNode( + `${label}.sub(${describeNode(value)})`, + depth - 1, + ), + ), + mul: vi.fn((value: unknown) => + createMockNode( + `${label}.mul(${describeNode(value)})`, + depth - 1, + ), + ), + length: vi.fn(() => createMockNode(`${label}.length()`, depth - 1)), + lessThan: vi.fn((value: unknown) => + createMockNode( + `${label}.lessThan(${describeNode(value)})`, + depth - 1, + ), + ), + discard: vi.fn(), + } as MockTSLNode; + + if (depth > 0) { + node.x = createMockNode(`${label}.x`, depth - 1); + node.y = createMockNode(`${label}.y`, depth - 1); + node.xz = createMockNode(`${label}.xz`, depth - 1); + } else { + node.x = node; + node.y = node; + node.xz = node; + } + + return node; + }; + + return { + createMockNode, + abs: vi.fn((value: unknown) => + createMockNode(`abs(${describeNode(value)})`), + ), + float: vi.fn((value: number) => createMockNode(`float(${value})`)), + fract: vi.fn((value: unknown) => + createMockNode(`fract(${describeNode(value)})`), + ), + fwidth: vi.fn((value: unknown) => + createMockNode(`fwidth(${describeNode(value)})`), + ), + max: vi.fn((left: unknown, right: unknown) => + createMockNode( + `max(${describeNode(left)}, ${describeNode(right)})`, + ), + ), + min: vi.fn((left: unknown, right: unknown) => + createMockNode( + `min(${describeNode(left)}, ${describeNode(right)})`, + ), + ), + mix: vi.fn((colorA: unknown, colorB: unknown, factor: unknown) => + createMockNode( + `mix(${describeNode(colorA)}, ${describeNode(colorB)}, ${describeNode(factor)})`, + ), + ), + smoothstep: vi.fn((edge0: unknown, edge1: unknown, value: unknown) => + createMockNode( + `smoothstep(${describeNode(edge0)}, ${describeNode(edge1)}, ${describeNode(value)})`, + ), + ), + step: vi.fn((edge: unknown, value: unknown) => + createMockNode( + `step(${describeNode(edge)}, ${describeNode(value)})`, + ), + ), + vec4: vi.fn((color: unknown, alpha: unknown) => + createMockNode( + `vec4(${describeNode(color)}, ${describeNode(alpha)})`, + ), + ), + positionWorld: createMockNode('positionWorld'), + cameraPosition: createMockNode('cameraPosition'), + }; +}); + +vi.mock('three/webgpu', () => { + class MockNode { + static instances: MockNode[] = []; + name = ''; + + constructor() { + MockNode.instances.push(this); + } + } + + return { + Node: MockNode, + }; +}); + +vi.mock('three/tsl', () => ({ + abs: mockState.abs, + cameraPosition: mockState.cameraPosition, + float: mockState.float, + fract: mockState.fract, + fwidth: mockState.fwidth, + max: mockState.max, + min: mockState.min, + mix: mockState.mix, + positionWorld: mockState.positionWorld, + smoothstep: mockState.smoothstep, + step: mockState.step, + vec4: mockState.vec4, +})); + +import { Node } from 'three/webgpu'; +import { GridNode, type GridNodeUniforms } from '../GridNode.ts'; + +describe('shader/GridNode', () => { + beforeEach(() => { + vi.clearAllMocks(); + (Node as any).instances = []; + }); + + it('should build a grid output node from the provided uniforms', () => { + const rawUniforms = { + uGridSize: mockState.createMockNode('uGridSize'), + uMajorLineEvery: mockState.createMockNode('uMajorLineEvery'), + uMinorLineColor: mockState.createMockNode('uMinorLineColor'), + uMajorLineColor: mockState.createMockNode('uMajorLineColor'), + uFadeDistance: mockState.createMockNode('uFadeDistance'), + }; + const uniforms = rawUniforms as unknown as GridNodeUniforms; + + const result = new GridNode(uniforms); + + const gridNodeInstance = (Node as any).instances[0]; + const majorSize = rawUniforms.uGridSize.mul.mock.results[0].value; + const minorAlpha = + mockState.float.mock.results[0].value.sub.mock.results[0].value; + const majorAlpha = + mockState.float.mock.results[1].value.sub.mock.results[0].value; + expect(gridNodeInstance.name).toBe('GridNode'); + + expect(mockState.positionWorld.xz.div).toHaveBeenNthCalledWith( + 1, + rawUniforms.uGridSize, + ); + expect(rawUniforms.uGridSize.mul).toHaveBeenCalledWith( + rawUniforms.uMajorLineEvery, + ); + expect(mockState.positionWorld.xz.div).toHaveBeenNthCalledWith( + 2, + majorSize, + ); + expect(mockState.positionWorld.xz.sub).toHaveBeenCalledWith( + mockState.cameraPosition.xz, + ); + expect(rawUniforms.uFadeDistance.mul).toHaveBeenCalledWith(0.5); + expect(mockState.smoothstep).toHaveBeenCalledWith( + rawUniforms.uFadeDistance.mul.mock.results[0].value, + rawUniforms.uFadeDistance, + mockState.positionWorld.xz.sub.mock.results[0].value.length.mock + .results[0].value, + ); + + const alpha = + mockState.max.mock.results[0].value.mul.mock.results[0].value; + const discardThreshold = mockState.float.mock.results.find( + (_result, index) => mockState.float.mock.calls[index][0] === 0.001, + )?.value; + const discardCondition = alpha.lessThan.mock.results[0].value; + + expect(discardThreshold).toBeDefined(); + expect(alpha.lessThan).toHaveBeenCalledWith(discardThreshold); + expect(discardCondition.discard).toHaveBeenCalled(); + expect(mockState.step).toHaveBeenCalledWith(minorAlpha, majorAlpha); + expect(mockState.mix).toHaveBeenCalledWith( + rawUniforms.uMinorLineColor, + rawUniforms.uMajorLineColor, + mockState.step.mock.results[0].value, + ); + expect(mockState.vec4).toHaveBeenCalledWith( + mockState.mix.mock.results[0].value, + alpha, + ); + expect(result).toBe(mockState.vec4.mock.results[0].value); + }); +}); diff --git a/src/plugins/shader/src/shaders/grid/fragment.glsl b/src/plugins/shader/src/shaders/grid/fragment.glsl deleted file mode 100644 index 7c2469e7..00000000 --- a/src/plugins/shader/src/shaders/grid/fragment.glsl +++ /dev/null @@ -1,37 +0,0 @@ -uniform float uGridSize; -uniform float uMajorLineEvery; -uniform vec3 uMinorLineColor; -uniform vec3 uMajorLineColor; -uniform float uFadeDistance; - -varying vec3 vWorldPosition; - -void main() { - vec2 coord = vWorldPosition.xz; - - // Minor grid - vec2 minorCoord = coord / uGridSize; - vec2 minorGrid = abs(fract(minorCoord - 0.5) - 0.5) / fwidth(minorCoord); - float lineMinor = min(minorGrid.x, minorGrid.y); - - // Major grid - float majorSize = uGridSize * uMajorLineEvery; - vec2 majorCoord = coord / majorSize; - vec2 majorGrid = abs(fract(majorCoord - 0.5) - 0.5) / fwidth(majorCoord); - float lineMajor = min(majorGrid.x, majorGrid.y); - - // Line alpha: minor = 1px, major = 2px wide - float minorAlpha = 1.0 - min(lineMinor, 1.0); - float majorAlpha = 1.0 - min(lineMajor / 2.0, 1.0); - - float alpha = max(minorAlpha, majorAlpha); - vec3 color = mix(uMinorLineColor, uMajorLineColor, step(minorAlpha, majorAlpha)); - - // Radial fade from camera - float dist = length(vWorldPosition.xz - cameraPosition.xz); - alpha *= 1.0 - smoothstep(uFadeDistance * 0.5, uFadeDistance, dist); - - if (alpha < 0.001) discard; - - gl_FragColor = vec4(color, alpha); -} \ No newline at end of file diff --git a/src/plugins/shader/src/shaders/grid/index.ts b/src/plugins/shader/src/shaders/grid/index.ts deleted file mode 100644 index 6d6e2820..00000000 --- a/src/plugins/shader/src/shaders/grid/index.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Color, type IUniform, type ShaderLibShader } from 'three'; -import vertexShader from './vertex.glsl?raw'; -import fragmentShader from './fragment.glsl?raw'; - -export interface GridShader extends ShaderLibShader { - uniforms: { - /** - * World-space size of each grid cell in meters - * @default 1 - */ - uGridSize: IUniform; - /** - * Draw a major line every N cells in meters - * @default 10 - */ - uMajorLineEvery: IUniform; - /** - * Color of minor grid lines - * @default Color('#dddddd') - */ - uMinorLineColor: IUniform; - /** - * Color of major grid lines - * @default Color('#888888') - */ - uMajorLineColor: IUniform; - /** - * Distance at which the grid fades out in meters - * @default 10 - */ - uFadeDistance: IUniform; - }; -} - -export const grid: GridShader = { - uniforms: { - uGridSize: { value: 1 }, - uMajorLineEvery: { value: 10 }, - uMinorLineColor: { value: new Color('#dddddd') }, - uMajorLineColor: { value: new Color('#888888') }, - uFadeDistance: { value: 10 }, - }, - vertexShader, - fragmentShader, -}; - -declare global { - interface DIVEShaders { - grid: GridShader; - } - - interface DIVEShaderChunk { - grid_vertex: string; - grid_fragment: string; - } -} diff --git a/src/plugins/shader/src/shaders/grid/vertex.glsl b/src/plugins/shader/src/shaders/grid/vertex.glsl deleted file mode 100644 index ccb3dac6..00000000 --- a/src/plugins/shader/src/shaders/grid/vertex.glsl +++ /dev/null @@ -1,7 +0,0 @@ -varying vec3 vWorldPosition; - -void main() { - vec4 worldPos = modelMatrix * vec4(position, 1.0); - vWorldPosition = worldPos.xyz; - gl_Position = projectionMatrix * viewMatrix * worldPos; -} \ No newline at end of file diff --git a/src/plugins/state/src/State.ts b/src/plugins/state/src/State.ts index 310884cb..781cc26c 100644 --- a/src/plugins/state/src/State.ts +++ b/src/plugins/state/src/State.ts @@ -1,4 +1,4 @@ -import { MathUtils } from 'three'; +import { MathUtils } from 'three/webgpu'; // type imports import { type EntitySchema, type DIVE } from '@shopware-ag/dive'; diff --git a/src/plugins/state/src/__test__/State.test.ts b/src/plugins/state/src/__test__/State.test.ts index 31365e87..4f3c6ea9 100644 --- a/src/plugins/state/src/__test__/State.test.ts +++ b/src/plugins/state/src/__test__/State.test.ts @@ -1,4 +1,4 @@ -vi.mock('three', async (importOriginal) => { +vi.mock('three/webgpu', async (importOriginal) => { const actual = await importOriginal(); return { ...actual }; }); @@ -15,7 +15,7 @@ import { Toolbox } from '@shopware-ag/dive/toolbox'; import { getActionClass } from '../ActionRegistry.ts'; import { Action } from '../actions/action.ts'; import { type ActionDependencies } from '../../types/index.ts'; -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; // Extend the global ActionTypes interface for our tests declare global { diff --git a/src/plugins/state/src/actions/camera/__test__/computeencompassingview.test.ts b/src/plugins/state/src/actions/camera/__test__/computeencompassingview.test.ts index 3edb48f2..b547df0b 100644 --- a/src/plugins/state/src/actions/camera/__test__/computeencompassingview.test.ts +++ b/src/plugins/state/src/actions/camera/__test__/computeencompassingview.test.ts @@ -1,7 +1,7 @@ import { DIVE, DIVEScene } from '@shopware-ag/dive'; import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; import { ComputeEncompassingViewAction } from '../computeencompassingview.ts'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; vi.mock('../../../../../../components/boundingbox/BoundingBox.ts', () => ({ BoundingBox: vi.fn(), diff --git a/src/plugins/state/src/actions/camera/__test__/getcameratransform.test.ts b/src/plugins/state/src/actions/camera/__test__/getcameratransform.test.ts index 99526dba..e65e8a5f 100644 --- a/src/plugins/state/src/actions/camera/__test__/getcameratransform.test.ts +++ b/src/plugins/state/src/actions/camera/__test__/getcameratransform.test.ts @@ -1,6 +1,6 @@ import { GetCameraTransformAction } from '../getcameratransform.ts'; import { type OrbitController } from '@shopware-ag/dive/orbitcontroller'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; describe('GetCameraTransformAction', () => { it('should get camera transform', async () => { diff --git a/src/plugins/state/src/actions/camera/__test__/movecamera.test.ts b/src/plugins/state/src/actions/camera/__test__/movecamera.test.ts index 4a46bba7..b067f93c 100644 --- a/src/plugins/state/src/actions/camera/__test__/movecamera.test.ts +++ b/src/plugins/state/src/actions/camera/__test__/movecamera.test.ts @@ -1,26 +1,19 @@ import { MoveCameraAction } from '../movecamera.ts'; import { EntitySchema } from '@shopware-ag/dive'; import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; import { DIVE } from '@shopware-ag/dive'; -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - const mockStop = vi.fn(); const mockPlay = vi.fn().mockReturnThis(); const mockAnimator = { play: mockPlay, stop: mockStop, }; -const mockAnimate = vi.fn().mockReturnValue(mockAnimator); +const mockFromTargets = vi.fn().mockResolvedValue(mockAnimator); const mockGetAnimationSystem = vi.fn().mockResolvedValue({ - animate: mockAnimate, + fromTargets: mockFromTargets, Easing: { Quadratic: { Out: vi.fn(), @@ -47,7 +40,7 @@ const mockController = { describe('MoveCameraAction', () => { beforeEach(() => { vi.clearAllMocks(); - mockAnimate.mockReturnValue(mockAnimator); + mockFromTargets.mockResolvedValue(mockAnimator); }); describe('Direct Position Movement', () => { @@ -74,7 +67,7 @@ describe('MoveCameraAction', () => { expect(mockGetAnimationSystem).toHaveBeenCalled(); expect(mockEngine.clock.addTicker).toHaveBeenCalled(); - expect(mockAnimate).toHaveBeenCalledWith( + expect(mockFromTargets).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ object: mockController.object.position, @@ -92,6 +85,7 @@ describe('MoveCameraAction', () => { onComplete: expect.any(Function), }), ); + expect(mockPlay).toHaveBeenCalledTimes(1); expect(result.stop).toBeDefined(); expect(typeof result.stop).toBe('function'); @@ -117,7 +111,8 @@ describe('MoveCameraAction', () => { await action.execute(); - const onCompleteCallback = mockAnimate.mock.calls[0][2].onComplete; + const onCompleteCallback = + mockFromTargets.mock.calls[0][2].onComplete; onCompleteCallback(); expect(mockController.enabled).toBe(true); @@ -157,7 +152,7 @@ describe('MoveCameraAction', () => { const result = await action.execute(); - expect(mockAnimate).toHaveBeenCalledWith( + expect(mockFromTargets).toHaveBeenCalledWith( expect.arrayContaining([ expect.objectContaining({ object: mockController.object.position, @@ -254,7 +249,7 @@ describe('MoveCameraAction', () => { await action.execute(); - const options = mockAnimate.mock.calls[0][2]; + const options = mockFromTargets.mock.calls[0][2]; options.onUpdate(); expect(mockController.object.lookAt).toHaveBeenCalledWith( diff --git a/src/plugins/state/src/actions/camera/__test__/setcameratransform.test.ts b/src/plugins/state/src/actions/camera/__test__/setcameratransform.test.ts index 43e63556..21d65769 100644 --- a/src/plugins/state/src/actions/camera/__test__/setcameratransform.test.ts +++ b/src/plugins/state/src/actions/camera/__test__/setcameratransform.test.ts @@ -1,6 +1,6 @@ import { SetCameraTransformAction } from '../setcameratransform.ts'; import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; describe('SetCameraTransformAction', () => { it('should set camera transform', async () => { diff --git a/src/plugins/state/src/actions/camera/computeencompassingview.ts b/src/plugins/state/src/actions/camera/computeencompassingview.ts index f89b6c77..36c97c07 100644 --- a/src/plugins/state/src/actions/camera/computeencompassingview.ts +++ b/src/plugins/state/src/actions/camera/computeencompassingview.ts @@ -1,6 +1,6 @@ import { Action } from '../action.ts'; import { registerAction } from '../../ActionRegistry.ts'; -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; import { type ActionDependencies } from '../../../types/index.ts'; import { BoundingBox } from '../../../../../components/boundingbox/BoundingBox.ts'; diff --git a/src/plugins/state/src/actions/camera/getcameratransform.ts b/src/plugins/state/src/actions/camera/getcameratransform.ts index 3520bea0..383fac99 100644 --- a/src/plugins/state/src/actions/camera/getcameratransform.ts +++ b/src/plugins/state/src/actions/camera/getcameratransform.ts @@ -1,7 +1,7 @@ import { Action } from '../action.ts'; import { registerAction } from '../../ActionRegistry.ts'; import { type ActionDependencies } from '../../../types/index.ts'; -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; export const GetCameraTransformAction = Action.define< void, diff --git a/src/plugins/state/src/actions/camera/movecamera.ts b/src/plugins/state/src/actions/camera/movecamera.ts index da808599..c827e966 100644 --- a/src/plugins/state/src/actions/camera/movecamera.ts +++ b/src/plugins/state/src/actions/camera/movecamera.ts @@ -2,7 +2,7 @@ import { Action } from '../action.ts'; import { registerAction } from '../../ActionRegistry.ts'; import { type ActionDependencies } from '../../../types/index.ts'; import { isPovSchema } from '@shopware-ag/dive'; -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; export const MoveCameraAction = Action.define< | { @@ -27,6 +27,7 @@ export const MoveCameraAction = Action.define< payload, { controller, registered, getAnimationSystem, engine }, ) => { + const animationSystem = await getAnimationSystem(); let position = { x: 0, y: 0, z: 0 }; let target = { x: 0, y: 0, z: 0 }; if ('id' in payload) { @@ -50,36 +51,36 @@ export const MoveCameraAction = Action.define< target = payload.target; } - const animator = await getAnimationSystem().then((animationSystem) => { - if (!engine.clock.hasTicker(animationSystem)) { - engine.clock.addTicker(animationSystem); - } + if (!engine.clock.hasTicker(animationSystem)) { + engine.clock.addTicker(animationSystem); + } - controller.enabled = true; + controller.enabled = true; - return animationSystem.animate( - [ - { - object: controller.object.position, - to: position, - }, - { - object: controller.target, - to: target, - }, - ], - payload.duration, + const animator = await animationSystem.fromTargets( + [ + { + object: controller.object.position, + to: position, + }, { - easing: animationSystem.Easing.Quadratic.Out, - onUpdate: () => { - controller.object.lookAt(controller.target); - }, - onComplete: () => { - controller.enabled = !payload.locked; - }, + object: controller.target, + to: target, }, - ); - }); + ], + payload.duration, + { + easing: animationSystem.Easing.Quadratic.Out, + onUpdate: () => { + controller.object.lookAt(controller.target); + }, + onComplete: () => { + controller.enabled = !payload.locked; + }, + }, + ); + + animator.play(); return { stop: () => animator.stop(), diff --git a/src/plugins/state/src/actions/camera/setcameratransform.ts b/src/plugins/state/src/actions/camera/setcameratransform.ts index b0fb5a96..aa45f698 100644 --- a/src/plugins/state/src/actions/camera/setcameratransform.ts +++ b/src/plugins/state/src/actions/camera/setcameratransform.ts @@ -1,7 +1,7 @@ import { Action } from '../action.ts'; import { registerAction } from '../../ActionRegistry.ts'; import { type ActionDependencies } from '../../../types/index.ts'; -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; export const SetCameraTransformAction = Action.define< { diff --git a/src/plugins/state/src/actions/media/__test__/generatemedia.test.ts b/src/plugins/state/src/actions/media/__test__/generatemedia.test.ts index 38539bb6..c935a6a2 100644 --- a/src/plugins/state/src/actions/media/__test__/generatemedia.test.ts +++ b/src/plugins/state/src/actions/media/__test__/generatemedia.test.ts @@ -1,13 +1,6 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - import { GenerateMediaAction } from '../generatemedia.ts'; import { type EntitySchema } from '@shopware-ag/dive'; -import { Vector3 } from 'three'; +import { Vector3 } from 'three/webgpu'; import { type MediaGenerationById, type MediaGenerationByPosition, diff --git a/src/plugins/state/src/actions/object/__test__/deselectobject.test.ts b/src/plugins/state/src/actions/object/__test__/deselectobject.test.ts index 6185c051..f8f9c1b1 100644 --- a/src/plugins/state/src/actions/object/__test__/deselectobject.test.ts +++ b/src/plugins/state/src/actions/object/__test__/deselectobject.test.ts @@ -1,5 +1,5 @@ import { DeselectObjectAction } from '../deselectobject.ts'; -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; import { DIVE, type DIVESelectable, diff --git a/src/plugins/state/src/actions/object/__test__/modelloaded.test.ts b/src/plugins/state/src/actions/object/__test__/modelloaded.test.ts index eade6594..2221c59a 100644 --- a/src/plugins/state/src/actions/object/__test__/modelloaded.test.ts +++ b/src/plugins/state/src/actions/object/__test__/modelloaded.test.ts @@ -1,10 +1,3 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - import { ModelLoadedAction } from '../modelloaded.ts'; import { type EntitySchema, type ModelSchema } from '@shopware-ag/dive'; diff --git a/src/plugins/state/src/actions/object/__test__/selectobject.test.ts b/src/plugins/state/src/actions/object/__test__/selectobject.test.ts index 77f2f27a..7eca49d0 100644 --- a/src/plugins/state/src/actions/object/__test__/selectobject.test.ts +++ b/src/plugins/state/src/actions/object/__test__/selectobject.test.ts @@ -5,7 +5,7 @@ import { type EntitySchema, } from '@shopware-ag/dive'; import { SelectObjectAction } from '../selectobject.ts'; -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; import { Toolbox, SelectionState } from '@shopware-ag/dive/toolbox'; const mockSceneObject = { diff --git a/src/plugins/state/src/actions/object/__test__/setparent.test.ts b/src/plugins/state/src/actions/object/__test__/setparent.test.ts index 4969add2..d550741e 100644 --- a/src/plugins/state/src/actions/object/__test__/setparent.test.ts +++ b/src/plugins/state/src/actions/object/__test__/setparent.test.ts @@ -5,7 +5,7 @@ import { DIVESceneObject, type EntitySchema, } from '@shopware-ag/dive'; -import { Object3D } from 'three'; +import { Object3D } from 'three/webgpu'; describe('SetParentAction', () => { // Mock dependencies diff --git a/src/plugins/state/src/actions/object/selectobject.ts b/src/plugins/state/src/actions/object/selectobject.ts index 539872ba..d85d63af 100644 --- a/src/plugins/state/src/actions/object/selectobject.ts +++ b/src/plugins/state/src/actions/object/selectobject.ts @@ -1,4 +1,4 @@ -import { type Object3D } from 'three'; +import { type Object3D } from 'three/webgpu'; import { Action } from '../action.ts'; import { registerAction } from '../../ActionRegistry.ts'; import { type ActionDependencies } from '../../../types/index.ts'; diff --git a/src/plugins/state/src/actions/scene/__test__/getallscenedata.test.ts b/src/plugins/state/src/actions/scene/__test__/getallscenedata.test.ts index 735fe3a1..7a789428 100644 --- a/src/plugins/state/src/actions/scene/__test__/getallscenedata.test.ts +++ b/src/plugins/state/src/actions/scene/__test__/getallscenedata.test.ts @@ -9,7 +9,7 @@ import { type PrimitiveSchema, } from '@shopware-ag/dive'; import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; -import { Color, MeshStandardMaterial, Vector3 } from 'three'; +import { Color, MeshStandardMaterial, Vector3 } from 'three/webgpu'; describe('GetAllSceneDataAction', () => { it('should get all scene data', async () => { diff --git a/src/plugins/state/src/actions/scene/__test__/updatescene.test.ts b/src/plugins/state/src/actions/scene/__test__/updatescene.test.ts index e31541de..b080b530 100644 --- a/src/plugins/state/src/actions/scene/__test__/updatescene.test.ts +++ b/src/plugins/state/src/actions/scene/__test__/updatescene.test.ts @@ -1,6 +1,6 @@ import { UpdateSceneAction } from '../updatescene.ts'; import { DIVE, DIVEScene } from '@shopware-ag/dive'; -import { Color, MeshStandardMaterial } from 'three'; +import { Color, MeshStandardMaterial } from 'three/webgpu'; describe('UpdateSceneAction', () => { it('should update scene properties', async () => { diff --git a/src/plugins/state/src/actions/scene/getallscenedata.ts b/src/plugins/state/src/actions/scene/getallscenedata.ts index 06675270..42285a6b 100644 --- a/src/plugins/state/src/actions/scene/getallscenedata.ts +++ b/src/plugins/state/src/actions/scene/getallscenedata.ts @@ -2,7 +2,7 @@ import { Action } from '../action.ts'; import { registerAction } from '../../ActionRegistry.ts'; import { type ActionDependencies } from '../../../types/index.ts'; import { type StateSceneData } from '../../../types/StateSceneData.ts'; -import { Color, MeshStandardMaterial } from 'three'; +import { Color, MeshStandardMaterial } from 'three/webgpu'; import { GroupSchema, LightSchema, diff --git a/src/plugins/state/src/actions/scene/updatescene.ts b/src/plugins/state/src/actions/scene/updatescene.ts index 747e1e1a..ed4994e2 100644 --- a/src/plugins/state/src/actions/scene/updatescene.ts +++ b/src/plugins/state/src/actions/scene/updatescene.ts @@ -1,7 +1,7 @@ import { Action } from '../action.ts'; import { registerAction } from '../../ActionRegistry.ts'; import { ActionDependencies } from '../../../types/index.ts'; -import { Color, MeshStandardMaterial } from 'three'; +import { Color, MeshStandardMaterial } from 'three/webgpu'; export const UpdateSceneAction = Action.define< Partial<{ diff --git a/src/plugins/state/src/actions/toolbox/__test__/usetool.test.ts b/src/plugins/state/src/actions/toolbox/__test__/usetool.test.ts index c98e98df..761c1c66 100644 --- a/src/plugins/state/src/actions/toolbox/__test__/usetool.test.ts +++ b/src/plugins/state/src/actions/toolbox/__test__/usetool.test.ts @@ -1,8 +1,8 @@ import { UseToolAction } from '../usetool.ts'; -const mockUseTool = vi.fn(); +const mockEnableTool = vi.fn(); const mockGetToolbox = vi.fn().mockResolvedValue({ - useTool: mockUseTool, + enableTool: mockEnableTool, }); describe('UseToolAction', () => { @@ -18,6 +18,6 @@ describe('UseToolAction', () => { await action.execute(); // Verify results - expect(mockUseTool).toHaveBeenCalledWith('select'); + expect(mockEnableTool).toHaveBeenCalledWith('select'); }); }); diff --git a/src/plugins/state/src/actions/toolbox/usetool.ts b/src/plugins/state/src/actions/toolbox/usetool.ts index 552a6122..57cacaae 100644 --- a/src/plugins/state/src/actions/toolbox/usetool.ts +++ b/src/plugins/state/src/actions/toolbox/usetool.ts @@ -11,7 +11,7 @@ export const UseToolAction = Action.define< description: 'Activates a specific tool from the toolbox.', execute: async (payload, { getToolbox }) => { const instance = await getToolbox(); - instance.useTool(payload.tool); + instance.enableTool(payload.tool); }, }); diff --git a/src/plugins/state/types/StateSceneData.ts b/src/plugins/state/types/StateSceneData.ts index 6aac1cc6..eb859af8 100644 --- a/src/plugins/state/types/StateSceneData.ts +++ b/src/plugins/state/types/StateSceneData.ts @@ -1,4 +1,4 @@ -import type { Vector3Like } from 'three'; +import type { Vector3Like } from 'three/webgpu'; import type { GroupSchema, LightSchema, diff --git a/src/plugins/toolbox/index.ts b/src/plugins/toolbox/index.ts index 6869bd70..d6bb9134 100644 --- a/src/plugins/toolbox/index.ts +++ b/src/plugins/toolbox/index.ts @@ -11,9 +11,6 @@ export * from './src/transform/TransformTool.ts'; export * from './src/drag/DragTool.ts'; export * from './src/drag/DraggableEvent.ts'; -// Legacy exports (deprecated) - exclude DraggableEvent to avoid conflict -export { DIVEBaseTool } from './src/BaseTool.ts'; - // Legacy aliases export { SelectTool as DIVESelectTool } from './src/select/SelectTool.ts'; export { TransformTool as DIVETransformTool } from './src/transform/TransformTool.ts'; diff --git a/src/plugins/toolbox/src/BaseTool.ts b/src/plugins/toolbox/src/BaseTool.ts deleted file mode 100644 index 88cfd1eb..00000000 --- a/src/plugins/toolbox/src/BaseTool.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { - type Intersection, - type Object3D, - Raycaster, - Vector2, - Vector3, -} from 'three'; -import { - DIVEScene, - DIVEDraggable, - DIVEHoverable, - PRODUCT_LAYER_MASK, - UI_LAYER_MASK, - findInterface, -} from '@shopware-ag/dive'; -import { type OrbitController } from '@shopware-ag/dive/orbitcontroller'; - -/** - * @deprecated Use DraggableEvent from './drag/DraggableEvent.ts' instead. - * This type will be removed in a future version. - */ -export type DraggableEvent = { - dragStart: Vector3; - dragCurrent: Vector3; - dragEnd: Vector3; - dragDelta: Vector3; -}; - -/** - * @deprecated Use the new Tool interface and individual tools (HoverTool, SelectTool, TransformTool, DragTool) instead. - * This class will be removed in a future version. - * - * The new architecture uses composition over inheritance: - * - Tool interface for lightweight event handlers - * - Toolbox as event dispatcher supporting multiple active tools - * - Individual tools: HoverTool, SelectTool, TransformTool, DragTool - */ -/* eslint-disable @typescript-eslint/no-unused-vars */ -export abstract class DIVEBaseTool { - readonly POINTER_DRAG_THRESHOLD: number = 0.001; - - public name: string; - - protected _canvas: HTMLElement; - protected _scene: DIVEScene; - protected _controller: OrbitController; - - // general pointer members - protected _pointer: Vector2; - - protected get _pointerAnyDown(): boolean { - return ( - this._pointerPrimaryDown || - this._pointerMiddleDown || - this._pointerSecondaryDown - ); - } - protected _pointerPrimaryDown: boolean; - protected _pointerMiddleDown: boolean; - protected _pointerSecondaryDown: boolean; - protected _lastPointerDown: Vector2; - protected _lastPointerUp: Vector2; - - // raycast members - protected _raycaster: Raycaster; - protected _intersects: Intersection[]; - - // hovering members - protected _hovered: (Object3D & DIVEHoverable) | null; - - // dragging members - protected _dragging: boolean; - protected _dragStart: Vector3; - protected _dragCurrent: Vector3; - protected _dragEnd: Vector3; - protected _dragDelta: Vector3; - protected _draggable: DIVEDraggable | null; - protected _dragRaycastOnObjects: Object3D[] | null; - - protected constructor(scene: DIVEScene, controller: OrbitController) { - this.name = 'BaseTool'; - - this._canvas = controller.domElement; - this._scene = scene; - this._controller = controller; - - this._pointer = new Vector2(); - - this._pointerPrimaryDown = false; - this._pointerMiddleDown = false; - this._pointerSecondaryDown = false; - - this._lastPointerDown = new Vector2(); - this._lastPointerUp = new Vector2(); - - this._raycaster = new Raycaster(); - this._raycaster.layers.mask = PRODUCT_LAYER_MASK | UI_LAYER_MASK; - this._intersects = []; - - this._hovered = null; - - this._dragging = false; - this._dragStart = new Vector3(); - this._dragCurrent = new Vector3(); - this._dragEnd = new Vector3(); - this._dragDelta = new Vector3(); - this._draggable = null; - this._dragRaycastOnObjects = null; - } - - public activate(): void {} - - public deactivate(): void {} - - public onPointerDown(e: PointerEvent): void { - switch (e.button) { - case 0: { - this._pointerPrimaryDown = true; - break; - } - case 1: { - this._pointerMiddleDown = true; - break; - } - case 2: { - this._pointerSecondaryDown = true; - break; - } - default: { - console.warn( - 'DIVEBaseTool.onPointerDown: Unknown button: ' + e.button, - ); - } - } - - this._lastPointerDown.copy(this._pointer); - - this._draggable = - findInterface( - this._intersects[0]?.object, - 'isDraggable', - ) || null; - } - - public onDragStart(e: PointerEvent): void { - if (!this._draggable) return; - - if (this._dragRaycastOnObjects !== null) { - this._intersects = this._raycaster.intersectObjects( - this._dragRaycastOnObjects, - true, - ); - } - - if (this._intersects.length === 0) return; - - this._dragStart.copy(this._intersects[0].point.clone()); - this._dragCurrent.copy(this._intersects[0].point.clone()); - this._dragEnd.copy(this._dragStart.clone()); - this._dragDelta.set(0, 0, 0); - - if (this._draggable && this._draggable.onDragStart) { - this._draggable.onDragStart({ - dragStart: this._dragStart, - dragCurrent: this._dragCurrent, - dragEnd: this._dragEnd, - dragDelta: this._dragDelta, - }); - - this._dragging = true; - this._controller.enabled = false; - } - } - - public onPointerMove(e: PointerEvent): void { - // update pointer - this._pointer.x = (e.offsetX / this._canvas.clientWidth) * 2 - 1; - this._pointer.y = -(e.offsetY / this._canvas.clientHeight) * 2 + 1; - - // set raycaster - this._raycaster.setFromCamera(this._pointer, this._controller.object); - - // refresh intersects - this._intersects = this.raycast(this._scene.children); - - // handle hover - const hoverable = findInterface( - this._intersects[0]?.object, - 'isHoverable', - ); - if (this._intersects[0] && hoverable) { - if (!this._hovered) { - if (hoverable.onPointerEnter) { - hoverable.onPointerEnter(this._intersects[0]); - } - this._hovered = hoverable; - return; - } - - if (this._hovered.uuid !== hoverable.uuid) { - if (this._hovered.onPointerLeave) - this._hovered.onPointerLeave(); - if (hoverable.onPointerEnter) - hoverable.onPointerEnter(this._intersects[0]); - this._hovered = hoverable; - return; - } - - if (hoverable.onPointerOver) - hoverable.onPointerOver(this._intersects[0]); - this._hovered = hoverable; - } else { - if (this._hovered) { - if (this._hovered.onPointerLeave) - this._hovered.onPointerLeave(); - } - - this._hovered = null; - } - - // handle drag - if (this._pointerAnyDown) { - if (!this._dragging) { - this.onDragStart(e); - } - - this.onDrag(e); - } - } - - public onDrag(e: PointerEvent): void { - if (this._dragRaycastOnObjects !== null) { - this._intersects = this._raycaster.intersectObjects( - this._dragRaycastOnObjects, - true, - ); - } - const intersect = this._intersects[0]; - if (!intersect) return; - - this._dragCurrent.copy(intersect.point.clone()); - this._dragEnd.copy(intersect.point.clone()); - this._dragDelta.subVectors( - this._dragCurrent.clone(), - this._dragStart.clone(), - ); - - if (this._draggable && this._draggable.onDrag) { - this._draggable.onDrag({ - dragStart: this._dragStart, - dragCurrent: this._dragCurrent, - dragEnd: this._dragEnd, - dragDelta: this._dragDelta, - }); - } - } - - public onPointerUp(e: PointerEvent): void { - if (this.pointerWasDragged() || this._dragging) { - if (this._draggable) { - this.onDragEnd(e); - } - } else { - this.onClick(e); - } - - switch (e.button) { - case 0: - this._pointerPrimaryDown = false; - break; - case 1: - this._pointerMiddleDown = false; - break; - case 2: - this._pointerSecondaryDown = false; - break; - } - - this._lastPointerUp.copy(this._pointer); - } - - public onClick(e: PointerEvent): void {} - - public onDragEnd(e: PointerEvent): void { - const intersect = this._intersects[0]; - if (intersect) { - this._dragEnd.copy(intersect.point.clone()); - this._dragCurrent.copy(intersect.point.clone()); - this._dragDelta.subVectors( - this._dragCurrent.clone(), - this._dragStart.clone(), - ); - } - - if (this._draggable && this._draggable.onDragEnd) { - this._draggable.onDragEnd({ - dragStart: this._dragStart, - dragCurrent: this._dragCurrent, - dragEnd: this._dragEnd, - dragDelta: this._dragDelta, - }); - } - - this._draggable = null; - this._dragging = false; - - this._dragStart.set(0, 0, 0); - this._dragCurrent.set(0, 0, 0); - this._dragEnd.set(0, 0, 0); - this._dragDelta.set(0, 0, 0); - - this._controller.enabled = true; - } - - public onWheel(e: WheelEvent): void {} - - protected raycast(objects?: Object3D[]): Intersection[] { - const sceneObjects = objects || this._scene.children; - const filteredObjects = sceneObjects.filter( - (i) => i.visible && 'isMesh' in i && i.isMesh, - ); - - return this._raycaster.intersectObjects(filteredObjects, true); - } - - private pointerWasDragged(): boolean { - return ( - this._lastPointerDown.distanceTo(this._pointer) > - this.POINTER_DRAG_THRESHOLD - ); - } -} diff --git a/src/plugins/toolbox/src/PointerContext.ts b/src/plugins/toolbox/src/PointerContext.ts index 1b015ce7..de8bb429 100644 --- a/src/plugins/toolbox/src/PointerContext.ts +++ b/src/plugins/toolbox/src/PointerContext.ts @@ -1,4 +1,4 @@ -import { type Intersection, type Vector2 } from 'three'; +import { type Intersection, type Vector2 } from 'three/webgpu'; /** * Context object passed to tools on pointer events. diff --git a/src/plugins/toolbox/src/SelectionState.ts b/src/plugins/toolbox/src/SelectionState.ts index 03622bc2..7393ff1d 100644 --- a/src/plugins/toolbox/src/SelectionState.ts +++ b/src/plugins/toolbox/src/SelectionState.ts @@ -1,4 +1,4 @@ -import { type Object3D } from 'three'; +import { type Object3D } from 'three/webgpu'; import { type DIVESelectable } from '@shopware-ag/dive'; type SelectionChangeCallback = ( diff --git a/src/plugins/toolbox/src/Toolbox.ts b/src/plugins/toolbox/src/Toolbox.ts index 76035c1c..7952b098 100644 --- a/src/plugins/toolbox/src/Toolbox.ts +++ b/src/plugins/toolbox/src/Toolbox.ts @@ -1,4 +1,4 @@ -import { Raycaster, Vector2, type Intersection, Layers } from 'three'; +import { Raycaster, Vector2, type Intersection, Layers } from 'three/webgpu'; import { type DIVEScene, PRODUCT_LAYER_MASK, @@ -330,34 +330,4 @@ export class Toolbox { POINTER_DRAG_THRESHOLD ); } - - // ============ Legacy API Compatibility ============ - - /** - * @deprecated Use enableTool/disableTool instead. - * Enable or disable a tool by type. - */ - public useTool(tool: ToolType): void { - // Enable all standard tools for the given type - const allTools: ToolType[] = [ - 'hover', - 'select', - 'transform', - 'drag', - ]; - - for (const t of allTools) { - if (t === tool || tool === 'select') { - this.enableTool(t); - } - } - } - - /** - * @deprecated Use getActiveTools instead. - * Get the first active tool (for legacy compatibility). - */ - public getActiveTool(): Tool | null { - return this._sortedActiveTools[0] || null; - } } diff --git a/src/plugins/toolbox/src/__test__/BaseTool.test.ts b/src/plugins/toolbox/src/__test__/BaseTool.test.ts deleted file mode 100644 index a8ad454a..00000000 --- a/src/plugins/toolbox/src/__test__/BaseTool.test.ts +++ /dev/null @@ -1,809 +0,0 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - -const RaycasterIntersectObjectMock = vi.fn(() => []); - -vi.mock('three', async () => { - const actual = await vi.importActual('three'); - - const Raycaster = vi.fn(function (this: any) { - this.layers = { mask: 0 }; - this.setFromCamera = vi.fn(() => this); - this.intersectObjects = RaycasterIntersectObjectMock; - return this; - }); - - return { - ...actual, - Raycaster, - }; -}); - -import { DIVEBaseTool } from '../BaseTool.ts'; -import { - Camera, - Vector2, - Vector3, - type Intersection, - type Object3D, -} from 'three'; -import { - DIVEScene, - type DIVEHoverable, - type DIVEDraggable, -} from '@shopware-ag/dive'; -import { OrbitController } from '@shopware-ag/dive/orbitcontroller'; - -/** - * @jest-environment jsdom - */ - -const mock_Canvas = { - width: 0, - height: 0, - getContext: vi.fn(), - clientWidth: 1000, - clientHeight: 1000, - offsetLeft: 0, - offsetTop: 0, -}; - -const mockController = { - domElement: mock_Canvas, - object: { - isPerspectiveCamera: true, - type: 'cameraP', - }, -} as unknown as OrbitController; - -const mockScene = { - children: [], -} as unknown as DIVEScene; - -const abstractWrapper = class Wrapper extends DIVEBaseTool { - constructor(scene: DIVEScene, controller: OrbitController) { - super(scene, controller); - this.name = 'DIVEBaseTool'; - } -}; - -describe('dive/toolbox/DIVEBaseTool', () => { - afterEach(() => { - vi.clearAllMocks(); - }); - - it('should instantiate', () => { - const baseTool = new abstractWrapper(mockScene, mockController); - expect(baseTool).toBeDefined(); - }); - - it('should Activate', () => { - const baseTool = new abstractWrapper(mockScene, mockController); - expect(() => baseTool.activate()).not.toThrow(); - }); - - it('should Deactivate', () => { - const baseTool = new abstractWrapper(mockScene, mockController); - expect(() => baseTool.deactivate()).not.toThrow(); - }); - - it('should raycast', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - RaycasterIntersectObjectMock.mockImplementationOnce(() => { - return [ - { - object: { - visible: true, - }, - } as unknown as Intersection, - ]; - }); - expect(() => toolBox['raycast']()).not.toThrow(); - expect(RaycasterIntersectObjectMock).toHaveBeenCalled(); - }); - - it('should raycast with selection of objects', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - const spy = vi - .spyOn(toolBox['_raycaster'], 'intersectObjects') - .mockImplementationOnce(() => { - return [ - { - object: { - visible: true, - }, - } as unknown as Intersection, - ]; - }); - expect(() => toolBox['raycast']([])).not.toThrow(); - expect(spy).toHaveBeenCalled(); - }); - - it('should return correct pointerAnyDown', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(toolBox).toBeDefined(); - expect(toolBox['_pointerAnyDown']).toBeDefined(); - expect(toolBox['_pointerAnyDown']).toBe(false); - - toolBox['_pointerPrimaryDown'] = false; - toolBox['_pointerMiddleDown'] = false; - toolBox['_pointerSecondaryDown'] = false; - expect(toolBox['_pointerAnyDown']).toBe(false); - - toolBox['_pointerPrimaryDown'] = true; - toolBox['_pointerMiddleDown'] = false; - toolBox['_pointerSecondaryDown'] = false; - expect(toolBox['_pointerAnyDown']).toBe(true); - - toolBox['_pointerPrimaryDown'] = false; - toolBox['_pointerMiddleDown'] = true; - toolBox['_pointerSecondaryDown'] = false; - expect(toolBox['_pointerAnyDown']).toBe(true); - - toolBox['_pointerPrimaryDown'] = false; - toolBox['_pointerMiddleDown'] = false; - toolBox['_pointerSecondaryDown'] = true; - expect(toolBox['_pointerAnyDown']).toBe(true); - }); - - it('should execute onPointerDown correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(() => - toolBox.onPointerDown({ button: 0 } as PointerEvent), - ).not.toThrow(); - - expect(() => - toolBox.onPointerDown({ button: 1 } as PointerEvent), - ).not.toThrow(); - - expect(() => - toolBox.onPointerDown({ button: 2 } as PointerEvent), - ).not.toThrow(); - - const spy = vi - .spyOn(console, 'warn') - .mockImplementation((message: string) => {}); - expect(() => - toolBox.onPointerDown({ button: 666 } as PointerEvent), - ).not.toThrow(); - expect(spy).toHaveBeenCalled(); - - toolBox['_intersects'] = [ - { - distance: 1, - point: { - clone() { - return { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3; - }, - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid2', - isHoverable: true, - onPointerEnter() { - return; - }, - } as unknown as Object3D & DIVEHoverable, - }, - ]; - - expect(() => - toolBox.onPointerDown({ button: 0 } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerDown({ button: 1 } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerDown({ button: 2 } as PointerEvent), - ).not.toThrow(); - }); - - it('should execute onPointerMove correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - vi.spyOn(toolBox['_raycaster'], 'setFromCamera').mockImplementation( - (coords: Vector2, camera: Camera) => toolBox['_raycaster'], - ); - - const spy = vi.spyOn(toolBox['_raycaster'], 'intersectObjects'); - - // test with no hit with hovered object before - spy.mockReturnValue([]); - - toolBox['_hovered'] = { - uuid: 'uuid', - onPointerLeave() { - return; - }, - } as Object3D & DIVEHoverable; - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 1, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 2, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - // test with no hovered object - spy.mockReturnValue([ - { - distance: 1, - point: { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid', - isHoverable: true, - visible: true, - } as Object3D & DIVEHoverable, - }, - ]); - - toolBox['_hovered'] = null; - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 1, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 2, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - // test with no hovered object with onPointerEnter - spy.mockReturnValue([ - { - distance: 1, - point: { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid', - isHoverable: true, - visible: true, - onPointerEnter() { - return; - }, - } as unknown as Object3D & DIVEHoverable, - }, - ]); - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 1, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 2, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - // test with same hovered object - spy.mockReturnValue([ - { - distance: 1, - point: { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid', - isHoverable: true, - visible: true, - onPointerOver() { - return; - }, - } as unknown as Object3D & DIVEHoverable, - }, - ]); - - toolBox['_hovered'] = { - uuid: 'uuid', - visible: true, - onPointerLeave() { - return; - }, - } as Object3D & DIVEHoverable; - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 1, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 2, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - // test with different hovered object - spy.mockReturnValue([ - { - distance: 1, - point: new Vector3(1, 1, 1), - object: { - uuid: 'uuid2', - isHoverable: true, - visible: true, - onPointerEnter() { - return; - }, - } as unknown as Object3D & DIVEHoverable, - }, - ]); - - toolBox['_hovered'] = { - uuid: 'uuid', - visible: true, - onPointerLeave() { - return; - }, - } as Object3D & DIVEHoverable; - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 1, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 2, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - // test with pointer down - toolBox['_pointerPrimaryDown'] = true; - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 1, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 2, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - // test with pointer down while already dragging - toolBox['_pointerPrimaryDown'] = true; - toolBox['_dragging'] = true; - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 1, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerMove({ - button: 2, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - spy.mockRestore(); - }); - - it('should execute onPointerUp correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(() => - toolBox.onPointerUp({ button: 0 } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerUp({ button: 1 } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerUp({ button: 2 } as PointerEvent), - ).not.toThrow(); - - toolBox['pointerWasDragged'] = () => { - return true; - }; - toolBox['_dragging'] = true; - toolBox['_intersects'] = [ - { - distance: 1, - point: { - clone() { - return { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3; - }, - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid2', - isHoverable: true, - onPointerEnter() { - return; - }, - } as unknown as Object3D & DIVEHoverable, - }, - ]; - toolBox['_draggable'] = { - onDragEnd() { - return; - }, - } as unknown as Object3D & DIVEDraggable; - expect(() => - toolBox.onPointerUp({ button: 0 } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerUp({ button: 1 } as PointerEvent), - ).not.toThrow(); - expect(() => - toolBox.onPointerUp({ button: 2 } as PointerEvent), - ).not.toThrow(); - }); - - it('should execute onDragStart correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - - toolBox['_draggable'] = { - onDragStart() { - return; - }, - } as unknown as Object3D & DIVEDraggable; - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - - toolBox['_dragRaycastOnObjects'] = []; - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValueOnce( - [], - ); - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValueOnce( - [], - ); - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValueOnce( - [ - { - distance: 1, - point: { - clone() { - return { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3; - }, - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid2', - isHoverable: true, - onPointerEnter() { - return; - }, - } as unknown as Object3D & DIVEHoverable, - }, - ], - ); - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - - toolBox['_draggable'] = { - onDragStart() { - return; - }, - } as unknown as Object3D & DIVEDraggable; - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValueOnce( - [], - ); - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - }); - - it('should execute onDrag correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(() => toolBox.onDrag({} as PointerEvent)).not.toThrow(); - - toolBox['_dragRaycastOnObjects'] = []; - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValueOnce( - [], - ); - expect(() => toolBox.onDrag({} as PointerEvent)).not.toThrow(); - - toolBox['_draggable'] = { - onDrag() { - return; - }, - } as unknown as Object3D & DIVEDraggable; - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValueOnce( - [ - { - distance: 1, - point: { - clone() { - return { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3; - }, - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid2', - isHoverable: true, - onPointerEnter() { - return; - }, - } as unknown as Object3D & DIVEHoverable, - }, - ], - ); - expect(() => toolBox.onDrag({} as PointerEvent)).not.toThrow(); - }); - - it('should execute onCLick correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(() => toolBox.onClick({} as PointerEvent)).not.toThrow(); - }); - - it('should execute onDragEnd correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(() => toolBox.onDragEnd({} as PointerEvent)).not.toThrow(); - }); - - it('should execute onWheel correctly', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - expect(() => toolBox.onWheel({} as WheelEvent)).not.toThrow(); - }); - - it('should handle onPointerMove with no intersects', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - vi.spyOn(toolBox['_raycaster'], 'setFromCamera').mockImplementation( - (coords: Vector2, camera: Camera) => { - return toolBox['_raycaster']; - }, - ); - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValue([]); - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - }); - - it('should handle onPointerMove with non-hoverable object', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - vi.spyOn(toolBox['_raycaster'], 'setFromCamera').mockImplementation( - (coords: Vector2, camera: Camera) => { - return toolBox['_raycaster']; - }, - ); - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValue([ - { - distance: 1, - point: { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: { - uuid: 'uuid', - visible: true, - } as Object3D, - }, - ]); - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - }); - - it('should handle onDrag with no intersects', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - toolBox['_draggable'] = { - onDrag() { - return; - }, - } as unknown as Object3D & DIVEDraggable; - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValue([]); - - expect(() => toolBox.onDrag({} as PointerEvent)).not.toThrow(); - }); - - it('should handle onDragEnd with no intersects', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - toolBox['_draggable'] = { - onDragEnd() { - return; - }, - } as unknown as Object3D & DIVEDraggable; - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValue([]); - - expect(() => toolBox.onDragEnd({} as PointerEvent)).not.toThrow(); - }); - - it('should handle onPointerUp with no drag', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - toolBox['pointerWasDragged'] = () => false; - toolBox['_dragging'] = false; - - expect(() => - toolBox.onPointerUp({ button: 0 } as PointerEvent), - ).not.toThrow(); - }); - - it('should handle onPointerUp with drag but no draggable', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - toolBox['pointerWasDragged'] = () => true; - toolBox['_dragging'] = true; - toolBox['_draggable'] = null; - - expect(() => - toolBox.onPointerUp({ button: 0 } as PointerEvent), - ).not.toThrow(); - }); - - it('should handle onDragStart with no draggable', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - toolBox['_draggable'] = null; - - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - }); - - it('should handle onDragStart with draggable but no intersects', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - toolBox['_draggable'] = { - onDragStart() { - return; - }, - } as unknown as Object3D & DIVEDraggable; - toolBox['_dragRaycastOnObjects'] = []; - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValue([]); - - expect(() => toolBox.onDragStart({} as PointerEvent)).not.toThrow(); - }); - - it('should handle onPointerMove with hoverable object and onPointerEnter', () => { - const toolBox = new abstractWrapper(mockScene, mockController); - vi.spyOn(toolBox['_raycaster'], 'setFromCamera').mockImplementation( - (coords: Vector2, camera: Camera) => { - return toolBox['_raycaster']; - }, - ); - - const mockHoverable = { - uuid: 'uuid', - isHoverable: true, - visible: true, - onPointerEnter: vi.fn(), - } as unknown as Object3D & DIVEHoverable; - - vi.spyOn(toolBox['_raycaster'], 'intersectObjects').mockReturnValue([ - { - distance: 1, - point: { - x: 1, - y: 1, - z: 1, - } as unknown as Vector3, - object: mockHoverable, - }, - ]); - - expect(() => - toolBox.onPointerMove({ - button: 0, - offsetX: 100, - offsetY: 100, - } as PointerEvent), - ).not.toThrow(); - - expect(mockHoverable.onPointerEnter).toHaveBeenCalled(); - }); -}); diff --git a/src/plugins/toolbox/src/__test__/Toolbox.test.ts b/src/plugins/toolbox/src/__test__/Toolbox.test.ts index bf840b32..ccec6fd1 100644 --- a/src/plugins/toolbox/src/__test__/Toolbox.test.ts +++ b/src/plugins/toolbox/src/__test__/Toolbox.test.ts @@ -23,8 +23,8 @@ class MockPointerEvent extends MouseEvent { } globalThis.PointerEvent = MockPointerEvent as unknown as typeof PointerEvent; -vi.mock('three', async () => { - const actual = await vi.importActual('three'); +vi.mock('three/webgpu', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, Layers: vi.fn().mockImplementation(() => ({ @@ -437,39 +437,6 @@ describe('Toolbox', () => { }); }); - describe('legacy API', () => { - it('should enable tools via useTool', () => { - toolbox.useTool('hover'); - - expect(toolbox.isToolEnabled('hover')).toBe(true); - }); - - it('should enable all tools when useTool is called with select', () => { - toolbox.useTool('select'); - - expect(toolbox.isToolEnabled('hover')).toBe(true); - expect(toolbox.isToolEnabled('select')).toBe(true); - expect(toolbox.isToolEnabled('transform')).toBe(true); - expect(toolbox.isToolEnabled('drag')).toBe(true); - }); - - it('should return first active tool via getActiveTool', () => { - toolbox.enableTool('hover'); - toolbox.enableTool('transform'); - - const activeTool = toolbox.getActiveTool(); - - // transform has higher priority (5 vs 20) - expect(activeTool?.name).toBe('transform'); - }); - - it('should return null from getActiveTool when no tools active', () => { - const activeTool = toolbox.getActiveTool(); - - expect(activeTool).toBeNull(); - }); - }); - describe('edge cases', () => { it('should not fail when disabling non-active tool', () => { expect(() => toolbox.disableTool('hover')).not.toThrow(); diff --git a/src/plugins/toolbox/src/drag/DragTool.ts b/src/plugins/toolbox/src/drag/DragTool.ts index a45eff45..b4697ad8 100644 --- a/src/plugins/toolbox/src/drag/DragTool.ts +++ b/src/plugins/toolbox/src/drag/DragTool.ts @@ -1,4 +1,9 @@ -import { type Object3D, type Intersection, Raycaster, Vector3 } from 'three'; +import { + type Object3D, + type Intersection, + Raycaster, + Vector3, +} from 'three/webgpu'; import { type DIVEDraggable, findInterface } from '@shopware-ag/dive'; import { type OrbitController } from '@shopware-ag/dive/orbitcontroller'; import { type Tool } from '../Tool.ts'; diff --git a/src/plugins/toolbox/src/drag/DraggableEvent.ts b/src/plugins/toolbox/src/drag/DraggableEvent.ts index 00b03c4e..c276b74c 100644 --- a/src/plugins/toolbox/src/drag/DraggableEvent.ts +++ b/src/plugins/toolbox/src/drag/DraggableEvent.ts @@ -1,4 +1,4 @@ -import { type Vector3 } from 'three'; +import { type Vector3 } from 'three/webgpu'; /** * Event data passed to DIVEDraggable callbacks during drag operations. diff --git a/src/plugins/toolbox/src/drag/__test__/DragTool.test.ts b/src/plugins/toolbox/src/drag/__test__/DragTool.test.ts index 9986f2b4..20d2fbad 100644 --- a/src/plugins/toolbox/src/drag/__test__/DragTool.test.ts +++ b/src/plugins/toolbox/src/drag/__test__/DragTool.test.ts @@ -1,16 +1,23 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); +import { Vector2, Vector3, type Object3D } from 'three/webgpu'; +import { DragTool } from '../DragTool.ts'; +import { type PointerContext } from '../../PointerContext.ts'; +import { type DIVEDraggable } from '@shopware-ag/dive'; +import { type OrbitController } from '@shopware-ag/dive/orbitcontroller'; + +/** + * @vitest-environment jsdom + */ + +const RaycasterIntersectObjectMock = vi.fn(() => []); -vi.mock('three', async () => { - const actual = await vi.importActual('three'); +vi.mock('three/webgpu', async () => { + const actual = + await vi.importActual('three/webgpu'); const Raycaster = vi.fn(function (this: any) { + this.layers = { mask: 0 }; this.setFromCamera = vi.fn(() => this); - this.intersectObjects = vi.fn(() => []); + this.intersectObjects = RaycasterIntersectObjectMock; return this; }); @@ -20,16 +27,6 @@ vi.mock('three', async () => { }; }); -import { Vector2, Vector3, type Object3D } from 'three'; -import { DragTool } from '../DragTool.ts'; -import { type PointerContext } from '../../PointerContext.ts'; -import { type DIVEDraggable } from '@shopware-ag/dive'; -import { type OrbitController } from '@shopware-ag/dive/orbitcontroller'; - -/** - * @vitest-environment jsdom - */ - // Mock PointerEvent for jsdom class MockPointerEvent extends MouseEvent { constructor(type: string, props?: PointerEventInit) { @@ -77,6 +74,7 @@ describe('DragTool', () => { afterEach(() => { vi.clearAllMocks(); + RaycasterIntersectObjectMock.mockReset(); }); describe('properties', () => { diff --git a/src/plugins/toolbox/src/hover/HoverTool.ts b/src/plugins/toolbox/src/hover/HoverTool.ts index f41ae9a6..ee0b1a5a 100644 --- a/src/plugins/toolbox/src/hover/HoverTool.ts +++ b/src/plugins/toolbox/src/hover/HoverTool.ts @@ -1,4 +1,4 @@ -import { type Object3D } from 'three'; +import { type Object3D } from 'three/webgpu'; import { type DIVEHoverable, findInterface } from '@shopware-ag/dive'; import { type Tool } from '../Tool.ts'; import { type PointerContext } from '../PointerContext.ts'; diff --git a/src/plugins/toolbox/src/hover/__test__/HoverTool.test.ts b/src/plugins/toolbox/src/hover/__test__/HoverTool.test.ts index a0f1af55..6d38d68d 100644 --- a/src/plugins/toolbox/src/hover/__test__/HoverTool.test.ts +++ b/src/plugins/toolbox/src/hover/__test__/HoverTool.test.ts @@ -1,11 +1,4 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - -import { Vector2 } from 'three'; +import { Vector2 } from 'three/webgpu'; import { HoverTool } from '../HoverTool.ts'; import { type PointerContext } from '../../PointerContext.ts'; import { type DIVEHoverable } from '@shopware-ag/dive'; diff --git a/src/plugins/toolbox/src/select/SelectTool.ts b/src/plugins/toolbox/src/select/SelectTool.ts index b2941f3b..6df33172 100644 --- a/src/plugins/toolbox/src/select/SelectTool.ts +++ b/src/plugins/toolbox/src/select/SelectTool.ts @@ -1,4 +1,4 @@ -import { type Object3D } from 'three'; +import { type Object3D } from 'three/webgpu'; import { type DIVESelectable, findInterface } from '@shopware-ag/dive'; import { type Tool } from '../Tool.ts'; import { type PointerContext } from '../PointerContext.ts'; diff --git a/src/plugins/toolbox/src/select/__test__/SelectTool.test.ts b/src/plugins/toolbox/src/select/__test__/SelectTool.test.ts index e38a8ea0..6efe4794 100644 --- a/src/plugins/toolbox/src/select/__test__/SelectTool.test.ts +++ b/src/plugins/toolbox/src/select/__test__/SelectTool.test.ts @@ -1,11 +1,4 @@ -vi.mock('@shopware-ag/dive/shader', () => ({ - DIVEShaderLib: { - grid: { uniforms: {}, vertexShader: '', fragmentShader: '' }, - }, - DIVEShaderMaterial: vi.fn(), -})); - -import { Vector2, type Object3D } from 'three'; +import { Vector2, type Object3D } from 'three/webgpu'; import { SelectTool } from '../SelectTool.ts'; import { SelectionState } from '../../SelectionState.ts'; import { type PointerContext } from '../../PointerContext.ts'; diff --git a/src/plugins/toolbox/src/transform/TransformTool.ts b/src/plugins/toolbox/src/transform/TransformTool.ts index b337faae..8aa064fa 100644 --- a/src/plugins/toolbox/src/transform/TransformTool.ts +++ b/src/plugins/toolbox/src/transform/TransformTool.ts @@ -4,7 +4,7 @@ import { type Mesh, type MeshBasicMaterial, EventDispatcher, -} from 'three'; +} from 'three/webgpu'; import { AxesColorBlue, AxesColorGreen, @@ -52,6 +52,7 @@ export class TransformTool private _controller: OrbitController; private _selectionState: SelectionState; private _gizmo: TransformControls; + private _gizmoHelper: Object3D; private _scaleLinked: boolean = false; private _gizmoVisible: boolean = true; @@ -71,7 +72,8 @@ export class TransformTool this._selectionState = selectionState; this._gizmo = this.initGizmo(); - this._scene.add(this._gizmo); + this._gizmoHelper = this._gizmo.getHelper() as unknown as Object3D; + this._scene.add(this._gizmoHelper); // Bind selection change handler this._selectionChangeHandler = this.onSelectionChange.bind(this); @@ -136,12 +138,12 @@ export class TransformTool public setGizmoVisible(visible: boolean): void { this._gizmoVisible = visible; - const contains = this._scene.children.includes(this._gizmo); + const contains = this._scene.children.includes(this._gizmoHelper); if (visible && !contains) { - this._scene.add(this._gizmo); + this._scene.add(this._gizmoHelper); this._gizmo.getRaycaster().layers.enableAll(); } else if (!visible && contains) { - this._scene.remove(this._gizmo); + this._scene.remove(this._gizmoHelper); this._gizmo.getRaycaster().layers.disableAll(); } } @@ -159,7 +161,7 @@ export class TransformTool public dispose(): void { this._selectionState.offChange(this._selectionChangeHandler); this._gizmo.detach(); - this._scene.remove(this._gizmo); + this._scene.remove(this._gizmoHelper); this._gizmo.dispose(); } @@ -188,7 +190,7 @@ export class TransformTool private isGizmoChild(obj: Object3D): boolean { let current: Object3D | null = obj; while (current) { - if (current === this._gizmo) return true; + if (current === this._gizmoHelper) return true; current = current.parent; } return false; @@ -202,7 +204,8 @@ export class TransformTool g.mode = 'translate'; // Apply custom colors to gizmo axes - g.traverse((child) => { + const helper = g.getHelper() as unknown as Object3D; + helper.traverse((child: Object3D) => { if (!('isMesh' in child)) return; const material = (child as Mesh).material as MeshBasicMaterial; diff --git a/src/plugins/toolbox/src/transform/__test__/TransformTool.test.ts b/src/plugins/toolbox/src/transform/__test__/TransformTool.test.ts index 7ea4c04f..15132cb4 100644 --- a/src/plugins/toolbox/src/transform/__test__/TransformTool.test.ts +++ b/src/plugins/toolbox/src/transform/__test__/TransformTool.test.ts @@ -1,4 +1,4 @@ -import { Vector2, type Object3D } from 'three'; +import { Vector2, type Object3D } from 'three/webgpu'; import { TransformTool, isTransformTool } from '../TransformTool.ts'; import { SelectionState } from '../../SelectionState.ts'; import { type PointerContext } from '../../PointerContext.ts'; @@ -15,10 +15,6 @@ import { Tween } from '@tweenjs/tween.js'; * @vitest-environment jsdom */ -vi.mock('three', async (importOriginal) => { - return await importOriginal(); -}); - // Mock PointerEvent for jsdom class MockPointerEvent extends MouseEvent { constructor(type: string, props?: PointerEventInit) { @@ -129,6 +125,7 @@ vi.mock('three/examples/jsm/controls/TransformControls.js', () => { instance.object = undefined; }); instance.dispose = vi.fn(); + instance.getHelper = vi.fn(() => instance); instance.traverse = vi.fn((callback: (obj: Object3D) => void) => { callback(instance); diff --git a/src/types/schema/GroupSchema.ts b/src/types/schema/GroupSchema.ts index caaf63d4..2c235c2e 100644 --- a/src/types/schema/GroupSchema.ts +++ b/src/types/schema/GroupSchema.ts @@ -1,4 +1,4 @@ -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; import { type BaseEntitySchema } from './BaseEntitySchema.ts'; import { type EntitySchema } from './EntitySchema.ts'; diff --git a/src/types/schema/LightSchema.ts b/src/types/schema/LightSchema.ts index 9d05c4e4..029e4cb2 100644 --- a/src/types/schema/LightSchema.ts +++ b/src/types/schema/LightSchema.ts @@ -1,4 +1,4 @@ -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; import { type BaseEntitySchema } from './BaseEntitySchema.ts'; import { type EntitySchema } from './EntitySchema.ts'; diff --git a/src/types/schema/MaterialSchema.ts b/src/types/schema/MaterialSchema.ts index aef9e1ae..11443d77 100644 --- a/src/types/schema/MaterialSchema.ts +++ b/src/types/schema/MaterialSchema.ts @@ -1,4 +1,4 @@ -import { type Texture } from 'three'; +import { type Texture } from 'three/webgpu'; export type MaterialSchema = { vertexColors: boolean; diff --git a/src/types/schema/ModelSchema.ts b/src/types/schema/ModelSchema.ts index f134f8a4..d41f27ab 100644 --- a/src/types/schema/ModelSchema.ts +++ b/src/types/schema/ModelSchema.ts @@ -1,4 +1,4 @@ -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; import { type MaterialSchema } from './MaterialSchema.ts'; import { type BaseEntitySchema } from './BaseEntitySchema.ts'; import { type EntitySchema } from './EntitySchema.ts'; diff --git a/src/types/schema/PovSchema.ts b/src/types/schema/PovSchema.ts index 79b4876e..97d0ed9e 100644 --- a/src/types/schema/PovSchema.ts +++ b/src/types/schema/PovSchema.ts @@ -1,4 +1,4 @@ -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; import { type BaseEntitySchema } from './BaseEntitySchema.ts'; import { type EntitySchema } from './EntitySchema.ts'; diff --git a/src/types/schema/PrimitiveSchema.ts b/src/types/schema/PrimitiveSchema.ts index 465f204d..07669044 100644 --- a/src/types/schema/PrimitiveSchema.ts +++ b/src/types/schema/PrimitiveSchema.ts @@ -1,4 +1,4 @@ -import { type Vector3Like } from 'three'; +import { type Vector3Like } from 'three/webgpu'; import { type BaseEntitySchema } from './BaseEntitySchema.ts'; import { type GeometrySchema } from './GeometrySchema.ts'; import { type MaterialSchema } from './MaterialSchema.ts'; diff --git a/yarn.lock b/yarn.lock index 1bb6f96a..8d97555e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -166,6 +166,11 @@ resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2" integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== +"@dimforge/rapier3d-compat@~0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389" + integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow== + "@esbuild/aix-ppc64@0.25.3": version "0.25.3" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz#014180d9a149cffd95aaeead37179433f5ea5437" @@ -742,10 +747,10 @@ resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-25.0.0.tgz#7266baebcc3affe62a3a54318a3ea82d904cd0b9" integrity sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A== -"@tweenjs/tween.js@~23.1.1": - version "23.1.2" - resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.2.tgz#4e5357fd6742f5aa50447d3fa808aed4cda93ed7" - integrity sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ== +"@tweenjs/tween.js@~23.1.3": + version "23.1.3" + resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d" + integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA== "@types/argparse@1.0.38": version "1.0.38" @@ -831,21 +836,23 @@ resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.3.tgz#705446e12ce0fad618557dd88236f51148b7a935" integrity sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ== -"@types/three@^0.163.0": - version "0.163.0" - resolved "https://registry.yarnpkg.com/@types/three/-/three-0.163.0.tgz#96f5440fcd39452d2c84dfe0c9b7a9cf0247b9e6" - integrity sha512-uIdDhsXRpQiBUkflBS/i1l3JX14fW6Ot9csed60nfbZNXHDTRsnV2xnTVwXcgbvTiboAR4IW+t+lTL5f1rqIqA== +"@types/three@^0.183.0": + version "0.183.1" + resolved "https://registry.yarnpkg.com/@types/three/-/three-0.183.1.tgz#d812d028b38ad68843725e3e7bd3268607cef150" + integrity sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw== dependencies: - "@tweenjs/tween.js" "~23.1.1" + "@dimforge/rapier3d-compat" "~0.12.0" + "@tweenjs/tween.js" "~23.1.3" "@types/stats.js" "*" - "@types/webxr" "*" + "@types/webxr" ">=0.5.17" + "@webgpu/types" "*" fflate "~0.8.2" - meshoptimizer "~0.18.1" + meshoptimizer "~1.0.1" -"@types/webxr@*": - version "0.5.16" - resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.16.tgz#28955aa2d1197d1ef0b9826ae0f7e68f7eca0601" - integrity sha512-0E0Cl84FECtzrB4qG19TNTqpunw0F1YF0QZZnFMF6pDw1kNKJtrlTKlVB34stGIsHbZsYQ7H0tNjPfZftkHHoA== +"@types/webxr@>=0.5.17": + version "0.5.24" + resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.24.tgz#734d5d90dadc5809a53e422726c60337fa2f4a44" + integrity sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg== "@types/yargs-parser@*": version "21.0.3" @@ -1084,6 +1091,11 @@ resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f" integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== +"@webgpu/types@*": + version "0.1.69" + resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.69.tgz#6b849bf370a1f29c78bd3aeba8e84c1150b237f2" + integrity sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -2476,10 +2488,10 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -meshoptimizer@~0.18.1: - version "0.18.1" - resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-0.18.1.tgz#cdb90907f30a7b5b1190facd3b7ee6b7087797d8" - integrity sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw== +meshoptimizer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-1.0.1.tgz#c3ef0d509a8b84ac562493dba5a108fd67fa76dc" + integrity sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g== micromatch@^4.0.4: version "4.0.7" @@ -3139,10 +3151,10 @@ three-spritetext@^1.8.2: resolved "https://registry.yarnpkg.com/three-spritetext/-/three-spritetext-1.9.6.tgz#f6e89dfbc158ad2f04392fc012ef390a7d7f7f99" integrity sha512-+O5hGi6u4nWLefY8+Hlufd0aysWZhS2Ex61LCQVSixbYbykdPUOYYA+V+4xG4mKNDm1AsC0yHINRQS5NQb6mSg== -three@^0.163.0: - version "0.163.0" - resolved "https://registry.yarnpkg.com/three/-/three-0.163.0.tgz#cbfefbfd64a1353ab7cc8bf0fc396ddca1875a49" - integrity sha512-HlMgCb2TF/dTLRtknBnjUTsR8FsDqBY43itYop2+Zg822I+Kd0Ua2vs8CvfBVefXkBdNDrLMoRTGCIIpfCuDew== +three@^0.183.0: + version "0.183.2" + resolved "https://registry.yarnpkg.com/three/-/three-0.183.2.tgz#606e3195bf210ef8d1eaaca2ab8c59d92d2bbc18" + integrity sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ== tinybench@^2.9.0: version "2.9.0"