@jkwd/inbase 0.1.7 → 0.1.9
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 +26 -2
- 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
package/README.md
CHANGED
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
|
|
8
8
|
A first-person 3D map of a JavaScript or TypeScript codebase. Files become blocks, folders become walkable areas, and imports become lines in the air.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img src="docs/inbase-1.png" alt="First-person walk view of the codebase map" width="47%" />
|
|
12
|
+
|
|
13
|
+
<img src="docs/inbase-2.png" alt="Map view with 3D overlay" width="47%" />
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
Install the package in a project, run `inbase init`, then `inbase run`. Cursor’s LLM then plans and patches through the visual map instead of editing files directly.
|
|
11
17
|
|
|
12
18
|
The npm package is `@jkwd/inbase` (npm blocks the unscoped name `inbase`). The command is still `inbase`.
|
|
13
19
|
|
|
@@ -35,7 +41,21 @@ Open the printed URL (http://localhost:5173 by default), click the scene, then w
|
|
|
35
41
|
inbase run --target /path/to/your/project
|
|
36
42
|
```
|
|
37
43
|
|
|
38
|
-
`inbase init` copies a Cursor skill into `.cursor/skills/inbase/` and gitignores `.inbase/`.
|
|
44
|
+
`inbase init` copies a Cursor skill into `.cursor/skills/inbase/` and gitignores `.inbase/`. Keep `inbase run` open, then ask Cursor to change source files — the LLM follows the visual plan and patch loop described below.
|
|
45
|
+
|
|
46
|
+
## LLM integration
|
|
47
|
+
|
|
48
|
+
<img src="docs/inbase-llm.png" alt="LLM working through a plan in the heads-up display overlay" align="left" width="280" hspace="16" />
|
|
49
|
+
|
|
50
|
+
Inbase does not call a model itself. The visual coding loop currently supports **Cursor**. The installed skill makes the agent work through the map: it reports a plan, waits on the **HUD** (heads-up display — the overlay panel on the 3D map), and publishes each step as a patch you can walk before it is applied.
|
|
51
|
+
|
|
52
|
+
When a chat starts, that overlay asks whether to set up a **blueprint**. **Create blueprint** lets you place files (`Space`) and folders (`B`), then **Send blueprint** — that layout is the source of truth for the chat. **Let LLM continue** skips placement and lets the agent choose files from the request.
|
|
53
|
+
|
|
54
|
+
Turn on **Make LLM look where I look** if the agent should prefer the island you are standing on and the blocks you are facing. With **Step by step** on, click **Run step** for each patch. With it off, the LLM implements the full plan; you can still walk Previous/Next over the diffs, then **Complete**. Send an alternative instruction from the HUD to revise the remaining plan, or **Stop** to end the session.
|
|
55
|
+
|
|
56
|
+
The LLM never writes project files itself. Patches apply when you run or complete steps in the visualizer.
|
|
57
|
+
|
|
58
|
+
<br clear="all" />
|
|
39
59
|
|
|
40
60
|
## Commands
|
|
41
61
|
|
|
@@ -47,6 +67,10 @@ inbase run --target /path/to/your/project
|
|
|
47
67
|
|
|
48
68
|
Session commands (`start-session`, `wait-for-blueprint`, `report-plan`, `wait-for-approval`, `propose-patch`) are used by the Cursor skill. You do not need to run them yourself.
|
|
49
69
|
|
|
70
|
+
## Editor support
|
|
71
|
+
|
|
72
|
+
The map runs in the browser. The LLM plan and patch loop currently works in **Cursor** only (`inbase init` installs a skill into `.cursor/skills/inbase/`).
|
|
73
|
+
|
|
50
74
|
## Language support
|
|
51
75
|
|
|
52
76
|
Source files (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`) become blocks with **functions**, **classes**, and **variables**, plus import edges from `import` and `require()`. Styles and docs (`.css`, `.scss`, `.html`, `.json`, `.md`) show up as blocks without symbols. Only relative specifiers (`./`, `../`) become edges — package names like `react` or `@angular/core` do not.
|
|
@@ -62,6 +62,10 @@ export function discardInactiveDiffSessions(
|
|
|
62
62
|
targetRoot?: string | null,
|
|
63
63
|
waiterIds?: Iterable<string>,
|
|
64
64
|
): string[]
|
|
65
|
+
export function clearDiffSessions(
|
|
66
|
+
dataDir: string,
|
|
67
|
+
targetRoot?: string | null,
|
|
68
|
+
): void
|
|
65
69
|
export function listSessionIntents(
|
|
66
70
|
dataDir: string,
|
|
67
71
|
knownFileIds?: string[],
|
|
@@ -1154,6 +1154,22 @@ export function discardInactiveDiffSessions(
|
|
|
1154
1154
|
return liveIds
|
|
1155
1155
|
}
|
|
1156
1156
|
|
|
1157
|
+
export function clearDiffSessions(dataDir, targetRoot = null) {
|
|
1158
|
+
for (const sessionId of listStoredSessionIds(dataDir)) {
|
|
1159
|
+
discardStoredSession(dataDir, sessionId, targetRoot)
|
|
1160
|
+
}
|
|
1161
|
+
writeActiveSession(dataDir, null)
|
|
1162
|
+
writeBlueprintSession(dataDir, null)
|
|
1163
|
+
|
|
1164
|
+
const root = diffSessionsRoot(dataDir)
|
|
1165
|
+
fs.mkdirSync(root, { recursive: true })
|
|
1166
|
+
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
1167
|
+
if (entry.name === '.gitkeep') continue
|
|
1168
|
+
fs.rmSync(path.join(root, entry.name), { recursive: true, force: true })
|
|
1169
|
+
}
|
|
1170
|
+
unstageDiffSessionArtifacts(dataDir, targetRoot)
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1157
1173
|
export function stopSession(dataDir, sessionId, targetRoot = null) {
|
|
1158
1174
|
const safeId = assertSessionId(sessionId)
|
|
1159
1175
|
writeStoppedMarker(dataDir, safeId)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { Canvas } from '@react-three/fiber'
|
|
3
3
|
import { emptyIntent, fetchAgentIntent, fetchAgentIntents, inspectTargetFile, performAgentAction, persistSessionBlueprint } from './agentIntent'
|
|
4
|
-
import { fetchCodebase } from './codebase'
|
|
4
|
+
import { fetchCodebase, updateCodebase } from './codebase'
|
|
5
5
|
import {
|
|
6
6
|
layoutWorld,
|
|
7
7
|
markCreatedFolders,
|
|
@@ -72,17 +72,35 @@ function intentSignature(intent: AgentIntent) {
|
|
|
72
72
|
export default function App() {
|
|
73
73
|
const [graph, setGraph] = useState<CodebaseGraph | null>(null)
|
|
74
74
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
75
|
+
const [updatingModel, setUpdatingModel] = useState(false)
|
|
76
|
+
const updatingModelRef = useRef(false)
|
|
75
77
|
|
|
76
|
-
const
|
|
77
|
-
const next = await fetchCodebase()
|
|
78
|
+
const applyGraph = useCallback((next: CodebaseGraph | null, failed: string) => {
|
|
78
79
|
if (next) {
|
|
79
80
|
setGraph(next)
|
|
80
81
|
setLoadError(null)
|
|
81
|
-
return
|
|
82
|
+
return true
|
|
82
83
|
}
|
|
83
|
-
setLoadError((current) => current ??
|
|
84
|
+
setLoadError((current) => current ?? failed)
|
|
85
|
+
return false
|
|
84
86
|
}, [])
|
|
85
87
|
|
|
88
|
+
const refreshGraph = useCallback(async () => {
|
|
89
|
+
applyGraph(await fetchCodebase(), 'Could not load the project map.')
|
|
90
|
+
}, [applyGraph])
|
|
91
|
+
|
|
92
|
+
const updateModel = useCallback(async () => {
|
|
93
|
+
if (updatingModelRef.current) return
|
|
94
|
+
updatingModelRef.current = true
|
|
95
|
+
setUpdatingModel(true)
|
|
96
|
+
try {
|
|
97
|
+
applyGraph(await updateCodebase(), 'Could not update the project map.')
|
|
98
|
+
} finally {
|
|
99
|
+
updatingModelRef.current = false
|
|
100
|
+
setUpdatingModel(false)
|
|
101
|
+
}
|
|
102
|
+
}, [applyGraph])
|
|
103
|
+
|
|
86
104
|
useEffect(() => {
|
|
87
105
|
void refreshGraph()
|
|
88
106
|
}, [refreshGraph])
|
|
@@ -95,15 +113,26 @@ export default function App() {
|
|
|
95
113
|
)
|
|
96
114
|
}
|
|
97
115
|
|
|
98
|
-
return
|
|
116
|
+
return (
|
|
117
|
+
<Explorer
|
|
118
|
+
graph={graph}
|
|
119
|
+
onRefreshGraph={refreshGraph}
|
|
120
|
+
onUpdateModel={updateModel}
|
|
121
|
+
updatingModel={updatingModel}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
function Explorer({
|
|
102
127
|
graph,
|
|
103
128
|
onRefreshGraph,
|
|
129
|
+
onUpdateModel,
|
|
130
|
+
updatingModel,
|
|
104
131
|
}: {
|
|
105
132
|
graph: CodebaseGraph
|
|
106
133
|
onRefreshGraph: () => Promise<void>
|
|
134
|
+
onUpdateModel: () => Promise<void>
|
|
135
|
+
updatingModel: boolean
|
|
107
136
|
}) {
|
|
108
137
|
const [intents, setIntents] = useState<AgentIntent[]>([])
|
|
109
138
|
const [focusedSessionId, setFocusedSessionId] = useState<string | null>(null)
|
|
@@ -238,7 +267,6 @@ function Explorer({
|
|
|
238
267
|
const [inspectTick, setInspectTick] = useState(0)
|
|
239
268
|
const [flyTo, setFlyTo] = useState<FlyTo | null>(null)
|
|
240
269
|
const [locked, setLocked] = useState(false)
|
|
241
|
-
const [currentFolder, setCurrentFolder] = useState(graph.targetName)
|
|
242
270
|
const [followLook, setFollowLook] = useState(false)
|
|
243
271
|
const [importedBy, setImportedBy] = useState(false)
|
|
244
272
|
const [changePathsOnly, setChangePathsOnly] = useState(false)
|
|
@@ -287,6 +315,7 @@ function Explorer({
|
|
|
287
315
|
const toggleMap = useCallback(() => {
|
|
288
316
|
if (mode === 'walk') {
|
|
289
317
|
setLandAt(walkPos.current)
|
|
318
|
+
setFlyTo(null)
|
|
290
319
|
setLocked(false)
|
|
291
320
|
document.exitPointerLock()
|
|
292
321
|
setMode('map')
|
|
@@ -322,6 +351,7 @@ function Explorer({
|
|
|
322
351
|
(fileId: string, fly: boolean) => {
|
|
323
352
|
const placed = layout.files[fileId]
|
|
324
353
|
if (!placed) return
|
|
354
|
+
const from = walkPos.current
|
|
325
355
|
const [x, z] = standInFront(placed)
|
|
326
356
|
walkPos.current = [x, z]
|
|
327
357
|
setAimedRelation(null)
|
|
@@ -330,6 +360,7 @@ function Explorer({
|
|
|
330
360
|
fly
|
|
331
361
|
? {
|
|
332
362
|
nonce: Date.now(),
|
|
363
|
+
from: [from[0], from[1]],
|
|
333
364
|
lookAt: [placed.position[0], placed.position[1], placed.position[2]],
|
|
334
365
|
}
|
|
335
366
|
: null,
|
|
@@ -1099,7 +1130,6 @@ function Explorer({
|
|
|
1099
1130
|
onSelect={selectFile}
|
|
1100
1131
|
onSelectFolder={selectFolder}
|
|
1101
1132
|
onLockedChange={setLocked}
|
|
1102
|
-
onFolderChange={setCurrentFolder}
|
|
1103
1133
|
onLand={landFromMap}
|
|
1104
1134
|
onWalkPosition={rememberWalk}
|
|
1105
1135
|
onContext={persistUserContext}
|
|
@@ -1145,7 +1175,6 @@ function Explorer({
|
|
|
1145
1175
|
landAt={landAt}
|
|
1146
1176
|
aimedRelation={aimedRelation}
|
|
1147
1177
|
aimedFileId={aimedFileId}
|
|
1148
|
-
currentFolder={currentFolder}
|
|
1149
1178
|
intent={intent}
|
|
1150
1179
|
intents={intents}
|
|
1151
1180
|
focusedSessionId={focusedSessionId}
|
|
@@ -1156,6 +1185,8 @@ function Explorer({
|
|
|
1156
1185
|
onWalk={openWalk}
|
|
1157
1186
|
followLook={followLook}
|
|
1158
1187
|
onToggleFollowLook={toggleFollowLook}
|
|
1188
|
+
onUpdateModel={onUpdateModel}
|
|
1189
|
+
updatingModel={updatingModel}
|
|
1159
1190
|
importedBy={importedBy}
|
|
1160
1191
|
onToggleImportedBy={toggleImportedBy}
|
|
1161
1192
|
changePathsOnly={changePathsOnly}
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import type { CodebaseGraph } from './types'
|
|
2
2
|
|
|
3
|
+
function parseCodebase(data: unknown): CodebaseGraph | null {
|
|
4
|
+
if (
|
|
5
|
+
!data ||
|
|
6
|
+
typeof data !== 'object' ||
|
|
7
|
+
!Array.isArray((data as CodebaseGraph).files) ||
|
|
8
|
+
!Array.isArray((data as CodebaseGraph).folders)
|
|
9
|
+
) {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
return data as CodebaseGraph
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export async function fetchCodebase(): Promise<CodebaseGraph | null> {
|
|
4
16
|
try {
|
|
5
17
|
const response = await fetch(`/api/codebase?t=${Date.now()}`)
|
|
6
18
|
if (!response.ok) return null
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
19
|
+
return parseCodebase(await response.json())
|
|
20
|
+
} catch {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function updateCodebase(): Promise<CodebaseGraph | null> {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch('/api/codebase', { method: 'POST' })
|
|
28
|
+
if (!response.ok) return null
|
|
29
|
+
return parseCodebase(await response.json())
|
|
12
30
|
} catch {
|
|
13
31
|
return null
|
|
14
32
|
}
|
|
@@ -174,11 +174,22 @@ button {
|
|
|
174
174
|
padding: 8px 12px;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
.hud-button:disabled {
|
|
178
|
+
opacity: 0.55;
|
|
179
|
+
cursor: default;
|
|
180
|
+
}
|
|
181
|
+
|
|
177
182
|
.hud-button[data-active='true'] {
|
|
178
183
|
border-color: #5d9ec4;
|
|
179
184
|
color: #d7eef8;
|
|
180
185
|
}
|
|
181
186
|
|
|
187
|
+
.hud-bottom-actions {
|
|
188
|
+
display: flex;
|
|
189
|
+
gap: 8px;
|
|
190
|
+
flex: none;
|
|
191
|
+
}
|
|
192
|
+
|
|
182
193
|
.hud-chip,
|
|
183
194
|
.hud-panel {
|
|
184
195
|
background: rgba(25, 28, 34, 0.86);
|
|
@@ -240,6 +251,51 @@ button {
|
|
|
240
251
|
background: rgba(25, 28, 34, 0.94);
|
|
241
252
|
}
|
|
242
253
|
|
|
254
|
+
.map-you-are-here {
|
|
255
|
+
display: grid;
|
|
256
|
+
justify-items: center;
|
|
257
|
+
transform: translateY(-10px);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.map-you-are-here-pin {
|
|
261
|
+
width: 18px;
|
|
262
|
+
height: 18px;
|
|
263
|
+
background: #e8c36a;
|
|
264
|
+
border: 2px solid #fff8e4;
|
|
265
|
+
border-radius: 50% 50% 50% 0;
|
|
266
|
+
box-shadow:
|
|
267
|
+
0 0 0 0 rgba(232, 195, 106, 0.7),
|
|
268
|
+
0 2px 10px rgba(0, 0, 0, 0.55);
|
|
269
|
+
transform: rotate(-45deg);
|
|
270
|
+
animation: map-you-are-here-pulse 1.7s ease-out infinite;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.map-you-are-here-pin::after {
|
|
274
|
+
content: '';
|
|
275
|
+
position: absolute;
|
|
276
|
+
inset: 4px;
|
|
277
|
+
border-radius: 50%;
|
|
278
|
+
background: #1a1408;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@keyframes map-you-are-here-pulse {
|
|
282
|
+
0% {
|
|
283
|
+
box-shadow:
|
|
284
|
+
0 0 0 0 rgba(232, 195, 106, 0.75),
|
|
285
|
+
0 2px 10px rgba(0, 0, 0, 0.55);
|
|
286
|
+
}
|
|
287
|
+
70% {
|
|
288
|
+
box-shadow:
|
|
289
|
+
0 0 0 12px rgba(232, 195, 106, 0),
|
|
290
|
+
0 2px 10px rgba(0, 0, 0, 0.55);
|
|
291
|
+
}
|
|
292
|
+
100% {
|
|
293
|
+
box-shadow:
|
|
294
|
+
0 0 0 0 rgba(232, 195, 106, 0),
|
|
295
|
+
0 2px 10px rgba(0, 0, 0, 0.55);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
243
299
|
.hud-bottom {
|
|
244
300
|
position: absolute;
|
|
245
301
|
left: 0;
|
|
@@ -256,12 +312,119 @@ button {
|
|
|
256
312
|
pointer-events: auto;
|
|
257
313
|
}
|
|
258
314
|
|
|
259
|
-
.hud-
|
|
315
|
+
.hud-instructions-overlay {
|
|
316
|
+
position: absolute;
|
|
317
|
+
inset: 0;
|
|
318
|
+
z-index: 20;
|
|
319
|
+
display: grid;
|
|
320
|
+
place-items: center;
|
|
321
|
+
background: rgba(25, 28, 34, 0.72);
|
|
322
|
+
pointer-events: auto;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.hud-instructions-card {
|
|
326
|
+
width: min(520px, calc(100% - 32px));
|
|
327
|
+
max-height: min(80dvh, 720px);
|
|
328
|
+
overflow: auto;
|
|
329
|
+
padding: 24px;
|
|
330
|
+
background: var(--vc-editor);
|
|
331
|
+
border: 1px solid var(--vc-border);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.hud-instructions-header {
|
|
335
|
+
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
justify-content: space-between;
|
|
338
|
+
gap: 12px;
|
|
339
|
+
margin-bottom: 16px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.hud-instructions-header h1 {
|
|
343
|
+
margin: 0;
|
|
344
|
+
font-size: 20px;
|
|
345
|
+
font-weight: 600;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.hud-instructions-sections {
|
|
349
|
+
display: flex;
|
|
350
|
+
flex-direction: column;
|
|
351
|
+
gap: 22px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.hud-instructions-section h2 {
|
|
260
355
|
display: flex;
|
|
356
|
+
align-items: center;
|
|
261
357
|
gap: 8px;
|
|
262
|
-
|
|
263
|
-
color: #
|
|
358
|
+
margin: 0 0 10px;
|
|
359
|
+
color: #8b95a5;
|
|
264
360
|
font-size: 12px;
|
|
361
|
+
font-weight: 600;
|
|
362
|
+
letter-spacing: 0.08em;
|
|
363
|
+
text-transform: uppercase;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.hud-instructions-section[data-current='true'] h2 {
|
|
367
|
+
color: #d7eef8;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.hud-instructions-current {
|
|
371
|
+
padding: 2px 6px;
|
|
372
|
+
border: 1px solid #5d9ec4;
|
|
373
|
+
border-radius: 3px;
|
|
374
|
+
color: #d7eef8;
|
|
375
|
+
font-size: 10px;
|
|
376
|
+
letter-spacing: 0.04em;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.hud-instructions-list {
|
|
380
|
+
margin: 0;
|
|
381
|
+
padding: 0;
|
|
382
|
+
list-style: none;
|
|
383
|
+
display: flex;
|
|
384
|
+
flex-direction: column;
|
|
385
|
+
gap: 8px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.hud-instructions-list .hud-instruction {
|
|
389
|
+
display: grid;
|
|
390
|
+
grid-template-columns: minmax(7.5rem, auto) 1fr;
|
|
391
|
+
align-items: center;
|
|
392
|
+
gap: 12px;
|
|
393
|
+
margin-top: 0;
|
|
394
|
+
color: #b7c0ce;
|
|
395
|
+
font-size: 14px;
|
|
396
|
+
line-height: 1.45;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.hud-instructions-list .hud-instruction:not(:has(.hud-instruction-keys)) {
|
|
400
|
+
grid-template-columns: 1fr;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.hud-instruction-keys {
|
|
404
|
+
display: flex;
|
|
405
|
+
flex-wrap: wrap;
|
|
406
|
+
justify-content: flex-end;
|
|
407
|
+
gap: 4px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.hud-instructions-card kbd {
|
|
411
|
+
display: inline-flex;
|
|
412
|
+
align-items: center;
|
|
413
|
+
justify-content: center;
|
|
414
|
+
min-width: 1.6rem;
|
|
415
|
+
height: 1.6rem;
|
|
416
|
+
padding: 0 7px;
|
|
417
|
+
border: 1px solid #3a4250;
|
|
418
|
+
border-bottom-width: 2px;
|
|
419
|
+
border-radius: 4px;
|
|
420
|
+
background: var(--vc-surface);
|
|
421
|
+
color: #e7ebf2;
|
|
422
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
423
|
+
font-size: 11px;
|
|
424
|
+
font-weight: 600;
|
|
425
|
+
letter-spacing: 0.02em;
|
|
426
|
+
line-height: 1;
|
|
427
|
+
white-space: nowrap;
|
|
265
428
|
}
|
|
266
429
|
|
|
267
430
|
.hud-icon-row {
|
|
@@ -419,6 +582,8 @@ button {
|
|
|
419
582
|
|
|
420
583
|
.hud-thumbnail {
|
|
421
584
|
position: fixed;
|
|
585
|
+
display: flex;
|
|
586
|
+
flex-direction: column;
|
|
422
587
|
right: 20px;
|
|
423
588
|
bottom: 72px;
|
|
424
589
|
left: auto;
|
|
@@ -431,6 +596,19 @@ button {
|
|
|
431
596
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
|
432
597
|
}
|
|
433
598
|
|
|
599
|
+
.hud-right-stack .hud-thumbnail[data-maximized='true'],
|
|
600
|
+
.hud-thumbnail[data-maximized='true'] {
|
|
601
|
+
position: fixed;
|
|
602
|
+
top: 68px;
|
|
603
|
+
right: 20px;
|
|
604
|
+
bottom: 72px;
|
|
605
|
+
left: 20px;
|
|
606
|
+
z-index: 40;
|
|
607
|
+
width: auto;
|
|
608
|
+
max-width: none;
|
|
609
|
+
margin-top: 0;
|
|
610
|
+
}
|
|
611
|
+
|
|
434
612
|
.hud-thumbnail-bar {
|
|
435
613
|
display: flex;
|
|
436
614
|
align-items: center;
|
|
@@ -555,13 +733,21 @@ button {
|
|
|
555
733
|
position: relative;
|
|
556
734
|
width: 100%;
|
|
557
735
|
height: 220px;
|
|
736
|
+
min-height: 0;
|
|
737
|
+
flex: none;
|
|
558
738
|
background: #000;
|
|
559
739
|
touch-action: none;
|
|
560
740
|
user-select: none;
|
|
561
741
|
cursor: grab;
|
|
562
742
|
}
|
|
563
743
|
|
|
564
|
-
.hud-thumbnail
|
|
744
|
+
.hud-thumbnail[data-maximized='true'] .hud-thumbnail-stage {
|
|
745
|
+
height: auto;
|
|
746
|
+
flex: 1 1 auto;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.hud-thumbnail-stage[data-panning='true'],
|
|
750
|
+
.hud-thumbnail-stage[data-orbiting='true'] {
|
|
565
751
|
cursor: grabbing;
|
|
566
752
|
}
|
|
567
753
|
|
|
@@ -183,18 +183,63 @@ function segmentDir(from: [number, number], to: [number, number]): [number, numb
|
|
|
183
183
|
return [dx / length, dz / length]
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
function signFontSize(label: string, lintelW: number) {
|
|
187
|
+
return Math.min(0.42, (lintelW - 0.55) / Math.max(label.length * 0.62, 3))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function GateSign({
|
|
191
|
+
z,
|
|
192
|
+
rotY,
|
|
193
|
+
signY,
|
|
194
|
+
lintelW,
|
|
195
|
+
label,
|
|
196
|
+
}: {
|
|
197
|
+
z: number
|
|
198
|
+
rotY: number
|
|
199
|
+
signY: number
|
|
200
|
+
lintelW: number
|
|
201
|
+
label: string
|
|
202
|
+
}) {
|
|
203
|
+
return (
|
|
204
|
+
<group>
|
|
205
|
+
<mesh position={[0, signY, z]}>
|
|
206
|
+
<boxGeometry args={[lintelW - 0.18, SIGN_H, SIGN_D]} />
|
|
207
|
+
<meshLambertMaterial color={SIGN_COLOR} />
|
|
208
|
+
</mesh>
|
|
209
|
+
<Suspense fallback={null}>
|
|
210
|
+
<Text
|
|
211
|
+
position={[0, signY, z + Math.sign(z) * (SIGN_D / 2 + 0.01)]}
|
|
212
|
+
rotation={[0, rotY, 0]}
|
|
213
|
+
fontSize={signFontSize(label, lintelW)}
|
|
214
|
+
color="#e7ebf2"
|
|
215
|
+
anchorX="center"
|
|
216
|
+
anchorY="middle"
|
|
217
|
+
maxWidth={lintelW - 0.4}
|
|
218
|
+
overflowWrap="break-word"
|
|
219
|
+
outlineWidth={0.02}
|
|
220
|
+
outlineColor="#07080b"
|
|
221
|
+
>
|
|
222
|
+
{label}
|
|
223
|
+
</Text>
|
|
224
|
+
</Suspense>
|
|
225
|
+
</group>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
186
229
|
function Gate({
|
|
187
230
|
x,
|
|
188
231
|
z,
|
|
189
232
|
faceX,
|
|
190
233
|
faceZ,
|
|
191
234
|
label,
|
|
235
|
+
backLabel,
|
|
192
236
|
}: {
|
|
193
237
|
x: number
|
|
194
238
|
z: number
|
|
195
239
|
faceX: number
|
|
196
240
|
faceZ: number
|
|
197
241
|
label: string
|
|
242
|
+
backLabel: string
|
|
198
243
|
}) {
|
|
199
244
|
const rotationY = Math.atan2(faceX, faceZ)
|
|
200
245
|
const postX = CONFIG.bridgeWidth / 2 + POST_W / 2
|
|
@@ -204,7 +249,6 @@ function Gate({
|
|
|
204
249
|
const capY = POST_H + LINTEL_H + CAP_H / 2
|
|
205
250
|
const signY = POST_H + LINTEL_H / 2
|
|
206
251
|
const signZ = POST_D / 2 + SIGN_D / 2 + 0.01
|
|
207
|
-
const fontSize = Math.min(0.42, (lintelW - 0.55) / Math.max(label.length * 0.62, 3))
|
|
208
252
|
|
|
209
253
|
return (
|
|
210
254
|
<group position={[x, 0, z]} rotation={[0, rotationY, 0]}>
|
|
@@ -224,25 +268,16 @@ function Gate({
|
|
|
224
268
|
<boxGeometry args={[capW, CAP_H, POST_D + 0.08]} />
|
|
225
269
|
<meshLambertMaterial color={CAP_COLOR} />
|
|
226
270
|
</mesh>
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
anchorY="middle"
|
|
238
|
-
maxWidth={lintelW - 0.4}
|
|
239
|
-
overflowWrap="break-word"
|
|
240
|
-
outlineWidth={0.02}
|
|
241
|
-
outlineColor="#07080b"
|
|
242
|
-
>
|
|
243
|
-
{label}
|
|
244
|
-
</Text>
|
|
245
|
-
</Suspense>
|
|
271
|
+
<GateSign z={signZ} rotY={0} signY={signY} lintelW={lintelW} label={label} />
|
|
272
|
+
{backLabel ? (
|
|
273
|
+
<GateSign
|
|
274
|
+
z={-signZ}
|
|
275
|
+
rotY={Math.PI}
|
|
276
|
+
signY={signY}
|
|
277
|
+
lintelW={lintelW}
|
|
278
|
+
label={backLabel}
|
|
279
|
+
/>
|
|
280
|
+
) : null}
|
|
246
281
|
</group>
|
|
247
282
|
)
|
|
248
283
|
}
|
|
@@ -292,6 +327,7 @@ export function Bridge({ bridge, folders }: BridgeProps) {
|
|
|
292
327
|
faceX={-startDir[0]}
|
|
293
328
|
faceZ={-startDir[1]}
|
|
294
329
|
label={bridge.label}
|
|
330
|
+
backLabel={bridge.fromLabel}
|
|
295
331
|
/>
|
|
296
332
|
)}
|
|
297
333
|
{end && endDir && (
|
|
@@ -301,6 +337,7 @@ export function Bridge({ bridge, folders }: BridgeProps) {
|
|
|
301
337
|
faceX={endDir[0]}
|
|
302
338
|
faceZ={endDir[1]}
|
|
303
339
|
label={bridge.fromLabel}
|
|
340
|
+
backLabel={bridge.label}
|
|
304
341
|
/>
|
|
305
342
|
)}
|
|
306
343
|
</group>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
2
|
-
import { useThree } from '@react-three/fiber'
|
|
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'
|
|
@@ -280,7 +280,7 @@ export function MapView({
|
|
|
280
280
|
key={folder.path}
|
|
281
281
|
position={[folder.x, 14, folder.z + 1.35]}
|
|
282
282
|
center
|
|
283
|
-
zIndexRange={[
|
|
283
|
+
zIndexRange={[80, 50]}
|
|
284
284
|
style={{ pointerEvents: 'none' }}
|
|
285
285
|
>
|
|
286
286
|
<div
|
|
@@ -307,7 +307,7 @@ export function MapView({
|
|
|
307
307
|
</div>
|
|
308
308
|
</Html>
|
|
309
309
|
))}
|
|
310
|
-
{marker && <LandMarker marker={marker} />}
|
|
310
|
+
{enabled && marker && <LandMarker marker={marker} />}
|
|
311
311
|
</>
|
|
312
312
|
)
|
|
313
313
|
}
|
|
@@ -323,12 +323,41 @@ function folderKindLabel(
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
function LandMarker({ marker }: { marker: [number, number] }) {
|
|
326
|
+
const camera = useThree((state) => state.camera)
|
|
327
|
+
const ring = useRef<THREE.Group>(null)
|
|
328
|
+
|
|
329
|
+
useFrame(() => {
|
|
330
|
+
if (!(camera instanceof THREE.OrthographicCamera) || !ring.current) return
|
|
331
|
+
const size = THREE.MathUtils.clamp(16 / Math.max(camera.zoom, 0.04), 2.4, 22)
|
|
332
|
+
ring.current.scale.setScalar(size)
|
|
333
|
+
})
|
|
334
|
+
|
|
326
335
|
return (
|
|
327
|
-
<group position={[marker[0], 0.
|
|
328
|
-
<
|
|
329
|
-
<
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
<group position={[marker[0], 0.35, marker[1]]}>
|
|
337
|
+
<group ref={ring}>
|
|
338
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
|
339
|
+
<circleGeometry args={[0.62, 32]} />
|
|
340
|
+
<meshBasicMaterial color="#e8c36a" transparent opacity={0.28} />
|
|
341
|
+
</mesh>
|
|
342
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.02, 0]}>
|
|
343
|
+
<ringGeometry args={[0.5, 0.72, 32]} />
|
|
344
|
+
<meshBasicMaterial color="#e8c36a" side={THREE.DoubleSide} />
|
|
345
|
+
</mesh>
|
|
346
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0.04, 0]}>
|
|
347
|
+
<circleGeometry args={[0.18, 20]} />
|
|
348
|
+
<meshBasicMaterial color="#fff6d4" />
|
|
349
|
+
</mesh>
|
|
350
|
+
</group>
|
|
351
|
+
<Html
|
|
352
|
+
center
|
|
353
|
+
zIndexRange={[20, 10]}
|
|
354
|
+
style={{ pointerEvents: 'none' }}
|
|
355
|
+
position={[0, 2.8, 0]}
|
|
356
|
+
>
|
|
357
|
+
<div className="map-you-are-here" role="img" aria-label="You are here">
|
|
358
|
+
<span className="map-you-are-here-pin" aria-hidden="true" />
|
|
359
|
+
</div>
|
|
360
|
+
</Html>
|
|
332
361
|
</group>
|
|
333
362
|
)
|
|
334
363
|
}
|