@jkwd/inbase 0.1.3 → 0.1.5
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 +3 -1
- package/apps/explorer/scripts/open-editor.d.ts +6 -0
- package/apps/explorer/scripts/open-editor.mjs +219 -0
- package/apps/explorer/scripts/session-store.d.ts +51 -1
- package/apps/explorer/scripts/session-store.mjs +461 -48
- package/apps/explorer/src/App.tsx +178 -78
- package/apps/explorer/src/agentIntent.ts +54 -1
- package/apps/explorer/src/index.css +238 -2
- package/apps/explorer/src/layout.ts +46 -0
- package/apps/explorer/src/scene/FileBlock.tsx +110 -145
- package/apps/explorer/src/scene/FolderArea.tsx +17 -4
- package/apps/explorer/src/scene/MapSelectBorder.tsx +3 -1
- package/apps/explorer/src/scene/MapView.tsx +41 -18
- package/apps/explorer/src/scene/Player.tsx +27 -0
- package/apps/explorer/src/scene/RelationLines.tsx +36 -32
- package/apps/explorer/src/scene/SelectionController.tsx +21 -3
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +451 -0
- package/apps/explorer/src/scene/World.tsx +19 -50
- package/apps/explorer/src/theme.ts +10 -1
- package/apps/explorer/src/types.ts +12 -0
- package/apps/explorer/src/ui/HUD.tsx +699 -390
- package/apps/explorer/vite.config.ts +72 -22
- package/bin/session.mjs +17 -3
- package/package.json +3 -1
- package/skill/inbase/SKILL.md +22 -14
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Suspense } from 'react'
|
|
2
|
-
import { Text } from '@react-three/drei'
|
|
2
|
+
import { Html, Text } from '@react-three/drei'
|
|
3
3
|
import { CHANGE_HIGHLIGHT, folderFloorColor, MAP_SELECTION, type ChangeKind } from '../theme'
|
|
4
4
|
import type { PlacedFolder } from '../types'
|
|
5
5
|
import { MapSelectBorder } from './MapSelectBorder'
|
|
@@ -10,6 +10,7 @@ type FolderAreaProps = {
|
|
|
10
10
|
selected?: boolean
|
|
11
11
|
mapMode?: boolean
|
|
12
12
|
highlightKind?: ChangeKind | null
|
|
13
|
+
previewLabels?: boolean
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function FolderArea({
|
|
@@ -18,10 +19,10 @@ export function FolderArea({
|
|
|
18
19
|
selected = false,
|
|
19
20
|
mapMode = false,
|
|
20
21
|
highlightKind = null,
|
|
22
|
+
previewLabels = false,
|
|
21
23
|
}: FolderAreaProps) {
|
|
22
24
|
const added = Boolean(folder.added)
|
|
23
|
-
const highlight =
|
|
24
|
-
mapMode && highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
|
|
25
|
+
const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
|
|
25
26
|
const addedOnly = added && !highlight
|
|
26
27
|
const outline = highlight ? highlight.color : addedOnly ? '#7ec8e8' : null
|
|
27
28
|
const color = highlight
|
|
@@ -57,6 +58,7 @@ export function FolderArea({
|
|
|
57
58
|
depth={folder.depth}
|
|
58
59
|
y={0.06}
|
|
59
60
|
stroke={MAP_SELECTION.islandPad}
|
|
61
|
+
color={MAP_SELECTION.island}
|
|
60
62
|
/>
|
|
61
63
|
)}
|
|
62
64
|
<mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
@@ -79,7 +81,18 @@ export function FolderArea({
|
|
|
79
81
|
</mesh>
|
|
80
82
|
)}
|
|
81
83
|
<Suspense fallback={null}>
|
|
82
|
-
{!naming && (
|
|
84
|
+
{!naming && previewLabels && (
|
|
85
|
+
<Html
|
|
86
|
+
position={[0, 1.35, -folder.depth / 2 + 1.6]}
|
|
87
|
+
center
|
|
88
|
+
occlude={false}
|
|
89
|
+
style={{ pointerEvents: 'none' }}
|
|
90
|
+
zIndexRange={[20, 0]}
|
|
91
|
+
>
|
|
92
|
+
<div className="thumbnail-folder-label">{label}</div>
|
|
93
|
+
</Html>
|
|
94
|
+
)}
|
|
95
|
+
{!naming && !previewLabels && !mapMode && (
|
|
83
96
|
<Text
|
|
84
97
|
position={[0, 0.05, -folder.depth / 2 + 1.6]}
|
|
85
98
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
@@ -7,6 +7,7 @@ type MapSelectBorderProps = {
|
|
|
7
7
|
depth: number
|
|
8
8
|
y?: number
|
|
9
9
|
stroke?: number
|
|
10
|
+
color?: string
|
|
10
11
|
userData?: Record<string, unknown>
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -15,6 +16,7 @@ export function MapSelectBorder({
|
|
|
15
16
|
depth,
|
|
16
17
|
y = 0.04,
|
|
17
18
|
stroke = MAP_SELECTION.blockPad,
|
|
19
|
+
color = MAP_SELECTION.color,
|
|
18
20
|
userData,
|
|
19
21
|
}: MapSelectBorderProps) {
|
|
20
22
|
const geometry = useMemo(() => {
|
|
@@ -47,7 +49,7 @@ export function MapSelectBorder({
|
|
|
47
49
|
userData={userData}
|
|
48
50
|
>
|
|
49
51
|
<meshBasicMaterial
|
|
50
|
-
color={
|
|
52
|
+
color={color}
|
|
51
53
|
toneMapped={false}
|
|
52
54
|
side={THREE.DoubleSide}
|
|
53
55
|
depthWrite={false}
|
|
@@ -49,6 +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 target = useMemo(() => [bounds.cx, 0, cz] as [number, number, number], [bounds.cx, cz])
|
|
52
53
|
|
|
53
54
|
useLayoutEffect(() => {
|
|
54
55
|
if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
|
|
@@ -66,8 +67,13 @@ export function MapView({
|
|
|
66
67
|
const element = gl.domElement
|
|
67
68
|
element.style.cursor = 'grab'
|
|
68
69
|
|
|
70
|
+
const isWalkClick = (event: PointerEvent | MouseEvent) => event.ctrlKey
|
|
71
|
+
|
|
72
|
+
const isWalkButton = (event: PointerEvent) =>
|
|
73
|
+
event.button === 0 || (event.ctrlKey && event.button === 2)
|
|
74
|
+
|
|
69
75
|
const onDown = (event: PointerEvent) => {
|
|
70
|
-
if (event
|
|
76
|
+
if (!isWalkButton(event)) return
|
|
71
77
|
drag.current = { x: event.clientX, y: event.clientY, moved: false, active: true }
|
|
72
78
|
element.style.cursor = 'grabbing'
|
|
73
79
|
}
|
|
@@ -93,20 +99,32 @@ export function MapView({
|
|
|
93
99
|
return { raycaster, relationHit, fileHit }
|
|
94
100
|
}
|
|
95
101
|
|
|
96
|
-
const
|
|
102
|
+
const landAt = (x: number, z: number) => {
|
|
103
|
+
const lock = element.requestPointerLock()
|
|
104
|
+
if (lock && typeof lock.catch === 'function') {
|
|
105
|
+
void lock.catch(() => {})
|
|
106
|
+
}
|
|
107
|
+
onLand(x, z)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const landAtPointer = (clientX: number, clientY: number, allowIsland: boolean) => {
|
|
97
111
|
const pick = pickAt(clientX, clientY)
|
|
98
112
|
if (!pick) return false
|
|
99
113
|
const { raycaster, relationHit, fileHit } = pick
|
|
100
|
-
if (
|
|
114
|
+
if (fileHit) return false
|
|
101
115
|
|
|
102
116
|
const hit = new THREE.Vector3()
|
|
103
117
|
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
104
118
|
if (!raycaster.ray.intersectPlane(plane, hit)) return false
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
|
|
120
|
+
const folder = folderAt(hit.x, hit.z, layout)
|
|
121
|
+
if (allowIsland && folder) {
|
|
122
|
+
landAt(hit.x, hit.z)
|
|
123
|
+
return true
|
|
108
124
|
}
|
|
109
|
-
|
|
125
|
+
if (relationHit) return false
|
|
126
|
+
|
|
127
|
+
landAt(hit.x, hit.z)
|
|
110
128
|
return true
|
|
111
129
|
}
|
|
112
130
|
|
|
@@ -114,27 +132,26 @@ export function MapView({
|
|
|
114
132
|
element.style.cursor = 'grab'
|
|
115
133
|
const startedOnCanvas = drag.current.active
|
|
116
134
|
drag.current.active = false
|
|
117
|
-
if (!startedOnCanvas || event
|
|
135
|
+
if (!startedOnCanvas || !isWalkButton(event) || drag.current.moved) return
|
|
118
136
|
|
|
119
|
-
if (event
|
|
137
|
+
if (isWalkClick(event) && landAtPointer(event.clientX, event.clientY, true)) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
120
140
|
|
|
121
141
|
const pick = pickAt(event.clientX, event.clientY)
|
|
122
142
|
if (!pick) return
|
|
123
143
|
const { relationHit, fileHit } = pick
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
144
|
+
if (fileHit) {
|
|
145
|
+
onSelect(fileHit.object.userData.fileId as string)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (relationHit) {
|
|
128
149
|
onTravelTo(
|
|
129
150
|
relationHit.object.userData.relationFrom as string,
|
|
130
151
|
relationHit.object.userData.relationTo as string,
|
|
131
152
|
)
|
|
132
153
|
return
|
|
133
154
|
}
|
|
134
|
-
if (fileHit) {
|
|
135
|
-
onSelect(fileHit.object.userData.fileId as string)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
155
|
|
|
139
156
|
const hit = new THREE.Vector3()
|
|
140
157
|
const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
|
|
@@ -149,14 +166,20 @@ export function MapView({
|
|
|
149
166
|
onSelectFolder(null)
|
|
150
167
|
}
|
|
151
168
|
|
|
169
|
+
const onContextMenu = (event: MouseEvent) => {
|
|
170
|
+
if (event.ctrlKey) event.preventDefault()
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
element.addEventListener('pointerdown', onDown)
|
|
153
174
|
window.addEventListener('pointermove', onMove)
|
|
154
175
|
window.addEventListener('pointerup', onUp)
|
|
176
|
+
element.addEventListener('contextmenu', onContextMenu)
|
|
155
177
|
return () => {
|
|
156
178
|
element.style.cursor = ''
|
|
157
179
|
element.removeEventListener('pointerdown', onDown)
|
|
158
180
|
window.removeEventListener('pointermove', onMove)
|
|
159
181
|
window.removeEventListener('pointerup', onUp)
|
|
182
|
+
element.removeEventListener('contextmenu', onContextMenu)
|
|
160
183
|
}
|
|
161
184
|
}, [
|
|
162
185
|
camera,
|
|
@@ -191,7 +214,7 @@ export function MapView({
|
|
|
191
214
|
zoomSpeed={1.15}
|
|
192
215
|
minZoom={Math.max(fitZoom * 0.35, 0.05)}
|
|
193
216
|
maxZoom={Math.max(fitZoom * 10, 20)}
|
|
194
|
-
target={
|
|
217
|
+
target={target}
|
|
195
218
|
/>
|
|
196
219
|
)}
|
|
197
220
|
{enabled &&
|
|
@@ -49,6 +49,7 @@ export function Player({
|
|
|
49
49
|
const [steering, setSteering] = useState(true)
|
|
50
50
|
const controlsRef = useRef<PointerLockControlsImpl>(null)
|
|
51
51
|
const capturedLook = useRef(false)
|
|
52
|
+
const lookHeld = useRef(false)
|
|
52
53
|
const flying = useRef<{
|
|
53
54
|
startPos: THREE.Vector3
|
|
54
55
|
endPos: THREE.Vector3
|
|
@@ -133,6 +134,31 @@ export function Player({
|
|
|
133
134
|
controls.lock()
|
|
134
135
|
}, [lockEnabled, onLockedChange, steering, walking])
|
|
135
136
|
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!walking) return
|
|
139
|
+
|
|
140
|
+
const looking = () =>
|
|
141
|
+
Boolean(controlsRef.current?.isLocked || document.pointerLockElement)
|
|
142
|
+
|
|
143
|
+
const onClick = (event: MouseEvent) => {
|
|
144
|
+
if (event.button !== 0 || event.detail !== 1) return
|
|
145
|
+
lookHeld.current = looking()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const onDblClick = () => {
|
|
149
|
+
if (!lookHeld.current) return
|
|
150
|
+
controlsRef.current?.unlock()
|
|
151
|
+
document.exitPointerLock()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
document.addEventListener('click', onClick, true)
|
|
155
|
+
document.addEventListener('dblclick', onDblClick)
|
|
156
|
+
return () => {
|
|
157
|
+
document.removeEventListener('click', onClick, true)
|
|
158
|
+
document.removeEventListener('dblclick', onDblClick)
|
|
159
|
+
}
|
|
160
|
+
}, [walking])
|
|
161
|
+
|
|
136
162
|
useFrame((_, delta) => {
|
|
137
163
|
try {
|
|
138
164
|
if (walking && flying.current) {
|
|
@@ -198,6 +224,7 @@ export function Player({
|
|
|
198
224
|
<PointerLockControls
|
|
199
225
|
ref={controlsRef}
|
|
200
226
|
makeDefault
|
|
227
|
+
selector=".stage canvas"
|
|
201
228
|
enabled={steering && lockEnabled}
|
|
202
229
|
onLock={() => onLockedChange(true)}
|
|
203
230
|
onUnlock={() => onLockedChange(false)}
|
|
@@ -125,49 +125,53 @@ export function RelationLines({
|
|
|
125
125
|
const selectedRadius = fromAbove ? 0.2 : 0.07
|
|
126
126
|
const plannedRadius = fromAbove ? 0.26 : 0.1
|
|
127
127
|
const lines: LineMesh[] = []
|
|
128
|
-
const
|
|
128
|
+
const drawn = new Set<string>()
|
|
129
|
+
|
|
130
|
+
const addLine = (
|
|
131
|
+
fromId: string,
|
|
132
|
+
toId: string,
|
|
133
|
+
planned: boolean,
|
|
134
|
+
radius: number,
|
|
135
|
+
) => {
|
|
136
|
+
const key = `${fromId}->${toId}`
|
|
137
|
+
if (drawn.has(key)) return
|
|
138
|
+
const from = placed[fromId]
|
|
139
|
+
const to = placed[toId]
|
|
140
|
+
if (!from || !to) return
|
|
141
|
+
drawn.add(key)
|
|
142
|
+
lines.push(edgeMesh(from, to, toId, planned, radius, fromAbove, false))
|
|
143
|
+
}
|
|
129
144
|
|
|
130
145
|
for (const edge of plannedEdges) {
|
|
131
|
-
|
|
132
|
-
const to = placed[edge.to]
|
|
133
|
-
if (!from || !to) continue
|
|
134
|
-
const key = `${edge.from}->${edge.to}`
|
|
135
|
-
if (plannedKeys.has(key)) continue
|
|
136
|
-
plannedKeys.add(key)
|
|
137
|
-
lines.push(edgeMesh(from, to, edge.to, true, plannedRadius, fromAbove, false))
|
|
146
|
+
addLine(edge.from, edge.to, true, plannedRadius)
|
|
138
147
|
}
|
|
139
148
|
|
|
140
149
|
if (selectedId) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
} else {
|
|
154
|
-
const file = files.find((item) => item.id === selectedId)
|
|
155
|
-
if (file) {
|
|
156
|
-
for (const importId of file.imports) {
|
|
157
|
-
if (plannedKeys.has(`${file.id}->${importId}`)) continue
|
|
158
|
-
const target = placed[importId]
|
|
159
|
-
if (!target) continue
|
|
160
|
-
lines.push(
|
|
161
|
-
edgeMesh(selected, target, importId, false, selectedRadius, fromAbove, false),
|
|
162
|
-
)
|
|
163
|
-
}
|
|
150
|
+
if (importedBy) {
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
if (file.id === selectedId || !file.imports.includes(selectedId)) continue
|
|
153
|
+
addLine(file.id, selectedId, false, selectedRadius)
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
const file = files.find((item) => item.id === selectedId)
|
|
157
|
+
if (file) {
|
|
158
|
+
for (const importId of file.imports) {
|
|
159
|
+
addLine(file.id, importId, false, selectedRadius)
|
|
164
160
|
}
|
|
165
161
|
}
|
|
166
162
|
}
|
|
167
163
|
}
|
|
168
164
|
|
|
169
165
|
return lines
|
|
170
|
-
}, [
|
|
166
|
+
}, [
|
|
167
|
+
extras,
|
|
168
|
+
files,
|
|
169
|
+
fromAbove,
|
|
170
|
+
importedBy,
|
|
171
|
+
layout.files,
|
|
172
|
+
plannedEdges,
|
|
173
|
+
selectedId,
|
|
174
|
+
])
|
|
171
175
|
|
|
172
176
|
if (meshes.length === 0) return null
|
|
173
177
|
|
|
@@ -68,21 +68,39 @@ export function SelectionController({
|
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
useEffect(() => {
|
|
71
|
-
|
|
71
|
+
let travelTimer: number | undefined
|
|
72
|
+
|
|
73
|
+
const onClick = (event: MouseEvent) => {
|
|
72
74
|
if (!locked) return
|
|
75
|
+
if (event.detail > 1) {
|
|
76
|
+
if (travelTimer) window.clearTimeout(travelTimer)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
73
79
|
raycaster.setFromCamera(ndc, camera)
|
|
74
80
|
const hits = raycaster.intersectObjects(scene.children, true)
|
|
75
81
|
const aim = aimedRelation(hits, camera.position, files)
|
|
76
82
|
if (aim) {
|
|
77
|
-
|
|
83
|
+
travelTimer = window.setTimeout(() => {
|
|
84
|
+
travelTimer = undefined
|
|
85
|
+
onTravelTo(aim.from, aim.to)
|
|
86
|
+
}, 280)
|
|
78
87
|
return
|
|
79
88
|
}
|
|
80
89
|
const file = hits.find((item) => item.object.userData.fileId)
|
|
81
90
|
onSelect(file ? (file.object.userData.fileId as string) : null)
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
const onDblClick = () => {
|
|
94
|
+
if (travelTimer) window.clearTimeout(travelTimer)
|
|
95
|
+
}
|
|
96
|
+
|
|
84
97
|
window.addEventListener('click', onClick)
|
|
85
|
-
|
|
98
|
+
window.addEventListener('dblclick', onDblClick)
|
|
99
|
+
return () => {
|
|
100
|
+
if (travelTimer) window.clearTimeout(travelTimer)
|
|
101
|
+
window.removeEventListener('click', onClick)
|
|
102
|
+
window.removeEventListener('dblclick', onDblClick)
|
|
103
|
+
}
|
|
86
104
|
}, [camera, files, locked, onSelect, onTravelTo, scene])
|
|
87
105
|
|
|
88
106
|
return null
|