-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSpawn3DObject.svelte
More file actions
113 lines (92 loc) · 3.31 KB
/
Spawn3DObject.svelte
File metadata and controls
113 lines (92 loc) · 3.31 KB
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
108
109
110
111
112
113
<script lang="ts">
import { T, useThrelte, useTask } from '@threlte/core'
import { Vector3, PerspectiveCamera, Group } from 'three'
import type { Spawn3DItem } from './spawn3dStore'
let { item }: { item: Spawn3DItem } = $props()
const { camera, size } = useThrelte()
let group = $state<Group>()
// Internal State for locking mechanics
let cachedNDC: { x: number; y: number } | null = null
// svelte-ignore state_referenced_locally
let previousLockMode = item.lockAt
// Reusables to prevent GC
const vec = new Vector3()
const pos = new Vector3()
const camDir = new Vector3()
const REF_DISTANCE = 5.0 // Reference for absolute sizing
// Helper: Compute NDC from current DOM rect
const computeNDC = () => {
const { top, left, width, height } = item
return {
x: ((left + width / 2) / $size.width) * 2 - 1,
y: -((top + height / 2) / $size.height) * 2 + 1
}
}
// --- TRANSITION LOGIC ---
// Handle switching between modes (e.g., locking current position)
$effect(() => {
if (item.lockAt !== previousLockMode) {
if (item.lockAt === 'camera') {
// Transitioning TO Camera: Freeze current position
cachedNDC = computeNDC()
} else if (item.lockAt === 'element') {
// Transitioning TO Element: Unfreeze
cachedNDC = null
}
previousLockMode = item.lockAt
}
})
useTask(() => {
if (!group || !$camera) return
const cam = $camera as PerspectiveCamera
// 1. VISIBILITY CHECK
const isVisible = item.visibleOverride && (item.autoHide ? item.visible : true)
group.visible = isVisible
if (!isVisible) return
// 2. LOCK MODE LOGIC
if (item.lockAt === 'none') return // Stop updating (Detached)
let xNDC, yNDC
if (item.lockAt === 'camera') {
// Use cached coordinates (HUD mode)
// Safety: If cachedNDC is null (e.g. spawned directly as 'camera'), calculate once now.
if (!cachedNDC) cachedNDC = computeNDC()
xNDC = cachedNDC.x
yNDC = cachedNDC.y
} else {
// 'element': Calculate fresh every frame
const current = computeNDC()
xNDC = current.x
yNDC = current.y
}
// 3. SPATIAL MATH
// Unproject NDC to Direction Vector
vec.set(xNDC, yNDC, 0.5)
vec.unproject(cam)
vec.sub(cam.position).normalize()
const dist = item.distance
// 4. SCALING
// Determine the distance used for sizing calculations
// If absoluteSizing is ON, we pretend the object is at REF_DISTANCE
// If OFF, we use the actual distance (so it scales up/down to look constant)
const scalingDistance = item.absoluteSizing ? REF_DISTANCE : dist
// Calculate Perpendicular Distance (Project vector onto camera forward axis)
// This prevents "Hypotenuse Effect" distortion at screen edges
cam.getWorldDirection(camDir)
const perpDist = scalingDistance * vec.dot(camDir)
// Calculate Scale to match pixels
const fovRad = (cam.fov * Math.PI) / 180
const visibleHeight = 2 * perpDist * Math.tan(fovRad / 2)
const unitsPerPixel = visibleHeight / $size.height
group.scale.set(unitsPerPixel, unitsPerPixel, unitsPerPixel)
// 5. POSITIONING
// Position is always based on actual distance
pos.copy(cam.position).add(vec.multiplyScalar(dist))
group.position.set(pos.x, pos.y, pos.z)
// 6. ROTATION
// Always face the camera plane (Billboarding)
group.quaternion.copy(cam.quaternion)
})
</script>
<T.Group bind:ref={group}>
{@render item.snippet?.(item)}
</T.Group>