@dcl-regenesislabs/opendcl 0.2.1 → 0.2.2-26850672477.commit-99ffd91

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.
Files changed (45) hide show
  1. package/README.md +5 -3
  2. package/context/sdk7-cheat-sheet.md +4 -0
  3. package/dist/index.js +0 -12
  4. package/dist/index.js.map +1 -1
  5. package/extensions/dcl-init.ts +58 -6
  6. package/package.json +3 -3
  7. package/prompts/system.md +71 -41
  8. package/skills/add-3d-models/SKILL.md +120 -70
  9. package/skills/add-interactivity/SKILL.md +74 -2
  10. package/skills/advanced-input/SKILL.md +34 -1
  11. package/skills/advanced-rendering/SKILL.md +82 -9
  12. package/skills/animations-tweens/SKILL.md +203 -98
  13. package/skills/audio-analysis/SKILL.md +164 -0
  14. package/skills/audio-video/SKILL.md +184 -83
  15. package/skills/build-ui/SKILL.md +25 -2
  16. package/skills/camera-control/SKILL.md +78 -7
  17. package/skills/create-scene/SKILL.md +56 -13
  18. package/skills/deploy-scene/SKILL.md +12 -0
  19. package/skills/deploy-worlds/SKILL.md +35 -0
  20. package/skills/editor-gizmo/.gitignore +11 -0
  21. package/skills/editor-gizmo/SKILL.md +222 -0
  22. package/skills/editor-gizmo/src/__editor/camera.ts +277 -0
  23. package/skills/editor-gizmo/src/__editor/discovery.ts +210 -0
  24. package/skills/editor-gizmo/src/__editor/drag.ts +265 -0
  25. package/skills/editor-gizmo/src/__editor/gizmo.ts +496 -0
  26. package/skills/editor-gizmo/src/__editor/history.ts +72 -0
  27. package/skills/editor-gizmo/src/__editor/index.ts +138 -0
  28. package/skills/editor-gizmo/src/__editor/input.ts +55 -0
  29. package/skills/editor-gizmo/src/__editor/math-utils.ts +114 -0
  30. package/skills/editor-gizmo/src/__editor/persistence.ts +113 -0
  31. package/skills/editor-gizmo/src/__editor/selection.ts +157 -0
  32. package/skills/editor-gizmo/src/__editor/state.ts +117 -0
  33. package/skills/editor-gizmo/src/__editor/ui.tsx +699 -0
  34. package/skills/game-design/SKILL.md +1 -2
  35. package/skills/lighting-environment/SKILL.md +103 -56
  36. package/skills/multiplayer-sync/SKILL.md +31 -2
  37. package/skills/nft-blockchain/SKILL.md +45 -40
  38. package/skills/npcs/SKILL.md +180 -0
  39. package/skills/optimize-scene/SKILL.md +7 -2
  40. package/skills/particle-system/SKILL.md +222 -0
  41. package/skills/player-avatar/SKILL.md +133 -7
  42. package/skills/player-physics/SKILL.md +93 -0
  43. package/skills/scene-runtime/SKILL.md +9 -5
  44. package/skills/visual-feedback/SKILL.md +1 -0
  45. package/extensions/dcl-setup-ollama.ts +0 -312
