@dcl-regenesislabs/opendcl 0.2.0 → 0.2.1-26238928766.commit-28648d7
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 +5 -3
- package/context/sdk7-cheat-sheet.md +4 -0
- package/dist/index.js +0 -12
- package/dist/index.js.map +1 -1
- package/extensions/dcl-init.ts +58 -6
- package/package.json +7 -3
- package/prompts/system.md +71 -41
- package/skills/add-3d-models/SKILL.md +120 -70
- package/skills/add-interactivity/SKILL.md +74 -2
- package/skills/advanced-input/SKILL.md +34 -1
- package/skills/advanced-rendering/SKILL.md +82 -9
- package/skills/animations-tweens/SKILL.md +203 -98
- package/skills/audio-analysis/SKILL.md +164 -0
- package/skills/audio-video/SKILL.md +184 -83
- package/skills/build-ui/SKILL.md +25 -2
- package/skills/camera-control/SKILL.md +78 -7
- package/skills/create-scene/SKILL.md +56 -13
- package/skills/deploy-scene/SKILL.md +12 -0
- package/skills/deploy-worlds/SKILL.md +35 -0
- package/skills/editor-gizmo/.gitignore +11 -0
- package/skills/editor-gizmo/SKILL.md +222 -0
- package/skills/editor-gizmo/src/__editor/camera.ts +277 -0
- package/skills/editor-gizmo/src/__editor/discovery.ts +186 -0
- package/skills/editor-gizmo/src/__editor/drag.ts +265 -0
- package/skills/editor-gizmo/src/__editor/gizmo.ts +496 -0
- package/skills/editor-gizmo/src/__editor/history.ts +72 -0
- package/skills/editor-gizmo/src/__editor/index.ts +137 -0
- package/skills/editor-gizmo/src/__editor/input.ts +55 -0
- package/skills/editor-gizmo/src/__editor/math-utils.ts +114 -0
- package/skills/editor-gizmo/src/__editor/persistence.ts +113 -0
- package/skills/editor-gizmo/src/__editor/selection.ts +157 -0
- package/skills/editor-gizmo/src/__editor/state.ts +117 -0
- package/skills/editor-gizmo/src/__editor/ui.tsx +697 -0
- package/skills/game-design/SKILL.md +1 -2
- package/skills/lighting-environment/SKILL.md +103 -56
- package/skills/multiplayer-sync/SKILL.md +31 -2
- package/skills/nft-blockchain/SKILL.md +45 -40
- package/skills/npcs/SKILL.md +180 -0
- package/skills/optimize-scene/SKILL.md +7 -2
- package/skills/particle-system/SKILL.md +222 -0
- package/skills/player-avatar/SKILL.md +133 -7
- package/skills/player-physics/SKILL.md +93 -0
- package/skills/scene-runtime/SKILL.md +9 -5
- package/skills/visual-feedback/SKILL.md +1 -0
- package/extensions/dcl-setup-ollama.ts +0 -312
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { Entity, Transform, VisibilityComponent } from '@dcl/sdk/ecs'
|
|
2
|
+
import { Color4, Quaternion } from '@dcl/sdk/math'
|
|
3
|
+
import { isWeb } from '@dcl/sdk/platform'
|
|
4
|
+
import ReactEcs, { ReactEcsRenderer, UiEntity, Label } from '@dcl/sdk/react-ecs'
|
|
5
|
+
import { getExplorerInformation } from '~system/Runtime'
|
|
6
|
+
import { toggleEditorCamera, focusSelectedEntity } from './camera'
|
|
7
|
+
import { createGizmo } from './gizmo'
|
|
8
|
+
import { undoCount, redoCount, undo, redo } from './history'
|
|
9
|
+
import { selectEntity, deselectEntity } from './selection'
|
|
10
|
+
import { state, selectableInfoMap, toggleEditorActive } from './state'
|
|
11
|
+
|
|
12
|
+
// ── Platform detection ──────────────────────────────────
|
|
13
|
+
|
|
14
|
+
let isBevy = false
|
|
15
|
+
void getExplorerInformation({}).then($ => { isBevy = $.agent === 'bevy' }).catch(() => {})
|
|
16
|
+
|
|
17
|
+
// ── Icons (Lucide via Iconify CDN) ──────────────────────
|
|
18
|
+
|
|
19
|
+
const IC = 'https://api.iconify.design/lucide'
|
|
20
|
+
const ICON = {
|
|
21
|
+
select: `${IC}/mouse-pointer.svg?color=white&width=64&height=64`,
|
|
22
|
+
move: `${IC}/move.svg?color=white&width=64&height=64`,
|
|
23
|
+
rotate: `${IC}/rotate-cw.svg?color=white&width=64&height=64`,
|
|
24
|
+
undo: `${IC}/undo-2.svg?color=white&width=64&height=64`,
|
|
25
|
+
redo: `${IC}/redo-2.svg?color=white&width=64&height=64`,
|
|
26
|
+
camera: `${IC}/video.svg?color=white&width=64&height=64`,
|
|
27
|
+
focus: `${IC}/crosshair.svg?color=white&width=64&height=64`,
|
|
28
|
+
edit: `${IC}/pencil.svg?color=white&width=64&height=64`,
|
|
29
|
+
chevUp: `${IC}/chevron-up.svg?color=white&width=64&height=64`,
|
|
30
|
+
chevDown: `${IC}/chevron-down.svg?color=white&width=64&height=64`,
|
|
31
|
+
model: `${IC}/box.svg?color=white&width=64&height=64`,
|
|
32
|
+
primitive: `${IC}/diamond.svg?color=white&width=64&height=64`,
|
|
33
|
+
help: `${IC}/circle-help.svg?color=white&width=64&height=64`,
|
|
34
|
+
eyeOn: `${IC}/eye.svg?color=white&width=64&height=64`,
|
|
35
|
+
eyeOff: `${IC}/eye-off.svg?color=white&width=64&height=64`,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Unicode fallbacks for Unity renderer
|
|
39
|
+
const UNI = {
|
|
40
|
+
select: '⊙',
|
|
41
|
+
move: '✥',
|
|
42
|
+
rotate: '↻',
|
|
43
|
+
undo: '↶',
|
|
44
|
+
redo: '↷',
|
|
45
|
+
camera: '◉',
|
|
46
|
+
focus: '◎',
|
|
47
|
+
edit: '✏',
|
|
48
|
+
chevUp: '▲',
|
|
49
|
+
chevDown: '▼',
|
|
50
|
+
model: '▣',
|
|
51
|
+
primitive: '◇',
|
|
52
|
+
help: '?',
|
|
53
|
+
eyeOn: '◉',
|
|
54
|
+
eyeOff: '○',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Render an icon — URL texture on Bevy-web, Unicode label on Unity */
|
|
58
|
+
function Icon(props: { icon: keyof typeof ICON, size: number, color: Color4 }) {
|
|
59
|
+
if (isWeb() || isBevy) {
|
|
60
|
+
return (
|
|
61
|
+
<UiEntity
|
|
62
|
+
uiTransform={{ width: props.size, height: props.size }}
|
|
63
|
+
uiBackground={{
|
|
64
|
+
textureMode: 'stretch',
|
|
65
|
+
texture: { src: ICON[props.icon] },
|
|
66
|
+
color: props.color,
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
return (
|
|
72
|
+
<UiEntity uiTransform={{ width: props.size, height: props.size, justifyContent: 'center', alignItems: 'center' }}>
|
|
73
|
+
<Label
|
|
74
|
+
value={UNI[props.icon]}
|
|
75
|
+
fontSize={props.size}
|
|
76
|
+
color={props.color}
|
|
77
|
+
uiTransform={{ width: props.size, height: props.size }}
|
|
78
|
+
textAlign="middle-center"
|
|
79
|
+
/>
|
|
80
|
+
</UiEntity>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Theme ───────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const C = {
|
|
87
|
+
// Backgrounds (all fully opaque — partial alpha causes the scene to bleed
|
|
88
|
+
// through and creates apparent two-tone effects on rounded buttons).
|
|
89
|
+
panel: Color4.create(0.10, 0.10, 0.12, 1),
|
|
90
|
+
panelBorder: Color4.create(0.32, 0.32, 0.38, 1),
|
|
91
|
+
|
|
92
|
+
// Buttons
|
|
93
|
+
btn: Color4.create(0.18, 0.18, 0.21, 1),
|
|
94
|
+
btnHover: Color4.create(0.26, 0.26, 0.30, 1),
|
|
95
|
+
btnActive: Color4.create(0.22, 0.42, 0.58, 1),
|
|
96
|
+
btnActiveH: Color4.create(0.28, 0.50, 0.66, 1),
|
|
97
|
+
|
|
98
|
+
// Rows
|
|
99
|
+
rowHover: Color4.create(0.20, 0.20, 0.24, 1),
|
|
100
|
+
rowSel: Color4.create(0.22, 0.42, 0.58, 0.70),
|
|
101
|
+
|
|
102
|
+
// Separators
|
|
103
|
+
sep: Color4.create(0.28, 0.28, 0.32, 0.40),
|
|
104
|
+
|
|
105
|
+
// Text
|
|
106
|
+
text: Color4.create(0.90, 0.90, 0.92, 1),
|
|
107
|
+
textMid: Color4.create(0.62, 0.62, 0.66, 1),
|
|
108
|
+
textDim: Color4.create(0.40, 0.40, 0.44, 1),
|
|
109
|
+
textOff: Color4.create(0.30, 0.30, 0.34, 1),
|
|
110
|
+
|
|
111
|
+
// Axes
|
|
112
|
+
xAxis: Color4.create(0.95, 0.40, 0.40, 1),
|
|
113
|
+
yAxis: Color4.create(0.40, 0.90, 0.40, 1),
|
|
114
|
+
zAxis: Color4.create(0.40, 0.55, 0.95, 1),
|
|
115
|
+
|
|
116
|
+
// Transparent
|
|
117
|
+
none: Color4.create(0, 0, 0, 0),
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Sizes ───────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const TOOL_SZ = 28
|
|
123
|
+
const PANEL_W = 200
|
|
124
|
+
const ROW_H = 22
|
|
125
|
+
const MAX_ROWS = 12
|
|
126
|
+
const HEADER_H = 26
|
|
127
|
+
const RADIUS_PANEL = 8
|
|
128
|
+
const RADIUS_BTN = 6
|
|
129
|
+
|
|
130
|
+
// ── Interaction ─────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
let hovered: string | null = null
|
|
133
|
+
let hierScroll = 0
|
|
134
|
+
let hierHov: number | null = null
|
|
135
|
+
let showShortcuts = false
|
|
136
|
+
const hiddenEntities = new Set<Entity>()
|
|
137
|
+
|
|
138
|
+
function toggleVisibility(entity: Entity) {
|
|
139
|
+
const nowHidden = !hiddenEntities.has(entity)
|
|
140
|
+
if (nowHidden) hiddenEntities.add(entity)
|
|
141
|
+
else hiddenEntities.delete(entity)
|
|
142
|
+
VisibilityComponent.createOrReplace(entity, { visible: !nowHidden })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
function pos3() {
|
|
148
|
+
if (state.selectedEntity !== undefined && Transform.has(state.selectedEntity)) {
|
|
149
|
+
const p = Transform.get(state.selectedEntity).position
|
|
150
|
+
return { x: p.x.toFixed(2), y: p.y.toFixed(2), z: p.z.toFixed(2) }
|
|
151
|
+
}
|
|
152
|
+
return { x: '-', y: '-', z: '-' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function rot3() {
|
|
156
|
+
if (state.selectedEntity !== undefined && Transform.has(state.selectedEntity)) {
|
|
157
|
+
const e = Quaternion.toEulerAngles(Transform.get(state.selectedEntity).rotation)
|
|
158
|
+
return { x: e.x.toFixed(1), y: e.y.toFixed(1), z: e.z.toFixed(1) }
|
|
159
|
+
}
|
|
160
|
+
return { x: '-', y: '-', z: '-' }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isHov(id: string) { return hovered === id }
|
|
164
|
+
function setHov(id: string) { hovered = id }
|
|
165
|
+
function clearHov(id: string) { if (hovered === id) hovered = null }
|
|
166
|
+
|
|
167
|
+
// ── Tool Button ─────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function toolBg(active: boolean, disabled: boolean, hovering: boolean): Color4 {
|
|
170
|
+
if (disabled) return C.btn
|
|
171
|
+
if (active) return hovering ? C.btnActiveH : C.btnActive
|
|
172
|
+
if (hovering) return C.btnHover
|
|
173
|
+
return C.btn
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function toolOpacity(active: boolean, disabled: boolean, hovering: boolean): number {
|
|
177
|
+
if (disabled) return 0.25
|
|
178
|
+
if (active) return 1.0
|
|
179
|
+
if (hovering) return 0.8
|
|
180
|
+
return 0.5
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function Tool(id: string, iconKey: keyof typeof ICON, active: boolean, disabled: boolean, fn: () => void) {
|
|
184
|
+
const h = isHov(id)
|
|
185
|
+
const bg = toolBg(active, disabled, h)
|
|
186
|
+
const opacity = toolOpacity(active, disabled, h)
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<UiEntity
|
|
190
|
+
uiTransform={{
|
|
191
|
+
width: TOOL_SZ, height: TOOL_SZ,
|
|
192
|
+
margin: { left: 2, right: 2 },
|
|
193
|
+
justifyContent: 'center', alignItems: 'center',
|
|
194
|
+
borderRadius: RADIUS_BTN,
|
|
195
|
+
}}
|
|
196
|
+
uiBackground={{ color: bg }}
|
|
197
|
+
onMouseEnter={() => setHov(id)}
|
|
198
|
+
onMouseLeave={() => clearHov(id)}
|
|
199
|
+
onMouseDown={() => { if (!disabled) fn() }}
|
|
200
|
+
>
|
|
201
|
+
{Icon({ icon: iconKey, size: 14, color: Color4.create(1, 1, 1, opacity) })}
|
|
202
|
+
</UiEntity>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function ToolSep() {
|
|
207
|
+
return (
|
|
208
|
+
<UiEntity
|
|
209
|
+
uiTransform={{ width: 1, height: TOOL_SZ - 8, margin: { left: 3, right: 3 }, alignSelf: 'center' }}
|
|
210
|
+
uiBackground={{ color: C.sep }}
|
|
211
|
+
/>
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Toolbar ─────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
function Toolbar(sel: boolean) {
|
|
218
|
+
const mode = state.gizmoMode
|
|
219
|
+
return (
|
|
220
|
+
<UiEntity
|
|
221
|
+
uiTransform={{
|
|
222
|
+
positionType: 'absolute',
|
|
223
|
+
position: { top: 8, left: -8 },
|
|
224
|
+
width: '100%',
|
|
225
|
+
justifyContent: 'center',
|
|
226
|
+
alignItems: 'flex-start',
|
|
227
|
+
flexDirection: 'row',
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<UiEntity
|
|
231
|
+
uiTransform={{
|
|
232
|
+
padding: { left: 4, right: 4, top: 6, bottom: 6 },
|
|
233
|
+
flexDirection: 'row', alignItems: 'center',
|
|
234
|
+
borderRadius: RADIUS_PANEL,
|
|
235
|
+
}}
|
|
236
|
+
uiBackground={{ color: C.panel }}
|
|
237
|
+
>
|
|
238
|
+
{/* Selection */}
|
|
239
|
+
{Tool('sel', 'select', !sel, false, () => { if (sel) deselectEntity() })}
|
|
240
|
+
|
|
241
|
+
{Tool('mov', 'move', sel && mode === 'translate', !sel, () => { state.gizmoMode = 'translate'; if (sel) createGizmo() })}
|
|
242
|
+
{Tool('rot', 'rotate', sel && mode === 'rotate', !sel, () => { state.gizmoMode = 'rotate'; if (sel) createGizmo() })}
|
|
243
|
+
|
|
244
|
+
{ToolSep()}
|
|
245
|
+
|
|
246
|
+
{/* Undo / Redo */}
|
|
247
|
+
{Tool('und', 'undo', false, undoCount() === 0, () => undo())}
|
|
248
|
+
{Tool('red', 'redo', false, redoCount() === 0, () => redo())}
|
|
249
|
+
|
|
250
|
+
{ToolSep()}
|
|
251
|
+
|
|
252
|
+
{/* Camera */}
|
|
253
|
+
{Tool('cam', 'camera', state.editorCamActive, false, () => toggleEditorCamera())}
|
|
254
|
+
{Tool('foc', 'focus', false, !sel, () => {
|
|
255
|
+
if (!state.editorCamActive) toggleEditorCamera()
|
|
256
|
+
focusSelectedEntity()
|
|
257
|
+
})}
|
|
258
|
+
</UiEntity>
|
|
259
|
+
</UiEntity>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Tree ────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
interface TreeRow { e: Entity; name: string; isModel: boolean; depth: number }
|
|
266
|
+
|
|
267
|
+
function buildTree(): TreeRow[] {
|
|
268
|
+
const names = new Map<number, { name: string; isModel: boolean }>()
|
|
269
|
+
const childrenOf = new Map<number, number[]>()
|
|
270
|
+
const rootIds: number[] = []
|
|
271
|
+
|
|
272
|
+
// Only Named entities make it into selectableInfoMap (filtered at
|
|
273
|
+
// discovery), so the hierarchy already excludes runtime-spawned dynamic
|
|
274
|
+
// entities by convention.
|
|
275
|
+
for (const [entity, info] of selectableInfoMap) {
|
|
276
|
+
const id = entity as number
|
|
277
|
+
names.set(id, { name: info.name, isModel: info.isModel })
|
|
278
|
+
const pid = info.parentEntity
|
|
279
|
+
if (pid !== undefined && selectableInfoMap.has(pid as Entity)) {
|
|
280
|
+
const kids = childrenOf.get(pid)
|
|
281
|
+
if (kids) kids.push(id)
|
|
282
|
+
else childrenOf.set(pid, [id])
|
|
283
|
+
} else {
|
|
284
|
+
rootIds.push(id)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const byName = (a: number, b: number) => {
|
|
289
|
+
const na = names.get(a)!.name.toLowerCase()
|
|
290
|
+
const nb = names.get(b)!.name.toLowerCase()
|
|
291
|
+
return na < nb ? -1 : na > nb ? 1 : 0
|
|
292
|
+
}
|
|
293
|
+
rootIds.sort(byName)
|
|
294
|
+
for (const [, kids] of childrenOf) kids.sort(byName)
|
|
295
|
+
|
|
296
|
+
const flat: TreeRow[] = []
|
|
297
|
+
const visit = (ids: number[], depth: number) => {
|
|
298
|
+
for (const id of ids) {
|
|
299
|
+
const { name, isModel } = names.get(id)!
|
|
300
|
+
flat.push({ e: id as Entity, name, isModel, depth })
|
|
301
|
+
const kids = childrenOf.get(id)
|
|
302
|
+
if (kids) visit(kids, depth + 1)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
visit(rootIds, 0)
|
|
306
|
+
return flat
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Hierarchy Panel (top-left) ──────────────────────────
|
|
310
|
+
|
|
311
|
+
function HierarchyPanel() {
|
|
312
|
+
const flat = buildTree()
|
|
313
|
+
const total = flat.length
|
|
314
|
+
const maxScr = Math.max(0, total - MAX_ROWS)
|
|
315
|
+
hierScroll = Math.max(0, Math.min(hierScroll, maxScr))
|
|
316
|
+
|
|
317
|
+
const sel = state.selectedEntity !== undefined
|
|
318
|
+
if (sel) {
|
|
319
|
+
const selIdx = flat.findIndex((n) => n.e === state.selectedEntity)
|
|
320
|
+
if (selIdx >= 0) {
|
|
321
|
+
if (selIdx < hierScroll) hierScroll = selIdx
|
|
322
|
+
else if (selIdx >= hierScroll + MAX_ROWS) hierScroll = selIdx - MAX_ROWS + 1
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const vis = Math.min(MAX_ROWS, total - hierScroll)
|
|
327
|
+
const canUp = hierScroll > 0
|
|
328
|
+
const canDown = hierScroll < maxScr
|
|
329
|
+
|
|
330
|
+
const rows: ReactEcs.JSX.Element[] = []
|
|
331
|
+
for (let i = 0; i < vis; i++) {
|
|
332
|
+
const node = flat[hierScroll + i]
|
|
333
|
+
const eid = node.e as number
|
|
334
|
+
const isSel = state.selectedEntity === node.e
|
|
335
|
+
const isH = hierHov === eid
|
|
336
|
+
|
|
337
|
+
let bg: Color4
|
|
338
|
+
if (isSel) bg = C.rowSel
|
|
339
|
+
else if (isH) bg = C.rowHover
|
|
340
|
+
else bg = C.none
|
|
341
|
+
|
|
342
|
+
let col: Color4
|
|
343
|
+
if (isSel) col = C.text
|
|
344
|
+
else if (isH) col = C.textMid
|
|
345
|
+
else col = C.textDim
|
|
346
|
+
const pad = 8 + node.depth * 10
|
|
347
|
+
const maxC = Math.max(6, 22 - node.depth * 2)
|
|
348
|
+
const lbl = node.name.length > maxC ? node.name.substring(0, maxC - 1) + '..' : node.name
|
|
349
|
+
const iconKey: keyof typeof ICON = node.isModel ? 'model' : 'primitive'
|
|
350
|
+
const isHidden = hiddenEntities.has(node.e)
|
|
351
|
+
|
|
352
|
+
rows.push(
|
|
353
|
+
<UiEntity
|
|
354
|
+
key={eid}
|
|
355
|
+
uiTransform={{
|
|
356
|
+
width: '100%', height: ROW_H,
|
|
357
|
+
padding: { left: pad, right: 6 },
|
|
358
|
+
alignItems: 'center', flexDirection: 'row',
|
|
359
|
+
borderRadius: 5, margin: { top: 1, bottom: 1 },
|
|
360
|
+
}}
|
|
361
|
+
uiBackground={{ color: bg }}
|
|
362
|
+
onMouseEnter={() => { hierHov = eid }}
|
|
363
|
+
onMouseLeave={() => { if (hierHov === eid) hierHov = null }}
|
|
364
|
+
>
|
|
365
|
+
<UiEntity
|
|
366
|
+
uiTransform={{ flexGrow: 1, flexDirection: 'row', alignItems: 'center', height: ROW_H }}
|
|
367
|
+
onMouseDown={() => selectEntity(eid as Entity)}
|
|
368
|
+
>
|
|
369
|
+
{Icon({ icon: iconKey, size: 12, color: Color4.create(col.r, col.g, col.b, 0.7) })}
|
|
370
|
+
<Label value={lbl} fontSize={11} color={isHidden ? C.textOff : col} uiTransform={{ height: 14, margin: { left: 6 } }} />
|
|
371
|
+
</UiEntity>
|
|
372
|
+
<UiEntity
|
|
373
|
+
uiTransform={{ width: 16, height: 16, margin: { left: 4 } }}
|
|
374
|
+
onMouseDown={() => toggleVisibility(node.e)}
|
|
375
|
+
>
|
|
376
|
+
{Icon({ icon: isHidden ? 'eyeOff' : 'eyeOn', size: 16, color: Color4.create(1, 1, 1, isHidden ? 0.3 : (isH ? 0.7 : 0.3)) })}
|
|
377
|
+
</UiEntity>
|
|
378
|
+
</UiEntity>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Visible row span height (cells + their margins). ROW_H + 2 margin per row.
|
|
383
|
+
const ROW_FULL = ROW_H + 2
|
|
384
|
+
return (
|
|
385
|
+
<UiEntity
|
|
386
|
+
uiTransform={{
|
|
387
|
+
width: PANEL_W, flexDirection: 'column',
|
|
388
|
+
borderRadius: RADIUS_PANEL,
|
|
389
|
+
borderWidth: 1,
|
|
390
|
+
borderColor: C.panelBorder,
|
|
391
|
+
}}
|
|
392
|
+
uiBackground={{ color: C.panel }}
|
|
393
|
+
>
|
|
394
|
+
{/* Header — no separate background, just text inside the panel */}
|
|
395
|
+
<UiEntity
|
|
396
|
+
uiTransform={{
|
|
397
|
+
width: '100%', height: HEADER_H,
|
|
398
|
+
flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
|
|
399
|
+
padding: { left: 14, right: 14 },
|
|
400
|
+
}}
|
|
401
|
+
>
|
|
402
|
+
<Label value="HIERARCHY" fontSize={9} color={C.textMid} uiTransform={{ height: 12 }} />
|
|
403
|
+
<Label value={`${total}`} fontSize={9} color={C.textDim} uiTransform={{ height: 12 }} />
|
|
404
|
+
</UiEntity>
|
|
405
|
+
|
|
406
|
+
{/* Thin separator under the header */}
|
|
407
|
+
<UiEntity
|
|
408
|
+
uiTransform={{ width: '100%', height: 1, margin: { left: 12, right: 12 } }}
|
|
409
|
+
uiBackground={{ color: C.sep }}
|
|
410
|
+
/>
|
|
411
|
+
|
|
412
|
+
{/* Scroll up */}
|
|
413
|
+
{canUp ? (
|
|
414
|
+
<UiEntity
|
|
415
|
+
uiTransform={{ width: '100%', height: 16, justifyContent: 'center', alignItems: 'center' }}
|
|
416
|
+
onMouseDown={() => { hierScroll = Math.max(0, hierScroll - 5) }}
|
|
417
|
+
>
|
|
418
|
+
{Icon({ icon: 'chevUp', size: 12, color: C.textDim })}
|
|
419
|
+
</UiEntity>
|
|
420
|
+
) : null}
|
|
421
|
+
|
|
422
|
+
{/* Rows */}
|
|
423
|
+
<UiEntity uiTransform={{ width: '100%', height: vis * ROW_FULL, flexDirection: 'column', padding: { left: 4, right: 4, top: 4, bottom: 4 } }}>
|
|
424
|
+
{rows}
|
|
425
|
+
</UiEntity>
|
|
426
|
+
|
|
427
|
+
{/* Scroll down */}
|
|
428
|
+
{canDown ? (
|
|
429
|
+
<UiEntity
|
|
430
|
+
uiTransform={{ width: '100%', height: 16, justifyContent: 'center', alignItems: 'center' }}
|
|
431
|
+
onMouseDown={() => { hierScroll = Math.min(maxScr, hierScroll + 5) }}
|
|
432
|
+
>
|
|
433
|
+
{Icon({ icon: 'chevDown', size: 12, color: C.textDim })}
|
|
434
|
+
</UiEntity>
|
|
435
|
+
) : null}
|
|
436
|
+
</UiEntity>
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Inspector Panel (top-right) ─────────────────────────
|
|
441
|
+
|
|
442
|
+
function InspectorPanel() {
|
|
443
|
+
if (state.selectedEntity === undefined) return null
|
|
444
|
+
|
|
445
|
+
const pos = pos3()
|
|
446
|
+
const rot = rot3()
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<UiEntity
|
|
450
|
+
uiTransform={{
|
|
451
|
+
width: PANEL_W, flexDirection: 'column',
|
|
452
|
+
margin: { top: 6 }, borderRadius: RADIUS_PANEL,
|
|
453
|
+
borderWidth: 1,
|
|
454
|
+
borderColor: C.panelBorder,
|
|
455
|
+
}}
|
|
456
|
+
uiBackground={{ color: C.panel }}
|
|
457
|
+
>
|
|
458
|
+
{/* Entity name */}
|
|
459
|
+
<UiEntity
|
|
460
|
+
uiTransform={{
|
|
461
|
+
width: '100%', height: HEADER_H + 4,
|
|
462
|
+
flexDirection: 'row', alignItems: 'center',
|
|
463
|
+
padding: { left: 14, right: 14 },
|
|
464
|
+
}}
|
|
465
|
+
>
|
|
466
|
+
<Label value={state.selectedName} fontSize={12} color={C.text} uiTransform={{ height: 16 }} />
|
|
467
|
+
</UiEntity>
|
|
468
|
+
|
|
469
|
+
{/* Separator */}
|
|
470
|
+
<UiEntity
|
|
471
|
+
uiTransform={{ width: '100%', height: 1, margin: { left: 12, right: 12 } }}
|
|
472
|
+
uiBackground={{ color: C.sep }}
|
|
473
|
+
/>
|
|
474
|
+
|
|
475
|
+
{/* TRANSFORM section */}
|
|
476
|
+
<UiEntity uiTransform={{ width: '100%', flexDirection: 'column', padding: { left: 14, right: 14, top: 10, bottom: 12 } }}>
|
|
477
|
+
<Label value="TRANSFORM" fontSize={9} color={C.textMid} uiTransform={{ height: 12, margin: { bottom: 8 } }} />
|
|
478
|
+
|
|
479
|
+
{/* Position */}
|
|
480
|
+
<UiEntity uiTransform={{ width: '100%', flexDirection: 'column', margin: { bottom: 6 } }}>
|
|
481
|
+
<Label value="Position" fontSize={8} color={C.textDim} uiTransform={{ height: 11, margin: { bottom: 3 } }} />
|
|
482
|
+
<UiEntity uiTransform={{ flexDirection: 'row', width: '100%', height: 18 }}>
|
|
483
|
+
{AxisField('X', pos.x, C.xAxis)}
|
|
484
|
+
{AxisField('Y', pos.y, C.yAxis)}
|
|
485
|
+
{AxisField('Z', pos.z, C.zAxis)}
|
|
486
|
+
</UiEntity>
|
|
487
|
+
</UiEntity>
|
|
488
|
+
|
|
489
|
+
{/* Rotation */}
|
|
490
|
+
<UiEntity uiTransform={{ width: '100%', flexDirection: 'column' }}>
|
|
491
|
+
<Label value="Rotation" fontSize={8} color={C.textDim} uiTransform={{ height: 11, margin: { bottom: 3 } }} />
|
|
492
|
+
<UiEntity uiTransform={{ flexDirection: 'row', width: '100%', height: 18 }}>
|
|
493
|
+
{AxisField('X', rot.x, C.xAxis)}
|
|
494
|
+
{AxisField('Y', rot.y, C.yAxis)}
|
|
495
|
+
{AxisField('Z', rot.z, C.zAxis)}
|
|
496
|
+
</UiEntity>
|
|
497
|
+
</UiEntity>
|
|
498
|
+
</UiEntity>
|
|
499
|
+
</UiEntity>
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function AxisField(label: string, value: string, color: Color4) {
|
|
504
|
+
return (
|
|
505
|
+
<UiEntity
|
|
506
|
+
uiTransform={{
|
|
507
|
+
height: 18,
|
|
508
|
+
margin: { right: 4 },
|
|
509
|
+
flexDirection: 'row',
|
|
510
|
+
alignItems: 'center',
|
|
511
|
+
flexGrow: 1,
|
|
512
|
+
borderRadius: 9,
|
|
513
|
+
padding: { left: 2, right: 4 }
|
|
514
|
+
}}
|
|
515
|
+
uiBackground={{ color: Color4.create(0.06, 0.06, 0.08, 1) }}
|
|
516
|
+
>
|
|
517
|
+
<UiEntity
|
|
518
|
+
uiTransform={{ width: 14, height: 14, justifyContent: 'center', alignItems: 'center', borderRadius: 7, margin: { right: 4 } }}
|
|
519
|
+
uiBackground={{ color: Color4.create(color.r * 0.35, color.g * 0.35, color.b * 0.35, 1) }}
|
|
520
|
+
>
|
|
521
|
+
<Label value={label} fontSize={9} color={Color4.create(color.r, color.g, color.b, 0.95)} uiTransform={{ height: 11 }} textAlign="middle-center" />
|
|
522
|
+
</UiEntity>
|
|
523
|
+
<Label value={value} fontSize={9} color={C.text} uiTransform={{ height: 13 }} />
|
|
524
|
+
</UiEntity>
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ── Shortcuts Panel ─────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
function ShortcutRow(key: string, label: string) {
|
|
531
|
+
return (
|
|
532
|
+
<UiEntity uiTransform={{ flexDirection: 'row', alignItems: 'center', height: 16, width: '100%' }}>
|
|
533
|
+
<UiEntity
|
|
534
|
+
uiTransform={{
|
|
535
|
+
width: 28, height: 14,
|
|
536
|
+
justifyContent: 'center', alignItems: 'center',
|
|
537
|
+
margin: { right: 6 },
|
|
538
|
+
}}
|
|
539
|
+
uiBackground={{ color: Color4.create(0.22, 0.22, 0.26, 0.8) }}
|
|
540
|
+
>
|
|
541
|
+
<Label value={key} fontSize={8} color={C.text} uiTransform={{ height: 10 }} textAlign="middle-center" />
|
|
542
|
+
</UiEntity>
|
|
543
|
+
<Label value={label} fontSize={8} color={C.textDim} uiTransform={{ height: 10 }} />
|
|
544
|
+
</UiEntity>
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function ShortcutsPanel() {
|
|
549
|
+
const camOn = state.editorCamActive
|
|
550
|
+
const sel = state.selectedEntity !== undefined
|
|
551
|
+
const h = isHov('help')
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
<UiEntity
|
|
555
|
+
uiTransform={{
|
|
556
|
+
positionType: 'absolute',
|
|
557
|
+
position: { bottom: 8, left: 64 },
|
|
558
|
+
flexDirection: 'column',
|
|
559
|
+
alignItems: 'flex-start',
|
|
560
|
+
}}
|
|
561
|
+
>
|
|
562
|
+
{/* Expanded panel */}
|
|
563
|
+
{showShortcuts ? (
|
|
564
|
+
<UiEntity
|
|
565
|
+
uiTransform={{
|
|
566
|
+
flexDirection: 'column',
|
|
567
|
+
padding: { left: 12, right: 14, top: 10, bottom: 10 },
|
|
568
|
+
margin: { bottom: 4 },
|
|
569
|
+
borderRadius: RADIUS_PANEL,
|
|
570
|
+
borderWidth: 1,
|
|
571
|
+
borderColor: C.panelBorder,
|
|
572
|
+
}}
|
|
573
|
+
uiBackground={{ color: C.panel }}
|
|
574
|
+
>
|
|
575
|
+
<UiEntity
|
|
576
|
+
uiTransform={{
|
|
577
|
+
flexDirection: 'column',
|
|
578
|
+
}}
|
|
579
|
+
>
|
|
580
|
+
{/* Header */}
|
|
581
|
+
<Label
|
|
582
|
+
value={camOn ? 'Editor Camera' : 'Shortcuts'}
|
|
583
|
+
fontSize={9}
|
|
584
|
+
color={C.textMid}
|
|
585
|
+
uiTransform={{ height: 14, margin: { bottom: 4 } }}
|
|
586
|
+
/>
|
|
587
|
+
|
|
588
|
+
{/* Camera controls */}
|
|
589
|
+
{camOn ? ShortcutRow('WASD', 'Pan') : null}
|
|
590
|
+
{camOn ? ShortcutRow('Space', 'Up') : null}
|
|
591
|
+
{camOn ? ShortcutRow('Shift', 'Down') : null}
|
|
592
|
+
{camOn ? ShortcutRow('2 / 3', 'Zoom in / out') : null}
|
|
593
|
+
{camOn ? ShortcutRow('Drag', 'Orbit') : null}
|
|
594
|
+
{/* Separator */}
|
|
595
|
+
{camOn ? (
|
|
596
|
+
<UiEntity uiTransform={{ width: '100%', height: 1, margin: { top: 4, bottom: 4 } }} uiBackground={{ color: C.sep }} />
|
|
597
|
+
) : null}
|
|
598
|
+
|
|
599
|
+
{/* General */}
|
|
600
|
+
{ShortcutRow('Click', 'Select')}
|
|
601
|
+
{ShortcutRow('E', 'Move / Rotate')}
|
|
602
|
+
{ShortcutRow('F', 'Deselect')}
|
|
603
|
+
{ShortcutRow('1', camOn ? 'Exit camera' : 'Editor camera')}
|
|
604
|
+
{sel ? ShortcutRow('2', 'Focus selected') : null}
|
|
605
|
+
|
|
606
|
+
</UiEntity>
|
|
607
|
+
</UiEntity>
|
|
608
|
+
) : null}
|
|
609
|
+
|
|
610
|
+
{/* Toggle button */}
|
|
611
|
+
<UiEntity
|
|
612
|
+
uiTransform={{
|
|
613
|
+
width: 28, height: 28,
|
|
614
|
+
justifyContent: 'center', alignItems: 'center',
|
|
615
|
+
borderRadius: RADIUS_BTN,
|
|
616
|
+
}}
|
|
617
|
+
uiBackground={{ color: showShortcuts ? C.btnActive : h ? C.btnHover : C.btn }}
|
|
618
|
+
onMouseEnter={() => setHov('help')}
|
|
619
|
+
onMouseLeave={() => clearHov('help')}
|
|
620
|
+
onMouseDown={() => { showShortcuts = !showShortcuts }}
|
|
621
|
+
>
|
|
622
|
+
{Icon({ icon: 'help', size: 14, color: Color4.create(1, 1, 1, showShortcuts ? 1.0 : h ? 0.8 : 0.5) })}
|
|
623
|
+
</UiEntity>
|
|
624
|
+
</UiEntity>
|
|
625
|
+
)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── Editor Toggle Button (preview only) ─────────────────
|
|
629
|
+
|
|
630
|
+
function EditorToggle() {
|
|
631
|
+
const edOn = state.editorActive
|
|
632
|
+
const h = isHov('edt')
|
|
633
|
+
|
|
634
|
+
return (
|
|
635
|
+
<UiEntity
|
|
636
|
+
uiTransform={{
|
|
637
|
+
positionType: 'absolute',
|
|
638
|
+
position: { bottom: 8, right: 8 },
|
|
639
|
+
width: 28, height: 28,
|
|
640
|
+
justifyContent: 'center', alignItems: 'center',
|
|
641
|
+
borderRadius: RADIUS_BTN,
|
|
642
|
+
}}
|
|
643
|
+
uiBackground={{ color: toolBg(edOn, false, h) }}
|
|
644
|
+
onMouseEnter={() => setHov('edt')}
|
|
645
|
+
onMouseLeave={() => clearHov('edt')}
|
|
646
|
+
onMouseDown={() => toggleEditorActive()}
|
|
647
|
+
>
|
|
648
|
+
{Icon({ icon: 'edit', size: 14, color: Color4.create(1, 1, 1, edOn ? 1.0 : h ? 0.8 : 0.5) })}
|
|
649
|
+
</UiEntity>
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ── Main UI ─────────────────────────────────────────────
|
|
654
|
+
|
|
655
|
+
function EditorUI() {
|
|
656
|
+
// Deployed scenes (not preview, not studio) never see any editor UI —
|
|
657
|
+
// not even the toggle button. Decided async by realmDetectSystem in
|
|
658
|
+
// index.ts once RealmInfo is published.
|
|
659
|
+
if (!state.isPreview) {
|
|
660
|
+
return <UiEntity uiTransform={{ width: 0, height: 0, display: 'none' }} />
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Editor off — just the pencil button.
|
|
664
|
+
if (!state.editorActive) {
|
|
665
|
+
return (
|
|
666
|
+
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
|
|
667
|
+
{EditorToggle()}
|
|
668
|
+
</UiEntity>
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Editor on — full UI.
|
|
673
|
+
const sel = state.selectedEntity !== undefined
|
|
674
|
+
return (
|
|
675
|
+
<UiEntity uiTransform={{ width: '100%', height: '100%' }}>
|
|
676
|
+
{Toolbar(sel)}
|
|
677
|
+
{/* Right-side stack: hierarchy + inspector */}
|
|
678
|
+
<UiEntity
|
|
679
|
+
uiTransform={{
|
|
680
|
+
positionType: 'absolute',
|
|
681
|
+
position: { top: 8, right: 8 },
|
|
682
|
+
flexDirection: 'column',
|
|
683
|
+
alignItems: 'flex-end',
|
|
684
|
+
}}
|
|
685
|
+
>
|
|
686
|
+
{HierarchyPanel()}
|
|
687
|
+
{InspectorPanel()}
|
|
688
|
+
</UiEntity>
|
|
689
|
+
{ShortcutsPanel()}
|
|
690
|
+
{EditorToggle()}
|
|
691
|
+
</UiEntity>
|
|
692
|
+
)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function setupEditorUi() {
|
|
696
|
+
ReactEcsRenderer.setUiRenderer(EditorUI, { virtualWidth: 1280, virtualHeight: 720 })
|
|
697
|
+
}
|