@jkwd/inbase 0.1.5 → 0.1.6
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 +4 -1
- package/apps/explorer/scripts/patch-lib.d.ts +13 -0
- package/apps/explorer/scripts/patch-lib.mjs +162 -25
- package/apps/explorer/scripts/session-store.d.ts +18 -1
- package/apps/explorer/scripts/session-store.mjs +102 -26
- package/apps/explorer/src/App.tsx +153 -3
- package/apps/explorer/src/agentIntent.ts +9 -0
- package/apps/explorer/src/index.css +103 -11
- package/apps/explorer/src/layout.ts +65 -1
- package/apps/explorer/src/scene/Bridge.tsx +23 -5
- package/apps/explorer/src/scene/FileBlock.tsx +13 -9
- package/apps/explorer/src/scene/FolderArea.tsx +18 -12
- package/apps/explorer/src/scene/MapView.tsx +69 -12
- package/apps/explorer/src/scene/Player.tsx +9 -1
- package/apps/explorer/src/scene/SelectionController.tsx +36 -13
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +477 -33
- package/apps/explorer/src/scene/World.tsx +27 -10
- package/apps/explorer/src/theme.ts +10 -3
- package/apps/explorer/src/types.ts +5 -0
- package/apps/explorer/src/ui/HUD.tsx +294 -37
- package/apps/explorer/vite.config.ts +11 -1
- package/bin/session.mjs +18 -6
- package/package.json +1 -1
- package/skill/inbase/SKILL.md +13 -8
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Suspense } from 'react'
|
|
2
2
|
import { Html, Text } from '@react-three/drei'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CHANGE_HIGHLIGHT,
|
|
5
|
+
CONFIG,
|
|
6
|
+
folderAisleColor,
|
|
7
|
+
folderFloorColor,
|
|
8
|
+
MAP_SELECTION,
|
|
9
|
+
type ChangeKind,
|
|
10
|
+
} from '../theme'
|
|
4
11
|
import type { PlacedFolder } from '../types'
|
|
5
12
|
import { MapSelectBorder } from './MapSelectBorder'
|
|
6
13
|
|
|
@@ -30,6 +37,11 @@ export function FolderArea({
|
|
|
30
37
|
: addedOnly
|
|
31
38
|
? '#16323f'
|
|
32
39
|
: folderFloorColor(folder.path)
|
|
40
|
+
const aisle = highlight
|
|
41
|
+
? highlight.aisle
|
|
42
|
+
: addedOnly
|
|
43
|
+
? '#23485a'
|
|
44
|
+
: folderAisleColor(folder.path)
|
|
33
45
|
const label =
|
|
34
46
|
highlightKind === 'remove'
|
|
35
47
|
? `- ${folder.name}`
|
|
@@ -62,22 +74,16 @@ export function FolderArea({
|
|
|
62
74
|
/>
|
|
63
75
|
)}
|
|
64
76
|
<mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
65
|
-
<planeGeometry args={[
|
|
66
|
-
<meshBasicMaterial
|
|
67
|
-
color={highlight ? highlight.aisle : addedOnly ? '#23485a' : '#2a3340'}
|
|
68
|
-
/>
|
|
77
|
+
<planeGeometry args={[CONFIG.bridgeWidth, folder.depth]} />
|
|
78
|
+
<meshBasicMaterial color={aisle} />
|
|
69
79
|
</mesh>
|
|
70
80
|
{folder.width > 28 && (
|
|
71
81
|
<mesh
|
|
72
|
-
position={[0, 0.02, folder.depth / 2 -
|
|
82
|
+
position={[0, 0.02, folder.depth / 2 - CONFIG.bridgeWidth / 2]}
|
|
73
83
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
74
84
|
>
|
|
75
|
-
<planeGeometry args={[folder.width
|
|
76
|
-
<meshBasicMaterial
|
|
77
|
-
color={
|
|
78
|
-
highlight ? highlight.aisle : addedOnly ? '#23485a' : '#2a3340'
|
|
79
|
-
}
|
|
80
|
-
/>
|
|
85
|
+
<planeGeometry args={[folder.width, CONFIG.bridgeWidth]} />
|
|
86
|
+
<meshBasicMaterial color={aisle} />
|
|
81
87
|
</mesh>
|
|
82
88
|
)}
|
|
83
89
|
<Suspense fallback={null}>
|
|
@@ -30,7 +30,7 @@ export function MapView({
|
|
|
30
30
|
onTravelTo,
|
|
31
31
|
}: MapViewProps) {
|
|
32
32
|
const size = useThree((state) => state.size)
|
|
33
|
-
const { camera, gl, scene } = useThree()
|
|
33
|
+
const { camera, gl, invalidate, scene } = useThree()
|
|
34
34
|
const bounds = useMemo(() => worldBounds(layout), [layout])
|
|
35
35
|
const drag = useRef({ x: 0, y: 0, moved: false, active: false })
|
|
36
36
|
const sized = size.width > 16 && size.height > 16
|
|
@@ -49,7 +49,7 @@ export function MapView({
|
|
|
49
49
|
// Ortho up is -Z, so +Z is down the screen. Shift the view so the map sits
|
|
50
50
|
// above the bottom HUD instead of centering under it.
|
|
51
51
|
const cz = bounds.cz + hudReserve / 2 / zoom
|
|
52
|
-
const
|
|
52
|
+
const controlsRef = useRef<{ target: THREE.Vector3; update: () => void }>(null)
|
|
53
53
|
|
|
54
54
|
useLayoutEffect(() => {
|
|
55
55
|
if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
|
|
@@ -60,7 +60,12 @@ export function MapView({
|
|
|
60
60
|
camera.far = 2000
|
|
61
61
|
camera.zoom = zoom
|
|
62
62
|
camera.updateProjectionMatrix()
|
|
63
|
-
|
|
63
|
+
const controls = controlsRef.current
|
|
64
|
+
if (controls) {
|
|
65
|
+
controls.target.set(bounds.cx, 0, cz)
|
|
66
|
+
controls.update()
|
|
67
|
+
}
|
|
68
|
+
}, [bounds.cx, camera, cz, enabled, sized, zoom])
|
|
64
69
|
|
|
65
70
|
useEffect(() => {
|
|
66
71
|
if (!enabled) return
|
|
@@ -170,21 +175,79 @@ export function MapView({
|
|
|
170
175
|
if (event.ctrlKey) event.preventDefault()
|
|
171
176
|
}
|
|
172
177
|
|
|
178
|
+
const ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
179
|
+
const ndc = new THREE.Vector2()
|
|
180
|
+
const before = new THREE.Vector3()
|
|
181
|
+
const after = new THREE.Vector3()
|
|
182
|
+
const raycaster = new THREE.Raycaster()
|
|
183
|
+
const minZoom = Math.max(fitZoom * 0.35, 0.05)
|
|
184
|
+
const maxZoom = Math.max(fitZoom * 10, 20)
|
|
185
|
+
const zoomScale = Math.pow(0.95, 1.15)
|
|
186
|
+
|
|
187
|
+
const worldUnderCursor = (clientX: number, clientY: number, target: THREE.Vector3) => {
|
|
188
|
+
const rect = element.getBoundingClientRect()
|
|
189
|
+
if (rect.width < 2 || rect.height < 2) return false
|
|
190
|
+
ndc.set(
|
|
191
|
+
((clientX - rect.left) / rect.width) * 2 - 1,
|
|
192
|
+
-((clientY - rect.top) / rect.height) * 2 + 1,
|
|
193
|
+
)
|
|
194
|
+
raycaster.setFromCamera(ndc, camera)
|
|
195
|
+
return Boolean(raycaster.ray.intersectPlane(ground, target))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const zoomAtCursor = (clientX: number, clientY: number, dollyScale: number) => {
|
|
199
|
+
if (!(camera instanceof THREE.OrthographicCamera)) return
|
|
200
|
+
if (!worldUnderCursor(clientX, clientY, before)) return
|
|
201
|
+
const nextZoom = Math.min(maxZoom, Math.max(minZoom, camera.zoom / dollyScale))
|
|
202
|
+
if (nextZoom === camera.zoom) return
|
|
203
|
+
camera.zoom = nextZoom
|
|
204
|
+
camera.updateProjectionMatrix()
|
|
205
|
+
if (!worldUnderCursor(clientX, clientY, after)) return
|
|
206
|
+
const dx = before.x - after.x
|
|
207
|
+
const dz = before.z - after.z
|
|
208
|
+
camera.position.x += dx
|
|
209
|
+
camera.position.z += dz
|
|
210
|
+
const controls = controlsRef.current
|
|
211
|
+
if (controls) {
|
|
212
|
+
controls.target.x += dx
|
|
213
|
+
controls.target.z += dz
|
|
214
|
+
controls.update()
|
|
215
|
+
}
|
|
216
|
+
invalidate()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const onWheel = (event: WheelEvent) => {
|
|
220
|
+
const overlay = document.elementFromPoint(event.clientX, event.clientY)
|
|
221
|
+
if (overlay?.closest('.hud-thumbnail')) {
|
|
222
|
+
event.preventDefault()
|
|
223
|
+
event.stopImmediatePropagation()
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
event.preventDefault()
|
|
227
|
+
event.stopImmediatePropagation()
|
|
228
|
+
if (event.deltaY < 0) zoomAtCursor(event.clientX, event.clientY, zoomScale)
|
|
229
|
+
else if (event.deltaY > 0) zoomAtCursor(event.clientX, event.clientY, 1 / zoomScale)
|
|
230
|
+
}
|
|
231
|
+
|
|
173
232
|
element.addEventListener('pointerdown', onDown)
|
|
174
233
|
window.addEventListener('pointermove', onMove)
|
|
175
234
|
window.addEventListener('pointerup', onUp)
|
|
176
235
|
element.addEventListener('contextmenu', onContextMenu)
|
|
236
|
+
element.addEventListener('wheel', onWheel, { capture: true, passive: false })
|
|
177
237
|
return () => {
|
|
178
238
|
element.style.cursor = ''
|
|
179
239
|
element.removeEventListener('pointerdown', onDown)
|
|
180
240
|
window.removeEventListener('pointermove', onMove)
|
|
181
241
|
window.removeEventListener('pointerup', onUp)
|
|
182
242
|
element.removeEventListener('contextmenu', onContextMenu)
|
|
243
|
+
element.removeEventListener('wheel', onWheel, { capture: true })
|
|
183
244
|
}
|
|
184
245
|
}, [
|
|
185
246
|
camera,
|
|
186
247
|
enabled,
|
|
248
|
+
fitZoom,
|
|
187
249
|
gl.domElement,
|
|
250
|
+
invalidate,
|
|
188
251
|
layout,
|
|
189
252
|
onLand,
|
|
190
253
|
onSelect,
|
|
@@ -196,25 +259,19 @@ export function MapView({
|
|
|
196
259
|
return (
|
|
197
260
|
<>
|
|
198
261
|
{enabled && (
|
|
199
|
-
<OrthographicCamera
|
|
200
|
-
makeDefault
|
|
201
|
-
position={[bounds.cx, 120, cz]}
|
|
202
|
-
zoom={zoom}
|
|
203
|
-
near={1}
|
|
204
|
-
far={2000}
|
|
205
|
-
up={[0, 0, -1]}
|
|
206
|
-
/>
|
|
262
|
+
<OrthographicCamera makeDefault near={1} far={2000} up={[0, 0, -1]} />
|
|
207
263
|
)}
|
|
208
264
|
{enabled && sized && (
|
|
209
265
|
<MapControls
|
|
266
|
+
ref={controlsRef}
|
|
210
267
|
enableRotate={false}
|
|
211
268
|
enableDamping
|
|
212
269
|
dampingFactor={0.12}
|
|
213
270
|
screenSpacePanning
|
|
271
|
+
zoomToCursor
|
|
214
272
|
zoomSpeed={1.15}
|
|
215
273
|
minZoom={Math.max(fitZoom * 0.35, 0.05)}
|
|
216
274
|
maxZoom={Math.max(fitZoom * 10, 20)}
|
|
217
|
-
target={target}
|
|
218
275
|
/>
|
|
219
276
|
)}
|
|
220
277
|
{enabled &&
|
|
@@ -226,7 +226,15 @@ export function Player({
|
|
|
226
226
|
makeDefault
|
|
227
227
|
selector=".stage canvas"
|
|
228
228
|
enabled={steering && lockEnabled}
|
|
229
|
-
onLock={() =>
|
|
229
|
+
onLock={() => {
|
|
230
|
+
if (!lockEnabled) {
|
|
231
|
+
controlsRef.current?.unlock()
|
|
232
|
+
document.exitPointerLock()
|
|
233
|
+
onLockedChange(false)
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
onLockedChange(true)
|
|
237
|
+
}}
|
|
230
238
|
onUnlock={() => onLockedChange(false)}
|
|
231
239
|
/>
|
|
232
240
|
</>
|
|
@@ -8,6 +8,8 @@ type SelectionControllerProps = {
|
|
|
8
8
|
locked: boolean
|
|
9
9
|
files: Record<string, PlacedFile>
|
|
10
10
|
onSelect: (fileId: string | null) => void
|
|
11
|
+
onInspect?: (fileId: string) => void
|
|
12
|
+
onAimFile?: (fileId: string | null) => void
|
|
11
13
|
onAimRelation?: (aim: AimedRelation | null) => void
|
|
12
14
|
onTravelTo: (fromId: string, toId: string) => void
|
|
13
15
|
}
|
|
@@ -36,35 +38,50 @@ function aimKey(aim: AimedRelation | null) {
|
|
|
36
38
|
return aim ? `${aim.from}->${aim.to}:${aim.flyTo}` : ''
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function aimedFileId(hits: THREE.Intersection[]): string | null {
|
|
42
|
+
const file = hits.find((item) => item.object.userData.fileId)
|
|
43
|
+
return file ? (file.object.userData.fileId as string) : null
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
export function SelectionController({
|
|
40
47
|
locked,
|
|
41
48
|
files,
|
|
42
49
|
onSelect,
|
|
50
|
+
onInspect,
|
|
51
|
+
onAimFile,
|
|
43
52
|
onAimRelation,
|
|
44
53
|
onTravelTo,
|
|
45
54
|
}: SelectionControllerProps) {
|
|
46
55
|
const { camera, scene } = useThree()
|
|
47
56
|
const lastAim = useRef('')
|
|
57
|
+
const lastAimedFile = useRef<string | null>(null)
|
|
48
58
|
|
|
49
59
|
useFrame(() => {
|
|
50
|
-
if (!onAimRelation) return
|
|
60
|
+
if (!onAimRelation && !onAimFile) return
|
|
51
61
|
if (!locked) {
|
|
52
62
|
if (lastAim.current) {
|
|
53
63
|
lastAim.current = ''
|
|
54
|
-
onAimRelation(null)
|
|
64
|
+
onAimRelation?.(null)
|
|
65
|
+
}
|
|
66
|
+
if (lastAimedFile.current) {
|
|
67
|
+
lastAimedFile.current = null
|
|
68
|
+
onAimFile?.(null)
|
|
55
69
|
}
|
|
56
70
|
return
|
|
57
71
|
}
|
|
58
72
|
raycaster.setFromCamera(ndc, camera)
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
camera.position,
|
|
62
|
-
files,
|
|
63
|
-
)
|
|
73
|
+
const hits = raycaster.intersectObjects(scene.children, true)
|
|
74
|
+
const next = aimedRelation(hits, camera.position, files)
|
|
64
75
|
const key = aimKey(next)
|
|
65
|
-
if (key
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
if (onAimRelation && key !== lastAim.current) {
|
|
77
|
+
lastAim.current = key
|
|
78
|
+
onAimRelation(next)
|
|
79
|
+
}
|
|
80
|
+
const fileId = aimedFileId(hits)
|
|
81
|
+
if (onAimFile && fileId !== lastAimedFile.current) {
|
|
82
|
+
lastAimedFile.current = fileId
|
|
83
|
+
onAimFile(fileId)
|
|
84
|
+
}
|
|
68
85
|
})
|
|
69
86
|
|
|
70
87
|
useEffect(() => {
|
|
@@ -86,12 +103,18 @@ export function SelectionController({
|
|
|
86
103
|
}, 280)
|
|
87
104
|
return
|
|
88
105
|
}
|
|
89
|
-
const
|
|
90
|
-
onSelect(
|
|
106
|
+
const fileId = aimedFileId(hits)
|
|
107
|
+
onSelect(fileId)
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
const onDblClick = () => {
|
|
94
111
|
if (travelTimer) window.clearTimeout(travelTimer)
|
|
112
|
+
if (!locked) return
|
|
113
|
+
raycaster.setFromCamera(ndc, camera)
|
|
114
|
+
const fileId = aimedFileId(raycaster.intersectObjects(scene.children, true))
|
|
115
|
+
if (!fileId) return
|
|
116
|
+
onSelect(fileId)
|
|
117
|
+
onInspect?.(fileId)
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
window.addEventListener('click', onClick)
|
|
@@ -101,7 +124,7 @@ export function SelectionController({
|
|
|
101
124
|
window.removeEventListener('click', onClick)
|
|
102
125
|
window.removeEventListener('dblclick', onDblClick)
|
|
103
126
|
}
|
|
104
|
-
}, [camera, files, locked, onSelect, onTravelTo, scene])
|
|
127
|
+
}, [camera, files, locked, onInspect, onSelect, onTravelTo, scene])
|
|
105
128
|
|
|
106
129
|
return null
|
|
107
130
|
}
|