lecodes-cli 0.19.0 → 0.20.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 (270) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +1069 -396
  3. package/package.json +14 -13
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk-types.json +1 -1
  6. package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
  7. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  8. package/runtime/web/assets/basis-C64VHDVD.js +1 -0
  9. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  10. package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
  11. package/runtime/web/assets/draco-BiISTFcR.js +118 -0
  12. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  13. package/runtime/web/assets/index-CfNCCBjN.js +2 -0
  14. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
  15. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
  16. package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
  17. package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
  18. package/runtime/web/embed.html +32 -0
  19. package/runtime/web/embed.js +171 -0
  20. package/src/api.ts +302 -0
  21. package/src/browserAuth.ts +87 -0
  22. package/src/cmgenTool.ts +104 -0
  23. package/src/commands/app.ts +896 -0
  24. package/src/commands/appAndroid.ts +603 -0
  25. package/src/commands/appDesktop.ts +339 -0
  26. package/src/commands/appDesktopMac.ts +470 -0
  27. package/src/commands/appIcon.ts +187 -0
  28. package/src/commands/appShared.ts +542 -0
  29. package/src/commands/appTemplates.ts +907 -0
  30. package/src/commands/appTemplatesAndroid.ts +599 -0
  31. package/src/commands/appTemplatesGradlew.ts +9 -0
  32. package/src/commands/assets.ts +28 -0
  33. package/src/commands/clone.ts +60 -0
  34. package/src/commands/compile.ts +253 -0
  35. package/src/commands/create.ts +59 -0
  36. package/src/commands/design.ts +863 -0
  37. package/src/commands/designTemplates.ts +11 -0
  38. package/src/commands/desktop.ts +127 -0
  39. package/src/commands/dev.ts +236 -0
  40. package/src/commands/diff.ts +59 -0
  41. package/src/commands/init.ts +191 -0
  42. package/src/commands/install.ts +198 -0
  43. package/src/commands/lightmap.ts +290 -0
  44. package/src/commands/link.ts +147 -0
  45. package/src/commands/login.ts +24 -0
  46. package/src/commands/navmesh.ts +225 -0
  47. package/src/commands/pn.ts +305 -0
  48. package/src/commands/projectTemplates.ts +130 -0
  49. package/src/commands/pull.ts +109 -0
  50. package/src/commands/push.ts +173 -0
  51. package/src/commands/render.ts +416 -0
  52. package/src/commands/scene.ts +148 -0
  53. package/src/commands/shaders.ts +157 -0
  54. package/src/commands/status.ts +29 -0
  55. package/src/commands/test.ts +371 -0
  56. package/src/commands/thumbs.ts +178 -0
  57. package/src/commands/types.ts +87 -0
  58. package/src/commands/update.ts +190 -0
  59. package/src/compile/assetIcons.ts +214 -0
  60. package/src/compile/collect.ts +46 -0
  61. package/src/compile/collectLocal.ts +37 -0
  62. package/src/compile/designCompile.ts +104 -0
  63. package/src/compile/fonts.ts +156 -0
  64. package/src/compile/headlessBundle.ts +129 -0
  65. package/src/compile/nativeStack.ts +21 -0
  66. package/src/compile/projectCompile.ts +109 -0
  67. package/src/compile/sceneCompile.ts +113 -0
  68. package/src/compile/screenEntry.ts +127 -0
  69. package/src/compile/shaders.ts +245 -0
  70. package/src/config.ts +42 -0
  71. package/src/designMeta.ts +35 -0
  72. package/src/desktopRenderer.ts +545 -0
  73. package/src/desktopScript.ts +276 -0
  74. package/src/dev/clientTemplates.ts +160 -0
  75. package/src/dev/devServer.ts +333 -0
  76. package/src/dev/webRunner.ts +22 -0
  77. package/src/dev/wsServer.ts +144 -0
  78. package/src/distRoot.ts +20 -0
  79. package/src/ignore.ts +163 -0
  80. package/src/index.ts +494 -0
  81. package/src/lecodes-3d-editor.d.ts +41 -0
  82. package/src/lecodes-assets.d.ts +7 -0
  83. package/src/lecodes-design.d.ts +191 -0
  84. package/src/lecodes-renderer.d.ts +131 -0
  85. package/src/localFiles.ts +144 -0
  86. package/src/manifest.ts +46 -0
  87. package/src/matcTool.ts +137 -0
  88. package/src/peers.ts +40 -0
  89. package/src/project.ts +61 -0
  90. package/src/projectEnv.ts +94 -0
  91. package/src/qrcode-terminal.d.ts +9 -0
  92. package/src/releases.ts +125 -0
  93. package/src/serverDiff.ts +78 -0
  94. package/src/textDiff.ts +103 -0
  95. package/src/types.ts +0 -0
  96. package/src/util.ts +146 -0
  97. package/runtime/sdk/animate/animate.ts +0 -238
  98. package/runtime/sdk/animate/bezier.ts +0 -138
  99. package/runtime/sdk/animate/easings.ts +0 -126
  100. package/runtime/sdk/canvas/Canvas.ts +0 -305
  101. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  102. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  103. package/runtime/sdk/compile/assetMacro.ts +0 -146
  104. package/runtime/sdk/compile/assetName.ts +0 -50
  105. package/runtime/sdk/compile/bundler.ts +0 -287
  106. package/runtime/sdk/compile/compileProject.ts +0 -129
  107. package/runtime/sdk/compile/detectEntry.ts +0 -128
  108. package/runtime/sdk/compile/fontMacro.ts +0 -459
  109. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  110. package/runtime/sdk/compile/header.ts +0 -67
  111. package/runtime/sdk/compile/index.ts +0 -100
  112. package/runtime/sdk/compile/libraryImports.ts +0 -52
  113. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  114. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  115. package/runtime/sdk/compile/serverSplit.ts +0 -233
  116. package/runtime/sdk/compile/serverTypes.ts +0 -227
  117. package/runtime/sdk/compile/sfnt.ts +0 -98
  118. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  119. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  120. package/runtime/sdk/compile/sourcemap.ts +0 -25
  121. package/runtime/sdk/core/Aspect.ts +0 -568
  122. package/runtime/sdk/core/InspectorUI.ts +0 -212
  123. package/runtime/sdk/core/StateMachine.ts +0 -308
  124. package/runtime/sdk/core/color.ts +0 -66
  125. package/runtime/sdk/core/compWrite.ts +0 -42
  126. package/runtime/sdk/core/events.ts +0 -38
  127. package/runtime/sdk/core/fields.ts +0 -120
  128. package/runtime/sdk/core/registry.ts +0 -23
  129. package/runtime/sdk/core/signals.ts +0 -277
  130. package/runtime/sdk/core/time.ts +0 -84
  131. package/runtime/sdk/g2/Camera2D.ts +0 -40
  132. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  133. package/runtime/sdk/g2/Node2D.ts +0 -267
  134. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  135. package/runtime/sdk/g2/Physics2D.ts +0 -346
  136. package/runtime/sdk/g2/Scene2D.ts +0 -209
  137. package/runtime/sdk/g2/Shape2D.ts +0 -259
  138. package/runtime/sdk/g2/Sprite.ts +0 -89
  139. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  140. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  141. package/runtime/sdk/g2/Texture2D.ts +0 -47
  142. package/runtime/sdk/g2/Tilemap.ts +0 -41
  143. package/runtime/sdk/g2/Tileset.ts +0 -71
  144. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  145. package/runtime/sdk/g2/autotile.ts +0 -433
  146. package/runtime/sdk/g2/cells.ts +0 -91
  147. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  148. package/runtime/sdk/g2/groups2d.ts +0 -106
  149. package/runtime/sdk/g2/loop.ts +0 -50
  150. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  151. package/runtime/sdk/g2/touch.ts +0 -83
  152. package/runtime/sdk/gl/Camera.ts +0 -160
  153. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  154. package/runtime/sdk/gl/CharacterController.ts +0 -238
  155. package/runtime/sdk/gl/Gearbox.ts +0 -212
  156. package/runtime/sdk/gl/Geometry.ts +0 -279
  157. package/runtime/sdk/gl/IK.ts +0 -193
  158. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  159. package/runtime/sdk/gl/Light.ts +0 -107
  160. package/runtime/sdk/gl/Lightmap.ts +0 -256
  161. package/runtime/sdk/gl/Material.ts +0 -260
  162. package/runtime/sdk/gl/Mesh.ts +0 -83
  163. package/runtime/sdk/gl/Model.ts +0 -124
  164. package/runtime/sdk/gl/NavAgent.ts +0 -337
  165. package/runtime/sdk/gl/NavMesh.ts +0 -397
  166. package/runtime/sdk/gl/Node.ts +0 -350
  167. package/runtime/sdk/gl/Noise.ts +0 -30
  168. package/runtime/sdk/gl/Particles.ts +0 -676
  169. package/runtime/sdk/gl/Physics.ts +0 -222
  170. package/runtime/sdk/gl/Plane.ts +0 -53
  171. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  172. package/runtime/sdk/gl/Ray.ts +0 -16
  173. package/runtime/sdk/gl/Scene.ts +0 -486
  174. package/runtime/sdk/gl/Shape.ts +0 -409
  175. package/runtime/sdk/gl/Terrain.ts +0 -1092
  176. package/runtime/sdk/gl/Texture.ts +0 -63
  177. package/runtime/sdk/gl/Trigger.ts +0 -45
  178. package/runtime/sdk/gl/Vehicle.ts +0 -473
  179. package/runtime/sdk/gl/Wheel.ts +0 -240
  180. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  181. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  182. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  183. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  184. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  185. package/runtime/sdk/gl/animation/core.ts +0 -336
  186. package/runtime/sdk/gl/controls.ts +0 -95
  187. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  188. package/runtime/sdk/gl/scenarios.ts +0 -291
  189. package/runtime/sdk/gl/state.ts +0 -6
  190. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  191. package/runtime/sdk/gl/touch.ts +0 -68
  192. package/runtime/sdk/inject.ts +0 -197
  193. package/runtime/sdk/math/Mathf.ts +0 -118
  194. package/runtime/sdk/math/mat4.ts +0 -278
  195. package/runtime/sdk/math/quat.ts +0 -232
  196. package/runtime/sdk/math/vec.ts +0 -255
  197. package/runtime/sdk/net/codec.ts +0 -119
  198. package/runtime/sdk/net/core.ts +0 -384
  199. package/runtime/sdk/net/index.ts +0 -181
  200. package/runtime/sdk/net/replication.ts +0 -622
  201. package/runtime/sdk/plugins/camera.ts +0 -81
  202. package/runtime/sdk/plugins/geolocation.ts +0 -123
  203. package/runtime/sdk/plugins/oauth.ts +0 -61
  204. package/runtime/sdk/plugins/permission.ts +0 -7
  205. package/runtime/sdk/plugins/push.ts +0 -132
  206. package/runtime/sdk/plugins/qr.ts +0 -73
  207. package/runtime/sdk/plugins/service.ts +0 -47
  208. package/runtime/sdk/runtime/app.ts +0 -101
  209. package/runtime/sdk/runtime/appEvents.ts +0 -54
  210. package/runtime/sdk/runtime/channel.ts +0 -61
  211. package/runtime/sdk/runtime/clipboard.ts +0 -20
  212. package/runtime/sdk/runtime/datetime.ts +0 -329
  213. package/runtime/sdk/runtime/device.ts +0 -293
  214. package/runtime/sdk/runtime/fetch.ts +0 -77
  215. package/runtime/sdk/runtime/files.ts +0 -23
  216. package/runtime/sdk/runtime/input.ts +0 -175
  217. package/runtime/sdk/runtime/media.ts +0 -111
  218. package/runtime/sdk/runtime/misc.ts +0 -16
  219. package/runtime/sdk/runtime/net.ts +0 -36
  220. package/runtime/sdk/runtime/rpc.ts +0 -218
  221. package/runtime/sdk/runtime/service.ts +0 -83
  222. package/runtime/sdk/runtime/share.ts +0 -9
  223. package/runtime/sdk/runtime/storage.ts +0 -13
  224. package/runtime/sdk/runtime/touch.ts +0 -76
  225. package/runtime/sdk/scene/defineScene.ts +0 -1353
  226. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  227. package/runtime/sdk/scene/gizmos.ts +0 -148
  228. package/runtime/sdk/scene/grammar.ts +0 -120
  229. package/runtime/sdk/scene/material.ts +0 -188
  230. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  231. package/runtime/sdk/server/auth/global.ts +0 -80
  232. package/runtime/sdk/server/auth/host.ts +0 -318
  233. package/runtime/sdk/server/auth/models.ts +0 -83
  234. package/runtime/sdk/server/auth/types.ts +0 -50
  235. package/runtime/sdk/server/channel.ts +0 -56
  236. package/runtime/sdk/server/context.ts +0 -36
  237. package/runtime/sdk/server/db/defineDb.ts +0 -237
  238. package/runtime/sdk/server/db/fields.ts +0 -132
  239. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  240. package/runtime/sdk/server/db/index.ts +0 -7
  241. package/runtime/sdk/server/db/marci/query.ts +0 -412
  242. package/runtime/sdk/server/db/types.ts +0 -202
  243. package/runtime/sdk/server/errors.ts +0 -12
  244. package/runtime/sdk/server/host.ts +0 -74
  245. package/runtime/sdk/server/inject.ts +0 -13
  246. package/runtime/sdk/server/runtime.ts +0 -133
  247. package/runtime/sdk/server/validate.ts +0 -87
  248. package/runtime/sdk/ui/NativeView.ts +0 -142
  249. package/runtime/sdk/ui/UI.ts +0 -39
  250. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  251. package/runtime/sdk/ui/UIButton.ts +0 -101
  252. package/runtime/sdk/ui/UIContainer.ts +0 -60
  253. package/runtime/sdk/ui/UIImage.ts +0 -83
  254. package/runtime/sdk/ui/UIInput.ts +0 -185
  255. package/runtime/sdk/ui/UIModal.ts +0 -139
  256. package/runtime/sdk/ui/UINode.ts +0 -830
  257. package/runtime/sdk/ui/UIPager.ts +0 -362
  258. package/runtime/sdk/ui/UIPopover.ts +0 -100
  259. package/runtime/sdk/ui/UIScreen.ts +0 -123
  260. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  261. package/runtime/sdk/ui/UISpacer.ts +0 -14
  262. package/runtime/sdk/ui/UITabs.ts +0 -236
  263. package/runtime/sdk/ui/UIText.ts +0 -51
  264. package/runtime/sdk/ui/UIVideo.ts +0 -88
  265. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  266. package/runtime/sdk/ui/UIWidget.ts +0 -127
  267. package/runtime/sdk/ui/fonts.ts +0 -13
  268. package/runtime/sdk/ui/presentable.ts +0 -117
  269. package/runtime/sdk/ui/router.ts +0 -132
  270. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,212 +0,0 @@
