@jkwd/inbase 0.1.11 → 0.1.13
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 +1 -1
- package/apps/explorer/scripts/session-store.d.ts +2 -0
- package/apps/explorer/scripts/session-store.mjs +72 -10
- package/apps/explorer/src/App.tsx +44 -5
- package/apps/explorer/src/agentIntent.ts +7 -1
- package/apps/explorer/src/index.css +135 -1
- package/apps/explorer/src/scene/DistantFileBlocks.tsx +44 -0
- package/apps/explorer/src/scene/FileBlock.tsx +28 -37
- package/apps/explorer/src/scene/FolderArea.tsx +4 -2
- package/apps/explorer/src/scene/MapView.tsx +149 -37
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +9 -1
- package/apps/explorer/src/scene/WalkLodTracker.tsx +92 -0
- package/apps/explorer/src/scene/World.tsx +115 -25
- package/apps/explorer/src/scene/walkLod.ts +201 -0
- package/apps/explorer/src/types.ts +1 -0
- package/apps/explorer/src/ui/HUD.tsx +67 -38
- package/apps/explorer/src/ui/MapContextMenu.tsx +89 -0
- package/apps/explorer/src/userCreated.ts +50 -0
- package/apps/explorer/vite.config.ts +2 -0
- package/bin/session.mjs +21 -5
- package/package.json +1 -1
- package/skill/commands/inbase.md +2 -2
- package/skill/inbase/SKILL.md +9 -6
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
4
|
import { Html, MapControls, OrthographicCamera } from '@react-three/drei'
|
|
4
5
|
import * as THREE from 'three'
|
|
5
6
|
import { folderAt, worldBounds } from '../layout'
|
|
6
7
|
import type { ChangeKind } from '../theme'
|
|
7
|
-
import type { WorldLayout } from '../types'
|
|
8
|
+
import type { PlacedFolder, WorldLayout } from '../types'
|
|
9
|
+
|
|
10
|
+
export type MapBlueprintMenu = {
|
|
11
|
+
x: number
|
|
12
|
+
y: number
|
|
13
|
+
folder: string
|
|
14
|
+
}
|
|
8
15
|
|
|
9
16
|
type MapViewProps = {
|
|
10
17
|
layout: WorldLayout
|
|
@@ -16,6 +23,7 @@ type MapViewProps = {
|
|
|
16
23
|
onSelect: (fileId: string | null) => void
|
|
17
24
|
onSelectFolder: (folderPath: string | null) => void
|
|
18
25
|
onTravelTo: (fromId: string, toId: string) => void
|
|
26
|
+
onBlueprintMenu?: (menu: MapBlueprintMenu) => void
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export function MapView({
|
|
@@ -28,9 +36,13 @@ export function MapView({
|
|
|
28
36
|
onSelect,
|
|
29
37
|
onSelectFolder,
|
|
30
38
|
onTravelTo,
|
|
39
|
+
onBlueprintMenu,
|
|
31
40
|
}: MapViewProps) {
|
|
32
41
|
const size = useThree((state) => state.size)
|
|
33
|
-
const
|
|
42
|
+
const camera = useThree((state) => state.camera)
|
|
43
|
+
const gl = useThree((state) => state.gl)
|
|
44
|
+
const invalidate = useThree((state) => state.invalidate)
|
|
45
|
+
const scene = useThree((state) => state.scene)
|
|
34
46
|
const bounds = useMemo(() => worldBounds(layout), [layout])
|
|
35
47
|
const drag = useRef({ x: 0, y: 0, moved: false, active: false })
|
|
36
48
|
const sized = size.width > 16 && size.height > 16
|
|
@@ -172,7 +184,28 @@ export function MapView({
|
|
|
172
184
|
}
|
|
173
185
|
|
|
174
186
|
const onContextMenu = (event: MouseEvent) => {
|
|
175
|
-
if (event.ctrlKey)
|
|
187
|
+
if (event.ctrlKey) {
|
|
188
|
+
event.preventDefault()
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
if (!onBlueprintMenu) return
|
|
192
|
+
event.preventDefault()
|
|
193
|
+
const pick = pickAt(event.clientX, event.clientY)
|
|
194
|
+
if (!pick) return
|
|
195
|
+
const hit = new THREE.Vector3()
|
|
196
|
+
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
197
|
+
if (!pick.raycaster.ray.intersectPlane(plane, hit)) return
|
|
198
|
+
const folder =
|
|
199
|
+
folderAt(hit.x, hit.z, layout) ??
|
|
200
|
+
(selectedFolder ? layout.folders[selectedFolder] : undefined) ??
|
|
201
|
+
layout.folders['.']
|
|
202
|
+
if (!folder) return
|
|
203
|
+
onSelectFolder(folder.path)
|
|
204
|
+
onBlueprintMenu({
|
|
205
|
+
x: event.clientX,
|
|
206
|
+
y: event.clientY,
|
|
207
|
+
folder: folder.path,
|
|
208
|
+
})
|
|
176
209
|
}
|
|
177
210
|
|
|
178
211
|
const ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
@@ -250,10 +283,12 @@ export function MapView({
|
|
|
250
283
|
invalidate,
|
|
251
284
|
layout,
|
|
252
285
|
onLand,
|
|
286
|
+
onBlueprintMenu,
|
|
253
287
|
onSelect,
|
|
254
288
|
onSelectFolder,
|
|
255
289
|
onTravelTo,
|
|
256
290
|
scene,
|
|
291
|
+
selectedFolder,
|
|
257
292
|
])
|
|
258
293
|
|
|
259
294
|
return (
|
|
@@ -274,44 +309,121 @@ export function MapView({
|
|
|
274
309
|
maxZoom={Math.max(fitZoom * 10, 20)}
|
|
275
310
|
/>
|
|
276
311
|
)}
|
|
277
|
-
{enabled &&
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
style={{ pointerEvents: 'none' }}
|
|
285
|
-
>
|
|
286
|
-
<div
|
|
287
|
-
className={[
|
|
288
|
-
'map-folder-label',
|
|
289
|
-
highlightedFolders?.[folder.path]
|
|
290
|
-
? `map-folder-label-${highlightedFolders[folder.path]}`
|
|
291
|
-
: folder.added
|
|
292
|
-
? 'map-folder-label-added'
|
|
293
|
-
: selectedFolder === folder.path
|
|
294
|
-
? 'map-folder-label-selected'
|
|
295
|
-
: '',
|
|
296
|
-
]
|
|
297
|
-
.filter(Boolean)
|
|
298
|
-
.join(' ')}
|
|
299
|
-
>
|
|
300
|
-
<span className="map-folder-name">
|
|
301
|
-
{folderKindLabel(
|
|
302
|
-
folder.name,
|
|
303
|
-
highlightedFolders?.[folder.path] ?? null,
|
|
304
|
-
folder.added ?? false,
|
|
305
|
-
)}
|
|
306
|
-
</span>
|
|
307
|
-
</div>
|
|
308
|
-
</Html>
|
|
309
|
-
))}
|
|
312
|
+
{enabled && (
|
|
313
|
+
<MapFolderLabels
|
|
314
|
+
folders={layout.folders}
|
|
315
|
+
highlightedFolders={highlightedFolders}
|
|
316
|
+
selectedFolder={selectedFolder}
|
|
317
|
+
/>
|
|
318
|
+
)}
|
|
310
319
|
{enabled && marker && <LandMarker marker={marker} />}
|
|
311
320
|
</>
|
|
312
321
|
)
|
|
313
322
|
}
|
|
314
323
|
|
|
324
|
+
const PROJECT = new THREE.Vector3()
|
|
325
|
+
const MIN_FOLDER_LABEL_PX = 28
|
|
326
|
+
|
|
327
|
+
function folderLabelClass(
|
|
328
|
+
folder: PlacedFolder,
|
|
329
|
+
highlightedFolders: Partial<Record<string, ChangeKind>> | undefined,
|
|
330
|
+
selectedFolder: string | null,
|
|
331
|
+
) {
|
|
332
|
+
return [
|
|
333
|
+
'map-folder-label',
|
|
334
|
+
highlightedFolders?.[folder.path]
|
|
335
|
+
? `map-folder-label-${highlightedFolders[folder.path]}`
|
|
336
|
+
: folder.added
|
|
337
|
+
? 'map-folder-label-added'
|
|
338
|
+
: selectedFolder === folder.path
|
|
339
|
+
? 'map-folder-label-selected'
|
|
340
|
+
: '',
|
|
341
|
+
]
|
|
342
|
+
.filter(Boolean)
|
|
343
|
+
.join(' ')
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function MapFolderLabels({
|
|
347
|
+
folders,
|
|
348
|
+
highlightedFolders,
|
|
349
|
+
selectedFolder,
|
|
350
|
+
}: {
|
|
351
|
+
folders: Record<string, PlacedFolder>
|
|
352
|
+
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
353
|
+
selectedFolder: string | null
|
|
354
|
+
}) {
|
|
355
|
+
const camera = useThree((state) => state.camera)
|
|
356
|
+
const gl = useThree((state) => state.gl)
|
|
357
|
+
const size = useThree((state) => state.size)
|
|
358
|
+
const layerRef = useRef<HTMLDivElement>(null)
|
|
359
|
+
const items = useMemo(() => Object.values(folders), [folders])
|
|
360
|
+
const [host, setHost] = useState<HTMLElement | null>(null)
|
|
361
|
+
|
|
362
|
+
useLayoutEffect(() => {
|
|
363
|
+
setHost(gl.domElement.parentElement)
|
|
364
|
+
}, [gl])
|
|
365
|
+
|
|
366
|
+
useFrame(() => {
|
|
367
|
+
const layer = layerRef.current
|
|
368
|
+
if (!layer) return
|
|
369
|
+
const zoom = 'zoom' in camera ? Number(camera.zoom) : 1
|
|
370
|
+
const nodes = layer.children
|
|
371
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
372
|
+
const el = nodes[i] as HTMLElement | undefined
|
|
373
|
+
const folder = items[i]
|
|
374
|
+
if (!el || !folder) continue
|
|
375
|
+
PROJECT.set(folder.x, 14, folder.z + 1.35).project(camera)
|
|
376
|
+
const x = (PROJECT.x * 0.5 + 0.5) * size.width
|
|
377
|
+
const y = (-PROJECT.y * 0.5 + 0.5) * size.height
|
|
378
|
+
const span = Math.max(folder.width, folder.depth) * zoom
|
|
379
|
+
const force =
|
|
380
|
+
selectedFolder === folder.path ||
|
|
381
|
+
Boolean(highlightedFolders?.[folder.path] || folder.added)
|
|
382
|
+
const onScreen =
|
|
383
|
+
PROJECT.z >= -1 &&
|
|
384
|
+
PROJECT.z <= 1 &&
|
|
385
|
+
x > -120 &&
|
|
386
|
+
x < size.width + 120 &&
|
|
387
|
+
y > -40 &&
|
|
388
|
+
y < size.height + 40
|
|
389
|
+
if (!onScreen || (!force && span < MIN_FOLDER_LABEL_PX)) {
|
|
390
|
+
if (el.style.visibility !== 'hidden') el.style.visibility = 'hidden'
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
const tx = Math.round(x)
|
|
394
|
+
const ty = Math.round(y)
|
|
395
|
+
const next = `${tx},${ty}`
|
|
396
|
+
if (el.dataset.pos !== next) {
|
|
397
|
+
el.dataset.pos = next
|
|
398
|
+
el.style.transform = `translate3d(${tx}px, ${ty}px, 0) translate(-50%, -50%)`
|
|
399
|
+
}
|
|
400
|
+
if (el.style.visibility !== 'visible') el.style.visibility = 'visible'
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
if (!host) return null
|
|
405
|
+
|
|
406
|
+
return createPortal(
|
|
407
|
+
<div ref={layerRef} className="map-folder-label-layer">
|
|
408
|
+
{items.map((folder) => (
|
|
409
|
+
<div
|
|
410
|
+
key={folder.path}
|
|
411
|
+
className={folderLabelClass(folder, highlightedFolders, selectedFolder)}
|
|
412
|
+
>
|
|
413
|
+
<span className="map-folder-name">
|
|
414
|
+
{folderKindLabel(
|
|
415
|
+
folder.name,
|
|
416
|
+
highlightedFolders?.[folder.path] ?? null,
|
|
417
|
+
folder.added ?? false,
|
|
418
|
+
)}
|
|
419
|
+
</span>
|
|
420
|
+
</div>
|
|
421
|
+
))}
|
|
422
|
+
</div>,
|
|
423
|
+
host,
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
315
427
|
function folderKindLabel(
|
|
316
428
|
name: string,
|
|
317
429
|
kind: ChangeKind | null | undefined,
|
|
@@ -425,16 +425,20 @@ function CameraRig({
|
|
|
425
425
|
function ThumbnailLabelFilter({
|
|
426
426
|
candidates,
|
|
427
427
|
zoom,
|
|
428
|
+
frozenRef,
|
|
428
429
|
onChange,
|
|
429
430
|
}: {
|
|
430
431
|
candidates: LabelCandidate[]
|
|
431
432
|
zoom: number
|
|
433
|
+
frozenRef: { current: boolean }
|
|
432
434
|
onChange: (ids: Set<string>) => void
|
|
433
435
|
}) {
|
|
434
|
-
const
|
|
436
|
+
const camera = useThree((state) => state.camera)
|
|
437
|
+
const size = useThree((state) => state.size)
|
|
435
438
|
const visibleRef = useRef<Set<string>>(new Set())
|
|
436
439
|
|
|
437
440
|
useFrame(() => {
|
|
441
|
+
if (frozenRef.current) return
|
|
438
442
|
const next = visibleThumbnailLabels(
|
|
439
443
|
candidates,
|
|
440
444
|
camera,
|
|
@@ -464,6 +468,7 @@ function ThumbnailScene({
|
|
|
464
468
|
panRef,
|
|
465
469
|
orbitRef,
|
|
466
470
|
cameraRef,
|
|
471
|
+
frozenRef,
|
|
467
472
|
visibleLabelIds,
|
|
468
473
|
onVisibleLabels,
|
|
469
474
|
}: {
|
|
@@ -480,6 +485,7 @@ function ThumbnailScene({
|
|
|
480
485
|
panRef: { current: Vec3 }
|
|
481
486
|
orbitRef: { current: Orbit }
|
|
482
487
|
cameraRef: { current: THREE.Camera | null }
|
|
488
|
+
frozenRef: { current: boolean }
|
|
483
489
|
visibleLabelIds: Set<string>
|
|
484
490
|
onVisibleLabels: (ids: Set<string>) => void
|
|
485
491
|
}) {
|
|
@@ -541,6 +547,7 @@ function ThumbnailScene({
|
|
|
541
547
|
<ThumbnailLabelFilter
|
|
542
548
|
candidates={labelCandidates}
|
|
543
549
|
zoom={zoom}
|
|
550
|
+
frozenRef={frozenRef}
|
|
544
551
|
onChange={onVisibleLabels}
|
|
545
552
|
/>
|
|
546
553
|
<FolderArea
|
|
@@ -977,6 +984,7 @@ export function SelectionThumbnail({
|
|
|
977
984
|
panRef={panRef}
|
|
978
985
|
orbitRef={orbitRef}
|
|
979
986
|
cameraRef={cameraRef}
|
|
987
|
+
frozenRef={panningRef}
|
|
980
988
|
visibleLabelIds={visibleLabelIds}
|
|
981
989
|
onVisibleLabels={setVisibleLabelIds}
|
|
982
990
|
/>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react'
|
|
2
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import type { PlacedBridge, PlacedFile, PlacedFolder } from '../types'
|
|
5
|
+
import {
|
|
6
|
+
computeWalkLod,
|
|
7
|
+
sameWalkLod,
|
|
8
|
+
walkLodCell,
|
|
9
|
+
type WalkLod,
|
|
10
|
+
} from './walkLod'
|
|
11
|
+
|
|
12
|
+
const look = new THREE.Vector3()
|
|
13
|
+
|
|
14
|
+
type WalkLodTrackerProps = {
|
|
15
|
+
files: Record<string, PlacedFile>
|
|
16
|
+
folders: Record<string, PlacedFolder>
|
|
17
|
+
bridges: PlacedBridge[]
|
|
18
|
+
keepFileIds: Set<string>
|
|
19
|
+
keepFolderPaths: Set<string>
|
|
20
|
+
origin: [number, number]
|
|
21
|
+
onChange: (lod: WalkLod) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function WalkLodTracker({
|
|
25
|
+
files,
|
|
26
|
+
folders,
|
|
27
|
+
bridges,
|
|
28
|
+
keepFileIds,
|
|
29
|
+
keepFolderPaths,
|
|
30
|
+
origin,
|
|
31
|
+
onChange,
|
|
32
|
+
}: WalkLodTrackerProps) {
|
|
33
|
+
const { camera } = useThree()
|
|
34
|
+
const prev = useRef<WalkLod | null>(null)
|
|
35
|
+
const cell = useRef('')
|
|
36
|
+
const keepFilesRef = useRef(keepFileIds)
|
|
37
|
+
const keepFoldersRef = useRef(keepFolderPaths)
|
|
38
|
+
const filesRef = useRef(files)
|
|
39
|
+
const foldersRef = useRef(folders)
|
|
40
|
+
const bridgesRef = useRef(bridges)
|
|
41
|
+
const onChangeRef = useRef(onChange)
|
|
42
|
+
keepFilesRef.current = keepFileIds
|
|
43
|
+
keepFoldersRef.current = keepFolderPaths
|
|
44
|
+
filesRef.current = files
|
|
45
|
+
foldersRef.current = folders
|
|
46
|
+
bridgesRef.current = bridges
|
|
47
|
+
onChangeRef.current = onChange
|
|
48
|
+
|
|
49
|
+
const publish = (x: number, z: number, y: number, lookX: number, lookZ: number) => {
|
|
50
|
+
const next = computeWalkLod({
|
|
51
|
+
x,
|
|
52
|
+
z,
|
|
53
|
+
y,
|
|
54
|
+
lookX,
|
|
55
|
+
lookZ,
|
|
56
|
+
files: filesRef.current,
|
|
57
|
+
folders: foldersRef.current,
|
|
58
|
+
bridges: bridgesRef.current,
|
|
59
|
+
keepFileIds: keepFilesRef.current,
|
|
60
|
+
keepFolderPaths: keepFoldersRef.current,
|
|
61
|
+
prev: prev.current,
|
|
62
|
+
})
|
|
63
|
+
if (sameWalkLod(prev.current, next)) return
|
|
64
|
+
prev.current = next
|
|
65
|
+
onChangeRef.current(next)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const keepKey = `${[...keepFileIds].join('|')}|${[...keepFolderPaths].join('|')}`
|
|
69
|
+
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
camera.getWorldDirection(look)
|
|
72
|
+
const lx = look.x
|
|
73
|
+
const lz = look.z
|
|
74
|
+
if (Math.hypot(lx, lz) < 0.001) {
|
|
75
|
+
publish(origin[0], origin[1], camera.position.y, 0, 1)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
publish(camera.position.x, camera.position.z, camera.position.y, lx, lz)
|
|
79
|
+
}, [bridges, camera, files, folders, keepKey, origin])
|
|
80
|
+
|
|
81
|
+
useFrame(() => {
|
|
82
|
+
camera.getWorldDirection(look)
|
|
83
|
+
const lx = look.x
|
|
84
|
+
const lz = look.z
|
|
85
|
+
const nextCell = `${walkLodCell(camera.position.x, camera.position.z, camera.position.y, lx, lz)}:${keepKey}`
|
|
86
|
+
if (nextCell === cell.current) return
|
|
87
|
+
cell.current = nextCell
|
|
88
|
+
publish(camera.position.x, camera.position.z, camera.position.y, lx, lz)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react'
|
|
1
2
|
import { FolderArea } from './FolderArea'
|
|
2
3
|
import { FileBlock } from './FileBlock'
|
|
4
|
+
import { DistantFileBlocks } from './DistantFileBlocks'
|
|
3
5
|
import { Bridge } from './Bridge'
|
|
4
6
|
import { RelationLines } from './RelationLines'
|
|
5
7
|
import { Player } from './Player'
|
|
6
|
-
import { MapView } from './MapView'
|
|
8
|
+
import { MapView, type MapBlueprintMenu } from './MapView'
|
|
7
9
|
import { SelectionController } from './SelectionController'
|
|
8
10
|
import { UserContextTracker } from './UserContextTracker'
|
|
9
11
|
import { BlockPlacer } from './BlockPlacer'
|
|
10
12
|
import { IslandPlacer } from './IslandPlacer'
|
|
13
|
+
import { WalkLodTracker } from './WalkLodTracker'
|
|
14
|
+
import { computeWalkLod, type WalkLod } from './walkLod'
|
|
11
15
|
import {
|
|
12
16
|
fileChangeKind,
|
|
13
17
|
filesImporting,
|
|
@@ -60,6 +64,7 @@ type WorldProps = {
|
|
|
60
64
|
namingId?: string | null
|
|
61
65
|
onPlaceBlock?: (spot: { x: number; z: number; folder: string }) => void
|
|
62
66
|
onPlaceIsland?: (parent: string) => void
|
|
67
|
+
onBlueprintMenu?: (menu: MapBlueprintMenu) => void
|
|
63
68
|
onCommitName?: (id: string, name: string) => boolean
|
|
64
69
|
onCancelName?: (id: string) => void
|
|
65
70
|
userCreatedBlocks?: UserCreatedBlock[]
|
|
@@ -100,6 +105,7 @@ export function World({
|
|
|
100
105
|
namingIslandId = null,
|
|
101
106
|
onPlaceBlock,
|
|
102
107
|
onPlaceIsland,
|
|
108
|
+
onBlueprintMenu,
|
|
103
109
|
onCommitName,
|
|
104
110
|
onCancelName,
|
|
105
111
|
userCreatedBlocks = [],
|
|
@@ -150,6 +156,59 @@ export function World({
|
|
|
150
156
|
deleted,
|
|
151
157
|
viewLayout.folders,
|
|
152
158
|
)
|
|
159
|
+
const ghostKey = Object.keys(ghosts).join('|')
|
|
160
|
+
const keepFileIds = useMemo(() => {
|
|
161
|
+
const ids = new Set<string>()
|
|
162
|
+
if (selectedId) ids.add(selectedId)
|
|
163
|
+
if (namingId) ids.add(namingId)
|
|
164
|
+
if (aimedRelation?.flyTo) ids.add(aimedRelation.flyTo)
|
|
165
|
+
if (ghostKey) {
|
|
166
|
+
for (const id of ghostKey.split('|')) ids.add(id)
|
|
167
|
+
}
|
|
168
|
+
return ids
|
|
169
|
+
}, [aimedRelation?.flyTo, ghostKey, namingId, selectedId])
|
|
170
|
+
const keepFolderPaths = useMemo(() => {
|
|
171
|
+
const paths = new Set<string>()
|
|
172
|
+
if (selectedFolder) paths.add(selectedFolder)
|
|
173
|
+
if (namingIslandId) paths.add(namingIslandId)
|
|
174
|
+
return paths
|
|
175
|
+
}, [namingIslandId, selectedFolder])
|
|
176
|
+
const originLod = useMemo(() => {
|
|
177
|
+
if (mapping) return null
|
|
178
|
+
return computeWalkLod({
|
|
179
|
+
x: landAt[0],
|
|
180
|
+
z: landAt[1],
|
|
181
|
+
lookX: 0,
|
|
182
|
+
lookZ: 1,
|
|
183
|
+
files: layout.files,
|
|
184
|
+
folders: layout.folders,
|
|
185
|
+
bridges: layout.bridges,
|
|
186
|
+
keepFileIds,
|
|
187
|
+
keepFolderPaths,
|
|
188
|
+
prev: null,
|
|
189
|
+
})
|
|
190
|
+
}, [
|
|
191
|
+
keepFileIds,
|
|
192
|
+
keepFolderPaths,
|
|
193
|
+
landAt,
|
|
194
|
+
layout.bridges,
|
|
195
|
+
layout.files,
|
|
196
|
+
layout.folders,
|
|
197
|
+
mapping,
|
|
198
|
+
])
|
|
199
|
+
const landSig = `${landAt[0]},${landAt[1]}`
|
|
200
|
+
const [lodLand, setLodLand] = useState(landSig)
|
|
201
|
+
const [walkLod, setWalkLod] = useState<WalkLod | null>(null)
|
|
202
|
+
if (lodLand !== landSig) {
|
|
203
|
+
setLodLand(landSig)
|
|
204
|
+
setWalkLod(null)
|
|
205
|
+
}
|
|
206
|
+
const lod = mapping ? null : (walkLod ?? originLod)
|
|
207
|
+
const distantFiles: {
|
|
208
|
+
file: FileNode
|
|
209
|
+
placed: PlacedFile
|
|
210
|
+
dimmed: boolean
|
|
211
|
+
}[] = []
|
|
153
212
|
|
|
154
213
|
return (
|
|
155
214
|
<>
|
|
@@ -171,23 +230,33 @@ export function World({
|
|
|
171
230
|
onSelect={onSelect}
|
|
172
231
|
onSelectFolder={onSelectFolder}
|
|
173
232
|
onTravelTo={onTravelTo}
|
|
233
|
+
onBlueprintMenu={
|
|
234
|
+
mapping && !placing && onBlueprintMenu ? onBlueprintMenu : undefined
|
|
235
|
+
}
|
|
174
236
|
/>
|
|
175
237
|
|
|
176
|
-
{Object.values(viewLayout.folders).map((folder) =>
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
mapping
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
238
|
+
{Object.values(viewLayout.folders).map((folder) => {
|
|
239
|
+
if (lod && !lod.folders.has(folder.path)) return null
|
|
240
|
+
return (
|
|
241
|
+
<FolderArea
|
|
242
|
+
key={folder.path}
|
|
243
|
+
folder={folder}
|
|
244
|
+
naming={folder.path === namingIslandId}
|
|
245
|
+
selected={folder.path === selectedFolder}
|
|
246
|
+
mapMode={mapping}
|
|
247
|
+
highlightKind={
|
|
248
|
+
mapping ? highlightedFolders[folder.path] ?? null : null
|
|
249
|
+
}
|
|
250
|
+
labelVisible={!lod || lod.folderLabels.has(folder.path)}
|
|
251
|
+
/>
|
|
252
|
+
)
|
|
253
|
+
})}
|
|
254
|
+
{viewLayout.bridges.map((bridge) => {
|
|
255
|
+
if (lod && !lod.bridges.has(bridge.id)) return null
|
|
256
|
+
return (
|
|
257
|
+
<Bridge key={bridge.id} bridge={bridge} folders={viewLayout.folders} />
|
|
258
|
+
)
|
|
259
|
+
})}
|
|
191
260
|
{viewGraph.files.map((file) => {
|
|
192
261
|
const placed = viewLayout.files[file.id]
|
|
193
262
|
if (!placed) return null
|
|
@@ -196,6 +265,21 @@ export function World({
|
|
|
196
265
|
const isPlanned = planned.has(file.id) || deleted.has(file.id)
|
|
197
266
|
const changeKind = fileChangeKind(file.id, planned, created, deleted)
|
|
198
267
|
const naming = file.id === namingId
|
|
268
|
+
const aimed = file.id === aimedRelation?.flyTo
|
|
269
|
+
const detailed =
|
|
270
|
+
selected || isRelated || isPlanned || naming || aimed || Boolean(changeKind)
|
|
271
|
+
if (lod && !lod.files.has(file.id)) return null
|
|
272
|
+
const dimmed =
|
|
273
|
+
hasFocus &&
|
|
274
|
+
!selected &&
|
|
275
|
+
!isRelated &&
|
|
276
|
+
!changeKind &&
|
|
277
|
+
!patchLinked.has(file.id) &&
|
|
278
|
+
!folderFileIds.has(file.id)
|
|
279
|
+
if (lod && !detailed && !lod.labels.has(file.id)) {
|
|
280
|
+
distantFiles.push({ file, placed, dimmed })
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
199
283
|
return (
|
|
200
284
|
<FileBlock
|
|
201
285
|
key={file.id}
|
|
@@ -206,17 +290,11 @@ export function World({
|
|
|
206
290
|
planned={isPlanned}
|
|
207
291
|
changeKind={changeKind}
|
|
208
292
|
added={created.has(file.id) || file.userCreated}
|
|
209
|
-
aimed={
|
|
210
|
-
dimmed={
|
|
211
|
-
hasFocus &&
|
|
212
|
-
!selected &&
|
|
213
|
-
!isRelated &&
|
|
214
|
-
!changeKind &&
|
|
215
|
-
!patchLinked.has(file.id) &&
|
|
216
|
-
!folderFileIds.has(file.id)
|
|
217
|
-
}
|
|
293
|
+
aimed={aimed}
|
|
294
|
+
dimmed={dimmed}
|
|
218
295
|
naming={naming}
|
|
219
296
|
mapMode={mapping}
|
|
297
|
+
labelVisible={!lod || lod.labels.has(file.id) || naming}
|
|
220
298
|
onCommitName={
|
|
221
299
|
naming && onCommitName
|
|
222
300
|
? (name) => {
|
|
@@ -230,6 +308,7 @@ export function World({
|
|
|
230
308
|
/>
|
|
231
309
|
)
|
|
232
310
|
})}
|
|
311
|
+
<DistantFileBlocks items={distantFiles} />
|
|
233
312
|
{Object.values(ghosts).map((placed) => {
|
|
234
313
|
const file: FileNode = {
|
|
235
314
|
id: placed.id,
|
|
@@ -278,6 +357,17 @@ export function World({
|
|
|
278
357
|
onWalkPosition={onWalkPosition}
|
|
279
358
|
flyTo={flyTo}
|
|
280
359
|
/>
|
|
360
|
+
{!mapping && (
|
|
361
|
+
<WalkLodTracker
|
|
362
|
+
files={layout.files}
|
|
363
|
+
folders={layout.folders}
|
|
364
|
+
bridges={layout.bridges}
|
|
365
|
+
keepFileIds={keepFileIds}
|
|
366
|
+
keepFolderPaths={keepFolderPaths}
|
|
367
|
+
origin={landAt}
|
|
368
|
+
onChange={setWalkLod}
|
|
369
|
+
/>
|
|
370
|
+
)}
|
|
281
371
|
{onPlaceBlock && (
|
|
282
372
|
<BlockPlacer
|
|
283
373
|
enabled={!mapping && !placing}
|