@dcl-regenesislabs/opendcl 0.2.1-26238928766.commit-28648d7 → 0.2.1
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 +3 -5
- package/context/sdk7-cheat-sheet.md +0 -4
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/extensions/dcl-init.ts +6 -58
- package/extensions/dcl-setup-ollama.ts +312 -0
- package/package.json +4 -3
- package/prompts/system.md +41 -71
- package/skills/add-3d-models/SKILL.md +70 -120
- package/skills/add-interactivity/SKILL.md +2 -74
- package/skills/advanced-input/SKILL.md +1 -34
- package/skills/advanced-rendering/SKILL.md +9 -82
- package/skills/animations-tweens/SKILL.md +98 -203
- package/skills/audio-video/SKILL.md +83 -184
- package/skills/build-ui/SKILL.md +2 -25
- package/skills/camera-control/SKILL.md +7 -78
- package/skills/create-scene/SKILL.md +13 -56
- package/skills/deploy-scene/SKILL.md +0 -12
- package/skills/deploy-worlds/SKILL.md +0 -35
- package/skills/game-design/SKILL.md +2 -1
- package/skills/lighting-environment/SKILL.md +56 -103
- package/skills/multiplayer-sync/SKILL.md +2 -31
- package/skills/nft-blockchain/SKILL.md +40 -45
- package/skills/optimize-scene/SKILL.md +2 -7
- package/skills/player-avatar/SKILL.md +7 -133
- package/skills/scene-runtime/SKILL.md +5 -9
- package/skills/visual-feedback/SKILL.md +0 -1
- package/skills/audio-analysis/SKILL.md +0 -164
- package/skills/editor-gizmo/.gitignore +0 -11
- package/skills/editor-gizmo/SKILL.md +0 -222
- package/skills/editor-gizmo/src/__editor/camera.ts +0 -277
- package/skills/editor-gizmo/src/__editor/discovery.ts +0 -186
- package/skills/editor-gizmo/src/__editor/drag.ts +0 -265
- package/skills/editor-gizmo/src/__editor/gizmo.ts +0 -496
- package/skills/editor-gizmo/src/__editor/history.ts +0 -72
- package/skills/editor-gizmo/src/__editor/index.ts +0 -137
- package/skills/editor-gizmo/src/__editor/input.ts +0 -55
- package/skills/editor-gizmo/src/__editor/math-utils.ts +0 -114
- package/skills/editor-gizmo/src/__editor/persistence.ts +0 -113
- package/skills/editor-gizmo/src/__editor/selection.ts +0 -157
- package/skills/editor-gizmo/src/__editor/state.ts +0 -117
- package/skills/editor-gizmo/src/__editor/ui.tsx +0 -697
- package/skills/npcs/SKILL.md +0 -180
- package/skills/particle-system/SKILL.md +0 -222
- package/skills/player-physics/SKILL.md +0 -93
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/** Key bindings, mode toggle, gizmo click flag, undo/redo. */
|
|
2
|
-
|
|
3
|
-
import { inputSystem, InputAction, PointerEventType } from '@dcl/sdk/ecs'
|
|
4
|
-
import { state, gizmoClickConsumed, setGizmoClickConsumed } from './state'
|
|
5
|
-
import { createGizmo } from './gizmo'
|
|
6
|
-
import { deselectEntity } from './selection'
|
|
7
|
-
import { toggleEditorCamera, deactivateEditorCamera, focusSelectedEntity } from './camera'
|
|
8
|
-
import { undo, redo } from './history'
|
|
9
|
-
|
|
10
|
-
export function modeToggleSystem() {
|
|
11
|
-
if (!state.editorActive) return
|
|
12
|
-
|
|
13
|
-
if (inputSystem.isTriggered(InputAction.IA_PRIMARY, PointerEventType.PET_DOWN)) {
|
|
14
|
-
if (state.isDragging) return
|
|
15
|
-
state.gizmoMode = state.gizmoMode === 'translate' ? 'rotate' : 'translate'
|
|
16
|
-
console.log(`[editor] mode: ${state.gizmoMode}`)
|
|
17
|
-
if (state.selectedEntity !== undefined) createGizmo()
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// F = deselect (select mode)
|
|
21
|
-
if (inputSystem.isTriggered(InputAction.IA_SECONDARY, PointerEventType.PET_DOWN)) {
|
|
22
|
-
if (state.isDragging) return
|
|
23
|
-
if (state.selectedEntity !== undefined) {
|
|
24
|
-
deselectEntity()
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// 1 = toggle editor camera
|
|
29
|
-
if (inputSystem.isTriggered(InputAction.IA_ACTION_3, PointerEventType.PET_DOWN)) {
|
|
30
|
-
toggleEditorCamera()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// 2 = focus selected entity
|
|
34
|
-
if (inputSystem.isTriggered(InputAction.IA_ACTION_4, PointerEventType.PET_DOWN)) {
|
|
35
|
-
if (state.isDragging) return
|
|
36
|
-
if (state.selectedEntity !== undefined) {
|
|
37
|
-
if (!state.editorCamActive) toggleEditorCamera()
|
|
38
|
-
focusSelectedEntity()
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 4 = undo, Shift+4 = redo
|
|
43
|
-
if (inputSystem.isTriggered(InputAction.IA_ACTION_6, PointerEventType.PET_DOWN)) {
|
|
44
|
-
if (state.isDragging) return
|
|
45
|
-
if (inputSystem.isPressed(InputAction.IA_WALK)) {
|
|
46
|
-
redo()
|
|
47
|
-
} else {
|
|
48
|
-
undo()
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function resetGizmoClickFlag() {
|
|
54
|
-
if (gizmoClickConsumed) setGizmoClickConsumed(false)
|
|
55
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/** Pure math utilities — no ECS imports, fully unit-testable. */
|
|
2
|
-
|
|
3
|
-
import { Vector3 } from '@dcl/sdk/math'
|
|
4
|
-
import { Axis } from './state'
|
|
5
|
-
|
|
6
|
-
export function axisToVector(axis: Axis): Vector3 {
|
|
7
|
-
switch (axis) {
|
|
8
|
-
case 'x': return Vector3.Right()
|
|
9
|
-
case 'y': return Vector3.Up()
|
|
10
|
-
case 'z': return Vector3.Forward()
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Pick the drag plane normal most perpendicular to the camera forward.
|
|
16
|
-
* Avoids grazing-angle intersections.
|
|
17
|
-
*
|
|
18
|
-
* When worldAxis is provided (parent-rotated), candidates are computed
|
|
19
|
-
* as the two axes perpendicular to it. Otherwise falls back to world axes.
|
|
20
|
-
*/
|
|
21
|
-
export function getDragPlaneNormal(axis: Axis, cameraForward: Vector3, worldAxis?: Vector3): Vector3 {
|
|
22
|
-
let candidates: Vector3[]
|
|
23
|
-
|
|
24
|
-
if (worldAxis) {
|
|
25
|
-
// Build two axes perpendicular to the (potentially rotated) drag axis
|
|
26
|
-
const up = Vector3.Up()
|
|
27
|
-
let perp1 = Vector3.cross(worldAxis, up)
|
|
28
|
-
if (Vector3.length(perp1) < 0.001) {
|
|
29
|
-
// worldAxis is nearly vertical — use Forward as fallback
|
|
30
|
-
perp1 = Vector3.cross(worldAxis, Vector3.Forward())
|
|
31
|
-
}
|
|
32
|
-
perp1 = Vector3.normalize(perp1)
|
|
33
|
-
const perp2 = Vector3.normalize(Vector3.cross(worldAxis, perp1))
|
|
34
|
-
candidates = [perp1, perp2]
|
|
35
|
-
} else {
|
|
36
|
-
const others = getOtherAxes(axis)
|
|
37
|
-
candidates = [axisToVector(others[0]), axisToVector(others[1])]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let best = candidates[0]
|
|
41
|
-
let bestDot = 0
|
|
42
|
-
for (const n of candidates) {
|
|
43
|
-
const d = Math.abs(Vector3.dot(cameraForward, n))
|
|
44
|
-
if (d > bestDot) { bestDot = d; best = n }
|
|
45
|
-
}
|
|
46
|
-
return best
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Cast a ray against an infinite plane. Returns hit point or null. */
|
|
50
|
-
export function rayPlaneIntersect(
|
|
51
|
-
rayOrigin: Vector3, rayDir: Vector3, planePoint: Vector3, planeNormal: Vector3
|
|
52
|
-
): Vector3 | null {
|
|
53
|
-
const denom = Vector3.dot(planeNormal, rayDir)
|
|
54
|
-
if (Math.abs(denom) < 1e-6) return null
|
|
55
|
-
const diff = Vector3.subtract(planePoint, rayOrigin)
|
|
56
|
-
const t = Vector3.dot(diff, planeNormal) / denom
|
|
57
|
-
if (t < 0) return null
|
|
58
|
-
return Vector3.add(rayOrigin, Vector3.scale(rayDir, t))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Compute the angle of a hit point on the rotation plane via atan2. */
|
|
62
|
-
export function hitAngleOnPlane(hit: Vector3, center: Vector3, axis: Axis): number {
|
|
63
|
-
const d = Vector3.subtract(hit, center)
|
|
64
|
-
switch (axis) {
|
|
65
|
-
case 'x': return Math.atan2(d.z, d.y)
|
|
66
|
-
case 'y': return Math.atan2(d.x, d.z)
|
|
67
|
-
case 'z': return Math.atan2(d.y, d.x)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function copyVec3(dst: { x: number; y: number; z: number }, src: { x: number; y: number; z: number }) {
|
|
72
|
-
dst.x = src.x; dst.y = src.y; dst.z = src.z
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function copyQuat(dst: { x: number; y: number; z: number; w: number }, src: { x: number; y: number; z: number; w: number }) {
|
|
76
|
-
dst.x = src.x; dst.y = src.y; dst.z = src.z; dst.w = src.w
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Compute the angle of a hit point on an arbitrarily oriented rotation plane.
|
|
81
|
-
* worldAxis is the plane normal (rotation axis in world space).
|
|
82
|
-
*/
|
|
83
|
-
export function hitAngleOnWorldPlane(hit: Vector3, center: Vector3, worldAxis: Vector3): number {
|
|
84
|
-
const d = Vector3.subtract(hit, center)
|
|
85
|
-
// Build two perpendicular vectors in the plane
|
|
86
|
-
const up = Math.abs(Vector3.dot(worldAxis, Vector3.Up())) < 0.99
|
|
87
|
-
? Vector3.Up() : Vector3.Forward()
|
|
88
|
-
const u = Vector3.normalize(Vector3.cross(worldAxis, up))
|
|
89
|
-
const v = Vector3.normalize(Vector3.cross(worldAxis, u))
|
|
90
|
-
return Math.atan2(Vector3.dot(d, v), Vector3.dot(d, u))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function getOtherAxes(axis: Axis): [Axis, Axis] {
|
|
94
|
-
switch (axis) {
|
|
95
|
-
case 'x': return ['y', 'z']
|
|
96
|
-
case 'y': return ['x', 'z']
|
|
97
|
-
case 'z': return ['x', 'y']
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function applyFlatTransform(
|
|
102
|
-
t: { position: {x:number;y:number;z:number}; rotation: {x:number;y:number;z:number;w:number}; scale: {x:number;y:number;z:number} },
|
|
103
|
-
s: { px:number; py:number; pz:number; rx:number; ry:number; rz:number; rw:number; sx:number; sy:number; sz:number }
|
|
104
|
-
) {
|
|
105
|
-
t.position.x = s.px; t.position.y = s.py; t.position.z = s.pz
|
|
106
|
-
t.rotation.x = s.rx; t.rotation.y = s.ry; t.rotation.z = s.rz; t.rotation.w = s.rw
|
|
107
|
-
t.scale.x = s.sx; t.scale.y = s.sy; t.scale.z = s.sz
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Round a number to N decimal places. */
|
|
111
|
-
export function round(v: number, decimals: number = 2): number {
|
|
112
|
-
const f = Math.pow(10, decimals)
|
|
113
|
-
return Math.round(v * f) / f
|
|
114
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Persistence — saves entity transforms to the preview server via HTTP.
|
|
3
|
-
*
|
|
4
|
-
* POST {baseUrl}/editor/changes → merge an entity update into main-entities.ts
|
|
5
|
-
*
|
|
6
|
-
* The server (sdk-commands in preview, opendcl-studio in web) is responsible
|
|
7
|
-
* for writing main-entities.ts on disk and triggering main.crdt regeneration.
|
|
8
|
-
*
|
|
9
|
-
* Uses `signedFetch` instead of plain `fetch` because opendcl-studio
|
|
10
|
-
* auth-gates this endpoint (signer address must match the scene owner).
|
|
11
|
-
* In the CLI preview server the endpoint is unauthenticated; the extra
|
|
12
|
-
* AuthChain headers signedFetch sends are simply ignored there. So the
|
|
13
|
-
* same call shape works in both contexts.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { Entity, Transform, engine, RealmInfo } from '@dcl/sdk/ecs'
|
|
17
|
-
import { signedFetch } from '~system/SignedFetch'
|
|
18
|
-
import { selectableInfoMap } from './state'
|
|
19
|
-
import { round } from './math-utils'
|
|
20
|
-
|
|
21
|
-
interface TransformPayload {
|
|
22
|
-
position: { x: number; y: number; z: number }
|
|
23
|
-
rotation: { x: number; y: number; z: number; w: number }
|
|
24
|
-
scale: { x: number; y: number; z: number }
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface ChangePayload {
|
|
28
|
-
components: { Transform: TransformPayload }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type ChangesMap = Record<string, ChangePayload>
|
|
32
|
-
|
|
33
|
-
// Resolve baseUrl lazily at each send. RealmInfo is populated by the
|
|
34
|
-
// runtime AFTER scene-module-load completes, so caching it once at
|
|
35
|
-
// init time was racing with module init and pinning `null` forever —
|
|
36
|
-
// the gizmo would work but every drag-end silently dropped the request.
|
|
37
|
-
export function initPersistence(): void {
|
|
38
|
-
// Kept for symmetry with the editor's bootstrap call; no longer
|
|
39
|
-
// resolves baseUrl up front (see comment above).
|
|
40
|
-
const baseUrl = RealmInfo.getOrNull(engine.RootEntity)?.baseUrl
|
|
41
|
-
console.log(`[editor] persistence ready (baseUrl=${baseUrl ?? 'unresolved'})`)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function resolveBaseUrl(): string | null {
|
|
45
|
-
return RealmInfo.getOrNull(engine.RootEntity)?.baseUrl ?? null
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Send the current transform of an entity to the server for persistence. */
|
|
49
|
-
export function sendEntityUpdate(entity: Entity) {
|
|
50
|
-
const baseUrl = resolveBaseUrl()
|
|
51
|
-
if (!baseUrl) {
|
|
52
|
-
console.log('[editor] sendEntityUpdate: no realm baseUrl yet')
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
if (!Transform.has(entity)) return
|
|
56
|
-
const info = selectableInfoMap.get(entity)
|
|
57
|
-
if (!info) return
|
|
58
|
-
|
|
59
|
-
const t = Transform.get(entity)
|
|
60
|
-
const payload: ChangesMap = {
|
|
61
|
-
[info.name]: {
|
|
62
|
-
components: {
|
|
63
|
-
Transform: {
|
|
64
|
-
position: { x: round(t.position.x), y: round(t.position.y), z: round(t.position.z) },
|
|
65
|
-
rotation: {
|
|
66
|
-
x: round(t.rotation.x, 4),
|
|
67
|
-
y: round(t.rotation.y, 4),
|
|
68
|
-
z: round(t.rotation.z, 4),
|
|
69
|
-
w: round(t.rotation.w, 4),
|
|
70
|
-
},
|
|
71
|
-
scale: { x: round(t.scale.x), y: round(t.scale.y), z: round(t.scale.z) },
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const url = `${baseUrl}/editor/changes`
|
|
78
|
-
const body = JSON.stringify(payload)
|
|
79
|
-
|
|
80
|
-
// signedFetch refuses non-https URLs by design. In local dev the studio
|
|
81
|
-
// serves http://localhost:3001 — fall back to plain fetch there. The
|
|
82
|
-
// server-side auth gate is correspondingly relaxed when DEV=true: in
|
|
83
|
-
// prod the editor-changes endpoint requires signedFetch + ownership.
|
|
84
|
-
if (url.startsWith('http://')) {
|
|
85
|
-
fetch(url, {
|
|
86
|
-
method: 'POST',
|
|
87
|
-
headers: { 'Content-Type': 'application/json' },
|
|
88
|
-
body,
|
|
89
|
-
})
|
|
90
|
-
.then((res) => {
|
|
91
|
-
if (!res.ok) console.log(`[editor] save returned ${res.status}`)
|
|
92
|
-
})
|
|
93
|
-
.catch((e) => console.log(`[editor] save failed: ${e}`))
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
signedFetch({
|
|
98
|
-
url,
|
|
99
|
-
init: {
|
|
100
|
-
method: 'POST',
|
|
101
|
-
headers: { 'Content-Type': 'application/json' },
|
|
102
|
-
body,
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
.then((res) => {
|
|
106
|
-
// signedFetch does NOT reject on non-2xx — log status so silent
|
|
107
|
-
// 401 (signer ≠ scene owner) or 400 (validation) is visible.
|
|
108
|
-
if (res.status !== undefined && (res.status < 200 || res.status >= 300)) {
|
|
109
|
-
console.log(`[editor] save returned ${res.status}: ${res.body ?? ''}`)
|
|
110
|
-
}
|
|
111
|
-
})
|
|
112
|
-
.catch((e) => console.log(`[editor] save failed: ${e}`))
|
|
113
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/** Select/deselect entities, highlight, collider management. */
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Entity,
|
|
5
|
-
Transform,
|
|
6
|
-
MeshCollider,
|
|
7
|
-
Material,
|
|
8
|
-
MaterialTransparencyMode,
|
|
9
|
-
GltfContainer,
|
|
10
|
-
GltfNodeModifiers,
|
|
11
|
-
ColliderLayer,
|
|
12
|
-
PointerEvents,
|
|
13
|
-
pointerEventsSystem,
|
|
14
|
-
InputAction,
|
|
15
|
-
} from '@dcl/sdk/ecs'
|
|
16
|
-
import { Color4, Color3 } from '@dcl/sdk/math'
|
|
17
|
-
import { state, selectableInfoMap, originalMaterials, gizmoClickConsumed } from './state'
|
|
18
|
-
import { createGizmo, destroyGizmo } from './gizmo'
|
|
19
|
-
|
|
20
|
-
const HIGHLIGHT_EMISSIVE = 0.6
|
|
21
|
-
const HIGHLIGHT_ALPHA = 0.35
|
|
22
|
-
|
|
23
|
-
/** Remove all collision so clicks pass through to gizmo handles behind the entity. */
|
|
24
|
-
function disableCollider(entity: Entity) {
|
|
25
|
-
if (MeshCollider.has(entity)) {
|
|
26
|
-
MeshCollider.deleteFrom(entity)
|
|
27
|
-
}
|
|
28
|
-
if (GltfContainer.has(entity)) {
|
|
29
|
-
const gltf = GltfContainer.getMutable(entity)
|
|
30
|
-
gltf.visibleMeshesCollisionMask = 0
|
|
31
|
-
gltf.invisibleMeshesCollisionMask = 0
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function restoreCollider(entity: Entity) {
|
|
36
|
-
const info = selectableInfoMap.get(entity)
|
|
37
|
-
if (!info) return
|
|
38
|
-
|
|
39
|
-
switch (info.colliderShape) {
|
|
40
|
-
case 'box':
|
|
41
|
-
MeshCollider.setBox(entity, ColliderLayer.CL_POINTER)
|
|
42
|
-
break
|
|
43
|
-
case 'sphere':
|
|
44
|
-
MeshCollider.setSphere(entity, ColliderLayer.CL_POINTER)
|
|
45
|
-
break
|
|
46
|
-
case 'cylinder':
|
|
47
|
-
MeshCollider.setCylinder(entity, undefined, undefined, ColliderLayer.CL_POINTER)
|
|
48
|
-
break
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (GltfContainer.has(entity) && info.originalVisibleMask !== undefined) {
|
|
52
|
-
const gltf = GltfContainer.getMutable(entity)
|
|
53
|
-
gltf.visibleMeshesCollisionMask = info.originalVisibleMask
|
|
54
|
-
gltf.invisibleMeshesCollisionMask = info.originalInvisibleMask ?? 0
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function highlight(entity: Entity) {
|
|
59
|
-
// GLB models: use GltfNodeModifiers with empty path to make all nodes semi-transparent
|
|
60
|
-
if (GltfContainer.has(entity)) {
|
|
61
|
-
GltfNodeModifiers.createOrReplace(entity, {
|
|
62
|
-
modifiers: [{
|
|
63
|
-
path: '',
|
|
64
|
-
material: {
|
|
65
|
-
material: {
|
|
66
|
-
$case: 'pbr' as const,
|
|
67
|
-
pbr: {
|
|
68
|
-
albedoColor: Color4.create(1, 1, 1, HIGHLIGHT_ALPHA),
|
|
69
|
-
transparencyMode: MaterialTransparencyMode.MTM_ALPHA_BLEND,
|
|
70
|
-
metallic: 0.1,
|
|
71
|
-
roughness: 0.5,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}]
|
|
76
|
-
})
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Primitives: override Material directly
|
|
81
|
-
const m = originalMaterials.get(entity)
|
|
82
|
-
if (!m) return
|
|
83
|
-
Material.setPbrMaterial(entity, {
|
|
84
|
-
albedoColor: Color4.create(m.r, m.g, m.b, HIGHLIGHT_ALPHA),
|
|
85
|
-
emissiveColor: Color3.create(m.r * 0.4, m.g * 0.4, m.b * 0.4),
|
|
86
|
-
emissiveIntensity: HIGHLIGHT_EMISSIVE,
|
|
87
|
-
metallic: 0.1,
|
|
88
|
-
roughness: 0.4,
|
|
89
|
-
transparencyMode: MaterialTransparencyMode.MTM_ALPHA_BLEND,
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function unhighlight(entity: Entity) {
|
|
94
|
-
// GLB models: remove the modifier
|
|
95
|
-
if (GltfContainer.has(entity) && GltfNodeModifiers.has(entity)) {
|
|
96
|
-
GltfNodeModifiers.deleteFrom(entity)
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Primitives: restore original material
|
|
101
|
-
const m = originalMaterials.get(entity)
|
|
102
|
-
if (!m) return
|
|
103
|
-
Material.setPbrMaterial(entity, {
|
|
104
|
-
albedoColor: Color4.create(m.r, m.g, m.b, m.a),
|
|
105
|
-
metallic: 0.1,
|
|
106
|
-
roughness: 0.5,
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function selectEntity(entity: Entity) {
|
|
111
|
-
if (state.selectedEntity === entity) {
|
|
112
|
-
deselectEntity()
|
|
113
|
-
return
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const info = selectableInfoMap.get(entity)
|
|
117
|
-
if (!info) return
|
|
118
|
-
|
|
119
|
-
// Deselect previous
|
|
120
|
-
if (state.selectedEntity !== undefined) {
|
|
121
|
-
deselectEntity()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
state.selectedEntity = entity
|
|
125
|
-
state.selectedName = info.name
|
|
126
|
-
// Remove pointer events completely so model doesn't intercept gizmo clicks
|
|
127
|
-
pointerEventsSystem.removeOnPointerDown(entity)
|
|
128
|
-
if (PointerEvents.has(entity)) PointerEvents.deleteFrom(entity)
|
|
129
|
-
highlight(entity)
|
|
130
|
-
disableCollider(entity)
|
|
131
|
-
createGizmo()
|
|
132
|
-
console.log(`[editor] selected: ${info.name}`)
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function deselectEntity() {
|
|
136
|
-
if (state.selectedEntity === undefined) return
|
|
137
|
-
|
|
138
|
-
const entity = state.selectedEntity
|
|
139
|
-
const info = selectableInfoMap.get(entity)
|
|
140
|
-
unhighlight(entity)
|
|
141
|
-
restoreCollider(entity)
|
|
142
|
-
destroyGizmo()
|
|
143
|
-
|
|
144
|
-
// Restore pointer event so entity is clickable again
|
|
145
|
-
if (info) {
|
|
146
|
-
pointerEventsSystem.onPointerDown(
|
|
147
|
-
{ entity, opts: { button: InputAction.IA_POINTER, hoverText: `Select ${info.name}`, maxDistance: 100 } },
|
|
148
|
-
() => {
|
|
149
|
-
if (!state.editorActive || state.isDragging || gizmoClickConsumed) return
|
|
150
|
-
selectEntity(entity)
|
|
151
|
-
}
|
|
152
|
-
)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
state.selectedEntity = undefined
|
|
156
|
-
state.selectedName = ''
|
|
157
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/** Shared editor state — the single source of truth for all editor modules. */
|
|
2
|
-
|
|
3
|
-
import { Entity } from '@dcl/sdk/ecs'
|
|
4
|
-
import { Vector3, Quaternion } from '@dcl/sdk/math'
|
|
5
|
-
|
|
6
|
-
export const EDITOR_VERSION = '0.6.0'
|
|
7
|
-
|
|
8
|
-
export type Axis = 'x' | 'y' | 'z'
|
|
9
|
-
export type GizmoMode = 'translate' | 'rotate'
|
|
10
|
-
|
|
11
|
-
/** Maximum depth for parent chain walking (prevents infinite loops). */
|
|
12
|
-
export const MAX_PARENT_DEPTH = 16
|
|
13
|
-
|
|
14
|
-
// ── Per-entity info (populated by discovery) ────────────
|
|
15
|
-
|
|
16
|
-
export interface SelectableInfo {
|
|
17
|
-
name: string
|
|
18
|
-
centerOffset: { x: number; y: number; z: number }
|
|
19
|
-
boundsSize: { x: number; y: number; z: number }
|
|
20
|
-
isModel: boolean
|
|
21
|
-
colliderShape: 'box' | 'sphere' | 'cylinder'
|
|
22
|
-
originalVisibleMask?: number
|
|
23
|
-
originalInvisibleMask?: number
|
|
24
|
-
src?: string
|
|
25
|
-
meshType?: 'box' | 'sphere' | 'cylinder'
|
|
26
|
-
parentEntity?: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ── Editor state ────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
export interface EditorState {
|
|
32
|
-
selectedEntity: Entity | undefined
|
|
33
|
-
selectedName: string
|
|
34
|
-
gizmoMode: GizmoMode
|
|
35
|
-
isDragging: boolean
|
|
36
|
-
dragAxis: Axis
|
|
37
|
-
|
|
38
|
-
// Translate drag
|
|
39
|
-
dragPlaneMode: Axis | undefined // when set, drag on plane perpendicular to this axis
|
|
40
|
-
dragStartPos: { x: number; y: number; z: number }
|
|
41
|
-
dragStartWorldPos: { x: number; y: number; z: number }
|
|
42
|
-
dragStartHit: { x: number; y: number; z: number }
|
|
43
|
-
dragPlaneNormal: { x: number; y: number; z: number }
|
|
44
|
-
|
|
45
|
-
// Rotate drag
|
|
46
|
-
dragStartRot: { x: number; y: number; z: number; w: number }
|
|
47
|
-
dragStartAngle: number
|
|
48
|
-
dragRotCenter: { x: number; y: number; z: number }
|
|
49
|
-
|
|
50
|
-
// Camera
|
|
51
|
-
editorCamActive: boolean
|
|
52
|
-
|
|
53
|
-
// Editor toggle (starts ON in preview)
|
|
54
|
-
editorActive: boolean
|
|
55
|
-
|
|
56
|
-
// Whether the scene is running in preview mode (set on init)
|
|
57
|
-
isPreview: boolean
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const state: EditorState = {
|
|
61
|
-
selectedEntity: undefined,
|
|
62
|
-
selectedName: '',
|
|
63
|
-
gizmoMode: 'translate',
|
|
64
|
-
isDragging: false,
|
|
65
|
-
dragPlaneMode: undefined,
|
|
66
|
-
dragAxis: 'x',
|
|
67
|
-
dragStartPos: Vector3.Zero(),
|
|
68
|
-
dragStartWorldPos: Vector3.Zero(),
|
|
69
|
-
dragStartHit: Vector3.Zero(),
|
|
70
|
-
dragPlaneNormal: Vector3.Up(),
|
|
71
|
-
dragStartRot: Quaternion.Identity(),
|
|
72
|
-
dragStartAngle: 0,
|
|
73
|
-
dragRotCenter: Vector3.Zero(),
|
|
74
|
-
editorCamActive: false,
|
|
75
|
-
editorActive: true,
|
|
76
|
-
isPreview: false,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ── Entity tracking ─────────────────────────────────────
|
|
80
|
-
|
|
81
|
-
/** Entities created by the editor (gizmo, ground plane) — skipped by discovery. */
|
|
82
|
-
export const editorEntities = new Set<Entity>()
|
|
83
|
-
|
|
84
|
-
/** Discovered scene entities → their info. */
|
|
85
|
-
export const selectableInfoMap = new Map<Entity, SelectableInfo>()
|
|
86
|
-
|
|
87
|
-
/** Original material colors for primitive highlight/unhighlight. */
|
|
88
|
-
export const originalMaterials = new Map<Entity, { r: number; g: number; b: number; a: number }>()
|
|
89
|
-
|
|
90
|
-
// ── Gizmo entities ──────────────────────────────────────
|
|
91
|
-
|
|
92
|
-
export const gizmoEntities: Entity[] = []
|
|
93
|
-
|
|
94
|
-
export let gizmoRoot: Entity | undefined
|
|
95
|
-
export function setGizmoRoot(e: Entity | undefined) { gizmoRoot = e }
|
|
96
|
-
|
|
97
|
-
export const handleAxisMap = new Map<Entity, Axis>()
|
|
98
|
-
export const handleDiscMap = new Map<Entity, Entity>()
|
|
99
|
-
export const handleArrowMap = new Map<Entity, Entity[]>()
|
|
100
|
-
|
|
101
|
-
// ── Editor toggle callback ──────────────────────────────
|
|
102
|
-
|
|
103
|
-
/** Set by index.ts to handle toggle cleanup (deselect, camera off, etc.) */
|
|
104
|
-
let _onToggle: (() => void) | undefined
|
|
105
|
-
export function setToggleHandler(fn: () => void) { _onToggle = fn }
|
|
106
|
-
|
|
107
|
-
/** Toggle the editor on/off. Safe to call from UI — no circular deps. */
|
|
108
|
-
export function toggleEditorActive() {
|
|
109
|
-
if (!state.isPreview) return
|
|
110
|
-
if (_onToggle) _onToggle()
|
|
111
|
-
else state.editorActive = !state.editorActive
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ── Click consumption flag ──────────────────────────────
|
|
115
|
-
|
|
116
|
-
export let gizmoClickConsumed = false
|
|
117
|
-
export function setGizmoClickConsumed(v: boolean) { gizmoClickConsumed = v }
|