1
- // The scene editor's immediate-mode inspector protocol (docs/scene-editor-plan.md, phase 9).
2
- //
3
- // Custom editor UI never renders into the editor panel directly — the live data (aspect instances,
4
- // nodes, `model.anim.clips`, generated meshes) lives in the SCENE WORLD (the compiled bundle), on
5
- // the other side of the controller. So a custom view is an imgui-style function running in the
6
- // world: it re-runs on every change/event and emits a WIDGET LIST (plain data); the editor panel is
7
- // a dumb renderer of that list; user events `{ id, value }` come back and trigger the next run —
8
- // `ui.button()` returns `true` on the run that consumes the click.
9
- //
10
- // class Road extends Aspect<'road'> {
11
- // from: Node | null = null
12
- // to: Node | null = null
13
- // width = 2
14
- // static editor = { rebuild: true }
15
- // static inspector(ui: InspectorUI, road: Road) {
16
- // ui.auto() // the inferred fields, as usual
17
- // if (road.from && road.to) ui.info(`${road.generated.children.length} pieces`)
18
- // else ui.warn('Assign both endpoints')
19
- // if (ui.button('Shuffle')) road.rebuild()
20
- // }
21
- // }
22
- //
23
- // BINDING RULE: a field whose key is a declared aspect field (a public class field) is DOC-BOUND —
24
- // the editor persists edits to the scene file through its normal commit/undo path. Any other key is
25
- // EDITOR STATE — transient, kept on this InspectorUI instance across runs, never written to a file
26
- // (the animation card's clip choice, a tool's brush size). One vocabulary, two lifetimes.
27
-
28
- import type { FieldDescriptor, FieldEditor } from "./fields"
29
-
30
- /** One user interaction coming back from the editor panel: the widget's id (+ value for fields). */
31
- export type InspectorEvent = { id: string, value?: unknown }
32
-
33
- export type InspectorFieldWidget = {
34
- kind: "field"
35
- id: string
36
- /** The bound key — an aspect field (doc-bound) or an editor-state key. */
37
- key: string
38
- label: string
39
- editor: FieldEditor | undefined
40
- value: unknown
41
- /** True = persisted to the scene file by the editor; false = transient editor state. */
42
- doc: boolean
43
- /** True = emitted by `ui.auto()` — the editor may overlay its syntactic hints (options/node). */
44
- auto?: boolean
45
- min?: number
46
- max?: number
47
- step?: number
48
- options?: readonly (string | number)[]
49
- }
50
-
51
- export type InspectorWidget =
52
- | InspectorFieldWidget
53
- | { kind: "button", id: string, label: string }
54
- | { kind: "toolButton", id: string, label: string, tool: string }
55
- | { kind: "header", id: string, label: string }
56
- | { kind: "info" | "warn", id: string, text: string }
57
-
58
- type FieldOpts = {
59
- label?: string
60
- min?: number
61
- max?: number
62
- step?: number
63
- /** Default for EDITOR-STATE keys (doc keys default from the class field). */
64
- value?: unknown
65
- }
66
-
67
- /**
68
- * The widget builder handed to `static inspector(ui, aspect)` (and to editor windows/tools later).
69
- * One instance lives per card and persists across runs — it carries the card's editor state.
70
- */
71
- export class InspectorUI {
72
- /** @internal This run's emitted widgets. */
73
- _widgets: InspectorWidget[] = []
74
- /** @internal Transient editor state (non-doc field keys), kept across runs. */
75
- _state: Record<string, unknown> = {}
76
- /** @internal The aspect entry's doc props (live reference — the runner keeps it current). */
77
- _props: Record<string, unknown> = {}
78
- /** @internal Keys that are declared aspect fields → doc-bound. Empty for node/window cards. */
79
- _docKeys = new Set<string>()
80
- /** @internal Field descriptors for auto() + doc-key defaults (describeFields output). */
81
- _fields: FieldDescriptor[] = []
82
- /** @internal The unconsumed button event of the current run. */
83
- _event: InspectorEvent | null = null
84
-
85
- // ---- fields (each emits one widget AND returns the current value) ---------------------------
86
-
87
- /** Numeric field (min/max/step render as a slider-style scrub range). */
88
- number(key: string, opts?: FieldOpts): number {
89
- const v = this._field("number", key, opts)
90
- return typeof v === "number" ? v : 0
91
- }
92
- /** Alias of `number` — give it min/max/step and the editor renders a scrubable range. */
93
- slider(key: string, opts?: FieldOpts): number { return this.number(key, opts) }
94
-
95
- text(key: string, opts?: FieldOpts): string {
96
- const v = this._field("text", key, opts)
97
- return typeof v === "string" ? v : ""
98
- }
99
-
100
- color(key: string, opts?: FieldOpts): string {
101
- const v = this._field("color", key, opts)
102
- return typeof v === "string" ? v : "#ffffff"
103
- }
104
-
105
- switch(key: string, opts?: FieldOpts): boolean {
106
- return this._field("switch", key, opts) === true
107
- }
108
-
109
- /** Dropdown — `options` may be computed fresh every run (dynamic lists come free). */
110
- select(key: string, options: readonly (string | number)[], opts?: FieldOpts): string | number {
111
- const v = this._field("select", key, { ...opts, value: opts?.value ?? options[0] }, options)
112
- return (typeof v === "string" || typeof v === "number") && options.includes(v) ? v : options[0]
113
- }
114
-
115
- vec2(key: string, opts?: FieldOpts): [number, number] { return this._vec("vec2", key, 2, opts) as [number, number] }
116
- vec3(key: string, opts?: FieldOpts): [number, number, number] { return this._vec("vec3", key, 3, opts) as [number, number, number] }
117
- vec4(key: string, opts?: FieldOpts): [number, number, number, number] { return this._vec("vec4", key, 4, opts) as [number, number, number, number] }
118
-
119
- /** Scene-node reference (`ref()` fields): name dropdown + pick-in-viewport. Returns the RAW doc
120
- * value (`{ $ref }` marker or null) — read the resolved live node off the aspect instance. */
121
- node(key: string, opts?: FieldOpts): unknown {
122
- return this._field("node", key, opts)
123
- }
124
-
125
- /** Project-asset path (the editor renders a dropdown of the project's GLBs). "" = none picked. */
126
- asset(key: string, opts?: FieldOpts): string {
127
- const v = this._field("asset", key, { ...opts, value: opts?.value ?? "" })
128
- return typeof v === "string" ? v : ""
129
- }
130
-
131
- // ---- everything else --------------------------------------------------------------------------
132
-
133
- /** True on the run that consumes this button's click — do the action right there. */
134
- button(label: string, opts?: { id?: string }): boolean {
135
- const id = opts?.id ?? `b:${label}`
136
- this._widgets.push({ kind: "button", id, label })
137
- if (this._event?.id === id) {
138
- this._event = null
139
- return true
140
- }
141
- return false
142
- }
143
-
144
- /** A toggle that activates/deactivates the named viewport tool (`registerEditorTool`). The
145
- * editor owns the active-tool state — the click never round-trips into the scene world. */
146
- toolButton(label: string, tool: string): void {
147
- this._widgets.push({ kind: "toolButton", id: `t:${tool}`, label, tool })
148
- }
149
-
150
- header(label: string): void { this._widgets.push({ kind: "header", id: `h:${label}`, label }) }
151
- info(text: string): void { this._widgets.push({ kind: "info", id: `i:${this._widgets.length}`, text }) }
152
- warn(text: string): void { this._widgets.push({ kind: "warn", id: `w:${this._widgets.length}`, text }) }
153
-
154
- /** Emit the inferred field widgets (all declared aspect fields, or just the named ones) — the
155
- * zero-ceremony baseline; append custom widgets around it. */
156
- auto(...keys: string[]): void {
157
- for (const f of this._fields) {
158
- if (keys.length > 0 && !keys.includes(f.key)) continue
159
- const value = this._props[f.key] !== undefined ? this._props[f.key] : f.value
160
- this._widgets.push({
161
- kind: "field", id: `f:${f.key}`, key: f.key, label: f.label, editor: f.editor,
162
- value, doc: true, auto: true, min: f.min, max: f.max, step: f.step, options: f.options,
163
- })
164
- }
165
- }
166
-
167
- // ---- internals ----------------------------------------------------------------------------
168
-
169
- private _vec(editor: FieldEditor, key: string, size: number, opts?: FieldOpts): number[] {
170
- const v = this._field(editor, key, opts)
171
- const arr = Array.isArray(v) ? v : []
172
- return Array.from({ length: size }, (_, i) => (typeof arr[i] === "number" ? arr[i] as number : 0))
173
- }
174
-
175
- private _field(
176
- editor: FieldEditor, key: string, opts?: FieldOpts, options?: readonly (string | number)[],
177
- ): unknown {
178
- const doc = this._docKeys.has(key)
179
- const fallback = doc ? this._fields.find((f) => f.key === key)?.value : opts?.value
180
- const stored = doc ? this._props[key] : this._state[key]
181
- const value = stored !== undefined ? stored : fallback
182
- this._widgets.push({
183
- kind: "field", id: `f:${key}`, key, label: opts?.label ?? key, editor, value, doc,
184
- min: opts?.min, max: opts?.max, step: opts?.step, options,
185
- })
186
- return value
187
- }
188
-
189
- /**
190
- * @internal One immediate-mode pass: deliver `event`, run `fn`, return the widget list.
191
- * Editor-state field events are applied here; DOC-key field events must be applied by the
192
- * caller beforehand (they go through the scene loader's prop machinery — rebuilds, ref
193
- * resolution); button events are consumed by the matching `button()` call during the run.
194
- * A throwing inspector logs and shows the error as a warn line — it can't take the editor down.
195
- */
196
- _run(fn: (ui: this) => void, event?: InspectorEvent): InspectorWidget[] {
197
- if (event && event.id.startsWith("f:") && !this._docKeys.has(event.id.slice(2))) {
198
- this._state[event.id.slice(2)] = event.value
199
- event = undefined
200
- }
201
- this._event = event ?? null
202
- this._widgets = []
203
- try {
204
- fn(this)
205
- } catch (e) {
206
- console.error("[scene] inspector render failed:", e)
207
- this.warn(String((e as Error)?.message ?? e))
208
- }
209
- this._event = null
210
- return this._widgets
211
- }
212
- }
@@ -1,308 +0,0 @@
1
- // A hierarchical state machine over a plain context object — the one place a game keeps "what am I
2
- // doing right now": an enemy brain, a weapon (ready → fire → cooldown → reload), a game mode
3
- // (warmup → round → over), a door. Declarative: the whole machine is ONE table you can read top
4
- // to bottom, log, and later show in an inspector; the machine knows nothing about aspects, nodes
5
- // or the network, so it runs wherever its owner ticks it (an `updateFixed` for anything that must
6
- // match another machine's, `update` for presentation).
7
- //
8
- // const Guard = defineStates<Bot>({
9
- // initial: 'patrol',
10
- // on: { hit: 'flinch' }, // from ANY state
11
- // to: { dead: (b) => b.hp <= 0 }, // checked in every state, first
12
- // states: {
13
- // patrol: { update: (b, dt) => b.walk(dt), to: { combat: (b) => b.target !== null } },
14
- // combat: {
15
- // initial: 'engage',
16
- // to: { search: (b) => b.lostFor > 4 }, // leaves whichever child is active
17
- // states: {
18
- // engage: { update: (b) => b.approach(), to: { shoot: (b) => b.visible && b.dist < 12 } },
19
- // shoot: { enter: (b) => b.fire = true, exit: (b) => b.fire = false, to: { engage: (b) => !b.visible } },
20
- // },
21
- // },
22
- // search: { after: [6, 'patrol'] }, // a timed transition
23
- // flinch: { after: [0.2, '$back'] }, // '$back' = the state we came from
24
- // dead: { enter: (b) => b.die() },
25
- // },
26
- // })
27
- // const fsm = new StateMachine(Guard, bot)
28
- // fsm.step(dt) // once per tick: guards (outer states first), then updates
29
- // fsm.send('hit') // an event: the innermost state that handles it wins
30
- // fsm.state // 'combat.shoot' fsm.is('combat') → true
31
- // fsm.on('change', (to, from) => console.log(`[fsm] ${from} → ${to}`))
32
- //
33
- // Semantics, in order, per `step(dt)`:
34
- // 1. every active state's clock advances (`t` = seconds in that state);
35
- // 2. transitions are tested OUTERMOST first — the root's `to` (any-state guards), then each
36
- // active state's `to` guards (first true wins) and its `after` timer; a taken transition is
37
- // re-tested from the new state (a pass-through state chains), at most 8 hops per step;
38
- // 3. `update` runs outermost → innermost; an update that RETURNS a state name transitions
39
- // there and ends the pass (the states it entered update next step).
40
- // A transition exits states innermost-first up to the common ancestor and enters the new branch
41
- // outermost-first, descending into `initial` children; the target state's own name (or an
42
- // ancestor's) re-enters it. Names resolve relative to the declaring state (siblings, then each
43
- // ancestor's siblings), or as a dotted path from the root; unknown names throw when the machine is
44
- // built, so a typo is a startup error, not a silent no-op. `send` looks innermost → outermost
45
- // (the root's `on` = any state) and returns whether someone handled it.
46
-
47
- export type StateGuard<C> = (ctx: C, t: number) => boolean
48
- export type StateHandler<C> = (ctx: C, ...args: any[]) => string | void
49
-
50
- export interface StateDef<C> {
51
- /** The child entered by default when this (compound) state is entered. */
52
- initial?: string
53
- /** Child states — this becomes a compound state; only a leaf is ever "the" state. */
54
- states?: Record<string, StateDef<C>>
55
- /** Entered (from = the previous leaf's path, '' at start). */
56
- enter?: (ctx: C, from: string) => void
57
- /** Left (to = the next leaf's path). */
58
- exit?: (ctx: C, to: string) => void
59
- /** Every step while active, outer states first; `t` = seconds in this state. Return a state name
60
- * to transition. */
61
- update?: (ctx: C, dt: number, t: number) => string | void
62
- /** Guarded transitions, tested every step before `update` — key = target, first true wins. */
63
- to?: Record<string, StateGuard<C>>
64
- /** Leave for `target` after `seconds` in this state. */
65
- after?: [seconds: number, target: string]
66
- /** Event handlers for `send(name, …args)`: a target name, or a function returning one (or nothing). */
67
- on?: Record<string, string | StateHandler<C>>
68
- }
69
-
70
- export interface StatesDef<C> {
71
- initial: string
72
- states: Record<string, StateDef<C>>
73
- /** Any-state guards — tested first, in every state. */
74
- to?: Record<string, StateGuard<C>>
75
- /** Any-state event handlers — the fallback when no active state handles the event. */
76
- on?: Record<string, string | StateHandler<C>>
77
- /** Any-state update, runs before the active states' own. */
78
- update?: (ctx: C, dt: number, t: number) => string | void
79
- }
80
-
81
- export type StateMachineEvents = {
82
- /** After a transition completed (both are leaf paths; `from` = '' for the initial entry). */
83
- change: (to: string, from: string) => void
84
- }
85
-
86
- /** Identity helper — names the context type once so every callback is typed. */
87
- export const defineStates = <C>(def: StatesDef<C>): StatesDef<C> => def
88
-
89
- const BACK = "$back"
90
- const MAX_HOPS = 8
91
-
92
- interface SNode<C> {
93
- name: string
94
- path: string
95
- def: StateDef<C>
96
- parent: SNode<C> | null
97
- children: Map<string, SNode<C>>
98
- initial: SNode<C> | null
99
- depth: number
100
- /** seconds in this state (valid while active) */
101
- t: number
102
- /** resolved `to` targets in declaration order */
103
- guards: Array<[SNode<C>, StateGuard<C>]>
104
- after: [number, SNode<C> | typeof BACK] | null
105
- /** resolution cache for names returned at runtime */
106
- resolved: Map<string, SNode<C> | typeof BACK>
107
- }
108
-
109
- export class StateMachine<C> {
110
- readonly ctx: C
111
- private readonly root: SNode<C>
112
- private readonly byPath = new Map<string, SNode<C>>()
113
- /** the active branch, root's child (index 0) → leaf */
114
- private chain: SNode<C>[] = []
115
- private prev: SNode<C> | null = null
116
- private listeners: Array<(to: string, from: string) => void> = []
117
- private changed = 0
118
-
119
- constructor(def: StatesDef<C>, ctx: C) {
120
- this.ctx = ctx
121
- this.root = this.build("", "", def as StateDef<C>, null, 0)
122
- // second pass: resolve names (every state exists now)
123
- for (const n of this.byPath.values()) this.link(n)
124
- this.link(this.root)
125
- if (!this.root.initial) throw new Error("StateMachine: `initial` is required")
126
- this.enterBranch([], this.leafOf(this.root.initial), "")
127
- }
128
-
129
- /** The active leaf's path ('combat.shoot'). */
130
- get state(): string { return this.chain[this.chain.length - 1]?.path ?? "" }
131
- /** The leaf we came from ('' before the first transition). */
132
- get previous(): string { return this.prev?.path ?? "" }
133
- /** Seconds in the active leaf. */
134
- get time(): number { return this.chain[this.chain.length - 1]?.t ?? 0 }
135
- /** Every active state's path, outermost first. */
136
- get branch(): string[] { return this.chain.map((n) => n.path) }
137
-
138
- /** Is `name` (a state name or dotted path) active — as the leaf OR as one of its ancestors? */
139
- is(name: string): boolean {
140
- for (const n of this.chain) if (n.name === name || n.path === name) return true
141
- return false
142
- }
143
- /** Seconds in the named active state (0 when it is not active). */
144
- timeIn(name: string): number {
145
- for (const n of this.chain) if (n.name === name || n.path === name) return n.t
146
- return 0
147
- }
148
-
149
- /** Force a transition (a spawn, a reset from outside the table). */
150
- go(name: string): void {
151
- const leaf = this.chain[this.chain.length - 1] ?? this.root
152
- this.transition(this.resolve(leaf, name))
153
- }
154
-
155
- /** Deliver an event. Innermost active state first, the root's `on` last. True if handled. */
156
- send(event: string, ...args: unknown[]): boolean {
157
- for (let i = this.chain.length - 1; i >= 0; i--) {
158
- const n = this.chain[i]!
159
- const h = n.def.on?.[event]
160
- if (h === undefined) continue
161
- const target = typeof h === "string" ? h : h(this.ctx, ...args)
162
- if (typeof target === "string") this.transition(this.resolve(n, target))
163
- return true
164
- }
165
- const h = this.root.def.on?.[event]
166
- if (h === undefined) return false
167
- const target = typeof h === "string" ? h : h(this.ctx, ...args)
168
- if (typeof target === "string") this.transition(this.resolve(this.chain[this.chain.length - 1] ?? this.root, target))
169
- return true
170
- }
171
-
172
- /** One tick: clocks, guards (outer first, chained), then updates (outer first). */
173
- step(dt: number): void {
174
- for (const n of this.chain) n.t += dt
175
- this.root.t += dt
176
- for (let hop = 0; hop < MAX_HOPS; hop++) {
177
- const target = this.findTransition()
178
- if (target === null) break
179
- this.transition(target)
180
- }
181
- const mark = this.changed
182
- const r0 = this.root.def.update?.(this.ctx, dt, this.root.t)
183
- if (typeof r0 === "string") { this.transition(this.resolve(this.chain[this.chain.length - 1]!, r0)); return }
184
- if (this.changed !== mark) return
185
- const chain = this.chain.slice()
186
- for (const n of chain) {
187
- const r = n.def.update?.(this.ctx, dt, n.t)
188
- if (typeof r === "string") { this.transition(this.resolve(n, r)); return }
189
- if (this.changed !== mark) return // an update transitioned through send()/go()
190
- }
191
- }
192
-
193
- /** Listen for transitions. */
194
- on(event: "change", fn: StateMachineEvents["change"]): this { this.listeners.push(fn); return this }
195
- off(event: "change", fn: StateMachineEvents["change"]): this {
196
- const i = this.listeners.indexOf(fn)
197
- if (i >= 0) this.listeners.splice(i, 1)
198
- return this
199
- }
200
-
201
- // ---- building ----
202
- private build(name: string, path: string, def: StateDef<C>, parent: SNode<C> | null, depth: number): SNode<C> {
203
- const node: SNode<C> = { name, path, def, parent, children: new Map(), initial: null, depth, t: 0, guards: [], after: null, resolved: new Map() }
204
- if (path) this.byPath.set(path, node)
205
- if (def.states) {
206
- for (const [childName, childDef] of Object.entries(def.states)) {
207
- if (childName.includes(".") || childName.startsWith("$")) throw new Error(`StateMachine: bad state name '${childName}'`)
208
- node.children.set(childName, this.build(childName, path ? `${path}.${childName}` : childName, childDef, node, depth + 1))
209
- }
210
- const initial = def.initial ?? Object.keys(def.states)[0]!
211
- const child = node.children.get(initial)
212
- if (!child) throw new Error(`StateMachine: '${path || "root"}' has no child '${initial}' for initial`)
213
- node.initial = child
214
- } else if (def.initial) {
215
- throw new Error(`StateMachine: '${path}' names an initial child but has no states`)
216
- }
217
- return node
218
- }
219
-
220
- private link(n: SNode<C>): void {
221
- if (n.def.to) for (const [target, guard] of Object.entries(n.def.to)) n.guards.push([this.resolveStrict(n, target), guard])
222
- if (n.def.after) n.after = [n.def.after[0], this.resolve(n, n.def.after[1])]
223
- if (n.def.on) for (const h of Object.values(n.def.on)) if (typeof h === "string") this.resolve(n, h)
224
- }
225
-
226
- private resolveStrict(from: SNode<C>, name: string): SNode<C> {
227
- const r = this.resolve(from, name)
228
- if (r === BACK) throw new Error(`StateMachine: '${BACK}' is not allowed as a guard target of '${from.path}'`)
229
- return r
230
- }
231
-
232
- /** Relative first (siblings of `from`, then of each ancestor), then a dotted path from the root. */
233
- private resolve(from: SNode<C>, name: string): SNode<C> | typeof BACK {
234
- const cached = from.resolved.get(name)
235
- if (cached) return cached
236
- let found: SNode<C> | typeof BACK | undefined
237
- if (name === BACK) found = BACK
238
- else {
239
- for (let n: SNode<C> | null = from; n && !found; n = n.parent) {
240
- const scope = n.parent ?? this.root
241
- found = scope.children.get(name)
242
- if (!found && n === from) found = n.children.get(name) // a compound state naming its own child
243
- }
244
- if (!found) found = this.byPath.get(name)
245
- }
246
- if (!found) throw new Error(`StateMachine: unknown state '${name}' (from '${from.path || "root"}')`)
247
- from.resolved.set(name, found)
248
- return found
249
- }
250
-
251
- // ---- running ----
252
- /** Guards never re-enter a state that is already active (a `dead` guard staying true does not
253
- * re-enter `dead` every step) — only an explicit `go()` / event does that. */
254
- private findTransition(): SNode<C> | typeof BACK | null {
255
- const ctx = this.ctx
256
- for (const [target, guard] of this.root.guards) if (!this.active(target) && guard(ctx, this.root.t)) return target
257
- for (const n of this.chain) {
258
- for (const [target, guard] of n.guards) if (!this.active(target) && guard(ctx, n.t)) return target
259
- if (n.after && n.t >= n.after[0] && (n.after[1] === BACK || !this.active(n.after[1]))) return n.after[1]
260
- }
261
- return null
262
- }
263
-
264
- private active(n: SNode<C>): boolean { return this.chain[n.depth - 1] === n }
265
-
266
- private leafOf(n: SNode<C>): SNode<C> {
267
- while (n.initial) n = n.initial
268
- return n
269
- }
270
-
271
- private transition(target: SNode<C> | typeof BACK): void {
272
- const current = this.chain[this.chain.length - 1] ?? null
273
- let node: SNode<C>
274
- if (target === BACK) {
275
- if (!this.prev) return
276
- node = this.prev
277
- } else node = target
278
- const leaf = this.leafOf(node)
279
- // the new branch (root excluded)
280
- const next: SNode<C>[] = []
281
- for (let n: SNode<C> | null = leaf; n && n !== this.root; n = n.parent) next.unshift(n)
282
- // common prefix; a target that is the current leaf or one of its ancestors re-enters itself
283
- let common = 0
284
- while (common < this.chain.length && common < next.length && this.chain[common] === next[common]) common++
285
- if (node !== this.root && common >= node.depth && this.chain[node.depth - 1] === node) common = node.depth - 1
286
- const from = current?.path ?? ""
287
- const to = leaf.path
288
- for (let i = this.chain.length - 1; i >= common; i--) this.chain[i]!.def.exit?.(this.ctx, to)
289
- this.prev = current
290
- this.enterBranch(this.chain.slice(0, common), leaf, from, next)
291
- }
292
-
293
- private enterBranch(kept: SNode<C>[], leaf: SNode<C>, from: string, next?: SNode<C>[]): void {
294
- if (!next) {
295
- next = []
296
- for (let n: SNode<C> | null = leaf; n && n !== this.root; n = n.parent) next.unshift(n)
297
- }
298
- this.chain = kept.slice()
299
- this.changed++
300
- for (let i = kept.length; i < next.length; i++) {
301
- const n = next[i]!
302
- n.t = 0
303
- this.chain.push(n)
304
- n.def.enter?.(this.ctx, from)
305
- }
306
- for (const fn of this.listeners.slice()) fn(leaf.path, from)
307
- }
308
- }
@@ -1,66 +0,0 @@
1
- // Shared color parsing for both engines. Users pass colors the natural ways — a CSS-ish hex string
2
- // ('#10131a', '#abc', '#rrggbbaa'), a packed number (0xff0000), or normalized components
3
- // ([r,g,b] / [r,g,b,a] in 0..1). The engines want different forms (2D wants 0..1 floats, 3D wants a
4
- // packed int), so this module is the single place that converts.
5
-
6
- export type ColorInput =
7
- | string
8
- | number
9
- | readonly [number, number, number]
10
- | readonly [number, number, number, number]
11
-
12
- const parseHexString = (input: string): [number, number, number, number] => {
13
- let s = input.trim()
14
- if (s[0] === "#") s = s.slice(1)
15
- // #rgb / #rgba shorthand → double each nibble
16
- if (s.length === 3 || s.length === 4) s = s.split("").map((c) => c + c).join("")
17
- const int = parseInt(s, 16)
18
- if (s.length === 8) return [ (int >>> 24) & 255, (int >> 16) & 255, (int >> 8) & 255, int & 255 ]
19
- // default 6-digit (rrggbb); anything malformed falls back to opaque black
20
- if (s.length === 6) return [ (int >> 16) & 255, (int >> 8) & 255, int & 255, 255 ]
21
- return [ 0, 0, 0, 255 ]
22
- }
23
-
24
- /** Normalize any ColorInput to four 0..1 float components (array inputs pass through losslessly). */
25
- const toFloats = (c: ColorInput): [number, number, number, number] => {
26
- if (typeof c === "number") return [ ((c >> 16) & 255) / 255, ((c >> 8) & 255) / 255, (c & 255) / 255, 1 ]
27
- if (typeof c === "string") {
28
- const b = parseHexString(c)
29
- return [ b[0] / 255, b[1] / 255, b[2] / 255, b[3] / 255 ]
30
- }
31
- return [ c[0], c[1], c[2], (c as readonly number[])[3] ?? 1 ]
32
- }
33
-
34
- const toBytes = (c: ColorInput): [number, number, number, number] => {
35
- const f = toFloats(c)
36
- return [ Math.round(f[0] * 255), Math.round(f[1] * 255), Math.round(f[2] * 255), Math.round(f[3] * 255) ]
37
- }
38
-
39
- export const Color = {
40
- /** [r,g,b] in 0..1 — for the 2D engine's float color setters. */
41
- toRgb01(c: ColorInput): [number, number, number] {
42
- const f = toFloats(c)
43
- return [ f[0], f[1], f[2] ]
44
- },
45
- /** [r,g,b,a] in 0..1. */
46
- toRgba01(c: ColorInput): [number, number, number, number] {
47
- return toFloats(c)
48
- },
49
- /** 0xRRGGBB packed int — for the 3D engine's color uniforms / skybox. */
50
- toPackedRgb(c: ColorInput): number {
51
- const b = toBytes(c)
52
- return (b[0] << 16) | (b[1] << 8) | b[2]
53
- },
54
- /** 0xRRGGBBAA packed int (unsigned). */
55
- toPackedRgba(c: ColorInput): number {
56
- const b = toBytes(c)
57
- return ((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]) >>> 0
58
- },
59
- /** '#rrggbb' (or '#rrggbbaa' when alpha < 1) — for the 3D material color uniforms. */
60
- toHexString(c: ColorInput): string {
61
- const b = toBytes(c)
62
- const hex = (n: number) => n.toString(16).padStart(2, "0")
63
- const base = `#${hex(b[0])}${hex(b[1])}${hex(b[2])}`
64
- return b[3] >= 255 ? base : base + hex(b[3])
65
- },
66
- }
@@ -1,42 +0,0 @@
1
- // Component writes on SDK-owned vectors — the runtime half of chisel's `comp_write` pass.
2
- //
3
- // hero.controller.velocity.y = 7 → __compWrite(hero.controller, "velocity", "y", 7)
4
- // hero.controller.velocity.y += 3 → __compOp(hero.controller, "velocity", "y", 0, 3)
5
- //
6
- // A getter like `velocity` hands out a fresh Vec3, so the literal spelling would write to a copy and
7
- // silently do nothing. chisel rewrites the exact `<owner>.<prop>.<x|y|z|w> = v` shape into these
8
- // helpers, which ask the OWNER to apply the component write (no getter call, no allocation) and
9
- // fall back to the plain write on anything else, so a user record `{ velocity: { x, y } }` behaves
10
- // exactly as written. Only the direct spelling is covered: `const v = c.velocity; v.y = 7` is still
11
- // a write to a copy.
12
- //
13
- // Owners today: Node / Node2D / Camera2D (`position` → the scalar setters, physics routing included),
14
- // Physics (`velocity`, `angularVelocity`), CharacterController / CharacterController2D (`velocity`).
15
- //
16
- // An owner declares WHICH getters qualify as compile-time data and answers them at runtime:
17
- //
18
- // static _comps = ["velocity"] // chisel reads the list, then strips it (0 bytes shipped)
19
- // _writeComp(prop, axis, value) { … } // called for listed props only; a hook without the
20
- // // list is a build diagnostic and routes nothing
21
-
22
- export type CompAxis = "x" | "y" | "z" | "w"
23
-
24
- /** Implemented by a class whose vector getters accept component writes (see above). The class also
25
- * carries `static _comps: string[]` naming those getters — compile-time data, absent at runtime. */
26
- export interface CompWriter {
27
- _writeComp(prop: string, axis: CompAxis, value: number): void
28
- }
29
-
30
- /** `<o>.<prop>.<axis> = value`, routed to the owner when it has the hook. Returns the value. */
31
- export const __compWrite = (o: any, prop: string, axis: CompAxis, value: number): number => {
32
- if (o._writeComp !== undefined) o._writeComp(prop, axis, value) // a direct call, not .call — cheaper on QuickJS
33
- else o[prop][axis] = value
34
- return value
35
- }
36
-
37
- /** `<o>.<prop>.<axis> op= v` — `op`: 0 `+=`, 1 `-=`, 2 `*=`, 3 `/=`. The current component is read
38
- * through the normal getter (one fresh vector), the result written like `__compWrite`. */
39
- export const __compOp = (o: any, prop: string, axis: CompAxis, op: number, v: number): number => {
40
- const cur = o[prop][axis] as number
41
- return __compWrite(o, prop, axis, op === 0 ? cur + v : op === 1 ? cur - v : op === 2 ? cur * v : cur / v)
42
- }
@@ -1,38 +0,0 @@
1
- // Tiny typed event dispatcher shared by media players, sockets, and (later) nodes. Keeps the
2
- // addEventListener / removeEventListener / dispatch boilerplate in one place.
3
-
4
- export type EventMap = Record<string, (...args: any[]) => void>
5
-
6
- export class Emitter<E extends EventMap> {
7
- private _listeners: { [K in keyof E]?: E[K][] } = {}
8
-
9
- addEventListener<K extends keyof E>(channel: K, callback: E[K]): void {
10
- const list = this._listeners[channel]
11
- if (list) list.push(callback)
12
- else this._listeners[channel] = [ callback ]
13
- }
14
-
15
- removeEventListener<K extends keyof E>(channel: K, callback: E[K]): void {
16
- const list = this._listeners[channel]
17
- if (!list) return
18
- const i = list.indexOf(callback)
19
- if (i >= 0) list.splice(i, 1)
20
- }
21
-
22
- /** Fire all listeners on a channel. Protected: only the owning class emits. */
23
- protected dispatch<K extends keyof E>(channel: K, ...args: Parameters<E[K]>): void {
24
- const list = this._listeners[channel]
25
- if (!list) return
26
- for (const fn of list.slice()) fn(...args)
27
- }
28
-
29
- protected hasListeners<K extends keyof E>(channel: K): boolean {
30
- const list = this._listeners[channel]
31
- return !!list && list.length > 0
32
- }
33
-
34
- /** Drop every listener on every channel (used when the owning object is disposed). */
35
- protected clearListeners(): void {
36
- this._listeners = {}
37
- }
38
- }