@jkwd/inbase 0.1.13 → 0.1.16
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 +258 -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 +74 -37
- 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/CanvasErrorBoundary.tsx +4 -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 +71 -5
- 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()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useMemo, useRef
|
|
2
|
-
import { createPortal } from 'react-dom'
|
|
1
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
3
2
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
4
3
|
import { Html, MapControls, OrthographicCamera } from '@react-three/drei'
|
|
5
4
|
import * as THREE from 'three'
|
|
@@ -19,6 +18,7 @@ type MapViewProps = {
|
|
|
19
18
|
marker: [number, number] | null
|
|
20
19
|
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
21
20
|
selectedFolder?: string | null
|
|
21
|
+
namingFolderPath?: string | null
|
|
22
22
|
onLand: (x: number, z: number) => void
|
|
23
23
|
onSelect: (fileId: string | null) => void
|
|
24
24
|
onSelectFolder: (folderPath: string | null) => void
|
|
@@ -32,6 +32,7 @@ export function MapView({
|
|
|
32
32
|
marker,
|
|
33
33
|
highlightedFolders,
|
|
34
34
|
selectedFolder = null,
|
|
35
|
+
namingFolderPath = null,
|
|
35
36
|
onLand,
|
|
36
37
|
onSelect,
|
|
37
38
|
onSelectFolder,
|
|
@@ -84,10 +85,9 @@ export function MapView({
|
|
|
84
85
|
const element = gl.domElement
|
|
85
86
|
element.style.cursor = 'grab'
|
|
86
87
|
|
|
87
|
-
const isWalkClick = (event: PointerEvent | MouseEvent) => event.
|
|
88
|
+
const isWalkClick = (event: PointerEvent | MouseEvent) => event.altKey
|
|
88
89
|
|
|
89
|
-
const isWalkButton = (event: PointerEvent) =>
|
|
90
|
-
event.button === 0 || (event.ctrlKey && event.button === 2)
|
|
90
|
+
const isWalkButton = (event: PointerEvent) => event.button === 0
|
|
91
91
|
|
|
92
92
|
const onDown = (event: PointerEvent) => {
|
|
93
93
|
if (!isWalkButton(event)) return
|
|
@@ -184,7 +184,7 @@ export function MapView({
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
const onContextMenu = (event: MouseEvent) => {
|
|
187
|
-
if (event.
|
|
187
|
+
if (event.altKey) {
|
|
188
188
|
event.preventDefault()
|
|
189
189
|
return
|
|
190
190
|
}
|
|
@@ -314,6 +314,7 @@ export function MapView({
|
|
|
314
314
|
folders={layout.folders}
|
|
315
315
|
highlightedFolders={highlightedFolders}
|
|
316
316
|
selectedFolder={selectedFolder}
|
|
317
|
+
namingFolderPath={namingFolderPath}
|
|
317
318
|
/>
|
|
318
319
|
)}
|
|
319
320
|
{enabled && marker && <LandMarker marker={marker} />}
|
|
@@ -323,6 +324,23 @@ export function MapView({
|
|
|
323
324
|
|
|
324
325
|
const PROJECT = new THREE.Vector3()
|
|
325
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
|
+
}
|
|
326
344
|
|
|
327
345
|
function folderLabelClass(
|
|
328
346
|
folder: PlacedFolder,
|
|
@@ -347,21 +365,50 @@ function MapFolderLabels({
|
|
|
347
365
|
folders,
|
|
348
366
|
highlightedFolders,
|
|
349
367
|
selectedFolder,
|
|
368
|
+
namingFolderPath,
|
|
350
369
|
}: {
|
|
351
370
|
folders: Record<string, PlacedFolder>
|
|
352
371
|
highlightedFolders?: Partial<Record<string, ChangeKind>>
|
|
353
372
|
selectedFolder: string | null
|
|
373
|
+
namingFolderPath: string | null
|
|
354
374
|
}) {
|
|
355
375
|
const camera = useThree((state) => state.camera)
|
|
356
376
|
const gl = useThree((state) => state.gl)
|
|
357
377
|
const size = useThree((state) => state.size)
|
|
358
|
-
const layerRef = useRef<HTMLDivElement>(null)
|
|
378
|
+
const layerRef = useRef<HTMLDivElement | null>(null)
|
|
359
379
|
const items = useMemo(() => Object.values(folders), [folders])
|
|
360
|
-
const [host, setHost] = useState<HTMLElement | null>(null)
|
|
361
380
|
|
|
362
381
|
useLayoutEffect(() => {
|
|
363
|
-
|
|
364
|
-
|
|
382
|
+
const parent = gl.domElement.parentElement
|
|
383
|
+
if (!parent) return
|
|
384
|
+
const layer = document.createElement('div')
|
|
385
|
+
layer.className = 'map-folder-label-layer'
|
|
386
|
+
layer.style.cssText =
|
|
387
|
+
'position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:80;background:transparent;'
|
|
388
|
+
for (const folder of items) {
|
|
389
|
+
const el = document.createElement('div')
|
|
390
|
+
el.className = folderLabelClass(folder, highlightedFolders, selectedFolder)
|
|
391
|
+
el.style.position = 'absolute'
|
|
392
|
+
el.style.top = '0'
|
|
393
|
+
el.style.left = '0'
|
|
394
|
+
el.style.visibility = 'hidden'
|
|
395
|
+
const name = document.createElement('span')
|
|
396
|
+
name.className = 'map-folder-name'
|
|
397
|
+
name.textContent = folderKindLabel(
|
|
398
|
+
folder.name,
|
|
399
|
+
highlightedFolders?.[folder.path] ?? null,
|
|
400
|
+
folder.added ?? false,
|
|
401
|
+
)
|
|
402
|
+
el.appendChild(name)
|
|
403
|
+
layer.appendChild(el)
|
|
404
|
+
}
|
|
405
|
+
parent.appendChild(layer)
|
|
406
|
+
layerRef.current = layer
|
|
407
|
+
return () => {
|
|
408
|
+
layer.remove()
|
|
409
|
+
layerRef.current = null
|
|
410
|
+
}
|
|
411
|
+
}, [gl, highlightedFolders, items, selectedFolder])
|
|
365
412
|
|
|
366
413
|
useFrame(() => {
|
|
367
414
|
const layer = layerRef.current
|
|
@@ -372,21 +419,31 @@ function MapFolderLabels({
|
|
|
372
419
|
const el = nodes[i] as HTMLElement | undefined
|
|
373
420
|
const folder = items[i]
|
|
374
421
|
if (!el || !folder) continue
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
378
432
|
const span = Math.max(folder.width, folder.depth) * zoom
|
|
379
433
|
const force =
|
|
380
434
|
selectedFolder === folder.path ||
|
|
381
435
|
Boolean(highlightedFolders?.[folder.path] || folder.added)
|
|
382
436
|
const onScreen =
|
|
383
|
-
|
|
384
|
-
PROJECT.z <= 1 &&
|
|
437
|
+
!screen.behind &&
|
|
385
438
|
x > -120 &&
|
|
386
439
|
x < size.width + 120 &&
|
|
387
440
|
y > -40 &&
|
|
388
441
|
y < size.height + 40
|
|
389
|
-
if (
|
|
442
|
+
if (
|
|
443
|
+
folder.path === namingFolderPath ||
|
|
444
|
+
!onScreen ||
|
|
445
|
+
(!force && span < MIN_FOLDER_LABEL_PX)
|
|
446
|
+
) {
|
|
390
447
|
if (el.style.visibility !== 'hidden') el.style.visibility = 'hidden'
|
|
391
448
|
continue
|
|
392
449
|
}
|
|
@@ -401,27 +458,7 @@ function MapFolderLabels({
|
|
|
401
458
|
}
|
|
402
459
|
})
|
|
403
460
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
return createPortal(
|
|
407
|
-
<div ref={layerRef} className="map-folder-label-layer">
|
|
408
|
-
{items.map((folder) => (
|
|
409
|
-
<div
|
|
410
|
-
key={folder.path}
|
|
411
|
-
className={folderLabelClass(folder, highlightedFolders, selectedFolder)}
|
|
412
|
-
>
|
|
413
|
-
<span className="map-folder-name">
|
|
414
|
-
{folderKindLabel(
|
|
415
|
-
folder.name,
|
|
416
|
-
highlightedFolders?.[folder.path] ?? null,
|
|
417
|
-
folder.added ?? false,
|
|
418
|
-
)}
|
|
419
|
-
</span>
|
|
420
|
-
</div>
|
|
421
|
-
))}
|
|
422
|
-
</div>,
|
|
423
|
-
host,
|
|
424
|
-
)
|
|
461
|
+
return null
|
|
425
462
|
}
|
|
426
463
|
|
|
427
464
|
function folderKindLabel(
|