@@ -0,0 +1,222 @@
1
+ ---
2
+ name: editor-gizmo
3
+ description: Enable the visual editor in a Decentraland scene with translate/rotate gizmos. Adds click-to-select, drag-to-move arrows, drag-to-rotate rings, plane handles, wireframe selection box, and UI overlay. Auto-discovers all entities declared in main-entities.ts. Use when user wants to enable the editor, add gizmos, edit the scene interactively, or tweak object positions and rotations in preview.
4
+ ---
5
+
6
+ # Visual Editor Gizmo
7
+
8
+ Add an in-scene visual editor that lets users click objects to select them, then drag arrow/disc handles to move or rotate them. The editor only edits entities declared in `main-entities.ts` — runtime-spawned entities are hidden from the hierarchy.
9
+
10
+ ## How It Works
11
+
12
+ - **Preview only**: the editor only activates in `/preview`. Deployed scenes never show editor UI.
13
+ - **Editor toggle**: a pencil button in the bottom-right corner toggles the editor on/off (starts OFF).
14
+ - **Auto-discovery**: finds all entities with `Transform` + `MeshRenderer` or `GltfContainer`. Hierarchy filters to entities whose `Name` is declared in `main-entities.ts`.
15
+ - **Click to select**: shows a wireframe bounding box and spawns the gizmo.
16
+ - **Translate mode**: 3 colored arrows (R/G/B = X/Y/Z) — drag to move along a world axis. 3 plane handles (XZ/XY/YZ) for 2-axis constrained movement.
17
+ - **Rotate mode**: 3 colored ring outlines — drag to rotate around a world axis.
18
+ - **World-aligned gizmos**: arrows and rings always point along world X/Y/Z, regardless of entity or parent rotation. Drag deltas are converted to local space for child entities.
19
+ - **E key**: toggle between Move and Rotate.
20
+ - **F key** or **click ground**: deselect.
21
+ - **Undo/redo**: key 4 = undo, Shift+4 = redo.
22
+ - **Auto-save**: changes POST to `${realm.baseUrl}/editor/changes` on every drag end. The preview server merges them into `main-entities.ts` and synchronously regenerates `main.crdt`.
23
+
24
+ ## Setup Steps
25
+
26
+ ### Step 0: Check if editor is already installed (and up-to-date)
27
+
28
+ If `src/__editor/state.ts` exists, read the first line and look for `EDITOR_VERSION`. Compare it with the version in `{baseDir}/src/__editor/state.ts`. If the versions match, skip Step 1 — the files are current. If they differ (or `src/__editor/` doesn't exist), proceed with Step 1 to install or update.
29
+
30
+ ### Step 1: Copy editor files into the scene
31
+
32
+ ```bash
33
+ mkdir -p src/__editor && cp -rf {baseDir}/src/__editor/* src/__editor/
34
+ ```
35
+
36
+ This creates a self-contained editor directory:
37
+ ```
38
+ src/__editor/
39
+ ├── index.ts — Entry point + enableEditor() export
40
+ ├── state.ts — Shared state and types
41
+ ├── persistence.ts — HTTP POST/GET to {baseUrl}/editor/changes
42
+ ├── selection.ts — Select/deselect + highlight
43
+ ├── discovery.ts — Auto-discover scene entities
44
+ ├── gizmo.ts — Translate/rotate gizmo handles
45
+ ├── drag.ts — Drag system (ray-plane intersection)
46
+ ├── camera.ts — Editor camera (orbit, WASD pan)
47
+ ├── input.ts — Key bindings (E, F, 1-4)
48
+ ├── history.ts — Undo/redo stack
49
+ ├── math-utils.ts — Vector/quaternion helpers
50
+ └── ui.tsx — Toolbar + hierarchy + properties panel
51
+ ```
52
+
53
+ ### Step 2: Add `enableEditor()` to the scene's main function
54
+
55
+ In `src/index.ts`:
56
+
57
+ ```typescript
58
+ import { enableEditor } from './__editor'
59
+
60
+ export function main() {
61
+ // ... your scene code ...
62
+ enableEditor()
63
+ }
64
+ ```
65
+
66
+ `enableEditor()` is a no-op outside preview mode, so it's safe to leave in deployed scenes.
67
+
68
+ ### Step 3: Make sure the scene has a `main-entities.ts`
69
+
70
+ If the scene doesn't have one yet, create `main-entities.ts` at the scene root with at least one entity (see "Authoring Model" below). Without it, the editor's hierarchy panel falls back to permissive mode and shows every named entity, including runtime ones.
71
+
72
+ ### Step 4: Update tsconfig.json to include `main-entities.ts`
73
+
74
+ The default scene `tsconfig.json` only checks `src/**/*`. Widen the include so the typed `Scene` shape is validated:
75
+
76
+ ```json
77
+ {
78
+ "extends": "@dcl/sdk/types/tsconfig.ecs7.json",
79
+ "include": ["src/**/*.ts", "src/**/*.tsx", "main-entities.ts"]
80
+ }
81
+ ```
82
+
83
+ ### Step 5: Verify
84
+
85
+ Run `/preview`. You should see a pencil button bottom-right. Click it to toggle the editor. Click any entity declared in `main-entities.ts` to select it; drag arrows or rings to move or rotate.
86
+
87
+ ## Scene Authoring Model
88
+
89
+ The editor only edits **declared** entities — those that exist in `main-entities.ts`. Dynamic entities created at runtime via `engine.addEntity()` are hidden from the hierarchy and not draggable.
90
+
91
+ ### `main-entities.ts` (canonical entity declarations)
92
+
93
+ `main-entities.ts` lives at the scene root, exports a typed `scene` constant, and is bundled into `main.crdt` at build time. The `satisfies Scene` clause keeps the literal keys typed (so code can reference entity names safely) while still validating the shape against the schema.
94
+
95
+ ```typescript
96
+ import type { Scene } from '@dcl/sdk/scene-types'
97
+
98
+ export const scene = {
99
+ "barrel_1": {
100
+ "components": {
101
+ "Transform": {
102
+ "position": { "x": 5, "y": 0, "z": 8 },
103
+ "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
104
+ "scale": { "x": 1, "y": 1, "z": 1 }
105
+ },
106
+ "GltfContainer": { "src": "models/Barrel.glb" }
107
+ }
108
+ },
109
+ "lamp_1": {
110
+ "components": {
111
+ "Transform": {
112
+ "position": { "x": 0, "y": 1.5, "z": 0 },
113
+ "rotation": { "x": 0, "y": 0, "z": 0, "w": 1 },
114
+ "scale": { "x": 1, "y": 1, "z": 1 },
115
+ "parent": "barrel_1"
116
+ },
117
+ "GltfContainer": { "src": "models/Lamp.glb" }
118
+ }
119
+ }
120
+ } satisfies Scene
121
+ ```
122
+
123
+ **Rules:**
124
+ - **Names are unique** within `scene` — they're the stable ID the editor and code use to reference an entity.
125
+ - **Parents are referenced by Name**, not entity ID (e.g. `"parent": "barrel_1"`). The build resolves names to IDs.
126
+ - **`Transform.position` is required.** `rotation` defaults to identity, `scale` defaults to `(1,1,1)`, but you must provide all three keys when authoring.
127
+ - **Literal-only constraint:** values inside `scene` must be plain JSON-compatible literals — no function calls (`Vector3.create(...)`), no spread, no computed expressions, no comments inside the literal. The build parses the AST and the editor save handler rewrites the whole literal as JSON, so anything outside this discipline gets stripped or breaks.
128
+
129
+ ### Behavior in `src/index.ts`
130
+
131
+ Code references entities by Name and attaches behavior. Use a type-only import of `scene` so the bundle stays small, and derive `EntityName` for typo-safe lookups:
132
+
133
+ ```typescript
134
+ import { engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs'
135
+ import { enableEditor } from './__editor'
136
+ import type { scene } from '../main-entities'
137
+
138
+ type EntityName = keyof typeof scene
139
+
140
+ export function main() {
141
+ const barrel = engine.getEntityOrNullByName<EntityName>('barrel_1')
142
+ if (barrel === null) return
143
+
144
+ pointerEventsSystem.onPointerDown(
145
+ { entity: barrel, opts: { button: InputAction.IA_POINTER, hoverText: 'Open' } },
146
+ () => console.log('clicked barrel')
147
+ )
148
+
149
+ enableEditor()
150
+ }
151
+ ```
152
+
153
+ - `engine.getEntityOrNullByName<EntityName>(name)` looks up an entity by its `Name` component, populated by the `main.crdt` preload (built from `main-entities.ts` at bundle time).
154
+ - The `<EntityName>` type parameter makes typos a compile error: passing `'barrl_1'` (typo) fails type-checking.
155
+ - Renaming an entity in `main-entities.ts` immediately surfaces every stale reference in code as a compile error.
156
+ - Use `getEntityOrNullByName` rather than `getEntityByName` — the SDK's typed `getEntityByName` requires awkward generics and silently returns `undefined` cast as `Entity`, so null-handling is cleaner.
157
+
158
+ ### Dynamic entities
159
+
160
+ Anything that needs to spawn at runtime (effects, projectiles, dynamic UI markers) still uses `engine.addEntity()` directly:
161
+
162
+ ```typescript
163
+ const explosion = engine.addEntity()
164
+ Transform.create(explosion, { position: Vector3.create(8, 1, 8) })
165
+ GltfContainer.create(explosion, { src: 'models/Explosion.glb' })
166
+ // Don't give dynamic entities a Name — they don't go in main-entities.ts.
167
+ ```
168
+
169
+ **Rule**: only declarative, editable entities go in `main-entities.ts`. Dynamic runtime entities use `engine.addEntity` and don't get `Name` components.
170
+
171
+ ## Persistence
172
+
173
+ When the user drags an entity in preview:
174
+
175
+ 1. The scene applies the new Transform client-side (instant).
176
+ 2. The editor POSTs `${realm.baseUrl}/editor/changes` with the entity's new Transform, keyed by Name.
177
+ 3. The preview server merges the change into `main-entities.ts` on disk by parsing the AST, mutating the scene object, and splicing the new JSON back into the source — preserving everything outside the `scene` literal (imports, `satisfies` clause, comments above the export).
178
+ 4. The same handler synchronously regenerates `main.crdt` so the next reload preloads the updated state.
179
+ 5. Both `main-entities.ts` and `main.crdt` are excluded from the file watcher, so editor saves do not trigger a scene reload.
180
+
181
+ There is no manual "save" step — every drag persists.
182
+
183
+ If the AI / a human edits `main-entities.ts` directly (without going through the editor), a dedicated watcher on `main-entities.ts` regenerates `main.crdt` out-of-band as well, with an mtime check that skips redundant work when the editor's POST handler already produced a fresh CRDT.
184
+
185
+ ## Removing the Editor
186
+
187
+ To remove:
188
+ 1. Delete `src/__editor/`
189
+ 2. Remove the `import { enableEditor } from './__editor'` line and the `enableEditor()` call
190
+
191
+ `main-entities.ts` is unaffected — it remains the source of truth for declared entities, regardless of whether the editor is installed.
192
+
193
+ ## Adding New Entities
194
+
195
+ When the user asks to add a new entity (e.g., "add a barrel"):
196
+
197
+ 1. **Add the entry to `main-entities.ts`** with a unique Name and the components needed to render it (`Transform`, `GltfContainer` or `MeshRenderer` + `Material`, etc.). The TS compiler will validate the shape against `Scene`.
198
+ 2. **Reference it in code** if you need to attach behavior:
199
+ ```typescript
200
+ const barrel = engine.getEntityOrNullByName<EntityName>('barrel_1')
201
+ ```
202
+ 3. Run `/preview` — the entity will appear in the scene at the position declared in `main-entities.ts`, and will be draggable in the editor.
203
+
204
+ ## Components supported in `main-entities.ts`
205
+
206
+ All ECS data components that the client renders/uses:
207
+
208
+ - `Transform` (required for every entity)
209
+ - `GltfContainer`, `MeshRenderer`, `MeshCollider`, `Material`
210
+ - `VisibilityComponent`, `Billboard`
211
+ - `AudioSource`, `VideoPlayer`, `TextShape`, `NftShape`
212
+ - `Animator` (state-machine config; runtime control via code)
213
+
214
+ Behavior — pointer event callbacks, systems, tweens, conditional logic — stays in `src/`.
215
+
216
+ ## Common Pitfalls
217
+
218
+ - **Component shapes mirror the SDK protobuf.** `MeshRenderer` is `{ mesh: { $case: 'box', box: { uvs: [] } } }`, not `MeshRenderer.setBox()`. The TS compiler validates against the protobuf-derived types and will flag mismatches.
219
+ - **Don't use `Vector3.create(...)` inside `main-entities.ts`.** Use plain `{ x, y, z }` objects. The literal-only constraint means function calls and identifier references will break the build/save pipeline.
220
+ - **`box` mesh requires `uvs: []`.** Same for `plane`. Sphere and cylinder default to `{}`. The TS type forces this — pay attention to red squiggles.
221
+ - **Renaming an entity** breaks every code reference until you update them. That's the type system working as intended; let TS guide you to the broken references.
222
+ - **Comments inside the `scene` literal get wiped on the first editor save.** Keep comments outside the literal (above the import, before the `export const scene =` line).
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Editor orbit camera + drag lock camera.
3
+ *
4
+ * Orbit model: target + yaw + pitch + distance.
5
+ * Lock camera: freezes view during gizmo drag (non-editor-cam mode only).
6
+ */
7
+
8
+ import {
9
+ engine,
10
+ Entity,
11
+ Transform,
12
+ VirtualCamera,
13
+ MainCamera,
14
+ InputModifier,
15
+ inputSystem,
16
+ InputAction,
17
+ PrimaryPointerInfo,
18
+ } from '@dcl/sdk/ecs'
19
+ import { Vector3, Quaternion } from '@dcl/sdk/math'
20
+ import { getSceneInformation } from '~system/Runtime'
21
+ import { state, editorEntities, selectableInfoMap, gizmoClickConsumed } from './state'
22
+ import { copyVec3, copyQuat } from './math-utils'
23
+
24
+ // ── Scene bounds (computed once) ────────────────────────
25
+ let sceneCenter = Vector3.create(8, 0, 8)
26
+ let sceneBoundsMin = Vector3.create(0, 0, 0)
27
+ let sceneBoundsMax = Vector3.create(16, 20, 16)
28
+
29
+ async function getSceneInformationAsync() {
30
+ try {
31
+ const info = await getSceneInformation({})
32
+ const metadata = JSON.parse(info.metadataJson)
33
+ const parcels: string[] = metadata?.scene?.parcels ?? ['0,0']
34
+ let minX = Infinity, minZ = Infinity, maxX = -Infinity, maxZ = -Infinity
35
+ for (const p of parcels) {
36
+ const [px, pz] = p.split(',').map(Number)
37
+ if (px < minX) minX = px
38
+ if (pz < minZ) minZ = pz
39
+ if (px > maxX) maxX = px
40
+ if (pz > maxZ) maxZ = pz
41
+ }
42
+ const base = parcels[0].split(',').map(Number)
43
+ const offX = -base[0] * 16
44
+ const offZ = -base[1] * 16
45
+ sceneBoundsMin = Vector3.create((minX * 16) + offX, 0, (minZ * 16) + offZ)
46
+ sceneBoundsMax = Vector3.create(((maxX + 1) * 16) + offX, 20, ((maxZ + 1) * 16) + offZ)
47
+ sceneCenter = Vector3.create(
48
+ (sceneBoundsMin.x + sceneBoundsMax.x) / 2,
49
+ 0,
50
+ (sceneBoundsMin.z + sceneBoundsMax.z) / 2,
51
+ )
52
+ } catch (e) {
53
+ console.log('[editor] failed to read scene bounds, using defaults', e)
54
+ }
55
+ }
56
+ void getSceneInformationAsync()
57
+
58
+ // ============================================================
59
+ // Editor Camera
60
+ // ============================================================
61
+
62
+ let editorCamEntity: Entity | undefined
63
+
64
+ const editorCam = {
65
+ target: Vector3.create(16, 2, 16),
66
+ yaw: -45,
67
+ pitch: 35,
68
+ distance: 25,
69
+ }
70
+
71
+ // Tuning
72
+ const PAN_SPEED = 12
73
+ const VERTICAL_SPEED = 8
74
+ const ORBIT_SENSITIVITY = 0.15
75
+ const ZOOM_SPEED = 15
76
+ const MIN_DISTANCE = 3
77
+ const MAX_DISTANCE = 80
78
+ const MIN_PITCH = 5
79
+ const MAX_PITCH = 89
80
+ const FOCUS_DISTANCE = 8
81
+
82
+ export function createEditorCamera() {
83
+ editorCamEntity = engine.addEntity()
84
+ Transform.create(editorCamEntity, {
85
+ position: Vector3.Zero(),
86
+ rotation: Quaternion.Identity(),
87
+ })
88
+ VirtualCamera.create(editorCamEntity, {
89
+ defaultTransition: { transitionMode: VirtualCamera.Transition.Time(0) },
90
+ })
91
+ editorEntities.add(editorCamEntity)
92
+ }
93
+
94
+ function updateEditorCamera() {
95
+ if (editorCamEntity === undefined) return
96
+
97
+ const pitchRad = editorCam.pitch * (Math.PI / 180)
98
+ const yawRad = editorCam.yaw * (Math.PI / 180)
99
+
100
+ const cosP = Math.cos(pitchRad)
101
+ const offset = Vector3.create(
102
+ -Math.sin(yawRad) * cosP * editorCam.distance,
103
+ Math.sin(pitchRad) * editorCam.distance,
104
+ -Math.cos(yawRad) * cosP * editorCam.distance,
105
+ )
106
+
107
+ const camPos = Vector3.add(editorCam.target, offset)
108
+ const forward = Vector3.normalize(Vector3.subtract(editorCam.target, camPos))
109
+ const camRot = Quaternion.lookRotation(forward)
110
+
111
+ const t = Transform.getMutable(editorCamEntity)
112
+ copyVec3(t.position, camPos)
113
+ copyQuat(t.rotation, camRot)
114
+ }
115
+
116
+ export function activateEditorCamera() {
117
+ if (state.editorCamActive || state.isDragging) return
118
+ state.editorCamActive = true
119
+
120
+ // Center on scene
121
+ editorCam.target = Vector3.create(sceneCenter.x, 0, sceneCenter.z)
122
+
123
+ updateEditorCamera()
124
+ MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = editorCamEntity
125
+
126
+ InputModifier.createOrReplace(engine.PlayerEntity, {
127
+ mode: InputModifier.Mode.Standard({ disableAll: true }),
128
+ })
129
+
130
+ console.log('[editor] editor camera ON — WASD pan, Space/Shift up/down, 2/3 zoom, drag to orbit, F focus')
131
+ }
132
+
133
+ export function deactivateEditorCamera() {
134
+ if (!state.editorCamActive) return
135
+ state.editorCamActive = false
136
+
137
+ MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
138
+
139
+ if (InputModifier.has(engine.PlayerEntity)) {
140
+ InputModifier.deleteFrom(engine.PlayerEntity)
141
+ }
142
+
143
+ console.log('[editor] editor camera OFF')
144
+ }
145
+
146
+ export function toggleEditorCamera() {
147
+ if (state.editorCamActive) deactivateEditorCamera()
148
+ else activateEditorCamera()
149
+ }
150
+
151
+ export function focusSelectedEntity() {
152
+ if (state.selectedEntity === undefined || !Transform.has(state.selectedEntity)) return
153
+ const info = selectableInfoMap.get(state.selectedEntity)
154
+ const entityPos = Transform.get(state.selectedEntity).position
155
+ const offset = info?.centerOffset ?? Vector3.Zero()
156
+
157
+ editorCam.target = Vector3.create(
158
+ entityPos.x + offset.x,
159
+ entityPos.y + offset.y,
160
+ entityPos.z + offset.z,
161
+ )
162
+ editorCam.distance = FOCUS_DISTANCE
163
+
164
+ updateEditorCamera()
165
+ console.log(`[editor] focused on ${info?.name ?? 'entity'}`)
166
+ }
167
+
168
+ function getCamRight(): Vector3 {
169
+ const yawRad = editorCam.yaw * (Math.PI / 180)
170
+ return Vector3.create(Math.cos(yawRad), 0, -Math.sin(yawRad))
171
+ }
172
+
173
+ function getCamForward(): Vector3 {
174
+ const yawRad = editorCam.yaw * (Math.PI / 180)
175
+ return Vector3.create(Math.sin(yawRad), 0, Math.cos(yawRad))
176
+ }
177
+
178
+ export function editorCameraSystem(dt: number) {
179
+ if (!state.editorActive || !state.editorCamActive) return
180
+
181
+ let changed = false
182
+ const right = getCamRight()
183
+ const forward = getCamForward()
184
+
185
+ if (inputSystem.isPressed(InputAction.IA_FORWARD)) {
186
+ editorCam.target = Vector3.add(editorCam.target, Vector3.scale(forward, PAN_SPEED * dt))
187
+ changed = true
188
+ }
189
+ if (inputSystem.isPressed(InputAction.IA_BACKWARD)) {
190
+ editorCam.target = Vector3.add(editorCam.target, Vector3.scale(forward, -PAN_SPEED * dt))
191
+ changed = true
192
+ }
193
+ if (inputSystem.isPressed(InputAction.IA_RIGHT)) {
194
+ editorCam.target = Vector3.add(editorCam.target, Vector3.scale(right, PAN_SPEED * dt))
195
+ changed = true
196
+ }
197
+ if (inputSystem.isPressed(InputAction.IA_LEFT)) {
198
+ editorCam.target = Vector3.add(editorCam.target, Vector3.scale(right, -PAN_SPEED * dt))
199
+ changed = true
200
+ }
201
+ if (inputSystem.isPressed(InputAction.IA_JUMP)) {
202
+ editorCam.target.y += VERTICAL_SPEED * dt
203
+ changed = true
204
+ }
205
+ if (inputSystem.isPressed(InputAction.IA_WALK)) {
206
+ editorCam.target.y -= VERTICAL_SPEED * dt
207
+ changed = true
208
+ }
209
+ if (inputSystem.isPressed(InputAction.IA_ACTION_4)) {
210
+ editorCam.distance = Math.max(MIN_DISTANCE, editorCam.distance - ZOOM_SPEED * dt)
211
+ changed = true
212
+ }
213
+ if (inputSystem.isPressed(InputAction.IA_ACTION_5)) {
214
+ editorCam.distance = Math.min(MAX_DISTANCE, editorCam.distance + ZOOM_SPEED * dt)
215
+ changed = true
216
+ }
217
+
218
+ if (inputSystem.isPressed(InputAction.IA_POINTER) && !state.isDragging && !gizmoClickConsumed) {
219
+ const pointer = PrimaryPointerInfo.getOrNull(engine.RootEntity)
220
+ if (pointer && pointer.screenDelta) {
221
+ const dx = pointer.screenDelta.x ?? 0
222
+ const dy = pointer.screenDelta.y ?? 0
223
+ if (Math.abs(dx) > 0.001 || Math.abs(dy) > 0.001) {
224
+ editorCam.yaw += dx * ORBIT_SENSITIVITY
225
+ editorCam.pitch = Math.max(MIN_PITCH, Math.min(MAX_PITCH, editorCam.pitch + dy * ORBIT_SENSITIVITY))
226
+ changed = true
227
+ }
228
+ }
229
+ }
230
+
231
+ if (changed) updateEditorCamera()
232
+ }
233
+
234
+ // ============================================================
235
+ // Drag Lock Camera
236
+ // ============================================================
237
+
238
+ let lockCamEntity: Entity | undefined
239
+
240
+ export function createLockCamera() {
241
+ lockCamEntity = engine.addEntity()
242
+ Transform.create(lockCamEntity, {
243
+ position: Vector3.Zero(),
244
+ rotation: Quaternion.Identity(),
245
+ })
246
+ VirtualCamera.create(lockCamEntity, {
247
+ defaultTransition: { transitionMode: VirtualCamera.Transition.Time(0) },
248
+ })
249
+ editorEntities.add(lockCamEntity)
250
+ }
251
+
252
+ export function lockCamera() {
253
+ if (state.editorCamActive || lockCamEntity === undefined) return
254
+ if (!Transform.has(engine.CameraEntity)) return
255
+ const camT = Transform.get(engine.CameraEntity)
256
+ const lockT = Transform.getMutable(lockCamEntity)
257
+ copyVec3(lockT.position, camT.position)
258
+ copyQuat(lockT.rotation, camT.rotation)
259
+ MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = lockCamEntity
260
+ }
261
+
262
+ export function unlockCamera() {
263
+ if (state.editorCamActive || lockCamEntity === undefined) return
264
+ MainCamera.getMutable(engine.CameraEntity).virtualCameraEntity = undefined
265
+ }
266
+
267
+ // ============================================================
268
+ // Active Camera Helper
269
+ // ============================================================
270
+
271
+ /** Returns the active camera transform -- editor cam when active, otherwise player camera. */
272
+ export function getActiveCameraTransform() {
273
+ if (state.editorCamActive && editorCamEntity !== undefined && Transform.has(editorCamEntity)) {
274
+ return Transform.get(editorCamEntity)
275
+ }
276
+ return Transform.get(engine.CameraEntity)
277
+ }