diff --git a/src/composables/__tests__/useGlassOpticalRenderer.spec.ts b/src/composables/__tests__/useGlassOpticalRenderer.spec.ts index 0ec20686..39f86c60 100644 --- a/src/composables/__tests__/useGlassOpticalRenderer.spec.ts +++ b/src/composables/__tests__/useGlassOpticalRenderer.spec.ts @@ -1960,6 +1960,64 @@ describe('glass optical surface discovery', () => { scope.stop() }) + it('restores a temporarily collapsed interaction clip without rebuilding registry membership', async () => { + vi.spyOn(window, 'innerWidth', 'get').mockReturnValue(1200) + vi.spyOn(window, 'innerHeight', 'get').mockReturnValue(800) + const three = await import('three') + const render = vi.spyOn(three.WebGLRenderer.prototype, 'render') + const outerSurface = appendOpticalSurface('v-card', { height: 500, width: 900, x: 80, y: 100 }) + outerSurface.dataset.glassOpticalSurface = '' + const card = document.createElement('article') + card.className = 'app-hover-lift-card' + setOpticalSurfaceBounds(card, { height: 180, width: 360, x: 100, y: 140 }) + outerSurface.append(card) + const querySelectorAll = vi.spyOn(outerSurface, 'querySelectorAll') + const scope = effectScope() + const renderer = scope.run(() => + useGlassOpticalRenderer({ + active: ref(true), + appearance: ref('clear'), + canvas: ref(document.createElement('canvas')), + quality: ref('balanced'), + routeKey: ref('/dashboard'), + surfaceSpace: 'scroll', + tintColor: ref('#8D51F9'), + wallpaperUrl: ref('https://example.com/wallpaper.jpg'), + }), + ) + + await vi.waitFor(() => expect(renderer?.state.value).toBe('ready')) + window.dispatchEvent(new MouseEvent('pointermove', { clientX: 160, clientY: 180 })) + await vi.waitFor(() => expect(render).toHaveBeenCalled()) + const getInteractionRectCount = () => { + const scene = render.mock.calls.at(-1)?.[0] as unknown as { + children: Array<{ + material: { + uniforms: { + uInteractionRectCount: { value: number } + } + } + }> + } + + return scene.children[0].material.uniforms.uInteractionRectCount.value + } + + expect(getInteractionRectCount()).toBe(1) + querySelectorAll.mockClear() + + setOpticalSurfaceBounds(card, { height: 12, width: 12, x: 100, y: 140 }) + ResizeObserverMock.instances.forEach(observer => observer.trigger()) + await vi.waitFor(() => expect(getInteractionRectCount()).toBe(0)) + + setOpticalSurfaceBounds(card, { height: 180, width: 360, x: 100, y: 140 }) + ResizeObserverMock.instances.forEach(observer => observer.trigger()) + await vi.waitFor(() => expect(getInteractionRectCount()).toBe(1)) + + expect(querySelectorAll).not.toHaveBeenCalled() + scope.stop() + }) + it('keeps the active texture and ready state when a replacement wallpaper fails', async () => { const three = await import('three') const canvas = document.createElement('canvas') diff --git a/src/composables/useGlassOpticalRenderer.ts b/src/composables/useGlassOpticalRenderer.ts index 0a631a00..1df3d541 100644 --- a/src/composables/useGlassOpticalRenderer.ts +++ b/src/composables/useGlassOpticalRenderer.ts @@ -1930,7 +1930,6 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) if (seen.has(element) || !element.isConnected || resolveGlassOpticalSurfaceMode(element) !== mode) return const rect = committedRect ?? getElementPresentationRect(element) - if (rect.width < 24 || rect.height < 24) return seen.add(element) candidates.push({ key: element, mode, owner, rect }) @@ -1968,12 +1967,14 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) } const rect = committedSurfaces.get(clip.key)?.rect ?? getElementPresentationRect(clip.key) - if (rect.width < 24 || rect.height < 24) return [] - return [{ ...clip, rect }] }) } + function isInteractionClipRenderable(rect: GlassOpticalRect) { + return rect.width >= 24 && rect.height >= 24 + } + function updateInteractionClips() { const slotKeys = new Set(surfaceSlots.map(slot => slot.key)) const viewportX = presentationSpace === 'scroll' ? window.scrollX : 0 @@ -2003,7 +2004,11 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) inOverscan, } }) - .filter(entry => entry.candidate.key === activeInteractionClip || entry.inOverscan) + .filter( + entry => + isInteractionClipRenderable(entry.candidate.rect) && + (entry.candidate.key === activeInteractionClip || entry.inOverscan), + ) .sort((left, right) => { const leftActive = left.candidate.key === activeInteractionClip const rightActive = right.candidate.key === activeInteractionClip @@ -2445,7 +2450,12 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions) if (!surface) return null const matchingClips = interactionClipRegistry - .filter(candidate => candidate.owner === surface.key && rectContainsPoint(candidate.rect, x, y)) + .filter( + candidate => + candidate.owner === surface.key && + isInteractionClipRenderable(candidate.rect) && + rectContainsPoint(candidate.rect, x, y), + ) .sort((left, right) => left.rect.width * left.rect.height - right.rect.width * right.rect.height) return {