@jkwd/inbase 0.1.6 → 0.1.8
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 +5 -0
- package/apps/explorer/scripts/session-store.d.ts +4 -0
- package/apps/explorer/scripts/session-store.mjs +16 -0
- package/apps/explorer/src/App.tsx +40 -9
- package/apps/explorer/src/codebase.ts +23 -5
- package/apps/explorer/src/index.css +190 -4
- package/apps/explorer/src/scene/Bridge.tsx +57 -20
- package/apps/explorer/src/scene/MapView.tsx +37 -8
- package/apps/explorer/src/scene/Player.tsx +106 -42
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +140 -8
- package/apps/explorer/src/scene/World.tsx +0 -3
- package/apps/explorer/src/types.ts +1 -0
- package/apps/explorer/src/ui/HUD.tsx +343 -90
- package/apps/explorer/vite.config.ts +13 -3
- package/package.json +1 -1
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
import { useEffect, useRef, useState } from 'react'
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
2
2
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
3
|
import { PerspectiveCamera, PointerLockControls } from '@react-three/drei'
|
|
4
4
|
import type { PointerLockControls as PointerLockControlsImpl } from 'three-stdlib'
|
|
5
5
|
import * as THREE from 'three'
|
|
6
6
|
import { CONFIG } from '../theme'
|
|
7
|
-
import { locationLabel } from '../layout'
|
|
8
7
|
import type { FlyTo, ViewMode, WorldLayout } from '../types'
|
|
9
8
|
|
|
9
|
+
const lookDir = new THREE.Vector3()
|
|
10
|
+
const flyPos = new THREE.Vector3()
|
|
11
|
+
const flyLook = new THREE.Vector3()
|
|
12
|
+
|
|
13
|
+
function smootherstep(t: number) {
|
|
14
|
+
const x = Math.min(1, Math.max(0, t))
|
|
15
|
+
return x * x * x * (x * (x * 6 - 15) + 10)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function quadBezier(
|
|
19
|
+
out: THREE.Vector3,
|
|
20
|
+
a: THREE.Vector3,
|
|
21
|
+
b: THREE.Vector3,
|
|
22
|
+
c: THREE.Vector3,
|
|
23
|
+
t: number,
|
|
24
|
+
) {
|
|
25
|
+
const u = 1 - t
|
|
26
|
+
out.set(
|
|
27
|
+
u * u * a.x + 2 * u * t * b.x + t * t * c.x,
|
|
28
|
+
u * u * a.y + 2 * u * t * b.y + t * t * c.y,
|
|
29
|
+
u * u * a.z + 2 * u * t * b.z + t * t * c.z,
|
|
30
|
+
)
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
type PlayerProps = {
|
|
11
35
|
layout: WorldLayout
|
|
12
36
|
mode: ViewMode
|
|
@@ -14,19 +38,16 @@ type PlayerProps = {
|
|
|
14
38
|
locked: boolean
|
|
15
39
|
lockEnabled?: boolean
|
|
16
40
|
onLockedChange: (locked: boolean) => void
|
|
17
|
-
onFolderChange: (label: string) => void
|
|
18
41
|
onWalkPosition: (x: number, z: number) => void
|
|
19
42
|
flyTo: FlyTo | null
|
|
20
43
|
}
|
|
21
44
|
|
|
22
45
|
export function Player({
|
|
23
|
-
layout,
|
|
24
46
|
mode,
|
|
25
47
|
landAt,
|
|
26
48
|
locked,
|
|
27
49
|
lockEnabled = true,
|
|
28
50
|
onLockedChange,
|
|
29
|
-
onFolderChange,
|
|
30
51
|
onWalkPosition,
|
|
31
52
|
flyTo,
|
|
32
53
|
}: PlayerProps) {
|
|
@@ -39,52 +60,85 @@ export function Player({
|
|
|
39
60
|
right: false,
|
|
40
61
|
sprint: false,
|
|
41
62
|
})
|
|
42
|
-
const lastFolder = useRef<string | null>(null)
|
|
43
63
|
const front = useRef(new THREE.Vector3())
|
|
44
64
|
const right = useRef(new THREE.Vector3())
|
|
45
65
|
const up = useRef(new THREE.Vector3(0, 1, 0))
|
|
46
66
|
const move = useRef(new THREE.Vector3())
|
|
47
67
|
const lastFly = useRef(0)
|
|
48
|
-
const lookAt = useRef(new THREE.Vector3())
|
|
49
68
|
const [steering, setSteering] = useState(true)
|
|
50
69
|
const controlsRef = useRef<PointerLockControlsImpl>(null)
|
|
51
70
|
const capturedLook = useRef(false)
|
|
52
71
|
const lookHeld = useRef(false)
|
|
72
|
+
const poseReady = useRef(false)
|
|
73
|
+
const groundedPose = useRef({
|
|
74
|
+
pos: new THREE.Vector3(),
|
|
75
|
+
look: new THREE.Vector3(),
|
|
76
|
+
})
|
|
53
77
|
const flying = useRef<{
|
|
54
78
|
startPos: THREE.Vector3
|
|
79
|
+
midPos: THREE.Vector3
|
|
55
80
|
endPos: THREE.Vector3
|
|
56
|
-
|
|
81
|
+
startLook: THREE.Vector3
|
|
82
|
+
endLook: THREE.Vector3
|
|
57
83
|
duration: number
|
|
58
84
|
t: number
|
|
59
85
|
} | null>(null)
|
|
60
86
|
|
|
61
|
-
|
|
87
|
+
useLayoutEffect(() => {
|
|
62
88
|
if (!walking) {
|
|
63
89
|
flying.current = null
|
|
64
90
|
capturedLook.current = false
|
|
91
|
+
poseReady.current = false
|
|
65
92
|
setSteering(true)
|
|
66
93
|
return
|
|
67
94
|
}
|
|
68
95
|
if (flyTo && flyTo.nonce !== lastFly.current) {
|
|
69
96
|
lastFly.current = flyTo.nonce
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const
|
|
97
|
+
const endPos = new THREE.Vector3(landAt[0], CONFIG.eyeHeight, landAt[1])
|
|
98
|
+
const endLook = new THREE.Vector3(flyTo.lookAt[0], flyTo.lookAt[1], flyTo.lookAt[2])
|
|
99
|
+
const startPos = flying.current
|
|
100
|
+
? camera.position.clone()
|
|
101
|
+
: poseReady.current
|
|
102
|
+
? groundedPose.current.pos.clone()
|
|
103
|
+
: new THREE.Vector3(flyTo.from[0], CONFIG.eyeHeight, flyTo.from[1])
|
|
104
|
+
const startLook = new THREE.Vector3()
|
|
105
|
+
if (flying.current || poseReady.current) {
|
|
106
|
+
camera.getWorldDirection(lookDir)
|
|
107
|
+
startLook.copy(startPos).addScaledVector(lookDir, 16)
|
|
108
|
+
} else {
|
|
109
|
+
startLook.copy(startPos).lerp(endLook, 0.4)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const dist = startPos.distanceTo(endPos)
|
|
113
|
+
if (dist < 0.2) {
|
|
114
|
+
placeCamera(camera, endPos, endLook)
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lift = Math.min(26, 6 + dist * 0.22)
|
|
73
119
|
flying.current = {
|
|
74
|
-
startPos
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
120
|
+
startPos,
|
|
121
|
+
midPos: new THREE.Vector3(
|
|
122
|
+
(startPos.x + endPos.x) * 0.5,
|
|
123
|
+
CONFIG.eyeHeight + lift,
|
|
124
|
+
(startPos.z + endPos.z) * 0.5,
|
|
125
|
+
),
|
|
126
|
+
endPos,
|
|
127
|
+
startLook,
|
|
128
|
+
endLook,
|
|
129
|
+
duration: Math.min(2.6, Math.max(0.8, 0.55 + dist * 0.04)),
|
|
78
130
|
t: 0,
|
|
79
131
|
}
|
|
132
|
+
placeCamera(camera, startPos, startLook)
|
|
80
133
|
setSteering(false)
|
|
81
134
|
return
|
|
82
135
|
}
|
|
83
136
|
if (flying.current) return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
137
|
+
placeCamera(
|
|
138
|
+
camera,
|
|
139
|
+
new THREE.Vector3(landAt[0], CONFIG.eyeHeight, landAt[1]),
|
|
140
|
+
new THREE.Vector3(landAt[0], CONFIG.eyeHeight, landAt[1] + 10),
|
|
141
|
+
)
|
|
88
142
|
}, [camera, flyTo, landAt, walking])
|
|
89
143
|
|
|
90
144
|
useEffect(() => {
|
|
@@ -163,20 +217,21 @@ export function Player({
|
|
|
163
217
|
try {
|
|
164
218
|
if (walking && flying.current) {
|
|
165
219
|
const flight = flying.current
|
|
166
|
-
flight.t += delta / flight.duration
|
|
220
|
+
flight.t += Math.min(delta, 0.05) / flight.duration
|
|
167
221
|
const t = Math.min(1, flight.t)
|
|
168
|
-
const ease =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
camera.position.
|
|
172
|
-
CONFIG.eyeHeight + Math.sin(t * Math.PI) * Math.min(14, 3.5 + dist * 0.1)
|
|
222
|
+
const ease = smootherstep(t)
|
|
223
|
+
quadBezier(flyPos, flight.startPos, flight.midPos, flight.endPos, ease)
|
|
224
|
+
flyLook.lerpVectors(flight.startLook, flight.endLook, ease)
|
|
225
|
+
camera.position.copy(flyPos)
|
|
173
226
|
camera.up.set(0, 1, 0)
|
|
174
|
-
camera.lookAt(
|
|
227
|
+
camera.lookAt(flyLook)
|
|
228
|
+
camera.rotation.order = 'YXZ'
|
|
175
229
|
if (t >= 1) {
|
|
176
|
-
camera.
|
|
177
|
-
camera.up.set(0, 1, 0)
|
|
178
|
-
camera.lookAt(flight.lookAt)
|
|
230
|
+
placeCamera(camera, flight.endPos, flight.endLook)
|
|
179
231
|
flying.current = null
|
|
232
|
+
poseReady.current = true
|
|
233
|
+
groundedPose.current.pos.copy(flight.endPos)
|
|
234
|
+
groundedPose.current.look.copy(flight.endLook)
|
|
180
235
|
setSteering(true)
|
|
181
236
|
}
|
|
182
237
|
} else if (walking && locked) {
|
|
@@ -198,12 +253,13 @@ export function Player({
|
|
|
198
253
|
}
|
|
199
254
|
|
|
200
255
|
if (!walking) return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
256
|
+
if (!flying.current) {
|
|
257
|
+
poseReady.current = true
|
|
258
|
+
groundedPose.current.pos.copy(camera.position)
|
|
259
|
+
camera.getWorldDirection(lookDir)
|
|
260
|
+
groundedPose.current.look.copy(camera.position).addScaledVector(lookDir, 16)
|
|
206
261
|
}
|
|
262
|
+
onWalkPosition(camera.position.x, camera.position.z)
|
|
207
263
|
} catch {
|
|
208
264
|
// Movement must never stop the render loop.
|
|
209
265
|
}
|
|
@@ -213,13 +269,7 @@ export function Player({
|
|
|
213
269
|
|
|
214
270
|
return (
|
|
215
271
|
<>
|
|
216
|
-
<PerspectiveCamera
|
|
217
|
-
makeDefault
|
|
218
|
-
fov={70}
|
|
219
|
-
near={0.1}
|
|
220
|
-
far={400}
|
|
221
|
-
position={[landAt[0], CONFIG.eyeHeight, landAt[1]]}
|
|
222
|
-
/>
|
|
272
|
+
<PerspectiveCamera makeDefault fov={70} near={0.1} far={400} />
|
|
223
273
|
<PlayerLight />
|
|
224
274
|
<PointerLockControls
|
|
225
275
|
ref={controlsRef}
|
|
@@ -241,6 +291,20 @@ export function Player({
|
|
|
241
291
|
)
|
|
242
292
|
}
|
|
243
293
|
|
|
294
|
+
function placeCamera(
|
|
295
|
+
camera: THREE.Camera,
|
|
296
|
+
position: THREE.Vector3,
|
|
297
|
+
lookAt: THREE.Vector3,
|
|
298
|
+
) {
|
|
299
|
+
camera.position.copy(position)
|
|
300
|
+
camera.up.set(0, 1, 0)
|
|
301
|
+
camera.lookAt(lookAt)
|
|
302
|
+
camera.rotation.order = 'YXZ'
|
|
303
|
+
if ('updateProjectionMatrix' in camera) {
|
|
304
|
+
;(camera as THREE.PerspectiveCamera).updateProjectionMatrix()
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
244
308
|
function PlayerLight() {
|
|
245
309
|
const { camera } = useThree()
|
|
246
310
|
const overhead = useRef<THREE.PointLight>(null)
|
|
@@ -15,6 +15,7 @@ import { FolderArea } from './FolderArea'
|
|
|
15
15
|
import { RelationLines } from './RelationLines'
|
|
16
16
|
|
|
17
17
|
type Vec3 = [number, number, number]
|
|
18
|
+
type Orbit = { yaw: number; pitch: number }
|
|
18
19
|
|
|
19
20
|
type SelectionThumbnailProps = {
|
|
20
21
|
graph: CodebaseGraph
|
|
@@ -24,10 +25,12 @@ type SelectionThumbnailProps = {
|
|
|
24
25
|
landAt: [number, number]
|
|
25
26
|
importedBy?: boolean
|
|
26
27
|
minimized?: boolean
|
|
28
|
+
maximized?: boolean
|
|
27
29
|
plannedIds?: string[]
|
|
28
30
|
createdIds?: string[]
|
|
29
31
|
deletedIds?: string[]
|
|
30
32
|
onMinimize?: () => void
|
|
33
|
+
onMaximize?: () => void
|
|
31
34
|
onHide: () => void
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -159,7 +162,10 @@ function resolveTarget(
|
|
|
159
162
|
|
|
160
163
|
const MIN_ZOOM = 0.45
|
|
161
164
|
const MAX_ZOOM = 6
|
|
165
|
+
const MIN_ORBIT_PITCH = 0.08
|
|
166
|
+
const MAX_ORBIT_PITCH = Math.PI / 2 - 0.06
|
|
162
167
|
const ZERO_PAN: Vec3 = [0, 0, 0]
|
|
168
|
+
const ZERO_ORBIT: Orbit = { yaw: 0, pitch: 0 }
|
|
163
169
|
const WORLD_UP: Vec3 = [0, 1, 0]
|
|
164
170
|
const LABEL_PROJECT = new THREE.Vector3()
|
|
165
171
|
const GROUND_PLANE = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
@@ -212,14 +218,61 @@ function cameraOffset(position: Vec3, lookAt: Vec3, zoom: number): Vec3 {
|
|
|
212
218
|
return vecScale(vecSub(position, lookAt), 1 / zoom)
|
|
213
219
|
}
|
|
214
220
|
|
|
221
|
+
function sphericalFromOffset(offset: Vec3) {
|
|
222
|
+
const radius = vecLength(offset)
|
|
223
|
+
return {
|
|
224
|
+
radius,
|
|
225
|
+
yaw: Math.atan2(offset[0], offset[2]),
|
|
226
|
+
pitch: Math.asin(
|
|
227
|
+
Math.min(1, Math.max(-1, offset[1] / Math.max(radius, 1e-6))),
|
|
228
|
+
),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function offsetFromSpherical(radius: number, yaw: number, pitch: number): Vec3 {
|
|
233
|
+
const cosPitch = Math.cos(pitch)
|
|
234
|
+
return [
|
|
235
|
+
radius * Math.sin(yaw) * cosPitch,
|
|
236
|
+
radius * Math.sin(pitch),
|
|
237
|
+
radius * Math.cos(yaw) * cosPitch,
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function clampOrbitPitch(basePitch: number, orbitPitch: number) {
|
|
242
|
+
return Math.min(
|
|
243
|
+
MAX_ORBIT_PITCH - basePitch,
|
|
244
|
+
Math.max(MIN_ORBIT_PITCH - basePitch, orbitPitch),
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function orbitedOffset(
|
|
249
|
+
position: Vec3,
|
|
250
|
+
lookAt: Vec3,
|
|
251
|
+
zoom: number,
|
|
252
|
+
orbit: Orbit,
|
|
253
|
+
): Vec3 {
|
|
254
|
+
const base = cameraOffset(position, lookAt, zoom)
|
|
255
|
+
if (orbit.yaw === 0 && orbit.pitch === 0) return base
|
|
256
|
+
const spherical = sphericalFromOffset(base)
|
|
257
|
+
return offsetFromSpherical(
|
|
258
|
+
spherical.radius,
|
|
259
|
+
spherical.yaw + orbit.yaw,
|
|
260
|
+
Math.min(
|
|
261
|
+
MAX_ORBIT_PITCH,
|
|
262
|
+
Math.max(MIN_ORBIT_PITCH, spherical.pitch + orbit.pitch),
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
215
267
|
function applyThumbnailCamera(
|
|
216
268
|
camera: THREE.Camera,
|
|
217
269
|
position: Vec3,
|
|
218
270
|
lookAt: Vec3,
|
|
219
271
|
zoom: number,
|
|
220
272
|
pan: Vec3,
|
|
273
|
+
orbit: Orbit,
|
|
221
274
|
) {
|
|
222
|
-
const offset =
|
|
275
|
+
const offset = orbitedOffset(position, lookAt, zoom, orbit)
|
|
223
276
|
camera.up.set(0, 1, 0)
|
|
224
277
|
camera.position.set(
|
|
225
278
|
lookAt[0] + pan[0] + offset[0],
|
|
@@ -247,8 +300,13 @@ function worldUnderCursor(
|
|
|
247
300
|
return Boolean(CURSOR_RAY.ray.intersectPlane(GROUND_PLANE, target))
|
|
248
301
|
}
|
|
249
302
|
|
|
250
|
-
function cameraPanBasis(
|
|
251
|
-
|
|
303
|
+
function cameraPanBasis(
|
|
304
|
+
position: Vec3,
|
|
305
|
+
lookAt: Vec3,
|
|
306
|
+
zoom: number,
|
|
307
|
+
orbit: Orbit,
|
|
308
|
+
) {
|
|
309
|
+
const offset = orbitedOffset(position, lookAt, zoom, orbit)
|
|
252
310
|
const forward = vecNormalize(vecScale(offset, -1))
|
|
253
311
|
const right = vecNormalize(vecCross(forward, WORLD_UP))
|
|
254
312
|
const up = vecNormalize(vecCross(right, forward))
|
|
@@ -328,12 +386,14 @@ function CameraRig({
|
|
|
328
386
|
lookAt,
|
|
329
387
|
zoomRef,
|
|
330
388
|
panRef,
|
|
389
|
+
orbitRef,
|
|
331
390
|
cameraRef,
|
|
332
391
|
}: {
|
|
333
392
|
position: Vec3
|
|
334
393
|
lookAt: Vec3
|
|
335
394
|
zoomRef: { current: number }
|
|
336
395
|
panRef: { current: Vec3 }
|
|
396
|
+
orbitRef: { current: Orbit }
|
|
337
397
|
cameraRef: { current: THREE.Camera | null }
|
|
338
398
|
}) {
|
|
339
399
|
const { camera } = useThree()
|
|
@@ -345,10 +405,19 @@ function CameraRig({
|
|
|
345
405
|
lookAt,
|
|
346
406
|
zoomRef.current,
|
|
347
407
|
panRef.current,
|
|
408
|
+
orbitRef.current,
|
|
348
409
|
)
|
|
349
410
|
}
|
|
350
411
|
|
|
351
|
-
useLayoutEffect(aim, [
|
|
412
|
+
useLayoutEffect(aim, [
|
|
413
|
+
camera,
|
|
414
|
+
cameraRef,
|
|
415
|
+
lookAt,
|
|
416
|
+
orbitRef,
|
|
417
|
+
panRef,
|
|
418
|
+
position,
|
|
419
|
+
zoomRef,
|
|
420
|
+
])
|
|
352
421
|
useFrame(aim)
|
|
353
422
|
return null
|
|
354
423
|
}
|
|
@@ -393,6 +462,7 @@ function ThumbnailScene({
|
|
|
393
462
|
zoom,
|
|
394
463
|
zoomRef,
|
|
395
464
|
panRef,
|
|
465
|
+
orbitRef,
|
|
396
466
|
cameraRef,
|
|
397
467
|
visibleLabelIds,
|
|
398
468
|
onVisibleLabels,
|
|
@@ -408,6 +478,7 @@ function ThumbnailScene({
|
|
|
408
478
|
zoom: number
|
|
409
479
|
zoomRef: { current: number }
|
|
410
480
|
panRef: { current: Vec3 }
|
|
481
|
+
orbitRef: { current: Orbit }
|
|
411
482
|
cameraRef: { current: THREE.Camera | null }
|
|
412
483
|
visibleLabelIds: Set<string>
|
|
413
484
|
onVisibleLabels: (ids: Set<string>) => void
|
|
@@ -464,6 +535,7 @@ function ThumbnailScene({
|
|
|
464
535
|
lookAt={target.camera.lookAt}
|
|
465
536
|
zoomRef={zoomRef}
|
|
466
537
|
panRef={panRef}
|
|
538
|
+
orbitRef={orbitRef}
|
|
467
539
|
cameraRef={cameraRef}
|
|
468
540
|
/>
|
|
469
541
|
<ThumbnailLabelFilter
|
|
@@ -524,16 +596,19 @@ export function SelectionThumbnail({
|
|
|
524
596
|
landAt,
|
|
525
597
|
importedBy = false,
|
|
526
598
|
minimized = false,
|
|
599
|
+
maximized = false,
|
|
527
600
|
plannedIds = [],
|
|
528
601
|
createdIds = [],
|
|
529
602
|
deletedIds = [],
|
|
530
603
|
onMinimize,
|
|
604
|
+
onMaximize,
|
|
531
605
|
onHide,
|
|
532
606
|
}: SelectionThumbnailProps) {
|
|
533
607
|
const stageRef = useRef<HTMLDivElement>(null)
|
|
534
608
|
const cameraRef = useRef<THREE.Camera | null>(null)
|
|
535
609
|
const zoomRef = useRef(1)
|
|
536
610
|
const panRef = useRef<Vec3>(ZERO_PAN)
|
|
611
|
+
const orbitRef = useRef<Orbit>(ZERO_ORBIT)
|
|
537
612
|
const pinchRef = useRef({ start: 0, zoom: 1 })
|
|
538
613
|
const zoomAtCursorRef = useRef(
|
|
539
614
|
(_clientX: number, _clientY: number, _dollyScale: number) => {},
|
|
@@ -543,9 +618,12 @@ export function SelectionThumbnail({
|
|
|
543
618
|
x: 0,
|
|
544
619
|
y: 0,
|
|
545
620
|
pan: ZERO_PAN,
|
|
621
|
+
orbit: ZERO_ORBIT,
|
|
622
|
+
rotate: false,
|
|
546
623
|
})
|
|
547
624
|
const [zoom, setZoom] = useState(1)
|
|
548
625
|
const [panning, setPanning] = useState(false)
|
|
626
|
+
const [orbiting, setOrbiting] = useState(false)
|
|
549
627
|
const [visibleLabelIds, setVisibleLabelIds] = useState<Set<string>>(
|
|
550
628
|
() => new Set(),
|
|
551
629
|
)
|
|
@@ -574,9 +652,11 @@ export function SelectionThumbnail({
|
|
|
574
652
|
useEffect(() => {
|
|
575
653
|
zoomRef.current = 1
|
|
576
654
|
panRef.current = ZERO_PAN
|
|
655
|
+
orbitRef.current = ZERO_ORBIT
|
|
577
656
|
setZoom(1)
|
|
578
657
|
panningRef.current = false
|
|
579
658
|
setPanning(false)
|
|
659
|
+
setOrbiting(false)
|
|
580
660
|
}, [selectedId, selectedFolder, landAt])
|
|
581
661
|
|
|
582
662
|
useEffect(() => {
|
|
@@ -624,6 +704,7 @@ export function SelectionThumbnail({
|
|
|
624
704
|
target.camera.lookAt,
|
|
625
705
|
nextZoom,
|
|
626
706
|
panRef.current,
|
|
707
|
+
orbitRef.current,
|
|
627
708
|
)
|
|
628
709
|
if (
|
|
629
710
|
hit &&
|
|
@@ -640,6 +721,7 @@ export function SelectionThumbnail({
|
|
|
640
721
|
target.camera.lookAt,
|
|
641
722
|
nextZoom,
|
|
642
723
|
panRef.current,
|
|
724
|
+
orbitRef.current,
|
|
643
725
|
)
|
|
644
726
|
}
|
|
645
727
|
}
|
|
@@ -692,6 +774,7 @@ export function SelectionThumbnail({
|
|
|
692
774
|
dragRef.current.pointerId = -1
|
|
693
775
|
panningRef.current = false
|
|
694
776
|
setPanning(false)
|
|
777
|
+
setOrbiting(false)
|
|
695
778
|
pinchRef.current = {
|
|
696
779
|
start: touchDistance(event.touches),
|
|
697
780
|
zoom: zoomRef.current,
|
|
@@ -722,6 +805,8 @@ export function SelectionThumbnail({
|
|
|
722
805
|
x: event.clientX,
|
|
723
806
|
y: event.clientY,
|
|
724
807
|
pan: panRef.current,
|
|
808
|
+
orbit: { ...orbitRef.current },
|
|
809
|
+
rotate: event.metaKey || event.ctrlKey,
|
|
725
810
|
}
|
|
726
811
|
stage.setPointerCapture(event.pointerId)
|
|
727
812
|
}
|
|
@@ -733,12 +818,28 @@ export function SelectionThumbnail({
|
|
|
733
818
|
if (!panningRef.current) {
|
|
734
819
|
if (Math.hypot(dx, dy) <= 3) return
|
|
735
820
|
panningRef.current = true
|
|
736
|
-
|
|
821
|
+
if (dragRef.current.rotate) setOrbiting(true)
|
|
822
|
+
else setPanning(true)
|
|
823
|
+
}
|
|
824
|
+
if (dragRef.current.rotate) {
|
|
825
|
+
const speed = (2 * Math.PI) / Math.max(stage.clientHeight, 1)
|
|
826
|
+
const basePitch = sphericalFromOffset(
|
|
827
|
+
cameraOffset(target.camera.position, target.camera.lookAt, 1),
|
|
828
|
+
).pitch
|
|
829
|
+
orbitRef.current = {
|
|
830
|
+
yaw: dragRef.current.orbit.yaw - dx * speed,
|
|
831
|
+
pitch: clampOrbitPitch(
|
|
832
|
+
basePitch,
|
|
833
|
+
dragRef.current.orbit.pitch + dy * speed,
|
|
834
|
+
),
|
|
835
|
+
}
|
|
836
|
+
return
|
|
737
837
|
}
|
|
738
838
|
const { right, up, distance } = cameraPanBasis(
|
|
739
839
|
target.camera.position,
|
|
740
840
|
target.camera.lookAt,
|
|
741
841
|
zoomRef.current,
|
|
842
|
+
orbitRef.current,
|
|
742
843
|
)
|
|
743
844
|
const speed = distance / Math.max(stage.clientHeight, 1)
|
|
744
845
|
panRef.current = vecAdd(
|
|
@@ -752,6 +853,7 @@ export function SelectionThumbnail({
|
|
|
752
853
|
dragRef.current.pointerId = -1
|
|
753
854
|
panningRef.current = false
|
|
754
855
|
setPanning(false)
|
|
856
|
+
setOrbiting(false)
|
|
755
857
|
if (stage.hasPointerCapture(event.pointerId)) {
|
|
756
858
|
stage.releasePointerCapture(event.pointerId)
|
|
757
859
|
}
|
|
@@ -762,6 +864,7 @@ export function SelectionThumbnail({
|
|
|
762
864
|
event.preventDefault()
|
|
763
865
|
zoomRef.current = 1
|
|
764
866
|
panRef.current = ZERO_PAN
|
|
867
|
+
orbitRef.current = ZERO_ORBIT
|
|
765
868
|
setZoom(1)
|
|
766
869
|
}
|
|
767
870
|
|
|
@@ -789,14 +892,29 @@ export function SelectionThumbnail({
|
|
|
789
892
|
}
|
|
790
893
|
}, [minimized, target])
|
|
791
894
|
|
|
895
|
+
useEffect(() => {
|
|
896
|
+
if (!maximized) return
|
|
897
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
898
|
+
if (event.key !== 'Escape') return
|
|
899
|
+
event.preventDefault()
|
|
900
|
+
onMaximize?.()
|
|
901
|
+
}
|
|
902
|
+
window.addEventListener('keydown', onKeyDown)
|
|
903
|
+
return () => window.removeEventListener('keydown', onKeyDown)
|
|
904
|
+
}, [maximized, onMaximize])
|
|
905
|
+
|
|
792
906
|
if (!target) return null
|
|
793
907
|
|
|
794
908
|
return (
|
|
795
|
-
<div
|
|
909
|
+
<div
|
|
910
|
+
className="hud-thumbnail"
|
|
911
|
+
data-minimized={minimized}
|
|
912
|
+
data-maximized={maximized}
|
|
913
|
+
>
|
|
796
914
|
<div className="hud-thumbnail-bar">
|
|
797
915
|
<span>3D view</span>
|
|
798
916
|
<div className="hud-panel-controls">
|
|
799
|
-
{onMinimize && (
|
|
917
|
+
{onMinimize && !maximized && (
|
|
800
918
|
<button
|
|
801
919
|
className="hud-button hud-icon-button hud-panel-control"
|
|
802
920
|
type="button"
|
|
@@ -806,6 +924,16 @@ export function SelectionThumbnail({
|
|
|
806
924
|
{minimized ? '+' : '−'}
|
|
807
925
|
</button>
|
|
808
926
|
)}
|
|
927
|
+
{onMaximize && (
|
|
928
|
+
<button
|
|
929
|
+
className="hud-button hud-icon-button hud-panel-control"
|
|
930
|
+
type="button"
|
|
931
|
+
aria-label={maximized ? 'Minimize 3D view' : 'Maximize 3D view'}
|
|
932
|
+
onClick={onMaximize}
|
|
933
|
+
>
|
|
934
|
+
{maximized ? '−' : '□'}
|
|
935
|
+
</button>
|
|
936
|
+
)}
|
|
809
937
|
<button
|
|
810
938
|
className="hud-button hud-icon-button hud-panel-control"
|
|
811
939
|
type="button"
|
|
@@ -821,6 +949,7 @@ export function SelectionThumbnail({
|
|
|
821
949
|
className="hud-thumbnail-stage"
|
|
822
950
|
ref={stageRef}
|
|
823
951
|
data-panning={panning}
|
|
952
|
+
data-orbiting={orbiting}
|
|
824
953
|
>
|
|
825
954
|
<Canvas
|
|
826
955
|
shadows={false}
|
|
@@ -846,12 +975,15 @@ export function SelectionThumbnail({
|
|
|
846
975
|
zoom={zoom}
|
|
847
976
|
zoomRef={zoomRef}
|
|
848
977
|
panRef={panRef}
|
|
978
|
+
orbitRef={orbitRef}
|
|
849
979
|
cameraRef={cameraRef}
|
|
850
980
|
visibleLabelIds={visibleLabelIds}
|
|
851
981
|
onVisibleLabels={setVisibleLabelIds}
|
|
852
982
|
/>
|
|
853
983
|
</Canvas>
|
|
854
|
-
<div className="hud-thumbnail-hint">
|
|
984
|
+
<div className="hud-thumbnail-hint">
|
|
985
|
+
Scroll zoom · drag pan · ⌘ drag rotate
|
|
986
|
+
</div>
|
|
855
987
|
<div className="hud-thumbnail-nav">
|
|
856
988
|
<button
|
|
857
989
|
className="hud-button hud-icon-button"
|
|
@@ -41,7 +41,6 @@ type WorldProps = {
|
|
|
41
41
|
onSelect: (fileId: string | null) => void
|
|
42
42
|
onSelectFolder: (folderPath: string | null) => void
|
|
43
43
|
onLockedChange: (locked: boolean) => void
|
|
44
|
-
onFolderChange: (label: string) => void
|
|
45
44
|
onLand: (x: number, z: number) => void
|
|
46
45
|
onWalkPosition: (x: number, z: number) => void
|
|
47
46
|
onContext: (context: UserContext) => void
|
|
@@ -81,7 +80,6 @@ export function World({
|
|
|
81
80
|
onSelect,
|
|
82
81
|
onSelectFolder,
|
|
83
82
|
onLockedChange,
|
|
84
|
-
onFolderChange,
|
|
85
83
|
onLand,
|
|
86
84
|
onWalkPosition,
|
|
87
85
|
onContext,
|
|
@@ -277,7 +275,6 @@ export function World({
|
|
|
277
275
|
locked={locked}
|
|
278
276
|
lockEnabled={!placing}
|
|
279
277
|
onLockedChange={onLockedChange}
|
|
280
|
-
onFolderChange={onFolderChange}
|
|
281
278
|
onWalkPosition={onWalkPosition}
|
|
282
279
|
flyTo={flyTo}
|
|
283
280
|
/>
|