@jkwd/inbase 0.1.15 → 0.1.17
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 +2 -2
- package/apps/explorer/scripts/branch-changes.mjs +2 -19
- package/apps/explorer/scripts/patch-lib.d.ts +3 -0
- package/apps/explorer/scripts/patch-lib.mjs +3 -0
- package/apps/explorer/scripts/scan-ignore.mjs +156 -0
- package/apps/explorer/scripts/scan-target.mjs +67 -29
- package/apps/explorer/scripts/session-store.d.ts +24 -3
- package/apps/explorer/scripts/session-store.mjs +180 -74
- package/apps/explorer/src/App.tsx +255 -147
- package/apps/explorer/src/agentIntent.ts +87 -5
- package/apps/explorer/src/index.css +106 -5
- package/apps/explorer/src/keyboard.ts +26 -0
- package/apps/explorer/src/scene/BlockPlacer.tsx +2 -11
- package/apps/explorer/src/scene/FileBlock.tsx +12 -4
- package/apps/explorer/src/scene/FolderArea.tsx +20 -0
- package/apps/explorer/src/scene/IslandPlacer.tsx +2 -11
- package/apps/explorer/src/scene/MapView.tsx +41 -10
- package/apps/explorer/src/scene/Player.tsx +9 -9
- package/apps/explorer/src/scene/SelectionThumbnail.tsx +2 -0
- package/apps/explorer/src/scene/World.tsx +46 -1
- package/apps/explorer/src/types.ts +28 -0
- package/apps/explorer/src/ui/HUD.tsx +435 -99
- package/apps/explorer/src/ui/MapContextMenu.tsx +2 -0
- package/apps/explorer/src/userCreated.ts +98 -0
- package/apps/explorer/vite.config.ts +101 -9
- package/bin/inbase.mjs +6 -5
- package/bin/project.mjs +30 -0
- package/bin/session.mjs +40 -12
- package/package.json +2 -1
- package/skill/commands/inbase.md +1 -1
- package/skill/inbase/SKILL.md +24 -22
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentIntent,
|
|
3
3
|
AgentIntentBundle,
|
|
4
|
+
BlueprintNote,
|
|
4
5
|
PatchImport,
|
|
5
6
|
PatchImportAddition,
|
|
6
7
|
PatchSymbolAddition,
|
|
@@ -8,7 +9,11 @@ import type {
|
|
|
8
9
|
UserCreatedIsland,
|
|
9
10
|
WorkflowAction,
|
|
10
11
|
} from './types'
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
parseBlueprintNotes,
|
|
14
|
+
parseUserCreatedBlocks,
|
|
15
|
+
parseUserCreatedIslands,
|
|
16
|
+
} from './userCreated'
|
|
12
17
|
|
|
13
18
|
function normalizeImports(value: unknown): PatchImport[] {
|
|
14
19
|
if (!Array.isArray(value)) return []
|
|
@@ -98,12 +103,15 @@ export const emptyIntent: AgentIntent = {
|
|
|
98
103
|
initialInstruction: null,
|
|
99
104
|
creationMode: false,
|
|
100
105
|
canEnterBlueprint: false,
|
|
106
|
+
blueprintHidden: false,
|
|
107
|
+
blueprintRevision: 0,
|
|
101
108
|
blueprintSessionId: null,
|
|
102
109
|
userCreatedBlocks: [],
|
|
103
110
|
userCreatedIslands: [],
|
|
104
111
|
blueprintFunctions: [],
|
|
105
112
|
blueprintVariables: [],
|
|
106
113
|
blueprintImports: [],
|
|
114
|
+
blueprintNotes: [],
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
|
|
@@ -149,6 +157,9 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
|
|
|
149
157
|
typeof data?.initialInstruction === 'string' ? data.initialInstruction : null,
|
|
150
158
|
creationMode: Boolean(data?.creationMode),
|
|
151
159
|
canEnterBlueprint: Boolean(data?.canEnterBlueprint),
|
|
160
|
+
blueprintHidden: Boolean(data?.blueprintHidden),
|
|
161
|
+
blueprintRevision:
|
|
162
|
+
typeof data?.blueprintRevision === 'number' ? data.blueprintRevision : 0,
|
|
152
163
|
blueprintSessionId:
|
|
153
164
|
typeof data?.blueprintSessionId === 'string'
|
|
154
165
|
? data.blueprintSessionId
|
|
@@ -158,18 +169,42 @@ function normalize(data: Partial<AgentIntent> | null | undefined): AgentIntent {
|
|
|
158
169
|
blueprintFunctions: normalizeSymbolAdditions(data?.blueprintFunctions),
|
|
159
170
|
blueprintVariables: normalizeSymbolAdditions(data?.blueprintVariables),
|
|
160
171
|
blueprintImports: normalizeImportAdditions(data?.blueprintImports),
|
|
172
|
+
blueprintNotes: parseBlueprintNotes(data?.blueprintNotes),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function normalizeBlueprint(data: Partial<AgentIntentBundle['blueprint']> | null | undefined) {
|
|
177
|
+
return {
|
|
178
|
+
hidden: Boolean(data?.hidden),
|
|
179
|
+
revision: typeof data?.revision === 'number' ? data.revision : 0,
|
|
180
|
+
enabled: Boolean(data?.enabled),
|
|
181
|
+
userCreatedBlocks: parseUserCreatedBlocks(data?.userCreatedBlocks),
|
|
182
|
+
userCreatedIslands: parseUserCreatedIslands(data?.userCreatedIslands),
|
|
183
|
+
addedFunctions: normalizeSymbolAdditions(data?.addedFunctions),
|
|
184
|
+
addedVariables: normalizeSymbolAdditions(data?.addedVariables),
|
|
185
|
+
addedImports: normalizeImportAdditions(data?.addedImports),
|
|
186
|
+
notes: parseBlueprintNotes(data?.notes),
|
|
161
187
|
}
|
|
162
188
|
}
|
|
163
189
|
|
|
164
190
|
export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
165
191
|
const query = new URLSearchParams({ t: String(Date.now()) })
|
|
166
192
|
const response = await fetch(`/api/agent-intent?${query}`)
|
|
167
|
-
|
|
193
|
+
const emptyBlueprint = normalizeBlueprint(null)
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
return {
|
|
196
|
+
focusedSessionId: null,
|
|
197
|
+
nextAttachSessionId: null,
|
|
198
|
+
intents: [],
|
|
199
|
+
blueprint: emptyBlueprint,
|
|
200
|
+
}
|
|
201
|
+
}
|
|
168
202
|
const data = (await response.json()) as {
|
|
169
203
|
focusedSessionId?: string | null
|
|
170
204
|
nextAttachSessionId?: string | null
|
|
171
205
|
intents?: unknown
|
|
172
206
|
sessionId?: string | null
|
|
207
|
+
blueprint?: Partial<AgentIntentBundle['blueprint']>
|
|
173
208
|
} & Partial<AgentIntent>
|
|
174
209
|
if (Array.isArray(data.intents)) {
|
|
175
210
|
return {
|
|
@@ -182,6 +217,7 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
|
182
217
|
intents: data.intents
|
|
183
218
|
.map((intent) => normalize(intent as Partial<AgentIntent>))
|
|
184
219
|
.filter((intent) => Boolean(intent.sessionId)),
|
|
220
|
+
blueprint: normalizeBlueprint(data.blueprint),
|
|
185
221
|
}
|
|
186
222
|
}
|
|
187
223
|
const intent = normalize(data)
|
|
@@ -189,6 +225,23 @@ export async function fetchAgentIntents(): Promise<AgentIntentBundle> {
|
|
|
189
225
|
focusedSessionId: intent.sessionId,
|
|
190
226
|
nextAttachSessionId: intent.awaitingAttach ? intent.sessionId : null,
|
|
191
227
|
intents: intent.sessionId ? [intent] : [],
|
|
228
|
+
blueprint: normalizeBlueprint(data.blueprint ?? {
|
|
229
|
+
hidden: intent.blueprintHidden,
|
|
230
|
+
revision: intent.blueprintRevision,
|
|
231
|
+
enabled:
|
|
232
|
+
intent.userCreatedBlocks.length > 0 ||
|
|
233
|
+
intent.userCreatedIslands.length > 0 ||
|
|
234
|
+
intent.blueprintFunctions.length > 0 ||
|
|
235
|
+
intent.blueprintVariables.length > 0 ||
|
|
236
|
+
intent.blueprintImports.length > 0 ||
|
|
237
|
+
intent.blueprintNotes.length > 0,
|
|
238
|
+
userCreatedBlocks: intent.userCreatedBlocks,
|
|
239
|
+
userCreatedIslands: intent.userCreatedIslands,
|
|
240
|
+
addedFunctions: intent.blueprintFunctions,
|
|
241
|
+
addedVariables: intent.blueprintVariables,
|
|
242
|
+
addedImports: intent.blueprintImports,
|
|
243
|
+
notes: intent.blueprintNotes,
|
|
244
|
+
}),
|
|
192
245
|
}
|
|
193
246
|
}
|
|
194
247
|
|
|
@@ -219,6 +272,7 @@ export async function performAgentAction(
|
|
|
219
272
|
addedFunctions?: PatchSymbolAddition[]
|
|
220
273
|
addedVariables?: PatchSymbolAddition[]
|
|
221
274
|
addedImports?: PatchImportAddition[]
|
|
275
|
+
notes?: BlueprintNote[]
|
|
222
276
|
} = {},
|
|
223
277
|
) {
|
|
224
278
|
const response = await fetch('/api/agent-intent', {
|
|
@@ -234,16 +288,17 @@ export async function performAgentAction(
|
|
|
234
288
|
}
|
|
235
289
|
|
|
236
290
|
export function persistSessionBlueprint(
|
|
237
|
-
sessionId: string,
|
|
291
|
+
sessionId: string | null | undefined,
|
|
238
292
|
payload: {
|
|
239
293
|
userCreatedBlocks: UserCreatedBlock[]
|
|
240
294
|
userCreatedIslands: UserCreatedIsland[]
|
|
241
295
|
addedFunctions?: PatchSymbolAddition[]
|
|
242
296
|
addedVariables?: PatchSymbolAddition[]
|
|
243
297
|
addedImports?: PatchImportAddition[]
|
|
298
|
+
notes?: BlueprintNote[]
|
|
244
299
|
},
|
|
245
300
|
) {
|
|
246
|
-
fetch('/api/agent-intent', {
|
|
301
|
+
return fetch('/api/agent-intent', {
|
|
247
302
|
method: 'POST',
|
|
248
303
|
headers: { 'Content-Type': 'application/json' },
|
|
249
304
|
body: JSON.stringify({
|
|
@@ -252,7 +307,34 @@ export function persistSessionBlueprint(
|
|
|
252
307
|
...payload,
|
|
253
308
|
}),
|
|
254
309
|
}).catch(() => {
|
|
255
|
-
// Keep local drafts if the
|
|
310
|
+
// Keep local drafts if the visualizer could not save the shared blueprint.
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function persistBlueprintHidden(hidden: boolean) {
|
|
315
|
+
return fetch('/api/agent-intent', {
|
|
316
|
+
method: 'POST',
|
|
317
|
+
headers: { 'Content-Type': 'application/json' },
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
action: 'blueprint_set_hidden',
|
|
320
|
+
hidden,
|
|
321
|
+
}),
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function persistBlueprintClear() {
|
|
326
|
+
return fetch('/api/agent-intent', {
|
|
327
|
+
method: 'POST',
|
|
328
|
+
headers: { 'Content-Type': 'application/json' },
|
|
329
|
+
body: JSON.stringify({ action: 'blueprint_clear' }),
|
|
330
|
+
})
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function persistBlueprintCleanup() {
|
|
334
|
+
return fetch('/api/agent-intent', {
|
|
335
|
+
method: 'POST',
|
|
336
|
+
headers: { 'Content-Type': 'application/json' },
|
|
337
|
+
body: JSON.stringify({ action: 'blueprint_cleanup' }),
|
|
256
338
|
})
|
|
257
339
|
}
|
|
258
340
|
|
|
@@ -188,6 +188,7 @@ button {
|
|
|
188
188
|
display: flex;
|
|
189
189
|
gap: 8px;
|
|
190
190
|
flex: none;
|
|
191
|
+
flex-wrap: wrap;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
.hud-chip,
|
|
@@ -1389,6 +1390,102 @@ button {
|
|
|
1389
1390
|
border-color: #9ad8ff;
|
|
1390
1391
|
}
|
|
1391
1392
|
|
|
1393
|
+
.hud-item-actions {
|
|
1394
|
+
display: flex;
|
|
1395
|
+
align-items: center;
|
|
1396
|
+
gap: 2px;
|
|
1397
|
+
flex: none;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
.hud-item-note {
|
|
1401
|
+
flex: none;
|
|
1402
|
+
padding: 2px 6px;
|
|
1403
|
+
color: #b7c0ce;
|
|
1404
|
+
background: transparent;
|
|
1405
|
+
border: 1px solid #3a4250;
|
|
1406
|
+
cursor: pointer;
|
|
1407
|
+
font-size: 11px;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.hud-item-note:hover,
|
|
1411
|
+
.hud-item-note:focus-visible,
|
|
1412
|
+
.hud-item-note[data-open='true'] {
|
|
1413
|
+
border-color: #9ad8ff;
|
|
1414
|
+
color: #d7eef8;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
.hud-item-note[data-has-note='true'],
|
|
1418
|
+
.hud-button[data-has-note='true'] {
|
|
1419
|
+
border-color: #5d9ec4;
|
|
1420
|
+
color: #d7eef8;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
.hud-note-overlay {
|
|
1424
|
+
position: absolute;
|
|
1425
|
+
inset: 0;
|
|
1426
|
+
z-index: 22;
|
|
1427
|
+
display: grid;
|
|
1428
|
+
place-items: center;
|
|
1429
|
+
padding: 24px;
|
|
1430
|
+
background: rgba(12, 14, 18, 0.82);
|
|
1431
|
+
pointer-events: auto;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
.hud-note-card {
|
|
1435
|
+
width: min(1400px, 100%);
|
|
1436
|
+
height: min(92dvh, 100%);
|
|
1437
|
+
display: flex;
|
|
1438
|
+
flex-direction: column;
|
|
1439
|
+
gap: 16px;
|
|
1440
|
+
padding: 24px 24px 20px;
|
|
1441
|
+
background: var(--vc-editor);
|
|
1442
|
+
border: 1px solid var(--vc-border);
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
.hud-note-header {
|
|
1446
|
+
display: flex;
|
|
1447
|
+
align-items: flex-start;
|
|
1448
|
+
justify-content: space-between;
|
|
1449
|
+
gap: 16px;
|
|
1450
|
+
flex: none;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.hud-note-heading {
|
|
1454
|
+
min-width: 0;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.hud-note-header h1 {
|
|
1458
|
+
margin: 0;
|
|
1459
|
+
font-size: 20px;
|
|
1460
|
+
font-weight: 600;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
.hud-note-subtitle {
|
|
1464
|
+
margin: 6px 0 0;
|
|
1465
|
+
color: #8b95a5;
|
|
1466
|
+
font-size: 13px;
|
|
1467
|
+
line-height: 1.4;
|
|
1468
|
+
word-break: break-all;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
.hud-note-field {
|
|
1472
|
+
width: 100%;
|
|
1473
|
+
min-height: 0;
|
|
1474
|
+
flex: 1;
|
|
1475
|
+
resize: none;
|
|
1476
|
+
box-sizing: border-box;
|
|
1477
|
+
padding: 16px;
|
|
1478
|
+
color: #e7ebf2;
|
|
1479
|
+
background: rgba(25, 28, 34, 0.96);
|
|
1480
|
+
border: 1px solid #3a4250;
|
|
1481
|
+
font: 15px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1482
|
+
outline: none;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
.hud-note-field:focus {
|
|
1486
|
+
border-color: #9ad8ff;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1392
1489
|
.hud-add-row {
|
|
1393
1490
|
display: flex;
|
|
1394
1491
|
gap: 6px;
|
|
@@ -1441,11 +1538,6 @@ button {
|
|
|
1441
1538
|
font-weight: 600;
|
|
1442
1539
|
}
|
|
1443
1540
|
|
|
1444
|
-
.block-name-overlay {
|
|
1445
|
-
z-index: 20 !important;
|
|
1446
|
-
pointer-events: auto;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
1541
|
.hud-name-gate {
|
|
1450
1542
|
position: absolute;
|
|
1451
1543
|
top: 42%;
|
|
@@ -1455,6 +1547,15 @@ button {
|
|
|
1455
1547
|
z-index: 4;
|
|
1456
1548
|
}
|
|
1457
1549
|
|
|
1550
|
+
.block-name-overlay {
|
|
1551
|
+
z-index: 90 !important;
|
|
1552
|
+
pointer-events: auto;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
.block-name-overlay-map .block-name-form {
|
|
1556
|
+
transform: translate(-50%, calc(-100% - 8px));
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1458
1559
|
.block-name-form {
|
|
1459
1560
|
pointer-events: auto;
|
|
1460
1561
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
let isolation = 0
|
|
2
|
+
|
|
3
|
+
export function beginKeyboardIsolation() {
|
|
4
|
+
isolation += 1
|
|
5
|
+
return () => {
|
|
6
|
+
isolation = Math.max(0, isolation - 1)
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isKeyboardIsolated() {
|
|
11
|
+
return isolation > 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isTypingInField(target: EventTarget | null) {
|
|
15
|
+
return (
|
|
16
|
+
target instanceof HTMLElement &&
|
|
17
|
+
(target.tagName === 'TEXTAREA' ||
|
|
18
|
+
target.tagName === 'INPUT' ||
|
|
19
|
+
target.tagName === 'SELECT' ||
|
|
20
|
+
target.isContentEditable)
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldIgnoreShortcut(event: KeyboardEvent) {
|
|
25
|
+
return isKeyboardIsolated() || isTypingInField(event.target)
|
|
26
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useEffect } from 'react'
|
|
|
2
2
|
import { useThree } from '@react-three/fiber'
|
|
3
3
|
import * as THREE from 'three'
|
|
4
4
|
import { folderAt } from '../layout'
|
|
5
|
+
import { shouldIgnoreShortcut } from '../keyboard'
|
|
5
6
|
import type { WorldLayout } from '../types'
|
|
6
7
|
|
|
7
8
|
type BlockPlacerProps = {
|
|
@@ -19,16 +20,6 @@ const forward = new THREE.Vector3()
|
|
|
19
20
|
const PLACE_MIN = 3
|
|
20
21
|
const PLACE_MAX = 22
|
|
21
22
|
|
|
22
|
-
function typingInField(target: EventTarget | null) {
|
|
23
|
-
return (
|
|
24
|
-
target instanceof HTMLElement &&
|
|
25
|
-
(target.tagName === 'TEXTAREA' ||
|
|
26
|
-
target.tagName === 'INPUT' ||
|
|
27
|
-
target.tagName === 'SELECT' ||
|
|
28
|
-
target.isContentEditable)
|
|
29
|
-
)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
23
|
function lookPoint(camera: THREE.Camera): { x: number; z: number } {
|
|
33
24
|
raycaster.setFromCamera(ndc, camera)
|
|
34
25
|
const reached = raycaster.ray.intersectPlane(ground, hit)
|
|
@@ -70,7 +61,7 @@ export function BlockPlacer({ enabled, layout, onPlace }: BlockPlacerProps) {
|
|
|
70
61
|
useEffect(() => {
|
|
71
62
|
const onKey = (event: KeyboardEvent) => {
|
|
72
63
|
if (!enabled || event.repeat || event.code !== 'Space') return
|
|
73
|
-
if (
|
|
64
|
+
if (shouldIgnoreShortcut(event)) return
|
|
74
65
|
event.preventDefault()
|
|
75
66
|
const { x, z } = lookPoint(camera)
|
|
76
67
|
const island =
|
|
@@ -193,11 +193,19 @@ export const FileBlock = memo(function FileBlock({
|
|
|
193
193
|
)}
|
|
194
194
|
{naming && onCommitName && onCancelName && (
|
|
195
195
|
<Html
|
|
196
|
-
position={
|
|
197
|
-
|
|
196
|
+
position={
|
|
197
|
+
mapMode
|
|
198
|
+
? [0, height / 2 + 0.04, -depth / 2]
|
|
199
|
+
: [0, height / 2 + 0.42, 0]
|
|
200
|
+
}
|
|
201
|
+
center={!mapMode}
|
|
198
202
|
occlude={false}
|
|
199
|
-
wrapperClass=
|
|
200
|
-
|
|
203
|
+
wrapperClass={
|
|
204
|
+
mapMode
|
|
205
|
+
? 'block-name-overlay block-name-overlay-map'
|
|
206
|
+
: 'block-name-overlay'
|
|
207
|
+
}
|
|
208
|
+
zIndexRange={[120, 0]}
|
|
201
209
|
>
|
|
202
210
|
<NameInput
|
|
203
211
|
placeholder="File name"
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../theme'
|
|
11
11
|
import type { PlacedFolder } from '../types'
|
|
12
12
|
import { MapSelectBorder } from './MapSelectBorder'
|
|
13
|
+
import { NameInput } from '../ui/NameInput'
|
|
13
14
|
|
|
14
15
|
type FolderAreaProps = {
|
|
15
16
|
folder: PlacedFolder
|
|
@@ -19,6 +20,8 @@ type FolderAreaProps = {
|
|
|
19
20
|
highlightKind?: ChangeKind | null
|
|
20
21
|
previewLabels?: boolean
|
|
21
22
|
labelVisible?: boolean
|
|
23
|
+
onCommitName?: (name: string) => void
|
|
24
|
+
onCancelName?: () => void
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export function FolderArea({
|
|
@@ -29,6 +32,8 @@ export function FolderArea({
|
|
|
29
32
|
highlightKind = null,
|
|
30
33
|
previewLabels = false,
|
|
31
34
|
labelVisible = true,
|
|
35
|
+
onCommitName,
|
|
36
|
+
onCancelName,
|
|
32
37
|
}: FolderAreaProps) {
|
|
33
38
|
const added = Boolean(folder.added)
|
|
34
39
|
const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
|
|
@@ -88,6 +93,21 @@ export function FolderArea({
|
|
|
88
93
|
<meshBasicMaterial color={aisle} />
|
|
89
94
|
</mesh>
|
|
90
95
|
)}
|
|
96
|
+
{naming && mapMode && onCommitName && onCancelName && (
|
|
97
|
+
<Html
|
|
98
|
+
position={[0, 1.35, -folder.depth / 2 + 1.35]}
|
|
99
|
+
center
|
|
100
|
+
occlude={false}
|
|
101
|
+
wrapperClass="block-name-overlay"
|
|
102
|
+
zIndexRange={[120, 0]}
|
|
103
|
+
>
|
|
104
|
+
<NameInput
|
|
105
|
+
placeholder="Folder name"
|
|
106
|
+
onCommit={onCommitName}
|
|
107
|
+
onCancel={onCancelName}
|
|
108
|
+
/>
|
|
109
|
+
</Html>
|
|
110
|
+
)}
|
|
91
111
|
<Suspense fallback={null}>
|
|
92
112
|
{!naming && previewLabels && labelVisible && (
|
|
93
113
|
<Html
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
2
|
import { useThree } from '@react-three/fiber'
|
|
3
3
|
import { folderAt } from '../layout'
|
|
4
|
+
import { shouldIgnoreShortcut } from '../keyboard'
|
|
4
5
|
import type { WorldLayout } from '../types'
|
|
5
6
|
|
|
6
7
|
type IslandPlacerProps = {
|
|
@@ -9,23 +10,13 @@ type IslandPlacerProps = {
|
|
|
9
10
|
onPlace: (parent: string) => void
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
function typingInField(target: EventTarget | null) {
|
|
13
|
-
return (
|
|
14
|
-
target instanceof HTMLElement &&
|
|
15
|
-
(target.tagName === 'TEXTAREA' ||
|
|
16
|
-
target.tagName === 'INPUT' ||
|
|
17
|
-
target.tagName === 'SELECT' ||
|
|
18
|
-
target.isContentEditable)
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
13
|
export function IslandPlacer({ enabled, layout, onPlace }: IslandPlacerProps) {
|
|
23
14
|
const { camera } = useThree()
|
|
24
15
|
|
|
25
16
|
useEffect(() => {
|
|
26
17
|
const onKey = (event: KeyboardEvent) => {
|
|
27
18
|
if (!enabled || event.repeat || event.code !== 'KeyB') return
|
|
28
|
-
if (
|
|
19
|
+
if (shouldIgnoreShortcut(event)) return
|
|
29
20
|
const island = folderAt(camera.position.x, camera.position.z, layout)
|
|
30
21
|
if (!island) return
|
|
31
22
|
event.preventDefault()
|
|
@@ -18,6 +18,7 @@ type MapViewProps = {
|
|
|
18
18
|
marker: [number, number] | null
|
|
19
19
|
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
20
20
|
selectedFolder?: string | null
|
|
21
|
+
namingFolderPath?: string | null
|
|
21
22
|
onLand: (x: number, z: number) => void
|
|
22
23
|
onSelect: (fileId: string | null) => void
|
|
23
24
|
onSelectFolder: (folderPath: string | null) => void
|
|
@@ -31,6 +32,7 @@ export function MapView({
|
|
|
31
32
|
marker,
|
|
32
33
|
highlightedFolders,
|
|
33
34
|
selectedFolder = null,
|
|
35
|
+
namingFolderPath = null,
|
|
34
36
|
onLand,
|
|
35
37
|
onSelect,
|
|
36
38
|
onSelectFolder,
|
|
@@ -83,10 +85,9 @@ export function MapView({
|
|
|
83
85
|
const element = gl.domElement
|
|
84
86
|
element.style.cursor = 'grab'
|
|
85
87
|
|
|
86
|
-
const isWalkClick = (event: PointerEvent | MouseEvent) => event.
|
|
88
|
+
const isWalkClick = (event: PointerEvent | MouseEvent) => event.altKey
|
|
87
89
|
|
|
88
|
-
const isWalkButton = (event: PointerEvent) =>
|
|
89
|
-
event.button === 0 || (event.ctrlKey && event.button === 2)
|
|
90
|
+
const isWalkButton = (event: PointerEvent) => event.button === 0
|
|
90
91
|
|
|
91
92
|
const onDown = (event: PointerEvent) => {
|
|
92
93
|
if (!isWalkButton(event)) return
|
|
@@ -183,7 +184,7 @@ export function MapView({
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
const onContextMenu = (event: MouseEvent) => {
|
|
186
|
-
if (event.
|
|
187
|
+
if (event.altKey) {
|
|
187
188
|
event.preventDefault()
|
|
188
189
|
return
|
|
189
190
|
}
|
|
@@ -313,6 +314,7 @@ export function MapView({
|
|
|
313
314
|
folders={layout.folders}
|
|
314
315
|
highlightedFolders={highlightedFolders}
|
|
315
316
|
selectedFolder={selectedFolder}
|
|
317
|
+
namingFolderPath={namingFolderPath}
|
|
316
318
|
/>
|
|
317
319
|
)}
|
|
318
320
|
{enabled && marker && <LandMarker marker={marker} />}
|
|
@@ -322,6 +324,23 @@ export function MapView({
|
|
|
322
324
|
|
|
323
325
|
const PROJECT = new THREE.Vector3()
|
|
324
326
|
const MIN_FOLDER_LABEL_PX = 28
|
|
327
|
+
const FOLDER_ENTRANCE_Z = 1.35
|
|
328
|
+
|
|
329
|
+
function projectToScreen(
|
|
330
|
+
x: number,
|
|
331
|
+
y: number,
|
|
332
|
+
z: number,
|
|
333
|
+
camera: THREE.Camera,
|
|
334
|
+
width: number,
|
|
335
|
+
height: number,
|
|
336
|
+
) {
|
|
337
|
+
PROJECT.set(x, y, z).project(camera)
|
|
338
|
+
return {
|
|
339
|
+
x: (PROJECT.x * 0.5 + 0.5) * width,
|
|
340
|
+
y: (-PROJECT.y * 0.5 + 0.5) * height,
|
|
341
|
+
behind: PROJECT.z < -1 || PROJECT.z > 1,
|
|
342
|
+
}
|
|
343
|
+
}
|
|
325
344
|
|
|
326
345
|
function folderLabelClass(
|
|
327
346
|
folder: PlacedFolder,
|
|
@@ -346,10 +365,12 @@ function MapFolderLabels({
|
|
|
346
365
|
folders,
|
|
347
366
|
highlightedFolders,
|
|
348
367
|
selectedFolder,
|
|
368
|
+
namingFolderPath,
|
|
349
369
|
}: {
|
|
350
370
|
folders: Record<string, PlacedFolder>
|
|
351
371
|
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
352
372
|
selectedFolder: string | null
|
|
373
|
+
namingFolderPath: string | null
|
|
353
374
|
}) {
|
|
354
375
|
const camera = useThree((state) => state.camera)
|
|
355
376
|
const gl = useThree((state) => state.gl)
|
|
@@ -398,21 +419,31 @@ function MapFolderLabels({
|
|
|
398
419
|
const el = nodes[i] as HTMLElement | undefined
|
|
399
420
|
const folder = items[i]
|
|
400
421
|
if (!el || !folder) continue
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
422
|
+
const screen = projectToScreen(
|
|
423
|
+
folder.x,
|
|
424
|
+
14,
|
|
425
|
+
folder.z + FOLDER_ENTRANCE_Z,
|
|
426
|
+
camera,
|
|
427
|
+
size.width,
|
|
428
|
+
size.height,
|
|
429
|
+
)
|
|
430
|
+
const x = screen.x
|
|
431
|
+
const y = screen.y
|
|
404
432
|
const span = Math.max(folder.width, folder.depth) * zoom
|
|
405
433
|
const force =
|
|
406
434
|
selectedFolder === folder.path ||
|
|
407
435
|
Boolean(highlightedFolders?.[folder.path] || folder.added)
|
|
408
436
|
const onScreen =
|
|
409
|
-
|
|
410
|
-
PROJECT.z <= 1 &&
|
|
437
|
+
!screen.behind &&
|
|
411
438
|
x > -120 &&
|
|
412
439
|
x < size.width + 120 &&
|
|
413
440
|
y > -40 &&
|
|
414
441
|
y < size.height + 40
|
|
415
|
-
if (
|
|
442
|
+
if (
|
|
443
|
+
folder.path === namingFolderPath ||
|
|
444
|
+
!onScreen ||
|
|
445
|
+
(!force && span < MIN_FOLDER_LABEL_PX)
|
|
446
|
+
) {
|
|
416
447
|
if (el.style.visibility !== 'hidden') el.style.visibility = 'hidden'
|
|
417
448
|
continue
|
|
418
449
|
}
|
|
@@ -4,6 +4,7 @@ import { PerspectiveCamera, PointerLockControls } from '@react-three/drei'
|
|
|
4
4
|
import type { PointerLockControls as PointerLockControlsImpl } from 'three-stdlib'
|
|
5
5
|
import * as THREE from 'three'
|
|
6
6
|
import { CONFIG } from '../theme'
|
|
7
|
+
import { isKeyboardIsolated, shouldIgnoreShortcut } from '../keyboard'
|
|
7
8
|
import type { FlyTo, ViewMode, WorldLayout } from '../types'
|
|
8
9
|
|
|
9
10
|
const lookDir = new THREE.Vector3()
|
|
@@ -143,15 +144,7 @@ export function Player({
|
|
|
143
144
|
|
|
144
145
|
useEffect(() => {
|
|
145
146
|
const onKey = (event: KeyboardEvent, down: boolean) => {
|
|
146
|
-
if (
|
|
147
|
-
event.target instanceof HTMLElement &&
|
|
148
|
-
(event.target.tagName === 'TEXTAREA' ||
|
|
149
|
-
event.target.tagName === 'INPUT' ||
|
|
150
|
-
event.target.tagName === 'SELECT' ||
|
|
151
|
-
event.target.isContentEditable)
|
|
152
|
-
) {
|
|
153
|
-
return
|
|
154
|
-
}
|
|
147
|
+
if (shouldIgnoreShortcut(event)) return
|
|
155
148
|
if (event.code === 'KeyW' || event.code === 'ArrowUp') keys.current.forward = down
|
|
156
149
|
if (event.code === 'KeyS' || event.code === 'ArrowDown') keys.current.back = down
|
|
157
150
|
if (event.code === 'KeyA' || event.code === 'ArrowLeft') keys.current.left = down
|
|
@@ -235,6 +228,13 @@ export function Player({
|
|
|
235
228
|
setSteering(true)
|
|
236
229
|
}
|
|
237
230
|
} else if (walking && locked) {
|
|
231
|
+
if (isKeyboardIsolated()) {
|
|
232
|
+
keys.current.forward = false
|
|
233
|
+
keys.current.back = false
|
|
234
|
+
keys.current.left = false
|
|
235
|
+
keys.current.right = false
|
|
236
|
+
keys.current.sprint = false
|
|
237
|
+
}
|
|
238
238
|
camera.getWorldDirection(front.current)
|
|
239
239
|
front.current.y = 0
|
|
240
240
|
front.current.normalize()
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
folderOfFile,
|
|
10
10
|
} from '../layout'
|
|
11
11
|
import { CONFIG, WORLD_VOID, type ChangeKind } from '../theme'
|
|
12
|
+
import { shouldIgnoreShortcut } from '../keyboard'
|
|
12
13
|
import type { CodebaseGraph, PlacedFile, PlacedFolder, WorldLayout } from '../types'
|
|
13
14
|
import { FileBlock } from './FileBlock'
|
|
14
15
|
import { FolderArea } from './FolderArea'
|
|
@@ -903,6 +904,7 @@ export function SelectionThumbnail({
|
|
|
903
904
|
if (!maximized) return
|
|
904
905
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
905
906
|
if (event.key !== 'Escape') return
|
|
907
|
+
if (shouldIgnoreShortcut(event)) return
|
|
906
908
|
event.preventDefault()
|
|
907
909
|
onMaximize?.()
|
|
908
910
|
}
|