@jkwd/inbase 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/apps/explorer/index.html +12 -0
- package/apps/explorer/package.json +28 -0
- package/apps/explorer/scripts/js-source.mjs +188 -0
- package/apps/explorer/scripts/patch-lib.d.ts +115 -0
- package/apps/explorer/scripts/patch-lib.mjs +472 -0
- package/apps/explorer/scripts/scan-target.mjs +188 -0
- package/apps/explorer/scripts/session-store.d.ts +156 -0
- package/apps/explorer/scripts/session-store.mjs +809 -0
- package/apps/explorer/scripts/target-config.d.ts +8 -0
- package/apps/explorer/scripts/target-config.mjs +42 -0
- package/apps/explorer/src/App.tsx +941 -0
- package/apps/explorer/src/agentIntent.ts +182 -0
- package/apps/explorer/src/codebase.ts +15 -0
- package/apps/explorer/src/index.css +632 -0
- package/apps/explorer/src/layout.ts +508 -0
- package/apps/explorer/src/main.tsx +16 -0
- package/apps/explorer/src/scene/BlockPlacer.tsx +87 -0
- package/apps/explorer/src/scene/Bridge.tsx +290 -0
- package/apps/explorer/src/scene/FileBlock.tsx +256 -0
- package/apps/explorer/src/scene/FolderArea.tsx +96 -0
- package/apps/explorer/src/scene/IslandPlacer.tsx +40 -0
- package/apps/explorer/src/scene/MapSelectBorder.tsx +57 -0
- package/apps/explorer/src/scene/MapView.tsx +247 -0
- package/apps/explorer/src/scene/Player.tsx +245 -0
- package/apps/explorer/src/scene/RelationLines.tsx +223 -0
- package/apps/explorer/src/scene/SelectionController.tsx +89 -0
- package/apps/explorer/src/scene/UserContextTracker.tsx +152 -0
- package/apps/explorer/src/scene/World.tsx +323 -0
- package/apps/explorer/src/theme.ts +111 -0
- package/apps/explorer/src/types.ts +245 -0
- package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +38 -0
- package/apps/explorer/src/ui/HUD.tsx +1090 -0
- package/apps/explorer/src/ui/NameInput.tsx +45 -0
- package/apps/explorer/src/userContext.ts +73 -0
- package/apps/explorer/src/userCreated.ts +354 -0
- package/apps/explorer/src/vite-env.d.ts +1 -0
- package/apps/explorer/tsconfig.json +21 -0
- package/apps/explorer/vite.config.ts +295 -0
- package/bin/inbase.mjs +170 -0
- package/bin/project.mjs +94 -0
- package/bin/session.mjs +241 -0
- package/package.json +63 -0
- package/skill/inbase/SKILL.md +167 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { MAP_SELECTION } from '../theme'
|
|
4
|
+
|
|
5
|
+
type MapSelectBorderProps = {
|
|
6
|
+
width: number
|
|
7
|
+
depth: number
|
|
8
|
+
y?: number
|
|
9
|
+
stroke?: number
|
|
10
|
+
userData?: Record<string, unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function MapSelectBorder({
|
|
14
|
+
width,
|
|
15
|
+
depth,
|
|
16
|
+
y = 0.04,
|
|
17
|
+
stroke = MAP_SELECTION.blockPad,
|
|
18
|
+
userData,
|
|
19
|
+
}: MapSelectBorderProps) {
|
|
20
|
+
const geometry = useMemo(() => {
|
|
21
|
+
const innerW = width
|
|
22
|
+
const innerD = depth
|
|
23
|
+
const outerW = width + stroke * 2
|
|
24
|
+
const outerD = depth + stroke * 2
|
|
25
|
+
const shape = new THREE.Shape()
|
|
26
|
+
shape.moveTo(-outerW / 2, -outerD / 2)
|
|
27
|
+
shape.lineTo(outerW / 2, -outerD / 2)
|
|
28
|
+
shape.lineTo(outerW / 2, outerD / 2)
|
|
29
|
+
shape.lineTo(-outerW / 2, outerD / 2)
|
|
30
|
+
shape.closePath()
|
|
31
|
+
const hole = new THREE.Path()
|
|
32
|
+
hole.moveTo(-innerW / 2, -innerD / 2)
|
|
33
|
+
hole.lineTo(-innerW / 2, innerD / 2)
|
|
34
|
+
hole.lineTo(innerW / 2, innerD / 2)
|
|
35
|
+
hole.lineTo(innerW / 2, -innerD / 2)
|
|
36
|
+
hole.closePath()
|
|
37
|
+
shape.holes.push(hole)
|
|
38
|
+
return new THREE.ShapeGeometry(shape)
|
|
39
|
+
}, [depth, stroke, width])
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<mesh
|
|
43
|
+
geometry={geometry}
|
|
44
|
+
position={[0, y, 0]}
|
|
45
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
46
|
+
renderOrder={2}
|
|
47
|
+
userData={userData}
|
|
48
|
+
>
|
|
49
|
+
<meshBasicMaterial
|
|
50
|
+
color={MAP_SELECTION.color}
|
|
51
|
+
toneMapped={false}
|
|
52
|
+
side={THREE.DoubleSide}
|
|
53
|
+
depthWrite={false}
|
|
54
|
+
/>
|
|
55
|
+
</mesh>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import { useThree } from '@react-three/fiber'
|
|
3
|
+
import { Html, MapControls, OrthographicCamera } from '@react-three/drei'
|
|
4
|
+
import * as THREE from 'three'
|
|
5
|
+
import { folderAt, worldBounds } from '../layout'
|
|
6
|
+
import type { ChangeKind } from '../theme'
|
|
7
|
+
import type { WorldLayout } from '../types'
|
|
8
|
+
|
|
9
|
+
type MapViewProps = {
|
|
10
|
+
layout: WorldLayout
|
|
11
|
+
enabled: boolean
|
|
12
|
+
marker: [number, number] | null
|
|
13
|
+
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
14
|
+
selectedFolder?: string | null
|
|
15
|
+
onLand: (x: number, z: number) => void
|
|
16
|
+
onSelect: (fileId: string | null) => void
|
|
17
|
+
onSelectFolder: (folderPath: string | null) => void
|
|
18
|
+
onTravelTo: (fromId: string, toId: string) => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function MapView({
|
|
22
|
+
layout,
|
|
23
|
+
enabled,
|
|
24
|
+
marker,
|
|
25
|
+
highlightedFolders,
|
|
26
|
+
selectedFolder = null,
|
|
27
|
+
onLand,
|
|
28
|
+
onSelect,
|
|
29
|
+
onSelectFolder,
|
|
30
|
+
onTravelTo,
|
|
31
|
+
}: MapViewProps) {
|
|
32
|
+
const size = useThree((state) => state.size)
|
|
33
|
+
const { camera, gl, scene } = useThree()
|
|
34
|
+
const bounds = useMemo(() => worldBounds(layout), [layout])
|
|
35
|
+
const drag = useRef({ x: 0, y: 0, moved: false, active: false })
|
|
36
|
+
const sized = size.width > 16 && size.height > 16
|
|
37
|
+
|
|
38
|
+
const fitZoom = sized
|
|
39
|
+
? Math.min(
|
|
40
|
+
size.width / Math.max(bounds.width + 24, 1),
|
|
41
|
+
size.height / Math.max(bounds.depth + 24, 1),
|
|
42
|
+
)
|
|
43
|
+
: 8
|
|
44
|
+
const zoom = Math.max(fitZoom * 0.92, 1)
|
|
45
|
+
|
|
46
|
+
useLayoutEffect(() => {
|
|
47
|
+
if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
|
|
48
|
+
camera.up.set(0, 0, -1)
|
|
49
|
+
camera.position.set(bounds.cx, 120, bounds.cz)
|
|
50
|
+
camera.lookAt(bounds.cx, 0, bounds.cz)
|
|
51
|
+
camera.near = 1
|
|
52
|
+
camera.far = 2000
|
|
53
|
+
camera.zoom = zoom
|
|
54
|
+
camera.updateProjectionMatrix()
|
|
55
|
+
}, [bounds.cx, bounds.cz, camera, enabled, zoom])
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!enabled) return
|
|
59
|
+
const element = gl.domElement
|
|
60
|
+
element.style.cursor = 'grab'
|
|
61
|
+
|
|
62
|
+
const onDown = (event: PointerEvent) => {
|
|
63
|
+
if (event.button !== 0) return
|
|
64
|
+
drag.current = { x: event.clientX, y: event.clientY, moved: false, active: true }
|
|
65
|
+
element.style.cursor = 'grabbing'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const onMove = (event: PointerEvent) => {
|
|
69
|
+
if (Math.hypot(event.clientX - drag.current.x, event.clientY - drag.current.y) > 5) {
|
|
70
|
+
drag.current.moved = true
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const pickAt = (clientX: number, clientY: number) => {
|
|
75
|
+
const rect = element.getBoundingClientRect()
|
|
76
|
+
if (rect.width < 2 || rect.height < 2) return null
|
|
77
|
+
const ndc = new THREE.Vector2(
|
|
78
|
+
((clientX - rect.left) / rect.width) * 2 - 1,
|
|
79
|
+
-((clientY - rect.top) / rect.height) * 2 + 1,
|
|
80
|
+
)
|
|
81
|
+
const raycaster = new THREE.Raycaster()
|
|
82
|
+
raycaster.setFromCamera(ndc, camera)
|
|
83
|
+
const hits = raycaster.intersectObjects(scene.children, true)
|
|
84
|
+
const relationHit = hits.find((hit) => hit.object.userData.relationTo)
|
|
85
|
+
const fileHit = hits.find((hit) => hit.object.userData.fileId)
|
|
86
|
+
return { raycaster, relationHit, fileHit }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const onUp = (event: PointerEvent) => {
|
|
90
|
+
element.style.cursor = 'grab'
|
|
91
|
+
const startedOnCanvas = drag.current.active
|
|
92
|
+
drag.current.active = false
|
|
93
|
+
if (!startedOnCanvas || event.button !== 0 || drag.current.moved) return
|
|
94
|
+
|
|
95
|
+
const pick = pickAt(event.clientX, event.clientY)
|
|
96
|
+
if (!pick) return
|
|
97
|
+
const { relationHit, fileHit } = pick
|
|
98
|
+
if (
|
|
99
|
+
relationHit &&
|
|
100
|
+
(!fileHit || relationHit.distance <= fileHit.distance + 0.4)
|
|
101
|
+
) {
|
|
102
|
+
onTravelTo(
|
|
103
|
+
relationHit.object.userData.relationFrom as string,
|
|
104
|
+
relationHit.object.userData.relationTo as string,
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (fileHit) {
|
|
109
|
+
onSelect(fileHit.object.userData.fileId as string)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const hit = new THREE.Vector3()
|
|
114
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
115
|
+
if (pick.raycaster.ray.intersectPlane(plane, hit)) {
|
|
116
|
+
const folder = folderAt(hit.x, hit.z, layout)
|
|
117
|
+
if (folder) {
|
|
118
|
+
onSelectFolder(folder.path)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
onSelect(null)
|
|
123
|
+
onSelectFolder(null)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const onDblClick = (event: MouseEvent) => {
|
|
127
|
+
if (event.button !== 0 || drag.current.moved) return
|
|
128
|
+
const pick = pickAt(event.clientX, event.clientY)
|
|
129
|
+
if (!pick) return
|
|
130
|
+
const { raycaster, relationHit, fileHit } = pick
|
|
131
|
+
if (relationHit || fileHit) return
|
|
132
|
+
|
|
133
|
+
const hit = new THREE.Vector3()
|
|
134
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
135
|
+
if (raycaster.ray.intersectPlane(plane, hit)) {
|
|
136
|
+
const lock = element.requestPointerLock()
|
|
137
|
+
if (lock && typeof lock.catch === 'function') {
|
|
138
|
+
void lock.catch(() => {})
|
|
139
|
+
}
|
|
140
|
+
onLand(hit.x, hit.z)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
element.addEventListener('pointerdown', onDown)
|
|
145
|
+
window.addEventListener('pointermove', onMove)
|
|
146
|
+
window.addEventListener('pointerup', onUp)
|
|
147
|
+
element.addEventListener('dblclick', onDblClick)
|
|
148
|
+
return () => {
|
|
149
|
+
element.style.cursor = ''
|
|
150
|
+
element.removeEventListener('pointerdown', onDown)
|
|
151
|
+
window.removeEventListener('pointermove', onMove)
|
|
152
|
+
window.removeEventListener('pointerup', onUp)
|
|
153
|
+
element.removeEventListener('dblclick', onDblClick)
|
|
154
|
+
}
|
|
155
|
+
}, [
|
|
156
|
+
camera,
|
|
157
|
+
enabled,
|
|
158
|
+
gl.domElement,
|
|
159
|
+
layout,
|
|
160
|
+
onLand,
|
|
161
|
+
onSelect,
|
|
162
|
+
onSelectFolder,
|
|
163
|
+
onTravelTo,
|
|
164
|
+
scene,
|
|
165
|
+
])
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<>
|
|
169
|
+
{enabled && (
|
|
170
|
+
<OrthographicCamera
|
|
171
|
+
makeDefault
|
|
172
|
+
position={[bounds.cx, 120, bounds.cz]}
|
|
173
|
+
zoom={zoom}
|
|
174
|
+
near={1}
|
|
175
|
+
far={2000}
|
|
176
|
+
up={[0, 0, -1]}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
{enabled && sized && (
|
|
180
|
+
<MapControls
|
|
181
|
+
enableRotate={false}
|
|
182
|
+
enableDamping
|
|
183
|
+
dampingFactor={0.12}
|
|
184
|
+
screenSpacePanning
|
|
185
|
+
zoomSpeed={1.15}
|
|
186
|
+
minZoom={Math.max(fitZoom * 0.45, 1)}
|
|
187
|
+
maxZoom={Math.max(fitZoom * 10, 20)}
|
|
188
|
+
target={[bounds.cx, 0, bounds.cz]}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
{enabled &&
|
|
192
|
+
Object.values(layout.folders).map((folder) => (
|
|
193
|
+
<Html
|
|
194
|
+
key={folder.path}
|
|
195
|
+
position={[folder.x, 14, folder.z + 1.35]}
|
|
196
|
+
center
|
|
197
|
+
style={{ pointerEvents: 'none' }}
|
|
198
|
+
>
|
|
199
|
+
<div
|
|
200
|
+
className={[
|
|
201
|
+
'map-folder-label',
|
|
202
|
+
highlightedFolders?.[folder.path]
|
|
203
|
+
? `map-folder-label-${highlightedFolders[folder.path]}`
|
|
204
|
+
: folder.added
|
|
205
|
+
? 'map-folder-label-added'
|
|
206
|
+
: selectedFolder === folder.path
|
|
207
|
+
? 'map-folder-label-selected'
|
|
208
|
+
: '',
|
|
209
|
+
]
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.join(' ')}
|
|
212
|
+
>
|
|
213
|
+
<span className="map-folder-name">
|
|
214
|
+
{folderKindLabel(
|
|
215
|
+
folder.name,
|
|
216
|
+
highlightedFolders?.[folder.path] ?? null,
|
|
217
|
+
folder.added ?? false,
|
|
218
|
+
)}
|
|
219
|
+
</span>
|
|
220
|
+
</div>
|
|
221
|
+
</Html>
|
|
222
|
+
))}
|
|
223
|
+
{marker && <LandMarker marker={marker} />}
|
|
224
|
+
</>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function folderKindLabel(
|
|
229
|
+
name: string,
|
|
230
|
+
kind: ChangeKind | null | undefined,
|
|
231
|
+
added: boolean,
|
|
232
|
+
) {
|
|
233
|
+
if (kind === 'remove') return `- ${name}`
|
|
234
|
+
if (kind === 'add' || added) return `+ ${name}`
|
|
235
|
+
return name
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function LandMarker({ marker }: { marker: [number, number] }) {
|
|
239
|
+
return (
|
|
240
|
+
<group position={[marker[0], 0.2, marker[1]]}>
|
|
241
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
|
242
|
+
<ringGeometry args={[0.55, 0.85, 24]} />
|
|
243
|
+
<meshBasicMaterial color="#e8c36a" side={THREE.DoubleSide} />
|
|
244
|
+
</mesh>
|
|
245
|
+
</group>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
|
+
import { PerspectiveCamera, PointerLockControls } from '@react-three/drei'
|
|
4
|
+
import type { PointerLockControls as PointerLockControlsImpl } from 'three-stdlib'
|
|
5
|
+
import * as THREE from 'three'
|
|
6
|
+
import { CONFIG } from '../theme'
|
|
7
|
+
import { locationLabel } from '../layout'
|
|
8
|
+
import type { FlyTo, ViewMode, WorldLayout } from '../types'
|
|
9
|
+
|
|
10
|
+
type PlayerProps = {
|
|
11
|
+
layout: WorldLayout
|
|
12
|
+
mode: ViewMode
|
|
13
|
+
landAt: [number, number]
|
|
14
|
+
locked: boolean
|
|
15
|
+
lockEnabled?: boolean
|
|
16
|
+
onLockedChange: (locked: boolean) => void
|
|
17
|
+
onFolderChange: (label: string) => void
|
|
18
|
+
onWalkPosition: (x: number, z: number) => void
|
|
19
|
+
flyTo: FlyTo | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Player({
|
|
23
|
+
layout,
|
|
24
|
+
mode,
|
|
25
|
+
landAt,
|
|
26
|
+
locked,
|
|
27
|
+
lockEnabled = true,
|
|
28
|
+
onLockedChange,
|
|
29
|
+
onFolderChange,
|
|
30
|
+
onWalkPosition,
|
|
31
|
+
flyTo,
|
|
32
|
+
}: PlayerProps) {
|
|
33
|
+
const { camera } = useThree()
|
|
34
|
+
const walking = mode === 'walk'
|
|
35
|
+
const keys = useRef({
|
|
36
|
+
forward: false,
|
|
37
|
+
back: false,
|
|
38
|
+
left: false,
|
|
39
|
+
right: false,
|
|
40
|
+
sprint: false,
|
|
41
|
+
})
|
|
42
|
+
const lastFolder = useRef<string | null>(null)
|
|
43
|
+
const front = useRef(new THREE.Vector3())
|
|
44
|
+
const right = useRef(new THREE.Vector3())
|
|
45
|
+
const up = useRef(new THREE.Vector3(0, 1, 0))
|
|
46
|
+
const move = useRef(new THREE.Vector3())
|
|
47
|
+
const lastFly = useRef(0)
|
|
48
|
+
const lookAt = useRef(new THREE.Vector3())
|
|
49
|
+
const [steering, setSteering] = useState(true)
|
|
50
|
+
const controlsRef = useRef<PointerLockControlsImpl>(null)
|
|
51
|
+
const capturedLook = useRef(false)
|
|
52
|
+
const flying = useRef<{
|
|
53
|
+
startPos: THREE.Vector3
|
|
54
|
+
endPos: THREE.Vector3
|
|
55
|
+
lookAt: THREE.Vector3
|
|
56
|
+
duration: number
|
|
57
|
+
t: number
|
|
58
|
+
} | null>(null)
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!walking) {
|
|
62
|
+
flying.current = null
|
|
63
|
+
capturedLook.current = false
|
|
64
|
+
setSteering(true)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
if (flyTo && flyTo.nonce !== lastFly.current) {
|
|
68
|
+
lastFly.current = flyTo.nonce
|
|
69
|
+
const end = new THREE.Vector3(landAt[0], CONFIG.eyeHeight, landAt[1])
|
|
70
|
+
lookAt.current.set(flyTo.lookAt[0], flyTo.lookAt[1], flyTo.lookAt[2])
|
|
71
|
+
const dist = camera.position.distanceTo(end)
|
|
72
|
+
flying.current = {
|
|
73
|
+
startPos: camera.position.clone(),
|
|
74
|
+
endPos: end,
|
|
75
|
+
lookAt: lookAt.current.clone(),
|
|
76
|
+
duration: Math.min(1.35, Math.max(0.55, dist * 0.025)),
|
|
77
|
+
t: 0,
|
|
78
|
+
}
|
|
79
|
+
setSteering(false)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
if (flying.current) return
|
|
83
|
+
camera.up.set(0, 1, 0)
|
|
84
|
+
camera.position.set(landAt[0], CONFIG.eyeHeight, landAt[1])
|
|
85
|
+
camera.lookAt(landAt[0], CONFIG.eyeHeight, landAt[1] + 10)
|
|
86
|
+
camera.updateProjectionMatrix()
|
|
87
|
+
}, [camera, flyTo, landAt, walking])
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const onKey = (event: KeyboardEvent, down: boolean) => {
|
|
91
|
+
if (
|
|
92
|
+
event.target instanceof HTMLElement &&
|
|
93
|
+
(event.target.tagName === 'TEXTAREA' ||
|
|
94
|
+
event.target.tagName === 'INPUT' ||
|
|
95
|
+
event.target.tagName === 'SELECT' ||
|
|
96
|
+
event.target.isContentEditable)
|
|
97
|
+
) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
if (event.code === 'KeyW' || event.code === 'ArrowUp') keys.current.forward = down
|
|
101
|
+
if (event.code === 'KeyS' || event.code === 'ArrowDown') keys.current.back = down
|
|
102
|
+
if (event.code === 'KeyA' || event.code === 'ArrowLeft') keys.current.left = down
|
|
103
|
+
if (event.code === 'KeyD' || event.code === 'ArrowRight') keys.current.right = down
|
|
104
|
+
if (event.code === 'ShiftLeft' || event.code === 'ShiftRight') keys.current.sprint = down
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const down = (event: KeyboardEvent) => onKey(event, true)
|
|
108
|
+
const up = (event: KeyboardEvent) => onKey(event, false)
|
|
109
|
+
window.addEventListener('keydown', down)
|
|
110
|
+
window.addEventListener('keyup', up)
|
|
111
|
+
return () => {
|
|
112
|
+
window.removeEventListener('keydown', down)
|
|
113
|
+
window.removeEventListener('keyup', up)
|
|
114
|
+
}
|
|
115
|
+
}, [])
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!walking || !lockEnabled) {
|
|
119
|
+
controlsRef.current?.unlock()
|
|
120
|
+
document.exitPointerLock()
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
if (!steering || capturedLook.current) return
|
|
124
|
+
const controls = controlsRef.current
|
|
125
|
+
const element = controls?.domElement
|
|
126
|
+
if (!controls || !element) return
|
|
127
|
+
capturedLook.current = true
|
|
128
|
+
if (element.ownerDocument.pointerLockElement === element) {
|
|
129
|
+
controls.isLocked = true
|
|
130
|
+
onLockedChange(true)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
controls.lock()
|
|
134
|
+
}, [lockEnabled, onLockedChange, steering, walking])
|
|
135
|
+
|
|
136
|
+
useFrame((_, delta) => {
|
|
137
|
+
try {
|
|
138
|
+
if (walking && flying.current) {
|
|
139
|
+
const flight = flying.current
|
|
140
|
+
flight.t += delta / flight.duration
|
|
141
|
+
const t = Math.min(1, flight.t)
|
|
142
|
+
const ease = t * t * (3 - 2 * t)
|
|
143
|
+
camera.position.lerpVectors(flight.startPos, flight.endPos, ease)
|
|
144
|
+
const dist = flight.startPos.distanceTo(flight.endPos)
|
|
145
|
+
camera.position.y =
|
|
146
|
+
CONFIG.eyeHeight + Math.sin(t * Math.PI) * Math.min(14, 3.5 + dist * 0.1)
|
|
147
|
+
camera.up.set(0, 1, 0)
|
|
148
|
+
camera.lookAt(flight.lookAt)
|
|
149
|
+
if (t >= 1) {
|
|
150
|
+
camera.position.copy(flight.endPos)
|
|
151
|
+
camera.up.set(0, 1, 0)
|
|
152
|
+
camera.lookAt(flight.lookAt)
|
|
153
|
+
flying.current = null
|
|
154
|
+
setSteering(true)
|
|
155
|
+
}
|
|
156
|
+
} else if (walking && locked) {
|
|
157
|
+
camera.getWorldDirection(front.current)
|
|
158
|
+
front.current.y = 0
|
|
159
|
+
front.current.normalize()
|
|
160
|
+
right.current.crossVectors(front.current, up.current).normalize()
|
|
161
|
+
move.current.set(0, 0, 0)
|
|
162
|
+
if (keys.current.forward) move.current.add(front.current)
|
|
163
|
+
if (keys.current.back) move.current.sub(front.current)
|
|
164
|
+
if (keys.current.right) move.current.add(right.current)
|
|
165
|
+
if (keys.current.left) move.current.sub(right.current)
|
|
166
|
+
if (move.current.lengthSq() > 0) {
|
|
167
|
+
const speed = keys.current.sprint ? CONFIG.sprintSpeed : CONFIG.walkSpeed
|
|
168
|
+
move.current.normalize().multiplyScalar(speed * delta)
|
|
169
|
+
camera.position.add(move.current)
|
|
170
|
+
}
|
|
171
|
+
camera.position.y = CONFIG.eyeHeight
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!walking) return
|
|
175
|
+
onWalkPosition(camera.position.x, camera.position.z)
|
|
176
|
+
const nextPath = locationLabel(camera.position.x, camera.position.z, layout)
|
|
177
|
+
if (nextPath !== lastFolder.current) {
|
|
178
|
+
lastFolder.current = nextPath
|
|
179
|
+
onFolderChange(nextPath)
|
|
180
|
+
}
|
|
181
|
+
} catch {
|
|
182
|
+
// Movement must never stop the render loop.
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
if (!walking) return null
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<>
|
|
190
|
+
<PerspectiveCamera
|
|
191
|
+
makeDefault
|
|
192
|
+
fov={70}
|
|
193
|
+
near={0.1}
|
|
194
|
+
far={400}
|
|
195
|
+
position={[landAt[0], CONFIG.eyeHeight, landAt[1]]}
|
|
196
|
+
/>
|
|
197
|
+
<PlayerLight />
|
|
198
|
+
<PointerLockControls
|
|
199
|
+
ref={controlsRef}
|
|
200
|
+
makeDefault
|
|
201
|
+
enabled={steering && lockEnabled}
|
|
202
|
+
onLock={() => onLockedChange(true)}
|
|
203
|
+
onUnlock={() => onLockedChange(false)}
|
|
204
|
+
/>
|
|
205
|
+
</>
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function PlayerLight() {
|
|
210
|
+
const { camera } = useThree()
|
|
211
|
+
const overhead = useRef<THREE.PointLight>(null)
|
|
212
|
+
const look = useRef<THREE.PointLight>(null)
|
|
213
|
+
const direction = useRef(new THREE.Vector3())
|
|
214
|
+
|
|
215
|
+
useFrame(() => {
|
|
216
|
+
if (!overhead.current || !look.current) return
|
|
217
|
+
|
|
218
|
+
overhead.current.position.copy(camera.position)
|
|
219
|
+
overhead.current.position.y += 2.6
|
|
220
|
+
|
|
221
|
+
camera.getWorldDirection(direction.current)
|
|
222
|
+
look.current.position.copy(camera.position)
|
|
223
|
+
look.current.position.addScaledVector(direction.current, 5)
|
|
224
|
+
look.current.position.y = camera.position.y + 1.4
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<>
|
|
229
|
+
<pointLight
|
|
230
|
+
ref={overhead}
|
|
231
|
+
color="#f4f1e8"
|
|
232
|
+
intensity={7.5}
|
|
233
|
+
distance={26}
|
|
234
|
+
decay={1.4}
|
|
235
|
+
/>
|
|
236
|
+
<pointLight
|
|
237
|
+
ref={look}
|
|
238
|
+
color="#e4eef8"
|
|
239
|
+
intensity={3.4}
|
|
240
|
+
distance={18}
|
|
241
|
+
decay={1.6}
|
|
242
|
+
/>
|
|
243
|
+
</>
|
|
244
|
+
)
|
|
245
|
+
}
|