lecodes-cli 0.19.0 → 0.19.2
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/dist/index.js +730 -235
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- 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
|
-
}
|