@jkwd/inbase 0.1.0
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 +76 -0
- package/apps/explorer/index.html +12 -0
- package/apps/explorer/package.json +28 -0
- package/apps/explorer/scripts/js-source.mjs +188 -0
- package/apps/explorer/scripts/patch-lib.d.ts +115 -0
- package/apps/explorer/scripts/patch-lib.mjs +472 -0
- package/apps/explorer/scripts/scan-target.mjs +188 -0
- package/apps/explorer/scripts/session-store.d.ts +156 -0
- package/apps/explorer/scripts/session-store.mjs +809 -0
- package/apps/explorer/scripts/target-config.d.ts +8 -0
- package/apps/explorer/scripts/target-config.mjs +42 -0
- package/apps/explorer/src/App.tsx +941 -0
- package/apps/explorer/src/agentIntent.ts +182 -0
- package/apps/explorer/src/codebase.ts +15 -0
- package/apps/explorer/src/index.css +632 -0
- package/apps/explorer/src/layout.ts +508 -0
- package/apps/explorer/src/main.tsx +16 -0
- package/apps/explorer/src/scene/BlockPlacer.tsx +87 -0
- package/apps/explorer/src/scene/Bridge.tsx +290 -0
- package/apps/explorer/src/scene/FileBlock.tsx +256 -0
- package/apps/explorer/src/scene/FolderArea.tsx +96 -0
- package/apps/explorer/src/scene/IslandPlacer.tsx +40 -0
- package/apps/explorer/src/scene/MapSelectBorder.tsx +57 -0
- package/apps/explorer/src/scene/MapView.tsx +247 -0
- package/apps/explorer/src/scene/Player.tsx +245 -0
- package/apps/explorer/src/scene/RelationLines.tsx +223 -0
- package/apps/explorer/src/scene/SelectionController.tsx +89 -0
- package/apps/explorer/src/scene/UserContextTracker.tsx +152 -0
- package/apps/explorer/src/scene/World.tsx +323 -0
- package/apps/explorer/src/theme.ts +111 -0
- package/apps/explorer/src/types.ts +245 -0
- package/apps/explorer/src/ui/CanvasErrorBoundary.tsx +38 -0
- package/apps/explorer/src/ui/HUD.tsx +1090 -0
- package/apps/explorer/src/ui/NameInput.tsx +45 -0
- package/apps/explorer/src/userContext.ts +73 -0
- package/apps/explorer/src/userCreated.ts +354 -0
- package/apps/explorer/src/vite-env.d.ts +1 -0
- package/apps/explorer/tsconfig.json +21 -0
- package/apps/explorer/vite.config.ts +295 -0
- package/bin/inbase.mjs +170 -0
- package/bin/project.mjs +94 -0
- package/bin/session.mjs +241 -0
- package/package.json +63 -0
- package/skill/inbase/SKILL.md +167 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import type { AimedRelation, FileNode, PlacedFile, PatchImport, WorldLayout } from '../types'
|
|
4
|
+
|
|
5
|
+
const SELECTED_COLOR = '#6ad2ff'
|
|
6
|
+
const SELECTED_AIM = '#e7f7ff'
|
|
7
|
+
const IMPORTED_BY_COLOR = '#b57bff'
|
|
8
|
+
const IMPORTED_BY_AIM = '#ecd9ff'
|
|
9
|
+
const PATCH_COLOR = '#f0d24a'
|
|
10
|
+
const PATCH_AIM = '#fff4c2'
|
|
11
|
+
const UP = new THREE.Vector3(0, 1, 0)
|
|
12
|
+
|
|
13
|
+
type RelationLinesProps = {
|
|
14
|
+
selectedId: string | null
|
|
15
|
+
aimedRelation: AimedRelation | null
|
|
16
|
+
files: FileNode[]
|
|
17
|
+
layout: WorldLayout
|
|
18
|
+
extras?: Record<string, PlacedFile>
|
|
19
|
+
plannedEdges?: PatchImport[]
|
|
20
|
+
fromAbove?: boolean
|
|
21
|
+
importedBy?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type ArrowHead = {
|
|
25
|
+
position: THREE.Vector3
|
|
26
|
+
quaternion: THREE.Quaternion
|
|
27
|
+
radius: number
|
|
28
|
+
height: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type LineMesh = {
|
|
32
|
+
id: string
|
|
33
|
+
from: string
|
|
34
|
+
to: string
|
|
35
|
+
planned: boolean
|
|
36
|
+
geometry: THREE.TubeGeometry
|
|
37
|
+
arrows: ArrowHead[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fileTop(file: PlacedFile) {
|
|
41
|
+
return new THREE.Vector3(
|
|
42
|
+
file.position[0],
|
|
43
|
+
file.position[1] + file.size[1] / 2,
|
|
44
|
+
file.position[2],
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function arcCurve(start: THREE.Vector3, end: THREE.Vector3) {
|
|
49
|
+
const mid = start.clone().lerp(end, 0.5)
|
|
50
|
+
const dx = end.x - start.x
|
|
51
|
+
const dz = end.z - start.z
|
|
52
|
+
const length = Math.hypot(dx, dz) || 1
|
|
53
|
+
const bow = Math.min(5.5, length * 0.14)
|
|
54
|
+
mid.x += (-dz / length) * bow
|
|
55
|
+
mid.z += (dx / length) * bow
|
|
56
|
+
mid.y = Math.max(start.y, end.y) + 2.4 + length * 0.06
|
|
57
|
+
return new THREE.QuadraticBezierCurve3(start, mid, end)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function arrowAt(
|
|
61
|
+
curve: THREE.QuadraticBezierCurve3,
|
|
62
|
+
t: number,
|
|
63
|
+
radius: number,
|
|
64
|
+
height: number,
|
|
65
|
+
towardImported: boolean,
|
|
66
|
+
): ArrowHead {
|
|
67
|
+
const tangent = curve.getTangentAt(t)
|
|
68
|
+
if (tangent.lengthSq() < 1e-8) tangent.set(0, 1, 0)
|
|
69
|
+
else tangent.normalize()
|
|
70
|
+
if (!towardImported) tangent.negate()
|
|
71
|
+
return {
|
|
72
|
+
position: curve.getPointAt(t),
|
|
73
|
+
quaternion: new THREE.Quaternion().setFromUnitVectors(UP, tangent),
|
|
74
|
+
radius,
|
|
75
|
+
height,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function arrowHeads(
|
|
80
|
+
curve: THREE.QuadraticBezierCurve3,
|
|
81
|
+
radius: number,
|
|
82
|
+
fromAbove: boolean,
|
|
83
|
+
towardImported: boolean,
|
|
84
|
+
): ArrowHead[] {
|
|
85
|
+
const height = Math.max(radius * 6.5, fromAbove ? 0.9 : 0.42)
|
|
86
|
+
const coneRadius = Math.max(radius * 2.8, fromAbove ? 0.32 : 0.15)
|
|
87
|
+
return [
|
|
88
|
+
arrowAt(curve, 0.18, coneRadius, height, towardImported),
|
|
89
|
+
arrowAt(curve, 0.82, coneRadius, height, towardImported),
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function edgeMesh(
|
|
94
|
+
from: PlacedFile,
|
|
95
|
+
to: PlacedFile,
|
|
96
|
+
toId: string,
|
|
97
|
+
planned: boolean,
|
|
98
|
+
radius: number,
|
|
99
|
+
fromAbove: boolean,
|
|
100
|
+
towardImported: boolean,
|
|
101
|
+
): LineMesh {
|
|
102
|
+
const curve = arcCurve(fileTop(from), fileTop(to))
|
|
103
|
+
return {
|
|
104
|
+
id: `${from.id}->${toId}`,
|
|
105
|
+
from: from.id,
|
|
106
|
+
to: toId,
|
|
107
|
+
planned,
|
|
108
|
+
geometry: new THREE.TubeGeometry(curve, 28, radius, 6, false),
|
|
109
|
+
arrows: arrowHeads(curve, radius, fromAbove, towardImported),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function RelationLines({
|
|
114
|
+
selectedId,
|
|
115
|
+
aimedRelation,
|
|
116
|
+
files,
|
|
117
|
+
layout,
|
|
118
|
+
extras = {},
|
|
119
|
+
plannedEdges = [],
|
|
120
|
+
fromAbove = false,
|
|
121
|
+
importedBy = false,
|
|
122
|
+
}: RelationLinesProps) {
|
|
123
|
+
const meshes = useMemo(() => {
|
|
124
|
+
const placed = { ...layout.files, ...extras }
|
|
125
|
+
const selectedRadius = fromAbove ? 0.2 : 0.07
|
|
126
|
+
const plannedRadius = fromAbove ? 0.26 : 0.1
|
|
127
|
+
const lines: LineMesh[] = []
|
|
128
|
+
const plannedKeys = new Set<string>()
|
|
129
|
+
|
|
130
|
+
for (const edge of plannedEdges) {
|
|
131
|
+
const from = placed[edge.from]
|
|
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))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (selectedId) {
|
|
141
|
+
const selected = placed[selectedId]
|
|
142
|
+
if (selected) {
|
|
143
|
+
if (importedBy) {
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
if (file.id === selectedId || !file.imports.includes(selectedId)) continue
|
|
146
|
+
if (plannedKeys.has(`${file.id}->${selectedId}`)) continue
|
|
147
|
+
const importer = placed[file.id]
|
|
148
|
+
if (!importer) continue
|
|
149
|
+
lines.push(
|
|
150
|
+
edgeMesh(importer, selected, selectedId, false, selectedRadius, fromAbove, false),
|
|
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
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return lines
|
|
170
|
+
}, [extras, files, fromAbove, importedBy, layout.files, plannedEdges, selectedId])
|
|
171
|
+
|
|
172
|
+
if (meshes.length === 0) return null
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<group>
|
|
176
|
+
{meshes.map((mesh) => {
|
|
177
|
+
const aimed =
|
|
178
|
+
aimedRelation?.from === mesh.from && aimedRelation?.to === mesh.to
|
|
179
|
+
const color = mesh.planned
|
|
180
|
+
? aimed
|
|
181
|
+
? PATCH_AIM
|
|
182
|
+
: PATCH_COLOR
|
|
183
|
+
: importedBy
|
|
184
|
+
? aimed
|
|
185
|
+
? IMPORTED_BY_AIM
|
|
186
|
+
: IMPORTED_BY_COLOR
|
|
187
|
+
: aimed
|
|
188
|
+
? SELECTED_AIM
|
|
189
|
+
: SELECTED_COLOR
|
|
190
|
+
const glow = aimed ? 2.4 : mesh.planned ? 1.7 : 1.4
|
|
191
|
+
const relation = { relationFrom: mesh.from, relationTo: mesh.to }
|
|
192
|
+
return (
|
|
193
|
+
<group key={mesh.id}>
|
|
194
|
+
<mesh geometry={mesh.geometry} userData={relation}>
|
|
195
|
+
<meshStandardMaterial
|
|
196
|
+
color={color}
|
|
197
|
+
emissive={color}
|
|
198
|
+
emissiveIntensity={glow}
|
|
199
|
+
roughness={0.3}
|
|
200
|
+
/>
|
|
201
|
+
</mesh>
|
|
202
|
+
{mesh.arrows.map((arrow, index) => (
|
|
203
|
+
<mesh
|
|
204
|
+
key={`${mesh.id}-arrow-${index}`}
|
|
205
|
+
position={arrow.position}
|
|
206
|
+
quaternion={arrow.quaternion}
|
|
207
|
+
userData={relation}
|
|
208
|
+
>
|
|
209
|
+
<coneGeometry args={[arrow.radius, arrow.height, 10]} />
|
|
210
|
+
<meshStandardMaterial
|
|
211
|
+
color={color}
|
|
212
|
+
emissive={color}
|
|
213
|
+
emissiveIntensity={glow}
|
|
214
|
+
roughness={0.3}
|
|
215
|
+
/>
|
|
216
|
+
</mesh>
|
|
217
|
+
))}
|
|
218
|
+
</group>
|
|
219
|
+
)
|
|
220
|
+
})}
|
|
221
|
+
</group>
|
|
222
|
+
)
|
|
223
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { relationTravelTarget } from '../layout'
|
|
5
|
+
import type { AimedRelation, PlacedFile } from '../types'
|
|
6
|
+
|
|
7
|
+
type SelectionControllerProps = {
|
|
8
|
+
locked: boolean
|
|
9
|
+
files: Record<string, PlacedFile>
|
|
10
|
+
onSelect: (fileId: string | null) => void
|
|
11
|
+
onAimRelation?: (aim: AimedRelation | null) => void
|
|
12
|
+
onTravelTo: (fromId: string, toId: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ndc = new THREE.Vector2(0, 0)
|
|
16
|
+
const raycaster = new THREE.Raycaster()
|
|
17
|
+
|
|
18
|
+
function aimedRelation(
|
|
19
|
+
hits: THREE.Intersection[],
|
|
20
|
+
origin: THREE.Vector3,
|
|
21
|
+
files: Record<string, PlacedFile>,
|
|
22
|
+
): AimedRelation | null {
|
|
23
|
+
const relation = hits.find((item) => item.object.userData.relationTo)
|
|
24
|
+
const file = hits.find((item) => item.object.userData.fileId)
|
|
25
|
+
if (!relation || (file && relation.distance > file.distance + 0.4)) return null
|
|
26
|
+
const fromId = relation.object.userData.relationFrom as string
|
|
27
|
+
const toId = relation.object.userData.relationTo as string
|
|
28
|
+
return {
|
|
29
|
+
from: fromId,
|
|
30
|
+
to: toId,
|
|
31
|
+
flyTo: relationTravelTarget(fromId, toId, origin.x, origin.z, files),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function aimKey(aim: AimedRelation | null) {
|
|
36
|
+
return aim ? `${aim.from}->${aim.to}:${aim.flyTo}` : ''
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function SelectionController({
|
|
40
|
+
locked,
|
|
41
|
+
files,
|
|
42
|
+
onSelect,
|
|
43
|
+
onAimRelation,
|
|
44
|
+
onTravelTo,
|
|
45
|
+
}: SelectionControllerProps) {
|
|
46
|
+
const { camera, scene } = useThree()
|
|
47
|
+
const lastAim = useRef('')
|
|
48
|
+
|
|
49
|
+
useFrame(() => {
|
|
50
|
+
if (!onAimRelation) return
|
|
51
|
+
if (!locked) {
|
|
52
|
+
if (lastAim.current) {
|
|
53
|
+
lastAim.current = ''
|
|
54
|
+
onAimRelation(null)
|
|
55
|
+
}
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
raycaster.setFromCamera(ndc, camera)
|
|
59
|
+
const next = aimedRelation(
|
|
60
|
+
raycaster.intersectObjects(scene.children, true),
|
|
61
|
+
camera.position,
|
|
62
|
+
files,
|
|
63
|
+
)
|
|
64
|
+
const key = aimKey(next)
|
|
65
|
+
if (key === lastAim.current) return
|
|
66
|
+
lastAim.current = key
|
|
67
|
+
onAimRelation(next)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const onClick = () => {
|
|
72
|
+
if (!locked) return
|
|
73
|
+
raycaster.setFromCamera(ndc, camera)
|
|
74
|
+
const hits = raycaster.intersectObjects(scene.children, true)
|
|
75
|
+
const aim = aimedRelation(hits, camera.position, files)
|
|
76
|
+
if (aim) {
|
|
77
|
+
onTravelTo(aim.from, aim.to)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const file = hits.find((item) => item.object.userData.fileId)
|
|
81
|
+
onSelect(file ? (file.object.userData.fileId as string) : null)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
window.addEventListener('click', onClick)
|
|
85
|
+
return () => window.removeEventListener('click', onClick)
|
|
86
|
+
}, [camera, files, locked, onSelect, onTravelTo, scene])
|
|
87
|
+
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useRef } from 'react'
|
|
2
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { folderAt } from '../layout'
|
|
5
|
+
import { fileById, toFileRef } from '../userContext'
|
|
6
|
+
import { namedCreatedBlocks, namedCreatedIslands } from '../userCreated'
|
|
7
|
+
import type { CodebaseGraph, UserContext, UserCreatedBlock, UserCreatedIsland, UserFileRef, ViewMode, WorldLayout } from '../types'
|
|
8
|
+
|
|
9
|
+
type UserContextTrackerProps = {
|
|
10
|
+
graph: CodebaseGraph
|
|
11
|
+
layout: WorldLayout
|
|
12
|
+
mode: ViewMode
|
|
13
|
+
selectedId: string | null
|
|
14
|
+
userCreatedBlocks?: UserCreatedBlock[]
|
|
15
|
+
userCreatedIslands?: UserCreatedIsland[]
|
|
16
|
+
onContext: (context: UserContext) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ndc = new THREE.Vector2(0, 0)
|
|
20
|
+
const raycaster = new THREE.Raycaster()
|
|
21
|
+
const look = new THREE.Vector3()
|
|
22
|
+
const toward = new THREE.Vector3()
|
|
23
|
+
|
|
24
|
+
function filesOnIsland(graph: CodebaseGraph, islandPath: string | null): UserFileRef[] {
|
|
25
|
+
if (!islandPath) return []
|
|
26
|
+
const folder = graph.folders.find((item) => item.path === islandPath)
|
|
27
|
+
if (!folder) return []
|
|
28
|
+
return folder.files
|
|
29
|
+
.map((id) => fileById(graph, id))
|
|
30
|
+
.filter((file): file is NonNullable<typeof file> => Boolean(file))
|
|
31
|
+
.map(toFileRef)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function filesInLookDirection(
|
|
35
|
+
graph: CodebaseGraph,
|
|
36
|
+
layout: WorldLayout,
|
|
37
|
+
origin: THREE.Vector3,
|
|
38
|
+
direction: THREE.Vector3,
|
|
39
|
+
): UserFileRef[] {
|
|
40
|
+
const seen: { score: number; file: UserFileRef }[] = []
|
|
41
|
+
|
|
42
|
+
for (const file of graph.files) {
|
|
43
|
+
const placed = layout.files[file.id]
|
|
44
|
+
if (!placed) continue
|
|
45
|
+
toward.set(placed.position[0], placed.position[1], placed.position[2]).sub(origin)
|
|
46
|
+
const distance = toward.length()
|
|
47
|
+
if (distance > 24 || distance < 0.35) continue
|
|
48
|
+
toward.y = 0
|
|
49
|
+
if (toward.lengthSq() === 0) continue
|
|
50
|
+
toward.normalize()
|
|
51
|
+
const alignment = toward.dot(direction)
|
|
52
|
+
if (alignment < 0.62) continue
|
|
53
|
+
seen.push({
|
|
54
|
+
score: alignment * 8 - distance * 0.08,
|
|
55
|
+
file: toFileRef(file),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
seen.sort((a, b) => b.score - a.score)
|
|
60
|
+
return seen.slice(0, 8).map((item) => item.file)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function UserContextTracker({
|
|
64
|
+
graph,
|
|
65
|
+
layout,
|
|
66
|
+
mode,
|
|
67
|
+
selectedId,
|
|
68
|
+
userCreatedBlocks = [],
|
|
69
|
+
userCreatedIslands = [],
|
|
70
|
+
onContext,
|
|
71
|
+
}: UserContextTrackerProps) {
|
|
72
|
+
const { camera, scene } = useThree()
|
|
73
|
+
const lastKey = useRef('')
|
|
74
|
+
|
|
75
|
+
const lastIsland = useRef<ReturnType<typeof folderAt>>(null)
|
|
76
|
+
|
|
77
|
+
useFrame(() => {
|
|
78
|
+
try {
|
|
79
|
+
const walking = mode === 'walk'
|
|
80
|
+
const island = walking
|
|
81
|
+
? folderAt(camera.position.x, camera.position.z, layout)
|
|
82
|
+
: lastIsland.current
|
|
83
|
+
if (walking && island) lastIsland.current = island
|
|
84
|
+
|
|
85
|
+
let lookingAtId: string | null = selectedId
|
|
86
|
+
if (walking) {
|
|
87
|
+
raycaster.setFromCamera(ndc, camera)
|
|
88
|
+
const hit = raycaster
|
|
89
|
+
.intersectObjects(scene.children, true)
|
|
90
|
+
.find((item) => item.object.userData.fileId)
|
|
91
|
+
lookingAtId = hit ? (hit.object.userData.fileId as string) : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
camera.getWorldDirection(look)
|
|
95
|
+
look.y = 0
|
|
96
|
+
if (look.lengthSq() > 0) look.normalize()
|
|
97
|
+
|
|
98
|
+
const gazed = fileById(graph, lookingAtId)
|
|
99
|
+
const lookingAtFiles = walking
|
|
100
|
+
? filesInLookDirection(graph, layout, camera.position, look)
|
|
101
|
+
: gazed
|
|
102
|
+
? [toFileRef(gazed)]
|
|
103
|
+
: []
|
|
104
|
+
|
|
105
|
+
const lookingAt = gazed ?? fileById(graph, lookingAtFiles[0]?.id ?? null)
|
|
106
|
+
const selected = fileById(graph, selectedId)
|
|
107
|
+
const islandPath = island?.path ?? selected?.folder ?? null
|
|
108
|
+
const islandName =
|
|
109
|
+
island?.name ??
|
|
110
|
+
graph.folders.find((folder) => folder.path === islandPath)?.name ??
|
|
111
|
+
'open ground'
|
|
112
|
+
|
|
113
|
+
const namedBlocks = namedCreatedBlocks(userCreatedBlocks)
|
|
114
|
+
const namedIslands = namedCreatedIslands(userCreatedIslands)
|
|
115
|
+
const key = [
|
|
116
|
+
mode,
|
|
117
|
+
islandPath ?? '',
|
|
118
|
+
lookingAt?.id ?? '',
|
|
119
|
+
selected?.id ?? '',
|
|
120
|
+
lookingAtFiles.map((file) => file.id).join(','),
|
|
121
|
+
namedBlocks.map((block) => block.id).join(','),
|
|
122
|
+
namedIslands.map((island) => island.id).join(','),
|
|
123
|
+
].join('|')
|
|
124
|
+
|
|
125
|
+
if (key === lastKey.current) return
|
|
126
|
+
lastKey.current = key
|
|
127
|
+
|
|
128
|
+
onContext({
|
|
129
|
+
updatedAt: new Date().toISOString(),
|
|
130
|
+
mode,
|
|
131
|
+
island: {
|
|
132
|
+
path: islandPath,
|
|
133
|
+
name: islandName,
|
|
134
|
+
},
|
|
135
|
+
lookingAt: lookingAt ? toFileRef(lookingAt) : null,
|
|
136
|
+
lookingAtFiles,
|
|
137
|
+
selected: selected ? toFileRef(selected) : null,
|
|
138
|
+
filesOnIsland: filesOnIsland(graph, islandPath),
|
|
139
|
+
position: {
|
|
140
|
+
x: Number(camera.position.x.toFixed(2)),
|
|
141
|
+
z: Number(camera.position.z.toFixed(2)),
|
|
142
|
+
},
|
|
143
|
+
userCreatedBlocks: namedBlocks,
|
|
144
|
+
userCreatedIslands: namedIslands,
|
|
145
|
+
})
|
|
146
|
+
} catch {
|
|
147
|
+
// Context tracking must never stop the render loop.
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
return null
|
|
152
|
+
}
|