Spaces:
Running
Running
| <script lang="ts"> | |
| import { T } from "@threlte/core"; | |
| import type { IntersectionEvent } from "@threlte/extras"; | |
| import { interactivity } from "@threlte/extras"; | |
| import type { Snippet } from "svelte"; | |
| import { Spring } from "svelte/motion"; | |
| import { useCursor } from "@threlte/extras"; | |
| import { onMount, onDestroy } from "svelte"; | |
| import { Group } from "three"; | |
| interface Props { | |
| content: Snippet<[{ isHovered: boolean; isSelected: boolean; debouncedIsHovered: boolean }]>; // renderable | |
| onClickObject?: () => void; | |
| rescale?: boolean; | |
| debouncedTimeout?: number; | |
| } | |
| let { content, onClickObject, rescale = true, debouncedTimeout = 100 }: Props = $props(); | |
| const scale = new Spring(1); | |
| // Height calculation state | |
| let groupRef = $state<Group | undefined>(undefined); | |
| // Hover state | |
| let isHovered = $state(false); | |
| let isSelected = $state(false); | |
| let isHighlighted = $derived(isHovered || isSelected); | |
| $effect(() => { | |
| if (isHighlighted) { | |
| if (rescale) { | |
| scale.target = 1.05; | |
| } | |
| } else { | |
| if (rescale) { | |
| scale.target = 1; | |
| } | |
| } | |
| }); | |
| const handleKeyDown = (event: KeyboardEvent) => { | |
| if (event.key === "Escape" && isSelected) { | |
| isSelected = false; | |
| } | |
| }; | |
| onMount(() => { | |
| document.addEventListener("keydown", handleKeyDown); | |
| }); | |
| onDestroy(() => { | |
| document.removeEventListener("keydown", handleKeyDown); | |
| }); | |
| const { onPointerEnter, onPointerLeave } = useCursor(); | |
| interactivity(); | |
| let debouncedIsHovered = $state(false); | |
| let hoverTimeout: ReturnType<typeof setTimeout>; | |
| function turningHoverOn() { | |
| clearTimeout(hoverTimeout); | |
| debouncedIsHovered = true; | |
| } | |
| function turningHoverOff() { | |
| // Debounce the hover state when turning off to prevent flickering | |
| clearTimeout(hoverTimeout); | |
| hoverTimeout = setTimeout(() => { | |
| debouncedIsHovered = false; | |
| }, debouncedTimeout); // wait 100ms (debouncedTimeout) before turning off | |
| } | |
| $effect(() => { | |
| if (isHovered) { | |
| turningHoverOn(); | |
| } else { | |
| turningHoverOff(); | |
| } | |
| }); | |
| </script> | |
| <T.Group | |
| bind:ref={groupRef} | |
| onpointerdown={(event: IntersectionEvent<MouseEvent>) => { | |
| event.stopPropagation(); | |
| isSelected = true; | |
| onClickObject?.(); | |
| }} | |
| onpointerenter={(event: IntersectionEvent<PointerEvent>) => { | |
| event.stopPropagation(); | |
| onPointerEnter(); | |
| isHovered = true; | |
| }} | |
| onpointerleave={(event: IntersectionEvent<PointerEvent>) => { | |
| event.stopPropagation(); | |
| onPointerLeave(); | |
| isHovered = false; | |
| }} | |
| scale={scale.current} | |
| > | |
| {#snippet children({ ref })} | |
| {@render content({ isHovered, isSelected, debouncedIsHovered })} | |
| {/snippet} | |
| </T.Group> | |