- Always respond in the language the user is writing in
- Always update this
AGENTS.mdwith new insights after code changes (workspace facts, patterns, preferences) - Always write
AGENTS.mdcontent in English
- Monorepo using yarn as package manager
- Testing framework: vitest with coverage via
yarn coverage - Global Vitest setup mocks were removed; tests now mock dependencies locally per file
- Shared test mock modules like
src/test/mocks/three.tsandsrc/test/mocks/three-spritetext.tswere 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/webgpuwhen the production module imports fromthree/webgpu; mockingthreedoes not affect those modules TransformToolmust add and traverseTransformControls.getHelper(), not theTransformControlsinstance 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.jssuffix when the source import does ARQuickLooktests must mock@shopware-ag/dive/assetloaderand@shopware-ag/dive/assetexporterin addition toAssetConverter, becausenew AssetLoader()andnew AssetExporter()are evaluated before the mockedAssetConverterconstructor runsDIVEGizmotests should mock child gizmo classes as realObject3Dinstances with spied methods to avoidTHREE.Object3D.addwarnings from plain-object stand-insDIVEPrimitivetests are more stable with realBox3plus per-test spies onBox3.prototype/Raycaster, instead of mocking the fullthreemodule surfaceOrientationDisplayAxestests should locally stubthree-spritetextbecause jsdom does not implement the canvas text context that the real package needsDIVERootshould detach both legacy scene-levelTransformControlsobjects and modernTransformControlsRoot.controlshelper roots when cleaning up transform controlsDIVERootPOV update/delete coverage requires manually seeding a matchingObject3Din tests becauseaddSceneObjectintentionally skips creating POV scene nodes- Plugins live in
src/plugins/<name>/and are auto-discovered by looking forindex.tsin subdirectories - Plugins are exported as subpath exports:
@shopware-ag/dive/<plugin-name>(e.g.@shopware-ag/dive/shader,@shopware-ag/dive/state) - The shader plugin (
src/plugins/shader/) now exposes node-based building blocks likeGridNodeandGridNodeUniforms; legacyDIVEShaderLib/DIVEShaderMaterialshader-lib wrappers are being removed DIVEGridcustom shader code must use TSL/node materials for WebGPU; plainShaderMaterialtriggersTHREE.NodeMaterial: Material "ShaderMaterial" is not compatibleDIVEGridowns itsMeshBasicNodeMaterialsetup and creates its grid uniform nodes locally;GridNodeonly provides the TSL output-node implementationDIVEGridcomponent uses the shader plugin; it is imported transitively viaScene→Grid→@shopware-ag/dive/shader- Shader plugin public docs must describe the new node-based API:
GridNodeplusGridNodeUniforms; legacyDIVEShaderLib/DIVEShaderMaterialdocs are outdated - Tests that mock
@shopware-ag/dive/shadermust provide aGridNodeconstructor stub after the shader plugin migration; legacyDIVEShaderLib-only mocks break transitive imports - Most tests do not need to mock
@shopware-ag/dive/shaderat all; after the WebGPU migration the only current direct need issrc/components/grid/__test__/Grid.test.ts, which assertsDIVEGridconstructsGridNode - When partially mocking
three/webgpu, base the mock onimportOriginal<typeof import('three/webgpu')>(); usingvi.importActual('three')drops WebGPU-only exports likeNodeand breaks transitive shader imports DIVEGridtests or otherMeshBasicNodeMaterialmocks must preserve the constructoroutputNodeparam because production code passesnew GridNode(uniforms)directly into material creationGridNodeunit tests are best written with localthree/tslandthree/webgpumocks plusvi.hoisted(...); plain top-level mock helpers break becausevi.mock(...)factories are hoistedGridNodereturns the finalvec4(...)TSL node from its constructor while still naming the underlying baseNodeinstanceGridNode- In
GridNodetests, keep a raw mock-uniform object separate from theGridNodeUniformscast; casting too early hides Vitest.mockmetadata from TypeScript DIVEEnvironmentno longer applies HDR state from the constructor alone; tests should wait for the async HDR load and callenv.init()before asserting environment/background updatesDIVEEnvironmentconcurrent-load cleanup is best covered by spying on the privateloadHDRImagemethod and resolving overlapping promises out of order; stale textures should be disposedDIVEtests must mockmainView.renderer.initializedandmainView.renderer.init()becauseDIVE.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 DIVERenderertests must mockthree/webgpuWebGPURenderer; oldthreeWebGLRendererexpectations are outdatedDIVERendererstale-init behavior aftersetCanvas()is best tested with a deferred firstinit()promise; the old renderer must not trigger a second environment init after the swapMediaCreatorfallback coverage is easiest by overriding test canvaswidth/heighttoundefinedandwritable: true, then lettingdrawCanvas()fall back throughclientWidthto the renderer canvas dimensionsMediaCreator.drawCanvas()must restore the previous WebGPU render target and camera layer mask before awaitingreadRenderTargetPixelsAsync(); otherwise the liveView.tick()render can keep drawing into the offscreen target and triggerWebGPUTextureUtils: Texture already initialized.- Demo fixture
/Users/f.frank/Public/Repos/dive-demo/public/model_reverse_animation_order_long_name_blank_name.glbis used for animation edge cases; it contains a blank clip name, an overlong clip name, and aWalkclip that now hard-fails loading via an invalid animation accessor reference yarn buildcan still exit successfully whilevite-plugin-dtsreports TypeScript API migration errors, so WebGPU refactors need explicit grep/type-review and not just a green build exit codeDIVE.start()is now a fire-and-forget wrapper aroundstartAsync(), so tests that need renderer readiness should awaitstartAsync()or a microtask before asserting downstream effectsDIVE.dispose()must dispose theDIVEClockbefore tearing down views/renderers, andstartAsync()must bail out after late renderer init if the instance was disposed; otherwise demo route switches can leave stale RAF ticks callingDIVEView.tick()on dead WebGPU renderers- Demo views in
/Users/f.frank/Public/Repos/dive-demo/src/views/that createQuickViewinstances must dispose them inonUnmounted; missing route-leave cleanup leaves old WebGPU render loops alive across example switches - Deprecated
BaseToolcoverage was removed entirely; ifsrc/plugins/toolbox/src/BaseTool.tsis gone in a future major, delete the legacy suite instead of recreating the class for tests MediaCreatorscreenshot generation is async under WebGPU and usesRenderTargetplusreadRenderTargetPixelsAsync; it no longer swapsrenderer.domElementDIVEXRLightRootcurrently guardsXREstimatedLightoff under WebGPU and falls back to the existing scene light until a dedicated WebGPU-compatible light-estimation path exists- Library builds must externalize
threewith a pattern that also matches subpaths likethree/webgpu,three/tsl, andthree/examples/jsm/*; externalizing only barethreebundles a second Three runtime intobuild/and triggersTHREE.WARNING: Multiple instances of Three.js being imported.in consumers - State action migrations must use
AnimationSystem.fromTargets(...).play()andToolbox.enableTool(); lingeringanimate()oruseTool()calls can still letyarn buildexit 0 whilevite-plugin-dtsreports TS2339 API drift - When swapping canvases under WebGPU,
DIVEEnvironment.setRenderer()must run before disposing the previousWebGPURenderer; disposing the old renderer first can crashPMREMGenerator.dispose()inside Three'sNodeManager.deletewithusedTimesaccess errors OrientationDisplay.tick()should size its overlay viewport fromDIVERenderer.canvas.clientHeightand restore the priorwebgpurenderer.autoClearvalue; unit tests can fall back to the saved viewport height when the mock omitscanvas- Neighboring
dive-demolocal verification can use anode_modules/@shopware-ag/divesymlink to this repo whenyalcis absent, as long as this repo'sbuild/artifacts are present - The
dive-demoorientation display example now uses a singleQuickViewcanvas withdisplayAxes: true; the previous side-by-side comparison against a manually wiredOrientationDisplayplugin is no longer the expected snapshot shape dive-demoviews that gate UI interactivity onQuickViewreadiness, such asDiveSwitchCanvasandDiveTargetAnimation, need to wait for a non-zero canvas layout plus a small initial delay before constructingQuickView; on CILinux + xvfb + llvmpipe, starting too early leaves control buttons permanently disabled- In
dive-demo, replacing the fixed QuickView startup sleep with a shared layout-driven wait helper (ResizeObserverplus animation-frame verification) keeps the initial load stable, but canvas-switch flows still need to yield one DOM frame after committing the active-panel state before callingmainView.setCanvas(...) DIVECanvasLifecycleManagerinsrc/engine/canvas/is again the single owner of canvas readiness state: it keeps the waiter promises, resolveswaitForHealthyCanvas(), and advances readiness via its owntick()DIVECanvasLifecycleManager.tick()must early-return while the current canvas remains valid; only invalid, detached, or freshly swapped canvases should re-enter the two-sample stabilization pathDIVECanvasLifecycleManager.tick()should stay as a shallow entrypoint that does the dispose guard and then delegates the actual lifecycle progression to the private_checkCanvasHealth()helper for readabilityDIVEView.tick()should always callDIVECanvasLifecycleManager.tick()before honoring the paused/render path so canvas readiness can continue progressing even while rendering is pausedDIVECanvasLifecycleManagerkeeps its layout/readiness helpers as private member methods instead of top-level module helpers, so the canvas lifecycle logic stays co-located inside the classDIVEView.init(),DIVERenderer.init(), andDIVEEnvironment.init()should stayasyncand explicitlyawaittheir cached_initPromisevalues; this repo prefers the consistent async method shape over collapsing those branches to direct promise returnsDIVECanvasLifecycleManager.waitForHealthyCanvas()can take an optionalAbortSignal; aborting resolves only that individual waiter withnull, while the CLM's shared readiness state keeps progressing through latertick()callsDIVEViewnow uses an internalAbortControllerto invalidate pending init work ondispose()andsetCanvas(); even with abort support,renderer !== this._rendererremains as the stale-renderer guard after awaited renderer initializationDIVERendererno longer owns DOM/canvas readiness logic; it only initializes WebGPU/environment state, swaps canvases, and handles render/resize calls- The old
DIVEResizeManagercompatibility layer has been removed entirely on v3; canvas ownership now lives directly betweenDIVEViewandDIVECanvasLifecycleManager DIVEView.setCanvas()must not force an immediateonResize()on the swapped canvas; theDIVECanvasLifecycleManageris the single source of truth for resize propagationDIVECanvasLifecycleManager.setCanvas()must reset its cached width/height so an equally sized replacement canvas still emits the initial resize sync for the new renderer/camera pair- In
DIVECanvasLifecycleManager, keep raw measurement in_getCanvasLayout()and the valid-layout fast path insidewaitForHealthyCanvas()/tick(); there is no longer a separate public readiness accessor DIVEViewshould pass a named_handleCanvasResizecallback intoDIVECanvasLifecycleManagerinstead of an inline lambda, so the renderer/camera resize orchestration stays explicit while the CLM remains decoupledDIVEViewinvalidation branches after async init are best covered by disposing the view whilerenderer.init()is still pending and by invoking theDIVECanvasLifecycleManagerresize callback directly to assert theonResize+ immediate render pathDIVECanvasLifecycleManagerkeeps a single steady-stateResizeObserveron the canvas itself; parent changes are handled intick()as validity invalidations rather than as observed resize eventsDIVEViewdoes not inject the clock intoDIVECanvasLifecycleManager; onlyDIVEViewitself is aDIVETicker, andDIVE.startAsync()must start theDIVEClockbefore awaitingmainView.init()so the CLM's internaltick()can progress inside the view loopDIVECanvasLifecycleManagercoverage is easiest to keep at 100% with explicittick()advancement in tests, observer invalidation cases, and signal-based waiter success/stale-resolution assertions- In
View.test.ts, thewaitForHealthyCanvasmock should be explicitly typed asPromise<DIVECanvasLayout | null>; otherwise the stalenullpath triggers a TypeScript error onmockResolvedValue(null) DIVECanvasLifecycleManagernow keeps its shared waiter state under_healthyCanvasPromiseand_resolveHealthyCanvasso the promise naming matches the canvas-health narrative- In
Dive.test.ts,mainView.initis typed as a plain async method, so tests should narrow it withvi.mocked(...)before calling mock-only helpers likemockRejectedValueOnceormockImplementationOnce - Full focused coverage for
CanvasLifecycleManager.tsnow needs explicit tests for parentless bootstrap polling, renderable-to-zero resets during stabilization, same-size canvas swaps, waiter-only aborts, and the private direct-layout fallback after bootstrap completion - Focused single-file coverage in this repo should use
vitest --coverage.include=<path>; running one suite with the default globalsrc/**/*coverage scope still enforces repo-wide thresholds and will fail even when the targeted file itself is at 100%