lecodes-sdk 1.1.0 → 2.0.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 +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +11 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Particles.d.ts +15 -0
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +59 -3
- package/dist/types/gl/terrain/Terrain.d.ts +8 -0
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +8 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +142 -142
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +77 -54
- package/prompts/dist/3d-app.md +75 -52
- package/prompts/dist/ar-app.md +74 -51
- package/prompts/dist/design.md +30 -8
- package/prompts/dist/ui-app.md +73 -46
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +262 -378
- package/src/animate/tween/animateValue.ts +103 -100
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +505 -479
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +162 -161
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +13 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Model.ts +172 -167
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +20 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/Locomotion.ts +77 -8
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/terrain/Terrain.ts +33 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +233 -236
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +21 -8
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +154 -148
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4730
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1760
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// The scene EDITOR's side of a loaded scene: everything a scene editor does to a `.scene.ts` world
|
|
2
|
+
// while it is edited — live patches, rename / reparent, editor-run aspects (generators) and their
|
|
3
|
+
// gizmo lines, custom inspector cards, the preview pose, live materials, the plugin registry.
|
|
4
|
+
//
|
|
5
|
+
// ONE object, `sceneEditor`, is the whole contract between the SDK and an editor's in-world half
|
|
6
|
+
// (the scene-editor harness): the harness names nothing else of the SDK's internals — no `_`
|
|
7
|
+
// member of a handle, a node or a material. It reaches it as the global `__sceneEditor`, which
|
|
8
|
+
// exists in EDITOR BUNDLES only (src/inject.editor.ts, injected by a compile with `editor: true`);
|
|
9
|
+
// a shipped bundle carries none of this file — nothing in it references the module, and the
|
|
10
|
+
// edit-mode branches of ./defineScene.ts that feed it fold away with `EDITOR`
|
|
11
|
+
// (sdk/tests/scene/editor-contract.test.ts holds both: the members, and their absence when shipped).
|
|
12
|
+
//
|
|
13
|
+
// A change here is a change of that contract: the harness is compiled against whatever SDK the
|
|
14
|
+
// compiling host has, so a member is added, never renamed or re-shaped in place.
|
|
15
|
+
|
|
16
|
+
import type { AspectCtor } from "../core/Aspect"
|
|
17
|
+
import { describeAspect, describeFields, type AspectClassInfo } from "../core/fields"
|
|
18
|
+
import { InspectorUI, type InspectorEvent, type InspectorWidget } from "../core/InspectorUI"
|
|
19
|
+
import type { Material } from "../gl/Material"
|
|
20
|
+
import { Model } from "../gl/Model"
|
|
21
|
+
import type { Node } from "../gl/Node"
|
|
22
|
+
import type { Scene } from "../gl/Scene"
|
|
23
|
+
import {
|
|
24
|
+
addEditorMarker, applyCameraProjection, applyEditorPose, applyNode, assignRefProps, attachAspects,
|
|
25
|
+
attachHost, attachPrefab, bumpGizmoVersion, createSource, currentGizmoVersion, foreignRuns,
|
|
26
|
+
MAKE_INDEX, modelPartRows, safeRebuild,
|
|
27
|
+
type ModelPartRow, type SceneHandle, type SceneNodeDef,
|
|
28
|
+
} from "./defineScene"
|
|
29
|
+
import { __editorPlugins } from "./editorPlugins"
|
|
30
|
+
import { GizmoBuffer, withGizmoScope, type GizmoBatch } from "./gizmos"
|
|
31
|
+
import { collectRefDeps, isEditMode, isNodeRef, resolveRefs, type AspectEntry } from "./grammar"
|
|
32
|
+
import { applyMaterialParam, assignMaterialDef } from "./material"
|
|
33
|
+
|
|
34
|
+
type Handle = SceneHandle<any>
|
|
35
|
+
|
|
36
|
+
/** Custom-inspector cards (`static inspector`) of a handle, per `<host>:<index>` — see inspectorRender. */
|
|
37
|
+
type InspectorCard = { ui: InspectorUI, inst: Record<string, unknown>, node: Node }
|
|
38
|
+
const inspectorCards = new WeakMap<Handle, Map<string, InspectorCard>>()
|
|
39
|
+
const cardsOf = (handle: Handle): Map<string, InspectorCard> => {
|
|
40
|
+
let cards = inspectorCards.get(handle)
|
|
41
|
+
if (!cards) { cards = new Map(); inspectorCards.set(handle, cards) }
|
|
42
|
+
return cards
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Re-derive every editor run's deps from the CURRENT record. Deps are RESOLVED absolute paths, so
|
|
46
|
+
* any structural change can invalidate them: an add can satisfy a previously-null ref, a
|
|
47
|
+
* remove/rename can re-bind one to a different scope (shadowing). */
|
|
48
|
+
const refreshRunDeps = (handle: Handle): void => {
|
|
49
|
+
const nodes = handle._live?.nodes
|
|
50
|
+
if (!nodes) return
|
|
51
|
+
for (const run of handle._editorRuns) {
|
|
52
|
+
run.deps = collectRefDeps(run.props, nodes, run.hostPath)
|
|
53
|
+
// aspect runs keep their host as a dep (see attachAspects); make() runs must NOT — the
|
|
54
|
+
// wrapper's transform is editor-owned and moving it never re-calls the factory
|
|
55
|
+
if (run.index !== MAKE_INDEX) run.deps.add(run.hostPath)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Re-key everything addressed under `oldPath` (the node itself, descendants, editor runs,
|
|
60
|
+
* inspector cards) to `newPath`, then re-derive deps. The record object is shared with the host —
|
|
61
|
+
* mutation, not replacement. */
|
|
62
|
+
const rekey = (handle: Handle, oldPath: string, newPath: string): void => {
|
|
63
|
+
const nodes = handle._live!.nodes
|
|
64
|
+
const move = (key: string): string | null =>
|
|
65
|
+
key === oldPath ? newPath
|
|
66
|
+
: key.startsWith(oldPath + "/") ? newPath + key.slice(oldPath.length)
|
|
67
|
+
: null
|
|
68
|
+
for (const key of Object.keys(nodes)) {
|
|
69
|
+
const next = move(key)
|
|
70
|
+
if (next === null) continue
|
|
71
|
+
const n = nodes[key]
|
|
72
|
+
delete nodes[key]
|
|
73
|
+
nodes[next] = n
|
|
74
|
+
}
|
|
75
|
+
for (const run of handle._editorRuns) {
|
|
76
|
+
const next = move(run.hostPath)
|
|
77
|
+
if (next !== null) run.hostPath = next
|
|
78
|
+
}
|
|
79
|
+
const cards = cardsOf(handle)
|
|
80
|
+
for (const [ key, card ] of [ ...cards ]) {
|
|
81
|
+
const i = key.lastIndexOf(":")
|
|
82
|
+
const next = move(key.slice(0, i))
|
|
83
|
+
if (next === null) continue
|
|
84
|
+
cards.delete(key)
|
|
85
|
+
cards.set(next + key.slice(i), card)
|
|
86
|
+
}
|
|
87
|
+
refreshRunDeps(handle)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Apply ONE node's change to the already-loaded scene without recompiling — the same "scenes as
|
|
92
|
+
* data" grammar, but as a patch: `def` rebuilds the node at `path` in place (or adds it when the
|
|
93
|
+
* path is new — the path's parent must exist, root paths mount at the root), `def === null`
|
|
94
|
+
* removes it with its whole subtree. The def must be PLAIN data — the editor falls back to a full
|
|
95
|
+
* re-run for `$expr` and `$asset` values; aspect changes are inert in edit mode and stay out of defs.
|
|
96
|
+
*
|
|
97
|
+
* Named children survive a rebuild: they are re-parented onto the replacement node keeping their
|
|
98
|
+
* local transforms (exactly what the scene file describes). Returns the fresh node, null for a
|
|
99
|
+
* removal (or an add under an unknown parent). Old GPU resources (geometry/material instances)
|
|
100
|
+
* are not reclaimed until the next full re-run — acceptable churn for an edit session.
|
|
101
|
+
*/
|
|
102
|
+
const patchNode = async (handle: Handle, path: string, def: SceneNodeDef | null): Promise<Node | null> => {
|
|
103
|
+
const { scene, nodes } = (await handle.load()) as unknown as { scene: Scene, nodes: Record<string, Node> }
|
|
104
|
+
const old: Node | undefined = nodes[path]
|
|
105
|
+
|
|
106
|
+
const dispose = (root: Node): void => {
|
|
107
|
+
// Hide first: the visibility cascade deactivates the subtree's pick colliders (the ABI has
|
|
108
|
+
// no removeCollider — a destroyed entity's stale collider entry must never pick again).
|
|
109
|
+
root.visible = false
|
|
110
|
+
const doomed = new Set<Node>()
|
|
111
|
+
root.traverse((n) => doomed.add(n))
|
|
112
|
+
for (const [ key, n ] of Object.entries(nodes)) if (doomed.has(n)) delete nodes[key]
|
|
113
|
+
// editor-run aspect instances hosted in the doomed subtree go with it (generated children
|
|
114
|
+
// are subtree children, so the destroy below reclaims them too)
|
|
115
|
+
handle._editorRuns = handle._editorRuns.filter((r) => !doomed.has(r.node))
|
|
116
|
+
bumpGizmoVersion()
|
|
117
|
+
scene.remove(root)
|
|
118
|
+
root.destroy() // native destroyEntity recurses over remaining children
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (def === null) {
|
|
122
|
+
if (old) {
|
|
123
|
+
dispose(old)
|
|
124
|
+
refreshRunDeps(handle)
|
|
125
|
+
}
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const build = async (p: string, d: SceneNodeDef, parent: Node | null): Promise<Node> => {
|
|
130
|
+
// child reuse is by FULL path — only the live subtree at this exact address is carried
|
|
131
|
+
// over (a bare-name lookup would adopt a like-named node from anywhere in the scene)
|
|
132
|
+
const existing = p === path ? undefined : nodes[p]
|
|
133
|
+
if (existing) { // an already-live child subtree — keep it, just re-parent (local transform stays)
|
|
134
|
+
if (parent) attachHost(parent, d, p).add(existing)
|
|
135
|
+
return existing
|
|
136
|
+
}
|
|
137
|
+
const node = await createSource(p, d, false)
|
|
138
|
+
if (parent) attachHost(parent, d, p).add(node)
|
|
139
|
+
scene.add(node)
|
|
140
|
+
applyNode(node, p.slice(p.lastIndexOf("/") + 1), d)
|
|
141
|
+
if (isEditMode()) addEditorMarker(node, d)
|
|
142
|
+
// a prefab wrapper instantiates its file's nodes (same path as buildNodes; no lightmap
|
|
143
|
+
// context — a patched node is an edit-mode rebuild, baked lighting is the level's business)
|
|
144
|
+
if (d.prefab !== undefined) await attachPrefab(node, p, d, scene, new Set([ handle.def ]), null)
|
|
145
|
+
attachAspects(node, p, d, nodes, scene, handle._editorRuns)
|
|
146
|
+
nodes[p] = node
|
|
147
|
+
await Promise.all(Object.entries(d.children ?? {}).map(([ cn, cd ]) => build(`${p}/${cn}`, cd, node)))
|
|
148
|
+
return node
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const cut = path.lastIndexOf("/")
|
|
152
|
+
const parentPath = cut < 0 ? null : path.slice(0, cut)
|
|
153
|
+
if (!old && parentPath !== null && !nodes[parentPath]) return null
|
|
154
|
+
const parent = old ? old.parent : (parentPath !== null ? nodes[parentPath] : null)
|
|
155
|
+
const fresh = await build(path, def, parent)
|
|
156
|
+
if (old) {
|
|
157
|
+
// named children not listed in the def still move over (the editor patches one node at a time)
|
|
158
|
+
const named = new Set(Object.values(nodes))
|
|
159
|
+
for (const child of old.children) if (named.has(child)) fresh.add(child)
|
|
160
|
+
dispose(old) // fresh already replaced nodes[path] in build(), so it survives
|
|
161
|
+
nodes[path] = fresh
|
|
162
|
+
}
|
|
163
|
+
// the projection lives on the scene camera, not on the node — re-apply it here so an inspector
|
|
164
|
+
// fov/near/far edit lands live (a rebuilt node alone would carry none of it)
|
|
165
|
+
if (def.camera !== undefined) applyCameraProjection(scene, def.camera, true)
|
|
166
|
+
refreshRunDeps(handle)
|
|
167
|
+
return fresh
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Rename ONE node (bare sibling segment — the subtree's paths follow). Owns the shared record's
|
|
171
|
+
* re-keying (the host re-keys only its own part/selection state). Returns the new path; null on
|
|
172
|
+
* refusal (unknown node, invalid name, sibling collision). */
|
|
173
|
+
const renameNode = (handle: Handle, path: string, newName: string): string | null => {
|
|
174
|
+
const nodes = handle._live?.nodes
|
|
175
|
+
const node = nodes?.[path]
|
|
176
|
+
if (!nodes || !node || newName === "" || newName.includes("/") || newName.includes(":")) return null
|
|
177
|
+
const cut = path.lastIndexOf("/")
|
|
178
|
+
const newPath = cut < 0 ? newName : path.slice(0, cut + 1) + newName
|
|
179
|
+
if (newPath === path) return path
|
|
180
|
+
if (nodes[newPath]) return null
|
|
181
|
+
rekey(handle, path, newPath)
|
|
182
|
+
node.name = newName // engine-side name stays the bare segment
|
|
183
|
+
return newPath
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Reparent keeping the LOCAL transform (null = scene root) — the node's and every descendant's
|
|
187
|
+
* paths follow. Returns the new path; null on refusal (unknown node/parent, cycle, name taken
|
|
188
|
+
* among the new siblings). */
|
|
189
|
+
const reparentNode = (handle: Handle, path: string, newParentPath: string | null): string | null => {
|
|
190
|
+
const nodes = handle._live?.nodes
|
|
191
|
+
const node = nodes?.[path]
|
|
192
|
+
if (!nodes || !node) return null
|
|
193
|
+
const parent = newParentPath === null ? null : nodes[newParentPath]
|
|
194
|
+
if (newParentPath !== null && !parent) return null
|
|
195
|
+
if (newParentPath !== null && (newParentPath === path || newParentPath.startsWith(path + "/"))) return null
|
|
196
|
+
const name = path.slice(path.lastIndexOf("/") + 1)
|
|
197
|
+
const newPath = newParentPath === null ? name : `${newParentPath}/${name}`
|
|
198
|
+
if (newPath === path) return path
|
|
199
|
+
if (nodes[newPath]) return null
|
|
200
|
+
rekey(handle, path, newPath)
|
|
201
|
+
node.setParent(parent ?? null, false)
|
|
202
|
+
return newPath
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A node changed (transform edit / gizmo drag / live patch) — re-resolve refs and re-run
|
|
206
|
+
* rebuild() on every editor-run aspect whose ref() props point at it. Deps are absolute paths, so
|
|
207
|
+
* "the change counts for its ancestors too" (generators read subtrees — FollowPath's waypoints
|
|
208
|
+
* are the children of its referenced path node) is a prefix test: a dep hits when the changed
|
|
209
|
+
* path IS the dep or lies inside the dep's subtree. */
|
|
210
|
+
const nodeChanged = (handle: Handle, path: string): void => {
|
|
211
|
+
const nodes = handle._live?.nodes
|
|
212
|
+
if (!nodes) return
|
|
213
|
+
for (const run of handle._editorRuns) {
|
|
214
|
+
let hit = false
|
|
215
|
+
for (const d of run.deps) if (path === d || path.startsWith(`${d}/`)) { hit = true; break }
|
|
216
|
+
if (!hit) continue
|
|
217
|
+
assignRefProps(run, nodes) // a patch may have replaced the referenced node instance
|
|
218
|
+
safeRebuild(run)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Live prop edit on ONE editor-run aspect (`index` = the doc's aspect index on the host node) —
|
|
223
|
+
* updates the instance (`{ $ref }` values resolve to live nodes), re-derives its deps, and
|
|
224
|
+
* rebuilds. False when that entry isn't editor-run (inert data — nothing to do). */
|
|
225
|
+
const setProp = (handle: Handle, hostPath: string, index: number, key: string, value: unknown): boolean => {
|
|
226
|
+
const run = handle._editorRuns.find((r) => r.hostPath === hostPath && r.index === index)
|
|
227
|
+
const nodes = handle._live?.nodes
|
|
228
|
+
if (!run || !nodes) return false
|
|
229
|
+
run.props[key] = value
|
|
230
|
+
run.deps = collectRefDeps(run.props, nodes, run.hostPath)
|
|
231
|
+
// aspect runs keep their host as a dep (see attachAspects); make() runs must NOT — the
|
|
232
|
+
// wrapper's transform is editor-owned and moving it never re-calls the factory
|
|
233
|
+
if (run.index !== MAKE_INDEX) run.deps.add(run.hostPath)
|
|
234
|
+
if (isNodeRef(value) || (Array.isArray(value) && value.some(isNodeRef))) assignRefProps(run, nodes)
|
|
235
|
+
else (run.inst as Record<string, unknown>)[key] = value
|
|
236
|
+
safeRebuild(run)
|
|
237
|
+
return true
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Live arg edit on a make() node — re-calls the factory through the tracked run (no compile; the
|
|
241
|
+
* factory is already in the bundle). False for non-make nodes. */
|
|
242
|
+
const setMakeArg = (handle: Handle, hostPath: string, key: string, value: unknown): boolean =>
|
|
243
|
+
setProp(handle, hostPath, MAKE_INDEX, key, value)
|
|
244
|
+
|
|
245
|
+
/** Every run's gizmo lines (world-space LINES batches) + a version that changes whenever any
|
|
246
|
+
* rebuild ran — the host re-pushes to the engine only on a change. */
|
|
247
|
+
const gizmos = (handle: Handle): { version: number, batches: GizmoBatch[] } => {
|
|
248
|
+
const batches: GizmoBatch[] = []
|
|
249
|
+
for (const run of handle._editorRuns) for (const b of run.gizmos.batches) if (b.segments.length > 0) batches.push(b)
|
|
250
|
+
for (const runs of foreignRuns.values()) for (const run of runs) for (const b of run.gizmos.batches) if (b.segments.length > 0) batches.push(b)
|
|
251
|
+
for (const node of Object.values(handle._live?.nodes ?? {})) {
|
|
252
|
+
const marker = (node as { _editorMarker?: GizmoBuffer })._editorMarker
|
|
253
|
+
if (marker) for (const b of marker.batches) batches.push(b)
|
|
254
|
+
}
|
|
255
|
+
return { version: currentGizmoVersion(), batches }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Run an aspect's custom `static inspector` card (immediate-mode — see core/InspectorUI.ts) and
|
|
260
|
+
* return its widget list. Null when the class has no inspector (the editor falls back to the
|
|
261
|
+
* inferred fields).
|
|
262
|
+
*
|
|
263
|
+
* `props` is the entry's CURRENT doc props, passed on EVERY call — the world syncs its side from
|
|
264
|
+
* it (a generator syncs through the `setProp` machinery, so an undo that changes a prop rebuilds
|
|
265
|
+
* for free; a plain aspect's preview instance is reassigned). `event` carries only buttons and
|
|
266
|
+
* editor-state field edits — doc-bound field edits arrive as changed `props`.
|
|
267
|
+
*
|
|
268
|
+
* Cards persist per `<host>:<index>` across calls (that's where `ui.state` lives); a card whose
|
|
269
|
+
* node instance was replaced by a live patch is rebuilt transparently.
|
|
270
|
+
*/
|
|
271
|
+
const inspectorRender = (
|
|
272
|
+
handle: Handle, hostPath: string, index: number,
|
|
273
|
+
props: Record<string, unknown>, event?: InspectorEvent,
|
|
274
|
+
): InspectorWidget[] | null => {
|
|
275
|
+
const live = handle._live
|
|
276
|
+
const node = live?.nodes[hostPath]
|
|
277
|
+
const entry = (node as unknown as { _sceneAspects?: readonly AspectEntry<any>[] } | undefined)
|
|
278
|
+
?._sceneAspects?.[index]
|
|
279
|
+
if (!live || !node || !entry) return null
|
|
280
|
+
const ctor = entry.ctor as unknown as { inspector?: (ui: InspectorUI, aspect: unknown) => void }
|
|
281
|
+
if (typeof ctor.inspector !== "function") return null
|
|
282
|
+
|
|
283
|
+
const run = handle._editorRuns.find((r) => r.hostPath === hostPath && r.index === index)
|
|
284
|
+
const cards = cardsOf(handle)
|
|
285
|
+
const key = `${hostPath}:${index}`
|
|
286
|
+
let card = cards.get(key)
|
|
287
|
+
if (!card || card.node !== node) {
|
|
288
|
+
// (Re)create — generators reuse their tracked live instance; plain aspects get a persistent
|
|
289
|
+
// preview instance: node set, refs resolved, NEVER onAttach (edit mode stays side-effect
|
|
290
|
+
// free). Editor state (ui._state) survives a node patch by carrying the old ui over.
|
|
291
|
+
let inst: Record<string, unknown>
|
|
292
|
+
if (run) {
|
|
293
|
+
inst = run.inst as unknown as Record<string, unknown>
|
|
294
|
+
} else {
|
|
295
|
+
inst = new (entry.ctor as unknown as new () => Record<string, unknown>)()
|
|
296
|
+
inst.node = node
|
|
297
|
+
Object.assign(inst, resolveRefs({ ...props }, live.nodes, hostPath))
|
|
298
|
+
}
|
|
299
|
+
const ui = card?.ui ?? new InspectorUI()
|
|
300
|
+
ui._fields = describeFields(entry.ctor as unknown as abstract new () => unknown)
|
|
301
|
+
ui._docKeys = new Set(ui._fields.map((f) => f.key))
|
|
302
|
+
card = { ui, inst, node }
|
|
303
|
+
cards.set(key, card)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Sync the doc props into the world side. A key REMOVED from the doc (undo past its first
|
|
307
|
+
// edit) resets to the class-field default — otherwise the instance would keep the stale value.
|
|
308
|
+
const c = card
|
|
309
|
+
const fieldDefault = (k: string): unknown => c.ui._fields.find((f) => f.key === k)?.value
|
|
310
|
+
// `$expr` markers (values set in code) never sync into instances — the widget shows them
|
|
311
|
+
// read-only; the instance keeps the compile-time evaluation.
|
|
312
|
+
const isExpr = (v: unknown): boolean =>
|
|
313
|
+
typeof v === "object" && v !== null && typeof (v as { $expr?: unknown }).$expr === "string"
|
|
314
|
+
const changed = (a: unknown, b: unknown): boolean =>
|
|
315
|
+
a !== b && JSON.stringify(a) !== JSON.stringify(b)
|
|
316
|
+
if (run) {
|
|
317
|
+
for (const [ k, v ] of Object.entries(props)) {
|
|
318
|
+
if (!isExpr(v) && changed(run.props[k], v)) setProp(handle, hostPath, index, k, v)
|
|
319
|
+
}
|
|
320
|
+
for (const k of Object.keys(run.props)) {
|
|
321
|
+
if (k in props) continue
|
|
322
|
+
setProp(handle, hostPath, index, k, fieldDefault(k))
|
|
323
|
+
delete run.props[k] // keep run.props mirroring the doc, or this reset re-fires every call
|
|
324
|
+
}
|
|
325
|
+
c.ui._props = run.props
|
|
326
|
+
} else {
|
|
327
|
+
const snapshot = { ...props }
|
|
328
|
+
const resolved = (resolveRefs(snapshot, live.nodes, hostPath) ?? snapshot) as Record<string, unknown>
|
|
329
|
+
for (const f of c.ui._fields) {
|
|
330
|
+
if (isExpr(resolved[f.key])) continue
|
|
331
|
+
c.inst[f.key] = f.key in resolved ? resolved[f.key] : f.value
|
|
332
|
+
}
|
|
333
|
+
c.ui._props = snapshot
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return c.ui._run((u) => ctor.inspector!(u, c.inst), event)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The INTERNAL part rows of a loaded model node or prefab instance (by its absolute def path) —
|
|
340
|
+
* path/name/depth/live node, in the same asset-internal part-path grammar `overrides` keys use.
|
|
341
|
+
* Empty for plain nodes / unknown paths. */
|
|
342
|
+
const modelParts = async (handle: Handle, path: string): Promise<ModelPartRow[]> => {
|
|
343
|
+
const { nodes } = (await handle.load()) as unknown as { nodes: Record<string, Node> }
|
|
344
|
+
const node = nodes[path]
|
|
345
|
+
if (node instanceof Model) return modelPartRows(node)
|
|
346
|
+
// prefab instances precompute their rows in DEF order (engine child order isn't stable)
|
|
347
|
+
return (node as { _prefabParts?: ModelPartRow[] } | undefined)?._prefabParts ?? []
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Describe every aspect class this scene references (fields + defaults). */
|
|
351
|
+
const describeAspects = (handle: Handle): AspectClassInfo[] => {
|
|
352
|
+
const ctors = new Set<AspectCtor<any>>()
|
|
353
|
+
const walk = (defs?: Record<string, SceneNodeDef>): void => {
|
|
354
|
+
for (const nd of Object.values(defs ?? {})) {
|
|
355
|
+
for (const e of nd.aspects ?? []) ctors.add(e.ctor)
|
|
356
|
+
walk(nd.children)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
walk(handle.def.nodes)
|
|
360
|
+
return [ ...ctors ].map((c) => describeAspect(c))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The `locked` flag of a node (`locked: true` in its def, or set live): viewport manipulation
|
|
364
|
+
* won't target it. */
|
|
365
|
+
const isLocked = (node: Node): boolean => (node as { _sceneLocked?: boolean })._sceneLocked === true
|
|
366
|
+
const setLocked = (node: Node, locked: boolean): void => { (node as { _sceneLocked?: boolean })._sceneLocked = locked }
|
|
367
|
+
|
|
368
|
+
/** True for the SDK's own materials (lit / unlit / shadow), whose `color` / `map` go through their
|
|
369
|
+
* accessors — they know the shader's uniform names; everything else takes `setMaterialParam`. */
|
|
370
|
+
const isBuiltinMaterial = (material: Material): boolean =>
|
|
371
|
+
(material as unknown as { _colorKey: string | null })._colorKey !== null
|
|
372
|
+
|
|
373
|
+
/** A slot's material was set behind the node's back (an engine-level restore of the glTF
|
|
374
|
+
* material): drop what the node remembers of it, so the next read asks the engine. */
|
|
375
|
+
const forgetMaterial = (node: Node, slot: number): void => {
|
|
376
|
+
const cache = (node as unknown as { _materials?: (Material | undefined)[] })._materials
|
|
377
|
+
if (cache) cache[slot] = undefined
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** One immediate-mode pass of a card the editor owns (a built-in node card, a plugin window). */
|
|
381
|
+
const runCard = (ui: InspectorUI, render: (ui: InspectorUI) => void, event?: InspectorEvent): InspectorWidget[] =>
|
|
382
|
+
ui._run(render, event)
|
|
383
|
+
|
|
384
|
+
export const sceneEditor = {
|
|
385
|
+
// ---- the loaded scene, by handle ----
|
|
386
|
+
patchNode, renameNode, reparentNode, nodeChanged, modelParts, describeAspects,
|
|
387
|
+
// ---- editor-run aspects (generators) and make() nodes ----
|
|
388
|
+
setProp, setMakeArg, gizmos, inspectorRender,
|
|
389
|
+
// ---- nodes ----
|
|
390
|
+
/** The preview pose of a model node (`editor: { clip, time }`) — idempotent; no clip = the rest pose. */
|
|
391
|
+
applyPose: applyEditorPose,
|
|
392
|
+
isLocked, setLocked,
|
|
393
|
+
// ---- materials ----
|
|
394
|
+
/** Assign a material def to one slot of a node (a material asset handle, an inline def, a shader). */
|
|
395
|
+
assignMaterial: assignMaterialDef,
|
|
396
|
+
/** One parameter into a live material: a value, or a texture URL that loads in. */
|
|
397
|
+
setMaterialParam: applyMaterialParam,
|
|
398
|
+
isBuiltinMaterial, forgetMaterial,
|
|
399
|
+
// ---- gizmo lines of the editor's own (a tool's hooks draw through `Gizmos` too) ----
|
|
400
|
+
GizmoBuffer, withGizmoScope,
|
|
401
|
+
// ---- cards + plugins ----
|
|
402
|
+
runCard,
|
|
403
|
+
/** The registry `registerEditorWindow` / `registerEditorTool` fill (`*.editor.ts` files). */
|
|
404
|
+
plugins: __editorPlugins,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** The contract of an editor's in-world half with the SDK — the type of the `__sceneEditor` global. */
|
|
408
|
+
export type SceneEditorApi = typeof sceneEditor
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// scene-editor bundles (phase 10), so none of this reaches shipped app.js (nothing references it
|
|
4
4
|
// there, and method-granular DCE drops it).
|
|
5
5
|
//
|
|
6
|
-
// The scene-editor harness
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// The scene-editor harness reads the registry through the editor contract (`__sceneEditor.plugins`,
|
|
7
|
+
// ./editor.ts) — the harness, the scene, and the editor files compile into ONE bundle, so they
|
|
8
|
+
// share this module instance. Windows and tools describe their UI through the
|
|
9
9
|
// same immediate-mode `InspectorUI` protocol custom inspector cards use (one widget vocabulary,
|
|
10
10
|
// three mount points), and mutate the scene ONLY through the `editor` doc API — every plugin write
|
|
11
11
|
// lands in the scene document (undoable, diffable), never in live engine state.
|