@jkwd/inbase 0.1.20 → 0.1.22
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 +13 -7
- package/apps/explorer/package.json +1 -0
- package/apps/explorer/scripts/explain-store.d.ts +135 -0
- package/apps/explorer/scripts/explain-store.mjs +666 -0
- package/apps/explorer/scripts/patch-lib.mjs +4 -0
- package/apps/explorer/scripts/scan-ignore.mjs +9 -0
- package/apps/explorer/scripts/scan-target.mjs +22 -5
- package/apps/explorer/scripts/session-store.d.ts +86 -11
- package/apps/explorer/scripts/session-store.mjs +371 -58
- package/apps/explorer/scripts/target-config.d.ts +38 -3
- package/apps/explorer/scripts/target-config.mjs +147 -3
- package/apps/explorer/src/App.tsx +1080 -158
- package/apps/explorer/src/agentIntent.ts +61 -7
- package/apps/explorer/src/codebase.ts +1 -1
- package/apps/explorer/src/devTargets.ts +66 -0
- package/apps/explorer/src/explain.ts +312 -0
- package/apps/explorer/src/index.css +884 -223
- package/apps/explorer/src/layout.ts +55 -0
- package/apps/explorer/src/scene/DistantFileBlocks.tsx +4 -2
- package/apps/explorer/src/scene/FileBlock.tsx +95 -72
- package/apps/explorer/src/scene/FolderArea.tsx +55 -32
- package/apps/explorer/src/scene/MapView.tsx +506 -33
- package/apps/explorer/src/scene/RelationLines.tsx +7 -0
- package/apps/explorer/src/scene/World.tsx +146 -32
- package/apps/explorer/src/speech.ts +228 -0
- package/apps/explorer/src/theme.ts +29 -0
- package/apps/explorer/src/types.ts +98 -8
- package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +3 -1
- package/apps/explorer/src/ui/ExplainAskCard.tsx +142 -0
- package/apps/explorer/src/ui/ExplainHud.tsx +524 -0
- package/apps/explorer/src/ui/ExplainInfoPanel.tsx +135 -0
- package/apps/explorer/src/ui/ExplainPointer.tsx +73 -0
- package/apps/explorer/src/ui/EyeIcon.tsx +38 -1
- package/apps/explorer/src/ui/HUD.tsx +1067 -792
- package/apps/explorer/src/ui/NameInput.tsx +114 -5
- package/apps/explorer/src/userContext.ts +0 -11
- package/apps/explorer/src/userCreated.ts +54 -1
- package/apps/explorer/vite.config.ts +173 -26
- package/bin/inbase.mjs +11 -2
- package/bin/project.mjs +1 -1
- package/bin/session.mjs +287 -38
- package/package.json +4 -1
- package/skill/commands/amber.md +23 -0
- package/skill/commands/blue.md +13 -0
- package/skill/commands/coral.md +23 -0
- package/skill/commands/explain.md +77 -0
- package/skill/commands/green.md +23 -0
- package/skill/commands/inbase.md +7 -5
- package/skill/commands/lime.md +23 -0
- package/skill/commands/orange.md +23 -0
- package/skill/commands/purple.md +23 -0
- package/skill/commands/red.md +23 -0
- package/skill/commands/skipinbase.md +1 -1
- package/skill/commands/violet.md +23 -0
- package/skill/commands/yellow.md +23 -0
- package/skill/inbase/SKILL.md +124 -76
- package/apps/explorer/src/scene/BlockPlacer.tsx +0 -78
- package/apps/explorer/src/scene/IslandPlacer.tsx +0 -31
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +0 -1050
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
3
|
import { Html, MapControls, OrthographicCamera } from '@react-three/drei'
|
|
4
4
|
import * as THREE from 'three'
|
|
5
5
|
import { folderAt, worldBounds } from '../layout'
|
|
6
6
|
import type { ChangeKind } from '../theme'
|
|
7
|
+
import { blueprintPalette } from '../theme'
|
|
7
8
|
import type { PlacedFolder, WorldLayout } from '../types'
|
|
9
|
+
import { eyeIconMarkup } from '../ui/EyeIcon'
|
|
8
10
|
|
|
9
11
|
export type MapBlueprintMenu = {
|
|
10
12
|
x: number
|
|
@@ -12,6 +14,111 @@ export type MapBlueprintMenu = {
|
|
|
12
14
|
folder: string
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export type MapFileLabel = {
|
|
18
|
+
id: string
|
|
19
|
+
name: string
|
|
20
|
+
x: number
|
|
21
|
+
z: number
|
|
22
|
+
width: number
|
|
23
|
+
depth: number
|
|
24
|
+
outer: 1 | -1
|
|
25
|
+
selected: boolean
|
|
26
|
+
pointed: boolean
|
|
27
|
+
pointedColor?: string
|
|
28
|
+
dimmed?: boolean
|
|
29
|
+
focused?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type MapFocusBounds = {
|
|
33
|
+
cx: number
|
|
34
|
+
cz: number
|
|
35
|
+
width: number
|
|
36
|
+
depth: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type MapPose = {
|
|
40
|
+
cx: number
|
|
41
|
+
cz: number
|
|
42
|
+
width: number
|
|
43
|
+
depth: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type MapFlight = {
|
|
47
|
+
from: MapPose
|
|
48
|
+
via: MapPose
|
|
49
|
+
to: MapPose
|
|
50
|
+
start: number
|
|
51
|
+
duration: number
|
|
52
|
+
split: number
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const FOCUS_FLY_IN_MS = 900
|
|
56
|
+
const FOCUS_FLY_OUT_IN_MS = 1500
|
|
57
|
+
const FOCUS_FLY_SPLIT = 0.4
|
|
58
|
+
|
|
59
|
+
function poseOf(bounds: MapPose): MapPose {
|
|
60
|
+
return { cx: bounds.cx, cz: bounds.cz, width: bounds.width, depth: bounds.depth }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function lerp(a: number, b: number, t: number) {
|
|
64
|
+
return a + (b - a) * t
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function lerpPose(a: MapPose, b: MapPose, t: number): MapPose {
|
|
68
|
+
return {
|
|
69
|
+
cx: lerp(a.cx, b.cx, t),
|
|
70
|
+
cz: lerp(a.cz, b.cz, t),
|
|
71
|
+
width: lerp(a.width, b.width, t),
|
|
72
|
+
depth: lerp(a.depth, b.depth, t),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function easeInOutCubic(t: number) {
|
|
77
|
+
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function flightPose(flight: MapFlight, now: number) {
|
|
81
|
+
const t = Math.min(1, (now - flight.start) / flight.duration)
|
|
82
|
+
const pose =
|
|
83
|
+
flight.split <= 0
|
|
84
|
+
? lerpPose(flight.from, flight.to, easeInOutCubic(t))
|
|
85
|
+
: t < flight.split
|
|
86
|
+
? lerpPose(flight.from, flight.via, easeInOutCubic(t / flight.split))
|
|
87
|
+
: lerpPose(
|
|
88
|
+
flight.via,
|
|
89
|
+
flight.to,
|
|
90
|
+
easeInOutCubic((t - flight.split) / (1 - flight.split)),
|
|
91
|
+
)
|
|
92
|
+
return { t, pose }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function applyMapPose(
|
|
96
|
+
camera: THREE.OrthographicCamera,
|
|
97
|
+
pose: MapPose,
|
|
98
|
+
viewWidth: number,
|
|
99
|
+
viewHeight: number,
|
|
100
|
+
hudReserve: number,
|
|
101
|
+
controls: { target: THREE.Vector3; update: () => void } | null,
|
|
102
|
+
) {
|
|
103
|
+
const fitZoom = Math.min(
|
|
104
|
+
viewWidth / Math.max(pose.width + 36, 1),
|
|
105
|
+
viewHeight / Math.max(pose.depth + 36, 1),
|
|
106
|
+
)
|
|
107
|
+
const zoom = Math.max(fitZoom * 0.92, 0.08)
|
|
108
|
+
const cz = pose.cz + hudReserve / 2 / zoom
|
|
109
|
+
camera.up.set(0, 0, -1)
|
|
110
|
+
camera.position.set(pose.cx, 120, cz)
|
|
111
|
+
camera.lookAt(pose.cx, 0, cz)
|
|
112
|
+
camera.near = 1
|
|
113
|
+
camera.far = 2000
|
|
114
|
+
camera.zoom = zoom
|
|
115
|
+
camera.updateProjectionMatrix()
|
|
116
|
+
if (controls) {
|
|
117
|
+
controls.target.set(pose.cx, 0, cz)
|
|
118
|
+
controls.update()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
15
122
|
type MapViewProps = {
|
|
16
123
|
layout: WorldLayout
|
|
17
124
|
enabled: boolean
|
|
@@ -19,9 +126,20 @@ type MapViewProps = {
|
|
|
19
126
|
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
20
127
|
selectedFolder?: string | null
|
|
21
128
|
namingFolderPath?: string | null
|
|
129
|
+
namingFileId?: string | null
|
|
130
|
+
pointedFolderPaths?: string[]
|
|
131
|
+
pointedFolderColors?: Record<string, string[]>
|
|
132
|
+
fileLabels?: MapFileLabel[]
|
|
133
|
+
focusBounds?: MapFocusBounds | null
|
|
134
|
+
focusFlightKey?: string | number
|
|
135
|
+
hudReserve?: number
|
|
136
|
+
topReserve?: number
|
|
137
|
+
landEnabled?: boolean
|
|
138
|
+
dimmedFolderPaths?: string[]
|
|
22
139
|
onLand: (x: number, z: number) => void
|
|
23
140
|
onSelect: (fileId: string | null) => void
|
|
24
141
|
onSelectFolder: (folderPath: string | null) => void
|
|
142
|
+
pickingImport?: boolean
|
|
25
143
|
onBlueprintMenu?: (menu: MapBlueprintMenu) => void
|
|
26
144
|
}
|
|
27
145
|
|
|
@@ -32,21 +150,32 @@ export function MapView({
|
|
|
32
150
|
highlightedFolders,
|
|
33
151
|
selectedFolder = null,
|
|
34
152
|
namingFolderPath = null,
|
|
153
|
+
namingFileId = null,
|
|
154
|
+
pointedFolderPaths = [],
|
|
155
|
+
pointedFolderColors = {},
|
|
156
|
+
fileLabels = [],
|
|
157
|
+
focusBounds = null,
|
|
158
|
+
focusFlightKey = 0,
|
|
159
|
+
hudReserve = 88,
|
|
160
|
+
topReserve = 28,
|
|
161
|
+
landEnabled = true,
|
|
162
|
+
dimmedFolderPaths = [],
|
|
35
163
|
onLand,
|
|
36
164
|
onSelect,
|
|
37
165
|
onSelectFolder,
|
|
38
166
|
onBlueprintMenu,
|
|
167
|
+
pickingImport = false,
|
|
39
168
|
}: MapViewProps) {
|
|
40
169
|
const size = useThree((state) => state.size)
|
|
41
170
|
const camera = useThree((state) => state.camera)
|
|
42
171
|
const gl = useThree((state) => state.gl)
|
|
43
172
|
const invalidate = useThree((state) => state.invalidate)
|
|
44
173
|
const scene = useThree((state) => state.scene)
|
|
45
|
-
const
|
|
174
|
+
const world = useMemo(() => worldBounds(layout), [layout])
|
|
175
|
+
const focusing = Boolean(focusBounds)
|
|
176
|
+
const bounds = focusBounds ?? world
|
|
46
177
|
const drag = useRef({ x: 0, y: 0, moved: false, active: false })
|
|
47
178
|
const sized = size.width > 16 && size.height > 16
|
|
48
|
-
const hudReserve = 88
|
|
49
|
-
const topReserve = 28
|
|
50
179
|
const viewWidth = Math.max(size.width, 1)
|
|
51
180
|
const viewHeight = Math.max(size.height - hudReserve - topReserve, 1)
|
|
52
181
|
|
|
@@ -56,34 +185,115 @@ export function MapView({
|
|
|
56
185
|
viewHeight / Math.max(bounds.depth + 36, 1),
|
|
57
186
|
)
|
|
58
187
|
: 8
|
|
59
|
-
const zoom = Math.max(fitZoom * 0.92, 0.08)
|
|
60
|
-
// Ortho up is -Z, so +Z is down the screen. Shift the view so the map sits
|
|
61
|
-
// above the bottom HUD instead of centering under it.
|
|
62
|
-
const cz = bounds.cz + hudReserve / 2 / zoom
|
|
63
188
|
const controlsRef = useRef<{ target: THREE.Vector3; update: () => void }>(null)
|
|
189
|
+
const poseRef = useRef<MapPose>(poseOf(bounds))
|
|
190
|
+
const flightRef = useRef<MapFlight | null>(null)
|
|
191
|
+
const flightKeyRef = useRef<number | string | null>(null)
|
|
192
|
+
const focusingRef = useRef(false)
|
|
193
|
+
const [flying, setFlying] = useState(false)
|
|
194
|
+
|
|
195
|
+
const snapTo = (pose: MapPose) => {
|
|
196
|
+
if (!(camera instanceof THREE.OrthographicCamera)) return
|
|
197
|
+
flightRef.current = null
|
|
198
|
+
poseRef.current = pose
|
|
199
|
+
applyMapPose(
|
|
200
|
+
camera,
|
|
201
|
+
pose,
|
|
202
|
+
viewWidth,
|
|
203
|
+
viewHeight,
|
|
204
|
+
hudReserve,
|
|
205
|
+
controlsRef.current,
|
|
206
|
+
)
|
|
207
|
+
setFlying(false)
|
|
208
|
+
invalidate()
|
|
209
|
+
}
|
|
64
210
|
|
|
65
211
|
useLayoutEffect(() => {
|
|
66
212
|
if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
213
|
+
const target = poseOf(bounds)
|
|
214
|
+
const key = focusing ? focusFlightKey : 'map'
|
|
215
|
+
const wasFocusing = focusingRef.current
|
|
216
|
+
const prevKey = flightKeyRef.current
|
|
217
|
+
focusingRef.current = focusing
|
|
218
|
+
flightKeyRef.current = key
|
|
219
|
+
|
|
220
|
+
if (!sized) {
|
|
221
|
+
snapTo(target)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (focusing && prevKey !== null && prevKey !== key) {
|
|
226
|
+
flightRef.current = {
|
|
227
|
+
from: poseRef.current,
|
|
228
|
+
via: wasFocusing ? poseOf(world) : poseRef.current,
|
|
229
|
+
to: target,
|
|
230
|
+
start: performance.now(),
|
|
231
|
+
duration: wasFocusing ? FOCUS_FLY_OUT_IN_MS : FOCUS_FLY_IN_MS,
|
|
232
|
+
split: wasFocusing ? FOCUS_FLY_SPLIT : 0,
|
|
233
|
+
}
|
|
234
|
+
setFlying(true)
|
|
235
|
+
invalidate()
|
|
236
|
+
return
|
|
78
237
|
}
|
|
79
|
-
|
|
238
|
+
|
|
239
|
+
if (flightRef.current) return
|
|
240
|
+
|
|
241
|
+
snapTo(target)
|
|
242
|
+
}, [
|
|
243
|
+
bounds.cx,
|
|
244
|
+
bounds.cz,
|
|
245
|
+
bounds.depth,
|
|
246
|
+
bounds.width,
|
|
247
|
+
camera,
|
|
248
|
+
enabled,
|
|
249
|
+
focusFlightKey,
|
|
250
|
+
focusing,
|
|
251
|
+
hudReserve,
|
|
252
|
+
invalidate,
|
|
253
|
+
sized,
|
|
254
|
+
viewHeight,
|
|
255
|
+
viewWidth,
|
|
256
|
+
world.cx,
|
|
257
|
+
world.cz,
|
|
258
|
+
world.depth,
|
|
259
|
+
world.width,
|
|
260
|
+
])
|
|
261
|
+
|
|
262
|
+
useFrame(() => {
|
|
263
|
+
const flight = flightRef.current
|
|
264
|
+
if (!flight || !enabled || !(camera instanceof THREE.OrthographicCamera)) return
|
|
265
|
+
const { t, pose } = flightPose(flight, performance.now())
|
|
266
|
+
poseRef.current = pose
|
|
267
|
+
applyMapPose(
|
|
268
|
+
camera,
|
|
269
|
+
pose,
|
|
270
|
+
viewWidth,
|
|
271
|
+
viewHeight,
|
|
272
|
+
hudReserve,
|
|
273
|
+
controlsRef.current,
|
|
274
|
+
)
|
|
275
|
+
if (t < 1) return
|
|
276
|
+
flightRef.current = null
|
|
277
|
+
poseRef.current = flight.to
|
|
278
|
+
applyMapPose(
|
|
279
|
+
camera,
|
|
280
|
+
flight.to,
|
|
281
|
+
viewWidth,
|
|
282
|
+
viewHeight,
|
|
283
|
+
hudReserve,
|
|
284
|
+
controlsRef.current,
|
|
285
|
+
)
|
|
286
|
+
setFlying(false)
|
|
287
|
+
})
|
|
80
288
|
|
|
81
289
|
useEffect(() => {
|
|
82
290
|
if (!enabled) return
|
|
83
291
|
const element = gl.domElement
|
|
84
|
-
|
|
292
|
+
const restCursor = pickingImport ? 'crosshair' : 'grab'
|
|
293
|
+
element.style.cursor = restCursor
|
|
85
294
|
|
|
86
|
-
const isWalkClick = (event: PointerEvent | MouseEvent) =>
|
|
295
|
+
const isWalkClick = (event: PointerEvent | MouseEvent) =>
|
|
296
|
+
landEnabled && event.altKey
|
|
87
297
|
|
|
88
298
|
const isWalkButton = (event: PointerEvent) => event.button === 0
|
|
89
299
|
|
|
@@ -144,12 +354,16 @@ export function MapView({
|
|
|
144
354
|
}
|
|
145
355
|
|
|
146
356
|
const onUp = (event: PointerEvent) => {
|
|
147
|
-
element.style.cursor =
|
|
357
|
+
element.style.cursor = restCursor
|
|
148
358
|
const startedOnCanvas = drag.current.active
|
|
149
359
|
drag.current.active = false
|
|
150
360
|
if (!startedOnCanvas || !isWalkButton(event) || drag.current.moved) return
|
|
151
361
|
|
|
152
|
-
if (
|
|
362
|
+
if (
|
|
363
|
+
!pickingImport &&
|
|
364
|
+
isWalkClick(event) &&
|
|
365
|
+
landAtPointer(event.clientX, event.clientY, true)
|
|
366
|
+
) {
|
|
153
367
|
return
|
|
154
368
|
}
|
|
155
369
|
|
|
@@ -161,6 +375,8 @@ export function MapView({
|
|
|
161
375
|
return
|
|
162
376
|
}
|
|
163
377
|
|
|
378
|
+
if (pickingImport) return
|
|
379
|
+
|
|
164
380
|
const hit = new THREE.Vector3()
|
|
165
381
|
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
166
382
|
if (pick.raycaster.ray.intersectPlane(plane, hit)) {
|
|
@@ -241,12 +457,6 @@ export function MapView({
|
|
|
241
457
|
}
|
|
242
458
|
|
|
243
459
|
const onWheel = (event: WheelEvent) => {
|
|
244
|
-
const overlay = document.elementFromPoint(event.clientX, event.clientY)
|
|
245
|
-
if (overlay?.closest('.hud-thumbnail')) {
|
|
246
|
-
event.preventDefault()
|
|
247
|
-
event.stopImmediatePropagation()
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
460
|
event.preventDefault()
|
|
251
461
|
event.stopImmediatePropagation()
|
|
252
462
|
if (event.deltaY < 0) zoomAtCursor(event.clientX, event.clientY, zoomScale)
|
|
@@ -273,10 +483,12 @@ export function MapView({
|
|
|
273
483
|
gl.domElement,
|
|
274
484
|
invalidate,
|
|
275
485
|
layout,
|
|
486
|
+
landEnabled,
|
|
276
487
|
onLand,
|
|
277
488
|
onBlueprintMenu,
|
|
278
489
|
onSelect,
|
|
279
490
|
onSelectFolder,
|
|
491
|
+
pickingImport,
|
|
280
492
|
scene,
|
|
281
493
|
selectedFolder,
|
|
282
494
|
])
|
|
@@ -289,6 +501,7 @@ export function MapView({
|
|
|
289
501
|
{enabled && sized && (
|
|
290
502
|
<MapControls
|
|
291
503
|
ref={controlsRef}
|
|
504
|
+
enabled={!flying}
|
|
292
505
|
enableRotate={false}
|
|
293
506
|
enableDamping
|
|
294
507
|
dampingFactor={0.12}
|
|
@@ -305,8 +518,14 @@ export function MapView({
|
|
|
305
518
|
highlightedFolders={highlightedFolders}
|
|
306
519
|
selectedFolder={selectedFolder}
|
|
307
520
|
namingFolderPath={namingFolderPath}
|
|
521
|
+
pointedFolderPaths={pointedFolderPaths}
|
|
522
|
+
pointedFolderColors={pointedFolderColors}
|
|
523
|
+
dimmedFolderPaths={dimmedFolderPaths}
|
|
308
524
|
/>
|
|
309
525
|
)}
|
|
526
|
+
{enabled && (
|
|
527
|
+
<MapFileLabels files={fileLabels} namingFileId={namingFileId} />
|
|
528
|
+
)}
|
|
310
529
|
{enabled && marker && <LandMarker marker={marker} />}
|
|
311
530
|
</>
|
|
312
531
|
)
|
|
@@ -315,6 +534,15 @@ export function MapView({
|
|
|
315
534
|
const PROJECT = new THREE.Vector3()
|
|
316
535
|
const MIN_FOLDER_LABEL_PX = 28
|
|
317
536
|
const FOLDER_ENTRANCE_Z = 1.35
|
|
537
|
+
const MIN_FILE_LABEL_PX = 16
|
|
538
|
+
const FILE_LABEL_HEIGHT = 15
|
|
539
|
+
const FILE_LABEL_GAP = 4
|
|
540
|
+
const MAX_FILE_LABELS = 28
|
|
541
|
+
const FILE_LABEL_CHAR_W = 7.2
|
|
542
|
+
const FILE_LABEL_PAD_X = 12
|
|
543
|
+
const FILE_LABEL_MIN_W = 108
|
|
544
|
+
const FILE_LABEL_MAX_W = 220
|
|
545
|
+
const FILE_LABEL_BLOCK_SCALE = 5.2
|
|
318
546
|
|
|
319
547
|
function projectToScreen(
|
|
320
548
|
x: number,
|
|
@@ -336,6 +564,8 @@ function folderLabelClass(
|
|
|
336
564
|
folder: PlacedFolder,
|
|
337
565
|
highlightedFolders: Partial<Record<string, ChangeKind>> | undefined,
|
|
338
566
|
selectedFolder: string | null,
|
|
567
|
+
pointed: boolean,
|
|
568
|
+
dimmed: boolean,
|
|
339
569
|
) {
|
|
340
570
|
return [
|
|
341
571
|
'map-folder-label',
|
|
@@ -346,6 +576,8 @@ function folderLabelClass(
|
|
|
346
576
|
: selectedFolder === folder.path
|
|
347
577
|
? 'map-folder-label-selected'
|
|
348
578
|
: '',
|
|
579
|
+
pointed ? 'map-folder-label-pointed' : '',
|
|
580
|
+
dimmed ? 'map-folder-label-dimmed' : '',
|
|
349
581
|
]
|
|
350
582
|
.filter(Boolean)
|
|
351
583
|
.join(' ')
|
|
@@ -356,17 +588,33 @@ function MapFolderLabels({
|
|
|
356
588
|
highlightedFolders,
|
|
357
589
|
selectedFolder,
|
|
358
590
|
namingFolderPath,
|
|
591
|
+
pointedFolderPaths,
|
|
592
|
+
pointedFolderColors,
|
|
593
|
+
dimmedFolderPaths,
|
|
359
594
|
}: {
|
|
360
595
|
folders: Record<string, PlacedFolder>
|
|
361
596
|
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
362
597
|
selectedFolder: string | null
|
|
363
598
|
namingFolderPath: string | null
|
|
599
|
+
pointedFolderPaths: string[]
|
|
600
|
+
pointedFolderColors: Record<string, string[]>
|
|
601
|
+
dimmedFolderPaths: string[]
|
|
364
602
|
}) {
|
|
365
603
|
const camera = useThree((state) => state.camera)
|
|
366
604
|
const gl = useThree((state) => state.gl)
|
|
367
605
|
const size = useThree((state) => state.size)
|
|
368
606
|
const layerRef = useRef<HTMLDivElement | null>(null)
|
|
369
607
|
const items = useMemo(() => Object.values(folders), [folders])
|
|
608
|
+
const pointedKey = pointedFolderPaths.join('\0')
|
|
609
|
+
const pointedFolders = useMemo(
|
|
610
|
+
() => new Set(pointedKey ? pointedKey.split('\0') : []),
|
|
611
|
+
[pointedKey],
|
|
612
|
+
)
|
|
613
|
+
const dimmedKey = dimmedFolderPaths.join('\0')
|
|
614
|
+
const dimmedFolders = useMemo(
|
|
615
|
+
() => new Set(dimmedKey ? dimmedKey.split('\0') : []),
|
|
616
|
+
[dimmedKey],
|
|
617
|
+
)
|
|
370
618
|
|
|
371
619
|
useLayoutEffect(() => {
|
|
372
620
|
const parent = gl.domElement.parentElement
|
|
@@ -376,12 +624,32 @@ function MapFolderLabels({
|
|
|
376
624
|
layer.style.cssText =
|
|
377
625
|
'position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:80;background:transparent;'
|
|
378
626
|
for (const folder of items) {
|
|
627
|
+
const pointed = pointedFolders.has(folder.path)
|
|
628
|
+
const dimmed = dimmedFolders.has(folder.path)
|
|
379
629
|
const el = document.createElement('div')
|
|
380
|
-
el.className = folderLabelClass(
|
|
630
|
+
el.className = folderLabelClass(
|
|
631
|
+
folder,
|
|
632
|
+
highlightedFolders,
|
|
633
|
+
selectedFolder,
|
|
634
|
+
pointed,
|
|
635
|
+
dimmed,
|
|
636
|
+
)
|
|
381
637
|
el.style.position = 'absolute'
|
|
382
638
|
el.style.top = '0'
|
|
383
639
|
el.style.left = '0'
|
|
384
640
|
el.style.visibility = 'hidden'
|
|
641
|
+
if (pointed) {
|
|
642
|
+
const colors = pointedFolderColors[folder.path] ?? []
|
|
643
|
+
const hex = colors[colors.length - 1]
|
|
644
|
+
if (hex) el.style.setProperty('--session-color', hex)
|
|
645
|
+
for (const color of colors.length > 0 ? colors : ['#9ad8ff']) {
|
|
646
|
+
const eye = document.createElement('span')
|
|
647
|
+
eye.className = 'map-folder-eye'
|
|
648
|
+
eye.style.color = color
|
|
649
|
+
eye.innerHTML = eyeIconMarkup(13)
|
|
650
|
+
el.appendChild(eye)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
385
653
|
const name = document.createElement('span')
|
|
386
654
|
name.className = 'map-folder-name'
|
|
387
655
|
name.textContent = folderKindLabel(
|
|
@@ -389,6 +657,11 @@ function MapFolderLabels({
|
|
|
389
657
|
highlightedFolders?.[folder.path] ?? null,
|
|
390
658
|
folder.added ?? false,
|
|
391
659
|
)
|
|
660
|
+
if (folder.added && folder.colorHex) {
|
|
661
|
+
const tint = blueprintPalette(folder.colorHex)
|
|
662
|
+
el.style.setProperty('--blueprint-color', tint.color)
|
|
663
|
+
el.style.setProperty('--blueprint-label', tint.label)
|
|
664
|
+
}
|
|
392
665
|
el.appendChild(name)
|
|
393
666
|
layer.appendChild(el)
|
|
394
667
|
}
|
|
@@ -398,7 +671,15 @@ function MapFolderLabels({
|
|
|
398
671
|
layer.remove()
|
|
399
672
|
layerRef.current = null
|
|
400
673
|
}
|
|
401
|
-
}, [
|
|
674
|
+
}, [
|
|
675
|
+
dimmedFolders,
|
|
676
|
+
gl,
|
|
677
|
+
highlightedFolders,
|
|
678
|
+
items,
|
|
679
|
+
pointedFolderColors,
|
|
680
|
+
pointedFolders,
|
|
681
|
+
selectedFolder,
|
|
682
|
+
])
|
|
402
683
|
|
|
403
684
|
useFrame(() => {
|
|
404
685
|
const layer = layerRef.current
|
|
@@ -420,9 +701,12 @@ function MapFolderLabels({
|
|
|
420
701
|
const x = screen.x
|
|
421
702
|
const y = screen.y
|
|
422
703
|
const span = Math.max(folder.width, folder.depth) * zoom
|
|
704
|
+
const dimmed = dimmedFolders.has(folder.path)
|
|
423
705
|
const force =
|
|
424
706
|
selectedFolder === folder.path ||
|
|
425
|
-
|
|
707
|
+
pointedFolders.has(folder.path) ||
|
|
708
|
+
Boolean(highlightedFolders?.[folder.path] || folder.added) ||
|
|
709
|
+
(dimmedFolders.size > 0 && !dimmed)
|
|
426
710
|
const onScreen =
|
|
427
711
|
!screen.behind &&
|
|
428
712
|
x > -120 &&
|
|
@@ -461,6 +745,195 @@ function folderKindLabel(
|
|
|
461
745
|
return name
|
|
462
746
|
}
|
|
463
747
|
|
|
748
|
+
function fileLabelClass(file: MapFileLabel) {
|
|
749
|
+
return [
|
|
750
|
+
'map-file-label',
|
|
751
|
+
file.selected ? 'map-file-label-selected' : '',
|
|
752
|
+
file.pointed ? 'map-file-label-pointed' : '',
|
|
753
|
+
file.focused ? 'map-file-label-focused' : '',
|
|
754
|
+
file.dimmed ? 'map-file-label-dimmed' : '',
|
|
755
|
+
]
|
|
756
|
+
.filter(Boolean)
|
|
757
|
+
.join(' ')
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function labelsOverlap(
|
|
761
|
+
left: number,
|
|
762
|
+
top: number,
|
|
763
|
+
right: number,
|
|
764
|
+
bottom: number,
|
|
765
|
+
placed: { l: number; t: number; r: number; b: number }[],
|
|
766
|
+
) {
|
|
767
|
+
for (let i = 0; i < placed.length; i += 1) {
|
|
768
|
+
const box = placed[i]
|
|
769
|
+
if (left < box.r && right > box.l && top < box.b && bottom > box.t) return true
|
|
770
|
+
}
|
|
771
|
+
return false
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function MapFileLabels({
|
|
775
|
+
files,
|
|
776
|
+
namingFileId,
|
|
777
|
+
}: {
|
|
778
|
+
files: MapFileLabel[]
|
|
779
|
+
namingFileId: string | null
|
|
780
|
+
}) {
|
|
781
|
+
const camera = useThree((state) => state.camera)
|
|
782
|
+
const gl = useThree((state) => state.gl)
|
|
783
|
+
const size = useThree((state) => state.size)
|
|
784
|
+
const layerRef = useRef<HTMLDivElement | null>(null)
|
|
785
|
+
const filesRef = useRef(files)
|
|
786
|
+
const namingRef = useRef(namingFileId)
|
|
787
|
+
filesRef.current = files
|
|
788
|
+
namingRef.current = namingFileId
|
|
789
|
+
|
|
790
|
+
useLayoutEffect(() => {
|
|
791
|
+
const parent = gl.domElement.parentElement
|
|
792
|
+
if (!parent) return
|
|
793
|
+
const layer = document.createElement('div')
|
|
794
|
+
layer.className = 'map-file-label-layer'
|
|
795
|
+
layer.style.cssText =
|
|
796
|
+
'position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:70;background:transparent;'
|
|
797
|
+
parent.appendChild(layer)
|
|
798
|
+
layerRef.current = layer
|
|
799
|
+
return () => {
|
|
800
|
+
layer.remove()
|
|
801
|
+
layerRef.current = null
|
|
802
|
+
}
|
|
803
|
+
}, [gl])
|
|
804
|
+
|
|
805
|
+
useFrame(() => {
|
|
806
|
+
const layer = layerRef.current
|
|
807
|
+
const items = filesRef.current
|
|
808
|
+
if (!layer) return
|
|
809
|
+
const zoom = 'zoom' in camera ? Number(camera.zoom) : 1
|
|
810
|
+
const cx = size.width * 0.5
|
|
811
|
+
const cy = size.height * 0.5
|
|
812
|
+
const candidates: {
|
|
813
|
+
file: MapFileLabel
|
|
814
|
+
x: number
|
|
815
|
+
y: number
|
|
816
|
+
w: number
|
|
817
|
+
outer: 1 | -1 | 0
|
|
818
|
+
rank: number
|
|
819
|
+
dist: number
|
|
820
|
+
}[] = []
|
|
821
|
+
|
|
822
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
823
|
+
const file = items[i]
|
|
824
|
+
if (file.id === namingRef.current) continue
|
|
825
|
+
const block = Math.min(file.width, file.depth) * zoom
|
|
826
|
+
const force = file.selected || file.pointed || file.focused
|
|
827
|
+
if (!force && block < MIN_FILE_LABEL_PX) continue
|
|
828
|
+
const screen = projectToScreen(
|
|
829
|
+
file.x,
|
|
830
|
+
0,
|
|
831
|
+
file.z,
|
|
832
|
+
camera,
|
|
833
|
+
size.width,
|
|
834
|
+
size.height,
|
|
835
|
+
)
|
|
836
|
+
if (
|
|
837
|
+
screen.behind ||
|
|
838
|
+
screen.x < -80 ||
|
|
839
|
+
screen.x > size.width + 80 ||
|
|
840
|
+
screen.y < -40 ||
|
|
841
|
+
screen.y > size.height + 40
|
|
842
|
+
) {
|
|
843
|
+
continue
|
|
844
|
+
}
|
|
845
|
+
const maxWidth = Math.max(
|
|
846
|
+
FILE_LABEL_MIN_W,
|
|
847
|
+
Math.min(FILE_LABEL_MAX_W, block * FILE_LABEL_BLOCK_SCALE),
|
|
848
|
+
)
|
|
849
|
+
const width = Math.min(
|
|
850
|
+
maxWidth,
|
|
851
|
+
file.name.length * FILE_LABEL_CHAR_W + FILE_LABEL_PAD_X,
|
|
852
|
+
)
|
|
853
|
+
const onBlock = block >= 48 && width <= block * 0.9
|
|
854
|
+
const edgeX = onBlock
|
|
855
|
+
? screen.x
|
|
856
|
+
: screen.x + file.outer * ((file.width * zoom) / 2 + FILE_LABEL_GAP)
|
|
857
|
+
candidates.push({
|
|
858
|
+
file,
|
|
859
|
+
x: edgeX,
|
|
860
|
+
y: screen.y,
|
|
861
|
+
w: width,
|
|
862
|
+
outer: onBlock ? 0 : file.outer,
|
|
863
|
+
rank: file.selected ? 0 : file.pointed || file.focused ? 1 : 2,
|
|
864
|
+
dist: Math.hypot(screen.x - cx, screen.y - cy),
|
|
865
|
+
})
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
candidates.sort((a, b) => a.rank - b.rank || a.dist - b.dist)
|
|
869
|
+
|
|
870
|
+
const placed: { l: number; t: number; r: number; b: number }[] = []
|
|
871
|
+
const visible: typeof candidates = []
|
|
872
|
+
for (let i = 0; i < candidates.length && visible.length < MAX_FILE_LABELS; i += 1) {
|
|
873
|
+
const next = candidates[i]
|
|
874
|
+
const left =
|
|
875
|
+
next.outer === 1 ? next.x : next.outer === -1 ? next.x - next.w : next.x - next.w / 2
|
|
876
|
+
const right =
|
|
877
|
+
next.outer === 1 ? next.x + next.w : next.outer === -1 ? next.x : next.x + next.w / 2
|
|
878
|
+
const top = next.y - FILE_LABEL_HEIGHT / 2
|
|
879
|
+
const bottom = next.y + FILE_LABEL_HEIGHT / 2
|
|
880
|
+
if (labelsOverlap(left, top - 2, right, bottom + 2, placed)) continue
|
|
881
|
+
placed.push({ l: left, t: top, r: right, b: bottom })
|
|
882
|
+
visible.push(next)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
while (layer.children.length < visible.length) {
|
|
886
|
+
const el = document.createElement('div')
|
|
887
|
+
el.className = 'map-file-label'
|
|
888
|
+
el.style.position = 'absolute'
|
|
889
|
+
el.style.top = '0'
|
|
890
|
+
el.style.left = '0'
|
|
891
|
+
el.style.visibility = 'hidden'
|
|
892
|
+
const name = document.createElement('span')
|
|
893
|
+
name.className = 'map-file-name'
|
|
894
|
+
el.appendChild(name)
|
|
895
|
+
layer.appendChild(el)
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const nodes = layer.children
|
|
899
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
900
|
+
const el = nodes[i] as HTMLElement
|
|
901
|
+
const next = visible[i]
|
|
902
|
+
if (!next) {
|
|
903
|
+
if (el.style.visibility !== 'hidden') el.style.visibility = 'hidden'
|
|
904
|
+
continue
|
|
905
|
+
}
|
|
906
|
+
const name = el.firstElementChild as HTMLElement | null
|
|
907
|
+
const className = fileLabelClass(next.file)
|
|
908
|
+
if (el.className !== className) el.className = className
|
|
909
|
+
if (el.dataset.id !== next.file.id) {
|
|
910
|
+
el.dataset.id = next.file.id
|
|
911
|
+
}
|
|
912
|
+
if (next.file.pointed && next.file.pointedColor) {
|
|
913
|
+
el.style.setProperty('--session-color', next.file.pointedColor)
|
|
914
|
+
} else {
|
|
915
|
+
el.style.removeProperty('--session-color')
|
|
916
|
+
}
|
|
917
|
+
if (name && name.textContent !== next.file.name) name.textContent = next.file.name
|
|
918
|
+
el.style.maxWidth = `${Math.round(next.w)}px`
|
|
919
|
+
el.style.textAlign =
|
|
920
|
+
next.outer === 1 ? 'left' : next.outer === -1 ? 'right' : 'center'
|
|
921
|
+
const tx = Math.round(next.x)
|
|
922
|
+
const ty = Math.round(next.y)
|
|
923
|
+
const pos = `${tx},${ty},${next.outer}`
|
|
924
|
+
const origin =
|
|
925
|
+
next.outer === 1 ? '0, -50%' : next.outer === -1 ? '-100%, -50%' : '-50%, -50%'
|
|
926
|
+
if (el.dataset.pos !== pos) {
|
|
927
|
+
el.dataset.pos = pos
|
|
928
|
+
el.style.transform = `translate3d(${tx}px, ${ty}px, 0) translate(${origin})`
|
|
929
|
+
}
|
|
930
|
+
if (el.style.visibility !== 'visible') el.style.visibility = 'visible'
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
return null
|
|
935
|
+
}
|
|
936
|
+
|
|
464
937
|
function LandMarker({ marker }: { marker: [number, number] }) {
|
|
465
938
|
const camera = useThree((state) => state.camera)
|
|
466
939
|
const ring = useRef<THREE.Group>(null)
|