File size: 2,595 Bytes
6ce4ca6
 
 
 
 
3cdf7b9
6ce4ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<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>