@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,290 @@
|
|
|
1
|
+
import { Suspense } from 'react'
|
|
2
|
+
import { Text } from '@react-three/drei'
|
|
3
|
+
import { CONFIG } from '../theme'
|
|
4
|
+
import type { PlacedBridge } from '../types'
|
|
5
|
+
|
|
6
|
+
const WALK_Y = 0.02
|
|
7
|
+
const LAND_COLOR = '#2a3340'
|
|
8
|
+
const SPAN_COLOR = '#232e3a'
|
|
9
|
+
const CORNER_SIZE = CONFIG.bridgeWidth
|
|
10
|
+
|
|
11
|
+
const POST_W = 0.3
|
|
12
|
+
const POST_D = 0.38
|
|
13
|
+
const POST_H = 3.45
|
|
14
|
+
const LINTEL_H = 0.34
|
|
15
|
+
const CAP_H = 0.2
|
|
16
|
+
const CAP_OVERHANG = 0.42
|
|
17
|
+
const SIGN_H = 0.72
|
|
18
|
+
const SIGN_D = 0.08
|
|
19
|
+
const GATE_COLOR = '#4a5564'
|
|
20
|
+
const CAP_COLOR = '#5a6574'
|
|
21
|
+
const SIGN_COLOR = '#323a46'
|
|
22
|
+
|
|
23
|
+
type BridgeProps = {
|
|
24
|
+
bridge: PlacedBridge
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Segment = {
|
|
28
|
+
key: string
|
|
29
|
+
x: number
|
|
30
|
+
z: number
|
|
31
|
+
width: number
|
|
32
|
+
depth: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type WalkPiece = Segment & { color: string }
|
|
36
|
+
|
|
37
|
+
function segmentsFromPoints(points: [number, number][]): Segment[] {
|
|
38
|
+
const segments: Segment[] = []
|
|
39
|
+
|
|
40
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
41
|
+
const [x0, z0] = points[i - 1]
|
|
42
|
+
const [x1, z1] = points[i]
|
|
43
|
+
const alongX = Math.abs(x1 - x0) >= Math.abs(z1 - z0)
|
|
44
|
+
const length = alongX ? Math.abs(x1 - x0) : Math.abs(z1 - z0)
|
|
45
|
+
if (length < 0.01) continue
|
|
46
|
+
|
|
47
|
+
segments.push({
|
|
48
|
+
key: `seg-${i}`,
|
|
49
|
+
x: (x0 + x1) / 2,
|
|
50
|
+
z: (z0 + z1) / 2,
|
|
51
|
+
width: alongX ? length : CONFIG.bridgeWidth,
|
|
52
|
+
depth: alongX ? CONFIG.bridgeWidth : length,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return segments
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cornersFromPoints(points: [number, number][]) {
|
|
60
|
+
const corners: { x: number; z: number }[] = []
|
|
61
|
+
for (let i = 1; i < points.length - 1; i += 1) {
|
|
62
|
+
const prev = points[i - 1]
|
|
63
|
+
const point = points[i]
|
|
64
|
+
const next = points[i + 1]
|
|
65
|
+
const incomingX = Math.abs(point[0] - prev[0]) > Math.abs(point[1] - prev[1])
|
|
66
|
+
const outgoingX = Math.abs(next[0] - point[0]) > Math.abs(next[1] - point[1])
|
|
67
|
+
if (incomingX !== outgoingX) {
|
|
68
|
+
corners.push({ x: point[0], z: point[1] })
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return corners
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function splitOverLand(segment: Segment): WalkPiece[] {
|
|
75
|
+
const overlap = CONFIG.bridgeOverlap
|
|
76
|
+
const alongX = segment.width >= segment.depth
|
|
77
|
+
const length = alongX ? segment.width : segment.depth
|
|
78
|
+
const spanLength = Math.max(0, length - overlap * 2)
|
|
79
|
+
|
|
80
|
+
if (spanLength < 0.01) {
|
|
81
|
+
return [{ ...segment, color: LAND_COLOR }]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (alongX) {
|
|
85
|
+
const left = segment.x - length / 2
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
key: `${segment.key}-land-a`,
|
|
89
|
+
x: left + overlap / 2,
|
|
90
|
+
z: segment.z,
|
|
91
|
+
width: overlap,
|
|
92
|
+
depth: segment.depth,
|
|
93
|
+
color: LAND_COLOR,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
key: `${segment.key}-span`,
|
|
97
|
+
x: segment.x,
|
|
98
|
+
z: segment.z,
|
|
99
|
+
width: spanLength,
|
|
100
|
+
depth: segment.depth,
|
|
101
|
+
color: SPAN_COLOR,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
key: `${segment.key}-land-b`,
|
|
105
|
+
x: left + length - overlap / 2,
|
|
106
|
+
z: segment.z,
|
|
107
|
+
width: overlap,
|
|
108
|
+
depth: segment.depth,
|
|
109
|
+
color: LAND_COLOR,
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const start = segment.z - length / 2
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
key: `${segment.key}-land-a`,
|
|
118
|
+
x: segment.x,
|
|
119
|
+
z: start + overlap / 2,
|
|
120
|
+
width: segment.width,
|
|
121
|
+
depth: overlap,
|
|
122
|
+
color: LAND_COLOR,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: `${segment.key}-span`,
|
|
126
|
+
x: segment.x,
|
|
127
|
+
z: segment.z,
|
|
128
|
+
width: segment.width,
|
|
129
|
+
depth: spanLength,
|
|
130
|
+
color: SPAN_COLOR,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: `${segment.key}-land-b`,
|
|
134
|
+
x: segment.x,
|
|
135
|
+
z: start + length - overlap / 2,
|
|
136
|
+
width: segment.width,
|
|
137
|
+
depth: overlap,
|
|
138
|
+
color: LAND_COLOR,
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Walk({
|
|
144
|
+
x,
|
|
145
|
+
z,
|
|
146
|
+
width,
|
|
147
|
+
depth,
|
|
148
|
+
color,
|
|
149
|
+
}: {
|
|
150
|
+
x: number
|
|
151
|
+
z: number
|
|
152
|
+
width: number
|
|
153
|
+
depth: number
|
|
154
|
+
color: string
|
|
155
|
+
}) {
|
|
156
|
+
return (
|
|
157
|
+
<mesh position={[x, WALK_Y, z]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
158
|
+
<planeGeometry args={[width, depth]} />
|
|
159
|
+
<meshBasicMaterial color={color} />
|
|
160
|
+
</mesh>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function segmentDir(from: [number, number], to: [number, number]): [number, number] {
|
|
165
|
+
const dx = to[0] - from[0]
|
|
166
|
+
const dz = to[1] - from[1]
|
|
167
|
+
const length = Math.hypot(dx, dz)
|
|
168
|
+
if (length < 0.001) return [0, 1]
|
|
169
|
+
return [dx / length, dz / length]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function Gate({
|
|
173
|
+
x,
|
|
174
|
+
z,
|
|
175
|
+
faceX,
|
|
176
|
+
faceZ,
|
|
177
|
+
label,
|
|
178
|
+
}: {
|
|
179
|
+
x: number
|
|
180
|
+
z: number
|
|
181
|
+
faceX: number
|
|
182
|
+
faceZ: number
|
|
183
|
+
label: string
|
|
184
|
+
}) {
|
|
185
|
+
const rotationY = Math.atan2(faceX, faceZ)
|
|
186
|
+
const postX = CONFIG.bridgeWidth / 2 + POST_W / 2
|
|
187
|
+
const lintelW = postX * 2 + POST_W
|
|
188
|
+
const capW = lintelW + CAP_OVERHANG * 2
|
|
189
|
+
const lintelY = POST_H + LINTEL_H / 2
|
|
190
|
+
const capY = POST_H + LINTEL_H + CAP_H / 2
|
|
191
|
+
const signY = POST_H + LINTEL_H / 2
|
|
192
|
+
const signZ = POST_D / 2 + SIGN_D / 2 + 0.01
|
|
193
|
+
const fontSize = Math.min(0.42, (lintelW - 0.55) / Math.max(label.length * 0.62, 3))
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<group position={[x, 0, z]} rotation={[0, rotationY, 0]}>
|
|
197
|
+
<mesh position={[-postX, POST_H / 2, 0]}>
|
|
198
|
+
<boxGeometry args={[POST_W, POST_H, POST_D]} />
|
|
199
|
+
<meshLambertMaterial color={GATE_COLOR} />
|
|
200
|
+
</mesh>
|
|
201
|
+
<mesh position={[postX, POST_H / 2, 0]}>
|
|
202
|
+
<boxGeometry args={[POST_W, POST_H, POST_D]} />
|
|
203
|
+
<meshLambertMaterial color={GATE_COLOR} />
|
|
204
|
+
</mesh>
|
|
205
|
+
<mesh position={[0, lintelY, 0]}>
|
|
206
|
+
<boxGeometry args={[lintelW, LINTEL_H, POST_D]} />
|
|
207
|
+
<meshLambertMaterial color={GATE_COLOR} />
|
|
208
|
+
</mesh>
|
|
209
|
+
<mesh position={[0, capY, 0]}>
|
|
210
|
+
<boxGeometry args={[capW, CAP_H, POST_D + 0.08]} />
|
|
211
|
+
<meshLambertMaterial color={CAP_COLOR} />
|
|
212
|
+
</mesh>
|
|
213
|
+
<mesh position={[0, signY, signZ]}>
|
|
214
|
+
<boxGeometry args={[lintelW - 0.18, SIGN_H, SIGN_D]} />
|
|
215
|
+
<meshLambertMaterial color={SIGN_COLOR} />
|
|
216
|
+
</mesh>
|
|
217
|
+
<Suspense fallback={null}>
|
|
218
|
+
<Text
|
|
219
|
+
position={[0, signY, signZ + SIGN_D / 2 + 0.01]}
|
|
220
|
+
fontSize={fontSize}
|
|
221
|
+
color="#e7ebf2"
|
|
222
|
+
anchorX="center"
|
|
223
|
+
anchorY="middle"
|
|
224
|
+
maxWidth={lintelW - 0.4}
|
|
225
|
+
overflowWrap="break-word"
|
|
226
|
+
outlineWidth={0.02}
|
|
227
|
+
outlineColor="#07080b"
|
|
228
|
+
>
|
|
229
|
+
{label}
|
|
230
|
+
</Text>
|
|
231
|
+
</Suspense>
|
|
232
|
+
</group>
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function Bridge({ bridge }: BridgeProps) {
|
|
237
|
+
const segments = segmentsFromPoints(bridge.points)
|
|
238
|
+
const corners = cornersFromPoints(bridge.points)
|
|
239
|
+
const start = bridge.points[0]
|
|
240
|
+
const end = bridge.points[bridge.points.length - 1]
|
|
241
|
+
const second = bridge.points[1]
|
|
242
|
+
const beforeEnd = bridge.points[bridge.points.length - 2]
|
|
243
|
+
const startDir = start && second ? segmentDir(start, second) : null
|
|
244
|
+
const endDir = beforeEnd && end ? segmentDir(beforeEnd, end) : null
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<group>
|
|
248
|
+
{segments.flatMap(splitOverLand).map((piece) => (
|
|
249
|
+
<Walk
|
|
250
|
+
key={piece.key}
|
|
251
|
+
x={piece.x}
|
|
252
|
+
z={piece.z}
|
|
253
|
+
width={piece.width}
|
|
254
|
+
depth={piece.depth}
|
|
255
|
+
color={piece.color}
|
|
256
|
+
/>
|
|
257
|
+
))}
|
|
258
|
+
|
|
259
|
+
{corners.map((corner) => (
|
|
260
|
+
<Walk
|
|
261
|
+
key={`corner-${corner.x}-${corner.z}`}
|
|
262
|
+
x={corner.x}
|
|
263
|
+
z={corner.z}
|
|
264
|
+
width={CORNER_SIZE}
|
|
265
|
+
depth={CORNER_SIZE}
|
|
266
|
+
color={SPAN_COLOR}
|
|
267
|
+
/>
|
|
268
|
+
))}
|
|
269
|
+
|
|
270
|
+
{start && startDir && (
|
|
271
|
+
<Gate
|
|
272
|
+
x={start[0]}
|
|
273
|
+
z={start[1]}
|
|
274
|
+
faceX={-startDir[0]}
|
|
275
|
+
faceZ={-startDir[1]}
|
|
276
|
+
label={bridge.label}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
{end && endDir && (
|
|
280
|
+
<Gate
|
|
281
|
+
x={end[0]}
|
|
282
|
+
z={end[1]}
|
|
283
|
+
faceX={endDir[0]}
|
|
284
|
+
faceZ={endDir[1]}
|
|
285
|
+
label={bridge.fromLabel}
|
|
286
|
+
/>
|
|
287
|
+
)}
|
|
288
|
+
</group>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { Suspense } from 'react'
|
|
2
|
+
import { Billboard, Edges, Html, Text } from '@react-three/drei'
|
|
3
|
+
import { CHANGE_HIGHLIGHT, CONFIG, dimColor, fileColor, type ChangeKind } from '../theme'
|
|
4
|
+
import { NameInput } from '../ui/NameInput'
|
|
5
|
+
import { MapSelectBorder } from './MapSelectBorder'
|
|
6
|
+
import type { FileNode } from '../types'
|
|
7
|
+
import type { PlacedFile } from '../types'
|
|
8
|
+
|
|
9
|
+
const BAR = 0.22
|
|
10
|
+
const SIDE_LABEL_PAD = 0.05
|
|
11
|
+
const SIDE_LABEL_MIN_HEIGHT = CONFIG.eyeHeight * 2.5
|
|
12
|
+
const SIDE_LABELS: { rotationY: number; axis: 'x' | 'z'; sign: 1 | -1 }[] = [
|
|
13
|
+
{ rotationY: 0, axis: 'z', sign: 1 },
|
|
14
|
+
{ rotationY: Math.PI, axis: 'z', sign: -1 },
|
|
15
|
+
{ rotationY: Math.PI / 2, axis: 'x', sign: 1 },
|
|
16
|
+
{ rotationY: -Math.PI / 2, axis: 'x', sign: -1 },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
type FileBlockProps = {
|
|
20
|
+
file: FileNode
|
|
21
|
+
placed: PlacedFile
|
|
22
|
+
selected: boolean
|
|
23
|
+
related: boolean
|
|
24
|
+
planned: boolean
|
|
25
|
+
changeKind?: ChangeKind | null
|
|
26
|
+
added?: boolean
|
|
27
|
+
aimed?: boolean
|
|
28
|
+
dimmed: boolean
|
|
29
|
+
naming?: boolean
|
|
30
|
+
mapMode?: boolean
|
|
31
|
+
onCommitName?: (name: string) => void
|
|
32
|
+
onCancelName?: () => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function PlannedFrame({
|
|
36
|
+
width,
|
|
37
|
+
height,
|
|
38
|
+
depth,
|
|
39
|
+
color,
|
|
40
|
+
}: {
|
|
41
|
+
width: number
|
|
42
|
+
height: number
|
|
43
|
+
depth: number
|
|
44
|
+
color: string
|
|
45
|
+
}) {
|
|
46
|
+
const ox = width / 2 + BAR / 2
|
|
47
|
+
const oy = height / 2 + BAR / 2
|
|
48
|
+
const oz = depth / 2 + BAR / 2
|
|
49
|
+
const bars: [number, number, number, number, number, number][] = [
|
|
50
|
+
[ox, 0, oz, BAR, height + BAR * 2, BAR],
|
|
51
|
+
[-ox, 0, oz, BAR, height + BAR * 2, BAR],
|
|
52
|
+
[ox, 0, -oz, BAR, height + BAR * 2, BAR],
|
|
53
|
+
[-ox, 0, -oz, BAR, height + BAR * 2, BAR],
|
|
54
|
+
[0, oy, oz, width + BAR * 2, BAR, BAR],
|
|
55
|
+
[0, oy, -oz, width + BAR * 2, BAR, BAR],
|
|
56
|
+
[ox, oy, 0, BAR, BAR, depth],
|
|
57
|
+
[-ox, oy, 0, BAR, BAR, depth],
|
|
58
|
+
[0, -oy, oz, width + BAR * 2, BAR, BAR],
|
|
59
|
+
[0, -oy, -oz, width + BAR * 2, BAR, BAR],
|
|
60
|
+
[ox, -oy, 0, BAR, BAR, depth],
|
|
61
|
+
[-ox, -oy, 0, BAR, BAR, depth],
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<group>
|
|
66
|
+
{bars.map(([x, y, z, w, h, d], index) => (
|
|
67
|
+
<mesh key={index} position={[x, y, z]}>
|
|
68
|
+
<boxGeometry args={[w, h, d]} />
|
|
69
|
+
<meshBasicMaterial color={color} toneMapped={false} />
|
|
70
|
+
</mesh>
|
|
71
|
+
))}
|
|
72
|
+
</group>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fileLabel(name: string, changeKind: ChangeKind | null, added: boolean) {
|
|
77
|
+
if (changeKind === 'remove') return `- ${name}`
|
|
78
|
+
if (changeKind === 'add' || added) return `+ ${name}`
|
|
79
|
+
return name
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function FileBlock({
|
|
83
|
+
file,
|
|
84
|
+
placed,
|
|
85
|
+
selected,
|
|
86
|
+
related,
|
|
87
|
+
planned,
|
|
88
|
+
changeKind = null,
|
|
89
|
+
added = false,
|
|
90
|
+
aimed = false,
|
|
91
|
+
dimmed,
|
|
92
|
+
naming = false,
|
|
93
|
+
mapMode = false,
|
|
94
|
+
onCommitName,
|
|
95
|
+
onCancelName,
|
|
96
|
+
}: FileBlockProps) {
|
|
97
|
+
const [width, height, depth] = placed.size
|
|
98
|
+
const highlight = changeKind ? CHANGE_HIGHLIGHT[changeKind] : null
|
|
99
|
+
const color = added || file.userCreated ? '#7ec8e8' : fileColor(file.language)
|
|
100
|
+
const muted = dimColor(color, 0.32)
|
|
101
|
+
const walkSelected = selected && !mapMode
|
|
102
|
+
const mapSelected = selected && mapMode
|
|
103
|
+
const label = fileLabel(file.name, changeKind, added || Boolean(file.userCreated))
|
|
104
|
+
const labelColor = aimed
|
|
105
|
+
? '#9ad8ff'
|
|
106
|
+
: highlight
|
|
107
|
+
? highlight.color
|
|
108
|
+
: dimmed
|
|
109
|
+
? '#b4bcc8'
|
|
110
|
+
: '#e7ebf2'
|
|
111
|
+
const meshColor = aimed
|
|
112
|
+
? '#9ad8ff'
|
|
113
|
+
: highlight
|
|
114
|
+
? highlight.color
|
|
115
|
+
: walkSelected
|
|
116
|
+
? '#5d9ec4'
|
|
117
|
+
: related
|
|
118
|
+
? '#4e7f72'
|
|
119
|
+
: dimmed
|
|
120
|
+
? muted
|
|
121
|
+
: color
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<group position={placed.position}>
|
|
125
|
+
{highlight && !mapSelected && (
|
|
126
|
+
<mesh
|
|
127
|
+
position={[0, height / 2 + 0.04, 0]}
|
|
128
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
129
|
+
userData={{ fileId: file.id }}
|
|
130
|
+
>
|
|
131
|
+
<planeGeometry args={[width + 0.7, depth + 0.7]} />
|
|
132
|
+
<meshBasicMaterial color={highlight.color} toneMapped={false} />
|
|
133
|
+
</mesh>
|
|
134
|
+
)}
|
|
135
|
+
<mesh userData={{ fileId: file.id }}>
|
|
136
|
+
<boxGeometry args={[width, height, depth]} />
|
|
137
|
+
<meshLambertMaterial
|
|
138
|
+
color={meshColor}
|
|
139
|
+
emissive={
|
|
140
|
+
aimed
|
|
141
|
+
? '#3a6a80'
|
|
142
|
+
: highlight
|
|
143
|
+
? highlight.emissive
|
|
144
|
+
: walkSelected
|
|
145
|
+
? '#2a4a5c'
|
|
146
|
+
: related
|
|
147
|
+
? '#1f4a44'
|
|
148
|
+
: added || file.userCreated
|
|
149
|
+
? '#2a5064'
|
|
150
|
+
: dimmed
|
|
151
|
+
? muted
|
|
152
|
+
: color
|
|
153
|
+
}
|
|
154
|
+
emissiveIntensity={
|
|
155
|
+
aimed
|
|
156
|
+
? 0.45
|
|
157
|
+
: highlight
|
|
158
|
+
? 0.95
|
|
159
|
+
: walkSelected
|
|
160
|
+
? 0.28
|
|
161
|
+
: related
|
|
162
|
+
? 0.18
|
|
163
|
+
: added || file.userCreated
|
|
164
|
+
? 0.22
|
|
165
|
+
: dimmed
|
|
166
|
+
? 0.08
|
|
167
|
+
: 0.12
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
{walkSelected && (
|
|
171
|
+
<Edges
|
|
172
|
+
color="#000000"
|
|
173
|
+
dashed
|
|
174
|
+
dashSize={0.18}
|
|
175
|
+
gapSize={0.1}
|
|
176
|
+
lineWidth={2.5}
|
|
177
|
+
toneMapped={false}
|
|
178
|
+
scale={1.002}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
</mesh>
|
|
182
|
+
{mapSelected && (
|
|
183
|
+
<MapSelectBorder
|
|
184
|
+
width={width}
|
|
185
|
+
depth={depth}
|
|
186
|
+
y={height / 2 + 0.05}
|
|
187
|
+
userData={{ fileId: file.id }}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
{highlight && !mapSelected && (
|
|
191
|
+
<PlannedFrame
|
|
192
|
+
width={width}
|
|
193
|
+
height={height}
|
|
194
|
+
depth={depth}
|
|
195
|
+
color={highlight.color}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
{naming && onCommitName && onCancelName && (
|
|
199
|
+
<Html
|
|
200
|
+
position={[0, height / 2 + 0.42, 0]}
|
|
201
|
+
center
|
|
202
|
+
occlude={false}
|
|
203
|
+
wrapperClass="block-name-overlay"
|
|
204
|
+
zIndexRange={[100, 0]}
|
|
205
|
+
>
|
|
206
|
+
<NameInput
|
|
207
|
+
placeholder="File name"
|
|
208
|
+
onCommit={onCommitName}
|
|
209
|
+
onCancel={onCancelName}
|
|
210
|
+
/>
|
|
211
|
+
</Html>
|
|
212
|
+
)}
|
|
213
|
+
<Suspense fallback={null}>
|
|
214
|
+
{!naming && (
|
|
215
|
+
<Billboard position={[0, height / 2 + (planned || highlight ? 0.55 : 0.38), 0]}>
|
|
216
|
+
<Text
|
|
217
|
+
fontSize={0.28}
|
|
218
|
+
color={labelColor}
|
|
219
|
+
anchorX="center"
|
|
220
|
+
anchorY="bottom"
|
|
221
|
+
maxWidth={3.4}
|
|
222
|
+
>
|
|
223
|
+
{label}
|
|
224
|
+
</Text>
|
|
225
|
+
</Billboard>
|
|
226
|
+
)}
|
|
227
|
+
{height >= SIDE_LABEL_MIN_HEIGHT &&
|
|
228
|
+
SIDE_LABELS.map((side) => {
|
|
229
|
+
const face = side.axis === 'x' ? width : depth
|
|
230
|
+
const x =
|
|
231
|
+
side.axis === 'x' ? side.sign * (width / 2 + SIDE_LABEL_PAD) : 0
|
|
232
|
+
const z =
|
|
233
|
+
side.axis === 'z' ? side.sign * (depth / 2 + SIDE_LABEL_PAD) : 0
|
|
234
|
+
return (
|
|
235
|
+
<Text
|
|
236
|
+
key={`${side.axis}:${side.sign}`}
|
|
237
|
+
position={[x, -height / 2 + 0.22, z]}
|
|
238
|
+
rotation={[0, side.rotationY, 0]}
|
|
239
|
+
fontSize={0.22}
|
|
240
|
+
color={labelColor}
|
|
241
|
+
anchorX="center"
|
|
242
|
+
anchorY="middle"
|
|
243
|
+
maxWidth={face - 0.2}
|
|
244
|
+
overflowWrap="break-word"
|
|
245
|
+
outlineWidth={0.012}
|
|
246
|
+
outlineColor="#11151c"
|
|
247
|
+
depthOffset={-1}
|
|
248
|
+
>
|
|
249
|
+
{label}
|
|
250
|
+
</Text>
|
|
251
|
+
)
|
|
252
|
+
})}
|
|
253
|
+
</Suspense>
|
|
254
|
+
</group>
|
|
255
|
+
)
|
|
256
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Suspense } from 'react'
|
|
2
|
+
import { Text } from '@react-three/drei'
|
|
3
|
+
import { CHANGE_HIGHLIGHT, folderFloorColor, MAP_SELECTION, type ChangeKind } from '../theme'
|
|
4
|
+
import type { PlacedFolder } from '../types'
|
|
5
|
+
import { MapSelectBorder } from './MapSelectBorder'
|
|
6
|
+
|
|
7
|
+
type FolderAreaProps = {
|
|
8
|
+
folder: PlacedFolder
|
|
9
|
+
naming?: boolean
|
|
10
|
+
selected?: boolean
|
|
11
|
+
highlightKind?: ChangeKind | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function FolderArea({
|
|
15
|
+
folder,
|
|
16
|
+
naming = false,
|
|
17
|
+
selected = false,
|
|
18
|
+
highlightKind = null,
|
|
19
|
+
}: FolderAreaProps) {
|
|
20
|
+
const added = Boolean(folder.added)
|
|
21
|
+
const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
|
|
22
|
+
const addedOnly = added && !highlight
|
|
23
|
+
const outline = highlight ? highlight.color : addedOnly ? '#7ec8e8' : null
|
|
24
|
+
const color = highlight
|
|
25
|
+
? highlight.floor
|
|
26
|
+
: addedOnly
|
|
27
|
+
? '#16323f'
|
|
28
|
+
: folderFloorColor(folder.path)
|
|
29
|
+
const label =
|
|
30
|
+
highlightKind === 'remove'
|
|
31
|
+
? `- ${folder.name}`
|
|
32
|
+
: highlightKind === 'add' || added
|
|
33
|
+
? `+ ${folder.name}`
|
|
34
|
+
: folder.name
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<group position={[folder.x, 0, folder.z + folder.depth / 2]}>
|
|
38
|
+
{outline && (
|
|
39
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.01, 0]}>
|
|
40
|
+
<planeGeometry args={[folder.width + 0.9, folder.depth + 0.9]} />
|
|
41
|
+
<meshBasicMaterial
|
|
42
|
+
color={outline}
|
|
43
|
+
toneMapped={false}
|
|
44
|
+
/>
|
|
45
|
+
</mesh>
|
|
46
|
+
)}
|
|
47
|
+
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
|
48
|
+
<planeGeometry args={[folder.width, folder.depth]} />
|
|
49
|
+
<meshBasicMaterial color={color} />
|
|
50
|
+
</mesh>
|
|
51
|
+
{selected && (
|
|
52
|
+
<MapSelectBorder
|
|
53
|
+
width={folder.width}
|
|
54
|
+
depth={folder.depth}
|
|
55
|
+
y={0.06}
|
|
56
|
+
stroke={MAP_SELECTION.islandPad}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
<mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
|
|
60
|
+
<planeGeometry args={[2.2, folder.depth - 1.5]} />
|
|
61
|
+
<meshBasicMaterial
|
|
62
|
+
color={highlight ? highlight.aisle : addedOnly ? '#23485a' : '#2a3340'}
|
|
63
|
+
/>
|
|
64
|
+
</mesh>
|
|
65
|
+
{folder.width > 28 && (
|
|
66
|
+
<mesh
|
|
67
|
+
position={[0, 0.02, folder.depth / 2 - 1.3]}
|
|
68
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
69
|
+
>
|
|
70
|
+
<planeGeometry args={[folder.width - 2.4, 2.4]} />
|
|
71
|
+
<meshBasicMaterial
|
|
72
|
+
color={
|
|
73
|
+
highlight ? highlight.aisle : addedOnly ? '#23485a' : '#2a3340'
|
|
74
|
+
}
|
|
75
|
+
/>
|
|
76
|
+
</mesh>
|
|
77
|
+
)}
|
|
78
|
+
<Suspense fallback={null}>
|
|
79
|
+
{!naming && (
|
|
80
|
+
<Text
|
|
81
|
+
position={[0, 0.05, -folder.depth / 2 + 1.6]}
|
|
82
|
+
rotation={[-Math.PI / 2, 0, 0]}
|
|
83
|
+
fontSize={0.55}
|
|
84
|
+
color={
|
|
85
|
+
highlight ? highlight.color : addedOnly ? '#9ad8ff' : '#8b95a5'
|
|
86
|
+
}
|
|
87
|
+
anchorX="center"
|
|
88
|
+
anchorY="middle"
|
|
89
|
+
>
|
|
90
|
+
{label}
|
|
91
|
+
</Text>
|
|
92
|
+
)}
|
|
93
|
+
</Suspense>
|
|
94
|
+
</group>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useThree } from '@react-three/fiber'
|
|
3
|
+
import { folderAt } from '../layout'
|
|
4
|
+
import type { WorldLayout } from '../types'
|
|
5
|
+
|
|
6
|
+
type IslandPlacerProps = {
|
|
7
|
+
enabled: boolean
|
|
8
|
+
layout: WorldLayout
|
|
9
|
+
onPlace: (parent: string) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
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
|
+
export function IslandPlacer({ enabled, layout, onPlace }: IslandPlacerProps) {
|
|
23
|
+
const { camera } = useThree()
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const onKey = (event: KeyboardEvent) => {
|
|
27
|
+
if (!enabled || event.repeat || event.code !== 'KeyB') return
|
|
28
|
+
if (typingInField(event.target)) return
|
|
29
|
+
const island = folderAt(camera.position.x, camera.position.z, layout)
|
|
30
|
+
if (!island) return
|
|
31
|
+
event.preventDefault()
|
|
32
|
+
onPlace(island.path)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
window.addEventListener('keydown', onKey)
|
|
36
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
37
|
+
}, [camera, enabled, layout, onPlace])
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|