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.
Files changed (257) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +11 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Particles.d.ts +15 -0
  25. package/dist/types/gl/Scene.d.ts +41 -13
  26. package/dist/types/gl/Texture.d.ts +1 -1
  27. package/dist/types/gl/animation/Locomotion.d.ts +59 -3
  28. package/dist/types/gl/terrain/Terrain.d.ts +8 -0
  29. package/dist/types/inject.d.ts +11 -11
  30. package/dist/types/inject.editor.d.ts +1 -0
  31. package/dist/types/net/core.d.ts +7 -0
  32. package/dist/types/plugin.d.ts +76 -0
  33. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  34. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  35. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  36. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  37. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  38. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  39. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  40. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  41. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  42. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  43. package/dist/types/runtime/app.d.ts +9 -2
  44. package/dist/types/runtime/fetch.d.ts +2 -0
  45. package/dist/types/runtime/input.d.ts +8 -1
  46. package/dist/types/runtime/media.d.ts +6 -10
  47. package/dist/types/runtime/misc.d.ts +4 -1
  48. package/dist/types/runtime/net.d.ts +3 -2
  49. package/dist/types/runtime/touch.d.ts +32 -0
  50. package/dist/types/scene/defineScene.d.ts +43 -2
  51. package/dist/types/scene/editor.d.ts +52 -0
  52. package/dist/types/scene/gizmos.d.ts +7 -4
  53. package/dist/types/ui/NativeView.d.ts +6 -4
  54. package/dist/types/ui/UI.d.ts +1 -1
  55. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  56. package/dist/types/ui/UIButton.d.ts +12 -14
  57. package/dist/types/ui/UIContainer.d.ts +0 -6
  58. package/dist/types/ui/UIImage.d.ts +1 -4
  59. package/dist/types/ui/UIInput.d.ts +8 -24
  60. package/dist/types/ui/UIModal.d.ts +0 -2
  61. package/dist/types/ui/UINode.d.ts +70 -56
  62. package/dist/types/ui/UIPager.d.ts +28 -27
  63. package/dist/types/ui/UIPopover.d.ts +0 -2
  64. package/dist/types/ui/UIScreen.d.ts +12 -17
  65. package/dist/types/ui/UIScrollable.d.ts +1 -4
  66. package/dist/types/ui/UIText.d.ts +0 -2
  67. package/dist/types/ui/UIVideo.d.ts +3 -5
  68. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  69. package/dist/types/ui/UIWidget.d.ts +10 -9
  70. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  71. package/dist/types/ui/presentable.d.ts +46 -32
  72. package/dist/types/ui/router.d.ts +18 -7
  73. package/dist/types/ui/styleColor.d.ts +1 -0
  74. package/dist/types/ui/transitions.d.ts +18 -0
  75. package/dist/types/ui/tree.d.ts +75 -0
  76. package/dist/types/version.d.ts +10 -0
  77. package/dist/types.json +1 -1
  78. package/package.json +12 -3
  79. package/prompts/2d.md +2 -6
  80. package/prompts/3d.md +1 -5
  81. package/prompts/README.md +142 -142
  82. package/prompts/canvas.md +9 -8
  83. package/prompts/compose.ts +1 -1
  84. package/prompts/core.md +3 -3
  85. package/prompts/design.md +1 -1
  86. package/prompts/dist/2d-game.md +77 -54
  87. package/prompts/dist/3d-app.md +75 -52
  88. package/prompts/dist/ar-app.md +74 -51
  89. package/prompts/dist/design.md +30 -8
  90. package/prompts/dist/ui-app.md +73 -46
  91. package/prompts/ui-design.md +2 -3
  92. package/prompts/ui.md +45 -37
  93. package/src/animate/tween/Animation.ts +262 -378
  94. package/src/animate/tween/animateValue.ts +103 -100
  95. package/src/animate/tween/easing.ts +10 -3
  96. package/src/animate/tween/spec.ts +505 -479
  97. package/src/audio/Sound.ts +3 -3
  98. package/src/audio/audio.ts +162 -161
  99. package/src/bridges/2d.d.ts +317 -0
  100. package/src/bridges/app.d.ts +91 -0
  101. package/src/bridges/audio.d.ts +97 -0
  102. package/src/bridges/canvas.d.ts +79 -0
  103. package/src/bridges/device.d.ts +72 -0
  104. package/src/bridges/fetch.d.ts +80 -0
  105. package/src/bridges/files.d.ts +70 -0
  106. package/src/bridges/gl.d.ts +1133 -0
  107. package/src/bridges/input.d.ts +72 -0
  108. package/src/bridges/media.d.ts +55 -0
  109. package/src/bridges/nav.d.ts +71 -0
  110. package/src/bridges/net.d.ts +52 -0
  111. package/src/bridges/service.d.ts +52 -0
  112. package/src/bridges/socket.d.ts +31 -0
  113. package/src/bridges/storage.d.ts +33 -0
  114. package/src/bridges/tree.d.ts +301 -0
  115. package/src/bridges/types.d.ts +49 -0
  116. package/src/canvas/Canvas.ts +114 -159
  117. package/src/canvas/gen/cssColor.ts +224 -0
  118. package/src/canvas/gen/recorder.ts +212 -0
  119. package/src/canvas/gen/spec.ts +201 -0
  120. package/src/chisel.ts +193 -0
  121. package/src/compile/assetMacro.ts +1 -1
  122. package/src/compile/bundler.ts +11 -2
  123. package/src/compile/compileProject.ts +43 -4
  124. package/src/compile/fontMacro.ts +3 -4
  125. package/src/compile/header.ts +26 -5
  126. package/src/compile/index.ts +3 -1
  127. package/src/compile/liteMaterial.ts +1 -1
  128. package/src/compile/sceneEditor.ts +11 -26
  129. package/src/core/color.ts +73 -30
  130. package/src/core/pins.ts +51 -0
  131. package/src/core/signals.ts +8 -1
  132. package/src/g2/CharacterController2D.ts +3 -3
  133. package/src/g2/Node2D.ts +57 -39
  134. package/src/g2/Physics2D.ts +2 -2
  135. package/src/g2/Scene2D.ts +35 -23
  136. package/src/g2/Texture2D.ts +1 -1
  137. package/src/g2/loop.ts +4 -4
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +13 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Model.ts +172 -167
  144. package/src/gl/Node.ts +91 -24
  145. package/src/gl/Particles.ts +20 -1
  146. package/src/gl/Scene.ts +100 -44
  147. package/src/gl/Texture.ts +8 -7
  148. package/src/gl/animation/AnimationClip.ts +1 -1
  149. package/src/gl/animation/Locomotion.ts +77 -8
  150. package/src/gl/nav/NavMesh.ts +3 -4
  151. package/src/gl/physics/Physics.ts +2 -2
  152. package/src/gl/physics/physicsEvents.ts +3 -3
  153. package/src/gl/terrain/Terrain.ts +33 -5
  154. package/src/gl/touch.ts +14 -15
  155. package/src/host.d.ts +2 -3
  156. package/src/inject.editor.ts +7 -0
  157. package/src/inject.ts +233 -236
  158. package/src/net/core.ts +6 -5
  159. package/src/net/index.ts +1 -1
  160. package/src/net/replication.ts +1 -1
  161. package/src/plugin.ts +191 -0
  162. package/src/plugins/gen/camera/contract.d.ts +27 -0
  163. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  164. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  165. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  166. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  167. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  168. package/src/plugins/gen/map/contract.d.ts +144 -0
  169. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  170. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  171. package/src/plugins/gen/push/contract.d.ts +61 -0
  172. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  173. package/src/plugins/gen/push/sdk/push.ts +105 -0
  174. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  175. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  176. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  177. package/src/plugins/permission.ts +5 -4
  178. package/src/runtime/app.ts +20 -9
  179. package/src/runtime/appEvents.ts +5 -4
  180. package/src/runtime/channel.ts +18 -15
  181. package/src/runtime/clipboard.ts +4 -3
  182. package/src/runtime/datetime.ts +2 -1
  183. package/src/runtime/device.ts +17 -15
  184. package/src/runtime/fetch.ts +30 -20
  185. package/src/runtime/files.ts +16 -15
  186. package/src/runtime/input.ts +21 -8
  187. package/src/runtime/media.ts +50 -46
  188. package/src/runtime/misc.ts +7 -3
  189. package/src/runtime/net.ts +8 -7
  190. package/src/runtime/rpc.ts +1 -3
  191. package/src/runtime/service.ts +19 -14
  192. package/src/runtime/share.ts +4 -3
  193. package/src/runtime/storage.ts +6 -4
  194. package/src/runtime/touch.ts +32 -0
  195. package/src/scene/defineScene.ts +61 -365
  196. package/src/scene/editor.ts +408 -0
  197. package/src/scene/editorPlugins.ts +3 -3
  198. package/src/scene/gizmos.ts +154 -148
  199. package/src/server/db/marci/query.ts +1 -1
  200. package/src/server/host.ts +1 -1
  201. package/src/server/runtime.ts +1 -1
  202. package/src/ui/NativeView.ts +72 -25
  203. package/src/ui/UI.ts +3 -3
  204. package/src/ui/UIBottomSheet.ts +16 -17
  205. package/src/ui/UIButton.ts +54 -16
  206. package/src/ui/UIContainer.ts +0 -6
  207. package/src/ui/UIImage.ts +34 -30
  208. package/src/ui/UIInput.ts +29 -37
  209. package/src/ui/UIModal.ts +1 -3
  210. package/src/ui/UINode.ts +347 -297
  211. package/src/ui/UIPager.ts +93 -78
  212. package/src/ui/UIPopover.ts +0 -2
  213. package/src/ui/UIScreen.ts +41 -36
  214. package/src/ui/UIScrollable.ts +19 -13
  215. package/src/ui/UISpacer.ts +1 -1
  216. package/src/ui/UITabs.ts +8 -6
  217. package/src/ui/UIText.ts +7 -17
  218. package/src/ui/UIVideo.ts +30 -27
  219. package/src/ui/UIVirtualizedList.ts +58 -59
  220. package/src/ui/UIWidget.ts +38 -18
  221. package/src/ui/colorKeys.gen.ts +37 -0
  222. package/src/ui/fonts.ts +2 -2
  223. package/src/ui/presentable.ts +59 -42
  224. package/src/ui/router.ts +52 -35
  225. package/src/ui/styleColor.ts +56 -0
  226. package/src/ui/theme.ts +7 -6
  227. package/src/ui/transitions.ts +249 -0
  228. package/src/ui/tree.ts +346 -0
  229. package/src/version.ts +24 -0
  230. package/tests/helpers/engineWorld.ts +11 -0
  231. package/tests/helpers/fakeTree.ts +353 -0
  232. package/tests/helpers/hostStubs.ts +31 -0
  233. package/tests/helpers/index.ts +14 -0
  234. package/tests/helpers/memoryMarci.ts +124 -0
  235. package/tests/helpers/phases.ts +23 -0
  236. package/tests/helpers/preload.ts +18 -0
  237. package/tests/helpers/stubApp.ts +2 -0
  238. package/tests/helpers/stubDevice.ts +2 -0
  239. package/tests/helpers/stubFetch.ts +2 -0
  240. package/tests/helpers/stubInput.ts +2 -0
  241. package/dist/inject.js +0 -4730
  242. package/dist/types/core/registry.d.ts +0 -7
  243. package/dist/types/plugins/camera.d.ts +0 -25
  244. package/dist/types/plugins/push.d.ts +0 -46
  245. package/src/bridges.d.ts +0 -1760
  246. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  247. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  248. package/src/compile/__tests__/assetName.test.ts +0 -55
  249. package/src/compile/__tests__/compile.test.ts +0 -310
  250. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  251. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  252. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  253. package/src/core/__tests__/stateMachine.test.ts +0 -132
  254. package/src/core/registry.ts +0 -23
  255. package/src/plugins/camera.ts +0 -81
  256. package/src/plugins/push.ts +0 -132
  257. 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 (packages/projects/src/scene-editor) reads the registry through the
7
- // injected `__editorPlugins` global — the harness, the scene, and the editor files compile into
8
- // ONE bundle, so they share this module instance. Windows and tools describe their UI through the
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.